@desplega.ai/agent-swarm 2.4.0 → 2.4.1

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 (222) hide show
  1. package/README.md +1 -1
  2. package/dist/{acp-adapter-kedstxx6.js → acp-adapter-zvzr538d.js} +9 -9
  3. package/dist/{actions-vget6r1k.js → actions-jhtqdw69.js} +21 -21
  4. package/dist/{amp-adapter-xeb5mktv.js → amp-adapter-bd84rhs2.js} +4 -4
  5. package/dist/{app-mqap8t8m.js → app-kreq1gwg.js} +3 -3
  6. package/dist/{assistant-h0gv97mv.js → assistant-3k22sbsq.js} +22 -22
  7. package/dist/{boot-reembed-qr8a894z.js → boot-reembed-e53x6b9v.js} +1 -1
  8. package/dist/{boot-reembed-rj85jjhw.js → boot-reembed-f6z923ny.js} +1 -1
  9. package/dist/{boot-scrub-sweep-eyfvn6v9.js → boot-scrub-sweep-0538vsb7.js} +4 -4
  10. package/dist/{boot-seeds-xs9a8dx1.js → boot-seeds-6wxqkfk2.js} +4 -4
  11. package/dist/{claude-adapter-bm23cye7.js → claude-adapter-gfts5044.js} +9 -9
  12. package/dist/{claude-managed-adapter-1mwrsm9d.js → claude-managed-adapter-h2y889ge.js} +2 -2
  13. package/dist/{claude-managed-setup-gkfgks4j.js → claude-managed-setup-h00hsgtz.js} +2 -2
  14. package/dist/{claude-sdk-session-apb4ypz3.js → claude-sdk-session-56besmfz.js} +13 -13
  15. package/dist/{cli-nrrg0fvy.js → cli-0bh477zn.js} +18 -5
  16. package/dist/{cli-7gh7020n.js → cli-1dmcq2eh.js} +1 -1
  17. package/dist/{cli-7ayprxp7.js → cli-2dfbv8qr.js} +8 -0
  18. package/dist/{cli-1cevz46c.js → cli-3kh4kpqy.js} +6 -6
  19. package/dist/{cli-tg9ksbn8.js → cli-3p8fqcnh.js} +2 -2
  20. package/dist/{cli-x1p5dhq2.js → cli-3r448dqn.js} +3 -3
  21. package/dist/{cli-ecshf504.js → cli-3tnpj2bh.js} +7 -7
  22. package/dist/{cli-ffbrkh1r.js → cli-471qk6s2.js} +46 -38
  23. package/dist/{cli-a49vpbf3.js → cli-5kqnemen.js} +1 -1
  24. package/dist/{cli-zvze9rbm.js → cli-6ncjvx55.js} +3 -3
  25. package/dist/{cli-fb6t92v8.js → cli-6x9ma5w5.js} +1 -1
  26. package/dist/{cli-c8fwpgqb.js → cli-7225gwj7.js} +2 -2
  27. package/dist/{cli-bn26xbcs.js → cli-8nyhf7za.js} +1 -1
  28. package/dist/{cli-y83zk5sb.js → cli-a0kcty51.js} +4 -4
  29. package/dist/{cli-8b9wvjps.js → cli-a4gvwttf.js} +4 -4
  30. package/dist/{cli-jhp7w1fy.js → cli-a5ebc95n.js} +2 -2
  31. package/dist/{cli-mz1ezzzh.js → cli-ash6xxk7.js} +2 -2
  32. package/dist/{cli-rsd7ga7v.js → cli-axh29tyr.js} +11 -11
  33. package/dist/{cli-p22zdvnt.js → cli-bn0rewsy.js} +3 -3
  34. package/dist/{cli-6ftrrwm1.js → cli-bn5kevnf.js} +5 -5
  35. package/dist/{cli-p1m2b6gc.js → cli-d2mb5s8a.js} +7 -7
  36. package/dist/{cli-c5sbmw1v.js → cli-d82btycs.js} +94 -15
  37. package/dist/{cli-vjfpshn3.js → cli-ejs3m1sx.js} +1 -1
  38. package/dist/{cli-a1esmmee.js → cli-fpmsqq59.js} +7 -2
  39. package/dist/{cli-8cxkw1f5.js → cli-fz9kewbh.js} +2 -2
  40. package/dist/{cli-8rwvksbc.js → cli-g0hgtz4p.js} +5 -5
  41. package/dist/{cli-kz116kvw.js → cli-gf0kfg81.js} +7 -7
  42. package/dist/{cli-eqgfs202.js → cli-hqhzg1qq.js} +2 -2
  43. package/dist/{cli-3wjs22x3.js → cli-j4rqdf2n.js} +2 -2
  44. package/dist/{cli-ywyrffff.js → cli-jh0qcrga.js} +1 -1
  45. package/dist/{cli-ttaz6c7m.js → cli-k9z4zd4p.js} +6 -5
  46. package/dist/{cli-nme7wery.js → cli-kmqgmcnb.js} +46 -13
  47. package/dist/{cli-x719j3vs.js → cli-kr2pmfqx.js} +392 -168
  48. package/dist/{cli-wrv09v7q.js → cli-m2qgh5sv.js} +1 -1
  49. package/dist/{cli-xfe63zx3.js → cli-mfe7f7b5.js} +2 -2
  50. package/dist/{cli-x3en6ct3.js → cli-nwyw3zhg.js} +1 -1
  51. package/dist/{cli-5s5hx57y.js → cli-nx462e4v.js} +1 -1
  52. package/dist/{cli-bfc3k1x5.js → cli-p0rakbrk.js} +9 -9
  53. package/dist/{cli-avmqhatn.js → cli-p6argxh9.js} +8 -8
  54. package/dist/{cli-3avfh5az.js → cli-pemywhfq.js} +2 -2
  55. package/dist/{cli-sppc0cmw.js → cli-pnp2j5zg.js} +2 -2
  56. package/dist/{cli-wk8ccrz6.js → cli-pw7v69vp.js} +2 -2
  57. package/dist/{cli-bhwa8rzc.js → cli-rdq7tzwm.js} +13 -12
  58. package/dist/{cli-rg7ntypm.js → cli-rqqsfz30.js} +379 -87
  59. package/dist/{cli-kk2r80n5.js → cli-s48yvzb8.js} +4 -4
  60. package/dist/{cli-zv4vy2b0.js → cli-s95dknxh.js} +1 -1
  61. package/dist/{cli-b80a1100.js → cli-sjyetj21.js} +2 -2
  62. package/dist/{cli-rdr166ht.js → cli-smvcw9z5.js} +1 -1
  63. package/dist/{cli-2rwet3r7.js → cli-sv5gb0yb.js} +1 -1
  64. package/dist/{cli-5ff2sjyt.js → cli-syhq378v.js} +2 -2
  65. package/dist/{cli-tj4j2ts4.js → cli-tbhwrxpt.js} +8 -8
  66. package/dist/{cli-4fhyq711.js → cli-vy0fay6k.js} +13 -13
  67. package/dist/{cli-ptkpaqfy.js → cli-wt645v7q.js} +1 -1
  68. package/dist/{cli-f3nx1h8z.js → cli-wxbn3yfx.js} +2 -2
  69. package/dist/{cli-wyrr38xm.js → cli-x5mq1fyq.js} +1 -1
  70. package/dist/{cli-ccn0eybf.js → cli-xbzvet96.js} +6 -6
  71. package/dist/{cli-4mkvwgzr.js → cli-xf234ws8.js} +1 -1
  72. package/dist/{cli-tgvpmnwz.js → cli-xftrvxag.js} +5 -5
  73. package/dist/{cli-02vhhssk.js → cli-z071fb95.js} +5 -5
  74. package/dist/{cli-mvg2hf8p.js → cli-z1ew8vbq.js} +14 -7
  75. package/dist/{cli-dsgj3e7a.js → cli-zfg9enh9.js} +2 -2
  76. package/dist/{cli-yxqag76b.js → cli-zw88zjba.js} +1 -1
  77. package/dist/cli.js +19 -19
  78. package/dist/{codex-adapter-pp2gq8gr.js → codex-adapter-p6rmm7vv.js} +6 -6
  79. package/dist/{codex-hook-qwzccd64.js → codex-hook-x7nejyrr.js} +7 -4
  80. package/dist/{codex-login-35p4sk85.js → codex-login-m819c41r.js} +1 -1
  81. package/dist/{codex-session-runner-0vzn09t8.js → codex-session-runner-8vd5t9t0.js} +7 -7
  82. package/dist/{commands-psqhkttb.js → commands-7ems2p13.js} +2 -2
  83. package/dist/{controller-t20vz2k8.js → controller-m263fg43.js} +4 -4
  84. package/dist/{controllers-connect-gkxmqw9d.js → controllers-connect-bg33ae53.js} +23 -23
  85. package/dist/{cursor-adapter-3v542qe5.js → cursor-adapter-cz6nm6mj.js} +3 -3
  86. package/dist/{db-2xbyenjr.js → db-w505dwa8.js} +2 -2
  87. package/dist/{db-nc1rd869.js → db-ys2rbdft.js} +3 -3
  88. package/dist/{devin-adapter-7egzmt0k.js → devin-adapter-zgqadvnm.js} +1 -1
  89. package/dist/{dispatcher-ewceb8ts.js → dispatcher-0s0mckqr.js} +20 -20
  90. package/dist/{docker-njavd3fb.js → docker-fkgtr75t.js} +2 -2
  91. package/dist/{dsh-adapter-75xwh78t.js → dsh-adapter-3n3net33.js} +3 -3
  92. package/dist/{e2b-5r6a5jeb.js → e2b-7am6tmq6.js} +2 -2
  93. package/dist/{e2b-pw1nvtmh.js → e2b-thdcxtjd.js} +2 -2
  94. package/dist/{easter-egg-3d-xsc47jwf.js → easter-egg-3d-5cgynaja.js} +1 -1
  95. package/dist/{event-bus-101fenkh.js → event-bus-9vpk8gmq.js} +1 -1
  96. package/dist/{execute-gzmw1mnw.js → execute-t4e6m3rw.js} +1 -1
  97. package/dist/{external-channel-a5dsmxn4.js → external-channel-5b23fv8z.js} +2 -2
  98. package/dist/{grok-adapter-vj88xr23.js → grok-adapter-q9fghe5q.js} +10 -10
  99. package/dist/handlers-mqrym347.js +39 -0
  100. package/dist/{heartbeat-t2m9r3wa.js → heartbeat-tww6yx1w.js} +23 -23
  101. package/dist/{hook-xa677f8r.js → hook-dx0pkvar.js} +16 -9
  102. package/dist/{hook-8femx1bg.js → hook-pvmh9b6m.js} +6 -6
  103. package/dist/{http-xpmmq028.js → http-k2ejbt05.js} +199 -131
  104. package/dist/{index-s7ckdajq.js → index-2z710v4x.js} +10 -10
  105. package/dist/{index-dg2tmj99.js → index-dgz61nff.js} +20 -20
  106. package/dist/{index-syztqdt6.js → index-g6tbb6ve.js} +1 -1
  107. package/dist/index-h2ek08d6.js +40 -0
  108. package/dist/index-pqfwh9nn.js +41 -0
  109. package/dist/{index-cdzyfrsp.js → index-x5aktmp8.js} +1 -1
  110. package/dist/{keepalive-3nr4st3r.js → keepalive-nhvpw10z.js} +4 -4
  111. package/dist/lead-jb6sf69z.js +78 -0
  112. package/dist/{maintenance-verwpe2c.js → maintenance-agakc519.js} +1 -1
  113. package/dist/{oauth-refresh-sweep-9q2t0y3q.js → oauth-refresh-sweep-4dcbhtn0.js} +3 -3
  114. package/dist/{onboard-aytqedmq.js → onboard-4n2kwa4v.js} +3 -3
  115. package/dist/{opencode-adapter-f4hryjny.js → opencode-adapter-3s7ax02e.js} +7 -7
  116. package/dist/{otel-impl-hckw4wt1.js → otel-impl-tfa12080.js} +2 -2
  117. package/dist/{pi-mono-adapter-hxby0ce1.js → pi-mono-adapter-ytp7gz8b.js} +22 -11
  118. package/dist/{pricing-refresh-5nvme3sm.js → pricing-refresh-3cqx3f3x.js} +3 -3
  119. package/dist/{rbac-roles-pd8c26m1.js → rbac-roles-9fae5rnv.js} +1 -1
  120. package/dist/{rbac-roles-vbpy21mg.js → rbac-roles-y6zbfzve.js} +2 -2
  121. package/dist/{render-v2-7nqgvk2q.js → render-v2-emawjvc4.js} +3 -3
  122. package/dist/{resume-b6djcj3c.js → resume-zhgts7h7.js} +10 -8
  123. package/dist/{runtime-txrxc9za.js → runtime-xe3aqy4x.js} +1 -1
  124. package/dist/{session-access-fc3f9y12.js → session-access-cm823e06.js} +4 -4
  125. package/dist/{setup-n39szm6b.js → setup-tkp99w1e.js} +1 -1
  126. package/dist/{virtual-modules-wyp2k6z3.js → virtual-modules-4r3zkc20.js} +2 -2
  127. package/dist/{work-objects-jem2fre0.js → work-objects-f722h263.js} +23 -23
  128. package/dist/worker-p7f336rq.js +78 -0
  129. package/dist/{x-8ewt8gxn.js → x-y45mthwn.js} +2 -2
  130. package/openapi.json +99 -52
  131. package/package.json +2 -2
  132. package/src/apps/action-runner.ts +146 -0
  133. package/src/apps/row-store.ts +4 -2
  134. package/src/apps/script-types.ts +1 -1
  135. package/src/be/db.ts +14 -4
  136. package/src/be/fleet-benchmark-stats.ts +10 -5
  137. package/src/be/models-catalog.ts +4 -0
  138. package/src/be/onboarding.ts +6 -0
  139. package/src/be/scripts/typecheck.ts +13 -0
  140. package/src/be/swarm-config-guard.ts +10 -1
  141. package/src/be/telemetry-enable.ts +1 -1
  142. package/src/commands/provider-credentials.ts +12 -2
  143. package/src/commands/runner.ts +3 -0
  144. package/src/heartbeat/heartbeat.ts +2 -2
  145. package/src/hooks/codex-hook.ts +6 -2
  146. package/src/hooks/hook.ts +7 -0
  147. package/src/hooks/pr-body-guard.ts +38 -11
  148. package/src/hooks/pr-room-guard.ts +88 -0
  149. package/src/http/agents.ts +1 -0
  150. package/src/http/apps.ts +84 -181
  151. package/src/http/fleet-benchmark.ts +79 -30
  152. package/src/http/global-config.ts +7 -0
  153. package/src/http/ranking.ts +24 -40
  154. package/src/http/tasks.ts +2 -1
  155. package/src/http/telemetry-ui.ts +1 -1
  156. package/src/http/used-by.ts +55 -19
  157. package/src/http/users.ts +2 -2
  158. package/src/providers/amp-adapter.ts +1 -1
  159. package/src/providers/pi-mono-adapter.ts +15 -0
  160. package/src/providers/reasoning-effort.ts +2 -1
  161. package/src/providers/types.ts +2 -0
  162. package/src/ranking/telemetry-ranking.ts +5 -33
  163. package/src/scripts-runtime/sdk-allowlist.ts +7 -0
  164. package/src/scripts-runtime/types/stdlib.d.ts +31 -0
  165. package/src/scripts-runtime/types/swarm-sdk.d.ts +31 -0
  166. package/src/server.ts +4 -0
  167. package/src/telemetry-context.ts +19 -1
  168. package/src/telemetry-contract/EVENTS.md +42 -8
  169. package/src/telemetry-contract/SOURCE.json +6 -5
  170. package/src/telemetry-contract/catalog.json +225 -5
  171. package/src/telemetry-contract/ranking/fixtures/valid/no-claim.json +10 -0
  172. package/src/telemetry-contract/ranking/listing.v1.schema.json +2 -2
  173. package/src/telemetry-contract/types.gen.ts +225 -27
  174. package/src/telemetry-identity.ts +47 -9
  175. package/src/telemetry-snapshot.ts +9 -1
  176. package/src/telemetry-ui-events.ts +9 -2
  177. package/src/telemetry.ts +14 -3
  178. package/src/tests/app-action-run-tool.test.ts +178 -0
  179. package/src/tests/app-row-tools.test.ts +360 -0
  180. package/src/tests/check-pr-body.test.ts +0 -23
  181. package/src/tests/credential-check.test.ts +38 -0
  182. package/src/tests/fleet-benchmark.test.ts +122 -2
  183. package/src/tests/http-users.test.ts +62 -1
  184. package/src/tests/model-catalog-table.test.ts +63 -0
  185. package/src/tests/model-profiles-api.test.ts +19 -7
  186. package/src/tests/models-catalog.test.ts +50 -0
  187. package/src/tests/onboarding.test.ts +21 -0
  188. package/src/tests/org-website.test.ts +62 -0
  189. package/src/tests/pi-mono-adapter.test.ts +23 -0
  190. package/src/tests/pr-body-leaks.test.ts +4 -0
  191. package/src/tests/pr-room-guard.test.ts +242 -0
  192. package/src/tests/ranking-listing.test.ts +8 -23
  193. package/src/tests/ranking-route.test.ts +49 -47
  194. package/src/tests/reasoning-effort.test.ts +39 -3
  195. package/src/tests/runner-harness-telemetry-env.test.ts +56 -1
  196. package/src/tests/swarm-tool-result-gate.test.ts +2 -4
  197. package/src/tests/telemetry-context.test.ts +128 -2
  198. package/src/tests/telemetry-ui-route.test.ts +123 -0
  199. package/src/tests/tool-annotations.test.ts +3 -2
  200. package/src/tests/used-by-route.test.ts +144 -10
  201. package/src/tests/workflow-agent-task-retry.test.ts +130 -4
  202. package/src/tests/workflow-wait-recovery.test.ts +116 -4
  203. package/src/tools/app-action-run.ts +63 -0
  204. package/src/tools/app-rows.ts +234 -0
  205. package/src/tools/model-catalog/index.ts +1 -1
  206. package/src/tools/tool-config.ts +8 -0
  207. package/src/tools/utils.ts +1 -1
  208. package/src/tools/workflows/retry-workflow-run.ts +1 -1
  209. package/src/types.ts +17 -0
  210. package/src/utils/company-profile.ts +24 -0
  211. package/src/utils/org-website.ts +58 -21
  212. package/src/workflows/recovery.ts +10 -22
  213. package/src/workflows/resume.ts +16 -0
  214. package/src/workflows/task-step-routing.ts +47 -7
  215. package/templates/skills/apps/SKILL.md +41 -2
  216. package/templates/skills/apps/content.md +41 -2
  217. package/dist/handlers-zbs90bny.js +0 -39
  218. package/dist/index-8pwc1jc8.js +0 -40
  219. package/dist/index-nxa6me0q.js +0 -41
  220. package/dist/lead-td8h2sfn.js +0 -78
  221. package/dist/worker-wr6xgfr2.js +0 -78
  222. package/src/tests/check-ranking-entry.test.ts +0 -146
@@ -0,0 +1,242 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { mkdtemp, rm } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { codexPreToolUseBlock } from "../hooks/codex-hook";
6
+ import { gitRemoteOrigin, guardGhPrRoom, type PrRoomGuardDeps } from "../hooks/pr-room-guard";
7
+ import { expectChildOk, runChild } from "./test-proc";
8
+
9
+ const ROOM_LINE = "PR room: prr-abcde23457";
10
+ const BLOCK_REASON =
11
+ 'This repo uses PR rooms. Create the room first with the pr-room skill (app action createRoom). Then pass the returned prBody with --body-file. The body must contain the line "PR room: prr-...".';
12
+
13
+ function fakeDeps(overrides: Partial<PrRoomGuardDeps> = {}): PrRoomGuardDeps {
14
+ return {
15
+ env: { PR_ROOM_REPOS: "owner/repo" },
16
+ readFile: async () => "No room marker",
17
+ remoteOrigin: async () => "https://github.com/owner/repo.git",
18
+ ...overrides,
19
+ };
20
+ }
21
+
22
+ const check = (command: string, deps = fakeDeps()) => guardGhPrRoom({ command }, "/w", deps);
23
+
24
+ describe("guardGhPrRoom", () => {
25
+ test.each([undefined, "", " , , "])("allows disabled configuration %p", async (value) => {
26
+ expect(
27
+ await check(
28
+ "gh pr create -R owner/repo -b plain",
29
+ fakeDeps({ env: { PR_ROOM_REPOS: value } }),
30
+ ),
31
+ ).toBeNull();
32
+ });
33
+
34
+ test("blocks a configured repo without a room marker", async () => {
35
+ expect(await check("gh pr create --repo owner/repo --body plain")).toBe(BLOCK_REASON);
36
+ expect(await check("gh pr create -Rowner/repo")).toBe(BLOCK_REASON);
37
+ });
38
+
39
+ test("allows a marker in the inline body", async () => {
40
+ expect(await check(`gh pr create -R owner/repo --body 'Summary\n${ROOM_LINE}'`)).toBeNull();
41
+ });
42
+
43
+ test("resolves body files relative to cd", async () => {
44
+ const paths: string[] = [];
45
+ const deps = fakeDeps({
46
+ readFile: async (path) => {
47
+ paths.push(path);
48
+ return ROOM_LINE;
49
+ },
50
+ });
51
+ expect(
52
+ await check("cd /checkout && gh pr create -R owner/repo --body-file body.md", deps),
53
+ ).toBeNull();
54
+ expect(paths).toEqual(["/checkout/body.md"]);
55
+ });
56
+
57
+ test("allows a heredoc body with a marker", async () => {
58
+ expect(
59
+ await check(`gh pr create -R owner/repo --body-file - <<'EOF'\n${ROOM_LINE}\nEOF`),
60
+ ).toBeNull();
61
+ });
62
+
63
+ test("blocks resolved files and heredocs without a marker", async () => {
64
+ expect(await check("gh pr create -R owner/repo -F body.md")).toBe(BLOCK_REASON);
65
+ expect(await check("gh pr create -R owner/repo -F - <<'EOF'\nplain\nEOF")).toBe(BLOCK_REASON);
66
+ });
67
+
68
+ test("allows another repo without reading its body", async () => {
69
+ expect(await check("gh pr create -R other/repo -b plain")).toBeNull();
70
+ });
71
+
72
+ test.each([
73
+ "GH_REPO=owner/repo gh pr create -b plain",
74
+ "export GH_REPO=owner/repo && gh pr create -b plain",
75
+ ])("checks command targeting: %s", async (command) => {
76
+ expect(await check(command)).toBe(BLOCK_REASON);
77
+ });
78
+
79
+ test("allows an ambiguous target unless every candidate is configured", async () => {
80
+ const otherCheckout = fakeDeps({ remoteOrigin: async () => "https://github.com/other/repo" });
81
+ expect(await check("GH_REPO=owner/repo && gh pr create -b plain", otherCheckout)).toBeNull();
82
+ expect(await check("GH_REPO=owner/repo && gh pr create -b plain")).toBe(BLOCK_REASON);
83
+ expect(await check("gh pr create -R owner/repo -R other/repo -b plain")).toBeNull();
84
+ });
85
+
86
+ test("checks inherited GH_REPO and honors an explicit override", async () => {
87
+ const deps = fakeDeps({ env: { PR_ROOM_REPOS: "owner/repo", GH_REPO: "owner/repo" } });
88
+ expect(await check("gh pr create -b plain", deps)).toBe(BLOCK_REASON);
89
+ expect(await check("gh pr create -R other/repo -b plain", deps)).toBeNull();
90
+ });
91
+
92
+ test("trims configured repos and matches case insensitively", async () => {
93
+ const deps = fakeDeps({ env: { PR_ROOM_REPOS: " other/repo, OWNER/REPO , " } });
94
+ expect(await check("gh -R Owner/Repo pr create -b plain", deps)).toBe(BLOCK_REASON);
95
+ });
96
+
97
+ test("allows edits and unrelated commands", async () => {
98
+ expect(await check("gh pr edit 123 -R owner/repo -b plain")).toBeNull();
99
+ expect(await check("gh pr view 123 -R owner/repo")).toBeNull();
100
+ expect(await guardGhPrRoom(undefined, "/w", fakeDeps())).toBeNull();
101
+ });
102
+
103
+ test.each([
104
+ "https://github.com/Owner/Repo.git",
105
+ "https://github.com/Owner/Repo",
106
+ "git@github.com:Owner/Repo.git",
107
+ "ssh://git@github.com/Owner/Repo.git",
108
+ ])("resolves checkout remote %s", async (remote) => {
109
+ const lookups: string[] = [];
110
+ const deps = fakeDeps({
111
+ remoteOrigin: async (cwd) => {
112
+ lookups.push(cwd);
113
+ return remote;
114
+ },
115
+ });
116
+ expect(await check("cd /checkout && gh pr create -b plain", deps)).toBe(BLOCK_REASON);
117
+ expect(lookups).toEqual(["/checkout"]);
118
+ });
119
+
120
+ test("allows failed remote lookup and unsupported remote forms", async () => {
121
+ expect(
122
+ await check("gh pr create -b plain", fakeDeps({ remoteOrigin: async () => "local/path" })),
123
+ ).toBeNull();
124
+ expect(
125
+ await check(
126
+ "gh pr create -b plain",
127
+ fakeDeps({
128
+ remoteOrigin: async () => {
129
+ throw new Error("no checkout");
130
+ },
131
+ }),
132
+ ),
133
+ ).toBeNull();
134
+ });
135
+
136
+ // git exports GIT_DIR to a pre-push hook in a linked worktree. The lookup then reads that repo.
137
+ test.skipIf(process.env.GIT_DIR !== undefined)(
138
+ "allows a checkout remote lookup that does not finish",
139
+ async () => {
140
+ const dir = await mkdtemp(join(tmpdir(), "pr-room-fifo-"));
141
+ try {
142
+ expectChildOk(await runChild(["git", "init", "-q", dir]), "git init");
143
+ await rm(join(dir, ".git", "config"));
144
+ expectChildOk(await runChild(["mkfifo", join(dir, ".git", "config")]), "mkfifo");
145
+ await expect(gitRemoteOrigin(dir, 200)).rejects.toThrow();
146
+ const deps = fakeDeps({ remoteOrigin: (cwd) => gitRemoteOrigin(cwd, 200) });
147
+ expect(await guardGhPrRoom({ command: "gh pr create -b plain" }, dir, deps)).toBeNull();
148
+ } finally {
149
+ await rm(dir, { recursive: true, force: true });
150
+ }
151
+ },
152
+ );
153
+
154
+ test.each([
155
+ 'gh pr create -R "$UNKNOWN" -b plain',
156
+ 'gh pr create -R owner/repo -b "$UNKNOWN"',
157
+ 'gh pr create -R owner/repo -F "$UNKNOWN"',
158
+ "cd - && gh pr create -R owner/repo -b plain",
159
+ 'cd "$UNKNOWN" && gh pr create -b plain',
160
+ "read GH_REPO && gh pr create -b plain",
161
+ ])("allows unresolved inputs: %s", async (command) => {
162
+ expect(await check(command)).toBeNull();
163
+ });
164
+
165
+ test("allows unreadable body files", async () => {
166
+ const deps = fakeDeps({
167
+ readFile: async () => {
168
+ throw new Error("unreadable");
169
+ },
170
+ });
171
+ expect(await check("gh pr create -R owner/repo -F missing.md", deps)).toBeNull();
172
+ });
173
+
174
+ test("resolves known variables without changing leak guard texts", async () => {
175
+ expect(await check('REPO=owner/repo && gh pr create -R "$REPO" -b plain')).toBe(BLOCK_REASON);
176
+ expect(await check(`BODY='${ROOM_LINE}' && gh pr create -R owner/repo -b "$BODY"`)).toBeNull();
177
+ });
178
+
179
+ test.each([
180
+ "cat room.md | gh pr create -R owner/repo -F -",
181
+ "printf plain | gh pr create -R owner/repo -F -",
182
+ "gh pr create -R owner/repo -b $(cat room.md)",
183
+ "gh pr create -R owner/repo --fill",
184
+ "gh pr create -R owner/repo -f",
185
+ "gh pr create -R owner/repo --fill-first",
186
+ "gh pr create -R owner/repo --fill-verbose",
187
+ "gh pr create -R owner/repo -T template.md -b plain",
188
+ "gh pr create -R owner/repo --template=template.md",
189
+ "gh pr create -R owner/repo -dw -b plain",
190
+ "gh pr create -R owner/repo --web",
191
+ "gh pr create -R owner/repo -e",
192
+ "gh pr create -R owner/repo --editor",
193
+ "gh pr create -R owner/repo --recover input.json",
194
+ ])("allows a body the guard cannot read: %s", async (command) => {
195
+ expect(await check(command)).toBeNull();
196
+ });
197
+
198
+ test.each([
199
+ "PR room: prr-abcde2345",
200
+ "PR room: prr-abcde234578",
201
+ "PR room: prr-ABCDE23457",
202
+ "prefix PR room: prr-abcde23457",
203
+ ])("rejects an invalid marker: %s", async (line) => {
204
+ expect(await check(`gh pr create -R owner/repo -b '${line}'`)).toBe(BLOCK_REASON);
205
+ });
206
+
207
+ test("does not accept a marker only in the title", async () => {
208
+ expect(await check(`gh pr create -R owner/repo -t '${ROOM_LINE}' -b plain`)).toBe(BLOCK_REASON);
209
+ });
210
+
211
+ test("checks every create invocation", async () => {
212
+ expect(await check(`gh pr edit 1 -b plain && gh pr create -R owner/repo -b plain`)).toBe(
213
+ BLOCK_REASON,
214
+ );
215
+ });
216
+
217
+ test("Codex runs the room guard after the leak guard, including app-server sessions", async () => {
218
+ const roomDeps = fakeDeps();
219
+ const leakDeps = {
220
+ env: { SWARM_CODEX_APP_SERVER: "1" },
221
+ readFile: roomDeps.readFile,
222
+ repoVisibility: async () => "PUBLIC",
223
+ };
224
+ const msg = {
225
+ hook_event_name: "PreToolUse",
226
+ cwd: "/w",
227
+ tool_input: { command: "gh pr create -R owner/repo -b plain" },
228
+ };
229
+ expect(await codexPreToolUseBlock(msg, leakDeps, roomDeps)).toBe(BLOCK_REASON);
230
+ const leaky = {
231
+ ...msg,
232
+ tool_input: {
233
+ command:
234
+ "gh pr create -R owner/repo -b 'https://acme.slack.com/archives/C01234567/p1700000000000000'",
235
+ },
236
+ };
237
+ expect(await codexPreToolUseBlock(leaky, leakDeps, roomDeps)).toContain("PR body leak check:");
238
+ expect(
239
+ await codexPreToolUseBlock({ ...msg, hook_event_name: "PostToolUse" }, leakDeps, roomDeps),
240
+ ).toBeNull();
241
+ });
242
+ });
@@ -2,12 +2,7 @@ import { describe, expect, test } from "bun:test";
2
2
  import { readdirSync, readFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { isInternalConfigKey, validateConfigValue } from "../be/swarm-config-guard";
5
- import {
6
- buildListing,
7
- mintRankingClaim,
8
- parseTelemetryRanking,
9
- rankingEntryFile,
10
- } from "../ranking/telemetry-ranking";
5
+ import { buildListing, parseTelemetryRanking } from "../ranking/telemetry-ranking";
11
6
  import { RANKING_TAXONOMY } from "../telemetry-contract/ranking-taxonomy.gen";
12
7
  import { orgIdentityStatus } from "../utils/org-identity";
13
8
 
@@ -41,7 +36,6 @@ describe("telemetry_ranking contract fixtures", () => {
41
36
 
42
37
  describe("buildListing", () => {
43
38
  const identity = orgIdentityStatus({ SWARM_ORG_NAME: "Acme", SWARM_ORG_WEBSITE: "acme.dev" });
44
- const claim = mintRankingClaim();
45
39
 
46
40
  test("copies the name and domain from the org identity and blanks to null", () => {
47
41
  const listing = buildListing(
@@ -52,12 +46,10 @@ describe("buildListing", () => {
52
46
  description: " ",
53
47
  sourceCodeUrl: null,
54
48
  },
55
- claim,
56
49
  new Date("2026-10-18T10:00:00Z"),
57
50
  );
58
51
  expect(listing).toEqual({
59
52
  v: 1,
60
- claim,
61
53
  name: "Acme",
62
54
  domain: "acme.dev",
63
55
  category: "engineering",
@@ -66,29 +58,22 @@ describe("buildListing", () => {
66
58
  sourceCodeUrl: null,
67
59
  updatedAt: "2026-10-18T10:00:00.000Z",
68
60
  });
69
- expect(JSON.parse(rankingEntryFile(listing))).toEqual({
70
- $schema: "./schema.json",
71
- domain: "acme.dev",
72
- claim,
73
- });
74
61
  });
75
62
 
76
63
  test("refuses a missing identity and an http source URL", () => {
77
64
  expect(() =>
78
- buildListing(
79
- orgIdentityStatus({ SWARM_ORG_NAME: "Swarm", SWARM_ORG_WEBSITE: "acme.dev" }),
80
- { category: "other" },
81
- claim,
82
- ),
65
+ buildListing(orgIdentityStatus({ SWARM_ORG_NAME: "Swarm", SWARM_ORG_WEBSITE: "acme.dev" }), {
66
+ category: "other",
67
+ }),
83
68
  ).toThrow("SWARM_ORG_NAME");
84
69
  expect(() =>
85
- buildListing(identity, { category: "other", sourceCodeUrl: "http://github.com/acme" }, claim),
70
+ buildListing(identity, { category: "other", sourceCodeUrl: "http://github.com/acme" }),
86
71
  ).toThrow();
87
72
  });
88
73
 
89
- test("mints rk_ + 32 hex claims", () => {
90
- expect(mintRankingClaim()).toMatch(/^rk_[0-9a-f]{32}$/);
91
- expect(mintRankingClaim()).not.toBe(mintRankingClaim());
74
+ test("a v2.4.0 listing with a claim still parses", () => {
75
+ const legacy = { ...JSON.parse(read("valid", "full.json")), claim: `rk_${"0".repeat(32)}` };
76
+ expect(parseTelemetryRanking(JSON.stringify(legacy)).claim).toBe(legacy.claim);
92
77
  });
93
78
 
94
79
  test("the taxonomy comes from the vendored contract", () => {
@@ -163,22 +163,15 @@ describe("ranking routes", () => {
163
163
  test("opt-in stores the listing, turns the setting on and emits it with a matching envelope", async () => {
164
164
  const result = await call("POST", "/api/ranking/opt-in", form);
165
165
  expect(result.statusCode).toBe(200);
166
- const claim = String(result.json.claim);
167
- expect(claim).toMatch(/^rk_[0-9a-f]{32}$/);
166
+ // Opting in is the whole flow: no claim, no entry file, no PR page.
167
+ expect(Object.keys(result.json).sort()).toEqual(["domain", "listing", "verifyUrl"]);
168
168
  expect(result.json.domain).toBe("acme.dev");
169
- expect(String(result.json.prUrl)).toContain("filename=community%2Franking%2Facme.dev.json");
170
- expect(JSON.parse(String(result.json.fileJson))).toEqual({
171
- $schema: "./schema.json",
172
- domain: "acme.dev",
173
- claim,
174
- });
175
169
  expect(String(result.json.verifyUrl)).toBe(
176
- `https://proxy.desplega.sh/v1/ranking/verify?domain=acme.dev&claim=${claim}`,
170
+ "https://proxy.desplega.sh/v1/ranking/verify?domain=acme.dev",
177
171
  );
178
172
 
179
173
  const stored = parseTelemetryRanking(String(await configValue("telemetry_ranking")));
180
174
  expect(stored).toMatchObject({
181
- claim,
182
175
  name: "Acme",
183
176
  domain: "acme.dev",
184
177
  category: "engineering",
@@ -186,6 +179,7 @@ describe("ranking routes", () => {
186
179
  description: "QA",
187
180
  sourceCodeUrl: null,
188
181
  });
182
+ expect(stored).not.toHaveProperty("claim");
189
183
  expect(await configValue("PUBLIC_RANKING_ENABLED")).toBe("true");
190
184
 
191
185
  expect(sent.map((event) => event.event)).toEqual(["ranking.opted_in"]);
@@ -193,10 +187,6 @@ describe("ranking routes", () => {
193
187
  expect(listing).toEqual(stored);
194
188
  expect(sent[0]?.context).toMatchObject({ org_name: "Acme", org_website: "acme.dev" });
195
189
 
196
- // A second opt-in for the same domain keeps the claim.
197
- const again = await call("POST", "/api/ranking/opt-in", { category: "other" });
198
- expect(again.json.claim).toBe(claim);
199
-
200
190
  const state = await call("GET", "/api/ranking");
201
191
  expect(state.json).toMatchObject({
202
192
  eligible: true,
@@ -204,10 +194,12 @@ describe("ranking routes", () => {
204
194
  missing: [],
205
195
  listingMatchesDomain: true,
206
196
  });
197
+ expect(state.json).not.toHaveProperty("prUrl");
198
+ expect(state.json).not.toHaveProperty("fileJson");
207
199
  });
208
200
 
209
- test("only a config admin reads the ranking state and its claim", async () => {
210
- const claim = String((await call("POST", "/api/ranking/opt-in", form)).json.claim);
201
+ test("only a config admin reads the ranking state", async () => {
202
+ await call("POST", "/api/ranking/opt-in", form);
211
203
  const worker: HttpRequestAuth = {
212
204
  kind: "agent",
213
205
  agentId: crypto.randomUUID(),
@@ -220,42 +212,49 @@ describe("ranking routes", () => {
220
212
  for (const auth of [worker, guest]) {
221
213
  const denied = await call("GET", "/api/ranking", undefined, handleRanking, auth);
222
214
  expect(denied.statusCode).toBe(403);
223
- expect(JSON.stringify(denied.json)).not.toContain(claim);
224
215
  }
225
- const state = await call("GET", "/api/ranking");
226
- expect(state.statusCode).toBe(200);
227
- expect((state.json.listing as { claim: string }).claim).toBe(claim);
216
+ expect((await call("GET", "/api/ranking")).statusCode).toBe(200);
228
217
  });
229
218
 
230
- test("concurrent first opt-ins share one claim, and leave then rejoin keeps it", async () => {
231
- const [first, second] = await Promise.all([
232
- call("POST", "/api/ranking/opt-in", form),
233
- call("POST", "/api/ranking/opt-in", form),
234
- ]);
235
- expect(first.statusCode).toBe(200);
236
- expect(second.statusCode).toBe(200);
237
- // Either response may be the one merged into community/ranking.
238
- const merged = String(first.json.claim);
239
- expect(second.json.claim).toBe(merged);
240
- expect(parseTelemetryRanking(String(await configValue("telemetry_ranking")))?.claim).toBe(
241
- merged,
242
- );
219
+ test("a v2.4.0 listing keeps working and its claim is never served or re-sent", async () => {
220
+ const claim = `rk_${"0".repeat(32)}`;
221
+ await upsertSwarmConfig({
222
+ scope: "global",
223
+ key: "telemetry_ranking",
224
+ value: JSON.stringify({
225
+ v: 1,
226
+ claim,
227
+ name: "Acme",
228
+ domain: "acme.dev",
229
+ category: "other",
230
+ subcategory: null,
231
+ description: null,
232
+ sourceCodeUrl: null,
233
+ updatedAt: "2026-10-18T10:00:00.000Z",
234
+ }),
235
+ });
236
+ await upsertSwarmConfig({ scope: "global", key: "PUBLIC_RANKING_ENABLED", value: "true" });
237
+ const state = await call("GET", "/api/ranking");
238
+ expect(state.json).toMatchObject({ enabled: true, listingMatchesDomain: true });
239
+ expect(JSON.stringify(state.json)).not.toContain(claim);
243
240
 
244
- expect((await call("POST", "/api/ranking/opt-out")).statusCode).toBe(200);
245
- const rejoin = await call("POST", "/api/ranking/opt-in", form);
246
- expect(rejoin.json.claim).toBe(merged);
241
+ const put = await call(
242
+ "PUT",
243
+ "/api/config",
244
+ { scope: "global", key: "SWARM_ORG_NAME", value: "Acme Labs" },
245
+ handleConfig,
246
+ );
247
+ expect(put.statusCode).toBe(200);
248
+ expect(sent.map((event) => event.event)).toEqual(["ranking.opted_in"]);
249
+ expect(String(sent[0]?.properties.listing)).not.toContain("claim");
247
250
  });
248
251
 
249
- test("opt-out stores false, emits ranking.opted_out and returns the delete page", async () => {
252
+ test("opt-out stores false and emits ranking.opted_out", async () => {
250
253
  await call("POST", "/api/ranking/opt-in", form);
251
254
  sent = [];
252
255
  const result = await call("POST", "/api/ranking/opt-out");
253
256
  expect(result.statusCode).toBe(200);
254
- expect(result.json).toEqual({
255
- enabled: false,
256
- leaveUrl:
257
- "https://github.com/desplega-ai/agent-swarm/delete/main/community/ranking/acme.dev.json",
258
- });
257
+ expect(result.json).toEqual({ enabled: false });
259
258
  expect(await configValue("PUBLIC_RANKING_ENABLED")).toBe("false");
260
259
  expect(sent.map((event) => [event.event, event.properties.domain])).toEqual([
261
260
  ["ranking.opted_out", "acme.dev"],
@@ -273,8 +272,8 @@ describe("ranking routes", () => {
273
272
  expect(String(result.json.error)).toContain("/api/ranking");
274
273
  });
275
274
 
276
- test("renaming the org re-emits the listing with the same claim", async () => {
277
- const optIn = await call("POST", "/api/ranking/opt-in", form);
275
+ test("renaming the org re-emits the listing", async () => {
276
+ await call("POST", "/api/ranking/opt-in", form);
278
277
  sent = [];
279
278
  const put = await call(
280
279
  "PUT",
@@ -287,7 +286,6 @@ describe("ranking routes", () => {
287
286
  const listing = parseTelemetryRanking(String(sent[0]?.properties.listing));
288
287
  expect(listing).toMatchObject({
289
288
  name: "Acme Labs",
290
- claim: optIn.json.claim,
291
289
  subcategory: "qa-testing",
292
290
  });
293
291
  // The envelope carries the new name before the config reload reaches process.env.
@@ -312,8 +310,12 @@ describe("ranking routes", () => {
312
310
  ["ranking.opted_out", "acme.dev"],
313
311
  ]);
314
312
  }
315
- // After the last case (a new website) the old listing needs a new PR.
313
+ // After the last case (a new website) the old listing needs a new opt-in.
316
314
  const state = await call("GET", "/api/ranking");
317
- expect(state.json).toMatchObject({ enabled: false, listingMatchesDomain: false, prUrl: null });
315
+ expect(state.json).toMatchObject({
316
+ enabled: false,
317
+ listingMatchesDomain: false,
318
+ verifyUrl: null,
319
+ });
318
320
  });
319
321
  });
@@ -212,6 +212,13 @@ describe("applyReasoningEffort — pi-session shape", () => {
212
212
  sessionOptions: { thinkingLevel: "medium" },
213
213
  });
214
214
  });
215
+
216
+ test("max passes through for an opencode-go model that declares it", () => {
217
+ expect(applyReasoningEffort("pi", "opencode-go/deepseek-v4-flash", "max")).toEqual({
218
+ kind: "pi-session",
219
+ sessionOptions: { thinkingLevel: "max" },
220
+ });
221
+ });
215
222
  });
216
223
 
217
224
  describe("applyReasoningEffort — opencode-options shape", () => {
@@ -245,6 +252,30 @@ describe("applyReasoningEffort — opencode-options shape", () => {
245
252
  kind: "noop",
246
253
  });
247
254
  });
255
+
256
+ test("opencode-go passes max through on its OpenAI-compatible transport", () => {
257
+ // `opencode-go/deepseek-v4-flash` declares low|high|max in the snapshot.
258
+ expect(reasoningCapability("opencode", "opencode-go/deepseek-v4-flash").levels).toEqual([
259
+ "low",
260
+ "high",
261
+ "max",
262
+ ]);
263
+ expect(applyReasoningEffort("opencode", "opencode-go/deepseek-v4-flash", "max")).toEqual({
264
+ kind: "opencode-options",
265
+ providerId: "opencode-go",
266
+ modelId: "deepseek-v4-flash",
267
+ options: { reasoningEffort: "max" },
268
+ });
269
+ });
270
+
271
+ test("anthropic max uses the effort key opencode's Anthropic transport reads", () => {
272
+ expect(applyReasoningEffort("opencode", "anthropic/claude-opus-5-5", "max")).toEqual({
273
+ kind: "opencode-options",
274
+ providerId: "anthropic",
275
+ modelId: "claude-opus-5-5",
276
+ options: { effort: "max" },
277
+ });
278
+ });
248
279
  });
249
280
 
250
281
  // The offered levels, the API's validation and the harness application all read the same
@@ -266,16 +297,21 @@ describe("harness × model → effort matrix", () => {
266
297
  },
267
298
  { harness: "codex", model: "gpt-6-astra", levels: ["low", "medium", "high", "xhigh", "max"] },
268
299
  { harness: "codex", model: "gpt-5.1-codex", levels: ["low", "medium", "high"] },
269
- { harness: "pi", model: "openrouter/deepseek/deepseek-v4.1-flash", levels: ["low", "high"] },
300
+ {
301
+ harness: "pi",
302
+ model: "openrouter/deepseek/deepseek-v4.1-flash",
303
+ levels: ["low", "high", "max"],
304
+ },
270
305
  {
271
306
  harness: "pi",
272
307
  model: "openrouter/anthropic/claude-opus-5.5",
273
- levels: ["low", "medium", "high", "xhigh"],
308
+ levels: ["low", "medium", "high", "xhigh", "max"],
274
309
  },
275
310
  {
276
311
  harness: "opencode",
277
312
  model: "openrouter/deepseek/deepseek-v4.1-flash",
278
- levels: ["low", "high"],
313
+ // The facts declare max, and opencode passes it through.
314
+ levels: ["low", "high", "max"],
279
315
  },
280
316
  { harness: "pi", model: "openrouter/qwen/qwen3-coder-flash", levels: [] },
281
317
  { harness: "claude", model: "totally-custom-model-xyz", levels: [] },
@@ -1,6 +1,7 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import { join } from "node:path";
3
- import { withHarnessTelemetryEnv } from "../commands/runner";
3
+ import { applyResolvedEnvToProcessEnv, withHarnessTelemetryEnv } from "../commands/runner";
4
+ import { _resetTelemetryStateForTests, initTelemetry, track } from "../telemetry";
4
5
  import { CHILD_PROCESS_TEST_BUDGET_MS, runChild } from "./test-proc";
5
6
 
6
7
  const TELEMETRY_PATH = join(import.meta.dir, "..", "telemetry.ts");
@@ -86,6 +87,60 @@ describe("withHarnessTelemetryEnv", () => {
86
87
  });
87
88
  });
88
89
 
90
+ test("worker telemetry receives and clears company profile through resolved config", async () => {
91
+ const profile = {
92
+ SWARM_ORG_WEBSITE: "acme.com",
93
+ SWARM_ORG_TEAM_SIZE: "2-10",
94
+ SWARM_ORG_USE_CASE: "ops",
95
+ SWARM_ORG_ACQUISITION_SOURCE: "github",
96
+ };
97
+ const saved = Object.fromEntries(
98
+ [...Object.keys(profile), "ANONYMIZED_TELEMETRY"].map((key) => [key, process.env[key]]),
99
+ );
100
+ const originalFetch = globalThis.fetch;
101
+ const sent: Array<{ context: Record<string, unknown> }> = [];
102
+ try {
103
+ process.env.ANONYMIZED_TELEMETRY = "true";
104
+ _resetTelemetryStateForTests();
105
+ globalThis.fetch = (async (_url: string, init?: { body?: string }) => {
106
+ sent.push(JSON.parse(init?.body ?? "{}"));
107
+ return new Response(null, { status: 204 });
108
+ }) as typeof fetch;
109
+ applyResolvedEnvToProcessEnv(profile);
110
+ const config: Record<string, string> = {
111
+ telemetry_installation_id: "install_worker_profile",
112
+ telemetry_org_id: "org_0123456789abcdef",
113
+ telemetry_org_domain: "acme.com",
114
+ };
115
+ await initTelemetry(
116
+ "worker",
117
+ (key) => config[key],
118
+ () => {},
119
+ );
120
+ track({ event: "session.started", properties: { agentId: crypto.randomUUID() } });
121
+ expect(sent[0]?.context).toMatchObject({
122
+ org_website: "acme.com",
123
+ org_domain: "acme.com",
124
+ team_size: "2-10",
125
+ use_case: "ops",
126
+ acquisition_source: "github",
127
+ });
128
+ applyResolvedEnvToProcessEnv(Object.fromEntries(Object.keys(profile).map((key) => [key, ""])));
129
+ track({ event: "session.started", properties: { agentId: crypto.randomUUID() } });
130
+ expect(sent[1]?.context).not.toHaveProperty("org_website");
131
+ expect(sent[1]?.context).not.toHaveProperty("team_size");
132
+ expect(sent[1]?.context).not.toHaveProperty("use_case");
133
+ expect(sent[1]?.context.acquisition_source).toBeNull();
134
+ } finally {
135
+ for (const [key, value] of Object.entries(saved)) {
136
+ if (value === undefined) delete process.env[key];
137
+ else process.env[key] = value;
138
+ }
139
+ globalThis.fetch = originalFetch;
140
+ _resetTelemetryStateForTests();
141
+ }
142
+ });
143
+
89
144
  describe("API boot telemetry environment", () => {
90
145
  test(
91
146
  "an unprefixed API boot from an agent session emits test",
@@ -288,7 +288,7 @@ describe("finalizeSwarmToolResult", () => {
288
288
  expect((nonEmpty.structuredContent as { nudge?: string }).nudge).toBeUndefined();
289
289
  });
290
290
 
291
- test("NUDGES map: app-get steers to the callable surface only when one exists", async () => {
291
+ test("NUDGES map: app-get steers to queries and actions only when the app has them", async () => {
292
292
  const withSurface = await finalizeSwarmToolResult("app-get", {
293
293
  ok: true,
294
294
  message: 'App "PM Inbox" (a1).',
@@ -302,9 +302,7 @@ describe("finalizeSwarmToolResult", () => {
302
302
  },
303
303
  },
304
304
  });
305
- expect((withSurface.structuredContent as { nudge?: string }).nudge).toContain(
306
- "callable surface",
307
- );
305
+ expect((withSurface.structuredContent as { nudge?: string }).nudge).toContain("app-action-run");
308
306
 
309
307
  const bare = await finalizeSwarmToolResult("app-get", {
310
308
  ok: true,