@desplega.ai/agent-swarm 2.7.5 → 2.7.7

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 (319) hide show
  1. package/dist/{acp-adapter-g3ads55s.js → acp-adapter-7b4vk3h5.js} +5 -4
  2. package/dist/{actions-zwfykqqk.js → actions-3tt153af.js} +18 -17
  3. package/dist/{amp-adapter-z3s0ssdt.js → amp-adapter-njpts0yr.js} +1 -1
  4. package/dist/{anthropic-messages-x4q6rfq5.js → anthropic-messages-xey2skwx.js} +7 -7
  5. package/dist/{app-vmp4fgep.js → app-nky4378f.js} +3 -3
  6. package/dist/{assistant-vwde653f.js → assistant-jnb669pa.js} +24 -22
  7. package/dist/{azure-openai-responses-s1w5n70z.js → azure-openai-responses-26t7j9ra.js} +5 -5
  8. package/dist/{boot-reembed-375t7tbk.js → boot-reembed-9mgvaztt.js} +1 -1
  9. package/dist/{boot-reembed-gq9rcpnj.js → boot-reembed-qs2djdej.js} +1 -1
  10. package/dist/{boot-scrub-sweep-nd938kyf.js → boot-scrub-sweep-yjpdj7s1.js} +3 -3
  11. package/dist/{boot-seeds-aykx51v3.js → boot-seeds-5cm6ambd.js} +4 -4
  12. package/dist/{boxd-k1j26g3h.js → boxd-nv507va8.js} +5 -5
  13. package/dist/{claude-adapter-8j4hm099.js → claude-adapter-fn287zmm.js} +5 -4
  14. package/dist/{claude-managed-setup-0wyynpn1.js → claude-managed-setup-wtdz2mj2.js} +1 -1
  15. package/dist/{claude-sdk-session-q6wj5hmy.js → claude-sdk-session-x91kqyhm.js} +9677 -9514
  16. package/dist/{cli-j3jvnpe8.js → cli-08bmz9z9.js} +1 -1
  17. package/dist/{cli-gjr2y2xk.js → cli-0agcv1v5.js} +4 -1
  18. package/dist/{cli-y24sc29k.js → cli-1c6tzt8g.js} +30 -8
  19. package/dist/{cli-mjzynrse.js → cli-35rsp8ye.js} +1 -1
  20. package/dist/{cli-rpm2sgv0.js → cli-3fg0bdcv.js} +19 -2
  21. package/dist/{cli-nn4d8dne.js → cli-5b456bpf.js} +2 -2
  22. package/dist/{cli-bhjt02p8.js → cli-5jnhv72d.js} +1 -1
  23. package/dist/{cli-fqzx7gwa.js → cli-5zx699j9.js} +2 -2
  24. package/dist/{cli-hf6zt5m6.js → cli-69szn7w7.js} +17 -1
  25. package/dist/{cli-vqypxzh4.js → cli-6g0hb4cp.js} +8 -8
  26. package/dist/{cli-tqgg40ek.js → cli-6pfd54vx.js} +9 -8
  27. package/dist/{cli-bsn8nhrq.js → cli-6vcp76qk.js} +3 -3
  28. package/dist/{cli-zppwg9p9.js → cli-72v799xn.js} +1 -1
  29. package/dist/cli-82j36f2m.js +45 -0
  30. package/dist/{cli-hn9hxmz9.js → cli-8305gbq0.js} +29 -16
  31. package/dist/{cli-z8eh3ex8.js → cli-8sckt74b.js} +8 -8
  32. package/dist/{cli-bnz7a7qf.js → cli-95jzxcp3.js} +1 -1
  33. package/dist/{cli-0bxqnvq9.js → cli-adeegh7d.js} +29 -7
  34. package/dist/{cli-g61qxmge.js → cli-afkqkrtw.js} +3 -3
  35. package/dist/{cli-7a42nfy0.js → cli-ayayvfg4.js} +24 -4
  36. package/dist/{cli-bqt1mptt.js → cli-az48n0nr.js} +4 -4
  37. package/dist/{cli-z6jkx23q.js → cli-bfv7vdg6.js} +9 -9
  38. package/dist/{cli-pywf3eh5.js → cli-caj33vcq.js} +5 -5
  39. package/dist/{cli-5pag6rmn.js → cli-cb2a8pna.js} +1 -1
  40. package/dist/{cli-wvaawacd.js → cli-cppkf404.js} +5236 -72
  41. package/dist/{cli-mry0qzym.js → cli-ddaanqkd.js} +123 -10
  42. package/dist/{cli-0gey5fzy.js → cli-e0kmphd6.js} +2 -2
  43. package/dist/{cli-zh3fp8v2.js → cli-e46hg0xw.js} +1 -1
  44. package/dist/{cli-jm2vfpfd.js → cli-e5bzrn01.js} +1 -1
  45. package/dist/{cli-qxnf8z2z.js → cli-excnnjg2.js} +165 -153
  46. package/dist/{cli-71ptav7x.js → cli-ferkkrg9.js} +1 -1
  47. package/dist/{cli-vpf89f34.js → cli-fmjd0t1y.js} +3 -3
  48. package/dist/{cli-rm6hqj3z.js → cli-fyt2g4wd.js} +33 -139
  49. package/dist/cli-g08zdaec.js +55 -0
  50. package/dist/{cli-ehq736cn.js → cli-g7s47cc5.js} +4 -4
  51. package/dist/{cli-82ae98pz.js → cli-gz6q4eve.js} +3 -3
  52. package/dist/{cli-82jg7fp9.js → cli-he5nxaa8.js} +1 -1
  53. package/dist/{cli-rqkxrmgw.js → cli-je518txm.js} +39 -14
  54. package/dist/{cli-8hs41zy2.js → cli-jtjtabxe.js} +1 -1
  55. package/dist/{cli-611a1294.js → cli-k0wwfqbg.js} +18 -1
  56. package/dist/{cli-nzxr556s.js → cli-k1nje3v3.js} +38 -43
  57. package/dist/{cli-3h4nhjn8.js → cli-k8196sh2.js} +9 -7
  58. package/dist/{cli-afrrppsw.js → cli-k899ehvh.js} +2 -2
  59. package/dist/{cli-0731hvbw.js → cli-m0da8j35.js} +23 -9
  60. package/dist/cli-m2z8jagn.js +12078 -0
  61. package/dist/{cli-8veytvbh.js → cli-mkk9rnzz.js} +5 -5
  62. package/dist/{cli-7wspm5gm.js → cli-n6bv12jn.js} +528 -6
  63. package/dist/{cli-spknvxbq.js → cli-nmzhp1b5.js} +14 -12
  64. package/dist/{cli-p599f1qm.js → cli-p82cnnjq.js} +1 -1
  65. package/dist/{cli-xtjhfmpc.js → cli-pc6n7fre.js} +1 -1
  66. package/dist/{cli-vbyfwh92.js → cli-pjctyea3.js} +2 -2
  67. package/dist/{cli-nmbmy64z.js → cli-pq73h5mz.js} +3 -3
  68. package/dist/{cli-aa6yxc7m.js → cli-pz4rk021.js} +1 -1
  69. package/dist/{cli-7wcyk2s5.js → cli-q3tkegby.js} +1 -1
  70. package/dist/cli-qghe0h66.js +67 -0
  71. package/dist/{cli-6s7vdcta.js → cli-qh4d4tvk.js} +4 -4
  72. package/dist/{cli-mmwm7zbf.js → cli-rtt8g4v9.js} +1 -1
  73. package/dist/{cli-kg8hchyg.js → cli-sa02jsqt.js} +1 -1
  74. package/dist/{cli-1h3wcpbw.js → cli-snpj5zbj.js} +2 -2
  75. package/dist/{cli-a7v854rf.js → cli-tgrv9cjx.js} +263 -12
  76. package/dist/{cli-a47xy8eq.js → cli-vz22fyqv.js} +186 -54
  77. package/dist/{cli-1ayk1eas.js → cli-wk5yrfwd.js} +285 -42
  78. package/dist/{cli-1338796d.js → cli-ws19768n.js} +1755 -119
  79. package/dist/{cli-70r77n04.js → cli-xae4g51r.js} +1 -1
  80. package/dist/{cli-gf5hbmdr.js → cli-yjdgea32.js} +3 -3
  81. package/dist/{cli-xbkdm710.js → cli-yndmfxb3.js} +3 -3
  82. package/dist/{cli-rhw40jgb.js → cli-yyj3j5m4.js} +2 -2
  83. package/dist/cli.js +26 -25
  84. package/dist/{cloudflare-workers-ai-system-one-bm60fjm0.js → cloudflare-workers-ai-system-one-ef2a980q.js} +2 -2
  85. package/dist/{codex-adapter-q6k3gnyg.js → codex-adapter-cf2yxe9m.js} +3 -2
  86. package/dist/{codex-hook-9efm2x1m.js → codex-hook-a0c6g6t6.js} +1 -1
  87. package/dist/{codex-session-runner-3mwmk3d9.js → codex-session-runner-6vfzdgbd.js} +3 -2
  88. package/dist/{commands-evrenjn9.js → commands-t3mfy7vn.js} +9 -3
  89. package/dist/{controller-xjp8vswh.js → controller-q7xrbaz0.js} +8 -3
  90. package/dist/{controllers-connect-pp5jnxvc.js → controllers-connect-c6nm70kg.js} +20 -19
  91. package/dist/{cursor-adapter-yf47w4t0.js → cursor-adapter-ftd81ga8.js} +1 -1
  92. package/dist/{db-zxpw6aht.js → db-dk572x25.js} +3 -3
  93. package/dist/{db-rgfsh91f.js → db-vemtpmcc.js} +14 -4
  94. package/dist/{devtools-82fzqra8.js → devtools-qxfgbsqz.js} +1 -1
  95. package/dist/{dispatcher-smasgws7.js → dispatcher-kh7ecxh9.js} +17 -16
  96. package/dist/{docker-rrjhaqt9.js → docker-c1a3gqmf.js} +89 -22
  97. package/dist/{dsh-adapter-ftyy8dnp.js → dsh-adapter-v8628r89.js} +1 -1
  98. package/dist/{e2b-w1p0bnpm.js → e2b-fr12yccx.js} +5 -5
  99. package/dist/{e2b-qcaj675y.js → e2b-x6ywzak3.js} +3 -3
  100. package/dist/{easter-egg-3d-yyts7p16.js → easter-egg-3d-nenw4x63.js} +1 -1
  101. package/dist/{embedding-space-1z0mbg95.js → embedding-space-0jepb239.js} +6 -6
  102. package/dist/{embeddings-ygeg7tm7.js → embeddings-xxt49yxg.js} +1 -1
  103. package/dist/{execute-14qhqp4d.js → execute-1mhw6e4x.js} +1 -1
  104. package/dist/{external-channel-ttgrs3sx.js → external-channel-55v12e60.js} +2 -2
  105. package/dist/{fx-adapter-gt6pncj2.js → fx-adapter-bs8wqrk5.js} +6 -5
  106. package/dist/{google-generative-ai-yvmdxyxa.js → google-generative-ai-v94hr7h0.js} +5 -5
  107. package/dist/{google-vertex-jrtdh9gs.js → google-vertex-g7vr6cf8.js} +5 -5
  108. package/dist/{grok-adapter-sm4m5k4g.js → grok-adapter-gpjqmkcq.js} +6 -5
  109. package/dist/handlers-zxmp1ta4.js +32 -0
  110. package/dist/{heartbeat-qahz4p62.js → heartbeat-k42svnr0.js} +20 -19
  111. package/dist/{hook-4fd5wq5w.js → hook-114m7hap.js} +4 -3
  112. package/dist/{hook-kj3d398m.js → hook-6nphweh0.js} +24 -9
  113. package/dist/{http-1ted0j1k.js → http-r8cc4wz7.js} +1196 -211
  114. package/dist/index-a8phxqnk.js +37 -0
  115. package/dist/{index-q4b917zs.js → index-e3s8wmxy.js} +1 -1
  116. package/dist/{index-6rq3r5zb.js → index-fpk4bcd9.js} +1 -1
  117. package/dist/{index-5rk6rfzr.js → index-fvzke5kx.js} +1 -1
  118. package/dist/index-k6h00t4c.js +30 -0
  119. package/dist/{index-7v51awbn.js → index-nh3wyqdt.js} +1027 -577
  120. package/dist/{index-yqhz88yy.js → index-pxn30mse.js} +8 -8
  121. package/dist/index-xrz7te8h.js +33 -0
  122. package/dist/{keepalive-6cf311t5.js → keepalive-x060nt9p.js} +3 -3
  123. package/dist/lead-956sd2yc.js +71 -0
  124. package/dist/{maintenance-a4bm1bmt.js → maintenance-2xxfexhf.js} +1 -1
  125. package/dist/{mistral-conversations-4pmwj1c0.js → mistral-conversations-3gjpdb4b.js} +4 -4
  126. package/dist/{oauth-refresh-sweep-3h0dfw1k.js → oauth-refresh-sweep-fckc4ar5.js} +2 -2
  127. package/dist/{onboard-myn5hraa.js → onboard-zc48j237.js} +5 -5
  128. package/dist/{openai-codex-responses-44j573nm.js → openai-codex-responses-8pbxq9h5.js} +5 -5
  129. package/dist/{openai-completions-a4gee2a8.js → openai-completions-rm00s912.js} +4 -4
  130. package/dist/{openai-decisions-w4qkrcar.js → openai-decisions-8v957r2r.js} +1 -1
  131. package/dist/{openai-responses-rbp6h5kr.js → openai-responses-agxpf96k.js} +5 -5
  132. package/dist/{opencode-adapter-p2nkb5am.js → opencode-adapter-r690h1cr.js} +4 -3
  133. package/dist/{otel-impl-a1a0ef6b.js → otel-impl-r5xbh81q.js} +4 -4
  134. package/dist/{pi-mono-adapter-rzmpb72a.js → pi-mono-adapter-c7v8sdec.js} +7 -6
  135. package/dist/{pricing-refresh-83drrnxj.js → pricing-refresh-3pmre11v.js} +3 -3
  136. package/dist/{rbac-roles-s8y6ecb5.js → rbac-roles-3vbshwsb.js} +1 -1
  137. package/dist/{rbac-roles-0fb6e7pj.js → rbac-roles-910mmkgz.js} +2 -2
  138. package/dist/{render-v2-twmbgrz4.js → render-v2-pxykkbab.js} +3 -3
  139. package/dist/{resume-zfqc36xj.js → resume-adda0bwy.js} +6 -6
  140. package/dist/{runtime-qwggdpqr.js → runtime-fnbm7stk.js} +1 -1
  141. package/dist/{session-access-sc9pn9da.js → session-access-2fvamrdc.js} +4 -4
  142. package/dist/{session-source-backfill-qh07xr8g.js → session-source-backfill-08xvv0sm.js} +2 -2
  143. package/dist/{setup-4fr0m7a1.js → setup-1aqmvmh1.js} +3 -3
  144. package/dist/{typesafe-system-one-5mavm259.js → typesafe-system-one-0bmqcxfg.js} +2 -2
  145. package/dist/{virtual-modules-fh4ha15b.js → virtual-modules-d2pjk9j1.js} +5 -5
  146. package/dist/{work-objects-0wr8zxct.js → work-objects-9s0yrm5j.js} +20 -19
  147. package/dist/{worker-follow-up-ktyyazyp.js → worker-follow-up-3q78fqmr.js} +17 -16
  148. package/dist/worker-y15hy08h.js +71 -0
  149. package/openapi.json +289 -4
  150. package/package.json +9 -8
  151. package/src/apps/definition.ts +6 -5
  152. package/src/be/db/page-theme-tokens.ts +12 -0
  153. package/src/be/db/script-runs.ts +23 -0
  154. package/src/be/db/tasks/read.ts +131 -4
  155. package/src/be/db/tasks/write.ts +19 -0
  156. package/src/be/db-client.ts +114 -11
  157. package/src/be/db.ts +49 -6
  158. package/src/be/memory/providers/sqlite-store.ts +49 -12
  159. package/src/be/migrations/260_runtime_pool_cache.sql +20 -0
  160. package/src/be/migrations/262_session_visibility_indexes.sql +18 -0
  161. package/src/be/migrations/263_task_list_filter_index.sql +11 -0
  162. package/src/be/migrations/264_slack_thread_cover_index.sql +9 -0
  163. package/src/be/migrations/266_script_runs_source_task.sql +11 -0
  164. package/src/be/migrations/269_pages_design_system.sql +23 -0
  165. package/src/be/runtime-pools/cache-cleanup.ts +109 -0
  166. package/src/be/runtime-pools/db.ts +65 -6
  167. package/src/be/runtime-pools/remote-controller.ts +5 -0
  168. package/src/be/runtime-pools/supervisor.ts +33 -2
  169. package/src/be/scripts/typecheck.ts +22 -0
  170. package/src/be/seed/schedules-seeder.ts +0 -10
  171. package/src/be/seed-scripts/catalog/catalog-report.inline.ts +2 -0
  172. package/src/be/seed-scripts/catalog/catalog-report.ts +2 -0
  173. package/src/be/seed-scripts/catalog/memory-eval.ts +2 -0
  174. package/src/be/seed-scripts/catalog/swarm-update-check.ts +150 -0
  175. package/src/be/seed-scripts/catalog/task-failure-audit.ts +5 -1
  176. package/src/be/seed-scripts/index.ts +9 -0
  177. package/src/be/seed-skills/bundled-files.generated.json +3 -3
  178. package/src/be/swarm-config-guard.ts +32 -0
  179. package/src/be/task-auto-title-listener.ts +127 -0
  180. package/src/commands/controller.ts +4 -0
  181. package/src/commands/entrypoint-api-readiness.ts +74 -0
  182. package/src/commands/onboard/agent-fs-version.ts +1 -1
  183. package/src/commands/profile-sync.ts +151 -16
  184. package/src/commands/runner.ts +15 -7
  185. package/src/hooks/hook.ts +20 -9
  186. package/src/http/index.ts +4 -1
  187. package/src/http/list-total-cache.ts +14 -7
  188. package/src/http/memory.ts +38 -9
  189. package/src/http/pages-public.ts +93 -129
  190. package/src/http/pages.ts +94 -21
  191. package/src/http/script-runs.ts +174 -5
  192. package/src/http/status.ts +10 -2
  193. package/src/http/tasks.ts +4 -2
  194. package/src/pages/browser-sdk.ts +48 -21
  195. package/src/pages/color-literals.ts +115 -0
  196. package/src/pages/page-head.ts +766 -0
  197. package/src/pages/version.ts +3 -0
  198. package/src/providers/amp-adapter.ts +1 -1
  199. package/src/providers/codex-adapter.ts +1 -0
  200. package/src/providers/codex-app-server.ts +27 -1
  201. package/src/providers/fx-adapter.ts +1 -1
  202. package/src/providers/pi-mono-extension.ts +30 -15
  203. package/src/runtime-providers/controller-protocol.ts +14 -1
  204. package/src/runtime-providers/controller.ts +5 -0
  205. package/src/runtime-providers/docker.ts +108 -23
  206. package/src/runtime-providers/types.ts +16 -0
  207. package/src/script-workflows/workflow-ctx.ts +8 -0
  208. package/src/scripts-runtime/types/stdlib.d.ts +22 -0
  209. package/src/scripts-runtime/types/swarm-sdk.d.ts +22 -0
  210. package/src/server-user.ts +1 -0
  211. package/src/slack/commands.ts +5 -1
  212. package/src/slack/handlers.ts +37 -2
  213. package/src/slack/left-threads.ts +27 -4
  214. package/src/tasks/follow-up-gate.ts +318 -0
  215. package/src/tasks/follow-up-redelegation-guard.ts +155 -0
  216. package/src/tasks/worker-follow-up.ts +21 -1
  217. package/src/tests/claude-artifacts-read-script.test.ts +209 -0
  218. package/src/tests/codex-app-server.test.ts +59 -0
  219. package/src/tests/codex-gateway-route.test.ts +50 -1
  220. package/src/tests/create-page-tool.test.ts +501 -0
  221. package/src/tests/db-client-busy-retry.test.ts +81 -0
  222. package/src/tests/db-client.test.ts +21 -0
  223. package/src/tests/db-task-read-contract.test.ts +9 -2
  224. package/src/tests/db-task-write-contract.test.ts +7 -1
  225. package/src/tests/db-tasks-read.test.ts +21 -0
  226. package/src/tests/entrypoint-api-readiness.test.ts +416 -210
  227. package/src/tests/explicit-model-validation.test.ts +14 -8
  228. package/src/tests/fixtures/pages/head-v1.html +56 -0
  229. package/src/tests/follow-up-gate-shadow.test.ts +513 -0
  230. package/src/tests/follow-up-redelivery-guard.test.ts +432 -202
  231. package/src/tests/identity-field-budget.test.ts +1 -1
  232. package/src/tests/internal-ai/credentials.test.ts +62 -0
  233. package/src/tests/internal-ai/task-title.test.ts +283 -0
  234. package/src/tests/list-total-cache.test.ts +27 -2
  235. package/src/tests/memory-fts-populate.test.ts +11 -9
  236. package/src/tests/memory-retrieval-purge.test.ts +80 -0
  237. package/src/tests/merge-gate-detect-changes.test.ts +145 -0
  238. package/src/tests/migration-260-runtime-pool-cache.test.ts +118 -0
  239. package/src/tests/page-proxy.test.ts +1 -1
  240. package/src/tests/page-session-dev-cookie.test.ts +188 -0
  241. package/src/tests/pages-frame-ancestors.test.ts +28 -1
  242. package/src/tests/pages-head-v2.test.ts +534 -0
  243. package/src/tests/pages-http.test.ts +330 -0
  244. package/src/tests/pages-public-html.test.ts +43 -0
  245. package/src/tests/pages-storage.test.ts +182 -0
  246. package/src/tests/pages-versioning.test.ts +72 -0
  247. package/src/tests/profile-sync-stale-writeback.test.ts +334 -0
  248. package/src/tests/profile-sync.test.ts +11 -15
  249. package/src/tests/query-audit-plans.test.ts +27 -0
  250. package/src/tests/runtime-cache-cleanup.test.ts +367 -0
  251. package/src/tests/runtime-e2b-pools.test.ts +41 -0
  252. package/src/tests/runtime-pools-db.test.ts +98 -0
  253. package/src/tests/runtime-pools-http.test.ts +29 -0
  254. package/src/tests/runtime-providers-docker.test.ts +253 -0
  255. package/src/tests/runtime-remote-controller.test.ts +66 -2
  256. package/src/tests/runtime-supervisor.test.ts +66 -2
  257. package/src/tests/scheduled-task-required-params-migration.test.ts +3 -1
  258. package/src/tests/script-connections.test.ts +13 -0
  259. package/src/tests/script-runs-http.test.ts +684 -2
  260. package/src/tests/seed-registry.test.ts +28 -6
  261. package/src/tests/seed-schedules.test.ts +61 -8
  262. package/src/tests/seed-scripts.test.ts +168 -2
  263. package/src/tests/slack-leave-thread.test.ts +145 -5
  264. package/src/tests/slack-render-v2-query-plans.test.ts +24 -2
  265. package/src/tests/status.test.ts +11 -0
  266. package/src/tests/swarm-diff.test.ts +121 -2
  267. package/src/tests/task-auto-title-config-guard.test.ts +37 -0
  268. package/src/tests/task-auto-title-listener.test.ts +165 -0
  269. package/src/tests/tasks-readable-row.test.ts +155 -0
  270. package/src/tests/theme-import.test.ts +758 -0
  271. package/src/tests/themes-page-theme.test.ts +155 -0
  272. package/src/tests/themes-presets.test.ts +172 -0
  273. package/src/tests/workflow-script-principal.test.ts +2 -1
  274. package/src/themes/classics.ts +949 -0
  275. package/src/themes/import.ts +420 -0
  276. package/src/themes/page-fields.ts +40 -0
  277. package/src/themes/page-theme.ts +102 -0
  278. package/src/themes/presets.ts +561 -0
  279. package/src/themes/tokens.ts +218 -0
  280. package/src/tools/app-upsert.ts +1 -1
  281. package/src/tools/create-page.ts +118 -12
  282. package/src/tools/send-task.ts +35 -47
  283. package/src/tools/slack-leave-thread.ts +1 -1
  284. package/src/tools/task-dedup.ts +45 -21
  285. package/src/tools/utils.ts +33 -0
  286. package/src/types.ts +83 -3
  287. package/src/utils/constants.ts +19 -0
  288. package/src/utils/follow-up-gate-schedule-ids.ts +13 -0
  289. package/src/utils/internal-ai/credentials.ts +146 -93
  290. package/src/utils/internal-ai/index.ts +10 -0
  291. package/src/utils/internal-ai/models.ts +18 -0
  292. package/src/utils/internal-ai/task-title.ts +282 -0
  293. package/src/utils/page-session.ts +37 -0
  294. package/templates/schedules/daily-status-report/content.md +88 -2
  295. package/templates/schedules/daily-swarm-update-check/content.md +16 -40
  296. package/templates/skills/apps/SKILL.md +1 -1
  297. package/templates/skills/apps/content.md +1 -1
  298. package/templates/skills/composio-google-docs/content.md +10 -0
  299. package/templates/skills/heartbeat-runbook/content.md +1 -1
  300. package/templates/skills/pages/config.json +1 -1
  301. package/templates/skills/pages/content.md +539 -86
  302. package/templates/skills/pages/files/examples/annotated-pr.html +21 -12
  303. package/templates/skills/pages/files/examples/multiplayer-board.html +41 -41
  304. package/templates/skills/pages/files/examples/report-page.html +125 -124
  305. package/templates/skills/script-workflows/content.md +19 -1
  306. package/templates/skills/swarm-docs/content.md +1 -0
  307. package/templates/skills/swarm-scripts/SKILL.md +2 -0
  308. package/templates/skills/swarm-scripts/content.md +2 -0
  309. package/dist/cli-2pnswgh8.js +0 -189
  310. package/dist/cli-dznrgaep.js +0 -30
  311. package/dist/cli-w3hcrxht.js +0 -6985
  312. package/dist/handlers-vwmba1m2.js +0 -31
  313. package/dist/index-5r71rf98.js +0 -32
  314. package/dist/index-anmwxs87.js +0 -29
  315. package/dist/index-spp0ra5c.js +0 -36
  316. package/dist/lead-fjy418cw.js +0 -69
  317. package/dist/worker-0c5tyzqe.js +0 -69
  318. package/templates/schedules/daily-workflow-health-audit/config.json +0 -19
  319. package/templates/schedules/daily-workflow-health-audit/content.md +0 -191
@@ -1,24 +1,39 @@
1
- import { afterEach, describe, expect, test } from "bun:test";
1
+ import { afterAll, afterEach, beforeAll, describe, expect, test } from "bun:test";
2
2
  import { readFileSync } from "node:fs";
3
+ import { chmod, mkdtemp, rm } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { waitForApiReady } from "../commands/entrypoint-api-readiness";
7
+ import { CHILD_PROCESS_TEST_BUDGET_MS, runChild } from "./test-proc";
3
8
 
4
9
  /**
5
10
  * Regression tests for the `wait_for_api_ready` bounded /health poll in
6
- * docker-entrypoint.sh (Phase 2 of the baked-skills-to-db-seeding plan:
7
- * "Gate Worker Boot on API Readiness").
11
+ * docker-entrypoint.sh before provider configuration reads.
8
12
  *
9
- * The function is extracted verbatim from docker-entrypoint.sh between its
10
- * `# BEGIN wait_for_api_ready` / `# END wait_for_api_ready` markers and run
11
- * in a real bash subprocess, so this test tracks the actual deployed
12
- * behavior instead of a hand-written mirror that could silently drift.
13
- *
14
- * The subprocess MUST be spawned asynchronously (`Bun.spawn`, not
15
- * `Bun.spawnSync`): the mock `Bun.serve` server used below runs its `fetch`
16
- * handler on this same process's event loop, and `Bun.spawnSync` blocks that
17
- * event loop for its entire duration — the mock server would never be able
18
- * to answer a request from the very curl subprocess we're waiting on.
13
+ * Run the deployed shell shim with its Bun helper against a real HTTP server.
14
+ * Image paths for Bun and the helper are relocated to the test runtime and an isolated copy; the shell
15
+ * call, argument handling, exit status and TypeScript logic are the real ones.
16
+ * Injected clocks and probes also cover policy without wall-clock waits.
19
17
  */
20
18
 
19
+ const TEST_TIMEOUT = CHILD_PROCESS_TEST_BUDGET_MS;
21
20
  const entrypointPath = `${import.meta.dir}/../../docker-entrypoint.sh`;
21
+ let helperDir: string;
22
+ let helperPath: string;
23
+
24
+ beforeAll(async () => {
25
+ helperDir = await mkdtemp(join(tmpdir(), "swarm-readiness-"));
26
+ helperPath = join(helperDir, "entrypoint-api-readiness.ts");
27
+ // Copy only the file shipped by each image: no repo imports or node_modules.
28
+ await Bun.write(
29
+ helperPath,
30
+ Bun.file(`${import.meta.dir}/../commands/entrypoint-api-readiness.ts`),
31
+ );
32
+ });
33
+
34
+ afterAll(async () => {
35
+ await rm(helperDir, { recursive: true, force: true });
36
+ });
22
37
 
23
38
  function extractWaitForApiReady(): string {
24
39
  const script = readFileSync(entrypointPath, "utf8");
@@ -42,38 +57,23 @@ function extractWaitForApiReady(): string {
42
57
  throw new Error("Could not locate `# END wait_for_api_ready` marker in docker-entrypoint.sh.");
43
58
  }
44
59
 
45
- return script.slice(funcStart, endIndex);
46
- }
47
-
48
- interface RunResult {
49
- exitCode: number | null;
50
- stdout: string;
51
- stderr: string;
52
- durationMs: number;
60
+ return script
61
+ .slice(funcStart, endIndex)
62
+ .replace("/usr/local/bin/bun", '"$3"')
63
+ .replace("/usr/local/lib/agent-swarm/entrypoint-api-readiness.ts", '"$2"');
53
64
  }
54
65
 
55
66
  /** Run the real extracted helper against `url` in a fresh bash subprocess. */
56
67
  async function runWaitForApiReady(
57
68
  url: string,
58
69
  env: Record<string, string> = {},
59
- ): Promise<RunResult> {
70
+ ): Promise<Awaited<ReturnType<typeof runChild>>> {
60
71
  const funcSrc = extractWaitForApiReady();
61
- const script = `set -u\n${funcSrc}\nwait_for_api_ready "$1"\n`;
62
-
63
- const start = performance.now();
64
- const proc = Bun.spawn(["bash", "-c", script, "bash", url], {
72
+ const script = `set -eu\n${funcSrc}\nwait_for_api_ready "$1" "$2" "$3"\necho 'provider setup reached'\n`;
73
+ return runChild(["bash", "-c", script, "bash", url, helperPath, process.execPath], {
74
+ cwd: helperDir,
65
75
  env: { PATH: process.env.PATH ?? "", ...env },
66
- stdout: "pipe",
67
- stderr: "pipe",
68
76
  });
69
- const [stdout, stderr, exitCode] = await Promise.all([
70
- new Response(proc.stdout).text(),
71
- new Response(proc.stderr).text(),
72
- proc.exited,
73
- ]);
74
- const durationMs = performance.now() - start;
75
-
76
- return { exitCode, stdout, stderr, durationMs };
77
77
  }
78
78
 
79
79
  let server: ReturnType<typeof Bun.serve> | undefined;
@@ -98,12 +98,6 @@ describe("wait_for_api_ready: source contract", () => {
98
98
  expect(firstProviderReadIndex).toBeGreaterThan(gateCallIndex);
99
99
  });
100
100
 
101
- test("uses bounded per-attempt curl flags, not an unbounded request", () => {
102
- const funcSrc = extractWaitForApiReady();
103
- expect(funcSrc).toContain("--connect-timeout");
104
- expect(funcSrc).toContain("--max-time");
105
- });
106
-
107
101
  test("never references API_KEY or an Authorization header", () => {
108
102
  const funcSrc = extractWaitForApiReady();
109
103
  expect(funcSrc).not.toContain("API_KEY");
@@ -112,187 +106,399 @@ describe("wait_for_api_ready: source contract", () => {
112
106
  });
113
107
 
114
108
  describe("wait_for_api_ready: invalid timeout values", () => {
115
- test.each(["0", "-5", "abc"])("rejects %s with a stable fatal message", async (raw) => {
116
- const result = await runWaitForApiReady("http://127.0.0.1:1/health", {
117
- WORKER_API_READY_TIMEOUT_SECONDS: raw,
118
- });
119
- expect(result.exitCode).not.toBe(0);
120
- expect(result.stdout).toContain(
121
- `WORKER_API_READY_TIMEOUT_SECONDS must be a positive integer, got '${raw}'; exiting.`,
122
- );
123
- // Fails fast on the config check — must not attempt any network call.
124
- expect(result.durationMs).toBeLessThan(1000);
125
- });
126
-
127
- test("an explicitly-empty value falls back to the 90s default (bash :- semantics)", async () => {
128
- // `${VAR:-90}` treats "set but empty" the same as "unset" — the empty
129
- // branch in the case statement is defensive, not reachable via env.
130
- // Don't wait for the full 90s poll to finish: read the first log line,
131
- // then kill the still-polling process.
132
- const funcSrc = extractWaitForApiReady();
133
- const script = `set -u\n${funcSrc}\nwait_for_api_ready "$1"\n`;
134
- const proc = Bun.spawn(["bash", "-c", script, "bash", "http://127.0.0.1:1/health"], {
135
- env: {
136
- PATH: process.env.PATH ?? "",
109
+ test.each(["0", "-5", "abc"])(
110
+ "rejects %s with a stable fatal message",
111
+ async (raw) => {
112
+ const result = await runWaitForApiReady("http://unused.invalid", {
113
+ WORKER_API_READY_TIMEOUT_SECONDS: raw,
114
+ });
115
+ expect(result.exitCode).not.toBe(0);
116
+ expect(result.stdout).toContain(
117
+ `WORKER_API_READY_TIMEOUT_SECONDS must be a positive integer, got '${raw}'; exiting.`,
118
+ );
119
+ // Fails fast on the config check — must not attempt any network call.
120
+ expect(result.durationMs).toBeLessThan(5000);
121
+ expect(result.stdout).not.toContain("provider setup reached");
122
+ },
123
+ TEST_TIMEOUT,
124
+ );
125
+
126
+ test(
127
+ "an explicitly-empty value falls back to the 90s default (bash :- semantics)",
128
+ async () => {
129
+ server = Bun.serve({ port: 0, fetch: () => new Response("ok") });
130
+ const result = await runWaitForApiReady(server.url.toString(), {
137
131
  WORKER_API_READY_TIMEOUT_SECONDS: "",
138
- },
139
- stdout: "pipe",
140
- stderr: "pipe",
141
- });
142
-
143
- const reader = proc.stdout.getReader();
144
- const { value } = await reader.read();
145
- const firstLine = new TextDecoder().decode(value);
146
- reader.releaseLock();
147
- proc.kill();
148
- await proc.exited;
149
-
150
- expect(firstLine).toContain("(timeout 90s)");
151
- expect(firstLine).not.toContain("must be a positive integer");
152
- }, 10000);
132
+ });
133
+ expect(result.exitCode).toBe(0);
134
+ expect(result.stdout).toContain("(timeout 90s)");
135
+ expect(result.stdout).not.toContain("must be a positive integer");
136
+ },
137
+ TEST_TIMEOUT,
138
+ );
153
139
  });
154
140
 
155
141
  describe("wait_for_api_ready: immediate success", () => {
156
- test("succeeds on the first attempt and logs waiting + ready lines", async () => {
157
- server = Bun.serve({
158
- port: 0,
159
- hostname: "127.0.0.1",
160
- fetch(req) {
161
- const url = new URL(req.url);
162
- if (url.pathname === "/health") return new Response("ok", { status: 200 });
163
- return new Response("not found", { status: 404 });
164
- },
165
- });
166
-
167
- const result = await runWaitForApiReady(server.url.toString(), {
168
- WORKER_API_READY_TIMEOUT_SECONDS: "5",
169
- });
142
+ test(
143
+ "succeeds on the first attempt and logs waiting + ready lines",
144
+ async () => {
145
+ server = Bun.serve({
146
+ port: 0,
147
+ hostname: "127.0.0.1",
148
+ fetch(req) {
149
+ const url = new URL(req.url);
150
+ if (url.pathname === "/health") return new Response("ok", { status: 200 });
151
+ return new Response("not found", { status: 404 });
152
+ },
153
+ });
154
+
155
+ const result = await runWaitForApiReady(server.url.toString(), {
156
+ WORKER_API_READY_TIMEOUT_SECONDS: "5",
157
+ });
158
+
159
+ expect(result.exitCode).toBe(0);
160
+ expect(result.stdout).toContain("Waiting for API readiness at");
161
+ expect(result.stdout).toContain("API ready at");
162
+ expect(result.stdout).toContain("provider setup reached");
163
+ expect(result.durationMs).toBeLessThan(5000);
164
+ },
165
+ TEST_TIMEOUT,
166
+ );
167
+
168
+ test(
169
+ "normalizes a trailing slash so it requests exactly /health",
170
+ async () => {
171
+ let requestedPath = "";
172
+ server = Bun.serve({
173
+ port: 0,
174
+ hostname: "127.0.0.1",
175
+ fetch(req) {
176
+ requestedPath = new URL(req.url).pathname;
177
+ return new Response("ok", { status: 200 });
178
+ },
179
+ });
180
+
181
+ const trailingSlashUrl = server.url.toString(); // Bun.serve URLs already end in "/"
182
+ expect(trailingSlashUrl.endsWith("/")).toBe(true);
183
+
184
+ const result = await runWaitForApiReady(trailingSlashUrl, {
185
+ WORKER_API_READY_TIMEOUT_SECONDS: "5",
186
+ });
187
+
188
+ expect(result.exitCode).toBe(0);
189
+ expect(requestedPath).toBe("/health");
190
+ },
191
+ TEST_TIMEOUT,
192
+ );
193
+
194
+ test(
195
+ "never leaks a secret placed in the process environment",
196
+ async () => {
197
+ let authorization: string | null | undefined;
198
+ server = Bun.serve({
199
+ port: 0,
200
+ hostname: "127.0.0.1",
201
+ fetch(req) {
202
+ authorization = req.headers.get("authorization");
203
+ const url = new URL(req.url);
204
+ if (url.pathname === "/health") return new Response("ok", { status: 200 });
205
+ return new Response("not found", { status: 404 });
206
+ },
207
+ });
208
+
209
+ const secret = "example-api-ready-secret-do-not-leak";
210
+ const result = await runWaitForApiReady(server.url.toString(), {
211
+ WORKER_API_READY_TIMEOUT_SECONDS: "5",
212
+ API_KEY: secret,
213
+ });
214
+
215
+ expect(result.exitCode).toBe(0);
216
+ expect(result.stdout).not.toContain(secret);
217
+ expect(result.stderr).not.toContain(secret);
218
+ expect(authorization).toBeNull();
219
+ },
220
+ TEST_TIMEOUT,
221
+ );
222
+ });
170
223
 
171
- expect(result.exitCode).toBe(0);
172
- expect(result.stdout).toContain("Waiting for API readiness at");
173
- expect(result.stdout).toContain("API ready at");
174
- expect(result.durationMs).toBeLessThan(5000);
175
- }, 15000);
176
-
177
- test("normalizes a trailing slash so it requests exactly /health", async () => {
178
- let requestedPath = "";
179
- server = Bun.serve({
180
- port: 0,
181
- hostname: "127.0.0.1",
182
- fetch(req) {
183
- requestedPath = new URL(req.url).pathname;
184
- return new Response("ok", { status: 200 });
185
- },
186
- });
224
+ describe("wait_for_api_ready: transient failure then success", () => {
225
+ test(
226
+ "retries roughly once per second until the server recovers",
227
+ async () => {
228
+ // A per-test path prefix: an ephemeral port can be reused from another test
229
+ // process whose poller still hits /health, and those stray requests used
230
+ // to count as retries (3 requests in 1033 ms on CI).
231
+ const prefix = `/${crypto.randomUUID()}`;
232
+ const requestTimes: number[] = [];
233
+ server = Bun.serve({
234
+ port: 0,
235
+ hostname: "127.0.0.1",
236
+ fetch(req) {
237
+ const url = new URL(req.url);
238
+ if (url.pathname !== `${prefix}/health`)
239
+ return new Response("not found", { status: 404 });
240
+ requestTimes.push(performance.now());
241
+ if (requestTimes.length < 3) return new Response("unavailable", { status: 503 });
242
+ return new Response("ok", { status: 200 });
243
+ },
244
+ });
245
+
246
+ const result = await runWaitForApiReady(`${server.url.origin}${prefix}/`, {
247
+ WORKER_API_READY_TIMEOUT_SECONDS: "10",
248
+ });
249
+
250
+ expect(result.exitCode).toBe(0);
251
+ expect(requestTimes).toHaveLength(3);
252
+ expect(result.stdout).toContain("API ready at");
253
+ // Each retry waits out `sleep 1` after the previous response, so the
254
+ // server-side gap cannot shrink under load: paced, not a hot loop.
255
+ expect(requestTimes[1]! - requestTimes[0]!).toBeGreaterThanOrEqual(900);
256
+ expect(requestTimes[2]! - requestTimes[1]!).toBeGreaterThanOrEqual(900);
257
+ expect(result.durationMs).toBeLessThan(15000);
258
+ },
259
+ TEST_TIMEOUT,
260
+ );
261
+ });
187
262
 
188
- const trailingSlashUrl = server.url.toString(); // Bun.serve URLs already end in "/"
189
- expect(trailingSlashUrl.endsWith("/")).toBe(true);
263
+ describe("wait_for_api_ready: unreachable / timeout", () => {
264
+ test(
265
+ "exits non-zero with a stable fatal line after the bounded window",
266
+ async () => {
267
+ server = Bun.serve({ port: 0, fetch: () => new Response("unused") });
268
+ const url = `${server.url.origin}/${crypto.randomUUID()}`;
269
+ await server.stop(true);
270
+ server = undefined;
271
+ const result = await runWaitForApiReady(url, { WORKER_API_READY_TIMEOUT_SECONDS: "2" });
272
+
273
+ expect(result.exitCode).not.toBe(0);
274
+ expect(result.stdout).toContain(
275
+ `FATAL: API readiness timed out after 2s waiting for ${url}/health; exiting.`,
276
+ );
277
+ expect(result.stdout).not.toContain("provider setup reached");
278
+ expect(result.durationMs).toBeGreaterThanOrEqual(900);
279
+ expect(result.durationMs).toBeLessThan(15000);
280
+ },
281
+ TEST_TIMEOUT,
282
+ );
283
+
284
+ test(
285
+ "bounds total wall-clock time even against a server that never responds",
286
+ async () => {
287
+ server = Bun.serve({
288
+ port: 0,
289
+ hostname: "127.0.0.1",
290
+ fetch() {
291
+ // No response headers: the request deadline must abort the fetch.
292
+ return new Promise<Response>(() => {});
293
+ },
294
+ });
295
+
296
+ const result = await runWaitForApiReady(server.url.toString(), {
297
+ WORKER_API_READY_TIMEOUT_SECONDS: "2",
298
+ });
299
+
300
+ expect(result.exitCode).not.toBe(0);
301
+ expect(result.stdout).toContain("FATAL: API readiness timed out after 2s waiting for");
302
+ expect(result.durationMs).toBeLessThan(15000);
303
+ },
304
+ TEST_TIMEOUT,
305
+ );
306
+ });
190
307
 
191
- const result = await runWaitForApiReady(trailingSlashUrl, {
192
- WORKER_API_READY_TIMEOUT_SECONDS: "5",
193
- });
308
+ describe("API readiness step with injected dependencies", () => {
309
+ test.each([
310
+ "0",
311
+ "-1",
312
+ "abc",
313
+ "1.5",
314
+ "Infinity",
315
+ "9007199254740992",
316
+ ])("rejects %s before probing or sleeping", async (value) => {
317
+ let probes = 0;
318
+ let sleeps = 0;
319
+ const logs: string[] = [];
320
+ expect(
321
+ await waitForApiReady("http://api", value, {
322
+ fetch: async () => {
323
+ probes++;
324
+ return new Response("ok");
325
+ },
326
+ sleep: async () => {
327
+ sleeps++;
328
+ },
329
+ log: (message) => logs.push(message),
330
+ }),
331
+ ).toBe(false);
332
+ expect(probes).toBe(0);
333
+ expect(sleeps).toBe(0);
334
+ expect(logs).toHaveLength(1);
335
+ expect(logs[0]).toContain("must be a positive integer");
336
+ });
194
337
 
195
- expect(result.exitCode).toBe(0);
196
- expect(requestedPath).toBe("/health");
197
- }, 15000);
198
-
199
- test("never leaks a secret placed in the process environment", async () => {
200
- server = Bun.serve({
201
- port: 0,
202
- hostname: "127.0.0.1",
203
- fetch(req) {
204
- const url = new URL(req.url);
205
- if (url.pathname === "/health") return new Response("ok", { status: 200 });
206
- return new Response("not found", { status: 404 });
338
+ test("paces HTTP and connection failures until the API recovers", async () => {
339
+ let now = 0;
340
+ let probes = 0;
341
+ const sleeps: number[] = [];
342
+ const logs: string[] = [];
343
+ const ready = await waitForApiReady("http://api/prefix/", "5", {
344
+ now: () => now,
345
+ fetch: async (url, init) => {
346
+ expect(url).toBe("http://api/prefix/health");
347
+ expect(init.redirect).toBe("manual");
348
+ expect(init.signal).toBeInstanceOf(AbortSignal);
349
+ if (++probes === 1) return new Response("booting", { status: 503 });
350
+ if (probes === 2) throw new Error("connection reset");
351
+ return new Response("ok");
207
352
  },
208
- });
209
-
210
- const secret = "example-api-ready-secret-do-not-leak";
211
- const result = await runWaitForApiReady(server.url.toString(), {
212
- WORKER_API_READY_TIMEOUT_SECONDS: "5",
213
- API_KEY: secret,
214
- });
215
-
216
- expect(result.exitCode).toBe(0);
217
- expect(result.stdout).not.toContain(secret);
218
- expect(result.stderr).not.toContain(secret);
219
- }, 15000);
220
- });
221
-
222
- describe("wait_for_api_ready: transient failure then success", () => {
223
- test("retries roughly once per second until the server recovers", async () => {
224
- // A per-test path prefix: an ephemeral port can be reused from another test
225
- // process whose poller still hits /health, and those stray requests used
226
- // to count as retries (3 requests in 1033 ms on CI).
227
- const prefix = `/${crypto.randomUUID()}`;
228
- const requestTimes: number[] = [];
229
- server = Bun.serve({
230
- port: 0,
231
- hostname: "127.0.0.1",
232
- fetch(req) {
233
- const url = new URL(req.url);
234
- if (url.pathname !== `${prefix}/health`) return new Response("not found", { status: 404 });
235
- requestTimes.push(performance.now());
236
- if (requestTimes.length < 3) return new Response("unavailable", { status: 503 });
237
- return new Response("ok", { status: 200 });
353
+ sleep: async (ms) => {
354
+ sleeps.push(ms);
355
+ now += ms;
238
356
  },
357
+ log: (message) => logs.push(message),
239
358
  });
359
+ expect(ready).toBe(true);
360
+ expect(probes).toBe(3);
361
+ expect(sleeps).toEqual([1000, 1000]);
362
+ expect(logs.at(-1)).toContain("API ready at");
363
+ });
240
364
 
241
- const result = await runWaitForApiReady(`${server.url.origin}${prefix}/`, {
242
- WORKER_API_READY_TIMEOUT_SECONDS: "10",
243
- });
244
-
245
- expect(result.exitCode).toBe(0);
246
- expect(requestTimes).toHaveLength(3);
247
- expect(result.stdout).toContain("API ready at");
248
- // Each retry waits out `sleep 1` after the previous response, so the
249
- // server-side gap cannot shrink under load: paced, not a hot loop.
250
- expect(requestTimes[1]! - requestTimes[0]!).toBeGreaterThanOrEqual(900);
251
- expect(requestTimes[2]! - requestTimes[1]!).toBeGreaterThanOrEqual(900);
252
- expect(result.durationMs).toBeLessThan(15000);
253
- }, 20000);
254
- });
255
-
256
- describe("wait_for_api_ready: unreachable / timeout", () => {
257
- test("exits non-zero with a stable fatal line after the bounded window", async () => {
258
- // Port 1 is a well-known privileged port nothing is listening on inside
259
- // this sandbox; the connection is refused immediately and deterministically.
260
- const url = "http://127.0.0.1:1";
261
- const result = await runWaitForApiReady(url, { WORKER_API_READY_TIMEOUT_SECONDS: "2" });
262
-
263
- expect(result.exitCode).not.toBe(0);
264
- expect(result.stdout).toContain(
265
- `FATAL: API readiness timed out after 2s waiting for ${url}/health; exiting.`,
365
+ test("stops at the overall deadline without an unbounded retry loop", async () => {
366
+ let now = 0;
367
+ const logs: string[] = [];
368
+ expect(
369
+ await waitForApiReady("http://api", "2", {
370
+ now: () => now,
371
+ fetch: async () => new Response("unavailable", { status: 503 }),
372
+ sleep: async (ms) => {
373
+ now += ms;
374
+ },
375
+ log: (message) => logs.push(message),
376
+ }),
377
+ ).toBe(false);
378
+ expect(now).toBe(2000);
379
+ expect(logs.at(-1)).toBe(
380
+ "[entrypoint] FATAL: API readiness timed out after 2s waiting for http://api/health; exiting.",
266
381
  );
267
- // Bounded: at least one retry happened (not instant) and it never runs
268
- // substantially longer than the configured timeout. The lower bound is
269
- // deliberately loose: the deadline is computed from `date +%s`, which has
270
- // 1-second granularity, so a 2s timeout can legitimately resolve in ~1-3
271
- // real seconds depending on where "now" lands within the current second.
272
- // That slop is proportionally invisible at the real 90s production default.
273
- expect(result.durationMs).toBeGreaterThanOrEqual(900);
274
- expect(result.durationMs).toBeLessThan(15000);
275
- }, 20000);
276
-
277
- test("bounds total wall-clock time even against a server that never responds", async () => {
278
- server = Bun.serve({
279
- port: 0,
280
- hostname: "127.0.0.1",
281
- fetch() {
282
- // Never resolves — simulates a hung/unresponsive API. curl's
283
- // --max-time must cut this off rather than hanging indefinitely.
284
- return new Promise<Response>(() => {});
285
- },
286
- });
382
+ });
287
383
 
288
- const result = await runWaitForApiReady(server.url.toString(), {
289
- WORKER_API_READY_TIMEOUT_SECONDS: "2",
290
- });
384
+ test("a successful response arriving after the deadline does not continue boot", async () => {
385
+ let now = 0;
386
+ const logs: string[] = [];
387
+ expect(
388
+ await waitForApiReady("http://api", "1", {
389
+ now: () => now,
390
+ fetch: async () => {
391
+ now = 1001;
392
+ return new Response("ok");
393
+ },
394
+ log: (message) => logs.push(message),
395
+ }),
396
+ ).toBe(false);
397
+ expect(logs.at(-1)).toContain("FATAL: API readiness timed out");
398
+ expect(logs.some((message) => message.includes("API ready at"))).toBe(false);
399
+ });
400
+ });
291
401
 
292
- expect(result.exitCode).not.toBe(0);
293
- expect(result.stdout).toContain("FATAL: API readiness timed out after 2s waiting for");
294
- // Per-attempt --max-time 3 plus the 2s deadline check bounds this well
295
- // under a runaway hang, but above the raw 2s deadline.
296
- expect(result.durationMs).toBeLessThan(15000);
297
- }, 20000);
402
+ describe("API readiness HTTP compatibility", () => {
403
+ test.each(["headers", "body"])(
404
+ "retries a stalled %s request after the three-second request cap",
405
+ async (phase) => {
406
+ const prefix = `/${crypto.randomUUID()}`;
407
+ const requestTimes: number[] = [];
408
+ server = Bun.serve({
409
+ port: 0,
410
+ hostname: "127.0.0.1",
411
+ fetch(req) {
412
+ if (new URL(req.url).pathname !== `${prefix}/health`)
413
+ return new Response("not found", { status: 404 });
414
+ requestTimes.push(performance.now());
415
+ if (requestTimes.length > 1) return new Response("ok");
416
+ if (phase === "headers") return new Promise<Response>(() => {});
417
+ return new Response(
418
+ new ReadableStream({
419
+ start(controller) {
420
+ controller.enqueue(new TextEncoder().encode("partial"));
421
+ },
422
+ }),
423
+ );
424
+ },
425
+ });
426
+
427
+ const result = await runWaitForApiReady(`${server.url.origin}${prefix}`, {
428
+ WORKER_API_READY_TIMEOUT_SECONDS: "8",
429
+ });
430
+
431
+ expect(result.exitCode).toBe(0);
432
+ expect(requestTimes).toHaveLength(2);
433
+ // Three seconds to abort, then the existing one-second retry spacing.
434
+ expect(requestTimes[1]! - requestTimes[0]!).toBeGreaterThanOrEqual(3500);
435
+ expect(requestTimes[1]! - requestTimes[0]!).toBeLessThan(7000);
436
+ expect(result.stdout).toContain("API ready at");
437
+ expect(result.stdout).toContain("provider setup reached");
438
+ },
439
+ TEST_TIMEOUT,
440
+ );
441
+
442
+ test(
443
+ "uses the image's Bun even when a worker-writable PATH shadows bun",
444
+ async () => {
445
+ const shadow = join(helperDir, "bun");
446
+ await Bun.write(shadow, "#!/bin/sh\necho 'untrusted bun was executed'\nexit 99\n");
447
+ await chmod(shadow, 0o755);
448
+ server = Bun.serve({ port: 0, fetch: () => new Response("ok") });
449
+ const result = await runWaitForApiReady(server.url.origin, {
450
+ WORKER_API_READY_TIMEOUT_SECONDS: "2",
451
+ PATH: `${helperDir}:${process.env.PATH ?? ""}`,
452
+ });
453
+ expect(result.exitCode).toBe(0);
454
+ expect(result.stdout).not.toContain("untrusted bun was executed");
455
+ expect(result.stdout).toContain("provider setup reached");
456
+ },
457
+ TEST_TIMEOUT,
458
+ );
459
+
460
+ test(
461
+ "accepts a redirect without following it, like curl -f without -L",
462
+ async () => {
463
+ const paths: string[] = [];
464
+ server = Bun.serve({
465
+ port: 0,
466
+ fetch(req) {
467
+ paths.push(new URL(req.url).pathname);
468
+ return Response.redirect(`${server!.url.origin}/should-not-be-requested`, 302);
469
+ },
470
+ });
471
+ const result = await runWaitForApiReady(server.url.origin, {
472
+ WORKER_API_READY_TIMEOUT_SECONDS: "2",
473
+ });
474
+ expect(result.exitCode).toBe(0);
475
+ expect(paths).toEqual(["/health"]);
476
+ },
477
+ TEST_TIMEOUT,
478
+ );
479
+
480
+ test(
481
+ "bounds a response body that never finishes after successful headers",
482
+ async () => {
483
+ server = Bun.serve({
484
+ port: 0,
485
+ fetch: () =>
486
+ new Response(
487
+ new ReadableStream({
488
+ start(controller) {
489
+ controller.enqueue(new TextEncoder().encode("partial"));
490
+ },
491
+ }),
492
+ ),
493
+ });
494
+ const result = await runWaitForApiReady(server.url.origin, {
495
+ WORKER_API_READY_TIMEOUT_SECONDS: "1",
496
+ });
497
+ expect(result.exitCode).not.toBe(0);
498
+ expect(result.stdout).toContain("FATAL: API readiness timed out after 1s");
499
+ expect(result.stdout).not.toContain("provider setup reached");
500
+ expect(result.durationMs).toBeLessThan(15000);
501
+ },
502
+ TEST_TIMEOUT,
503
+ );
298
504
  });