@desplega.ai/agent-swarm 2.7.6 → 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 (244) hide show
  1. package/dist/{acp-adapter-bp91d37f.js → acp-adapter-7b4vk3h5.js} +5 -4
  2. package/dist/{actions-yj0eeaqh.js → actions-3tt153af.js} +18 -18
  3. package/dist/{amp-adapter-4pg9h34q.js → amp-adapter-njpts0yr.js} +1 -1
  4. package/dist/{anthropic-messages-x4q6rfq5.js → anthropic-messages-xey2skwx.js} +7 -7
  5. package/dist/{app-909e97cr.js → app-nky4378f.js} +3 -3
  6. package/dist/{assistant-qdyb5fyy.js → assistant-jnb669pa.js} +19 -19
  7. package/dist/{azure-openai-responses-s1w5n70z.js → azure-openai-responses-26t7j9ra.js} +5 -5
  8. package/dist/{boot-reembed-fk0s9z18.js → boot-reembed-9mgvaztt.js} +1 -1
  9. package/dist/{boot-reembed-gs1rzrnh.js → boot-reembed-qs2djdej.js} +1 -1
  10. package/dist/{boot-scrub-sweep-4qfd4kfx.js → boot-scrub-sweep-yjpdj7s1.js} +3 -3
  11. package/dist/{boot-seeds-yr00k13d.js → boot-seeds-5cm6ambd.js} +4 -4
  12. package/dist/{boxd-vdmfssd9.js → boxd-nv507va8.js} +1 -1
  13. package/dist/{claude-adapter-qta1f1xn.js → claude-adapter-fn287zmm.js} +5 -4
  14. package/dist/{claude-managed-setup-sc6j4dm0.js → claude-managed-setup-wtdz2mj2.js} +1 -1
  15. package/dist/{claude-sdk-session-zgrz6yvc.js → claude-sdk-session-x91kqyhm.js} +8288 -8224
  16. package/dist/{cli-wp9sp1qb.js → cli-08bmz9z9.js} +1 -1
  17. package/dist/{cli-yy3rhsdw.js → cli-1c6tzt8g.js} +7 -7
  18. package/dist/{cli-q0k1w5zd.js → cli-35rsp8ye.js} +1 -1
  19. package/dist/{cli-xnrwazav.js → cli-3fg0bdcv.js} +1 -1
  20. package/dist/{cli-89knwags.js → cli-5b456bpf.js} +2 -2
  21. package/dist/{cli-drr5v6av.js → cli-5jnhv72d.js} +1 -1
  22. package/dist/{cli-dxae2yg7.js → cli-5zx699j9.js} +2 -2
  23. package/dist/{cli-0at8w34w.js → cli-6g0hb4cp.js} +8 -8
  24. package/dist/{cli-e3r2neck.js → cli-6pfd54vx.js} +5 -5
  25. package/dist/{cli-bsn8nhrq.js → cli-6vcp76qk.js} +3 -3
  26. package/dist/{cli-ekwks2vs.js → cli-72v799xn.js} +1 -1
  27. package/dist/cli-82j36f2m.js +45 -0
  28. package/dist/{cli-yx4kfngt.js → cli-8305gbq0.js} +29 -16
  29. package/dist/{cli-4necwtsv.js → cli-8sckt74b.js} +8 -8
  30. package/dist/{cli-vvpx3gsx.js → cli-95jzxcp3.js} +1 -1
  31. package/dist/{cli-hr2e2fft.js → cli-adeegh7d.js} +7 -7
  32. package/dist/{cli-m48g6p95.js → cli-afkqkrtw.js} +3 -3
  33. package/dist/{cli-0qatpy8q.js → cli-ayayvfg4.js} +24 -4
  34. package/dist/{cli-bqt1mptt.js → cli-az48n0nr.js} +4 -4
  35. package/dist/{cli-zm72anhg.js → cli-bfv7vdg6.js} +9 -9
  36. package/dist/{cli-hgaazk69.js → cli-caj33vcq.js} +5 -5
  37. package/dist/{cli-3ypk9vk1.js → cli-cb2a8pna.js} +1 -1
  38. package/dist/{cli-waqbxg08.js → cli-cppkf404.js} +17 -17
  39. package/dist/{cli-yn4kxs06.js → cli-ddaanqkd.js} +4 -4
  40. package/dist/{cli-386tyxxg.js → cli-e0kmphd6.js} +2 -2
  41. package/dist/{cli-54t00gt8.js → cli-e46hg0xw.js} +1 -1
  42. package/dist/{cli-jm2vfpfd.js → cli-e5bzrn01.js} +1 -1
  43. package/dist/{cli-436532ck.js → cli-excnnjg2.js} +165 -153
  44. package/dist/{cli-71ptav7x.js → cli-ferkkrg9.js} +1 -1
  45. package/dist/{cli-vpf89f34.js → cli-fmjd0t1y.js} +3 -3
  46. package/dist/{cli-pd7vagab.js → cli-fyt2g4wd.js} +3 -3
  47. package/dist/{cli-gdhpcbhf.js → cli-g08zdaec.js} +1 -1
  48. package/dist/{cli-ehq736cn.js → cli-g7s47cc5.js} +4 -4
  49. package/dist/{cli-svj5090z.js → cli-gz6q4eve.js} +3 -3
  50. package/dist/{cli-8snvgs5r.js → cli-je518txm.js} +10 -10
  51. package/dist/{cli-rmmmrjkd.js → cli-jtjtabxe.js} +1 -1
  52. package/dist/{cli-611a1294.js → cli-k0wwfqbg.js} +18 -1
  53. package/dist/{cli-es6va8jk.js → cli-k1nje3v3.js} +38 -43
  54. package/dist/{cli-p5gp8pcx.js → cli-k8196sh2.js} +6 -6
  55. package/dist/{cli-nwj0ee2w.js → cli-k899ehvh.js} +2 -2
  56. package/dist/{cli-decfb6v8.js → cli-m0da8j35.js} +23 -9
  57. package/dist/{cli-he9wchme.js → cli-mkk9rnzz.js} +4 -4
  58. package/dist/{cli-nhrvpaj7.js → cli-n6bv12jn.js} +510 -4
  59. package/dist/{cli-8bc1w0tw.js → cli-nmzhp1b5.js} +14 -12
  60. package/dist/{cli-0yv5z2pg.js → cli-p82cnnjq.js} +1 -1
  61. package/dist/{cli-c60vetmm.js → cli-pc6n7fre.js} +1 -1
  62. package/dist/{cli-xds5f77d.js → cli-pjctyea3.js} +2 -2
  63. package/dist/{cli-xbbndvjy.js → cli-pq73h5mz.js} +3 -3
  64. package/dist/{cli-aa6yxc7m.js → cli-pz4rk021.js} +1 -1
  65. package/dist/{cli-xnqgpshy.js → cli-q3tkegby.js} +1 -1
  66. package/dist/cli-qghe0h66.js +67 -0
  67. package/dist/{cli-zbbwez0r.js → cli-qh4d4tvk.js} +4 -4
  68. package/dist/{cli-w6dt9ged.js → cli-rtt8g4v9.js} +1 -1
  69. package/dist/{cli-1h3wcpbw.js → cli-snpj5zbj.js} +2 -2
  70. package/dist/{cli-a7v854rf.js → cli-tgrv9cjx.js} +263 -12
  71. package/dist/{cli-faxe6thg.js → cli-vz22fyqv.js} +186 -54
  72. package/dist/{cli-k2gzwsj2.js → cli-wk5yrfwd.js} +160 -26
  73. package/dist/{cli-shpbx5ee.js → cli-ws19768n.js} +1641 -49
  74. package/dist/{cli-70r77n04.js → cli-xae4g51r.js} +1 -1
  75. package/dist/{cli-kqdg1bsb.js → cli-yndmfxb3.js} +2 -2
  76. package/dist/{cli-bjb3z51z.js → cli-yyj3j5m4.js} +2 -2
  77. package/dist/cli.js +21 -21
  78. package/dist/{cloudflare-workers-ai-system-one-bm60fjm0.js → cloudflare-workers-ai-system-one-ef2a980q.js} +2 -2
  79. package/dist/{codex-adapter-ayd1fb6h.js → codex-adapter-cf2yxe9m.js} +3 -2
  80. package/dist/{codex-hook-9efm2x1m.js → codex-hook-a0c6g6t6.js} +1 -1
  81. package/dist/{codex-session-runner-jesa32xc.js → codex-session-runner-6vfzdgbd.js} +3 -2
  82. package/dist/{commands-hanewgf2.js → commands-t3mfy7vn.js} +3 -3
  83. package/dist/{controller-dt1n3c1q.js → controller-q7xrbaz0.js} +3 -3
  84. package/dist/{controllers-connect-xx8x64n5.js → controllers-connect-c6nm70kg.js} +19 -19
  85. package/dist/{cursor-adapter-9s4dwwdw.js → cursor-adapter-ftd81ga8.js} +1 -1
  86. package/dist/{db-3vttcbv4.js → db-dk572x25.js} +3 -3
  87. package/dist/{db-9dv0g1ab.js → db-vemtpmcc.js} +4 -2
  88. package/dist/{dispatcher-tx9hd1sv.js → dispatcher-kh7ecxh9.js} +17 -17
  89. package/dist/{dsh-adapter-007y4ejs.js → dsh-adapter-v8628r89.js} +1 -1
  90. package/dist/{e2b-we6hs6q5.js → e2b-fr12yccx.js} +1 -1
  91. package/dist/{e2b-a19d28rz.js → e2b-x6ywzak3.js} +1 -1
  92. package/dist/{easter-egg-3d-rgbrqb8s.js → easter-egg-3d-nenw4x63.js} +1 -1
  93. package/dist/{embedding-space-ny1e2940.js → embedding-space-0jepb239.js} +6 -6
  94. package/dist/{embeddings-2t386t13.js → embeddings-xxt49yxg.js} +1 -1
  95. package/dist/{execute-pksh0ceh.js → execute-1mhw6e4x.js} +1 -1
  96. package/dist/{external-channel-phgmhmd7.js → external-channel-55v12e60.js} +2 -2
  97. package/dist/{fx-adapter-fy8rr921.js → fx-adapter-bs8wqrk5.js} +6 -5
  98. package/dist/{google-generative-ai-8y3m3k4b.js → google-generative-ai-v94hr7h0.js} +5 -5
  99. package/dist/{google-vertex-w3v98n7b.js → google-vertex-g7vr6cf8.js} +5 -5
  100. package/dist/{grok-adapter-q0abvpaj.js → grok-adapter-gpjqmkcq.js} +6 -5
  101. package/dist/handlers-zxmp1ta4.js +32 -0
  102. package/dist/{heartbeat-kadyek8p.js → heartbeat-k42svnr0.js} +20 -20
  103. package/dist/{hook-9k551xnj.js → hook-114m7hap.js} +4 -3
  104. package/dist/{hook-5zs3msf6.js → hook-6nphweh0.js} +24 -9
  105. package/dist/{http-mhz9vw1f.js → http-r8cc4wz7.js} +1025 -173
  106. package/dist/index-a8phxqnk.js +37 -0
  107. package/dist/{index-vdygbq10.js → index-e3s8wmxy.js} +1 -1
  108. package/dist/{index-q361cg2f.js → index-fpk4bcd9.js} +1 -1
  109. package/dist/index-k6h00t4c.js +30 -0
  110. package/dist/{index-rvr00sg7.js → index-nh3wyqdt.js} +1017 -574
  111. package/dist/index-xrz7te8h.js +33 -0
  112. package/dist/{keepalive-w56f32cg.js → keepalive-x060nt9p.js} +3 -3
  113. package/dist/lead-956sd2yc.js +71 -0
  114. package/dist/{maintenance-7mdgxcea.js → maintenance-2xxfexhf.js} +1 -1
  115. package/dist/{mistral-conversations-4pmwj1c0.js → mistral-conversations-3gjpdb4b.js} +4 -4
  116. package/dist/{oauth-refresh-sweep-29txy9vk.js → oauth-refresh-sweep-fckc4ar5.js} +2 -2
  117. package/dist/{onboard-kpsrkarj.js → onboard-zc48j237.js} +3 -3
  118. package/dist/{openai-codex-responses-44j573nm.js → openai-codex-responses-8pbxq9h5.js} +5 -5
  119. package/dist/{openai-completions-a4gee2a8.js → openai-completions-rm00s912.js} +4 -4
  120. package/dist/{openai-decisions-w4qkrcar.js → openai-decisions-8v957r2r.js} +1 -1
  121. package/dist/{openai-responses-rbp6h5kr.js → openai-responses-agxpf96k.js} +5 -5
  122. package/dist/{opencode-adapter-j25vm3rr.js → opencode-adapter-r690h1cr.js} +4 -3
  123. package/dist/{otel-impl-6kkct7a8.js → otel-impl-r5xbh81q.js} +4 -4
  124. package/dist/{pi-mono-adapter-exhkzhn2.js → pi-mono-adapter-c7v8sdec.js} +7 -6
  125. package/dist/{pricing-refresh-n2x7jh6k.js → pricing-refresh-3pmre11v.js} +3 -3
  126. package/dist/{rbac-roles-81ypx6es.js → rbac-roles-3vbshwsb.js} +1 -1
  127. package/dist/{rbac-roles-b5htkjby.js → rbac-roles-910mmkgz.js} +2 -2
  128. package/dist/{render-v2-qn6jmpk4.js → render-v2-pxykkbab.js} +3 -3
  129. package/dist/{resume-wd30wx23.js → resume-adda0bwy.js} +6 -6
  130. package/dist/{runtime-4hsg7h60.js → runtime-fnbm7stk.js} +1 -1
  131. package/dist/{session-access-5qvfevhk.js → session-access-2fvamrdc.js} +4 -4
  132. package/dist/{session-source-backfill-n1czavbr.js → session-source-backfill-08xvv0sm.js} +2 -2
  133. package/dist/{setup-dnb56qy5.js → setup-1aqmvmh1.js} +1 -1
  134. package/dist/{typesafe-system-one-5mavm259.js → typesafe-system-one-0bmqcxfg.js} +2 -2
  135. package/dist/{virtual-modules-76xxv9fs.js → virtual-modules-d2pjk9j1.js} +5 -5
  136. package/dist/{work-objects-4n21tfkm.js → work-objects-9s0yrm5j.js} +20 -20
  137. package/dist/{worker-follow-up-77hvzxww.js → worker-follow-up-3q78fqmr.js} +17 -17
  138. package/dist/worker-y15hy08h.js +71 -0
  139. package/openapi.json +197 -4
  140. package/package.json +5 -5
  141. package/src/apps/definition.ts +6 -5
  142. package/src/be/db/page-theme-tokens.ts +12 -0
  143. package/src/be/db/tasks/read.ts +9 -1
  144. package/src/be/db/tasks/write.ts +19 -0
  145. package/src/be/db-client.ts +107 -7
  146. package/src/be/db.ts +34 -2
  147. package/src/be/migrations/269_pages_design_system.sql +23 -0
  148. package/src/be/seed/schedules-seeder.ts +0 -10
  149. package/src/be/seed-scripts/catalog/catalog-report.inline.ts +2 -0
  150. package/src/be/seed-scripts/catalog/catalog-report.ts +2 -0
  151. package/src/be/seed-scripts/catalog/memory-eval.ts +2 -0
  152. package/src/be/seed-scripts/catalog/swarm-update-check.ts +150 -0
  153. package/src/be/seed-scripts/index.ts +9 -0
  154. package/src/be/seed-skills/bundled-files.generated.json +3 -3
  155. package/src/be/swarm-config-guard.ts +32 -0
  156. package/src/be/task-auto-title-listener.ts +127 -0
  157. package/src/commands/onboard/agent-fs-version.ts +1 -1
  158. package/src/commands/profile-sync.ts +151 -16
  159. package/src/commands/runner.ts +15 -7
  160. package/src/hooks/hook.ts +20 -9
  161. package/src/http/index.ts +3 -0
  162. package/src/http/pages-public.ts +93 -129
  163. package/src/http/pages.ts +94 -21
  164. package/src/pages/browser-sdk.ts +48 -21
  165. package/src/pages/color-literals.ts +115 -0
  166. package/src/pages/page-head.ts +766 -0
  167. package/src/pages/version.ts +3 -0
  168. package/src/providers/amp-adapter.ts +1 -1
  169. package/src/providers/codex-adapter.ts +1 -0
  170. package/src/providers/codex-app-server.ts +27 -1
  171. package/src/providers/pi-mono-extension.ts +30 -15
  172. package/src/tasks/follow-up-gate.ts +318 -0
  173. package/src/tasks/worker-follow-up.ts +21 -1
  174. package/src/tests/codex-app-server.test.ts +59 -0
  175. package/src/tests/codex-gateway-route.test.ts +50 -1
  176. package/src/tests/create-page-tool.test.ts +501 -0
  177. package/src/tests/db-client-busy-retry.test.ts +81 -0
  178. package/src/tests/db-task-read-contract.test.ts +1 -0
  179. package/src/tests/db-task-write-contract.test.ts +2 -0
  180. package/src/tests/db-tasks-read.test.ts +21 -0
  181. package/src/tests/fixtures/pages/head-v1.html +56 -0
  182. package/src/tests/follow-up-gate-shadow.test.ts +513 -0
  183. package/src/tests/identity-field-budget.test.ts +1 -1
  184. package/src/tests/internal-ai/credentials.test.ts +62 -0
  185. package/src/tests/internal-ai/task-title.test.ts +283 -0
  186. package/src/tests/merge-gate-detect-changes.test.ts +145 -0
  187. package/src/tests/page-proxy.test.ts +1 -1
  188. package/src/tests/page-session-dev-cookie.test.ts +188 -0
  189. package/src/tests/pages-frame-ancestors.test.ts +28 -1
  190. package/src/tests/pages-head-v2.test.ts +534 -0
  191. package/src/tests/pages-http.test.ts +330 -0
  192. package/src/tests/pages-public-html.test.ts +43 -0
  193. package/src/tests/pages-storage.test.ts +182 -0
  194. package/src/tests/pages-versioning.test.ts +72 -0
  195. package/src/tests/profile-sync-stale-writeback.test.ts +334 -0
  196. package/src/tests/profile-sync.test.ts +11 -15
  197. package/src/tests/scheduled-task-required-params-migration.test.ts +3 -1
  198. package/src/tests/script-connections.test.ts +2 -0
  199. package/src/tests/seed-registry.test.ts +28 -6
  200. package/src/tests/seed-schedules.test.ts +61 -8
  201. package/src/tests/seed-scripts.test.ts +142 -2
  202. package/src/tests/swarm-diff.test.ts +121 -2
  203. package/src/tests/task-auto-title-config-guard.test.ts +37 -0
  204. package/src/tests/task-auto-title-listener.test.ts +165 -0
  205. package/src/tests/theme-import.test.ts +758 -0
  206. package/src/tests/themes-page-theme.test.ts +155 -0
  207. package/src/tests/themes-presets.test.ts +172 -0
  208. package/src/tests/workflow-script-principal.test.ts +2 -1
  209. package/src/themes/classics.ts +949 -0
  210. package/src/themes/import.ts +420 -0
  211. package/src/themes/page-fields.ts +40 -0
  212. package/src/themes/page-theme.ts +102 -0
  213. package/src/themes/presets.ts +561 -0
  214. package/src/themes/tokens.ts +218 -0
  215. package/src/tools/app-upsert.ts +1 -1
  216. package/src/tools/create-page.ts +118 -12
  217. package/src/tools/utils.ts +33 -0
  218. package/src/types.ts +45 -3
  219. package/src/utils/constants.ts +19 -0
  220. package/src/utils/follow-up-gate-schedule-ids.ts +13 -0
  221. package/src/utils/internal-ai/credentials.ts +146 -93
  222. package/src/utils/internal-ai/index.ts +10 -0
  223. package/src/utils/internal-ai/models.ts +18 -0
  224. package/src/utils/internal-ai/task-title.ts +282 -0
  225. package/src/utils/page-session.ts +37 -0
  226. package/templates/schedules/daily-status-report/content.md +88 -2
  227. package/templates/schedules/daily-swarm-update-check/content.md +16 -40
  228. package/templates/skills/apps/SKILL.md +1 -1
  229. package/templates/skills/apps/content.md +1 -1
  230. package/templates/skills/heartbeat-runbook/content.md +1 -1
  231. package/templates/skills/pages/config.json +1 -1
  232. package/templates/skills/pages/content.md +539 -86
  233. package/templates/skills/pages/files/examples/annotated-pr.html +21 -12
  234. package/templates/skills/pages/files/examples/multiplayer-board.html +41 -41
  235. package/templates/skills/pages/files/examples/report-page.html +125 -124
  236. package/dist/cli-2pnswgh8.js +0 -189
  237. package/dist/handlers-by3sfxyx.js +0 -32
  238. package/dist/index-4f2pvxae.js +0 -37
  239. package/dist/index-98gz7tmp.js +0 -30
  240. package/dist/index-k08z2cak.js +0 -33
  241. package/dist/lead-enq35nqp.js +0 -70
  242. package/dist/worker-gg1576j3.js +0 -70
  243. package/templates/schedules/daily-workflow-health-audit/config.json +0 -19
  244. package/templates/schedules/daily-workflow-health-audit/content.md +0 -191
@@ -0,0 +1,334 @@
1
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import {
5
+ advanceIdentityBaselines,
6
+ bootIdentityBaselines,
7
+ CLAUDE_MD_LINEAGE_PATH,
8
+ CLAUDE_MD_PATH,
9
+ collectProfilePayloads,
10
+ contentSha256,
11
+ type FileReader,
12
+ HEARTBEAT_MD_PATH,
13
+ IDENTITY_BASELINES_PATH,
14
+ IDENTITY_MD_PATH,
15
+ type ProfileFileIO,
16
+ SETUP_SCRIPT_PATH,
17
+ SOUL_MD_PATH,
18
+ syncProfileFilesToServer,
19
+ TOOLS_MD_PATH,
20
+ WORKSPACE_CLAUDE_MD_PATH,
21
+ } from "../commands/profile-sync";
22
+ import {
23
+ type SwarmHooksConfig,
24
+ syncIdentityFilesToServer,
25
+ syncSetupScriptToServer,
26
+ } from "../providers/pi-mono-extension";
27
+
28
+ // A file the agent never touched still holds the copy written at boot. A sync
29
+ // that sends it anyway writes that stale copy over any DB-side edit made since
30
+ // (Lead's update-profile, a nightly profile edit). These tests hold the DB
31
+ // edit in a fake profile API and check that it survives the agent's sync.
32
+
33
+ const BOOT_IDENTITY = `# Identity\n\n${"boot ".repeat(120)}\n\n## Self-Evolution\n\nold section\n`;
34
+ const EDITED_IDENTITY = `# Identity\n\n${"boot ".repeat(120)}\n`;
35
+
36
+ /** In-memory stand-in for the worker's workspace and /tmp baselines. */
37
+ function memoryIO(files: Record<string, string>): ProfileFileIO & {
38
+ files: Record<string, string>;
39
+ } {
40
+ return {
41
+ files,
42
+ readFile: async (path) => files[path],
43
+ writeFile: async (path, content) => {
44
+ files[path] = content;
45
+ },
46
+ baselinesLockPath: join(tmpdir(), `identity-baselines-${crypto.randomUUID()}.lock`),
47
+ };
48
+ }
49
+
50
+ /**
51
+ * Holds each read of the baselines file until a second read arrives (or a
52
+ * short timeout), after both have captured the file as it was: the
53
+ * interleaving in which two unserialized updates drop one another's hashes.
54
+ */
55
+ function pairBaselineReads(io: ReturnType<typeof memoryIO>): typeof io {
56
+ let waiting: (() => void) | null = null;
57
+ return {
58
+ ...io,
59
+ readFile: async (path) => {
60
+ const content = io.files[path];
61
+ if (path !== IDENTITY_BASELINES_PATH) return content;
62
+ if (waiting) {
63
+ const partner = waiting;
64
+ waiting = null;
65
+ partner();
66
+ return content;
67
+ }
68
+ await new Promise<void>((resolve) => {
69
+ waiting = resolve;
70
+ setTimeout(() => {
71
+ if (waiting === resolve) waiting = null;
72
+ resolve();
73
+ }, 300);
74
+ });
75
+ return content;
76
+ },
77
+ };
78
+ }
79
+
80
+ function baselinesOf(io: ReturnType<typeof memoryIO>): Record<string, string> {
81
+ return JSON.parse(io.files[IDENTITY_BASELINES_PATH] ?? "{}");
82
+ }
83
+
84
+ /** The workspace right after runner boot: files and baselines from the DB. */
85
+ function bootedWorkspace(profile: Record<string, string>) {
86
+ const baselines: Record<string, string> = {};
87
+ for (const [field, content] of Object.entries(profile)) {
88
+ baselines[field] = contentSha256(content);
89
+ }
90
+ return memoryIO({
91
+ [IDENTITY_MD_PATH]: profile.identityMd ?? "",
92
+ [IDENTITY_BASELINES_PATH]: JSON.stringify(baselines),
93
+ });
94
+ }
95
+
96
+ describe("profile sync never writes an untouched file over a DB edit", () => {
97
+ let db: Record<string, string> = {};
98
+ let server: ReturnType<typeof Bun.serve>;
99
+ let config: SwarmHooksConfig;
100
+
101
+ beforeAll(() => {
102
+ server = Bun.serve({
103
+ port: 0,
104
+ async fetch(req) {
105
+ if (req.method !== "PUT") return new Response("not found", { status: 404 });
106
+ const body = (await req.json()) as Record<string, string>;
107
+ for (const [field, value] of Object.entries(body)) {
108
+ if (field !== "changeSource") db[field] = value;
109
+ }
110
+ return Response.json({ ok: true });
111
+ },
112
+ });
113
+ config = {
114
+ apiUrl: `http://localhost:${server.port}`,
115
+ apiKey: "test-key",
116
+ agentId: "agent-1",
117
+ taskId: "task-1",
118
+ isLead: false,
119
+ };
120
+ });
121
+
122
+ afterAll(() => {
123
+ server.stop(true);
124
+ });
125
+
126
+ test("opencode/pi session end keeps a DB edit made during the session when IDENTITY.md is untouched", async () => {
127
+ db = { identityMd: BOOT_IDENTITY };
128
+ const io = bootedWorkspace({ identityMd: BOOT_IDENTITY });
129
+
130
+ // Mid-session DB edit; the agent never touches /workspace/IDENTITY.md.
131
+ db.identityMd = EDITED_IDENTITY;
132
+
133
+ await syncIdentityFilesToServer(config, "session_sync", undefined, io);
134
+
135
+ expect(db.identityMd).toBe(EDITED_IDENTITY);
136
+ });
137
+
138
+ test("a synced local edit advances the baseline, so a later DB edit survives the next session end", async () => {
139
+ db = { identityMd: BOOT_IDENTITY };
140
+ const io = bootedWorkspace({ identityMd: BOOT_IDENTITY });
141
+ const agentEdit = `${BOOT_IDENTITY}\n## Learned\n\nagent edit\n`;
142
+
143
+ io.files[IDENTITY_MD_PATH] = agentEdit;
144
+ await syncIdentityFilesToServer(config, "self_edit", undefined, io);
145
+ expect(db.identityMd).toBe(agentEdit);
146
+
147
+ db.identityMd = EDITED_IDENTITY;
148
+ await syncIdentityFilesToServer(config, "session_sync", undefined, io);
149
+
150
+ expect(db.identityMd).toBe(EDITED_IDENTITY);
151
+ });
152
+
153
+ test("a real local edit still syncs", async () => {
154
+ db = { identityMd: BOOT_IDENTITY };
155
+ const io = bootedWorkspace({ identityMd: BOOT_IDENTITY });
156
+ io.files[IDENTITY_MD_PATH] = EDITED_IDENTITY;
157
+
158
+ await syncIdentityFilesToServer(config, "session_sync", undefined, io);
159
+
160
+ expect(db.identityMd).toBe(EDITED_IDENTITY);
161
+ });
162
+
163
+ test("an untouched start-up.sh does not overwrite a DB setupScript edit", async () => {
164
+ db = { setupScript: "echo boot" };
165
+ const io = memoryIO({
166
+ [SETUP_SCRIPT_PATH]: "#!/bin/bash\necho boot\n",
167
+ [IDENTITY_BASELINES_PATH]: JSON.stringify({ setupScript: contentSha256("echo boot") }),
168
+ });
169
+ db.setupScript = "echo edited";
170
+
171
+ await syncSetupScriptToServer(config, "session_sync", io);
172
+
173
+ expect(db.setupScript).toBe("echo edited");
174
+ });
175
+
176
+ const runnerSync = (
177
+ io: ProfileFileIO,
178
+ opts: Partial<Parameters<typeof syncProfileFilesToServer>[0]>,
179
+ ) =>
180
+ syncProfileFilesToServer({
181
+ agentId: config.agentId,
182
+ apiUrl: config.apiUrl,
183
+ apiKey: config.apiKey,
184
+ changeSource: "session_sync",
185
+ io,
186
+ ...opts,
187
+ });
188
+
189
+ test("an accepted workspace CLAUDE.md edit advances its baseline, so a later DB edit survives the next session end", async () => {
190
+ const boot = "# CLAUDE.md\n\nboot rules\n";
191
+ db = { claudeMd: boot };
192
+ const io = memoryIO({
193
+ [WORKSPACE_CLAUDE_MD_PATH]: boot,
194
+ [IDENTITY_BASELINES_PATH]: JSON.stringify({ claudeMd: contentSha256(boot) }),
195
+ });
196
+ const sync = () =>
197
+ runnerSync(io, { fields: ["claude"], claudeMdPath: WORKSPACE_CLAUDE_MD_PATH });
198
+
199
+ // Session 1: a codex/opencode agent edits /workspace/CLAUDE.md.
200
+ const agentEdit = `${boot}\n## Learned\n\nagent rule\n`;
201
+ io.files[WORKSPACE_CLAUDE_MD_PATH] = agentEdit;
202
+ await sync();
203
+ expect(db.claudeMd).toBe(agentEdit);
204
+
205
+ // Lead edits the DB; session 2 never touches the file.
206
+ db.claudeMd = "# CLAUDE.md\n\nlead rules\n";
207
+ await sync();
208
+
209
+ expect(db.claudeMd).toBe("# CLAUDE.md\n\nlead rules\n");
210
+ });
211
+
212
+ test("an accepted personal CLAUDE.md sync keeps the hook's lineage and the boot baseline", async () => {
213
+ const boot = "# CLAUDE.md\n\nboot rules\n";
214
+ const lineage = JSON.stringify({ written: contentSha256(boot), base: contentSha256(boot) });
215
+ const bootBaselines = JSON.stringify({ claudeMd: contentSha256(boot) });
216
+ db = { claudeMd: boot };
217
+ const io = memoryIO({
218
+ [CLAUDE_MD_PATH]: `${boot}\nagent rule\n`,
219
+ [CLAUDE_MD_LINEAGE_PATH]: lineage,
220
+ [IDENTITY_BASELINES_PATH]: bootBaselines,
221
+ });
222
+
223
+ await runnerSync(io, {
224
+ fields: ["claude"],
225
+ claudeMdPath: CLAUDE_MD_PATH,
226
+ claudeMdLock: { path: join(tmpdir(), `claude-md-${crypto.randomUUID()}.lock`) },
227
+ });
228
+
229
+ expect(db.claudeMd).toBe(`${boot}\nagent rule\n`);
230
+ expect(db.expectedHashes).toEqual({ claudeMd: contentSha256(boot) });
231
+ expect(io.files[CLAUDE_MD_LINEAGE_PATH]).toBe(lineage);
232
+ expect(io.files[IDENTITY_BASELINES_PATH]).toBe(bootBaselines);
233
+ });
234
+
235
+ test("two concurrent baseline updates both persist their hashes", async () => {
236
+ const io = memoryIO({
237
+ [IDENTITY_BASELINES_PATH]: JSON.stringify({ soulMd: contentSha256("soul") }),
238
+ });
239
+ const paired = pairBaselineReads(io);
240
+
241
+ await Promise.all([
242
+ advanceIdentityBaselines({ identityMd: "identity, edited" }, paired),
243
+ advanceIdentityBaselines({ setupScript: "echo edited" }, paired),
244
+ ]);
245
+
246
+ expect(baselinesOf(io)).toEqual({
247
+ soulMd: contentSha256("soul"),
248
+ identityMd: contentSha256("identity, edited"),
249
+ setupScript: contentSha256("echo edited"),
250
+ });
251
+ });
252
+
253
+ test("concurrent runner and pi syncs both advance their baselines, so neither file is re-sent over a later DB edit", async () => {
254
+ db = { identityMd: BOOT_IDENTITY, setupScript: "echo boot" };
255
+ const io = bootedWorkspace({ identityMd: BOOT_IDENTITY });
256
+ io.files[IDENTITY_BASELINES_PATH] = JSON.stringify({
257
+ identityMd: contentSha256(BOOT_IDENTITY),
258
+ setupScript: contentSha256("echo boot"),
259
+ });
260
+ io.files[IDENTITY_MD_PATH] = EDITED_IDENTITY;
261
+ io.files[SETUP_SCRIPT_PATH] = "#!/bin/bash\necho edited\n";
262
+ const paired = pairBaselineReads(io);
263
+
264
+ // The runner's session-end sync races the pi extension's on-edit sync.
265
+ await Promise.all([
266
+ runnerSync(paired, { fields: ["identity"] }),
267
+ syncSetupScriptToServer(config, "self_edit", paired),
268
+ ]);
269
+ expect(db).toEqual({ identityMd: EDITED_IDENTITY, setupScript: "echo edited" });
270
+
271
+ db = { identityMd: "lead identity", setupScript: "echo lead" };
272
+ await runnerSync(io, { fields: ["identity", "setup"] });
273
+ await syncIdentityFilesToServer(config, "session_sync", undefined, io);
274
+ await syncSetupScriptToServer(config, "session_sync", io);
275
+
276
+ expect(db).toEqual({ identityMd: "lead identity", setupScript: "echo lead" });
277
+ });
278
+
279
+ test("an empty HEARTBEAT.md that survives a restart is not sent over a later DB edit", async () => {
280
+ // Boot after a restart: the DB value is empty, so the runner leaves the
281
+ // surviving empty file in place and records the boot baselines.
282
+ db = { heartbeatMd: "" };
283
+ const io = memoryIO({ [HEARTBEAT_MD_PATH]: "" });
284
+ io.files[IDENTITY_BASELINES_PATH] = JSON.stringify(
285
+ bootIdentityBaselines({ heartbeatMd: db.heartbeatMd }, undefined),
286
+ );
287
+
288
+ db.heartbeatMd = "- check the queue";
289
+ await runnerSync(io, { fields: ["identity"] });
290
+ expect(db.heartbeatMd).toBe("- check the queue");
291
+
292
+ // A real edit of the empty file still syncs.
293
+ io.files[HEARTBEAT_MD_PATH] = "- agent checklist";
294
+ await runnerSync(io, { fields: ["identity"] });
295
+ expect(db.heartbeatMd).toBe("- agent checklist");
296
+ });
297
+ });
298
+
299
+ describe("collectProfilePayloads (claude hook + runner)", () => {
300
+ const reader =
301
+ (files: Record<string, string>): FileReader =>
302
+ async (path) =>
303
+ files[path];
304
+
305
+ test("Lead's self_edit of HEARTBEAT.md does not resend a stale IDENTITY.md", async () => {
306
+ const files = reader({
307
+ [SOUL_MD_PATH]: BOOT_IDENTITY,
308
+ [IDENTITY_MD_PATH]: BOOT_IDENTITY,
309
+ [TOOLS_MD_PATH]: "tools",
310
+ [HEARTBEAT_MD_PATH]: "heartbeat, edited",
311
+ [IDENTITY_BASELINES_PATH]: JSON.stringify({
312
+ soulMd: contentSha256(BOOT_IDENTITY),
313
+ identityMd: contentSha256(BOOT_IDENTITY),
314
+ toolsMd: contentSha256("tools"),
315
+ heartbeatMd: contentSha256("heartbeat"),
316
+ }),
317
+ });
318
+
319
+ const payloads = await collectProfilePayloads(["identity"], "self_edit", files);
320
+
321
+ expect(payloads.map((payload) => payload.body)).toEqual([
322
+ { heartbeatMd: "heartbeat, edited", changeSource: "self_edit" },
323
+ ]);
324
+ });
325
+
326
+ test("session_sync skips an untouched start-up.sh", async () => {
327
+ const files = reader({
328
+ [SETUP_SCRIPT_PATH]: "#!/bin/bash\necho boot\n",
329
+ [IDENTITY_BASELINES_PATH]: JSON.stringify({ setupScript: contentSha256("echo boot") }),
330
+ });
331
+
332
+ expect(await collectProfilePayloads(["setup"], "session_sync", files)).toEqual([]);
333
+ });
334
+ });
@@ -439,7 +439,7 @@ describe("postProfileUpdate (non-2xx is surfaced, not swallowed)", () => {
439
439
  new Response("boom", { status: 500, statusText: "Server Error" })) as typeof fetch;
440
440
  try {
441
441
  // Must resolve (non-fatal), not reject.
442
- await expect(postProfileUpdate({ ...opts, fetchImpl }, payload)).resolves.toBeUndefined();
442
+ await expect(postProfileUpdate({ ...opts, fetchImpl }, payload)).resolves.toBe(false);
443
443
  expect(warnSpy).toHaveBeenCalledTimes(1);
444
444
  const msg = String(warnSpy.mock.calls[0]?.[0]);
445
445
  expect(msg).toContain("setup sync failed");
@@ -455,7 +455,7 @@ describe("postProfileUpdate (non-2xx is surfaced, not swallowed)", () => {
455
455
  throw new Error("network down");
456
456
  }) as typeof fetch;
457
457
  try {
458
- await expect(postProfileUpdate({ ...opts, fetchImpl }, payload)).resolves.toBeUndefined();
458
+ await expect(postProfileUpdate({ ...opts, fetchImpl }, payload)).resolves.toBe(false);
459
459
  expect(warnSpy).toHaveBeenCalledTimes(1);
460
460
  expect(String(warnSpy.mock.calls[0]?.[0])).toContain("setup sync errored");
461
461
  } finally {
@@ -634,27 +634,23 @@ describe("collectProfilePayloads (baseline integration)", () => {
634
634
  expect(payloads[0]?.body.identityMd).toBeUndefined();
635
635
  });
636
636
 
637
- test("self_edit bypasses baselines (agent explicitly changed the file)", async () => {
638
- const identityContent = LONG;
639
-
637
+ test("self_edit skips the identity files the agent did not edit", async () => {
638
+ // An edit to one file triggers a sync of all of them; the untouched ones
639
+ // still hold the boot-time copy, which may predate a DB-side edit.
640
640
  const files = reader({
641
- [SOUL_MD_PATH]: identityContent,
642
- [IDENTITY_MD_PATH]: identityContent,
643
- [TOOLS_MD_PATH]: "tools",
641
+ [SOUL_MD_PATH]: LONG,
642
+ [IDENTITY_MD_PATH]: LONG,
643
+ [TOOLS_MD_PATH]: "edited tools",
644
644
  [IDENTITY_BASELINES_PATH]: JSON.stringify({
645
- soulMd: contentSha256(identityContent),
646
- identityMd: contentSha256(identityContent),
645
+ soulMd: contentSha256(LONG),
646
+ identityMd: contentSha256(LONG),
647
647
  toolsMd: contentSha256("tools"),
648
648
  }),
649
649
  });
650
650
 
651
651
  const payloads = await collectProfilePayloads(["identity"], "self_edit", files);
652
- expect(payloads).toHaveLength(3);
653
- // self_edit should include ALL files regardless of baselines, independently.
654
652
  expect(payloads.map((payload) => payload.body)).toEqual([
655
- { soulMd: identityContent, changeSource: "self_edit" },
656
- { identityMd: identityContent, changeSource: "self_edit" },
657
- { toolsMd: "tools", changeSource: "self_edit" },
653
+ { toolsMd: "edited tools", changeSource: "self_edit" },
658
654
  ]);
659
655
  });
660
656
 
@@ -104,7 +104,9 @@ describe("scheduled task required params repair", () => {
104
104
  });
105
105
 
106
106
  test("keeps every requirement on genuinely tokenized fresh templates", async () => {
107
- for (const name of canonicalNames.filter((name) => name !== "dream-daily")) {
107
+ // Migrations 141 and 151 still name these, but no template ships for them.
108
+ const unshipped = ["dream-daily", "daily-workflow-health-audit"];
109
+ for (const name of canonicalNames.filter((name) => !unshipped.includes(name))) {
108
110
  const template = new URL(`../../templates/schedules/${name}/content.md`, import.meta.url);
109
111
  insert(name, await Bun.file(template).text());
110
112
  }
@@ -678,6 +678,8 @@ describe("script connections", () => {
678
678
  markMigrationApplied(database, "264_slack_thread_cover_index.sql");
679
679
  // 266 alters script_runs, which this migration-112-only fixture does not create.
680
680
  markMigrationApplied(database, "266_script_runs_source_task.sql");
681
+ // 269 alters pages, which this migration-112-only fixture does not create.
682
+ markMigrationApplied(database, "269_pages_design_system.sql");
681
683
 
682
684
  const id = crypto.randomUUID();
683
685
  const now = new Date().toISOString();
@@ -1,9 +1,17 @@
1
1
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
2
  import { unlink } from "node:fs/promises";
3
- import { closeDb, getScheduledTasks, initDb, listWorkflows } from "../be/db";
3
+ import {
4
+ closeDb,
5
+ getScheduledTaskByName,
6
+ getScheduledTasks,
7
+ initDb,
8
+ listWorkflows,
9
+ } from "../be/db";
10
+ import { getScript } from "../be/scripts/db";
4
11
  import { runSeeder, SEEDERS } from "../be/seed";
5
12
  import { schedulesSeeder } from "../be/seed/schedules-seeder";
6
13
  import { workflowsSeeder } from "../be/seed/workflows-seeder";
14
+ import { scriptsSeeder } from "../be/seed-scripts";
7
15
 
8
16
  const TEST_DB_PATH = "./test-seed-registry.sqlite";
9
17
 
@@ -58,15 +66,29 @@ describe("seeder registry", () => {
58
66
  }
59
67
  });
60
68
 
61
- test("seeds all 23 automation templates on a fresh database and re-runs as a no-op", async () => {
69
+ test("seeds all 22 automation templates on a fresh database and re-runs as a no-op", async () => {
62
70
  expect(await listWorkflows()).toHaveLength(0);
63
71
  expect(await getScheduledTasks()).toHaveLength(0);
64
72
 
73
+ // Boot order: scripts seed before schedules, so a script-target schedule
74
+ // (daily-swarm-update-check) resolves its catalog script on a fresh install.
75
+ const order = SEEDERS.map((seeder) => seeder.kind);
76
+ expect(order.indexOf("script")).toBeLessThan(order.indexOf("schedule"));
77
+ expect(await getScript({ name: "swarm-update-check", scope: "global" })).toBeNull();
78
+ const scriptResult = await runSeeder(scriptsSeeder, {
79
+ quiet: true,
80
+ scriptEmbeddingMode: "skip",
81
+ });
65
82
  const workflowResult = await runSeeder(workflowsSeeder, { quiet: true });
66
83
  const scheduleResult = await runSeeder(schedulesSeeder, { quiet: true });
67
84
 
85
+ expect(scriptResult.failed).toEqual([]);
68
86
  expect(workflowResult).toMatchObject({ created: 11, failed: [] });
69
- expect(scheduleResult).toMatchObject({ created: 12, failed: [] });
87
+ expect(scheduleResult).toMatchObject({ created: 11, failed: [] });
88
+ expect(await getScheduledTaskByName("daily-swarm-update-check")).toMatchObject({
89
+ targetType: "script",
90
+ scriptName: "swarm-update-check",
91
+ });
70
92
  expect((await listWorkflows()).map((workflow) => workflow.name).sort()).toEqual(
71
93
  workflowsSeeder.items().map((item) => item.key),
72
94
  );
@@ -83,10 +105,10 @@ describe("seeder registry", () => {
83
105
  expect(await runSeeder(schedulesSeeder, { quiet: true })).toMatchObject({
84
106
  created: 0,
85
107
  updated: 0,
86
- skippedUnchanged: 12,
108
+ skippedUnchanged: 11,
87
109
  failed: [],
88
110
  });
89
111
  expect(await listWorkflows()).toHaveLength(11);
90
- expect(await getScheduledTasks()).toHaveLength(12);
91
- });
112
+ expect(await getScheduledTasks()).toHaveLength(11);
113
+ }, 60_000); // typechecks every catalog script with tsc
92
114
  });
@@ -8,6 +8,7 @@ import {
8
8
  initDb,
9
9
  updateScheduledTask,
10
10
  } from "../be/db";
11
+ import { upsertScriptByName } from "../be/scripts/db";
11
12
  import { runSeeder } from "../be/seed";
12
13
  import {
13
14
  createSchedulesSeeder,
@@ -15,6 +16,7 @@ import {
15
16
  type ScheduleTemplateSource,
16
17
  } from "../be/seed/schedules-seeder";
17
18
  import { createWorkflowsSeeder } from "../be/seed/workflows-seeder";
19
+ import { SEED_SCRIPTS } from "../be/seed-scripts";
18
20
 
19
21
  const TEST_DB_PATH = "./test-seed-schedules.sqlite";
20
22
 
@@ -95,6 +97,23 @@ function targetScheduleSource(target: Record<string, unknown>, prompt?: string)
95
97
  } satisfies ScheduleTemplateSource;
96
98
  }
97
99
 
100
+ /** Stand-in for the catalog script daily-swarm-update-check targets; boot seeds the real one first. */
101
+ async function seedUpdateCheckScript() {
102
+ await upsertScriptByName({
103
+ name: "swarm-update-check",
104
+ scope: "global",
105
+ scopeId: null,
106
+ source: "export default async function main() { return {}; }",
107
+ description: "test stand-in",
108
+ intent: "seed-schedules test fixture",
109
+ signatureJson: "{}",
110
+ fsMode: "none",
111
+ agentId: null,
112
+ isScratch: false,
113
+ typeChecked: true,
114
+ });
115
+ }
116
+
98
117
  async function makeWorkflow(name: string) {
99
118
  return createWorkflow({ name, definition: { nodes: [] } });
100
119
  }
@@ -118,9 +137,9 @@ afterEach(async () => {
118
137
  });
119
138
 
120
139
  describe("schedules seeder", () => {
121
- test("loads all twelve schedule templates, including the unfenced schedule block", () => {
140
+ test("loads all eleven schedule templates, including the unfenced schedule block", () => {
122
141
  const schedules = loadSeedSchedules();
123
- expect(schedules).toHaveLength(12);
142
+ expect(schedules).toHaveLength(11);
124
143
  // Zero-config, requires:[], placeholders:[], template enabled:true -> auto-enabled
125
144
  // under the default (unset -> on) SEED_AUTOMATIONS_ENABLED switch.
126
145
  expect(schedules.find((schedule) => schedule.name === "daily-status-report")).toMatchObject({
@@ -135,6 +154,11 @@ describe("schedules seeder", () => {
135
154
  enabled: true,
136
155
  requiredParams: [],
137
156
  requires: [],
157
+ // A script fire: no agent task, no prompt.
158
+ targetType: "script",
159
+ scriptName: "swarm-update-check",
160
+ scriptArgs: {},
161
+ taskTemplate: "",
138
162
  });
139
163
  expect(
140
164
  schedules.find((schedule) => schedule.name === "weekly-harness-upgrade-check"),
@@ -149,9 +173,10 @@ describe("schedules seeder", () => {
149
173
  "daily-blocker-digest",
150
174
  "daily-status-report",
151
175
  "daily-swarm-update-check",
152
- "daily-workflow-health-audit",
153
176
  "dreaming-daily",
154
177
  ]);
178
+ // The status report absorbed the workflow-health audit's six queries.
179
+ expect(schedules.map((schedule) => schedule.name)).not.toContain("daily-workflow-health-audit");
155
180
  // dreaming replaces Daily Evolution, which seeds off even though it is zero-config.
156
181
  expect(
157
182
  schedules.find((schedule) => schedule.name === "daily-compounding-reflection"),
@@ -160,20 +185,22 @@ describe("schedules seeder", () => {
160
185
 
161
186
  test("SEED_AUTOMATIONS_ENABLED=false still seeds all schedules disabled", async () => {
162
187
  process.env.SEED_AUTOMATIONS_ENABLED = "false";
163
- // dreaming-daily targets the seeded dreaming workflow.
188
+ // dreaming-daily targets the seeded dreaming workflow, daily-swarm-update-check a catalog script.
164
189
  await runSeeder(createWorkflowsSeeder(), { quiet: true });
190
+ await seedUpdateCheckScript();
165
191
  const result = await runSeeder(createSchedulesSeeder(), { quiet: true });
166
- expect(result).toMatchObject({ created: 12, failed: [] });
192
+ expect(result).toMatchObject({ created: 11, failed: [] });
167
193
  const schedules = await getScheduledTasks();
168
- expect(schedules).toHaveLength(12);
194
+ expect(schedules).toHaveLength(11);
169
195
  expect(schedules.every((schedule) => schedule.enabled === false)).toBe(true);
170
196
  });
171
197
 
172
198
  test("seeds a disabled schedule whose timezone still needs setup", async () => {
173
- // dreaming-daily targets the seeded dreaming workflow.
199
+ // dreaming-daily targets the seeded dreaming workflow, daily-swarm-update-check a catalog script.
174
200
  await runSeeder(createWorkflowsSeeder(), { quiet: true });
201
+ await seedUpdateCheckScript();
175
202
  const result = await runSeeder(createSchedulesSeeder(), { quiet: true });
176
- expect(result).toMatchObject({ created: 12, failed: [] });
203
+ expect(result).toMatchObject({ created: 11, failed: [] });
177
204
  expect(await getScheduledTaskByName("weekly-dependabot-triage")).toMatchObject({
178
205
  cronExpression: "40 3 * * 0",
179
206
  timezone: "{{TIMEZONE}}",
@@ -313,6 +340,32 @@ describe("schedules seeder", () => {
313
340
  expect(result.failed[0]?.error).toContain('Global script "no-such-script" not found');
314
341
  });
315
342
 
343
+ test("daily-swarm-update-check runs the catalog's swarm-update-check script, no agent task", async () => {
344
+ // The scripts catalog ships the target, so boot's script seeder creates it first.
345
+ expect(SEED_SCRIPTS.map((script) => script.name)).toContain("swarm-update-check");
346
+ const updateCheck = loadSeedSchedules().filter(
347
+ (schedule) => schedule.name === "daily-swarm-update-check",
348
+ );
349
+ expect(updateCheck).toHaveLength(1);
350
+
351
+ // Control: without the script the item fails instead of seeding an agent task.
352
+ const seeder = createSchedulesSeeder();
353
+ const before = await runSeeder(seeder, { quiet: true });
354
+ expect(before.failed.map((failure) => failure.key)).toContain("daily-swarm-update-check");
355
+ expect(await getScheduledTaskByName("daily-swarm-update-check")).toBeNull();
356
+
357
+ await seedUpdateCheckScript();
358
+ await runSeeder(seeder, { quiet: true });
359
+ const row = await getScheduledTaskByName("daily-swarm-update-check");
360
+ expect(row).toMatchObject({
361
+ targetType: "script",
362
+ scriptName: "swarm-update-check",
363
+ scriptArgs: {},
364
+ enabled: true,
365
+ });
366
+ expect(row?.taskTemplate).toBeUndefined();
367
+ });
368
+
316
369
  test("fails a workflow target whose workflow does not exist, without creating a row", async () => {
317
370
  const result = await runSeeder(
318
371
  createSchedulesSeeder([