@desplega.ai/agent-swarm 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (238) hide show
  1. package/dist/{acp-adapter-sj2g0m84.js → acp-adapter-kr4abrsk.js} +10 -10
  2. package/dist/{actions-fegk44gr.js → actions-eh4g6w2r.js} +27 -27
  3. package/dist/{amp-adapter-ad8ft89x.js → amp-adapter-7h7wdt8q.js} +5 -5
  4. package/dist/{app-kfvgbvzy.js → app-7k8s9w5p.js} +9 -9
  5. package/dist/{assistant-z7zdy9ja.js → assistant-wswt2y7p.js} +28 -28
  6. package/dist/{boot-reembed-j8xa181e.js → boot-reembed-9apm9qw6.js} +7 -7
  7. package/dist/{boot-reembed-5c2z0mbe.js → boot-reembed-whzbdthj.js} +7 -7
  8. package/dist/{boot-scrub-sweep-hnkn3rgt.js → boot-scrub-sweep-ndzmvmgt.js} +8 -8
  9. package/dist/{boot-seeds-kwjq4d2z.js → boot-seeds-e9sta0yt.js} +13 -11
  10. package/dist/{claude-adapter-9vmvpvgd.js → claude-adapter-4mqd3r6r.js} +11 -11
  11. package/dist/{claude-managed-adapter-k9qae05j.js → claude-managed-adapter-9wswb52m.js} +3 -3
  12. package/dist/{claude-managed-setup-q54zx5q2.js → claude-managed-setup-tpe1effx.js} +4 -4
  13. package/dist/{claude-sdk-session-9jye6cxr.js → claude-sdk-session-q49ghmh9.js} +10340 -10197
  14. package/dist/{cli-ke896fcf.js → cli-01xvj1rq.js} +10 -10
  15. package/dist/{cli-tr4sxp68.js → cli-035vekcw.js} +2 -2
  16. package/dist/{cli-z5jjpaxw.js → cli-0pd8hzhc.js} +3 -3
  17. package/dist/{cli-n5vwg842.js → cli-0veb92a3.js} +6 -1
  18. package/dist/{cli-wayw93nt.js → cli-10jesr25.js} +3 -3
  19. package/dist/{cli-se8w6wny.js → cli-1y7yywmj.js} +1 -1
  20. package/dist/{cli-v1yb0rqb.js → cli-20krdhz5.js} +47 -19
  21. package/dist/{cli-d5f9ed5c.js → cli-29rja109.js} +39 -20
  22. package/dist/{cli-apx87jne.js → cli-2pwa0q4d.js} +15 -4
  23. package/dist/{cli-ee9gtz70.js → cli-4mengw46.js} +348 -25
  24. package/dist/{cli-03gedhng.js → cli-5twdke5x.js} +6 -6
  25. package/dist/{cli-6razj6t4.js → cli-711vwbxn.js} +2 -2
  26. package/dist/{cli-s2sp957d.js → cli-78vv7446.js} +1 -1
  27. package/dist/{cli-3fbmmawm.js → cli-7n6s5man.js} +4 -4
  28. package/dist/{cli-hnhtk6cj.js → cli-8wwxw8a3.js} +1 -1
  29. package/dist/{cli-hnmhtzvh.js → cli-9jzdj90z.js} +2 -0
  30. package/dist/{cli-skxvvpap.js → cli-cvv9k0hh.js} +28 -25
  31. package/dist/{cli-74ahz61x.js → cli-d9286m9h.js} +2 -2
  32. package/dist/{cli-4jxm2qtz.js → cli-gxzkg1de.js} +28 -16
  33. package/dist/{cli-zkpks3r3.js → cli-h1arqn31.js} +1 -1
  34. package/dist/{cli-r62a35ea.js → cli-hqr88ew6.js} +1 -1
  35. package/dist/{cli-ra0xzggj.js → cli-j67mh2vg.js} +2 -2
  36. package/dist/{cli-29jc5q7w.js → cli-ksphgh48.js} +2 -2
  37. package/dist/{cli-eejsgvm4.js → cli-mgz4nbgg.js} +4 -4
  38. package/dist/{cli-vatyhshy.js → cli-mz5as5ha.js} +4 -4
  39. package/dist/{cli-f7kr7svz.js → cli-n8ndw8j7.js} +2 -2
  40. package/dist/{cli-1e4b6bzh.js → cli-naqzr17j.js} +7 -7
  41. package/dist/{cli-q0ke1yww.js → cli-nm1p8jfr.js} +6 -6
  42. package/dist/{cli-m50t4309.js → cli-p3zbc1rw.js} +5 -5
  43. package/dist/{cli-cgtksawx.js → cli-pe05f8yk.js} +23 -3
  44. package/dist/{cli-vdach5a9.js → cli-qmzsbkvy.js} +4 -4
  45. package/dist/{cli-n3hzg89b.js → cli-qnm4e9e4.js} +57 -579
  46. package/dist/{cli-s4setq91.js → cli-qqy4q48b.js} +3 -3
  47. package/dist/{cli-k382tshb.js → cli-r35rw5de.js} +8 -7
  48. package/dist/{cli-k3dp5tpf.js → cli-r610p9bs.js} +2 -2
  49. package/dist/{cli-e6mpjhed.js → cli-rc6pf5kv.js} +1 -1
  50. package/dist/{cli-3dj8k4vy.js → cli-rdxm77k3.js} +1 -1
  51. package/dist/{cli-tqaazemc.js → cli-s69nfyeh.js} +1 -1
  52. package/dist/{cli-6jyj59nb.js → cli-sawv63e8.js} +2 -2
  53. package/dist/{cli-q50942rs.js → cli-sg19gbf0.js} +1 -1
  54. package/dist/{cli-p1x85d2t.js → cli-shk7r5cs.js} +12 -9
  55. package/dist/{cli-przpmdgr.js → cli-sksetb2q.js} +11 -1
  56. package/dist/{cli-d2qr7aj4.js → cli-t02z59f2.js} +1 -1
  57. package/dist/{cli-j0pfptdp.js → cli-v7g551te.js} +39 -43
  58. package/dist/{cli-bzszjf2p.js → cli-vgqzp4em.js} +519 -126
  59. package/dist/cli-wfnemr1t.js +86 -0
  60. package/dist/{cli-ne236afy.js → cli-wms6x9db.js} +8 -8
  61. package/dist/{cli-5hkm7whv.js → cli-x1r545x8.js} +1 -1
  62. package/dist/{cli-kt6zqg0e.js → cli-xczsgshn.js} +2 -2
  63. package/dist/{cli-ef1hg1wv.js → cli-y3167wt1.js} +430 -238
  64. package/dist/{cli-tsde1hdr.js → cli-yjmqpxt3.js} +12 -11
  65. package/dist/{cli-9nfx44eh.js → cli-z780ec7c.js} +1 -1
  66. package/dist/{cli-r2pnvj8g.js → cli-zm9t2vde.js} +2 -2
  67. package/dist/cli.js +24 -23
  68. package/dist/{codex-adapter-y81sssk0.js → codex-adapter-4z16v9n0.js} +8 -8
  69. package/dist/{codex-login-f2rcfpp9.js → codex-login-pkepy2j1.js} +2 -2
  70. package/dist/{codex-session-runner-8vx4j6a9.js → codex-session-runner-rvms9y2x.js} +8 -8
  71. package/dist/{commands-kcpvpcrr.js → commands-2a9rtyat.js} +8 -8
  72. package/dist/{controller-ywrmt6yd.js → controller-3k3madbn.js} +4 -4
  73. package/dist/{controllers-connect-kwpvj3rf.js → controllers-connect-v9yn8zb9.js} +30 -30
  74. package/dist/{cursor-adapter-cehv1qns.js → cursor-adapter-98gbhyzn.js} +4 -4
  75. package/dist/{db-ww5d7jq3.js → db-eywwsnjw.js} +9 -7
  76. package/dist/{db-k8ankj20.js → db-ncypk9np.js} +9 -9
  77. package/dist/{devin-adapter-x1022qb1.js → devin-adapter-zeq8b9gz.js} +2 -2
  78. package/dist/{dispatcher-e8gq5m68.js → dispatcher-w94kc575.js} +26 -26
  79. package/dist/{docker-ff6y1190.js → docker-b1e4cqab.js} +2 -2
  80. package/dist/{dsh-adapter-3epjm3af.js → dsh-adapter-47ztej36.js} +4 -4
  81. package/dist/{e2b-8ygwhg0f.js → e2b-bbt8ey45.js} +3 -3
  82. package/dist/{e2b-swhnpw3n.js → e2b-dp804sen.js} +3 -3
  83. package/dist/{event-bus-6axnngdj.js → event-bus-j2jdg3qb.js} +2 -2
  84. package/dist/{external-channel-h3359cwn.js → external-channel-zrkg8ar9.js} +8 -8
  85. package/dist/{grok-adapter-0d4x7gkx.js → grok-adapter-s9ncp9vy.js} +11 -11
  86. package/dist/{handlers-an1gzv21.js → handlers-kqftvjgw.js} +29 -29
  87. package/dist/{heartbeat-6dakeb61.js → heartbeat-9q96p8v9.js} +31 -31
  88. package/dist/{hook-6v6cv6d5.js → hook-56qe10dp.js} +7 -7
  89. package/dist/{hook-sa5xye4y.js → hook-yy6rw42k.js} +8 -8
  90. package/dist/{http-r7f3eky0.js → http-r2dnb8f8.js} +963 -75
  91. package/dist/{index-7ep4gzdk.js → index-3gtmkhc6.js} +26 -26
  92. package/dist/{index-v1v6n7hg.js → index-3k5hkz7r.js} +31 -31
  93. package/dist/{index-rmpw9a8q.js → index-5e101240.js} +26 -26
  94. package/dist/{index-s2wxgzr0.js → index-5wyfjc2c.js} +392 -15
  95. package/dist/{index-zk96aq1k.js → index-vxvhxxbn.js} +7 -7
  96. package/dist/{keepalive-be9vwy5h.js → keepalive-psj6j54c.js} +9 -9
  97. package/dist/{lead-0861cwrf.js → lead-hajz4688.js} +43 -43
  98. package/dist/{maintenance-mzzgc8bn.js → maintenance-ps02sb5m.js} +7 -7
  99. package/dist/{node-mn1zs7an.js → node-mwb5r697.js} +1 -1
  100. package/dist/{oauth-refresh-sweep-cpdnfwdj.js → oauth-refresh-sweep-0284h93b.js} +8 -8
  101. package/dist/{onboard-fh8qs4r3.js → onboard-rbf6bg01.js} +2 -2
  102. package/dist/{opencode-adapter-nm42wwj8.js → opencode-adapter-13dcatgq.js} +8 -8
  103. package/dist/{otel-impl-x6c1c9g4.js → otel-impl-zvqcbhp7.js} +2 -2
  104. package/dist/{pi-mono-adapter-9n2ya240.js → pi-mono-adapter-xxy47q4v.js} +7 -7
  105. package/dist/{pricing-refresh-qb0rv0ap.js → pricing-refresh-kefx9v3t.js} +9 -9
  106. package/dist/{rbac-roles-1gzdwbh1.js → rbac-roles-3m36rxm2.js} +8 -8
  107. package/dist/{rbac-roles-3mraey52.js → rbac-roles-z76xv0hq.js} +9 -9
  108. package/dist/{render-v2-agpa45zz.js → render-v2-wz73bs25.js} +9 -9
  109. package/dist/{resume-x0ydy9s9.js → resume-k4wzge8w.js} +14 -14
  110. package/dist/{setup-as9ga0d6.js → setup-2fp2pypp.js} +2 -2
  111. package/dist/{work-objects-y5g1y7ev.js → work-objects-6k0ywz3m.js} +29 -29
  112. package/dist/{worker-860c3e38.js → worker-5v3rxsdh.js} +43 -43
  113. package/dist/{x-m477fr0h.js → x-4mtdeym1.js} +2 -2
  114. package/openapi.json +777 -6
  115. package/package.json +8 -7
  116. package/src/agentmail/handlers.ts +21 -11
  117. package/src/apps/definition.ts +16 -0
  118. package/src/azure-devops/api.ts +52 -2
  119. package/src/azure-devops/boards-sync.ts +50 -20
  120. package/src/azure-devops/index.ts +1 -0
  121. package/src/azure-devops/outbound.ts +95 -0
  122. package/src/be/boot-seeds.ts +11 -0
  123. package/src/be/db/model-profiles.ts +315 -0
  124. package/src/be/db/tasks/read.ts +7 -0
  125. package/src/be/db.ts +53 -47
  126. package/src/be/migrations/219_relabel_pi_context_compactions.sql +22 -0
  127. package/src/be/migrations/220_model_profiles.sql +137 -0
  128. package/src/be/model-tier-resolution.ts +145 -11
  129. package/src/be/model-validation.ts +8 -1
  130. package/src/be/scripts/typecheck.ts +12 -0
  131. package/src/be/secret-registry.ts +6 -2
  132. package/src/be/seed-model-profiles.ts +41 -0
  133. package/src/be/seed-scripts/catalog/heartbeat-jev-gate.ts +368 -0
  134. package/src/be/seed-scripts/index.ts +9 -0
  135. package/src/be/swarm-config-guard.ts +1 -0
  136. package/src/commands/runner.ts +3 -17
  137. package/src/extensions/catalog.generated.json +17 -0
  138. package/src/extensions/contract-types.generated.ts +1 -1
  139. package/src/extensions/loader.ts +10 -5
  140. package/src/heartbeat/heartbeat.ts +2 -1
  141. package/src/http/agents.ts +49 -2
  142. package/src/http/all-routes.ts +1 -0
  143. package/src/http/apps.ts +30 -3
  144. package/src/http/config.ts +26 -2
  145. package/src/http/core.ts +4 -0
  146. package/src/http/index.ts +11 -1
  147. package/src/http/model-profiles.ts +303 -0
  148. package/src/http/pages-public.ts +11 -5
  149. package/src/http/poll.ts +16 -5
  150. package/src/http/script-runs.ts +1 -1
  151. package/src/http/scripts.ts +25 -2
  152. package/src/http/tasks.ts +18 -0
  153. package/src/http/webhooks.ts +1 -2
  154. package/src/pages/feedback-overlay.ts +65 -6
  155. package/src/providers/acp-targets.ts +1 -1
  156. package/src/providers/amp-adapter.ts +1 -1
  157. package/src/providers/dsh-adapter.ts +1 -1
  158. package/src/providers/pi-mono-extension.ts +17 -3
  159. package/src/providers/reasoning-effort.ts +10 -0
  160. package/src/rbac/legacy-policy.ts +8 -0
  161. package/src/rbac/permissions.ts +5 -0
  162. package/src/scheduler/deferred-task-waits.ts +18 -5
  163. package/src/scheduler/scheduler.ts +7 -1
  164. package/src/scripts-runtime/executors/quickjs-prelude.ts +75 -6
  165. package/src/scripts-runtime/executors/quickjs-runner.ts +85 -8
  166. package/src/scripts-runtime/executors/quickjs-web-shims.ts +1041 -0
  167. package/src/scripts-runtime/extract-args-schema.ts +9 -8
  168. package/src/scripts-runtime/quickjs-compat.ts +464 -0
  169. package/src/scripts-runtime/sdk-allowlist.ts +4 -0
  170. package/src/scripts-runtime/types/stdlib.d.ts +12 -0
  171. package/src/scripts-runtime/types/swarm-extension.d.ts +3 -0
  172. package/src/scripts-runtime/types/swarm-sdk.d.ts +12 -0
  173. package/src/server.ts +10 -1
  174. package/src/slack/blocks.ts +4 -2
  175. package/src/slack/reaction-shortcode.ts +14 -0
  176. package/src/slack/render-v2.ts +12 -7
  177. package/src/slack/responses.ts +5 -4
  178. package/src/slack/thread-buffer.ts +41 -22
  179. package/src/tasks/worker-follow-up.ts +11 -2
  180. package/src/tests/agentmail-filters.test.ts +47 -0
  181. package/src/tests/apps-spike2.test.ts +35 -0
  182. package/src/tests/azure-devops-outbound.test.ts +420 -0
  183. package/src/tests/db-task-read-contract.test.ts +2 -0
  184. package/src/tests/db-task-write-contract.test.ts +1 -0
  185. package/src/tests/deferred-task-wake.test.ts +78 -1
  186. package/src/tests/docs-index-footprint.test.js +2 -2
  187. package/src/tests/dsh-adapter.test.ts +1 -1
  188. package/src/tests/env-flag.test.ts +1 -0
  189. package/src/tests/extensions-catalog.test.ts +1 -0
  190. package/src/tests/extensions-example-comms-adherence-gate.test.ts +413 -0
  191. package/src/tests/extensions-loader.test.ts +15 -0
  192. package/src/tests/heartbeat-jev-gate-credentials.test.ts +168 -0
  193. package/src/tests/heartbeat-jev-gate.test.ts +385 -0
  194. package/src/tests/model-profile-backcompat.test.ts +327 -0
  195. package/src/tests/model-profile-providers.test.ts +302 -0
  196. package/src/tests/model-profile-runtime-secrets.test.ts +223 -0
  197. package/src/tests/model-profiles-api.test.ts +614 -0
  198. package/src/tests/pages-feedback-keys.test.ts +121 -0
  199. package/src/tests/pages-feedback.test.ts +3 -0
  200. package/src/tests/pi-context-compaction.test.ts +193 -0
  201. package/src/tests/profile-claim.test.ts +269 -0
  202. package/src/tests/quickjs-compat.test.ts +213 -0
  203. package/src/tests/rbac-engine.test.ts +21 -0
  204. package/src/tests/script-connections.test.ts +4 -0
  205. package/src/tests/script-executor-conformance.test.ts +252 -1
  206. package/src/tests/script-executor-quickjs.test.ts +96 -0
  207. package/src/tests/script-runs-http.test.ts +89 -0
  208. package/src/tests/scripts-http.test.ts +16 -0
  209. package/src/tests/secret-scrubber.test.ts +10 -0
  210. package/src/tests/seed-model-profiles.test.ts +174 -0
  211. package/src/tests/seed-scripts.test.ts +2 -2
  212. package/src/tests/skill-invocations.test.ts +4 -2
  213. package/src/tests/slack-blocks.test.ts +36 -0
  214. package/src/tests/slack-inline-output.test.ts +14 -0
  215. package/src/tests/slack-reaction-config.test.ts +20 -0
  216. package/src/tests/slack-render-v2.test.ts +37 -0
  217. package/src/tests/slack-thread-buffer.test.ts +98 -1
  218. package/src/tests/workflow-llm.test.ts +61 -0
  219. package/src/tools/model-profiles/list-model-profiles.ts +54 -0
  220. package/src/tools/request-human-input.ts +11 -3
  221. package/src/tools/script-apis.ts +5 -1
  222. package/src/tools/script-upsert.ts +4 -1
  223. package/src/tools/send-task.ts +24 -1
  224. package/src/tools/system-one-decide.ts +104 -0
  225. package/src/tools/task-action.ts +22 -1
  226. package/src/tools/tool-config.ts +6 -0
  227. package/src/types.ts +36 -1
  228. package/src/utils/secret-scrubber.ts +19 -12
  229. package/src/utils/vcs-repo-match.ts +29 -0
  230. package/src/workflows/executors/raw-llm.ts +8 -3
  231. package/src/workflows/executors/system-one-decision.ts +21 -11
  232. package/src/workflows/executors/workflow-llm.ts +22 -0
  233. package/src/workflows/index.ts +21 -12
  234. package/templates/extensions/README.md +1 -0
  235. package/templates/extensions/comms-adherence-gate/README.md +88 -0
  236. package/templates/extensions/comms-adherence-gate/hooks.ts +489 -0
  237. package/templates/extensions/comms-adherence-gate/manifest.json +8 -0
  238. package/dist/cli-5t4f29ez.js +0 -33
@@ -0,0 +1,420 @@
1
+ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from "bun:test";
2
+ import { unlink } from "node:fs/promises";
3
+ import type { IncomingMessage, ServerResponse } from "node:http";
4
+ import { resetAzureDevOpsBotIdCache, updateWorkItemFields } from "../azure-devops/api";
5
+ import { initAzureDevOps, resetAzureDevOps } from "../azure-devops/auth";
6
+ import { initAzureDevOpsOutbound, teardownAzureDevOpsOutbound } from "../azure-devops/outbound";
7
+ import {
8
+ closeDb,
9
+ createAgent,
10
+ createTaskExtended,
11
+ createWorkflow,
12
+ getDbClient,
13
+ initDb,
14
+ listWorkflowRuns,
15
+ } from "../be/db";
16
+ import {
17
+ createTrackerSyncIfAbsent,
18
+ deleteTrackerSync,
19
+ getAllTrackerSyncs,
20
+ } from "../be/db-queries/tracker";
21
+ import { emitTaskStarted } from "../be/task-lifecycle-events";
22
+ import { handleWebhooks } from "../http/webhooks";
23
+ import { initWorkflows, stopRetryPoller, stopWaitPoller } from "../workflows";
24
+ import { workflowEventBus } from "../workflows/event-bus";
25
+ import commentedFixture from "./fixtures/azure-boards/workitem.commented.json";
26
+ import updatedFixture from "./fixtures/azure-boards/workitem.updated.json";
27
+
28
+ const TEST_DB_PATH = "./test-azure-devops-outbound.sqlite";
29
+ const SECRET = "outbound-secret";
30
+ const BOT_ID = "7d40ac7f-6bdb-6baf-a2e2-dc9594e967d4";
31
+ const ORG_URL = "https://dev.azure.com/acme-org";
32
+ const ENV_KEYS = [
33
+ "AZURE_DEVOPS_WEBHOOK_SECRET",
34
+ "AZURE_DEVOPS_TOKEN",
35
+ "AZURE_DEVOPS_ORG_URL",
36
+ "AZURE_DEVOPS_BOT_ID",
37
+ "AZURE_DEVOPS_ALLOWED_ACTORS",
38
+ "AZURE_DEVOPS_WORKITEM_STATE_STARTED",
39
+ "AZURE_DEVOPS_WORKITEM_STATE_DONE",
40
+ ] as const;
41
+
42
+ type Json = Record<string, any>;
43
+ const originalFetch = globalThis.fetch;
44
+ let nextId = 0;
45
+ let patches: { url: string; contentType: string | null; body: Json[] }[] = [];
46
+ let patchStatus = 200;
47
+
48
+ function clearEnv() {
49
+ for (const key of ENV_KEYS) delete process.env[key];
50
+ resetAzureDevOps();
51
+ resetAzureDevOpsBotIdCache();
52
+ }
53
+
54
+ async function linkedTask(workItemId: number | string): Promise<string> {
55
+ const task = await createTaskExtended("linked work", { source: "azure-devops" });
56
+ await createTrackerSyncIfAbsent({
57
+ provider: "azure-devops",
58
+ entityType: "task",
59
+ providerEntityType: "WorkItem",
60
+ swarmId: task.id,
61
+ externalId: `dev.azure.com/acme-org#${workItemId}`,
62
+ lastSyncOrigin: "external",
63
+ syncDirection: "inbound",
64
+ });
65
+ return task.id;
66
+ }
67
+
68
+ /** Let the fire-and-forget handlers run. */
69
+ const settle = () => new Promise((r) => setTimeout(r, 25));
70
+
71
+ async function post(body: Json) {
72
+ const req = {
73
+ method: "POST",
74
+ headers: { authorization: `Basic ${Buffer.from(`hook:${SECRET}`).toString("base64")}` },
75
+ async *[Symbol.asyncIterator]() {
76
+ yield Buffer.from(JSON.stringify(body));
77
+ },
78
+ } as unknown as IncomingMessage;
79
+ const res = { writeHead: () => res, end: () => res } as unknown as ServerResponse;
80
+ await handleWebhooks(req, res, ["api", "azure-devops", "webhook"]);
81
+ }
82
+
83
+ beforeAll(async () => {
84
+ clearEnv();
85
+ try {
86
+ await unlink(TEST_DB_PATH);
87
+ } catch {}
88
+ initDb(TEST_DB_PATH);
89
+ await createAgent({
90
+ id: "lead-outbound-001",
91
+ name: "OutboundLead",
92
+ status: "idle",
93
+ isLead: true,
94
+ });
95
+ });
96
+
97
+ afterAll(async () => {
98
+ teardownAzureDevOpsOutbound();
99
+ clearEnv();
100
+ closeDb();
101
+ for (const suffix of ["", "-wal", "-shm"]) {
102
+ try {
103
+ await unlink(`${TEST_DB_PATH}${suffix}`);
104
+ } catch {}
105
+ }
106
+ });
107
+
108
+ beforeEach(async () => {
109
+ process.env.AZURE_DEVOPS_WEBHOOK_SECRET = SECRET;
110
+ process.env.AZURE_DEVOPS_TOKEN = "pat";
111
+ process.env.AZURE_DEVOPS_ORG_URL = ORG_URL;
112
+ initAzureDevOps();
113
+ patches = [];
114
+ patchStatus = 200;
115
+ globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
116
+ if (String(input).endsWith("/_apis/connectionData")) {
117
+ return Response.json({
118
+ authenticatedUser: {
119
+ id: BOT_ID,
120
+ properties: { Account: { $value: "swarm-bot@example.com" } },
121
+ },
122
+ });
123
+ }
124
+ if (init?.method === "PATCH") {
125
+ patches.push({
126
+ url: String(input),
127
+ contentType: new Headers(init.headers).get("content-type"),
128
+ body: JSON.parse(String(init.body)),
129
+ });
130
+ return new Response("{}", { status: patchStatus });
131
+ }
132
+ throw new Error(`unexpected fetch ${String(input)}`);
133
+ }) as typeof fetch;
134
+ for (const sync of await getAllTrackerSyncs("azure-devops")) await deleteTrackerSync(sync.id);
135
+ initAzureDevOpsOutbound();
136
+ });
137
+
138
+ afterEach(() => {
139
+ teardownAzureDevOpsOutbound();
140
+ globalThis.fetch = originalFetch;
141
+ clearEnv();
142
+ });
143
+
144
+ describe("work item state transitions", () => {
145
+ test("sets the started state on the linked work item", async () => {
146
+ process.env.AZURE_DEVOPS_WORKITEM_STATE_STARTED = "Doing";
147
+ const taskId = await linkedTask(9);
148
+ emitTaskStarted({ id: taskId } as never);
149
+ await settle();
150
+ expect(patches).toHaveLength(1);
151
+ expect(patches[0]?.url).toBe(`${ORG_URL}/_apis/wit/workitems/9?api-version=7.1`);
152
+ expect(patches[0]?.contentType).toBe("application/json-patch+json");
153
+ expect(patches[0]?.body).toEqual([{ op: "add", path: "/fields/System.State", value: "Doing" }]);
154
+ });
155
+
156
+ test("sets the done state when the task completes", async () => {
157
+ process.env.AZURE_DEVOPS_WORKITEM_STATE_DONE = " Done ";
158
+ const taskId = await linkedTask(9);
159
+ workflowEventBus.emit("task.completed", { taskId, output: "ok" });
160
+ await settle();
161
+ expect(patches[0]?.body[0]?.value).toBe("Done");
162
+ });
163
+
164
+ test("does nothing when the state vars are unset", async () => {
165
+ const taskId = await linkedTask(9);
166
+ emitTaskStarted({ id: taskId } as never);
167
+ workflowEventBus.emit("task.completed", { taskId });
168
+ await settle();
169
+ expect(patches).toHaveLength(0);
170
+ });
171
+
172
+ test("ignores tasks without a work item link", async () => {
173
+ process.env.AZURE_DEVOPS_WORKITEM_STATE_DONE = "Done";
174
+ const task = await createTaskExtended("plain", {});
175
+ workflowEventBus.emit("task.completed", { taskId: task.id });
176
+ await settle();
177
+ expect(patches).toHaveLength(0);
178
+ });
179
+
180
+ test("a rejected update does not throw", async () => {
181
+ process.env.AZURE_DEVOPS_WORKITEM_STATE_DONE = "Nope";
182
+ patchStatus = 400;
183
+ const taskId = await linkedTask(9);
184
+ workflowEventBus.emit("task.completed", { taskId });
185
+ await settle();
186
+ expect(patches).toHaveLength(1);
187
+ });
188
+
189
+ test("stops after teardown", async () => {
190
+ process.env.AZURE_DEVOPS_WORKITEM_STATE_DONE = "Done";
191
+ const taskId = await linkedTask(9);
192
+ teardownAzureDevOpsOutbound();
193
+ workflowEventBus.emit("task.completed", { taskId });
194
+ await settle();
195
+ expect(patches).toHaveLength(0);
196
+ });
197
+ });
198
+
199
+ describe("work item workflow events", () => {
200
+ const unsubscribers: (() => void)[] = [];
201
+ afterEach(() => {
202
+ for (const off of unsubscribers.splice(0)) off();
203
+ });
204
+ function capture(name: string): Json[] {
205
+ const seen: Json[] = [];
206
+ const handler = (d: Json) => seen.push(d);
207
+ workflowEventBus.on(name, handler);
208
+ unsubscribers.push(() => workflowEventBus.off(name, handler));
209
+ return seen;
210
+ }
211
+ function fresh(sample: unknown): Json {
212
+ const event = structuredClone(sample) as Json;
213
+ event.id = `00000000-0000-4000-8000-${String(900 + ++nextId).padStart(12, "0")}`;
214
+ if (event.resource?.commentVersionRef) {
215
+ event.resource.commentVersionRef.commentId = 2_000_000 + nextId;
216
+ }
217
+ return event;
218
+ }
219
+
220
+ test("emits azure-devops.work_item.assigned on an assignee change", async () => {
221
+ const seen = capture("azure-devops.work_item.assigned");
222
+ const event = fresh(updatedFixture);
223
+ event.resource.fields = {
224
+ ...event.resource.fields,
225
+ "System.AssignedTo": { oldValue: "A <a@example.com>", newValue: "B <b@example.com>" },
226
+ };
227
+ await post(event);
228
+ expect(seen).toHaveLength(1);
229
+ expect(seen[0]).toMatchObject({
230
+ work_item_id: event.resource.workItemId,
231
+ action: "assigned",
232
+ assigned_to: "B <b@example.com>",
233
+ previous_assignee: "A <a@example.com>",
234
+ });
235
+ });
236
+
237
+ test("does not emit assigned when the assignee did not change", async () => {
238
+ const seen = capture("azure-devops.work_item.assigned");
239
+ const event = fresh(updatedFixture);
240
+ delete event.resource.fields["System.AssignedTo"];
241
+ await post(event);
242
+ expect(seen).toHaveLength(0);
243
+ });
244
+
245
+ test("emits azure-devops.work_item.commented for a new comment only", async () => {
246
+ const seen = capture("azure-devops.work_item.commented");
247
+ const first = fresh(commentedFixture);
248
+ first.resource.commentVersionRef.version = 1;
249
+ await post(first);
250
+ const edit = fresh(commentedFixture);
251
+ edit.resource.commentVersionRef.version = 2;
252
+ await post(edit);
253
+ expect(seen).toHaveLength(1);
254
+ expect(seen[0]).toMatchObject({
255
+ work_item_id: first.resource.id,
256
+ action: "commented",
257
+ comment: first.resource.fields["System.History"],
258
+ });
259
+ });
260
+ });
261
+
262
+ describe("work item state ordering and commit boundary", () => {
263
+ test("a held Doing PATCH cannot land after Done", async () => {
264
+ process.env.AZURE_DEVOPS_WORKITEM_STATE_STARTED = "Doing";
265
+ process.env.AZURE_DEVOPS_WORKITEM_STATE_DONE = "Done";
266
+ const taskId = await linkedTask(9);
267
+ const applied: string[] = [];
268
+ let releaseDoing!: () => void;
269
+ const held = new Promise<void>((resolve) => {
270
+ releaseDoing = resolve;
271
+ });
272
+ const inner = globalThis.fetch;
273
+ globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
274
+ if (init?.method !== "PATCH") return inner(input, init);
275
+ const value = JSON.parse(String(init.body))[0].value as string;
276
+ if (value === "Doing") await held;
277
+ applied.push(value);
278
+ return new Response("{}", { status: 200 });
279
+ }) as typeof fetch;
280
+
281
+ emitTaskStarted({ id: taskId } as never);
282
+ await settle();
283
+ workflowEventBus.emit("task.completed", { taskId });
284
+ await settle();
285
+ expect(applied).toEqual([]);
286
+ releaseDoing();
287
+ await settle();
288
+ expect(applied).toEqual(["Doing", "Done"]);
289
+ });
290
+
291
+ test("a rolled-back start never reaches Azure", async () => {
292
+ process.env.AZURE_DEVOPS_WORKITEM_STATE_STARTED = "Doing";
293
+ const taskId = await linkedTask(9);
294
+ await getDbClient()
295
+ .transaction(async () => {
296
+ emitTaskStarted({ id: taskId } as never);
297
+ throw new Error("rollback");
298
+ })
299
+ .catch(() => {});
300
+ await settle();
301
+ expect(patches).toHaveLength(0);
302
+ });
303
+
304
+ test("a committed start is delivered after the commit", async () => {
305
+ process.env.AZURE_DEVOPS_WORKITEM_STATE_STARTED = "Doing";
306
+ const taskId = await linkedTask(9);
307
+ await getDbClient().transaction(async () => {
308
+ emitTaskStarted({ id: taskId } as never);
309
+ });
310
+ await settle();
311
+ expect(patches).toHaveLength(1);
312
+ expect(patches[0]?.body[0]?.value).toBe("Doing");
313
+ });
314
+
315
+ test("scrubs secrets from a rejected update response", async () => {
316
+ const token = `ghp_${"a1B2c3D4e5".repeat(4)}`;
317
+ const inner = globalThis.fetch;
318
+ globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) =>
319
+ init?.method === "PATCH"
320
+ ? new Response(`bad token ${token}`, { status: 401 })
321
+ : inner(input, init)) as typeof fetch;
322
+ const logged: string[] = [];
323
+ const original = console.error;
324
+ console.error = (...args: unknown[]) => void logged.push(args.join(" "));
325
+ try {
326
+ await updateWorkItemFields(ORG_URL, 9, [
327
+ { op: "add", path: "/fields/System.State", value: "X" },
328
+ ]);
329
+ } finally {
330
+ console.error = original;
331
+ }
332
+ expect(logged.join("\n")).toContain("401");
333
+ expect(logged.join("\n")).not.toContain(token);
334
+ });
335
+ });
336
+
337
+ describe("work item workflow events: trust and delivery boundaries", () => {
338
+ const unsubscribers: (() => void)[] = [];
339
+ afterEach(() => {
340
+ for (const off of unsubscribers.splice(0)) off();
341
+ });
342
+ function capture(name: string): Json[] {
343
+ const seen: Json[] = [];
344
+ const handler = (d: Json) => seen.push(d);
345
+ workflowEventBus.on(name, handler);
346
+ unsubscribers.push(() => workflowEventBus.off(name, handler));
347
+ return seen;
348
+ }
349
+ function fresh(sample: unknown): Json {
350
+ const event = structuredClone(sample) as Json;
351
+ event.id = `00000000-0000-4000-8000-${String(5000 + ++nextId).padStart(12, "0")}`;
352
+ if (event.resource?.commentVersionRef) {
353
+ event.resource.commentVersionRef.commentId = 3_000_000 + nextId;
354
+ event.resource.commentVersionRef.version = 1;
355
+ }
356
+ return event;
357
+ }
358
+ function assigned(): Json {
359
+ const event = fresh(updatedFixture);
360
+ event.resource.fields = {
361
+ ...event.resource.fields,
362
+ "System.AssignedTo": { oldValue: "A <a@example.com>", newValue: "B <b@example.com>" },
363
+ };
364
+ return event;
365
+ }
366
+ function excludeOrg(event: Json): Json {
367
+ const swap = (value: unknown) => String(value).replace("acme-org", "excluded-org");
368
+ event.resourceContainers.account.baseUrl = swap(event.resourceContainers.account.baseUrl);
369
+ event.resourceContainers.collection.baseUrl = swap(event.resourceContainers.collection.baseUrl);
370
+ event.resource.url = swap(event.resource.url);
371
+ return event;
372
+ }
373
+
374
+ test("events from an untrusted organization are not published", async () => {
375
+ const assignedSeen = capture("azure-devops.work_item.assigned");
376
+ const commentedSeen = capture("azure-devops.work_item.commented");
377
+ await post(excludeOrg(assigned()));
378
+ await post(excludeOrg(fresh(commentedFixture)));
379
+ expect(assignedSeen).toHaveLength(0);
380
+ expect(commentedSeen).toHaveLength(0);
381
+ });
382
+
383
+ test("concurrent and sequential retries publish once", async () => {
384
+ const assignedSeen = capture("azure-devops.work_item.assigned");
385
+ const commentedSeen = capture("azure-devops.work_item.commented");
386
+ const assignment = assigned();
387
+ const comment = fresh(commentedFixture);
388
+ await Promise.all([post(assignment), post(assignment)]);
389
+ await post(assignment);
390
+ await Promise.all([post(comment), post(comment)]);
391
+ await post(comment);
392
+ // Same comment re-delivered under a new delivery id: the comment receipt covers it.
393
+ const redelivered = structuredClone(comment);
394
+ redelivered.id = `00000000-0000-4000-8000-${String(5000 + ++nextId).padStart(12, "0")}`;
395
+ await post(redelivered);
396
+ expect(assignedSeen).toHaveLength(1);
397
+ expect(commentedSeen).toHaveLength(1);
398
+ });
399
+
400
+ test("published events start enabled event-trigger workflows", async () => {
401
+ await initWorkflows();
402
+ stopRetryPoller();
403
+ stopWaitPoller();
404
+ const make = (eventName: string) =>
405
+ createWorkflow({
406
+ name: `azure-trigger-${crypto.randomUUID().slice(0, 8)}`,
407
+ definition: {
408
+ nodes: [{ id: "n1", type: "notify", config: { channel: "swarm", template: "t" } }],
409
+ },
410
+ triggers: [{ type: "event", eventName: eventName as never }],
411
+ });
412
+ const onAssigned = await make("azure-devops.work_item.assigned");
413
+ const onCommented = await make("azure-devops.work_item.commented");
414
+ await post(assigned());
415
+ await post(fresh(commentedFixture));
416
+ await settle();
417
+ expect(await listWorkflowRuns(onAssigned.id)).toHaveLength(1);
418
+ expect(await listWorkflowRuns(onCommented.id)).toHaveLength(1);
419
+ });
420
+ });
@@ -110,6 +110,7 @@ value:NOT_EXTENSION_AGENT_SQL
110
110
  value:ReservedAgentRoleError
111
111
  value:SESSION_ACTIVITY_REFRESH_MIN_INTERVAL_MS
112
112
  value:SKILL_FILE_LIMITS
113
+ value:SUPERSEDED_RESUME_REPAIR_WINDOW_MS
113
114
  value:UNATTRIBUTED_USER_ID
114
115
  value:__resetSqliteVecExtensionPathCacheForTests
115
116
  value:abandonSlackOutcomeDelivery
@@ -610,6 +611,7 @@ const selected = `type:ExistingTrackerContextWork
610
611
  type:ExistingTrackerContextWorkReason
611
612
  type:PendingChatRelay
612
613
  type:TaskFilters
614
+ value:SUPERSEDED_RESUME_REPAIR_WINDOW_MS
613
615
  value:findCompletedTaskInThread
614
616
  value:findExistingLinearTrackerContextWork
615
617
  value:findRecentCancelledTaskInThread
@@ -133,6 +133,7 @@ value:NOT_EXTENSION_AGENT_SQL
133
133
  value:ReservedAgentRoleError
134
134
  value:SESSION_ACTIVITY_REFRESH_MIN_INTERVAL_MS
135
135
  value:SKILL_FILE_LIMITS
136
+ value:SUPERSEDED_RESUME_REPAIR_WINDOW_MS
136
137
  value:UNATTRIBUTED_USER_ID
137
138
  value:__resetSqliteVecExtensionPathCacheForTests
138
139
  value:abandonSlackOutcomeDelivery
@@ -21,6 +21,10 @@ import {
21
21
  supersedeTask,
22
22
  updateScheduledTask,
23
23
  } from "../be/db";
24
+ import { installExtension, setExtensionState } from "../be/extensions/db";
25
+ import { listRegistered, registerLoaded, unregister } from "../extensions/dispatcher";
26
+ import { ensureExtensionAgent } from "../extensions/identity";
27
+ import { loadExtension } from "../extensions/loader";
24
28
  import {
25
29
  dispatchDeferredTaskWait,
26
30
  initDeferredTaskWaits,
@@ -32,6 +36,7 @@ import { dispatchScheduleTarget, executeSchedule } from "../scheduler/scheduler"
32
36
  import { createResumeFollowUp } from "../tasks/worker-follow-up";
33
37
  import { registerDeferTaskTool } from "../tools/defer-task";
34
38
  import type { AgentTask } from "../types";
39
+ import { loadBundleFixture } from "./fixtures/extensions/load";
35
40
 
36
41
  type Result = { structuredContent: { success: boolean; message: string; scheduleId?: string } };
37
42
  type Tool = {
@@ -69,7 +74,13 @@ beforeAll(async () => {
69
74
  "defer-task"
70
75
  ]!;
71
76
  });
72
- afterEach(() => stopDeferredTaskWaits());
77
+ afterEach(async () => {
78
+ stopDeferredTaskWaits();
79
+ for (const loaded of listRegistered()) {
80
+ unregister(loaded.record.id);
81
+ await loaded.dispose();
82
+ }
83
+ });
73
84
  afterAll(async () => {
74
85
  closeDb();
75
86
  templateGlobals.__testMigrationTemplate = savedTemplate;
@@ -172,6 +183,72 @@ async function multiFixture(mode?: "all" | "any", event: TaskWakeEvent = "settle
172
183
  }
173
184
 
174
185
  describe("defer-task wakeOn", () => {
186
+ async function ceilingHook(action: string) {
187
+ const bundle = await loadBundleFixture("minimal");
188
+ bundle.manifest = { ...bundle.manifest, name: `ceiling-${crypto.randomUUID()}` };
189
+ bundle.files["hooks.ts"] = `
190
+ import { block, modify } from "swarm-extension";
191
+ export default (api) => {
192
+ api.on("pre.task.create", () => ${action});
193
+ };
194
+ `;
195
+ const installed = await installExtension(bundle);
196
+ const record = await setExtensionState(installed.extension.id, {
197
+ agentId: await ensureExtensionAgent(installed.extension.name),
198
+ });
199
+ const loaded = await loadExtension({ record: record!, ...bundle });
200
+ registerLoaded(loaded);
201
+ return loaded;
202
+ }
203
+
204
+ test("ceiling honours pre.task.create blocks and leaves the wait pending", async () => {
205
+ const { schedule } = await fixture();
206
+ await ceilingHook('block("ceiling denied")');
207
+ await executeSchedule(schedule);
208
+ expect(await children(schedule.id)).toHaveLength(0);
209
+ expect(await waitState(schedule.id)).toMatchObject({ status: "pending", childTaskId: null });
210
+ expect(await getScheduledTaskById(schedule.id)).toMatchObject({
211
+ consecutiveErrors: 1,
212
+ lastErrorMessage: expect.stringContaining("ceiling denied"),
213
+ });
214
+ });
215
+
216
+ test("ceiling applies pre.task.create payload modifications", async () => {
217
+ const { schedule } = await fixture();
218
+ await ceilingHook('modify({ description: "modified ceiling wake" })');
219
+ await executeSchedule(schedule);
220
+ const found = await children(schedule.id);
221
+ expect(found).toHaveLength(1);
222
+ expect(found[0]!.task).toBe("modified ceiling wake");
223
+ expect(await waitState(schedule.id)).toMatchObject({ status: "fired", firedBy: "ceiling" });
224
+ });
225
+
226
+ test("a ceiling moved during pre-hook preparation cannot fire the stale occurrence", async () => {
227
+ const { schedule } = await fixture();
228
+ const nextRunAt = new Date(Date.now() + 120_000).toISOString();
229
+ const loaded = await ceilingHook('modify({ description: "stale preparation" })');
230
+ const handler = loaded.handlers[0]!;
231
+ const original = handler.handler;
232
+ handler.handler = async (event, ctx) => {
233
+ await updateScheduledTask(schedule.id, { nextRunAt });
234
+ return original(event, ctx);
235
+ };
236
+ await executeSchedule(schedule);
237
+ expect(await children(schedule.id)).toHaveLength(0);
238
+ expect(await waitState(schedule.id)).toMatchObject({ status: "pending" });
239
+ expect(await getScheduledTaskById(schedule.id)).toMatchObject({ enabled: true, nextRunAt });
240
+ });
241
+
242
+ test("a stale ceiling snapshot cannot fire after re-defer moves nextRunAt", async () => {
243
+ const { schedule } = await fixture();
244
+ const nextRunAt = new Date(Date.now() + 120_000).toISOString();
245
+ await updateScheduledTask(schedule.id, { nextRunAt });
246
+ await executeSchedule(schedule);
247
+ expect(await children(schedule.id)).toHaveLength(0);
248
+ expect(await waitState(schedule.id)).toMatchObject({ status: "pending" });
249
+ expect(await getScheduledTaskById(schedule.id)).toMatchObject({ enabled: true, nextRunAt });
250
+ });
251
+
175
252
  test.each([
176
253
  "completed",
177
254
  "failed",
@@ -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(425);
107
- expect(generatedKeys).toHaveLength(425);
106
+ expect(operationKeys).toHaveLength(430);
107
+ expect(generatedKeys).toHaveLength(430);
108
108
  expect(new Set(generatedKeys)).toEqual(new Set(operationKeys));
109
109
  });
110
110
 
@@ -376,7 +376,7 @@ describe("dsh harness", () => {
376
376
  await rename(join(config.cwd, "dsh"), join(config.cwd, "npx"));
377
377
  config.env = { ...config.env, DSH_BINARY: "", PATH: config.cwd };
378
378
  await expect(new DshAdapter().createSession(config)).rejects.toThrow(
379
- "dsh CLI not found. Install @deepseek-ai/dsh@0.2.1-alpha.4",
379
+ "dsh CLI not found. Install @deepseek-ai/dsh@0.2.1-alpha.5",
380
380
  );
381
381
  expect(await Bun.file(join(config.cwd, "invocation.json")).exists()).toBe(false);
382
382
  });
@@ -76,6 +76,7 @@ describe("swarm-config-guard: Configuration-page value validation", () => {
76
76
  "SLACK_REACTION_STEERED",
77
77
  "SLACK_REACTION_COMPLETED",
78
78
  "SLACK_REACTION_FAILED",
79
+ "SLACK_STATUS_ICON_COMPLETED",
79
80
  ];
80
81
  for (const key of keys) {
81
82
  expect(validateConfigValue(key, "thumbsup")).toBeNull();
@@ -37,6 +37,7 @@ describe("predefined extension catalog", () => {
37
37
  async () => {
38
38
  const catalog = getExtensionCatalog();
39
39
  expect(Object.keys(catalog).sort()).toEqual([
40
+ "comms-adherence-gate",
40
41
  "deploy-awareness",
41
42
  "github-sender-allowlist",
42
43
  "notify-on-complete",