@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,385 @@
1
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import {
4
+ closeDb,
5
+ createAgent,
6
+ createTaskExtended,
7
+ getApprovalRequestById,
8
+ initDb,
9
+ startTask,
10
+ } from "../be/db";
11
+ import gate from "../be/seed-scripts/catalog/heartbeat-jev-gate";
12
+ import { registerRequestHumanInputTool } from "../tools/request-human-input";
13
+
14
+ const LEAD = "11111111-2222-4333-8444-555555555555";
15
+ const NOW = Date.parse("2026-10-15T14:00:00.000Z");
16
+ const MMDD = "10-15";
17
+
18
+ type Fixture = {
19
+ configs: Record<string, string>;
20
+ heartbeatMd?: string;
21
+ prevStatus?: string;
22
+ prevOutput?: string | null;
23
+ failed?: Array<Record<string, unknown>>;
24
+ approvalStatus?: string;
25
+ /** Stands in for `ctx.swarm.systemOne_decide`; returns the bridge result. */
26
+ jev?: (args: Record<string, any>) => Promise<unknown>;
27
+ };
28
+
29
+ function fakeCtx(f: Fixture) {
30
+ const kv = new Map<string, unknown>();
31
+ const calls = {
32
+ prompts: [] as any[],
33
+ kvSets: 0,
34
+ decides: [] as Array<Record<string, any>>,
35
+ approvalReads: 0,
36
+ };
37
+ const table = (columns: string[], rows: unknown[][]) => ({
38
+ success: true,
39
+ status: 200,
40
+ data: { columns, rows },
41
+ });
42
+ const ctx = {
43
+ swarm: {
44
+ config_get: async (args: { includeSecrets?: boolean }) => {
45
+ if (args.includeSecrets) throw new Error("the gate must never read secret values");
46
+ return {
47
+ success: true,
48
+ status: 200,
49
+ data: {
50
+ configs: Object.entries(f.configs).map(([key, value]) => ({
51
+ key,
52
+ // Secret rows come back masked without includeSecrets.
53
+ value: key.endsWith("_API_KEY") ? "********" : value,
54
+ scope: "global",
55
+ })),
56
+ },
57
+ };
58
+ },
59
+ db_query: async ({ sql }: { sql: string }) => {
60
+ if (sql.includes("FROM approval_requests")) {
61
+ calls.approvalReads++;
62
+ return table(["status"], f.approvalStatus ? [[f.approvalStatus]] : []);
63
+ }
64
+ if (sql.includes("heartbeatMd")) return table(["heartbeatMd"], [[f.heartbeatMd ?? ""]]);
65
+ if (sql.includes("'heartbeat-checklist'")) {
66
+ return table(
67
+ ["id", "output", "status"],
68
+ [["prev-id", f.prevOutput ?? null, f.prevStatus ?? ""]],
69
+ );
70
+ }
71
+ if (sql.includes("completed24")) {
72
+ return table(
73
+ ["inProgress", "pending", "unassigned", "completed24", "failed24"],
74
+ [[3, 1, 0, 40, 2]],
75
+ );
76
+ }
77
+ if (sql.includes("status = 'failed' AND finishedAt")) {
78
+ const rows = f.failed ?? [];
79
+ return table(
80
+ ["id", "agentId", "task", "failureReason", "finishedAt"],
81
+ rows.map((r) => [r.id, r.agentId, r.task, r.failureReason, r.finishedAt]),
82
+ );
83
+ }
84
+ if (sql.includes("GROUP BY status")) {
85
+ return table(
86
+ ["status", "n"],
87
+ [
88
+ ["idle", 4],
89
+ ["busy", 2],
90
+ ["offline", 5],
91
+ ],
92
+ );
93
+ }
94
+ throw new Error(`unexpected sql: ${sql}`);
95
+ },
96
+ kv_getOrNull: async ({ key }: { key: string }) =>
97
+ kv.has(key) ? { key, value: kv.get(key) } : null,
98
+ kv_set: async ({ key, value }: { key: string; value: unknown }) => {
99
+ calls.kvSets++;
100
+ kv.set(key, value);
101
+ return { success: true };
102
+ },
103
+ request_humanInput: async (args: unknown) => {
104
+ calls.prompts.push(args);
105
+ return {
106
+ success: true,
107
+ status: 200,
108
+ data: { requestId: "req-1", url: "https://app.example/approval-requests/req-1" },
109
+ };
110
+ },
111
+ systemOne_decide: async (args: Record<string, any>) => {
112
+ calls.decides.push(args);
113
+ if (!f.jev) throw new Error("no Jev stub");
114
+ return f.jev(args);
115
+ },
116
+ },
117
+ stdlib: {
118
+ fetch: async () => {
119
+ throw new Error("the gate must not fetch Jev from the script realm");
120
+ },
121
+ },
122
+ };
123
+ return { ctx, calls, kv };
124
+ }
125
+
126
+ const jevSays = (p: number) => async () => ({
127
+ success: true,
128
+ status: 200,
129
+ data: { success: true, model: "jev-latest", answers: { act: { type: "noul", noul: p } } },
130
+ });
131
+ const jevFails = (message: string) => async () => ({
132
+ success: true,
133
+ status: 200,
134
+ data: { success: false, message },
135
+ });
136
+ const run = (ctx: unknown, args: Record<string, unknown> = {}) =>
137
+ gate({ leadAgentId: LEAD, ...args }, ctx) as Promise<any>;
138
+
139
+ let realNow: typeof Date.now;
140
+ beforeAll(() => {
141
+ realNow = Date.now;
142
+ Date.now = () => NOW;
143
+ });
144
+ afterAll(() => {
145
+ Date.now = realNow;
146
+ });
147
+
148
+ describe("heartbeat-jev-gate with a provider", () => {
149
+ test("quiet below the threshold; Jev is asked through the server, with no credential", async () => {
150
+ const { ctx, calls } = fakeCtx({ configs: { TYPESAFE_API_KEY: "x" }, jev: jevSays(0.12) });
151
+ const verdict = await run(ctx);
152
+ expect(verdict).toEqual({
153
+ quiet: true,
154
+ reason: "jev p(needs action)=0.120 < 0.5 via typesafe",
155
+ });
156
+ expect(calls.decides).toHaveLength(1);
157
+ const args = calls.decides[0]!;
158
+ expect(args.provider).toBe("typesafe");
159
+ expect(args.model).toBe("jev-latest");
160
+ expect(args.questions.act.type).toBe("noul");
161
+ expect(args.timeoutMs).toBe(25_000);
162
+ expect(args.maxRetries).toBe(0);
163
+ expect(JSON.stringify(args)).not.toMatch(/REDACTED|API_KEY|authorization/i);
164
+ expect(calls.prompts).toHaveLength(0);
165
+ expect(calls.kvSets).toBe(0);
166
+ });
167
+
168
+ test("wake at or above the threshold, with the new failures and due items in summary", async () => {
169
+ const { ctx } = fakeCtx({
170
+ configs: { TYPESAFE_API_KEY: "x" },
171
+ heartbeatMd: [
172
+ "# Standing orders",
173
+ `- Watch: release on ${MMDD} — lift after ship`,
174
+ "- Blocker: old thing (DELETE 10-12)",
175
+ "- Future item (DELETE 10-30)",
176
+ ].join("\n"),
177
+ // The previous checklist already listed aaaaaaaa, so only bbbbbbbb is new.
178
+ prevStatus:
179
+ '## Recent Failures (last 6h) [auto-generated]\n- [aaaaaaaa] "old failure" — agent: 12345678, reason: boom, at: 2026-10-15T12:00:00Z\n',
180
+ prevOutput: "All clear.\n\nNothing to do.",
181
+ failed: [
182
+ {
183
+ id: "bbbbbbbb-0000",
184
+ agentId: "87654321-aaaa",
185
+ task: "deploy the thing",
186
+ failureReason: "exit 1",
187
+ finishedAt: "2026-10-15T13:50:00Z",
188
+ },
189
+ {
190
+ id: "aaaaaaaa-0000",
191
+ agentId: "12345678-bbbb",
192
+ task: "old failure",
193
+ failureReason: "boom",
194
+ finishedAt: "2026-10-15T12:00:00Z",
195
+ },
196
+ ],
197
+ jev: jevSays(0.5),
198
+ });
199
+ const verdict = await run(ctx, { dryRun: true });
200
+ expect(verdict.quiet).toBe(false);
201
+ expect(verdict.reason).toBe("jev p(needs action)=0.500 >= 0.5 via typesafe");
202
+ expect(verdict.summary).toContain("bbbbbbbb: deploy the thing | exit 1");
203
+ expect(verdict.summary).not.toContain("aaaaaaaa");
204
+ expect(verdict.summary).toContain("(DELETE 10-12)");
205
+ expect(verdict.state).toEqual({
206
+ now: "2026-10-15 14:00 UTC",
207
+ new_failures_since_previous_heartbeat: ["bbbbbbbb: deploy the thing | exit 1"],
208
+ tracked_items_mentioning_today_or_past_delete_date: [
209
+ `- Watch: release on ${MMDD} — lift after ship`,
210
+ "- Blocker: old thing (DELETE 10-12)",
211
+ ],
212
+ previous_heartbeat_result: "All clear. Nothing to do.",
213
+ failures_last_6h_total: 2,
214
+ task_overview:
215
+ "- In Progress: 3 - Pending: 1 - Unassigned: 0 - Completed (24h): 40 - Failed (24h): 2",
216
+ agents: "- Online: 6 (4 idle, 2 busy), Offline: 5",
217
+ });
218
+ });
219
+
220
+ test("threshold and time zone come from swarm config", async () => {
221
+ const { ctx } = fakeCtx({
222
+ configs: {
223
+ TYPESAFE_API_KEY: "x",
224
+ HEARTBEAT_JEV_GATE_THRESHOLD: "0.3",
225
+ HEARTBEAT_JEV_GATE_TIMEZONE: "Europe/Madrid",
226
+ },
227
+ jev: jevSays(0.35),
228
+ });
229
+ const verdict = await run(ctx, { dryRun: true });
230
+ expect(verdict.quiet).toBe(false);
231
+ expect(verdict.threshold).toBe(0.3);
232
+ expect(verdict.state.now).toBe("2026-10-15 16:00 Europe/Madrid");
233
+ });
234
+
235
+ test("openrouter is used when it is the only key", async () => {
236
+ const { ctx, calls } = fakeCtx({ configs: { OPENROUTER_API_KEY: "x" }, jev: jevSays(0.9) });
237
+ expect((await run(ctx)).reason).toContain("via openrouter");
238
+ expect(calls.decides[0]).toMatchObject({
239
+ provider: "openrouter",
240
+ model: "~typesafe/jev-latest",
241
+ });
242
+ });
243
+
244
+ test("a refused decision throws, so the heartbeat records a fail-open error verdict", async () => {
245
+ const refused = fakeCtx({
246
+ configs: { OPENROUTER_API_KEY: "x" },
247
+ jev: jevFails("OPENROUTER_BASE_URL points at a gateway."),
248
+ });
249
+ await expect(run(refused.ctx)).rejects.toThrow(
250
+ "jev openrouter: OPENROUTER_BASE_URL points at a gateway.",
251
+ );
252
+
253
+ const bridgeDown = fakeCtx({
254
+ configs: { TYPESAFE_API_KEY: "x" },
255
+ jev: async () => ({ success: false, status: 500, data: { error: "boom" } }),
256
+ });
257
+ await expect(run(bridgeDown.ctx)).rejects.toThrow("jev typesafe: boom");
258
+
259
+ const malformed = fakeCtx({
260
+ configs: { TYPESAFE_API_KEY: "x" },
261
+ jev: async () => ({ success: true, status: 200, data: { success: true, answers: {} } }),
262
+ });
263
+ await expect(run(malformed.ctx)).rejects.toThrow("no numeric answers.act.noul");
264
+ });
265
+
266
+ test("jevTimeoutMs is the server-side time budget", async () => {
267
+ const { ctx, calls } = fakeCtx({ configs: { TYPESAFE_API_KEY: "x" }, jev: jevSays(0.1) });
268
+ await run(ctx, { jevTimeoutMs: 5_000 });
269
+ expect(calls.decides[0]!.timeoutMs).toBe(5_000);
270
+ });
271
+ });
272
+
273
+ describe("heartbeat-jev-gate without a provider", () => {
274
+ const inactive = (verdict: any) => {
275
+ expect(verdict.quiet).toBe(false);
276
+ expect(verdict.reason).toStartWith("jev-gate inactive:");
277
+ expect(verdict.summary).toBeUndefined();
278
+ };
279
+
280
+ test("first run asks exactly once; pending ticks never ask again", async () => {
281
+ const f: Fixture = { configs: {}, approvalStatus: "pending" };
282
+ const { ctx, calls, kv } = fakeCtx(f);
283
+
284
+ const first = await run(ctx);
285
+ inactive(first);
286
+ expect(first.reason).toContain("asked a human (approval request req-1)");
287
+ expect(calls.prompts).toHaveLength(1);
288
+ const prompt = calls.prompts[0];
289
+ expect(prompt.detached).toBe(true);
290
+ expect(prompt.questions[0].type).toBe("approval");
291
+ expect(prompt.questions[0].description).toContain("TYPESAFE_API_KEY");
292
+ expect(prompt.questions[0].description).toContain("OPENROUTER_API_KEY");
293
+ expect(prompt.questions[0].description).toContain("$0.00004");
294
+ expect(kv.get("setup")).toMatchObject({ status: "asked", requestId: "req-1" });
295
+
296
+ const second = await run(ctx);
297
+ inactive(second);
298
+ expect(second.reason).toContain("awaiting answer");
299
+ expect(calls.prompts).toHaveLength(1);
300
+ expect(calls.decides).toHaveLength(0);
301
+ });
302
+
303
+ test("a rejection is stored as declined; later ticks neither ask nor poll", async () => {
304
+ const f: Fixture = { configs: {}, approvalStatus: "pending" };
305
+ const { ctx, calls, kv } = fakeCtx(f);
306
+ await run(ctx);
307
+ f.approvalStatus = "rejected";
308
+ inactive(await run(ctx));
309
+ expect(kv.get("setup")).toMatchObject({ status: "declined" });
310
+ const reads = calls.approvalReads;
311
+
312
+ const later = await run(ctx);
313
+ inactive(later);
314
+ expect(later.reason).toContain("declined");
315
+ expect(calls.approvalReads).toBe(reads);
316
+ expect(calls.prompts).toHaveLength(1);
317
+ });
318
+
319
+ test("approved without a key waits; an expired request stops asking", async () => {
320
+ const approved = fakeCtx({ configs: {}, approvalStatus: "approved" });
321
+ await run(approved.ctx);
322
+ const waiting = await run(approved.ctx);
323
+ inactive(waiting);
324
+ expect(waiting.reason).toContain("approved, waiting for");
325
+ expect(approved.calls.prompts).toHaveLength(1);
326
+
327
+ const expired = fakeCtx({ configs: {}, approvalStatus: "cancelled" });
328
+ await run(expired.ctx);
329
+ inactive(await run(expired.ctx));
330
+ expect(expired.kv.get("setup")).toMatchObject({ status: "expired" });
331
+ expect(expired.calls.prompts).toHaveLength(1);
332
+ });
333
+
334
+ test("declined, then a key appears: the gate resumes with no human step", async () => {
335
+ const f: Fixture = { configs: {}, approvalStatus: "rejected", jev: jevSays(0.2) };
336
+ const { ctx, calls } = fakeCtx(f);
337
+ await run(ctx);
338
+ inactive(await run(ctx));
339
+ f.configs.TYPESAFE_API_KEY = "x";
340
+ const verdict = await run(ctx);
341
+ expect(verdict.quiet).toBe(true);
342
+ expect(verdict.reason).toContain("via typesafe");
343
+ expect(calls.prompts).toHaveLength(1);
344
+ });
345
+
346
+ test("dryRun returns the prompt and state without asking or writing KV", async () => {
347
+ const { ctx, calls } = fakeCtx({ configs: {} });
348
+ const verdict = await run(ctx, { dryRun: true });
349
+ inactive(verdict);
350
+ expect(verdict.dryRun).toBe(true);
351
+ expect(verdict.prompt.title).toContain("Jev");
352
+ expect(verdict.state.agents).toBe("- Online: 6 (4 idle, 2 busy), Offline: 5");
353
+ expect(calls.prompts).toHaveLength(0);
354
+ expect(calls.kvSets).toBe(0);
355
+ });
356
+ });
357
+
358
+ describe("request-human-input detached", () => {
359
+ beforeAll(() => initDb(":memory:"));
360
+ afterAll(() => closeDb());
361
+
362
+ test("a detached request is not attached to the agent's current task", async () => {
363
+ const server = new McpServer({ name: "jev-gate-hitl", version: "1" });
364
+ registerRequestHumanInputTool(server);
365
+ const tool = (server as any)._registeredTools["request-human-input"];
366
+ const agent = await createAgent({ name: "jev-gate-lead", isLead: true, status: "idle" });
367
+ const task = await createTaskExtended("unrelated lead work", {
368
+ agentId: agent.id,
369
+ source: "mcp",
370
+ slackChannelId: "C_UNRELATED",
371
+ slackThreadTs: "1.000",
372
+ });
373
+ await startTask(task.id);
374
+ const extra = { sessionId: "s", requestInfo: { headers: { "x-agent-id": agent.id } } };
375
+ const questions = [{ id: "q", type: "approval", label: "ok?" }];
376
+
377
+ const attached = await tool.handler({ title: "a", questions }, extra);
378
+ const detached = await tool.handler({ title: "d", questions, detached: true }, extra);
379
+
380
+ const a = await getApprovalRequestById(attached.structuredContent.requestId);
381
+ const d = await getApprovalRequestById(detached.structuredContent.requestId);
382
+ expect(a?.sourceTaskId).toBe(task.id);
383
+ expect(d?.sourceTaskId).toBeNull();
384
+ });
385
+ });