@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,489 @@
1
+ import { type ApiCtx, block, type SwarmExtension } from "swarm-extension";
2
+ import { z } from "zod";
3
+
4
+ /**
5
+ * comms-adherence-gate
6
+ *
7
+ * When an agent calls `store-progress` with status "completed" on a task that
8
+ * reaches a human in Slack, judge the `output` against the requesting user's
9
+ * stored comms preferences (users.metadata.$.comms) with Jev, and against what
10
+ * that user actually asked for in their latest message. An explicit request
11
+ * ("tell me a long joke", "explain in detail") outranks the stored default, so
12
+ * a requested long answer is not a verbosity failure.
13
+ *
14
+ * Transport (config.provider): Jev is served by TypeSafe directly and by
15
+ * OpenRouter's Decisions endpoint. Both take the same {model, state, questions}
16
+ * body and return the same `answers` shape. `auto` uses TYPESAFE_API_KEY when
17
+ * set, else OPENROUTER_API_KEY.
18
+ *
19
+ * modes (config.mode):
20
+ * off -> never evaluate
21
+ * shadow -> evaluate + record, never block (default)
22
+ * enforce -> record, and block the completion up to maxBlocksPerTask times
23
+ * per task with actionable feedback
24
+ *
25
+ * Fails OPEN on every error: a judge outage must never wall the swarm.
26
+ * Every verdict lands in KV namespace `ext:comms-gate`.
27
+ */
28
+
29
+ export const config = z.object({
30
+ mode: z.enum(["off", "shadow", "enforce"]).default("shadow"),
31
+ /** A dimension scoring below this fails. */
32
+ threshold: z.number().min(0).max(1).default(0.5),
33
+ timeoutMs: z.number().int().positive().default(4000),
34
+ maxBlocksPerTask: z.number().int().min(0).default(1),
35
+ provider: z.enum(["auto", "typesafe", "openrouter"]).default("auto"),
36
+ /** Jev model on the TypeSafe transport. */
37
+ model: z.string().default("jev-latest"),
38
+ /** Jev model on the OpenRouter transport. */
39
+ openrouterModel: z.string().default("typesafe/jev-1.13"),
40
+ /** Explicit TypeSafe key; otherwise TYPESAFE_API_KEY from the API process env. */
41
+ apiKey: z.string().optional(),
42
+ });
43
+
44
+ type Cfg = z.infer<typeof config>;
45
+ type Swarm = ApiCtx["swarm"];
46
+
47
+ const NS = "ext:comms-gate";
48
+ const TYPESAFE_ENDPOINT = "https://api.typesafe.ai/v1/systemone";
49
+ const OPENROUTER_ENDPOINT = "https://openrouter.ai/api/alpha/decisions";
50
+ const UUID_RE = /^[0-9a-fA-F-]{36}$/;
51
+ // The dispatcher cancels a handler after 5 s; leave room for the DB read and KV writes.
52
+ const MAX_TIMEOUT_MS = 4000;
53
+
54
+ export const cfgOf = (raw: unknown): Cfg => {
55
+ const parsed = config.safeParse(raw ?? {});
56
+ return parsed.success ? parsed.data : config.parse({});
57
+ };
58
+
59
+ // kv keys accept [a-zA-Z0-9._:/%-]
60
+ const safeKey = (raw: string | undefined | null): string =>
61
+ (raw && raw.length > 0 ? raw : "unknown").replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 120);
62
+
63
+ /** SDK envelopes: {success, data:{...}} or the bare payload. */
64
+ type Envelope = {
65
+ data?: { rows?: unknown[]; value?: unknown };
66
+ rows?: unknown[];
67
+ value?: unknown;
68
+ } | null;
69
+
70
+ const cell = (row: unknown, i: number, key: string): unknown =>
71
+ Array.isArray(row) ? row[i] : (row as Record<string, unknown>)?.[key];
72
+
73
+ /**
74
+ * The newest human message sits at the END of a Slack task: the
75
+ * `<thread_context>` blob comes first and can run to thousands of chars, so a
76
+ * leading slice of `agent_tasks.task` shows the judge everything except the
77
+ * actual request. Take the tail, then the part after the last `</thread_context>`.
78
+ */
79
+ const CTX_END = "</thread_context>";
80
+ export const latestAsk = (tail: string): string => {
81
+ const end = tail.lastIndexOf(CTX_END);
82
+ let out = (end >= 0 ? tail.slice(end + CTX_END.length) : tail)
83
+ .replace(/\[Thread follow-up[^\]]*\]/g, "")
84
+ .trim();
85
+ // Some tasks end with the context block; fall back to the last messages in it.
86
+ if (out.length < 8) out = (end >= 0 ? tail.slice(0, end) : tail).trim().slice(-800);
87
+ return out.slice(0, 900);
88
+ };
89
+
90
+ // Preferences are the DEFAULT. A direct request outranks them.
91
+ const JUDGING_RULES = [
92
+ "The requester's explicit instruction in latest_request OUTRANKS requester_preferences. Preferences describe the DEFAULT; they never override a direct request.",
93
+ "If latest_request asks for something long, detailed, creative, a story, a joke, a full explanation or a document, then length and structure serving that request are CORRECT and must NOT be penalized.",
94
+ "Judge whether agent_message serves what latest_request actually asked for, written in the requester's manner. Do not judge it against the default preference when the request overrides it.",
95
+ ];
96
+
97
+ type Prefs = { verbosity?: unknown; tone?: unknown; language?: unknown } & Record<string, unknown>;
98
+
99
+ /** One `noul` question per stated preference the gate knows how to judge. */
100
+ export const buildQuestions = (prefs: Prefs): Record<string, unknown> => {
101
+ const questions: Record<string, unknown> = {};
102
+ if (prefs.verbosity) {
103
+ questions.verbosity = {
104
+ type: "noul",
105
+ instructions:
106
+ "Does agent_message respect the requester's verbosity preference, AS OVERRIDDEN by any explicit instruction in latest_request? Apply judging_rules.",
107
+ criteria: {
108
+ true: "Length and structure fit what latest_request asked for (or the stated preference when the request does not override it), and it leads with the result.",
109
+ false:
110
+ "Too long for what was asked, or it opens with preamble, recaps the request, narrates the process, or ends with an offer to elaborate. Length explicitly requested in latest_request is NOT a failure.",
111
+ },
112
+ };
113
+ }
114
+ if (prefs.tone) {
115
+ questions.tone = {
116
+ type: "noul",
117
+ instructions:
118
+ "Does agent_message match the requester's stated tone preference, as overridden by any explicit instruction in latest_request? Apply judging_rules.",
119
+ criteria: {
120
+ true: "The tone matches what the requester asked for.",
121
+ false:
122
+ "The tone conflicts with it (for example hedging, flattery or filler when the requester asked for direct and plain). A voice appropriate to an explicitly requested creative or narrative form is NOT a failure.",
123
+ },
124
+ };
125
+ }
126
+ if (prefs.language) {
127
+ questions.language = {
128
+ type: "noul",
129
+ instructions: "Is agent_message written in the requester's stated language?",
130
+ criteria: {
131
+ true: "Written in the stated language.",
132
+ false: "Written in a different language.",
133
+ },
134
+ };
135
+ }
136
+ return questions;
137
+ };
138
+
139
+ const secretCache = new Map<string, { value: string; at: number }>();
140
+ const KEY_TTL_MS = 5 * 60 * 1000;
141
+ const usable = (v: string): boolean => Boolean(v) && !v.startsWith("[REDACTED");
142
+ const envOf = (name: string): string => String(process.env[name] ?? "");
143
+
144
+ /**
145
+ * Resolve a secret: explicit value, then the API process env. The API process loads
146
+ * global swarm_config rows into process.env at boot and on config reload.
147
+ * ctx.swarm.config_get cannot serve it: the SDK scrubs secrets to "[REDACTED:<name>]".
148
+ */
149
+ const secret = (name: string, explicit?: string): string => {
150
+ const cached = secretCache.get(name);
151
+ if (cached && Date.now() - cached.at < KEY_TTL_MS) return cached.value;
152
+ const value = [String(explicit ?? ""), envOf(name)].find(usable) ?? "";
153
+ if (usable(value)) secretCache.set(name, { value, at: Date.now() });
154
+ return value;
155
+ };
156
+
157
+ /** Test hook: forget cached secrets. */
158
+ export const resetKeyCache = (): void => {
159
+ secretCache.clear();
160
+ };
161
+
162
+ export type JevTransport = {
163
+ provider: "typesafe" | "openrouter";
164
+ endpoint: string;
165
+ key: string;
166
+ model: string;
167
+ };
168
+
169
+ /**
170
+ * Pick the Jev transport. `auto` prefers TypeSafe and falls back to OpenRouter.
171
+ * Throws when the chosen provider has no usable key; the handler fails open.
172
+ */
173
+ export const resolveTransport = (cfg: Cfg): JevTransport => {
174
+ const typesafe = (): JevTransport | null => {
175
+ const key = secret("TYPESAFE_API_KEY", cfg.apiKey);
176
+ return usable(key)
177
+ ? { provider: "typesafe", endpoint: TYPESAFE_ENDPOINT, key, model: cfg.model }
178
+ : null;
179
+ };
180
+ const openrouter = (): JevTransport | null => {
181
+ const key = secret("OPENROUTER_API_KEY");
182
+ return usable(key)
183
+ ? { provider: "openrouter", endpoint: OPENROUTER_ENDPOINT, key, model: cfg.openrouterModel }
184
+ : null;
185
+ };
186
+ const transport =
187
+ cfg.provider === "typesafe"
188
+ ? typesafe()
189
+ : cfg.provider === "openrouter"
190
+ ? openrouter()
191
+ : (typesafe() ?? openrouter());
192
+ if (transport) return transport;
193
+ throw new Error(
194
+ cfg.provider === "typesafe"
195
+ ? "no TYPESAFE_API_KEY"
196
+ : cfg.provider === "openrouter"
197
+ ? "no OPENROUTER_API_KEY"
198
+ : "no TYPESAFE_API_KEY or OPENROUTER_API_KEY",
199
+ );
200
+ };
201
+
202
+ /** One Jev call; returns the `noul` score per question. Throws on any failure. */
203
+ export const askJev = async (
204
+ state: Record<string, unknown>,
205
+ questions: Record<string, unknown>,
206
+ transport: JevTransport,
207
+ cfg: Cfg,
208
+ outer?: AbortSignal,
209
+ ): Promise<{ scores: Record<string, number>; latencyMs: number }> => {
210
+ const controller = new AbortController();
211
+ const timer = setTimeout(() => controller.abort(), Math.min(cfg.timeoutMs, MAX_TIMEOUT_MS));
212
+ const onOuterAbort = () => controller.abort();
213
+ outer?.addEventListener("abort", onOuterAbort);
214
+ const t0 = Date.now();
215
+ try {
216
+ const res = await fetch(transport.endpoint, {
217
+ method: "POST",
218
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${transport.key}` },
219
+ body: JSON.stringify({ state, model: transport.model, questions }),
220
+ signal: controller.signal,
221
+ });
222
+ if (!res.ok) throw new Error(`${transport.provider} ${res.status}`);
223
+ const body = (await res.json()) as { answers?: Record<string, { noul?: unknown }> } | null;
224
+ const answers = body?.answers;
225
+ if (!answers || typeof answers !== "object") throw new Error("no answers in response");
226
+ const scores: Record<string, number> = {};
227
+ for (const [id, a] of Object.entries(answers)) {
228
+ if (typeof a?.noul === "number") scores[id] = a.noul;
229
+ }
230
+ if (Object.keys(scores).length === 0) throw new Error("no scores in response");
231
+ return { scores, latencyMs: Date.now() - t0 };
232
+ } finally {
233
+ clearTimeout(timer);
234
+ outer?.removeEventListener("abort", onOuterAbort);
235
+ }
236
+ };
237
+
238
+ const SELFTEST = "comms-gate-selftest";
239
+ const SELFTEST_PREFS = {
240
+ tone: "direct, plain, no hedging",
241
+ language: "English",
242
+ verbosity:
243
+ "very terse - answer in the first sentence, ~400 chars max. No preamble, no recap, no closing offer.",
244
+ };
245
+ const SELFTEST_BAD =
246
+ "Great question! Let me walk you through what I did here. First I looked at the task, then I reviewed the relevant files, and after careful consideration I decided to go with option B. I hope this helps - let me know if you would like me to elaborate further on any of the above!";
247
+ const SELFTEST_GOOD =
248
+ "Went with option B: it is the only one that keeps the migration reversible. PR #123.";
249
+
250
+ /**
251
+ * Run the judge on a known-bad (positive control, must score below threshold) and a
252
+ * known-good (negative control, must score at or above it) message and record both
253
+ * under `selftest:last`. Proves key, egress and KV from inside the API runtime
254
+ * without touching real traffic.
255
+ */
256
+ const selfTest = async (swarm: Swarm, cfg: Cfg, signal?: AbortSignal): Promise<void> => {
257
+ const started = Date.now();
258
+ try {
259
+ const transport = resolveTransport(cfg);
260
+ const questions = { verbosity: buildQuestions(SELFTEST_PREFS).verbosity };
261
+ const judge = (message: string) =>
262
+ askJev(
263
+ { requester_preferences: SELFTEST_PREFS, agent_message: message },
264
+ questions,
265
+ transport,
266
+ cfg,
267
+ signal,
268
+ ).then((r) => r.scores.verbosity ?? null);
269
+ const [bad, good] = await Promise.all([judge(SELFTEST_BAD), judge(SELFTEST_GOOD)]);
270
+ await swarm.kv_set({
271
+ namespace: NS,
272
+ key: "selftest:last",
273
+ value: {
274
+ at: new Date().toISOString(),
275
+ provider: transport.provider,
276
+ threshold: cfg.threshold,
277
+ positiveControlBadScore: bad,
278
+ negativeControlGoodScore: good,
279
+ controlsOk:
280
+ typeof bad === "number" &&
281
+ typeof good === "number" &&
282
+ bad < cfg.threshold &&
283
+ good >= cfg.threshold,
284
+ latencyMs: Date.now() - started,
285
+ },
286
+ });
287
+ } catch (err) {
288
+ await swarm
289
+ .kv_set({
290
+ namespace: NS,
291
+ key: "selftest:last",
292
+ value: {
293
+ at: new Date().toISOString(),
294
+ error: String(err).slice(0, 400),
295
+ latencyMs: Date.now() - started,
296
+ },
297
+ })
298
+ .catch(() => {});
299
+ }
300
+ };
301
+
302
+ const TASK_SQL = `SELECT t.source, t.slackThreadTs, t.agentId, t.requestedByUserId,
303
+ json_extract(u.metadata, '$.comms') AS comms, u.name,
304
+ substr(t.task, 1, 400) AS askHead,
305
+ substr(t.task, -1400) AS askTail
306
+ FROM agent_tasks t
307
+ LEFT JOIN users u ON u.id = t.requestedByUserId
308
+ WHERE t.id = ?`;
309
+
310
+ const LEAD_SQL = "SELECT isLead FROM agents WHERE id = ?";
311
+
312
+ /**
313
+ * Mirrors the `task.progress.write` rule store-progress enforces
314
+ * (src/rbac/legacy-policy.ts): the caller owns the task, the task is
315
+ * unassigned, or the caller is a lead. Lead status comes from the caller's
316
+ * agents row, never from tool args. Extension callers return before this.
317
+ */
318
+ const mayWriteProgress = async (
319
+ swarm: Swarm,
320
+ callerId: string | undefined,
321
+ taskAgentId: string,
322
+ ): Promise<boolean> => {
323
+ if (!callerId) return false;
324
+ if (!taskAgentId || taskAgentId === callerId) return true;
325
+ const q = (await swarm.db_query({ sql: LEAD_SQL, params: [callerId] })) as Envelope;
326
+ const row = (q?.data?.rows ?? q?.rows ?? [])[0];
327
+ return Number(cell(row, 0, "isLead") ?? 0) === 1;
328
+ };
329
+
330
+ const extension: SwarmExtension = (api) => {
331
+ api.on("pre.tool.call", async (event, ctx) => {
332
+ if (event.tool !== "store-progress") return;
333
+ if (event.requestInfo?.callOrigin === "extension") return;
334
+
335
+ const cfg = cfgOf(ctx.config);
336
+ if (cfg.mode === "off") return;
337
+
338
+ const args = (event.args ?? {}) as Record<string, unknown>;
339
+
340
+ // Self-test hatch: a progress call whose text starts with the sentinel runs
341
+ // the controls and records them under `selftest:last`.
342
+ if (args.status !== "completed") {
343
+ if (typeof args.progress === "string" && args.progress.startsWith(SELFTEST)) {
344
+ await selfTest(ctx.swarm, cfg, ctx.signal);
345
+ }
346
+ return;
347
+ }
348
+
349
+ const output = typeof args.output === "string" ? args.output.trim() : "";
350
+ if (!output) return;
351
+
352
+ const taskId = String(args.taskId ?? event.requestInfo?.sourceTaskId ?? "");
353
+ if (!UUID_RE.test(taskId)) return;
354
+
355
+ try {
356
+ const q = (await ctx.swarm.db_query({ sql: TASK_SQL, params: [taskId] })) as Envelope;
357
+ const row = (q?.data?.rows ?? q?.rows ?? [])[0];
358
+ if (!row) return;
359
+
360
+ const source = String(cell(row, 0, "source") ?? "");
361
+ const slackThreadTs = cell(row, 1, "slackThreadTs");
362
+ const agentId = String(cell(row, 2, "agentId") ?? "");
363
+ const requesterId = String(cell(row, 3, "requestedByUserId") ?? "");
364
+ const commsRaw = cell(row, 4, "comms");
365
+ const requesterName = String(cell(row, 5, "name") ?? requesterId);
366
+ const threadOpening = String(cell(row, 6, "askHead") ?? "");
367
+ const ask = latestAsk(String(cell(row, 7, "askTail") ?? ""));
368
+
369
+ // Judge only a completion store-progress will accept, so no agent can ship
370
+ // another task's thread and preferences to the external judge.
371
+ if (!(await mayWriteProgress(ctx.swarm, event.requestInfo?.agentId, agentId))) return;
372
+
373
+ // Only where the text actually reaches a human in Slack.
374
+ if (source !== "slack" && !slackThreadTs) return;
375
+ if (!commsRaw) return; // no stated preferences -> nothing to judge against
376
+ const prefs = (typeof commsRaw === "string" ? JSON.parse(commsRaw) : commsRaw) as Prefs;
377
+ if (!prefs || typeof prefs !== "object") return;
378
+
379
+ const questions = buildQuestions(prefs);
380
+ if (Object.keys(questions).length === 0) return;
381
+
382
+ const transport = resolveTransport(cfg);
383
+ const { scores, latencyMs } = await askJev(
384
+ {
385
+ role: "You audit an AI agent's final message to the human who requested the work.",
386
+ requester: requesterName,
387
+ requester_preferences: prefs,
388
+ latest_request: ask,
389
+ thread_opening: threadOpening,
390
+ judging_rules: JUDGING_RULES,
391
+ agent_message: output,
392
+ },
393
+ questions,
394
+ transport,
395
+ cfg,
396
+ ctx.signal,
397
+ );
398
+
399
+ const failed = Object.entries(scores)
400
+ .filter(([, v]) => v < cfg.threshold)
401
+ .map(([k]) => k);
402
+
403
+ const verdict = {
404
+ at: new Date().toISOString(),
405
+ taskId,
406
+ agentId,
407
+ source,
408
+ requester: requesterName,
409
+ requesterId,
410
+ provider: transport.provider,
411
+ scores,
412
+ failed,
413
+ threshold: cfg.threshold,
414
+ mode: cfg.mode,
415
+ latencyMs,
416
+ preferences: prefs,
417
+ askPreview: ask.slice(0, 400),
418
+ outputPreview: output.slice(0, 700),
419
+ };
420
+
421
+ await ctx.swarm.kv_incr({ namespace: NS, key: "total" });
422
+ await ctx.swarm.kv_set({ namespace: NS, key: "last", value: verdict });
423
+
424
+ if (failed.length === 0) {
425
+ await ctx.swarm.kv_incr({ namespace: NS, key: "pass" });
426
+ return;
427
+ }
428
+
429
+ await ctx.swarm.kv_incr({ namespace: NS, key: "fail" });
430
+ for (const dim of failed) {
431
+ await ctx.swarm.kv_incr({ namespace: NS, key: `dim:${safeKey(dim)}` });
432
+ }
433
+
434
+ const blocksKey = `blocks:${safeKey(taskId)}`;
435
+ // kv_get answers {success, data:{value}}, and {success:false} when absent.
436
+ const prior = (await ctx.swarm.kv_get({ namespace: NS, key: blocksKey })) as Envelope;
437
+ const priorBlocks = Number(prior?.data?.value ?? prior?.value ?? 0) || 0;
438
+ const willBlock = cfg.mode === "enforce" && priorBlocks < cfg.maxBlocksPerTask;
439
+
440
+ await ctx.swarm.kv_set({
441
+ namespace: NS,
442
+ key: `fail:${Date.now()}:${safeKey(taskId)}`,
443
+ value: { ...verdict, action: willBlock ? "blocked" : "recorded" },
444
+ });
445
+
446
+ if (!willBlock) {
447
+ await ctx.swarm.kv_incr({
448
+ namespace: NS,
449
+ key: cfg.mode === "enforce" ? "escaped" : "shadowed",
450
+ });
451
+ return;
452
+ }
453
+
454
+ await ctx.swarm.kv_incr({ namespace: NS, key: "blocked" });
455
+ await ctx.swarm.kv_incr({ namespace: NS, key: blocksKey });
456
+
457
+ const detail = failed
458
+ .map(
459
+ (d) =>
460
+ `${d} (${scores[d]!.toFixed(2)} vs ${cfg.threshold}) — requested: ${String(prefs[d] ?? "").slice(0, 220)}`,
461
+ )
462
+ .join("\n");
463
+
464
+ return block(
465
+ [
466
+ `Comms gate: this output does not match ${requesterName}'s stated preferences.`,
467
+ detail,
468
+ "Rewrite the output to match and call store-progress again. Do not change the facts, only how they are said.",
469
+ ].join("\n"),
470
+ );
471
+ } catch (err) {
472
+ // Fail open, always.
473
+ try {
474
+ await ctx.swarm.kv_incr({ namespace: NS, key: "error" });
475
+ await ctx.swarm.kv_set({
476
+ namespace: NS,
477
+ key: "error:last",
478
+ value: { at: new Date().toISOString(), taskId, error: String(err).slice(0, 400) },
479
+ });
480
+ } catch {
481
+ /* ignore */
482
+ }
483
+ ctx.log.warn("comms-adherence-gate: failing open", { error: String(err) });
484
+ return;
485
+ }
486
+ });
487
+ };
488
+
489
+ export default extension;
@@ -0,0 +1,8 @@
1
+ {
2
+ "$schema": "../manifest.schema.json",
3
+ "name": "comms-adherence-gate",
4
+ "description": "Judges every Slack-facing store-progress completion against the requester's comms preferences and latest request with Jev (TypeSafe or OpenRouter), and in enforce blocks a non-adherent completion once per task",
5
+ "version": "1.5.0",
6
+ "runtime": "api",
7
+ "assets": { "hooks": "hooks.ts" }
8
+ }
@@ -1,33 +0,0 @@
1
- import {
2
- seedPricingFromModelsDev
3
- } from "./cli-r62a35ea.js";
4
- import {
5
- ensureRbacSeedsSynced,
6
- init_rbac_roles
7
- } from "./cli-74ahz61x.js";
8
- import {
9
- getDb,
10
- init_db1 as init_db
11
- } from "./cli-ef1hg1wv.js";
12
-
13
- // src/be/boot-seeds.ts
14
- init_db();
15
- init_rbac_roles();
16
- var pricingSeededDbs = new WeakSet;
17
- var rbacSyncedDbs = new WeakSet;
18
- function ensurePricingSeeded() {
19
- const db = getDb();
20
- if (pricingSeededDbs.has(db))
21
- return;
22
- seedPricingFromModelsDev();
23
- pricingSeededDbs.add(db);
24
- }
25
- function ensureRbacSeeded() {
26
- const db = getDb();
27
- if (rbacSyncedDbs.has(db))
28
- return;
29
- ensureRbacSeedsSynced();
30
- rbacSyncedDbs.add(db);
31
- }
32
-
33
- export { ensurePricingSeeded, ensureRbacSeeded };