@desplega.ai/agent-swarm 2.3.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 (332) hide show
  1. package/README.md +1 -0
  2. package/dist/{acp-adapter-v85a2bqy.js → acp-adapter-zvzr538d.js} +9 -9
  3. package/dist/{actions-zgcxfspj.js → actions-jhtqdw69.js} +21 -19
  4. package/dist/{amp-adapter-jw9xfz42.js → amp-adapter-bd84rhs2.js} +4 -4
  5. package/dist/{app-4e1s4675.js → app-kreq1gwg.js} +3 -3
  6. package/dist/{assistant-v238n9dk.js → assistant-3k22sbsq.js} +23 -21
  7. package/dist/{boot-reembed-p666cw5n.js → boot-reembed-e53x6b9v.js} +1 -1
  8. package/dist/{boot-reembed-w6pm64j4.js → boot-reembed-f6z923ny.js} +1 -1
  9. package/dist/{boot-scrub-sweep-jcyjagn3.js → boot-scrub-sweep-0538vsb7.js} +4 -4
  10. package/dist/{boot-seeds-gbedmamp.js → boot-seeds-6wxqkfk2.js} +4 -4
  11. package/dist/{claude-adapter-q7wspzt8.js → claude-adapter-gfts5044.js} +9 -9
  12. package/dist/{claude-managed-adapter-n7ahjyx6.js → claude-managed-adapter-h2y889ge.js} +3 -3
  13. package/dist/{claude-managed-setup-wza784fb.js → claude-managed-setup-h00hsgtz.js} +2 -2
  14. package/dist/{claude-sdk-session-yf400ptr.js → claude-sdk-session-56besmfz.js} +13 -13
  15. package/dist/{cli-65q70v2v.js → cli-0bh477zn.js} +33 -6
  16. package/dist/{cli-4ync7m60.js → cli-0ebey9hg.js} +1 -1
  17. package/dist/{cli-665j3feb.js → cli-0xk505e8.js} +1 -1
  18. package/dist/{cli-7gh7020n.js → cli-1dmcq2eh.js} +1 -1
  19. package/dist/{cli-gzvb3d4a.js → cli-1jx0srjt.js} +3 -3
  20. package/dist/{cli-zj4d6afs.js → cli-1pa3rz69.js} +21 -6
  21. package/dist/{cli-7ayprxp7.js → cli-2dfbv8qr.js} +8 -0
  22. package/dist/{cli-30m5q8mc.js → cli-3kh4kpqy.js} +6 -6
  23. package/dist/{cli-qrwp40st.js → cli-3p8fqcnh.js} +2 -2
  24. package/dist/{cli-y3dvz6ge.js → cli-3r448dqn.js} +3 -3
  25. package/dist/{cli-a17tzmn0.js → cli-3tnpj2bh.js} +7 -7
  26. package/dist/{cli-ea7chd26.js → cli-471qk6s2.js} +93 -46
  27. package/dist/{cli-a49vpbf3.js → cli-5kqnemen.js} +1 -1
  28. package/dist/{cli-0rh20bp3.js → cli-6ncjvx55.js} +3 -3
  29. package/dist/{cli-fb6t92v8.js → cli-6x9ma5w5.js} +1 -1
  30. package/dist/{cli-k28p6xh7.js → cli-7225gwj7.js} +2 -2
  31. package/dist/{cli-8cvvytad.js → cli-8nyhf7za.js} +3 -3
  32. package/dist/{cli-e09hj50q.js → cli-8wkb09q5.js} +11 -11
  33. package/dist/{cli-j0v75y13.js → cli-a0kcty51.js} +4 -4
  34. package/dist/{cli-zes4tsbb.js → cli-a4gvwttf.js} +4 -4
  35. package/dist/{cli-jhp7w1fy.js → cli-a5ebc95n.js} +2 -2
  36. package/dist/{cli-5d1m1qca.js → cli-ash6xxk7.js} +2 -2
  37. package/dist/{cli-fp4xbw07.js → cli-axh29tyr.js} +14 -12
  38. package/dist/{cli-p22zdvnt.js → cli-bn0rewsy.js} +3 -3
  39. package/dist/{cli-hz64bcnj.js → cli-bn5kevnf.js} +11 -373
  40. package/dist/{cli-8jtrekpy.js → cli-d2mb5s8a.js} +7 -7
  41. package/dist/{cli-c5sbmw1v.js → cli-d82btycs.js} +94 -15
  42. package/dist/{cli-vjfpshn3.js → cli-ejs3m1sx.js} +1 -1
  43. package/dist/{cli-yv0vgscc.js → cli-fpmsqq59.js} +18 -2
  44. package/dist/cli-fz9kewbh.js +382 -0
  45. package/dist/{cli-4s1kqrqg.js → cli-g0hgtz4p.js} +6 -6
  46. package/dist/{cli-hn3pftj2.js → cli-gf0kfg81.js} +7 -7
  47. package/dist/{cli-d43xqzbe.js → cli-hqhzg1qq.js} +2 -2
  48. package/dist/{cli-azs9bhc5.js → cli-j4rqdf2n.js} +2 -2
  49. package/dist/{cli-wtg6edvd.js → cli-jh0qcrga.js} +1 -1
  50. package/dist/{cli-kqypty34.js → cli-k9z4zd4p.js} +6 -5
  51. package/dist/{cli-jhmjwqcg.js → cli-kmqgmcnb.js} +47 -14
  52. package/dist/{cli-8qsx817b.js → cli-kr2pmfqx.js} +583 -769
  53. package/dist/{cli-4g629sgr.js → cli-m2qgh5sv.js} +20 -1
  54. package/dist/{cli-9e9kjqsj.js → cli-mfe7f7b5.js} +2 -2
  55. package/dist/{cli-x3en6ct3.js → cli-nwyw3zhg.js} +1 -1
  56. package/dist/{cli-6vcyw6v7.js → cli-nx462e4v.js} +5 -3
  57. package/dist/{cli-tr24eeny.js → cli-p0rakbrk.js} +9 -9
  58. package/dist/{cli-4hmaheb6.js → cli-p6argxh9.js} +9 -9
  59. package/dist/{cli-3avfh5az.js → cli-pemywhfq.js} +2 -2
  60. package/dist/{cli-etvn5ej2.js → cli-pnp2j5zg.js} +2 -2
  61. package/dist/{cli-wk8ccrz6.js → cli-pw7v69vp.js} +2 -2
  62. package/dist/{cli-ka01m5rb.js → cli-rdq7tzwm.js} +17 -14
  63. package/dist/{cli-re4wkbzx.js → cli-rqqsfz30.js} +677 -104
  64. package/dist/{cli-xgn4rwyv.js → cli-s48yvzb8.js} +4 -4
  65. package/dist/{cli-zv4vy2b0.js → cli-s95dknxh.js} +1 -1
  66. package/dist/{cli-g6p7c2my.js → cli-sjyetj21.js} +2 -2
  67. package/dist/{cli-rdr166ht.js → cli-smvcw9z5.js} +1 -1
  68. package/dist/{cli-fhvqt2fp.js → cli-sv5gb0yb.js} +1 -1
  69. package/dist/{cli-yv3n83x8.js → cli-syhq378v.js} +2 -2
  70. package/dist/{cli-q1812jbw.js → cli-tbhwrxpt.js} +9 -9
  71. package/dist/{cli-w7z94e6c.js → cli-tx00c0h6.js} +4 -2
  72. package/dist/{cli-dxwh4j32.js → cli-vy0fay6k.js} +46 -37
  73. package/dist/{cli-ptkpaqfy.js → cli-wt645v7q.js} +1 -1
  74. package/dist/{cli-ppapy34w.js → cli-wxbn3yfx.js} +2 -2
  75. package/dist/{cli-cby329ny.js → cli-x5mq1fyq.js} +3 -3
  76. package/dist/{cli-rc3r0bnh.js → cli-xbzvet96.js} +6 -6
  77. package/dist/{cli-4mkvwgzr.js → cli-xf234ws8.js} +1 -1
  78. package/dist/{cli-6p7ddg72.js → cli-xftrvxag.js} +5 -5
  79. package/dist/cli-z071fb95.js +848 -0
  80. package/dist/{cli-1v8w782f.js → cli-z1ew8vbq.js} +14 -7
  81. package/dist/{cli-9td5ss65.js → cli-zfg9enh9.js} +3 -3
  82. package/dist/{cli-yxqag76b.js → cli-zw88zjba.js} +1 -1
  83. package/dist/cli.js +19 -19
  84. package/dist/{codex-adapter-nhjj2veb.js → codex-adapter-p6rmm7vv.js} +6 -6
  85. package/dist/{codex-hook-x6tm6y5m.js → codex-hook-x7nejyrr.js} +8 -5
  86. package/dist/{codex-login-35p4sk85.js → codex-login-m819c41r.js} +1 -1
  87. package/dist/{codex-session-runner-6hggrjbx.js → codex-session-runner-8vd5t9t0.js} +7 -7
  88. package/dist/{commands-mrw1ph2w.js → commands-7ems2p13.js} +2 -2
  89. package/dist/{controller-pt1zam5w.js → controller-m263fg43.js} +4 -4
  90. package/dist/{controllers-connect-ymw5541d.js → controllers-connect-bg33ae53.js} +28 -24
  91. package/dist/{cursor-adapter-cf4zh5x0.js → cursor-adapter-cz6nm6mj.js} +3 -3
  92. package/dist/{db-hy80219q.js → db-w505dwa8.js} +2 -2
  93. package/dist/{db-hhpy6m4x.js → db-ys2rbdft.js} +3 -2
  94. package/dist/{devin-adapter-7egzmt0k.js → devin-adapter-zgqadvnm.js} +1 -1
  95. package/dist/{dispatcher-6pdjtkr1.js → dispatcher-0s0mckqr.js} +20 -18
  96. package/dist/{docker-njavd3fb.js → docker-fkgtr75t.js} +2 -2
  97. package/dist/{dsh-adapter-fbx06re2.js → dsh-adapter-3n3net33.js} +3 -3
  98. package/dist/{e2b-5r6a5jeb.js → e2b-7am6tmq6.js} +2 -2
  99. package/dist/{e2b-pw1nvtmh.js → e2b-thdcxtjd.js} +2 -2
  100. package/dist/{easter-egg-3d-6hg2r942.js → easter-egg-3d-5cgynaja.js} +1 -1
  101. package/dist/{event-bus-101fenkh.js → event-bus-9vpk8gmq.js} +1 -1
  102. package/dist/{execute-bcqc69a5.js → execute-t4e6m3rw.js} +1 -1
  103. package/dist/{external-channel-j6vb6ya1.js → external-channel-5b23fv8z.js} +2 -2
  104. package/dist/{google-generative-ai-phpmwxvg.js → google-generative-ai-bmq8gkf3.js} +1 -1
  105. package/dist/{google-vertex-es9ybgdy.js → google-vertex-b1rvp52s.js} +1 -1
  106. package/dist/{grok-adapter-1193sprp.js → grok-adapter-q9fghe5q.js} +10 -10
  107. package/dist/handlers-mqrym347.js +39 -0
  108. package/dist/{heartbeat-h1w1ktce.js → heartbeat-tww6yx1w.js} +23 -21
  109. package/dist/{hook-ejja4bpe.js → hook-dx0pkvar.js} +16 -9
  110. package/dist/{hook-skrbja18.js → hook-pvmh9b6m.js} +6 -6
  111. package/dist/{http-mzh41mfj.js → http-k2ejbt05.js} +975 -292
  112. package/dist/{index-6rmnn89d.js → index-2z710v4x.js} +10 -10
  113. package/dist/{index-xmmkyx82.js → index-dgz61nff.js} +20 -18
  114. package/dist/{index-wgsw9xcz.js → index-g6tbb6ve.js} +1 -1
  115. package/dist/index-h2ek08d6.js +40 -0
  116. package/dist/index-pqfwh9nn.js +41 -0
  117. package/dist/{index-epr2gnbw.js → index-x5aktmp8.js} +1 -1
  118. package/dist/{index-x0q2a4f9.js → index-ysd45kgc.js} +1 -1
  119. package/dist/{index-g909y089.js → index-zgvqzjsw.js} +8 -8
  120. package/dist/{keepalive-4djvbm2j.js → keepalive-nhvpw10z.js} +4 -4
  121. package/dist/lead-jb6sf69z.js +78 -0
  122. package/dist/{maintenance-f3xhnjsm.js → maintenance-agakc519.js} +1 -1
  123. package/dist/{oauth-refresh-sweep-fpd6x6rw.js → oauth-refresh-sweep-4dcbhtn0.js} +3 -3
  124. package/dist/{onboard-196tjgyn.js → onboard-4n2kwa4v.js} +3 -3
  125. package/dist/{opencode-adapter-eeesgfqk.js → opencode-adapter-3s7ax02e.js} +7 -7
  126. package/dist/{otel-impl-g8fh4c21.js → otel-impl-tfa12080.js} +2 -2
  127. package/dist/{pi-mono-adapter-zasp3ze3.js → pi-mono-adapter-ytp7gz8b.js} +24 -13
  128. package/dist/{pricing-refresh-sdcf5yaz.js → pricing-refresh-3cqx3f3x.js} +3 -3
  129. package/dist/{rbac-roles-bsp5657v.js → rbac-roles-9fae5rnv.js} +5 -3
  130. package/dist/{rbac-roles-2x4g1wb5.js → rbac-roles-y6zbfzve.js} +2 -2
  131. package/dist/{render-v2-0pyb39bb.js → render-v2-emawjvc4.js} +3 -3
  132. package/dist/{resume-a86tkj6r.js → resume-zhgts7h7.js} +10 -8
  133. package/dist/{runtime-ar8310ba.js → runtime-xe3aqy4x.js} +1 -1
  134. package/dist/session-access-cm823e06.js +25 -0
  135. package/dist/{setup-ypbzcapd.js → setup-tkp99w1e.js} +1 -1
  136. package/dist/{virtual-modules-wzk6jf3a.js → virtual-modules-4r3zkc20.js} +2 -2
  137. package/dist/{work-objects-yjp8de12.js → work-objects-f722h263.js} +23 -21
  138. package/dist/worker-p7f336rq.js +78 -0
  139. package/dist/{x-8ewt8gxn.js → x-y45mthwn.js} +2 -2
  140. package/openapi.json +1086 -305
  141. package/package.json +2 -2
  142. package/src/apps/action-caller.ts +6 -0
  143. package/src/apps/action-runner.ts +146 -0
  144. package/src/apps/row-store.ts +4 -2
  145. package/src/apps/script-types.ts +17 -4
  146. package/src/be/db/active-sessions.ts +5 -0
  147. package/src/be/db/session-visibility.ts +29 -0
  148. package/src/be/db/tasks/read.ts +61 -3
  149. package/src/be/db/tasks/write.ts +15 -6
  150. package/src/be/db.ts +106 -37
  151. package/src/be/events.ts +11 -0
  152. package/src/be/fleet-benchmark-stats.ts +10 -5
  153. package/src/be/migrations/230_session_shares.sql +23 -0
  154. package/src/be/models-catalog.ts +4 -0
  155. package/src/be/onboarding.ts +6 -0
  156. package/src/be/rbac-roles.ts +2 -0
  157. package/src/be/scripts/typecheck.ts +13 -0
  158. package/src/be/session-shares.ts +244 -0
  159. package/src/be/swarm-config-guard.ts +33 -3
  160. package/src/be/telemetry-enable.ts +1 -1
  161. package/src/commands/provider-credentials.ts +12 -2
  162. package/src/commands/runner.ts +14 -0
  163. package/src/heartbeat/heartbeat.ts +2 -2
  164. package/src/hooks/codex-hook.ts +6 -2
  165. package/src/hooks/hook.ts +7 -0
  166. package/src/hooks/pr-body-guard.ts +38 -11
  167. package/src/hooks/pr-room-guard.ts +88 -0
  168. package/src/http/active-sessions.ts +13 -2
  169. package/src/http/agents.ts +11 -3
  170. package/src/http/all-routes.ts +1 -0
  171. package/src/http/api-keys.ts +3 -1
  172. package/src/http/approval-requests.ts +2 -1
  173. package/src/http/apps.ts +118 -190
  174. package/src/http/config.ts +3 -0
  175. package/src/http/context.ts +2 -0
  176. package/src/http/core.ts +39 -2
  177. package/src/http/events.ts +6 -2
  178. package/src/http/favorite-owner.ts +1 -1
  179. package/src/http/fleet-benchmark.ts +79 -30
  180. package/src/http/fs.ts +38 -3
  181. package/src/http/global-config.ts +7 -0
  182. package/src/http/index.ts +3 -1
  183. package/src/http/memory.ts +3 -1
  184. package/src/http/ranking.ts +351 -0
  185. package/src/http/realtime.ts +26 -21
  186. package/src/http/route-def.ts +77 -11
  187. package/src/http/script-runs.ts +2 -1
  188. package/src/http/scripts.ts +8 -0
  189. package/src/http/session-access.ts +179 -0
  190. package/src/http/session-data.ts +10 -2
  191. package/src/http/sessions.ts +212 -12
  192. package/src/http/stats.ts +7 -3
  193. package/src/http/tasks.ts +85 -8
  194. package/src/http/telemetry-ui.ts +1 -1
  195. package/src/http/used-by.ts +64 -20
  196. package/src/http/users.ts +2 -2
  197. package/src/prompts/session-templates.ts +3 -1
  198. package/src/providers/amp-adapter.ts +1 -1
  199. package/src/providers/pi-mono-adapter.ts +15 -0
  200. package/src/providers/reasoning-effort.ts +2 -1
  201. package/src/providers/types.ts +2 -0
  202. package/src/ranking/telemetry-ranking.ts +119 -0
  203. package/src/ranking/usage.ts +57 -0
  204. package/src/rbac/admission.ts +22 -7
  205. package/src/rbac/legacy-policy.ts +2 -0
  206. package/src/rbac/permissions.ts +9 -0
  207. package/src/realtime/auth.ts +69 -0
  208. package/src/realtime/tickets.ts +4 -1
  209. package/src/realtime/transport.ts +77 -9
  210. package/src/scripts-runtime/sdk-allowlist.ts +7 -0
  211. package/src/scripts-runtime/types/stdlib.d.ts +31 -0
  212. package/src/scripts-runtime/types/swarm-sdk.d.ts +31 -0
  213. package/src/server.ts +4 -0
  214. package/src/slack/blocks.ts +18 -0
  215. package/src/slack/dm.ts +29 -0
  216. package/src/telemetry-context.ts +19 -1
  217. package/src/telemetry-contract/EVENTS.md +45 -8
  218. package/src/telemetry-contract/SOURCE.json +38 -4
  219. package/src/telemetry-contract/catalog.json +294 -5
  220. package/src/telemetry-contract/ranking/fixtures/expected.json +30 -0
  221. package/src/telemetry-contract/ranking/fixtures/invalid/bad-category.json +11 -0
  222. package/src/telemetry-contract/ranking/fixtures/invalid/bad-claim.json +11 -0
  223. package/src/telemetry-contract/ranking/fixtures/invalid/blank-description.json +11 -0
  224. package/src/telemetry-contract/ranking/fixtures/invalid/blank-name.json +11 -0
  225. package/src/telemetry-contract/ranking/fixtures/invalid/description-too-long.json +11 -0
  226. package/src/telemetry-contract/ranking/fixtures/invalid/domain-uppercase.json +11 -0
  227. package/src/telemetry-contract/ranking/fixtures/invalid/domain-with-scheme.json +11 -0
  228. package/src/telemetry-contract/ranking/fixtures/invalid/domain-with-www.json +11 -0
  229. package/src/telemetry-contract/ranking/fixtures/invalid/http-source-url.json +11 -0
  230. package/src/telemetry-contract/ranking/fixtures/invalid/missing-description.json +10 -0
  231. package/src/telemetry-contract/ranking/fixtures/invalid/missing-version.json +10 -0
  232. package/src/telemetry-contract/ranking/fixtures/invalid/name-not-string.json +11 -0
  233. package/src/telemetry-contract/ranking/fixtures/invalid/name-too-long.json +11 -0
  234. package/src/telemetry-contract/ranking/fixtures/invalid/not-object.json +1 -0
  235. package/src/telemetry-contract/ranking/fixtures/invalid/null.json +1 -0
  236. package/src/telemetry-contract/ranking/fixtures/invalid/other-with-subcategory.json +11 -0
  237. package/src/telemetry-contract/ranking/fixtures/invalid/oversize.json +1 -0
  238. package/src/telemetry-contract/ranking/fixtures/invalid/short-claim.json +11 -0
  239. package/src/telemetry-contract/ranking/fixtures/invalid/subcategory-other-category.json +11 -0
  240. package/src/telemetry-contract/ranking/fixtures/invalid/trailing-data.json +1 -0
  241. package/src/telemetry-contract/ranking/fixtures/invalid/unknown-field.json +12 -0
  242. package/src/telemetry-contract/ranking/fixtures/invalid/unsupported-version.json +11 -0
  243. package/src/telemetry-contract/ranking/fixtures/invalid/updated-at-not-date.json +11 -0
  244. package/src/telemetry-contract/ranking/fixtures/invalid/updated-at-offset.json +11 -0
  245. package/src/telemetry-contract/ranking/fixtures/invalid/wrong-case-field.json +11 -0
  246. package/src/telemetry-contract/ranking/fixtures/valid/category-without-subcategory.json +11 -0
  247. package/src/telemetry-contract/ranking/fixtures/valid/full.json +11 -0
  248. package/src/telemetry-contract/ranking/fixtures/valid/minimal.json +11 -0
  249. package/src/telemetry-contract/ranking/fixtures/valid/no-claim.json +10 -0
  250. package/src/telemetry-contract/ranking/fixtures/valid/padded-text.json +11 -0
  251. package/src/telemetry-contract/ranking/fixtures/valid/unicode-name.json +11 -0
  252. package/src/telemetry-contract/ranking/listing.v1.schema.json +61 -0
  253. package/src/telemetry-contract/ranking/taxonomy.json +8 -0
  254. package/src/telemetry-contract/ranking-taxonomy.gen.ts +11 -0
  255. package/src/telemetry-contract/types.gen.ts +963 -58
  256. package/src/telemetry-identity.ts +47 -9
  257. package/src/telemetry-snapshot.ts +9 -1
  258. package/src/telemetry-ui-events.ts +9 -2
  259. package/src/telemetry.ts +40 -5
  260. package/src/tests/app-action-run-tool.test.ts +178 -0
  261. package/src/tests/app-row-tools.test.ts +360 -0
  262. package/src/tests/apps-rbac.test.ts +189 -2
  263. package/src/tests/apps-script-types.test.ts +16 -3
  264. package/src/tests/credential-check.test.ts +38 -0
  265. package/src/tests/db-extraction-contract.test.ts +1 -0
  266. package/src/tests/db-task-read-contract.test.ts +1 -0
  267. package/src/tests/docs-index-footprint.test.js +2 -2
  268. package/src/tests/fleet-benchmark.test.ts +122 -2
  269. package/src/tests/http-users.test.ts +62 -1
  270. package/src/tests/model-catalog-table.test.ts +63 -0
  271. package/src/tests/model-profiles-api.test.ts +19 -7
  272. package/src/tests/models-catalog.test.ts +50 -0
  273. package/src/tests/onboarding.test.ts +21 -0
  274. package/src/tests/org-identity.test.ts +44 -0
  275. package/src/tests/org-website.test.ts +62 -0
  276. package/src/tests/pi-mono-adapter.test.ts +23 -0
  277. package/src/tests/pr-body-leaks.test.ts +4 -0
  278. package/src/tests/pr-room-guard.test.ts +242 -0
  279. package/src/tests/ranking-listing.test.ts +97 -0
  280. package/src/tests/ranking-route.test.ts +321 -0
  281. package/src/tests/ranking-usage.test.ts +50 -0
  282. package/src/tests/rbac-admission-e2e.test.ts +8 -0
  283. package/src/tests/rbac-admission.test.ts +38 -0
  284. package/src/tests/rbac-charact-http.test.ts +4 -3
  285. package/src/tests/rbac-engine.test.ts +2 -0
  286. package/src/tests/rbac-roles.test.ts +2 -0
  287. package/src/tests/realtime-room-surfaces.test.ts +3 -1
  288. package/src/tests/realtime-session-join-race.test.ts +142 -0
  289. package/src/tests/realtime-tickets.test.ts +24 -17
  290. package/src/tests/realtime-transport.test.ts +91 -0
  291. package/src/tests/reasoning-effort.test.ts +39 -3
  292. package/src/tests/runner-harness-telemetry-env.test.ts +56 -1
  293. package/src/tests/session-route-posture.test.ts +639 -0
  294. package/src/tests/session-sharing-routes.test.ts +282 -0
  295. package/src/tests/session-sharing.test.ts +526 -0
  296. package/src/tests/sessions-detail-batch.test.ts +7 -1
  297. package/src/tests/slack-tool-blocks.test.ts +101 -0
  298. package/src/tests/slack-update.test.ts +55 -0
  299. package/src/tests/slack-user-dm.test.ts +177 -0
  300. package/src/tests/swarm-tool-result-gate.test.ts +2 -4
  301. package/src/tests/telemetry-context.test.ts +128 -2
  302. package/src/tests/telemetry-ui-route.test.ts +123 -0
  303. package/src/tests/tool-annotations.test.ts +3 -2
  304. package/src/tests/used-by-route.test.ts +153 -9
  305. package/src/tests/workflow-agent-task-retry.test.ts +130 -4
  306. package/src/tests/workflow-wait-recovery.test.ts +116 -4
  307. package/src/tools/app-action-run.ts +63 -0
  308. package/src/tools/app-rows.ts +234 -0
  309. package/src/tools/model-catalog/index.ts +1 -1
  310. package/src/tools/slack-post.ts +18 -12
  311. package/src/tools/slack-read.ts +13 -1
  312. package/src/tools/slack-reply.ts +7 -10
  313. package/src/tools/slack-start-thread.ts +18 -11
  314. package/src/tools/slack-update.ts +2 -9
  315. package/src/tools/tool-config.ts +8 -0
  316. package/src/tools/utils.ts +1 -1
  317. package/src/tools/workflows/retry-workflow-run.ts +1 -1
  318. package/src/types.ts +52 -0
  319. package/src/utils/company-profile.ts +24 -0
  320. package/src/utils/org-identity.ts +33 -0
  321. package/src/utils/org-website.ts +58 -21
  322. package/src/workflows/recovery.ts +10 -22
  323. package/src/workflows/resume.ts +16 -0
  324. package/src/workflows/task-step-routing.ts +47 -7
  325. package/templates/skills/apps/SKILL.md +58 -2
  326. package/templates/skills/apps/content.md +58 -2
  327. package/dist/handlers-tb3xgjnm.js +0 -37
  328. package/dist/index-nyfcdpky.js +0 -39
  329. package/dist/index-vjn3wbzf.js +0 -38
  330. package/dist/lead-61kqp604.js +0 -76
  331. package/dist/worker-4ptnn1ge.js +0 -76
  332. package/dist/{cli-n5mrsg0f.js → cli-60cgh1rv.js} +3 -3
@@ -128,6 +128,61 @@ describe("slack-update", () => {
128
128
  });
129
129
  });
130
130
 
131
+ test("splits a long message into sections under Slack's cap", async () => {
132
+ const message = Array.from(
133
+ { length: 120 },
134
+ (_, index) => `line ${String(index).padStart(3, "0")} radar item with some words`,
135
+ )
136
+ .join("\n")
137
+ .slice(0, 3946);
138
+ const tool = buildTool();
139
+ const result = await tool.handler(
140
+ { channelId: "C1", messageTs: "1783411554.596189", message },
141
+ meta(leadAgentId),
142
+ );
143
+
144
+ expect(result.structuredContent.success).toBe(true);
145
+ const payload = mockChatUpdate.mock.calls[0][0] as {
146
+ text: string;
147
+ blocks: { type: string; text: { type: string; text: string } }[];
148
+ };
149
+ expect(payload.text).toBe(message);
150
+ expect(payload.blocks.length).toBeGreaterThanOrEqual(2);
151
+ for (const block of payload.blocks) {
152
+ expect(block.type).toBe("section");
153
+ expect(block.text.text.length).toBeLessThanOrEqual(2900);
154
+ }
155
+ const joined = payload.blocks.map((block) => block.text.text).join("");
156
+ expect(joined.replace(/\s/g, "")).toBe(message.replace(/\s/g, ""));
157
+ });
158
+
159
+ test("keeps a 2,931-char message with an emoji at 2,900 in one intact section", async () => {
160
+ const message = `${"x".repeat(2899)}😀${"y".repeat(30)}`;
161
+ expect(message.length).toBe(2931);
162
+ const tool = buildTool();
163
+ const result = await tool.handler(
164
+ { channelId: "C1", messageTs: "1783411554.596189", message },
165
+ meta(leadAgentId),
166
+ );
167
+
168
+ expect(result.structuredContent.success).toBe(true);
169
+ expect(mockChatUpdate.mock.calls[0][0].blocks).toEqual([
170
+ { type: "section", text: { type: "mrkdwn", text: message } },
171
+ ]);
172
+ });
173
+
174
+ test("keeps a short message in exactly one section", async () => {
175
+ const tool = buildTool();
176
+ await tool.handler(
177
+ { channelId: "C1", messageTs: "1783411554.596189", message: "short" },
178
+ meta(leadAgentId),
179
+ );
180
+
181
+ expect(mockChatUpdate.mock.calls[0][0]).toMatchObject({
182
+ blocks: [{ type: "section", text: { type: "mrkdwn", text: "short" } }],
183
+ });
184
+ });
185
+
131
186
  test.each([
132
187
  ["message_not_found", "No message found at that timestamp in this channel."],
133
188
  [
@@ -0,0 +1,177 @@
1
+ import { afterAll, beforeAll, beforeEach, describe, expect, mock, test } from "bun:test";
2
+ import { unlink } from "node:fs/promises";
3
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
+ import { closeDb, createAgent, getSlackMessageByChannelTs, initDb } from "../be/db";
5
+ import { isSlackUserId } from "../slack/dm";
6
+ import { registerSlackPostTool } from "../tools/slack-post";
7
+ import { registerSlackReadTool } from "../tools/slack-read";
8
+ import { registerSlackStartThreadTool } from "../tools/slack-start-thread";
9
+
10
+ const TEST_DB_PATH = "./test-slack-user-dm.sqlite";
11
+ const USER_ID = "U08USERDM";
12
+ // The bot's own IM with USER_ID: readable, writable, delivers message.im events.
13
+ const BOT_DM = "D0BOTDM01";
14
+ // Where Slack puts a bot post addressed to the bare user id: the bot is not a member.
15
+ const FOREIGN_DM = "D0FOREIGN";
16
+
17
+ function slackError(code: string): Error {
18
+ return Object.assign(new Error(`An API error occurred: ${code}`), {
19
+ data: { ok: false, error: code },
20
+ });
21
+ }
22
+
23
+ let tsCounter = 0;
24
+ const postMessage = mock(async (args: { channel: string; thread_ts?: string }) => {
25
+ if (args.channel === FOREIGN_DM) throw slackError("channel_not_found");
26
+ const ts = `500.${++tsCounter}`;
27
+ return { ok: true, ts, channel: args.channel === USER_ID ? FOREIGN_DM : args.channel };
28
+ });
29
+ const conversationsOpen = mock(async (args: { users: string }) => {
30
+ if (args.users !== USER_ID) throw slackError("user_not_found");
31
+ return { ok: true, channel: { id: BOT_DM } };
32
+ });
33
+ const conversationsReplies = mock(async (args: { channel: string; ts: string }) => {
34
+ if (args.channel !== BOT_DM) throw slackError("channel_not_found");
35
+ return { ok: true, messages: [{ user: USER_ID, text: "reply from human", ts: args.ts }] };
36
+ });
37
+
38
+ mock.module("../slack/app", () => ({
39
+ getSlackApp: () => ({
40
+ client: {
41
+ chat: { postMessage },
42
+ conversations: { open: conversationsOpen, replies: conversationsReplies },
43
+ auth: { test: async () => ({ ok: true, user_id: "UBOT" }) },
44
+ users: { info: async () => ({ ok: true, user: { real_name: "Human" } }) },
45
+ },
46
+ }),
47
+ }));
48
+
49
+ type ToolResult = {
50
+ structuredContent: {
51
+ success: boolean;
52
+ message: string;
53
+ channelId?: string;
54
+ ts?: string;
55
+ messages?: Array<{ text: string }>;
56
+ };
57
+ };
58
+ type RegisteredTool = { handler: (args: unknown, extra: unknown) => Promise<ToolResult> };
59
+
60
+ function buildTools() {
61
+ const server = new McpServer({ name: "slack-user-dm-test", version: "1.0.0" });
62
+ registerSlackPostTool(server);
63
+ registerSlackReadTool(server);
64
+ registerSlackStartThreadTool(server);
65
+ return (server as unknown as { _registeredTools: Record<string, RegisteredTool> })
66
+ ._registeredTools;
67
+ }
68
+
69
+ function meta(agentId: string) {
70
+ return { sessionId: "s1", requestInfo: { headers: { "x-agent-id": agentId } } };
71
+ }
72
+
73
+ async function removeDbFiles() {
74
+ for (const suffix of ["", "-wal", "-shm"]) {
75
+ try {
76
+ await unlink(`${TEST_DB_PATH}${suffix}`);
77
+ } catch {}
78
+ }
79
+ }
80
+
81
+ let leadId: string;
82
+
83
+ beforeAll(async () => {
84
+ await removeDbFiles();
85
+ initDb(TEST_DB_PATH);
86
+ leadId = (await createAgent({ name: "Lead DM", isLead: true, status: "idle" })).id;
87
+ });
88
+
89
+ beforeEach(() => {
90
+ postMessage.mockClear();
91
+ conversationsOpen.mockClear();
92
+ conversationsReplies.mockClear();
93
+ });
94
+
95
+ afterAll(async () => {
96
+ closeDb();
97
+ await removeDbFiles();
98
+ });
99
+
100
+ describe("Slack tools addressed with a user id", () => {
101
+ test("isSlackUserId matches user ids only", () => {
102
+ expect(isSlackUserId("U08NR6QD6CS")).toBe(true);
103
+ expect(isSlackUserId("W012ABCDEF")).toBe(true);
104
+ expect(isSlackUserId("D0A4T383FMF")).toBe(false);
105
+ expect(isSlackUserId("C0AR967K0KZ")).toBe(false);
106
+ expect(isSlackUserId("G01PRIVATE")).toBe(false);
107
+ });
108
+
109
+ test("slack-start-thread opens the bot DM and returns an id that reads and writes work on", async () => {
110
+ const tools = buildTools();
111
+
112
+ const started = await tools["slack-start-thread"]!.handler(
113
+ { channelId: USER_ID, message: "hello" },
114
+ meta(leadId),
115
+ );
116
+ expect(started.structuredContent.success).toBe(true);
117
+ expect(conversationsOpen).toHaveBeenCalledWith({ users: USER_ID });
118
+ expect(postMessage.mock.calls[0]![0].channel).toBe(BOT_DM);
119
+ expect(started.structuredContent.channelId).toBe(BOT_DM);
120
+ const threadTs = started.structuredContent.ts!;
121
+ // The recorded message points at the bot DM, so inbound replies match it.
122
+ expect((await getSlackMessageByChannelTs(BOT_DM, threadTs))?.threadTs).toBe(threadTs);
123
+
124
+ const posted = await tools["slack-post"]!.handler(
125
+ { channelId: BOT_DM, threadTs, message: "follow-up" },
126
+ meta(leadId),
127
+ );
128
+ expect(posted.structuredContent.success).toBe(true);
129
+ expect(postMessage.mock.calls[1]![0]).toMatchObject({ channel: BOT_DM, thread_ts: threadTs });
130
+
131
+ const read = await tools["slack-read"]!.handler({ channelId: BOT_DM, threadTs }, meta(leadId));
132
+ expect(read.structuredContent.success).toBe(true);
133
+ expect(read.structuredContent.messages?.[0]?.text).toContain("reply from human");
134
+ });
135
+
136
+ test("slack-post and slack-read with the user id resolve to the same bot DM", async () => {
137
+ const tools = buildTools();
138
+
139
+ const posted = await tools["slack-post"]!.handler(
140
+ { channelId: USER_ID, threadTs: "500.1", message: "by user id" },
141
+ meta(leadId),
142
+ );
143
+ expect(posted.structuredContent.success).toBe(true);
144
+ expect(postMessage.mock.calls[0]![0]).toMatchObject({ channel: BOT_DM, thread_ts: "500.1" });
145
+
146
+ const read = await tools["slack-read"]!.handler(
147
+ { channelId: USER_ID, threadTs: "500.1" },
148
+ meta(leadId),
149
+ );
150
+ expect(read.structuredContent.success).toBe(true);
151
+ expect(conversationsReplies.mock.calls[0]![0].channel).toBe(BOT_DM);
152
+ });
153
+
154
+ test("conversation ids pass through without opening a DM", async () => {
155
+ const tools = buildTools();
156
+
157
+ const started = await tools["slack-start-thread"]!.handler(
158
+ { channelId: "C0CHANNEL1", message: "channel post" },
159
+ meta(leadId),
160
+ );
161
+ expect(started.structuredContent.channelId).toBe("C0CHANNEL1");
162
+ expect(postMessage.mock.calls[0]![0].channel).toBe("C0CHANNEL1");
163
+ expect(conversationsOpen).not.toHaveBeenCalled();
164
+ });
165
+
166
+ test("a user id Slack cannot open a DM with fails without posting", async () => {
167
+ const tools = buildTools();
168
+
169
+ const started = await tools["slack-start-thread"]!.handler(
170
+ { channelId: "U0NOBODY1", message: "hello" },
171
+ meta(leadId),
172
+ );
173
+ expect(started.structuredContent.success).toBe(false);
174
+ expect(started.structuredContent.message).toContain("user_not_found");
175
+ expect(postMessage).not.toHaveBeenCalled();
176
+ });
177
+ });
@@ -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,
@@ -1,11 +1,13 @@
1
1
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
2
  import { unlink } from "node:fs/promises";
3
- import { closeDb, createUser, initDb } from "../be/db";
3
+ import { closeDb, createUser, getDbClient, initDb, updateUser, upsertSwarmConfig } from "../be/db";
4
4
  import { attachRole, detachRole } from "../be/rbac-roles";
5
+ import { loadGlobalConfigsIntoEnv } from "../http/core";
5
6
  import {
6
7
  _getOrgIdForTests,
7
8
  _resetTelemetryStateForTests,
8
9
  initTelemetry,
10
+ setTelemetryFunctionResolver,
9
11
  setTelemetryOrgDomain,
10
12
  track,
11
13
  } from "../telemetry";
@@ -19,12 +21,22 @@ import {
19
21
  validOrgDomain,
20
22
  validOrgId,
21
23
  } from "../telemetry-context";
22
- import { _resetTelemetryIdentityForTests, computeOrgDomain } from "../telemetry-identity";
24
+ import {
25
+ _resetTelemetryIdentityForTests,
26
+ cachedUserFunction,
27
+ computeOrgDomain,
28
+ primeUserRole,
29
+ } from "../telemetry-identity";
23
30
 
24
31
  // initTelemetry no-ops when ANONYMIZED_TELEMETRY=false, and the CI env may set it.
25
32
  process.env.ANONYMIZED_TELEMETRY = "true";
26
33
 
27
34
  const TEST_DB_PATH = "./test-telemetry-context.sqlite";
35
+ const PROFILE_KEYS = [
36
+ "SWARM_ORG_TEAM_SIZE",
37
+ "SWARM_ORG_USE_CASE",
38
+ "SWARM_ORG_ACQUISITION_SOURCE",
39
+ ] as const;
28
40
 
29
41
  async function removeTestDb(): Promise<void> {
30
42
  for (const suffix of ["", "-wal", "-shm"]) {
@@ -116,6 +128,30 @@ describe("telemetry context helpers", () => {
116
128
  expect(mapTriggerSurface(null)).toBe("other");
117
129
  });
118
130
 
131
+ test("buildContext omits invalid profile values and a function without an actor", () => {
132
+ const base = {
133
+ orgId: "org_0123456789abcdef",
134
+ installationId: "install_test",
135
+ isCloud: false,
136
+ isE2b: false,
137
+ swarmVersion: "1.2.3",
138
+ };
139
+ const context = buildContext({
140
+ ...base,
141
+ teamSize: "invalid",
142
+ useCase: "invalid",
143
+ acquisitionSource: "invalid",
144
+ userFunction: "founder",
145
+ });
146
+ expect(context).not.toHaveProperty("team_size");
147
+ expect(context).not.toHaveProperty("use_case");
148
+ expect(context).not.toHaveProperty("user_function");
149
+ expect(context.acquisition_source).toBeNull();
150
+ expect(
151
+ buildContext({ ...base, actor: { userId: "u-1" }, userFunction: "administrator" }),
152
+ ).not.toHaveProperty("user_function");
153
+ });
154
+
119
155
  test("normalizeProperties keeps an event alive when one optional value is unsendable", () => {
120
156
  const out = normalizeProperties("task.completed", {
121
157
  taskId: "t-1",
@@ -248,6 +284,19 @@ describe("telemetry envelope", () => {
248
284
  }
249
285
  });
250
286
 
287
+ test("track resolves the actor's job function without changing the RBAC role", async () => {
288
+ const config = memoryConfig({ telemetry_installation_id: "install_function" });
289
+ await initTelemetry("api-server", config.get, config.set, { generateIfMissing: true });
290
+ setTelemetryFunctionResolver(() => "founder");
291
+ track({
292
+ event: "workflow.deleted",
293
+ properties: { workflowId: "wf-1" },
294
+ actor: { userId: "u-1", role: "member" },
295
+ });
296
+ await tick();
297
+ expect(sent[0]?.context).toMatchObject({ user_function: "founder", user_role: "member" });
298
+ });
299
+
251
300
  test("an org with no ID yet sends nothing instead of a half-identified event", async () => {
252
301
  // A worker on an old server: installation ID known, no org ID stored.
253
302
  const config = memoryConfig({ telemetry_installation_id: "install_noorg" });
@@ -270,18 +319,95 @@ describe("telemetry envelope", () => {
270
319
  });
271
320
 
272
321
  describe("org email domain", () => {
322
+ const savedProfile = Object.fromEntries(PROFILE_KEYS.map((key) => [key, process.env[key]]));
323
+
273
324
  beforeEach(async () => {
274
325
  closeDb();
275
326
  await removeTestDb();
276
327
  initDb(TEST_DB_PATH);
277
328
  _resetTelemetryIdentityForTests();
329
+ for (const key of PROFILE_KEYS) delete process.env[key];
278
330
  });
279
331
 
280
332
  afterEach(async () => {
333
+ for (const key of PROFILE_KEYS) {
334
+ if (savedProfile[key] === undefined) delete process.env[key];
335
+ else process.env[key] = savedProfile[key];
336
+ }
281
337
  closeDb();
282
338
  await removeTestDb();
283
339
  });
284
340
 
341
+ test("stored company profile reaches context after reload and clears without restart", async () => {
342
+ const values = ["11-50", "features", "hacker_news"];
343
+ for (const [index, key] of PROFILE_KEYS.entries()) {
344
+ await upsertSwarmConfig({ scope: "global", key, value: values[index] as string });
345
+ }
346
+ await loadGlobalConfigsIntoEnv(true);
347
+ const originalFetch = globalThis.fetch;
348
+ const sent: Array<{ context: Record<string, unknown> }> = [];
349
+ globalThis.fetch = (async (_url: string, init?: { body?: string }) => {
350
+ if (init?.body) sent.push(JSON.parse(init.body));
351
+ return new Response(null, { status: 204 });
352
+ }) as typeof fetch;
353
+ _resetTelemetryStateForTests();
354
+ try {
355
+ const config = memoryConfig({
356
+ telemetry_installation_id: "install_profile",
357
+ telemetry_org_id: "org_0123456789abcdef",
358
+ });
359
+ await initTelemetry("api-server", config.get, config.set);
360
+ track({ event: "server.started", properties: { port: 3013 } });
361
+ expect(sent).toHaveLength(1);
362
+ expect(sent[0]?.context).toMatchObject({
363
+ team_size: "11-50",
364
+ use_case: "features",
365
+ acquisition_source: "hacker_news",
366
+ });
367
+ for (const key of PROFILE_KEYS) {
368
+ await upsertSwarmConfig({ scope: "global", key, value: "" });
369
+ }
370
+ await loadGlobalConfigsIntoEnv(true);
371
+ track({ event: "server.started", properties: { port: 3013 } });
372
+ expect(sent).toHaveLength(2);
373
+ expect(sent[1]?.context).not.toHaveProperty("team_size");
374
+ expect(sent[1]?.context).not.toHaveProperty("use_case");
375
+ expect(sent[1]?.context.acquisition_source).toBeNull();
376
+ } finally {
377
+ globalThis.fetch = originalFetch;
378
+ _resetTelemetryStateForTests();
379
+ }
380
+ });
381
+
382
+ test("job function uses the role cache and invalidates after a committed user update", async () => {
383
+ const user = await createUser({
384
+ name: "Founder",
385
+ role: "admin",
386
+ metadata: { jobFunction: "founder" },
387
+ });
388
+ await primeUserRole(user.id);
389
+ expect(cachedUserFunction(user.id)).toBe("founder");
390
+ await getDbClient().transaction(async () => {
391
+ await updateUser(user.id, { metadata: { jobFunction: "engineer" } });
392
+ expect(cachedUserFunction(user.id)).toBe("founder");
393
+ });
394
+ await primeUserRole(user.id);
395
+ expect(cachedUserFunction(user.id)).toBe("engineer");
396
+ await updateUser(user.id, { metadata: { jobFunction: "a free-text legacy role" } });
397
+ await primeUserRole(user.id);
398
+ expect(cachedUserFunction(user.id)).toBeUndefined();
399
+ const context = buildContext({
400
+ orgId: "org_0123456789abcdef",
401
+ installationId: "install_profile",
402
+ isCloud: false,
403
+ isE2b: false,
404
+ swarmVersion: "1.2.3",
405
+ actor: { userId: user.id },
406
+ userFunction: cachedUserFunction(user.id),
407
+ });
408
+ expect(context).not.toHaveProperty("user_function");
409
+ });
410
+
285
411
  test("is undefined with no users", async () => {
286
412
  expect(await computeOrgDomain()).toBeUndefined();
287
413
  });
@@ -5,6 +5,7 @@ import { isPublicRoute } from "../http/route-def";
5
5
  import { handleTelemetryUi } from "../http/telemetry-ui";
6
6
  import { getPathSegments, parseQueryParams } from "../http/utils";
7
7
  import { _resetTelemetryStateForTests, initTelemetry } from "../telemetry";
8
+ import catalog from "../telemetry-contract/catalog.json";
8
9
  import { UI_EVENT_NAMES, validateUiEvent } from "../telemetry-ui-events";
9
10
  import type { HttpRequestAuth } from "../utils/request-auth-context";
10
11
  import { setRequestAuth } from "../utils/request-auth-context";
@@ -118,6 +119,128 @@ describe("POST /api/telemetry/ui-event", () => {
118
119
  expect(sent).toHaveLength(0);
119
120
  });
120
121
 
122
+ test.each([
123
+ { event: "identity.lookup", properties: { matched: true } },
124
+ ...["identity_modal", "setup"].map((surface) => ({
125
+ event: "identity.user_created",
126
+ properties: { surface, email_kind: "work", has_function: true, has_company: true },
127
+ })),
128
+ {
129
+ event: "fleet_benchmark.unlocked",
130
+ properties: { fields: ["website", "team_size", "use_case"] },
131
+ },
132
+ ...["compose", "docker", "kubernetes"].map((target) => ({
133
+ event: "setup.worker_command_copied",
134
+ properties: { target, harness: "codex" },
135
+ })),
136
+ ...[
137
+ "prompt",
138
+ "setup",
139
+ "settings",
140
+ "org_name_popup",
141
+ "feedback",
142
+ "fleet_benchmark",
143
+ "telemetry_banner",
144
+ "identity_modal",
145
+ ].map((surface) => ({
146
+ event: "org.profile_updated",
147
+ properties: {
148
+ fields: ["name", "website", "team_size", "use_case", "acquisition_source"],
149
+ surface,
150
+ },
151
+ })),
152
+ {
153
+ event: "popup.submitted",
154
+ properties: {
155
+ popup: "feedback",
156
+ has_message: false,
157
+ has_contact: false,
158
+ answered: 3,
159
+ perks: ["used_by", "slack_connect"],
160
+ },
161
+ },
162
+ ...["notification.viewed", "notification.dismissed", "notification.submitted"].flatMap(
163
+ (event) =>
164
+ ["task_failed", "first_workflow", "default"].map((moment) => ({
165
+ event,
166
+ properties: {
167
+ key: "slack_connect_invite",
168
+ moment,
169
+ ...(event === "notification.submitted" ? { user_function: "engineer" } : {}),
170
+ },
171
+ })),
172
+ ),
173
+ ])("accepts the company-profile catalog event $event with $properties", async ({
174
+ event,
175
+ properties,
176
+ }) => {
177
+ const result = await post({ event, properties });
178
+ await flush();
179
+ expect(result.statusCode).toBe(204);
180
+ expect(sent).toHaveLength(1);
181
+ expect(sent[0]).toMatchObject({ event, source: "ui", properties });
182
+ });
183
+
184
+ test.each([
185
+ {
186
+ event: "identity.user_created",
187
+ properties: {
188
+ surface: "invalid",
189
+ email_kind: "work",
190
+ has_function: true,
191
+ has_company: false,
192
+ },
193
+ },
194
+ { event: "setup.worker_command_copied", properties: { target: "invalid", harness: "codex" } },
195
+ { event: "org.profile_updated", properties: { fields: ["team_size"], surface: "invalid" } },
196
+ { event: "org.profile_updated", properties: { fields: ["invalid"], surface: "settings" } },
197
+ { event: "fleet_benchmark.unlocked", properties: { fields: ["invalid"] } },
198
+ {
199
+ event: "popup.submitted",
200
+ properties: { popup: "feedback", has_message: false, has_contact: false, perks: ["invalid"] },
201
+ },
202
+ {
203
+ event: "notification.submitted",
204
+ properties: { key: "slack_connect_invite", moment: "invalid" },
205
+ },
206
+ {
207
+ event: "notification.submitted",
208
+ properties: { key: "slack_connect_invite", user_function: "invalid" },
209
+ },
210
+ ])("rejects a bad enum in $event with $properties", async ({ event, properties }) => {
211
+ const result = await post({ event, properties });
212
+ expect(result.statusCode).toBe(400);
213
+ expect(sent).toHaveLength(0);
214
+ });
215
+
216
+ test("array item references enforce the referenced enum", async () => {
217
+ const entry = catalog.events.find((entry) => entry.event === "fleet_benchmark.unlocked")!;
218
+ const fields = entry.properties.fields!;
219
+ const original = fields.items;
220
+ // Exercise a referenced item definition without changing the vendored catalog.
221
+ Object.assign(fields, { items: { ref: "use_case" } });
222
+ try {
223
+ expect(
224
+ (
225
+ await post({
226
+ event: "fleet_benchmark.unlocked",
227
+ properties: { fields: ["features"] },
228
+ })
229
+ ).statusCode,
230
+ ).toBe(204);
231
+ sent = [];
232
+ const result = await post({
233
+ event: "fleet_benchmark.unlocked",
234
+ properties: { fields: ["invalid"] },
235
+ });
236
+ expect(result.statusCode).toBe(400);
237
+ expect(result.body).toContain("items must be one of");
238
+ expect(sent).toHaveLength(0);
239
+ } finally {
240
+ fields.items = original;
241
+ }
242
+ });
243
+
121
244
  test("an unknown event name gets 400 and sends nothing", async () => {
122
245
  const result = await post({ event: "task.created", properties: {} });
123
246
  await flush();
@@ -376,9 +376,10 @@ describe("Tool Annotations & Classification", () => {
376
376
  // (this suite forces ALL_CAPABILITIES, including default-disabled ones).
377
377
  // Includes 11 skill tools, 7 MCP server tools, reusable script tools, and the
378
378
  // native Kapso/WhatsApp tools (register/unregister number + send/reply message),
379
- // and the six Chat SDK tools (chat-* + register/unregister-chat-channel).
379
+ // the six Chat SDK tools (chat-* + register/unregister-chat-channel),
380
+ // and the seven app row tools plus app-action-run.
380
381
  expect(count).toBeGreaterThanOrEqual(45);
381
- expect(count).toBeLessThanOrEqual(163);
382
+ expect(count).toBeLessThanOrEqual(169);
382
383
  });
383
384
 
384
385
  test("core tools are fewer than deferred tools", () => {