@desplega.ai/agent-swarm 2.4.4 → 2.5.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 (382) hide show
  1. package/README.md +4 -2
  2. package/dist/{acp-adapter-thq04adh.js → acp-adapter-11gp67nr.js} +8 -6
  3. package/dist/{actions-j6ywh6x5.js → actions-8p2kze8j.js} +24 -22
  4. package/dist/{amp-adapter-k8b5xmj8.js → amp-adapter-z4qvhacj.js} +2 -2
  5. package/dist/{anthropic-messages-g6pqmqk4.js → anthropic-messages-txs190dd.js} +17 -20
  6. package/dist/{app-rb7vc02s.js → app-0qp0a2gk.js} +3 -3
  7. package/dist/{assistant-ftta1zjf.js → assistant-jedd3ktm.js} +27 -25
  8. package/dist/{azure-openai-responses-9dbmj346.js → azure-openai-responses-9kfwhmn7.js} +18 -13
  9. package/dist/{boot-reembed-xns7h5h9.js → boot-reembed-4xwq5tg0.js} +1 -1
  10. package/dist/{boot-reembed-srp6v6af.js → boot-reembed-n4wq5dd2.js} +1 -1
  11. package/dist/{boot-scrub-sweep-3qqrh2mz.js → boot-scrub-sweep-69h90xcq.js} +4 -4
  12. package/dist/{boot-seeds-9ks8hce7.js → boot-seeds-3vn0m9sg.js} +4 -4
  13. package/dist/{claude-adapter-k5ksyres.js → claude-adapter-221mvad5.js} +8 -6
  14. package/dist/{claude-managed-adapter-h2y889ge.js → claude-managed-adapter-gt6pa35d.js} +2 -2
  15. package/dist/{claude-managed-setup-n5a5eqks.js → claude-managed-setup-c3hp36ph.js} +4 -4
  16. package/dist/{claude-sdk-session-2j3j4kwn.js → claude-sdk-session-8thjsk5e.js} +9729 -9628
  17. package/dist/{cli-b9fcxxev.js → cli-007cnhm1.js} +50 -4
  18. package/dist/{cli-j4rqdf2n.js → cli-03vpq45j.js} +2 -2
  19. package/dist/{cli-amyhm3gj.js → cli-0y6f0ckq.js} +1 -1
  20. package/dist/{cli-4dg55wsk.js → cli-0z3vjrpm.js} +1 -1
  21. package/dist/{cli-pccfmtb5.js → cli-179xcyky.js} +11 -11
  22. package/dist/{cli-3r8samv3.js → cli-1f0gp9vz.js} +3 -2
  23. package/dist/{cli-hj3debgw.js → cli-1pr672hz.js} +3 -3
  24. package/dist/{cli-0bqteszy.js → cli-2nf3b08t.js} +328 -25
  25. package/dist/{cli-t7cyef5g.js → cli-36j4jnk3.js} +4 -4
  26. package/dist/{cli-er83cjbn.js → cli-3qv8naq8.js} +527 -231
  27. package/dist/cli-3zwye9sn.js +1193 -0
  28. package/dist/{cli-60cgh1rv.js → cli-459fk97x.js} +7 -7
  29. package/dist/{cli-xj0re5tm.js → cli-4w24f7z3.js} +1 -1
  30. package/dist/{cli-v3yr2gbg.js → cli-5kzk3472.js} +2198 -968
  31. package/dist/{cli-h7gc0b4r.js → cli-5m84zd2k.js} +55 -18
  32. package/dist/{cli-w6nadkh4.js → cli-5rvg1ddk.js} +5 -5
  33. package/dist/{cli-dvkmp9bt.js → cli-6ctwrf1z.js} +20 -10
  34. package/dist/{cli-9qtjfd60.js → cli-6rg2jn2y.js} +3 -2
  35. package/dist/{cli-2v045xyf.js → cli-7bwtc6yx.js} +3 -3
  36. package/dist/{cli-3cgngyd8.js → cli-7g40g8hq.js} +35 -17
  37. package/dist/{cli-q6r7t299.js → cli-92mqenkc.js} +2379 -154
  38. package/dist/{cli-cynvc9fj.js → cli-9ce2xw98.js} +8 -8
  39. package/dist/{cli-gqvh8d8a.js → cli-bgdtv62r.js} +36 -22
  40. package/dist/{cli-wxbn3yfx.js → cli-cz5nmdxt.js} +2 -2
  41. package/dist/{cli-wt645v7q.js → cli-dabehxhp.js} +1 -1
  42. package/dist/{cli-vfha21c9.js → cli-e5bxnjps.js} +2 -2
  43. package/dist/{cli-p20r8w42.js → cli-fcc5qe3f.js} +763 -268
  44. package/dist/{cli-1ephgwnq.js → cli-fes51ya2.js} +3 -3
  45. package/dist/{cli-1d70crsy.js → cli-ftmq4nvk.js} +2 -2
  46. package/dist/{cli-qkhc5aqy.js → cli-fy1jyyym.js} +13 -13
  47. package/dist/{cli-s95dknxh.js → cli-g1cw7xtt.js} +1 -1
  48. package/dist/{cli-qje9k2nh.js → cli-g9ypd5en.js} +12 -2
  49. package/dist/{cli-74c1wpm0.js → cli-gpv60278.js} +1 -1
  50. package/dist/{cli-pemywhfq.js → cli-ha9rmwgq.js} +2 -1
  51. package/dist/{cli-bnz0r3kc.js → cli-j1eghqfm.js} +5 -5
  52. package/dist/{cli-2zqpkvtt.js → cli-jpxsrxcb.js} +1 -1
  53. package/dist/{cli-ejs3m1sx.js → cli-kxpfx2xr.js} +1 -1
  54. package/dist/{cli-37yb7307.js → cli-mbfj11fq.js} +1 -1
  55. package/dist/{cli-pnp2j5zg.js → cli-mmhj8vd7.js} +2 -2
  56. package/dist/{cli-xf234ws8.js → cli-n4c8e3s5.js} +2 -0
  57. package/dist/{cli-a0kcty51.js → cli-n6kmqngw.js} +2 -2
  58. package/dist/{cli-te6b5wvg.js → cli-nnvjb17s.js} +3 -3
  59. package/dist/{cli-1dmcq2eh.js → cli-nzfnt2gs.js} +1 -1
  60. package/dist/{cli-625tb7aq.js → cli-p859hcc8.js} +2 -2
  61. package/dist/{cli-smvcw9z5.js → cli-pasyvjmm.js} +1 -1
  62. package/dist/cli-pkaqk57w.js +128 -0
  63. package/dist/{cli-8nyhf7za.js → cli-qqzmjxkf.js} +129 -105
  64. package/dist/{cli-zrwpctp4.js → cli-r25qpaxt.js} +161 -63
  65. package/dist/{cli-nc35pxb7.js → cli-rh5smt8t.js} +19 -4
  66. package/dist/{cli-qwkcwjdn.js → cli-rycdaj0a.js} +1 -1
  67. package/dist/{cli-esxerzg7.js → cli-s4sntwt4.js} +2 -2
  68. package/dist/{cli-b8k7epj1.js → cli-segs7hx9.js} +8 -8
  69. package/dist/{cli-36ckw0wx.js → cli-stxdrqs2.js} +2 -2
  70. package/dist/{cli-vdm20wej.js → cli-sy8f9705.js} +4 -4
  71. package/dist/{cli-1m17dc9d.js → cli-ta8bd7th.js} +2 -2
  72. package/dist/{cli-2y91wnbh.js → cli-we0ra057.js} +222 -10
  73. package/dist/{cli-55qmbp6z.js → cli-x21dqn18.js} +5 -5
  74. package/dist/{cli-dqb4n629.js → cli-x6vf4sad.js} +11 -11
  75. package/dist/{cli-d2mb5s8a.js → cli-x77gen2p.js} +680 -440
  76. package/dist/{cli-vq97g1jq.js → cli-xawhpgc9.js} +95 -195
  77. package/dist/{cli-82d3nkc3.js → cli-xd4eqtr2.js} +2 -2
  78. package/dist/{cli-9fhkkpj5.js → cli-xt768r5m.js} +2 -2
  79. package/dist/{cli-zrk6jxv8.js → cli-y4x8ww6s.js} +2 -2
  80. package/dist/{cli-xc27fwzg.js → cli-y9jvxfhk.js} +3 -3
  81. package/dist/{cli-44159k28.js → cli-z63a269p.js} +2 -2
  82. package/dist/{cli-sm8rses9.js → cli-zfdp01hn.js} +8 -5
  83. package/dist/cli.js +31 -29
  84. package/dist/{cloudflare-workers-ai-system-one-ch2wxfcp.js → cloudflare-workers-ai-system-one-43v9qkjk.js} +7 -2
  85. package/dist/{codex-adapter-968xwfy7.js → codex-adapter-tcg1q3jk.js} +6 -4
  86. package/dist/{codex-login-m819c41r.js → codex-login-rn7c5gxt.js} +1 -1
  87. package/dist/{codex-session-runner-ht4z99fa.js → codex-session-runner-xfx4wd9x.js} +7 -5
  88. package/dist/{commands-pcymaeg9.js → commands-ew83nj70.js} +2 -2
  89. package/dist/{controller-fpjyk46r.js → controller-gnq1k1mf.js} +4 -4
  90. package/dist/{controllers-connect-mwzx1fh6.js → controllers-connect-tw1dan9r.js} +26 -24
  91. package/dist/{cursor-adapter-cp4s2e2f.js → cursor-adapter-yq5xgfcg.js} +1 -1
  92. package/dist/{db-xsjg9jqq.js → db-7gbc53fd.js} +14 -2
  93. package/dist/{db-sgd12bt9.js → db-t86xrnes.js} +3 -3
  94. package/dist/{devin-adapter-zgqadvnm.js → devin-adapter-4ycy4vrf.js} +1 -1
  95. package/dist/{dispatcher-x0jfw2c9.js → dispatcher-64bdyhps.js} +23 -21
  96. package/dist/{docker-fkgtr75t.js → docker-nfy2gc8t.js} +2 -2
  97. package/dist/{dsh-adapter-jrq7ma7h.js → dsh-adapter-tvhc4k2d.js} +1 -1
  98. package/dist/{e2b-16d6ch39.js → e2b-9gkdhm3c.js} +2 -2
  99. package/dist/{e2b-zn0b4w5w.js → e2b-cjtf8k1z.js} +2 -2
  100. package/dist/{easter-egg-3d-5cgynaja.js → easter-egg-3d-e4qv8dxj.js} +1 -1
  101. package/dist/{event-bus-9vpk8gmq.js → event-bus-3z78ep5n.js} +1 -1
  102. package/dist/{execute-t4e6m3rw.js → execute-0xpz8950.js} +85 -23
  103. package/dist/{external-channel-8njj4h3b.js → external-channel-e9nw24zv.js} +2 -2
  104. package/dist/{google-generative-ai-bmq8gkf3.js → google-generative-ai-mk9vcpe1.js} +15 -15
  105. package/dist/{google-vertex-b1rvp52s.js → google-vertex-q580kz8k.js} +20 -20
  106. package/dist/{grok-adapter-kkr97v2f.js → grok-adapter-v82srhrx.js} +9 -7
  107. package/dist/handlers-c9ajzhbn.js +41 -0
  108. package/dist/{heartbeat-wd03zhn0.js → heartbeat-2vqs1319.js} +26 -24
  109. package/dist/{hook-xzj2ts2p.js → hook-01nk13wx.js} +7 -5
  110. package/dist/{hook-f0vghj42.js → hook-jzpheegg.js} +9 -7
  111. package/dist/{http-4hwxv62p.js → http-fc1k6ec8.js} +859 -252
  112. package/dist/index-07ef7nkm.js +42 -0
  113. package/dist/{index-g6tbb6ve.js → index-31ceyf42.js} +1 -1
  114. package/dist/index-7yts8n9s.js +43 -0
  115. package/dist/{index-c4ykd0f1.js → index-dr94zbnh.js} +13348 -11611
  116. package/dist/index-q3t40ryv.js +46 -0
  117. package/dist/{index-ww2vznkp.js → index-sepja052.js} +1 -1
  118. package/dist/{index-97q4xn03.js → index-zdycv3ja.js} +135 -79
  119. package/dist/{index-g216q6jr.js → index-zsyx51ap.js} +1 -4
  120. package/dist/{keepalive-hrh7xnr9.js → keepalive-5nh2rhmr.js} +4 -4
  121. package/dist/lead-aayq64jn.js +79 -0
  122. package/dist/{llama-cpp-classify-vv43gswy.js → llama-cpp-classify-ee7vmd7q.js} +4 -1
  123. package/dist/{maintenance-e8wnwnqx.js → maintenance-hjhzdt3k.js} +1 -1
  124. package/dist/{mistral-conversations-1medfwv7.js → mistral-conversations-sd0xf1tn.js} +13 -13
  125. package/dist/{node-3v3f08f7.js → node-jzjj7r9x.js} +10 -9
  126. package/dist/{oauth-refresh-sweep-zm0yj4sj.js → oauth-refresh-sweep-y3n1knmd.js} +3 -3
  127. package/dist/{onboard-dtwcq603.js → onboard-1zq3m7db.js} +2 -2
  128. package/dist/{openai-codex-responses-de9pvs4d.js → openai-codex-responses-79s8xnbh.js} +29 -23
  129. package/dist/{openai-completions-mtqge3nf.js → openai-completions-tdbyts0x.js} +21 -20
  130. package/dist/{openai-decisions-ygk4kkxd.js → openai-decisions-bqhnjyfz.js} +2 -1
  131. package/dist/{openai-responses-dkyng71g.js → openai-responses-dm3t5vqp.js} +26 -17
  132. package/dist/{opencode-adapter-x4tqzhkh.js → opencode-adapter-e56086m1.js} +7 -5
  133. package/dist/{openrouter-images-wvv7camj.js → openrouter-images-ssttr1y4.js} +6 -3
  134. package/dist/{otel-impl-w3fbhpn0.js → otel-impl-h9yyza47.js} +2 -2
  135. package/dist/{pi-messages-nhh1pkz2.js → pi-messages-3nf8bq1a.js} +6 -4
  136. package/dist/{pi-mono-adapter-mcfkn9vf.js → pi-mono-adapter-tzps8f38.js} +9 -37
  137. package/dist/{pricing-refresh-j38dehfs.js → pricing-refresh-nevhat8h.js} +3 -3
  138. package/dist/{rbac-roles-ae8yttq4.js → rbac-roles-4y6ngvzq.js} +1 -1
  139. package/dist/{rbac-roles-ejxx9b35.js → rbac-roles-exbg8xb6.js} +2 -2
  140. package/dist/{registry-rxw10xzg.js → registry-ajhexe44.js} +1 -1
  141. package/dist/{render-v2-n16n0phe.js → render-v2-cfsh13z3.js} +3 -3
  142. package/dist/{resume-z0fgjc38.js → resume-5s2t0dbs.js} +10 -8
  143. package/dist/{runtime-xe3aqy4x.js → runtime-bt5pkpm1.js} +1 -1
  144. package/dist/{session-access-cv3mq6mc.js → session-access-dnbyp3cn.js} +6 -4
  145. package/dist/{setup-mafhj3yt.js → setup-1wj1y77j.js} +1 -1
  146. package/dist/{typesafe-system-one-2hgpayzy.js → typesafe-system-one-bre38hvs.js} +4 -2
  147. package/dist/{virtual-modules-4r3zkc20.js → virtual-modules-mnfbh5mr.js} +8 -2
  148. package/dist/{work-objects-8b86mgjp.js → work-objects-7mg6216g.js} +26 -24
  149. package/dist/worker-nwb789mg.js +79 -0
  150. package/dist/{x-y45mthwn.js → x-pbpezr0s.js} +2 -2
  151. package/openapi.json +793 -6
  152. package/package.json +11 -11
  153. package/src/apps/catalog.generated.json +206 -4
  154. package/src/apps/page-validator.ts +13 -1
  155. package/src/assets/key.ts +17 -0
  156. package/src/azure-devops/handlers.ts +1 -1
  157. package/src/be/chunking.ts +42 -9
  158. package/src/be/db/approvals.ts +18 -0
  159. package/src/be/db/session-moves.ts +164 -0
  160. package/src/be/db/session-tree.ts +384 -0
  161. package/src/be/db/tasks/draft-text.ts +15 -0
  162. package/src/be/db/tasks/read.ts +54 -5
  163. package/src/be/db/workflow-runs.ts +96 -0
  164. package/src/be/db.ts +85 -237
  165. package/src/be/memory/index-content.ts +36 -17
  166. package/src/be/memory/providers/openai-embedding.ts +115 -61
  167. package/src/be/memory/providers/sqlite-store.ts +69 -23
  168. package/src/be/memory/reranker.ts +31 -1
  169. package/src/be/memory/search-candidates.ts +47 -0
  170. package/src/be/memory/types.ts +14 -0
  171. package/src/be/onboarding.ts +3 -0
  172. package/src/be/scripts/typecheck.ts +2 -0
  173. package/src/be/seed-scripts/catalog/heartbeat-jev-gate.ts +106 -54
  174. package/src/be/seed-scripts/index.ts +2 -2
  175. package/src/be/skill-sync.ts +43 -2
  176. package/src/be/swarm-config-guard.ts +25 -1
  177. package/src/be/task-attachment-store.ts +30 -0
  178. package/src/bitbucket/api.ts +81 -0
  179. package/src/bitbucket/auth.ts +64 -0
  180. package/src/bitbucket/handlers.ts +434 -0
  181. package/src/bitbucket/index.ts +28 -0
  182. package/src/bitbucket/templates.ts +134 -0
  183. package/src/bitbucket/types.ts +65 -0
  184. package/src/commands/runner.ts +55 -3
  185. package/src/e2b/env.ts +1 -0
  186. package/src/extensions/catalog.generated.json +4 -4
  187. package/src/extensions/contract-types.generated.ts +1 -1
  188. package/src/extensions/contract.ts +9 -2
  189. package/src/extensions/post-bridge.ts +1 -0
  190. package/src/fs/provider.ts +13 -1
  191. package/src/github/handlers.ts +6 -6
  192. package/src/gitlab/handlers.ts +5 -5
  193. package/src/heartbeat/heartbeat.ts +31 -11
  194. package/src/http/assets.ts +14 -2
  195. package/src/http/core.ts +12 -0
  196. package/src/http/fs.ts +54 -34
  197. package/src/http/index.ts +4 -0
  198. package/src/http/mcp-body.ts +37 -0
  199. package/src/http/mcp-user.ts +3 -5
  200. package/src/http/mcp.ts +6 -7
  201. package/src/http/memory.ts +26 -15
  202. package/src/http/onboarding.ts +1 -0
  203. package/src/http/page-proxy.ts +61 -5
  204. package/src/http/pages-public.ts +27 -44
  205. package/src/http/pages.ts +4 -4
  206. package/src/http/script-runs.ts +2 -0
  207. package/src/http/sessions.ts +366 -12
  208. package/src/http/skills.ts +48 -1
  209. package/src/http/tasks.ts +1 -1
  210. package/src/http/users.ts +13 -1
  211. package/src/http/webhooks.ts +141 -0
  212. package/src/integrations/chat-sdk/adapters.ts +24 -4
  213. package/src/integrations/chat-sdk/attachments.ts +90 -12
  214. package/src/integrations/chat-sdk/facts.ts +138 -0
  215. package/src/integrations/chat-sdk/inbound.ts +156 -8
  216. package/src/integrations/chat-sdk/limits.ts +24 -0
  217. package/src/integrations/chat-sdk/outbound-files.ts +186 -0
  218. package/src/integrations/chat-sdk/outbound.ts +272 -5
  219. package/src/integrations/chat-sdk/relay.ts +4 -1
  220. package/src/integrations/chat-sdk/router.ts +28 -2
  221. package/src/integrations/chat-sdk/steering.ts +9 -0
  222. package/src/integrations/chat-sdk/templates.ts +14 -0
  223. package/src/integrations/chat-sdk/thread-buffer.ts +4 -0
  224. package/src/mcp-limits.ts +20 -0
  225. package/src/pages/browser-sdk.ts +8 -3
  226. package/src/pages/feedback-overlay.ts +1 -1
  227. package/src/providers/acp-targets.ts +1 -1
  228. package/src/providers/amp-adapter.ts +1 -1
  229. package/src/providers/codex-adapter.ts +106 -119
  230. package/src/providers/codex-agents-md.ts +35 -229
  231. package/src/providers/cursor-adapter.ts +1 -1
  232. package/src/providers/pi-mono-adapter.ts +5 -53
  233. package/src/realtime/transport.ts +1 -2
  234. package/src/script-workflows/workflow-ctx.ts +1 -0
  235. package/src/scripts-runtime/types/stdlib.d.ts +2 -0
  236. package/src/scripts-runtime/types/swarm-extension.d.ts +14 -4
  237. package/src/scripts-runtime/types/swarm-sdk.d.ts +2 -0
  238. package/src/server-user.ts +2 -0
  239. package/src/server.ts +2 -0
  240. package/src/slack/assistant.ts +2 -2
  241. package/src/slack/files.ts +2 -0
  242. package/src/slack/handlers.test.ts +158 -0
  243. package/src/slack/handlers.ts +23 -32
  244. package/src/slack/inbound-files.ts +39 -6
  245. package/src/slack/thread-buffer.ts +2 -1
  246. package/src/tasks/context-key.ts +61 -1
  247. package/src/tasks/task-terminal-effects.ts +21 -17
  248. package/src/tests/apps-card-grid.test.ts +197 -0
  249. package/src/tests/apps-catalog-additions.test.ts +120 -0
  250. package/src/tests/apps-tree.test.ts +129 -0
  251. package/src/tests/bitbucket-auth.test.ts +156 -0
  252. package/src/tests/bitbucket-handlers.test.ts +425 -0
  253. package/src/tests/bitbucket-webhook.test.ts +220 -0
  254. package/src/tests/chat-sdk-attachments-reply.test.ts +52 -0
  255. package/src/tests/chat-sdk-config.test.ts +11 -0
  256. package/src/tests/chat-sdk-facts.test.ts +23 -0
  257. package/src/tests/chat-sdk-inbound.test.ts +597 -4
  258. package/src/tests/chat-sdk-outbound-files.test.ts +652 -0
  259. package/src/tests/chat-sdk-telegram-markdown.test.ts +92 -0
  260. package/src/tests/codex-adapter.test.ts +167 -242
  261. package/src/tests/config-api-only-keys.test.ts +9 -0
  262. package/src/tests/context-key.test.ts +50 -0
  263. package/src/tests/core-auth.test.ts +5 -0
  264. package/src/tests/cors-allowlist.test.ts +1 -1
  265. package/src/tests/db-task-read-contract.test.ts +7 -0
  266. package/src/tests/db-task-write-contract.test.ts +6 -0
  267. package/src/tests/docs-index-footprint.test.js +3 -3
  268. package/src/tests/extensions-example-slack-reply-gate.test.ts +262 -91
  269. package/src/tests/fixtures/bitbucket/pullrequest-comment-created-inline.json +174 -0
  270. package/src/tests/fixtures/bitbucket/pullrequest-comment-created.json +170 -0
  271. package/src/tests/fixtures/bitbucket/pullrequest-created-mention.json +132 -0
  272. package/src/tests/fixtures/bitbucket/pullrequest-created-reviewer.json +140 -0
  273. package/src/tests/fixtures/bitbucket/pullrequest-fulfilled.json +134 -0
  274. package/src/tests/fixtures/bitbucket/pullrequest-rejected.json +133 -0
  275. package/src/tests/generate-mcp-docs.test.ts +101 -0
  276. package/src/tests/gitlab-handlers.test.ts +3 -3
  277. package/src/tests/gitlab-vcs-db.test.ts +4 -4
  278. package/src/tests/heartbeat-checklist.test.ts +33 -0
  279. package/src/tests/heartbeat-jev-gate-credentials.test.ts +44 -0
  280. package/src/tests/heartbeat-jev-gate.test.ts +242 -1
  281. package/src/tests/integration-disable-flags.test.ts +36 -0
  282. package/src/tests/kv-page-proxy.test.ts +91 -5
  283. package/src/tests/launch-password-rejection.test.ts +2 -2
  284. package/src/tests/mcp-input-caps.test.ts +294 -0
  285. package/src/tests/mcp-tools.test.ts +1 -1
  286. package/src/tests/memory-chunked-search-starvation.test.ts +200 -0
  287. package/src/tests/memory-embedding-input-cap.test.ts +114 -0
  288. package/src/tests/memory.test.ts +14 -0
  289. package/src/tests/onboarding.test.ts +12 -0
  290. package/src/tests/page-guest-session.test.ts +19 -8
  291. package/src/tests/page-proxy-authed.test.ts +1 -1
  292. package/src/tests/page-proxy.test.ts +17 -10
  293. package/src/tests/page-session.test.ts +102 -1
  294. package/src/tests/pages-authed-mode.test.ts +82 -12
  295. package/src/tests/pages-password-mode.test.ts +5 -5
  296. package/src/tests/pi-codemode.test.ts +8 -2
  297. package/src/tests/preload-env.test.ts +57 -0
  298. package/src/tests/preload.ts +21 -0
  299. package/src/tests/prompt-template-remaining.test.ts +38 -0
  300. package/src/tests/realtime-transport.test.ts +13 -7
  301. package/src/tests/run-root-coverage.test.ts +39 -0
  302. package/src/tests/runner-bitbucket-vcs.test.ts +191 -0
  303. package/src/tests/runner-skills-refresh.test.ts +58 -0
  304. package/src/tests/script-runs-http.test.ts +84 -0
  305. package/src/tests/scripts-http.test.ts +10 -1
  306. package/src/tests/secret-scrubber.test.ts +15 -0
  307. package/src/tests/sessions-move.test.ts +576 -0
  308. package/src/tests/sessions-tree.test.ts +493 -0
  309. package/src/tests/skill-files-http.test.ts +50 -0
  310. package/src/tests/skills-bundle.test.ts +99 -0
  311. package/src/tests/swarm-config-export-to-env.test.ts +23 -3
  312. package/src/tests/task-audio-transcription.test.ts +299 -0
  313. package/src/tests/task-completion-memory-chunking.test.ts +434 -0
  314. package/src/tests/task-terminal-secret-scrub.test.ts +6 -1
  315. package/src/tests/transcription.test.ts +299 -0
  316. package/src/tests/vcs-bitbucket.test.ts +91 -0
  317. package/src/tests/vcs-provider-isolation.test.ts +123 -0
  318. package/src/tests/vcs-provider.test.ts +16 -1
  319. package/src/tests/vcs-tracking.test.ts +3 -3
  320. package/src/tests/workflow-foreach-bodies.test.ts +637 -0
  321. package/src/tests/workflow-foreach-concurrency.test.ts +342 -0
  322. package/src/tests/workflow-foreach-retry.test.ts +537 -0
  323. package/src/tests/workflow-foreach.test.ts +44 -23
  324. package/src/tests/workflow-step-claims.test.ts +6 -6
  325. package/src/tests/workflow-system-one-decision-providers.test.ts +98 -1
  326. package/src/tests/workflow-system-one-decision.test.ts +2 -1
  327. package/src/tests/workflow-tla-races.test.ts +2 -2
  328. package/src/tools/chat-access.ts +10 -0
  329. package/src/tools/chat-post.ts +31 -11
  330. package/src/tools/chat-reply.ts +32 -10
  331. package/src/tools/memory-search.ts +3 -1
  332. package/src/tools/script-delete.ts +1 -1
  333. package/src/tools/system-one-decide.ts +17 -4
  334. package/src/tools/workflows/create-workflow.ts +5 -3
  335. package/src/types.ts +9 -2
  336. package/src/utils/config-env-export.ts +4 -1
  337. package/src/utils/page-session.ts +133 -20
  338. package/src/utils/secret-scrubber.ts +4 -0
  339. package/src/utils/skills-refresh.ts +83 -43
  340. package/src/utils/transcription.ts +246 -0
  341. package/src/vcs/bitbucket.ts +58 -0
  342. package/src/vcs/index.ts +2 -0
  343. package/src/vcs/types.ts +2 -2
  344. package/src/workflows/approval-resolution.ts +4 -1
  345. package/src/workflows/checkpoint.ts +9 -1
  346. package/src/workflows/constants.ts +3 -0
  347. package/src/workflows/definition.ts +48 -17
  348. package/src/workflows/engine.ts +25 -15
  349. package/src/workflows/executors/foreach.ts +45 -99
  350. package/src/workflows/executors/system-one-decision.ts +28 -7
  351. package/src/workflows/executors/system-one-providers.ts +39 -4
  352. package/src/workflows/foreach-body.ts +146 -0
  353. package/src/workflows/foreach-driver.ts +552 -0
  354. package/src/workflows/foreach-join.ts +206 -7
  355. package/src/workflows/node-timeout.ts +21 -0
  356. package/src/workflows/recovery.ts +68 -3
  357. package/src/workflows/resume.ts +221 -43
  358. package/src/workflows/retry-poller.ts +39 -24
  359. package/src/workflows/step-input.ts +59 -0
  360. package/src/workflows/task-step-routing.ts +114 -56
  361. package/templates/extensions/slack-reply-gate/README.md +24 -22
  362. package/templates/extensions/slack-reply-gate/hooks.ts +146 -95
  363. package/templates/extensions/slack-reply-gate/manifest.json +2 -2
  364. package/templates/skills/apps/SKILL.md +28 -6
  365. package/templates/skills/apps/content.md +28 -6
  366. package/templates/skills/chat-sdk/content.md +6 -2
  367. package/templates/skills/code-quality/content.md +2 -1
  368. package/templates/skills/heartbeat-runbook/content.md +5 -5
  369. package/templates/skills/pages/content.md +6 -4
  370. package/templates/skills/slack-interaction/content.md +5 -0
  371. package/templates/skills/swarm-scripts/SKILL.md +2 -2
  372. package/templates/skills/swarm-scripts/content.md +2 -2
  373. package/templates/skills/work-on-task/SKILL.md +1 -1
  374. package/templates/skills/work-on-task/content.md +1 -1
  375. package/dist/cli-51y09c6p.js +0 -2657
  376. package/dist/cli-yw91rnmw.js +0 -13
  377. package/dist/handlers-0pvee9nw.js +0 -39
  378. package/dist/index-gdh8jrdm.js +0 -43
  379. package/dist/index-hg0x2cdv.js +0 -40
  380. package/dist/index-thm0y6a8.js +0 -41
  381. package/dist/lead-ee8vazz2.js +0 -77
  382. package/dist/worker-16s75hm5.js +0 -77
@@ -16,7 +16,7 @@
16
16
  */
17
17
 
18
18
  import { getSessionLogsByTaskId } from "@/be/db";
19
- import { getEmbeddingProvider, getMemoryStore } from "@/be/memory";
19
+ import { indexMemoryContent, taskCompletionMemoryKey } from "@/be/memory/index-content";
20
20
  import { getRetrievalsForTask } from "@/be/memory/raters/retrieval";
21
21
  import { runServerRaters } from "@/be/memory/raters/run-server-raters";
22
22
  import { shouldPersistTaskCompletionMemory } from "@/memory/automatic-task-gate";
@@ -55,21 +55,22 @@ export async function runTaskTerminalEffects(args: {
55
55
  // Skip indexing if there's truly no content
56
56
  if (taskContent.length < 30) return;
57
57
 
58
- const store = getMemoryStore();
59
- const provider = getEmbeddingProvider();
60
-
61
- const memory = await store.store({
58
+ // Same path as memory-store: chunk, store, and embed each chunk, so a
59
+ // long output never meets the embedding model's input cap. The per-task
60
+ // key makes the chunks one document, like a keyed manual memory, and a
61
+ // repeat write for the task replaces that document atomically.
62
+ const key = taskCompletionMemoryKey(taskId);
63
+ await indexMemoryContent({
62
64
  agentId: agentId ?? null,
63
65
  content: taskContent,
64
66
  name: `Task: ${safeTask.slice(0, 80)}`,
65
67
  scope: "agent",
66
68
  source: "task_completion",
67
69
  sourceTaskId: taskId,
70
+ key,
71
+ intent: "task completion memory",
72
+ replaceExisting: true,
68
73
  });
69
- const embedding = await provider.embed(taskContent);
70
- if (embedding) {
71
- await store.updateEmbedding(memory.id, embedding, provider.name);
72
- }
73
74
 
74
75
  // Auto-promote high-value completions to swarm memory (P3)
75
76
  const shouldShareWithSwarm =
@@ -80,24 +81,27 @@ export async function runTaskTerminalEffects(args: {
80
81
 
81
82
  if (shouldShareWithSwarm) {
82
83
  try {
83
- const swarmMemory = await store.store({
84
+ await indexMemoryContent({
84
85
  agentId: agentId ?? null,
85
- scope: "swarm",
86
- name: `Shared: ${safeTask.slice(0, 80)}`,
87
86
  content: `Task completed by agent ${agentId}:\n\n${taskContent}`,
87
+ name: `Shared: ${safeTask.slice(0, 80)}`,
88
+ scope: "swarm",
88
89
  source: "task_completion",
89
90
  sourceTaskId: taskId,
91
+ key,
92
+ intent: "task completion memory",
93
+ replaceExisting: true,
90
94
  });
91
- const swarmEmbedding = await provider.embed(taskContent);
92
- if (swarmEmbedding) {
93
- await store.updateEmbedding(swarmMemory.id, swarmEmbedding, provider.name);
94
- }
95
95
  } catch {
96
96
  // Non-blocking — swarm memory promotion failure is not critical
97
97
  }
98
98
  }
99
- } catch {
99
+ } catch (err) {
100
100
  // Non-blocking — task completion memory failure should not affect task status
101
+ console.error(
102
+ "[task-terminal-effects] task completion memory write failed:",
103
+ scrubSecrets(err instanceof Error ? err.message : String(err)),
104
+ );
101
105
  }
102
106
  })().catch((err) =>
103
107
  console.error(
@@ -0,0 +1,197 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { parseAppDefinition } from "../apps/definition";
3
+
4
+ /**
5
+ * Validator coverage for `CardGrid` and the Table column `format` template.
6
+ * Pure `parseAppDefinition` calls — no DB, no HTTP — mirroring
7
+ * `apps-catalog-additions.test.ts`.
8
+ */
9
+
10
+ type Element = { type: string; props?: Record<string, unknown>; children?: string[] };
11
+
12
+ function appWith(elements: Record<string, Element>) {
13
+ return {
14
+ models: {
15
+ doc: {
16
+ columns: {
17
+ title: { kind: "string", required: true },
18
+ author: { kind: "string" },
19
+ cover: { kind: "string" },
20
+ status: { kind: "enum", enum: ["draft", "published"], default: "draft" },
21
+ },
22
+ },
23
+ },
24
+ queries: {
25
+ docs: { model: "doc" },
26
+ docById: { model: "doc", filter: { id: { $param: "doc" } } },
27
+ },
28
+ pages: {
29
+ main: { title: "Main", root: "root", elements },
30
+ detail: {
31
+ title: "Detail",
32
+ params: { doc: { kind: "string" }, position: { kind: "number" } },
33
+ root: "detailRoot",
34
+ elements: { detailRoot: { type: "Text", props: { content: "Detail" } } },
35
+ },
36
+ },
37
+ defaultPage: "main",
38
+ };
39
+ }
40
+
41
+ async function issuesFor(input: unknown) {
42
+ const parsed = await parseAppDefinition(input);
43
+ return parsed.success ? [] : parsed.issues;
44
+ }
45
+
46
+ const el = (id: string) => `pages.main.elements.${id}`;
47
+
48
+ function cardGrid(props: Record<string, unknown>) {
49
+ return appWith({
50
+ root: { type: "Stack", props: {}, children: ["cards"] },
51
+ cards: {
52
+ type: "CardGrid",
53
+ props: {
54
+ data: { $state: "/queries/docs/data" },
55
+ loading: { $state: "/queries/docs/loading" },
56
+ error: { $state: "/queries/docs/error" },
57
+ titleKey: "title",
58
+ ...props,
59
+ },
60
+ },
61
+ });
62
+ }
63
+
64
+ const navigateToDetail = [
65
+ { action: "app.navigate", params: { page: "detail", params: { doc: { $row: "id" } } } },
66
+ ];
67
+
68
+ describe("CardGrid", () => {
69
+ test("onClick running app.navigate to a declared page with $row params passes", async () => {
70
+ const issues = await issuesFor(
71
+ cardGrid({
72
+ imageKey: "cover",
73
+ fields: [
74
+ { key: "author" },
75
+ { key: "createdAt", kind: "date" },
76
+ { key: "status", kind: "badge", tones: { published: "success" } },
77
+ ],
78
+ columns: { base: 1, md: 2, lg: 4 },
79
+ emptyMessage: "No documents yet",
80
+ onClick: navigateToDetail,
81
+ }),
82
+ );
83
+ if (issues.length > 0) console.error(issues);
84
+ expect(issues).toEqual([]);
85
+ });
86
+
87
+ test("$row and $rowIndex are accepted inside the onClick chain", async () => {
88
+ const issues = await issuesFor(
89
+ cardGrid({
90
+ onClick: [
91
+ { action: "app.refresh", params: { query: "docs" } },
92
+ {
93
+ action: "app.navigate",
94
+ params: {
95
+ page: "detail",
96
+ params: { doc: { $row: "id" }, position: { $rowIndex: true } },
97
+ },
98
+ },
99
+ ],
100
+ }),
101
+ );
102
+ if (issues.length > 0) console.error(issues);
103
+ expect(issues).toEqual([]);
104
+ });
105
+
106
+ test("$row outside the onClick chain is rejected", async () => {
107
+ const issues = await issuesFor(cardGrid({ emptyMessage: { $row: "title" } }));
108
+ expect(issues).toContainEqual({
109
+ path: `${el("cards")}.props.emptyMessage`,
110
+ message: "$row, $rowIndex, $form, and $chain bindings are only allowed in action params",
111
+ });
112
+ });
113
+
114
+ test("onClick naming an unknown action fails", async () => {
115
+ const issues = await issuesFor(cardGrid({ onClick: [{ action: "app.nope", params: {} }] }));
116
+ expect(issues).toContainEqual({
117
+ path: `${el("cards")}.props.onClick.0.action`,
118
+ message: 'unknown action type "app.nope"',
119
+ });
120
+ });
121
+
122
+ test("onClick navigating to an undeclared page fails", async () => {
123
+ const issues = await issuesFor(
124
+ cardGrid({ onClick: [{ action: "app.navigate", params: { page: "nope" } }] }),
125
+ );
126
+ expect(issues).toContainEqual({
127
+ path: `${el("cards")}.props.onClick.0.params.page`,
128
+ message: 'unknown page "nope"',
129
+ });
130
+ });
131
+
132
+ test("titleKey is required", async () => {
133
+ const copy = cardGrid({});
134
+ delete copy.pages.main.elements.cards!.props!.titleKey;
135
+ const issues = await issuesFor(copy);
136
+ expect(issues.some((i) => i.path.startsWith(`${el("cards")}.props`))).toBe(true);
137
+ });
138
+
139
+ test("data bound to a $state path passes", async () => {
140
+ const issues = await issuesFor(
141
+ appWith({
142
+ root: { type: "Stack", props: {}, children: ["cards"] },
143
+ cards: {
144
+ type: "CardGrid",
145
+ props: { data: { $state: "/queries/docs/data" }, titleKey: "title" },
146
+ },
147
+ }),
148
+ );
149
+ if (issues.length > 0) console.error(issues);
150
+ expect(issues).toEqual([]);
151
+ });
152
+
153
+ test("an unknown field kind is rejected", async () => {
154
+ const issues = await issuesFor(cardGrid({ fields: [{ key: "author", kind: "status" }] }));
155
+ expect(issues.some((i) => i.path.startsWith(`${el("cards")}.props.fields.0.kind`))).toBe(true);
156
+ });
157
+ });
158
+
159
+ describe("Table column format", () => {
160
+ test("a column with a {{field}} format template passes", async () => {
161
+ const issues = await issuesFor(
162
+ appWith({
163
+ root: { type: "Stack", props: {}, children: ["table"] },
164
+ table: {
165
+ type: "Table",
166
+ props: {
167
+ data: { $state: "/queries/docs/data" },
168
+ columns: [
169
+ { key: "title", label: "Document", format: "{{title}} ({{status}})" },
170
+ { key: "author" },
171
+ ],
172
+ },
173
+ },
174
+ }),
175
+ );
176
+ if (issues.length > 0) console.error(issues);
177
+ expect(issues).toEqual([]);
178
+ });
179
+
180
+ test("format must be a string", async () => {
181
+ const issues = await issuesFor(
182
+ appWith({
183
+ root: { type: "Stack", props: {}, children: ["table"] },
184
+ table: {
185
+ type: "Table",
186
+ props: {
187
+ data: { $state: "/queries/docs/data" },
188
+ columns: [{ key: "title", format: 42 }],
189
+ },
190
+ },
191
+ }),
192
+ );
193
+ expect(issues.some((i) => i.path.startsWith(`${el("table")}.props.columns.0.format`))).toBe(
194
+ true,
195
+ );
196
+ });
197
+ });
@@ -0,0 +1,120 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { parseAppDefinition } from "../apps/definition";
3
+
4
+ /**
5
+ * Validator coverage for the small catalog additions: `NumberInput`, Select
6
+ * `defaultValue`, and `fill` on Stack / Split. Pure `parseAppDefinition`
7
+ * calls — no DB, no HTTP — mirroring `apps-catalog-inputs.test.ts`.
8
+ */
9
+
10
+ type Element = { type: string; props?: Record<string, unknown>; children?: string[] };
11
+
12
+ function appWith(elements: Record<string, Element>) {
13
+ return {
14
+ models: {},
15
+ pages: { main: { title: "Main", root: "root", elements } },
16
+ defaultPage: "main",
17
+ };
18
+ }
19
+
20
+ async function issuesFor(input: unknown) {
21
+ const parsed = await parseAppDefinition(input);
22
+ return parsed.success ? [] : parsed.issues;
23
+ }
24
+
25
+ const el = (id: string) => `pages.main.elements.${id}`;
26
+
27
+ describe("NumberInput", () => {
28
+ test("a NumberInput's /ui/<id>/value read by a bound Text validates", async () => {
29
+ const issues = await issuesFor(
30
+ appWith({
31
+ root: { type: "Stack", props: {}, children: ["width", "echo"] },
32
+ width: {
33
+ type: "NumberInput",
34
+ props: { id: "x", label: "Width", placeholder: "px", min: 1, max: 4096 },
35
+ },
36
+ echo: { type: "Text", props: { content: { $state: "/ui/x/value" } } },
37
+ }),
38
+ );
39
+ if (issues.length > 0) console.error(issues);
40
+ expect(issues).toEqual([]);
41
+ });
42
+
43
+ test("a /ui/<id>/value read with no UI control of that id is rejected", async () => {
44
+ const issues = await issuesFor(
45
+ appWith({
46
+ root: { type: "Stack", props: {}, children: ["echo"] },
47
+ echo: { type: "Text", props: { content: { $state: "/ui/x/value" } } },
48
+ }),
49
+ );
50
+ expect(issues).toContainEqual({
51
+ path: `${el("echo")}.props.content`,
52
+ message: 'state reference targets unknown UI control "x"',
53
+ });
54
+ });
55
+
56
+ test("id is required and min/max must be numbers", async () => {
57
+ const missingId = await issuesFor(
58
+ appWith({
59
+ root: { type: "Stack", props: {}, children: ["width"] },
60
+ width: { type: "NumberInput", props: { label: "Width" } },
61
+ }),
62
+ );
63
+ expect(missingId.map((issue) => issue.path)).toContain(`${el("width")}.props.id`);
64
+
65
+ const badBounds = await issuesFor(
66
+ appWith({
67
+ root: { type: "Stack", props: {}, children: ["width"] },
68
+ width: { type: "NumberInput", props: { id: "x", min: "1", max: "10" } },
69
+ }),
70
+ );
71
+ const paths = badBounds.map((issue) => issue.path);
72
+ expect(paths).toContain(`${el("width")}.props.min`);
73
+ expect(paths).toContain(`${el("width")}.props.max`);
74
+ });
75
+ });
76
+
77
+ describe("Select defaultValue", () => {
78
+ const select = (props: Record<string, unknown>) =>
79
+ appWith({
80
+ root: { type: "Stack", props: {}, children: ["device", "echo"] },
81
+ device: {
82
+ type: "Select",
83
+ props: { id: "device", options: ["phone", "tablet", "desktop"], ...props },
84
+ },
85
+ echo: { type: "Text", props: { content: { $state: "/ui/device/value" } } },
86
+ });
87
+
88
+ test("a Select with defaultValue validates", async () => {
89
+ const issues = await issuesFor(select({ defaultValue: "tablet" }));
90
+ if (issues.length > 0) console.error(issues);
91
+ expect(issues).toEqual([]);
92
+ });
93
+
94
+ test("defaultValue must be a string", async () => {
95
+ const paths = (await issuesFor(select({ defaultValue: 2 }))).map((issue) => issue.path);
96
+ expect(paths).toContain(`${el("device")}.props.defaultValue`);
97
+ });
98
+ });
99
+
100
+ describe("Stack and Split fill", () => {
101
+ const layout = (stackFill: unknown, splitFill: unknown) =>
102
+ appWith({
103
+ root: { type: "Stack", props: { fill: stackFill }, children: ["split"] },
104
+ split: { type: "Split", props: { fill: splitFill, ratio: "1-1" }, children: ["a", "b"] },
105
+ a: { type: "Text", props: { content: "left" } },
106
+ b: { type: "Text", props: { content: "right" } },
107
+ });
108
+
109
+ test("fill: true validates on both", async () => {
110
+ const issues = await issuesFor(layout(true, true));
111
+ if (issues.length > 0) console.error(issues);
112
+ expect(issues).toEqual([]);
113
+ });
114
+
115
+ test("fill must be a boolean", async () => {
116
+ const paths = (await issuesFor(layout("yes", 1))).map((issue) => issue.path);
117
+ expect(paths).toContain(`${el("root")}.props.fill`);
118
+ expect(paths).toContain(`${el("split")}.props.fill`);
119
+ });
120
+ });
@@ -0,0 +1,129 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { parseAppDefinition } from "../apps/definition";
3
+
4
+ /**
5
+ * Validator coverage for the `Tree` component: its `/ui/<id>/value` state slot,
6
+ * the type-less `nodes` prop (a bare `z.unknown()` in the catalog), and the
7
+ * generated schema's enforcement. Pure `parseAppDefinition` calls — no DB, no
8
+ * HTTP — mirroring `apps-catalog-additions.test.ts`.
9
+ */
10
+
11
+ type Element = { type: string; props?: Record<string, unknown>; children?: string[] };
12
+
13
+ const el = (id: string) => `pages.main.elements.${id}`;
14
+
15
+ /**
16
+ * A `document` model with a json `doc` column, a `documentById` query over it,
17
+ * a `Tree` bound to that query's doc, and a `Text` reading the tree's
18
+ * selection slot. `tree` props are overridable per case.
19
+ */
20
+ function treeDefinition(
21
+ treeProps: Record<string, unknown> = {},
22
+ elements: Record<string, Element> = {},
23
+ ) {
24
+ return {
25
+ models: {
26
+ document: { columns: { doc: { kind: "json" } } },
27
+ },
28
+ queries: { documentById: { model: "document" } },
29
+ pages: {
30
+ main: {
31
+ title: "Main",
32
+ root: "root",
33
+ elements: {
34
+ root: { type: "Stack", props: {}, children: ["tree", "selected"] },
35
+ tree: {
36
+ type: "Tree",
37
+ props: {
38
+ id: "nodes",
39
+ nodes: { $state: "/queries/documentById/data/0/doc" },
40
+ ...treeProps,
41
+ },
42
+ },
43
+ selected: { type: "Text", props: { content: { $state: "/ui/nodes/value" } } },
44
+ ...elements,
45
+ },
46
+ },
47
+ },
48
+ defaultPage: "main",
49
+ };
50
+ }
51
+
52
+ async function issuesFor(input: unknown) {
53
+ const parsed = await parseAppDefinition(input);
54
+ return parsed.success ? [] : parsed.issues;
55
+ }
56
+
57
+ describe("Tree validation", () => {
58
+ test("a Tree whose /ui/<id>/value slot is read by a bound Text validates", async () => {
59
+ const issues = await issuesFor(treeDefinition());
60
+ if (issues.length > 0) console.error(issues);
61
+ expect(issues).toEqual([]);
62
+ });
63
+
64
+ test("a /ui/<id>/value read with no Tree of that id is rejected", async () => {
65
+ const issues = await issuesFor(
66
+ treeDefinition(
67
+ {},
68
+ { selected: { type: "Text", props: { content: { $state: "/ui/phantom/value" } } } },
69
+ ),
70
+ );
71
+ expect(issues).toContainEqual({
72
+ path: `${el("selected")}.props.content`,
73
+ message: 'state reference targets unknown UI control "phantom"',
74
+ });
75
+ });
76
+
77
+ test("nodes accepts an inline doc-shaped literal as well as a $state binding", async () => {
78
+ const issues = await issuesFor(
79
+ treeDefinition({ nodes: { root: "a", elements: { a: { type: "Stack" } } } }),
80
+ );
81
+ if (issues.length > 0) console.error(issues);
82
+ expect(issues).toEqual([]);
83
+ });
84
+
85
+ test("label, maxHeight and fill validate together", async () => {
86
+ const issues = await issuesFor(
87
+ treeDefinition({ label: "Outline", maxHeight: "320px", fill: true }),
88
+ );
89
+ if (issues.length > 0) console.error(issues);
90
+ expect(issues).toEqual([]);
91
+ });
92
+
93
+ test("an unknown prop is rejected", async () => {
94
+ const issues = await issuesFor(treeDefinition({ foo: 1 }));
95
+ expect(issues).toContainEqual({
96
+ path: `${el("tree")}.props.foo`,
97
+ message: "unknown property",
98
+ });
99
+ });
100
+
101
+ test("id is required", async () => {
102
+ // No `selected` reader here: without an id there is no slot for it to target.
103
+ const issues = await issuesFor({
104
+ ...treeDefinition(),
105
+ pages: {
106
+ main: {
107
+ title: "Main",
108
+ root: "root",
109
+ elements: {
110
+ root: { type: "Stack", props: {}, children: ["tree"] },
111
+ tree: {
112
+ type: "Tree",
113
+ props: { nodes: { $state: "/queries/documentById/data/0/doc" } },
114
+ },
115
+ },
116
+ },
117
+ },
118
+ });
119
+ expect(issues.map((issue) => issue.path)).toContain(`${el("tree")}.props.id`);
120
+ });
121
+
122
+ test("maxHeight must be a string and fill a boolean", async () => {
123
+ const paths = (await issuesFor(treeDefinition({ maxHeight: 320, fill: "yes" }))).map(
124
+ (issue) => issue.path,
125
+ );
126
+ expect(paths).toContain(`${el("tree")}.props.maxHeight`);
127
+ expect(paths).toContain(`${el("tree")}.props.fill`);
128
+ });
129
+ });
@@ -0,0 +1,156 @@
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
+ import { createHmac } from "node:crypto";
3
+ import { resetBitbucketBotCache, resolveBitbucketBotAccountId } from "../bitbucket/api";
4
+ import {
5
+ getBitbucketToken,
6
+ initBitbucket,
7
+ isBitbucketEnabled,
8
+ resetBitbucket,
9
+ verifyBitbucketWebhook,
10
+ } from "../bitbucket/auth";
11
+
12
+ const ENV_KEYS = [
13
+ "BITBUCKET_WEBHOOK_SECRET",
14
+ "BITBUCKET_TOKEN",
15
+ "BITBUCKET_DISABLE",
16
+ "BITBUCKET_BOT_ACCOUNT_ID",
17
+ ] as const;
18
+
19
+ function sign(secret: string, body: string): string {
20
+ return `sha256=${createHmac("sha256", secret).update(body).digest("hex")}`;
21
+ }
22
+
23
+ afterEach(() => {
24
+ for (const key of ENV_KEYS) delete process.env[key];
25
+ resetBitbucket();
26
+ resetBitbucketBotCache();
27
+ });
28
+
29
+ describe("isBitbucketEnabled", () => {
30
+ test("disabled without a webhook secret", () => {
31
+ expect(isBitbucketEnabled()).toBe(false);
32
+ });
33
+
34
+ test("enabled with a webhook secret", () => {
35
+ process.env.BITBUCKET_WEBHOOK_SECRET = "example-secret";
36
+ expect(isBitbucketEnabled()).toBe(true);
37
+ });
38
+
39
+ test("BITBUCKET_DISABLE wins over the secret", () => {
40
+ process.env.BITBUCKET_WEBHOOK_SECRET = "example-secret";
41
+ process.env.BITBUCKET_DISABLE = "true";
42
+ expect(isBitbucketEnabled()).toBe(false);
43
+ });
44
+ });
45
+
46
+ describe("verifyBitbucketWebhook", () => {
47
+ test("matches Atlassian's documented test vector", () => {
48
+ process.env.BITBUCKET_WEBHOOK_SECRET = "It's a Secret to Everybody";
49
+ initBitbucket();
50
+ expect(
51
+ verifyBitbucketWebhook(
52
+ "Hello World!",
53
+ "sha256=a4771c39fbe90f317c7824e83ddef3caae9cb3d976c214ace1f2937e133263c9",
54
+ ),
55
+ ).toBe(true);
56
+ });
57
+
58
+ test("rejects a signature made with the wrong secret", () => {
59
+ process.env.BITBUCKET_WEBHOOK_SECRET = "example-secret";
60
+ initBitbucket();
61
+ expect(verifyBitbucketWebhook("{}", sign("other-secret", "{}"))).toBe(false);
62
+ expect(verifyBitbucketWebhook("{}", sign("example-secret", "{}"))).toBe(true);
63
+ });
64
+
65
+ test("rejects a tampered body", () => {
66
+ process.env.BITBUCKET_WEBHOOK_SECRET = "example-secret";
67
+ initBitbucket();
68
+ expect(verifyBitbucketWebhook('{"a":2}', sign("example-secret", '{"a":1}'))).toBe(false);
69
+ });
70
+
71
+ test("rejects a missing header", () => {
72
+ process.env.BITBUCKET_WEBHOOK_SECRET = "example-secret";
73
+ initBitbucket();
74
+ expect(verifyBitbucketWebhook("{}", undefined)).toBe(false);
75
+ });
76
+
77
+ test("rejects a bare hex digest without the sha256= prefix", () => {
78
+ process.env.BITBUCKET_WEBHOOK_SECRET = "example-secret";
79
+ initBitbucket();
80
+ const bare = sign("example-secret", "{}").slice("sha256=".length);
81
+ expect(verifyBitbucketWebhook("{}", bare)).toBe(false);
82
+ });
83
+
84
+ test("rejects everything before init and after reset", () => {
85
+ process.env.BITBUCKET_WEBHOOK_SECRET = "example-secret";
86
+ const sig = sign("example-secret", "{}");
87
+ expect(verifyBitbucketWebhook("{}", sig)).toBe(false);
88
+ initBitbucket();
89
+ expect(verifyBitbucketWebhook("{}", sig)).toBe(true);
90
+ resetBitbucket();
91
+ expect(verifyBitbucketWebhook("{}", sig)).toBe(false);
92
+ });
93
+ });
94
+
95
+ describe("initBitbucket", () => {
96
+ test("does not initialize when disabled", () => {
97
+ process.env.BITBUCKET_WEBHOOK_SECRET = "example-secret";
98
+ process.env.BITBUCKET_TOKEN = "example-token";
99
+ process.env.BITBUCKET_DISABLE = "true";
100
+ initBitbucket();
101
+ expect(getBitbucketToken()).toBeNull();
102
+ });
103
+
104
+ test("loads the API token", () => {
105
+ process.env.BITBUCKET_WEBHOOK_SECRET = "example-secret";
106
+ process.env.BITBUCKET_TOKEN = "example-token";
107
+ initBitbucket();
108
+ expect(getBitbucketToken()).toBe("example-token");
109
+ });
110
+ });
111
+
112
+ describe("resolveBitbucketBotAccountId", () => {
113
+ test("prefers BITBUCKET_BOT_ACCOUNT_ID", async () => {
114
+ process.env.BITBUCKET_BOT_ACCOUNT_ID = "557058:env-bot";
115
+ expect(await resolveBitbucketBotAccountId()).toBe("557058:env-bot");
116
+ });
117
+
118
+ test("returns null without a token", async () => {
119
+ expect(await resolveBitbucketBotAccountId()).toBeNull();
120
+ });
121
+
122
+ test("resolves GET /user with a Bearer token and caches it", async () => {
123
+ process.env.BITBUCKET_WEBHOOK_SECRET = "example-secret";
124
+ process.env.BITBUCKET_TOKEN = "example-token";
125
+ initBitbucket();
126
+ const calls: Array<{ url: string; auth?: string }> = [];
127
+ const realFetch = globalThis.fetch;
128
+ globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit) => {
129
+ const headers = (init?.headers ?? {}) as Record<string, string>;
130
+ calls.push({ url: String(input), auth: headers.Authorization });
131
+ return new Response(JSON.stringify({ account_id: "557058:api-bot" }), { status: 200 });
132
+ }) as typeof fetch;
133
+ try {
134
+ expect(await resolveBitbucketBotAccountId()).toBe("557058:api-bot");
135
+ expect(await resolveBitbucketBotAccountId()).toBe("557058:api-bot");
136
+ } finally {
137
+ globalThis.fetch = realFetch;
138
+ }
139
+ expect(calls).toEqual([
140
+ { url: "https://api.bitbucket.org/2.0/user", auth: "Bearer example-token" },
141
+ ]);
142
+ });
143
+
144
+ test("returns null on a failed lookup", async () => {
145
+ process.env.BITBUCKET_WEBHOOK_SECRET = "example-secret";
146
+ process.env.BITBUCKET_TOKEN = "example-token";
147
+ initBitbucket();
148
+ const realFetch = globalThis.fetch;
149
+ globalThis.fetch = (async () => new Response("nope", { status: 401 })) as typeof fetch;
150
+ try {
151
+ expect(await resolveBitbucketBotAccountId()).toBeNull();
152
+ } finally {
153
+ globalThis.fetch = realFetch;
154
+ }
155
+ });
156
+ });