@desplega.ai/agent-swarm 1.172.0 → 2.0.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 (525) hide show
  1. package/README.md +1 -1
  2. package/dist/{acp-adapter-fpecx1g1.js → acp-adapter-sj2g0m84.js} +18 -17
  3. package/dist/{actions-pw87a1a8.js → actions-fegk44gr.js} +65 -35
  4. package/dist/{amp-adapter-8jem2bc0.js → amp-adapter-ad8ft89x.js} +8 -7
  5. package/dist/{anthropic-messages-8ee2f22q.js → anthropic-messages-r6czkjv1.js} +7 -7
  6. package/dist/{app-wmf0m843.js → app-kfvgbvzy.js} +19 -13
  7. package/dist/{assistant-emp1e108.js → assistant-z7zdy9ja.js} +65 -38
  8. package/dist/{boot-reembed-1de4f4aj.js → boot-reembed-5c2z0mbe.js} +11 -9
  9. package/dist/{boot-reembed-4ymn9atn.js → boot-reembed-j8xa181e.js} +11 -9
  10. package/dist/{boot-scrub-sweep-adhk1s8c.js → boot-scrub-sweep-hnkn3rgt.js} +12 -10
  11. package/dist/{boot-seeds-8q637aew.js → boot-seeds-kwjq4d2z.js} +15 -13
  12. package/dist/{claude-adapter-t26cv8a4.js → claude-adapter-9vmvpvgd.js} +20 -19
  13. package/dist/{claude-managed-adapter-xgxhqrt2.js → claude-managed-adapter-k9qae05j.js} +7 -5
  14. package/dist/{claude-managed-setup-bnmjarjc.js → claude-managed-setup-q54zx5q2.js} +6 -4
  15. package/dist/{claude-sdk-session-gsfsh662.js → claude-sdk-session-9jye6cxr.js} +12698 -11995
  16. package/dist/{cli-k46r9hmk.js → cli-03gedhng.js} +16 -14
  17. package/dist/{cli-kb4ehzx5.js → cli-1e4b6bzh.js} +7 -7
  18. package/dist/{cli-d603qf86.js → cli-29jc5q7w.js} +10 -8
  19. package/dist/cli-2m1ktfzz.js +17795 -0
  20. package/dist/{cli-8vs9trd4.js → cli-3dj8k4vy.js} +1 -1
  21. package/dist/{cli-zxm9p3x4.js → cli-3fbmmawm.js} +5 -5
  22. package/dist/{cli-gmdajfw5.js → cli-4jxm2qtz.js} +4987 -1708
  23. package/dist/{cli-tadfgd0t.js → cli-5hkm7whv.js} +33 -4
  24. package/dist/{cli-6jb2rf77.js → cli-5t4f29ez.js} +3 -3
  25. package/dist/{cli-7a0sde2x.js → cli-6cagzc96.js} +9 -0
  26. package/dist/cli-6cvfxs8r.js +47 -0
  27. package/dist/{cli-pj6x9qce.js → cli-6jyj59nb.js} +2 -2
  28. package/dist/{cli-388egyf6.js → cli-6razj6t4.js} +2 -2
  29. package/dist/{cli-qsztpfry.js → cli-74ahz61x.js} +2 -2
  30. package/dist/{cli-4nz8p1p5.js → cli-7x0g4sn1.js} +15474 -16082
  31. package/dist/{cli-yatxqynm.js → cli-9ha6hs2d.js} +1 -290
  32. package/dist/{cli-81r1wjyv.js → cli-9nfx44eh.js} +1 -1
  33. package/dist/cli-9traj3dn.js +638 -0
  34. package/dist/{cli-smcrrmhy.js → cli-agzebbd5.js} +9 -9
  35. package/dist/{cli-bf7485pm.js → cli-apx87jne.js} +2 -2
  36. package/dist/{cli-ztrv9y21.js → cli-bzszjf2p.js} +3689 -1081
  37. package/dist/{cli-c5km5znr.js → cli-c8r3kzzb.js} +1 -1
  38. package/dist/{cli-hsem16hb.js → cli-cgtksawx.js} +7 -4
  39. package/dist/{cli-db3m6zdt.js → cli-d2qr7aj4.js} +1 -1
  40. package/dist/{cli-1hgat6pa.js → cli-d5f9ed5c.js} +5 -5
  41. package/dist/cli-e3g5mbkz.js +776 -0
  42. package/dist/{cli-7jykv7ef.js → cli-e6mpjhed.js} +1 -1
  43. package/dist/{cli-j6kpj9az.js → cli-ee9gtz70.js} +1553 -68
  44. package/dist/{cli-e0a9sk00.js → cli-eejsgvm4.js} +4 -4
  45. package/dist/{cli-tgpfnm6r.js → cli-ef1hg1wv.js} +518 -59
  46. package/dist/cli-f527ce74.js +781 -0
  47. package/dist/{cli-ppy4nnnh.js → cli-f7kr7svz.js} +2 -2
  48. package/dist/cli-hjwmhcg1.js +10622 -0
  49. package/dist/{cli-2bw3w47j.js → cli-hnhtk6cj.js} +1 -1
  50. package/dist/{cli-ywm1azg9.js → cli-hnmhtzvh.js} +6 -4
  51. package/dist/{cli-4fnb6884.js → cli-hvqbtna4.js} +7 -1259
  52. package/dist/{cli-pstq9888.js → cli-j0pfptdp.js} +220 -103
  53. package/dist/cli-jta2eyzc.js +619 -0
  54. package/dist/{cli-esnpehf5.js → cli-k382tshb.js} +12 -13
  55. package/dist/cli-k3dp5tpf.js +263 -0
  56. package/dist/{cli-wd0hz6ah.js → cli-kbrt8gqa.js} +9 -91
  57. package/dist/cli-kceah1eh.js +92 -0
  58. package/dist/{cli-prrt9fa0.js → cli-ke896fcf.js} +10 -10
  59. package/dist/{cli-nv4zd0b9.js → cli-kt6zqg0e.js} +6 -6
  60. package/dist/{cli-g6t9texm.js → cli-m50t4309.js} +5 -18
  61. package/dist/cli-n2wq4j7m.js +6985 -0
  62. package/dist/{cli-c2pq2rjb.js → cli-n3hzg89b.js} +46 -15
  63. package/dist/{cli-tmhsxhb8.js → cli-n5vwg842.js} +23 -6
  64. package/dist/{cli-5rv1j9ga.js → cli-ne236afy.js} +53 -7
  65. package/dist/cli-p0sg24zp.js +16 -0
  66. package/dist/{cli-d161g69q.js → cli-p1x85d2t.js} +50 -2
  67. package/dist/{cli-7pz3b7x5.js → cli-przpmdgr.js} +25 -0
  68. package/dist/{cli-g4f5pj26.js → cli-q0ke1yww.js} +111 -25
  69. package/dist/{cli-e3pmyt39.js → cli-q50942rs.js} +1 -1
  70. package/dist/{cli-r44t9wqc.js → cli-r106725k.js} +2 -2
  71. package/dist/{cli-gj81e47t.js → cli-r2pnvj8g.js} +2 -2
  72. package/dist/{cli-r8smj8b8.js → cli-r62a35ea.js} +19 -3
  73. package/dist/{cli-cd0hv9nh.js → cli-ra0xzggj.js} +2 -2
  74. package/dist/cli-rhs9pz33.js +294 -0
  75. package/dist/{cli-nyhb1r6x.js → cli-s2sp957d.js} +52 -29
  76. package/dist/{cli-1x7da0de.js → cli-s4setq91.js} +8 -8
  77. package/dist/{cli-xczpdp9e.js → cli-se8w6wny.js} +6 -6
  78. package/dist/cli-skxvvpap.js +10140 -0
  79. package/dist/{cli-014vns2y.js → cli-stfs0k5s.js} +208 -525
  80. package/dist/{cli-0p3enk35.js → cli-tqaazemc.js} +9 -1
  81. package/dist/{cli-cf69s3ew.js → cli-tr4sxp68.js} +3 -3
  82. package/dist/{cli-8hzfb6xz.js → cli-tsde1hdr.js} +15 -15
  83. package/dist/{cli-cceskcvt.js → cli-txqb9s38.js} +8 -8
  84. package/dist/{cli-aspcaef3.js → cli-v1yb0rqb.js} +6 -6
  85. package/dist/{cli-j1pmc07z.js → cli-vatyhshy.js} +268 -40
  86. package/dist/{cli-04emfe2q.js → cli-vdach5a9.js} +5 -5
  87. package/dist/{cli-7avy4v8w.js → cli-w1q7h2r2.js} +5 -3
  88. package/dist/{cli-rcp5aqdw.js → cli-w7m81tb7.js} +1 -1
  89. package/dist/{cli-hwwvy36w.js → cli-wayw93nt.js} +3 -3
  90. package/dist/cli-wmgrkscj.js +23254 -0
  91. package/dist/{cli-gp1rtyvk.js → cli-z5jjpaxw.js} +29 -6
  92. package/dist/{cli-451ks6aw.js → cli-zkpks3r3.js} +1 -1
  93. package/dist/cli.js +32 -48
  94. package/dist/{codex-adapter-m4tgdysy.js → codex-adapter-y81sssk0.js} +16 -15
  95. package/dist/{codex-login-841kv5fk.js → codex-login-f2rcfpp9.js} +3 -2
  96. package/dist/{codex-session-runner-ymwzm6ea.js → codex-session-runner-8vx4j6a9.js} +16 -15
  97. package/dist/{commands-n0hshcvs.js → commands-kcpvpcrr.js} +21 -10
  98. package/dist/{controller-hq5ggpcp.js → controller-ywrmt6yd.js} +5 -4
  99. package/dist/{controllers-connect-y07cqcvy.js → controllers-connect-kwpvj3rf.js} +45 -38
  100. package/dist/{cursor-adapter-q8pgv5qa.js → cursor-adapter-cehv1qns.js} +7 -6
  101. package/dist/{db-hkqvrdcb.js → db-k8ankj20.js} +13 -11
  102. package/dist/{db-4w7tmt1g.js → db-ww5d7jq3.js} +29 -9
  103. package/dist/{devin-adapter-xgtj680j.js → devin-adapter-x1022qb1.js} +3 -2
  104. package/dist/{dispatcher-5sd69y14.js → dispatcher-e8gq5m68.js} +41 -34
  105. package/dist/{docker-f503k9p0.js → docker-ff6y1190.js} +3 -2
  106. package/dist/{dsh-adapter-2h8m4cab.js → dsh-adapter-3epjm3af.js} +7 -6
  107. package/dist/{e2b-66xra4az.js → e2b-8ygwhg0f.js} +4 -3
  108. package/dist/{e2b-xwhyj8cw.js → e2b-swhnpw3n.js} +4 -3
  109. package/dist/{event-bus-vjtv82gm.js → event-bus-6axnngdj.js} +3 -2
  110. package/dist/external-channel-h3359cwn.js +52 -0
  111. package/dist/{google-generative-ai-7pavrbv7.js → google-generative-ai-g8db7x0f.js} +5 -3
  112. package/dist/{google-vertex-3f1jk41f.js → google-vertex-r2692q0r.js} +5 -3
  113. package/dist/{grok-adapter-8m9yqzbq.js → grok-adapter-0d4x7gkx.js} +19 -18
  114. package/dist/{handlers-s20xr10r.js → handlers-an1gzv21.js} +44 -37
  115. package/dist/{heartbeat-3k31q4g2.js → heartbeat-6dakeb61.js} +46 -39
  116. package/dist/{hook-gdn6a2fn.js → hook-6v6cv6d5.js} +12 -24
  117. package/dist/{hook-ygwerrdh.js → hook-sa5xye4y.js} +13 -12
  118. package/dist/{http-jdteeg81.js → http-r7f3eky0.js} +1598 -279
  119. package/dist/index-52j1aja4.js +3465 -0
  120. package/dist/{index-e0snym4t.js → index-7ep4gzdk.js} +41 -34
  121. package/dist/index-9gw58n4k.js +193 -0
  122. package/dist/index-9j87mcm8.js +56305 -0
  123. package/dist/index-bsmcresk.js +79424 -0
  124. package/dist/index-mh85qhec.js +475 -0
  125. package/dist/{index-vm3bm9t7.js → index-rmpw9a8q.js} +41 -34
  126. package/dist/{index-6ksewd5y.js → index-s2wxgzr0.js} +1015 -732
  127. package/dist/{index-tsszt7g9.js → index-v1v6n7hg.js} +46 -39
  128. package/dist/index-we27kd31.js +12810 -0
  129. package/dist/{index-ffqzqgwb.js → index-zk96aq1k.js} +11 -9
  130. package/dist/{keepalive-jekc896t.js → keepalive-be9vwy5h.js} +13 -11
  131. package/dist/{lead-4k9s9szx.js → lead-0861cwrf.js} +64 -57
  132. package/dist/{maintenance-shazch4n.js → maintenance-mzzgc8bn.js} +11 -9
  133. package/dist/node-c9we95xf.js +1 -1
  134. package/dist/{node-fkajaq96.js → node-mn1zs7an.js} +2 -1
  135. package/dist/{oauth-refresh-sweep-5gb36kn8.js → oauth-refresh-sweep-cpdnfwdj.js} +12 -10
  136. package/dist/{onboard-ydrbnvww.js → onboard-fh8qs4r3.js} +11 -3
  137. package/dist/{opencode-adapter-5zh0he20.js → opencode-adapter-nm42wwj8.js} +19 -14
  138. package/dist/{otel-impl-6pf5cv86.js → otel-impl-x6c1c9g4.js} +248 -869
  139. package/dist/{pi-mono-adapter-ef9wwx9m.js → pi-mono-adapter-9n2ya240.js} +245 -107
  140. package/dist/{pricing-refresh-s52nsa5q.js → pricing-refresh-qb0rv0ap.js} +13 -11
  141. package/dist/{rbac-roles-aq3msh4e.js → rbac-roles-1gzdwbh1.js} +12 -10
  142. package/dist/{rbac-roles-q5gb5e8v.js → rbac-roles-3mraey52.js} +13 -11
  143. package/dist/{render-v2-pd1rg54q.js → render-v2-agpa45zz.js} +19 -13
  144. package/dist/{resume-gw4a2c6v.js → resume-x0ydy9s9.js} +27 -21
  145. package/dist/{runtime-qfry88v2.js → runtime-sa2vr16g.js} +2 -2
  146. package/dist/{setup-jda1ksdh.js → setup-as9ga0d6.js} +2 -2
  147. package/dist/{virtual-modules-b3ga2m65.js → virtual-modules-9gkxqrap.js} +7 -6
  148. package/dist/{work-objects-tb7ycye8.js → work-objects-y5g1y7ev.js} +49 -37
  149. package/dist/{worker-njjccz5d.js → worker-860c3e38.js} +64 -57
  150. package/dist/{x-qfmygqqv.js → x-m477fr0h.js} +3 -2
  151. package/openapi.json +951 -356
  152. package/package.json +12 -13
  153. package/src/apps/catalog.generated.json +1309 -53
  154. package/src/apps/page-validator.ts +93 -31
  155. package/src/azure-devops/api.ts +94 -18
  156. package/src/azure-devops/auth.ts +32 -0
  157. package/src/azure-devops/boards-sync.ts +708 -0
  158. package/src/azure-devops/handlers.ts +52 -8
  159. package/src/azure-devops/html.ts +26 -0
  160. package/src/azure-devops/index.ts +15 -0
  161. package/src/azure-devops/templates.ts +86 -0
  162. package/src/azure-devops/types.ts +65 -1
  163. package/src/be/config-refs.ts +60 -0
  164. package/src/be/db/page-versions.ts +15 -0
  165. package/src/be/db/runtime.ts +35 -8
  166. package/src/be/db/skill-name-resolution.ts +36 -0
  167. package/src/be/db/tasks/read.ts +230 -0
  168. package/src/be/db/tasks/write.ts +7 -0
  169. package/src/be/db.ts +74 -13
  170. package/src/be/migrations/213_script_connections_sql.sql +79 -0
  171. package/src/be/migrations/215_swarm_config_export_to_env.sql +34 -0
  172. package/src/be/migrations/216_task_resolved_effort.sql +5 -0
  173. package/src/be/migrations/217_chat_sdk_state.sql +104 -0
  174. package/src/be/model-catalog-store.ts +5 -1
  175. package/src/be/model-tier-resolution.ts +12 -1
  176. package/src/be/model-validation.ts +5 -2
  177. package/src/be/models-catalog.ts +26 -1
  178. package/src/be/modelsdev-cache.json +198 -63
  179. package/src/be/modelsdev-cache.ts +9 -1
  180. package/src/be/script-connections.ts +47 -8
  181. package/src/be/script-credential-broker.ts +39 -2
  182. package/src/be/scripts/run-saved.ts +1 -0
  183. package/src/be/scripts/typecheck.ts +10 -8
  184. package/src/be/seed/runner.ts +13 -1
  185. package/src/be/seed/state-db.ts +5 -0
  186. package/src/be/seed/types.ts +6 -0
  187. package/src/be/seed-pricing.ts +19 -2
  188. package/src/be/seed-scripts/catalog/ops-catalog-audit.inline.ts +1 -1
  189. package/src/be/seed-scripts/catalog/ops-catalog-audit.ts +1 -1
  190. package/src/be/seed-skills/bundled-files.generated.json +10 -18
  191. package/src/be/seed-skills/index.ts +107 -3
  192. package/src/be/sql-connections.ts +118 -0
  193. package/src/be/swarm-config-guard.ts +53 -0
  194. package/src/cli.tsx +0 -16
  195. package/src/commands/onboard/non-interactive.ts +4 -2
  196. package/src/commands/profile-sync.ts +5 -3
  197. package/src/commands/provider-credentials.ts +52 -6
  198. package/src/commands/runner.ts +196 -56
  199. package/src/e2b/env.ts +5 -0
  200. package/src/extensions/contract-types.generated.ts +1 -1
  201. package/src/extensions/contract.ts +45 -0
  202. package/src/extensions/dispatcher.ts +7 -0
  203. package/src/extensions/post-bridge.ts +40 -0
  204. package/src/github/handlers.ts +11 -4
  205. package/src/heartbeat/templates.ts +8 -5
  206. package/src/hooks/hook.ts +0 -26
  207. package/src/http/all-routes.ts +0 -1
  208. package/src/http/config.ts +36 -17
  209. package/src/http/core.ts +4 -0
  210. package/src/http/index.ts +14 -2
  211. package/src/http/integrations.ts +88 -1
  212. package/src/http/mcp-oauth.ts +6 -13
  213. package/src/http/mcp.ts +15 -5
  214. package/src/http/page-proxy.ts +2 -4
  215. package/src/http/pages-public.ts +6 -5
  216. package/src/http/pages.ts +5 -14
  217. package/src/http/poll.ts +28 -68
  218. package/src/http/requested-by.ts +54 -0
  219. package/src/http/route-def.ts +7 -6
  220. package/src/http/script-connection-proxy.ts +51 -4
  221. package/src/http/script-connections.ts +5 -2
  222. package/src/http/scripts.ts +1 -0
  223. package/src/http/session-cost-recompute.ts +12 -2
  224. package/src/http/session-data.ts +4 -0
  225. package/src/http/stats.ts +1 -30
  226. package/src/http/tasks.ts +61 -5
  227. package/src/http/utils.ts +14 -0
  228. package/src/http/webhooks.ts +183 -2
  229. package/src/http/x.ts +1 -0
  230. package/src/integrations/chat-sdk/adapters.ts +129 -0
  231. package/src/integrations/chat-sdk/attachments.ts +232 -0
  232. package/src/integrations/chat-sdk/background.ts +36 -0
  233. package/src/integrations/chat-sdk/commands.ts +194 -0
  234. package/src/integrations/chat-sdk/config.ts +198 -0
  235. package/src/integrations/chat-sdk/discord-api.ts +63 -0
  236. package/src/integrations/chat-sdk/discord-gateway.ts +185 -0
  237. package/src/integrations/chat-sdk/handlers.ts +28 -0
  238. package/src/integrations/chat-sdk/inbound.ts +426 -0
  239. package/src/integrations/chat-sdk/limits.ts +48 -0
  240. package/src/integrations/chat-sdk/logger.ts +81 -0
  241. package/src/integrations/chat-sdk/outbound.ts +123 -0
  242. package/src/integrations/chat-sdk/relay.ts +172 -0
  243. package/src/integrations/chat-sdk/router.ts +169 -0
  244. package/src/integrations/chat-sdk/runtime.ts +351 -0
  245. package/src/integrations/chat-sdk/schema.ts +245 -0
  246. package/src/integrations/chat-sdk/state.ts +243 -0
  247. package/src/integrations/chat-sdk/status.ts +244 -0
  248. package/src/integrations/chat-sdk/steering.ts +58 -0
  249. package/src/integrations/chat-sdk/teams-origin.ts +77 -0
  250. package/src/integrations/chat-sdk/telegram-api.ts +56 -0
  251. package/src/integrations/chat-sdk/templates.ts +16 -0
  252. package/src/integrations/chat-sdk/test-hooks.ts +37 -0
  253. package/src/integrations/chat-sdk/thread-buffer.ts +100 -0
  254. package/src/integrations/chat-sdk/typing.ts +65 -0
  255. package/src/jira/adf.ts +3 -1
  256. package/src/kv-reserved-namespaces.ts +2 -0
  257. package/src/oauth/mcp-wrapper.ts +10 -6
  258. package/src/otel-impl.ts +2 -1
  259. package/src/pages/version.ts +4 -5
  260. package/src/prompts/base-prompt.ts +111 -45
  261. package/src/providers/acp-target-catalog.ts +6 -0
  262. package/src/providers/acp-targets.ts +56 -3
  263. package/src/providers/amp-adapter.ts +1 -1
  264. package/src/providers/claude-adapter.ts +27 -2
  265. package/src/providers/claude-flag-settings.ts +133 -0
  266. package/src/providers/claude-managed-adapter.ts +1 -1
  267. package/src/providers/claude-sdk-session.ts +7 -0
  268. package/src/providers/claude-session-events.ts +6 -1
  269. package/src/providers/dsh-adapter.ts +1 -1
  270. package/src/providers/opencode-adapter.ts +268 -37
  271. package/src/providers/opencode-billing.ts +86 -0
  272. package/src/providers/opencode-swarm-plugin.ts +51 -0
  273. package/src/providers/pi-mono-adapter.ts +398 -153
  274. package/src/providers/pricing-sources.md +37 -3
  275. package/src/providers/reasoning-effort.ts +27 -0
  276. package/src/providers/types.ts +11 -2
  277. package/src/rbac/legacy-policy.ts +8 -2
  278. package/src/rbac/permissions.ts +21 -0
  279. package/src/scheduler/scheduler.ts +4 -2
  280. package/src/scripts-runtime/api-client.ts +15 -1
  281. package/src/scripts-runtime/api-types.ts +9 -1
  282. package/src/scripts-runtime/ctx.ts +1 -1
  283. package/src/scripts-runtime/loader.ts +19 -7
  284. package/src/scripts-runtime/mcp-client.ts +30 -19
  285. package/src/scripts-runtime/sdk-allowlist.ts +8 -6
  286. package/src/scripts-runtime/swarm-sdk.ts +0 -11
  287. package/src/scripts-runtime/types/stdlib.d.ts +27 -15
  288. package/src/scripts-runtime/types/swarm-extension.d.ts +63 -1
  289. package/src/scripts-runtime/types/swarm-sdk.d.ts +27 -15
  290. package/src/server.ts +52 -18
  291. package/src/slack/actions.ts +29 -1
  292. package/src/slack/app.ts +14 -2
  293. package/src/slack/assistant.ts +36 -2
  294. package/src/slack/channel-join.ts +3 -16
  295. package/src/slack/commands.ts +9 -1
  296. package/src/slack/event-dedup.ts +1 -1
  297. package/src/slack/external-channel-egress.ts +113 -0
  298. package/src/slack/external-channel.ts +309 -0
  299. package/src/slack/handlers.ts +61 -0
  300. package/src/slack/render-v2.ts +40 -47
  301. package/src/slack/work-objects.ts +5 -0
  302. package/src/tasks/additive-buffer.ts +4 -2
  303. package/src/tasks/context-key.ts +94 -2
  304. package/src/tasks/model-pin.ts +67 -0
  305. package/src/tasks/worker-follow-up.ts +4 -31
  306. package/src/telemetry.ts +2 -1
  307. package/src/tests/acp-adapter.test.ts +42 -0
  308. package/src/tests/api-drain-worker-e2e.test.ts +1 -6
  309. package/src/tests/apps-catalog-feedback.test.ts +310 -0
  310. package/src/tests/apps-catalog-inputs.test.ts +70 -0
  311. package/src/tests/apps-catalog-table.test.ts +113 -0
  312. package/src/tests/azure-devops-boards-sync.test.ts +584 -0
  313. package/src/tests/azure-devops-html.test.ts +13 -0
  314. package/src/tests/azure-devops-webhook-malformed.test.ts +4 -0
  315. package/src/tests/base-prompt.test.ts +58 -54
  316. package/src/tests/chat-sdk-commands.test.ts +191 -0
  317. package/src/tests/chat-sdk-config.test.ts +334 -0
  318. package/src/tests/chat-sdk-discord-api.test.ts +68 -0
  319. package/src/tests/chat-sdk-discord-gateway.test.ts +252 -0
  320. package/src/tests/chat-sdk-discord-webhook.test.ts +126 -0
  321. package/src/tests/chat-sdk-fields-sync.test.ts +50 -0
  322. package/src/tests/chat-sdk-harness.ts +146 -0
  323. package/src/tests/chat-sdk-inbound.test.ts +363 -0
  324. package/src/tests/chat-sdk-relay.test.ts +470 -0
  325. package/src/tests/chat-sdk-reload.test.ts +128 -0
  326. package/src/tests/chat-sdk-shutdown.test.ts +220 -0
  327. package/src/tests/chat-sdk-slack.test.ts +268 -0
  328. package/src/tests/chat-sdk-state.test.ts +163 -0
  329. package/src/tests/chat-sdk-status-route.test.ts +382 -0
  330. package/src/tests/chat-sdk-teams.test.ts +277 -0
  331. package/src/tests/chat-sdk-tools.test.ts +520 -0
  332. package/src/tests/chat-sdk-typing.test.ts +120 -0
  333. package/src/tests/chat-sdk-webhook-telegram.test.ts +200 -0
  334. package/src/tests/claude-adapter-binary.test.ts +144 -0
  335. package/src/tests/claude-flag-settings.test.ts +114 -0
  336. package/src/tests/claude-managed-adapter.test.ts +1 -1
  337. package/src/tests/claude-sdk-lifecycle.test.ts +148 -1
  338. package/src/tests/claude-sdk-process.fixture.ts +7 -0
  339. package/src/tests/claude-thinking-tokens.test.ts +57 -0
  340. package/src/tests/claude-worker-parity.test.ts +2 -7
  341. package/src/tests/config-env-export-chat-sdk.test.ts +11 -0
  342. package/src/tests/config-refs.test.ts +66 -0
  343. package/src/tests/context-key.test.ts +51 -0
  344. package/src/tests/cors-allowlist.test.ts +25 -0
  345. package/src/tests/create-page-tool.test.ts +18 -0
  346. package/src/tests/credential-check.test.ts +139 -0
  347. package/src/tests/credentials.test.ts +21 -0
  348. package/src/tests/db-runtime.test.ts +61 -0
  349. package/src/tests/db-task-read-contract.test.ts +20 -0
  350. package/src/tests/db-task-write-contract.test.ts +10 -0
  351. package/src/tests/deferred-task-wake.test.ts +52 -4
  352. package/src/tests/docs-index-footprint.test.js +3 -3
  353. package/src/tests/dsh-adapter.test.ts +1 -1
  354. package/src/tests/enabled-capabilities.test.ts +20 -0
  355. package/src/tests/entrypoint-api-readiness.test.ts +14 -8
  356. package/src/tests/entrypoint-config-env-export.test.ts +5 -2
  357. package/src/tests/entrypoint-export-to-env.test.ts +78 -0
  358. package/src/tests/explicit-model-validation.test.ts +105 -1
  359. package/src/tests/extensions-example-route-chat-channel.test.ts +154 -0
  360. package/src/tests/extensions-post-bridge.test.ts +51 -0
  361. package/src/tests/fetch-resolved-env.test.ts +128 -0
  362. package/src/tests/fixtures/azure-boards/workitem.commented.json +89 -0
  363. package/src/tests/fixtures/azure-boards/workitem.created.json +81 -0
  364. package/src/tests/fixtures/azure-boards/workitem.deleted.json +70 -0
  365. package/src/tests/fixtures/azure-boards/workitem.restored.json +82 -0
  366. package/src/tests/fixtures/azure-boards/workitem.updated.json +130 -0
  367. package/src/tests/fixtures/catalog-feedback-definition.json.txt +860 -0
  368. package/src/tests/fixtures/catalog-inputs-definition.json.txt +910 -0
  369. package/src/tests/fixtures/catalog-table-definition.json.txt +468 -0
  370. package/src/tests/fixtures/chat-sdk/telegram-private-text.json +10 -0
  371. package/src/tests/fixtures/extensions/route-chat-channel-to-agent/hooks.ts +27 -0
  372. package/src/tests/fixtures/extensions/route-chat-channel-to-agent/manifest.json +9 -0
  373. package/src/tests/fixtures/openai-compatible-registry-extension.child.ts +146 -0
  374. package/src/tests/fixtures/runner-identity-refresh.ts +3 -0
  375. package/src/tests/fixtures/runner-legacy-resume-prompt.ts +122 -0
  376. package/src/tests/github-handlers.test.ts +35 -0
  377. package/src/tests/harness-provider-resolution.test.ts +12 -0
  378. package/src/tests/heartbeat-checklist.test.ts +3 -2
  379. package/src/tests/hook-runtime-close.test.ts +0 -2
  380. package/src/tests/http-api-integration.test.ts +0 -25
  381. package/src/tests/migration-046-budgets.test.ts +17 -16
  382. package/src/tests/migration-217-chat-sdk-state.test.ts +113 -0
  383. package/src/tests/model-tier-resolution.test.ts +93 -0
  384. package/src/tests/models-catalog.test.ts +42 -1
  385. package/src/tests/node-env-runtime.test.ts +98 -0
  386. package/src/tests/openai-compatible-providers.test.ts +134 -0
  387. package/src/tests/openai-compatible-registry-extension.test.ts +42 -0
  388. package/src/tests/opencode-adapter.test.ts +348 -3
  389. package/src/tests/opencode-billing.test.ts +28 -0
  390. package/src/tests/opencode-helmcode-sdk.test.ts +201 -0
  391. package/src/tests/opencode-shell-env.test.ts +145 -0
  392. package/src/tests/openrouter-base-url.test.ts +101 -151
  393. package/src/tests/page-proxy.test.ts +1 -1
  394. package/src/tests/pages-browser-sdk.test.ts +76 -0
  395. package/src/tests/pages-feedback.test.ts +1 -1
  396. package/src/tests/paused-tasks-requested-by.test.ts +102 -0
  397. package/src/tests/pi-custom-provider-fake-server.test.ts +442 -0
  398. package/src/tests/pi-mono-adapter.test.ts +385 -5
  399. package/src/tests/pi-shell-env.test.ts +170 -0
  400. package/src/tests/prompt-template-remaining.test.ts +35 -0
  401. package/src/tests/rbac-engine.test.ts +11 -3
  402. package/src/tests/runner-exit-after-tasks.test.ts +0 -4
  403. package/src/tests/runner-identity-refresh.test.ts +3 -0
  404. package/src/tests/runner-legacy-resume-prompt.test.ts +64 -0
  405. package/src/tests/script-connections-sql.test.ts +675 -0
  406. package/src/tests/script-connections.test.ts +7 -1
  407. package/src/tests/scripts-http.test.ts +201 -1
  408. package/src/tests/scripts-only-gating.test.ts +2 -3
  409. package/src/tests/scripts-runtime.test.ts +58 -40
  410. package/src/tests/secret-scrubber.test.ts +86 -0
  411. package/src/tests/seed-skills-bundled-files.test.ts +45 -53
  412. package/src/tests/seed-skills-retire.test.ts +182 -0
  413. package/src/tests/send-task-requester-gate.test.ts +58 -0
  414. package/src/tests/session-cost-recompute-unit.test.ts +94 -0
  415. package/src/tests/skill-get-name-resolution.test.ts +137 -0
  416. package/src/tests/slack-channel-join.test.ts +4 -1
  417. package/src/tests/slack-external-channel-config.test.ts +65 -0
  418. package/src/tests/slack-external-channel-egress.test.ts +237 -0
  419. package/src/tests/slack-external-channel-guard.test.ts +280 -0
  420. package/src/tests/slack-external-channel-tools.test.ts +173 -0
  421. package/src/tests/slack-external-channel.test.ts +370 -0
  422. package/src/tests/slack-render-v2.test.ts +86 -10
  423. package/src/tests/sonnet-5-5-registry.test.ts +9 -1
  424. package/src/tests/spec-base-url.test.ts +16 -1
  425. package/src/tests/swarm-config-export-to-env.test.ts +267 -0
  426. package/src/tests/swarm-config-reserved-keys.test.ts +16 -0
  427. package/src/tests/swarm-diff.test.ts +1 -1
  428. package/src/tests/task-model-effort-rerun.test.ts +164 -0
  429. package/src/tests/task-session-prompts.test.ts +132 -0
  430. package/src/tests/task-tool-manifest.test.ts +14 -0
  431. package/src/tests/task-tools-ctx.test.ts +2 -0
  432. package/src/tests/tool-annotations.test.ts +6 -6
  433. package/src/tests/ui-logs-parser.test.ts +43 -0
  434. package/src/tests/used-by-route.test.ts +6 -0
  435. package/src/tests/vendored-openapi.test.ts +16 -1
  436. package/src/tests/vercel-ai-gateway.test.ts +149 -0
  437. package/src/tests/workflow-swarm-script.test.ts +38 -0
  438. package/src/tests/workflow-wait-filter.test.ts +5 -3
  439. package/src/tools/chat-access.ts +41 -0
  440. package/src/tools/chat-open-dm.ts +59 -0
  441. package/src/tools/chat-post.ts +74 -0
  442. package/src/tools/chat-read-thread.ts +100 -0
  443. package/src/tools/chat-reply.ts +75 -0
  444. package/src/tools/create-page.ts +3 -13
  445. package/src/tools/defer-task.ts +17 -0
  446. package/src/tools/model-catalog/index.ts +2 -1
  447. package/src/tools/register-chat-channel.ts +139 -0
  448. package/src/tools/send-task.ts +24 -0
  449. package/src/tools/skills/skill-get.ts +17 -9
  450. package/src/tools/slack-post.ts +6 -0
  451. package/src/tools/slack-read.ts +8 -0
  452. package/src/tools/slack-reply.ts +6 -0
  453. package/src/tools/slack-start-thread.ts +6 -0
  454. package/src/tools/slack-update.ts +6 -0
  455. package/src/tools/slack-upload-file.ts +6 -0
  456. package/src/tools/swarm-config/get-config.ts +2 -1
  457. package/src/tools/swarm-config/list-config.ts +2 -1
  458. package/src/tools/swarm-config/set-config.ts +16 -3
  459. package/src/tools/tool-config.ts +8 -6
  460. package/src/types.ts +12 -2
  461. package/src/utils/config-env-export.ts +74 -0
  462. package/src/utils/credentials.test.ts +92 -0
  463. package/src/utils/credentials.ts +52 -4
  464. package/src/utils/harness-provider.ts +15 -6
  465. package/src/utils/node-env.ts +24 -0
  466. package/src/utils/runtime-model-catalog.ts +11 -1
  467. package/src/utils/secret-scrubber.ts +71 -2
  468. package/src/utils/task-tool-manifest.ts +19 -2
  469. package/src/workflows/executors/swarm-script.ts +1 -0
  470. package/templates/skills/agent-browser/SKILL.md +1 -1
  471. package/templates/skills/agent-browser/content.md +1 -1
  472. package/templates/skills/apps/SKILL.md +749 -0
  473. package/templates/skills/apps/content.md +60 -22
  474. package/templates/skills/chat-sdk/config.json +14 -0
  475. package/templates/skills/chat-sdk/content.md +84 -0
  476. package/templates/skills/composio-gmail/content.md +7 -0
  477. package/templates/skills/composio-google-docs/content.md +1 -1
  478. package/templates/skills/email-domain-health/config.json +14 -0
  479. package/templates/skills/email-domain-health/content.md +131 -0
  480. package/templates/skills/kv-storage/content.md +2 -2
  481. package/templates/skills/pages/content.md +8 -10
  482. package/templates/skills/qa-loom/config.json +14 -0
  483. package/templates/skills/qa-loom/content.md +187 -0
  484. package/templates/skills/qa-loom/files/build-loom.mjs +221 -0
  485. package/templates/skills/qa-loom/files/run-qa.sh +57 -0
  486. package/templates/skills/slack-interaction/content.md +4 -0
  487. package/templates/skills/swarm-extensions/content.md +2 -0
  488. package/templates/skills/swarm-scripts/SKILL.md +4 -2
  489. package/templates/skills/swarm-scripts/config.json +1 -1
  490. package/templates/skills/swarm-scripts/content.md +4 -2
  491. package/templates/skills/workflow-iterate/content.md +38 -0
  492. package/dist/artifact-eczsknc3.js +0 -3843
  493. package/dist/cli-9xsmdpdt.js +0 -56377
  494. package/dist/cli-bd957dgb.js +0 -153
  495. package/dist/cli-q51yprfq.js +0 -1104
  496. package/dist/cli-t1ze64h3.js +0 -811
  497. package/dist/index-9ch1n761.js +0 -1102
  498. package/src/artifact-sdk/index.ts +0 -2
  499. package/src/artifact-sdk/localtunnel.d.ts +0 -20
  500. package/src/artifact-sdk/port.ts +0 -12
  501. package/src/artifact-sdk/server.ts +0 -183
  502. package/src/artifact-sdk/tunnel.ts +0 -19
  503. package/src/commands/artifact.ts +0 -249
  504. package/src/http/ecosystem.ts +0 -77
  505. package/src/tests/artifact-commands.test.ts +0 -92
  506. package/src/tests/artifact-sdk.test.ts +0 -793
  507. package/src/tests/register-service-security.test.ts +0 -194
  508. package/src/tools/list-services.ts +0 -107
  509. package/src/tools/register-service.ts +0 -177
  510. package/src/tools/unregister-service.ts +0 -73
  511. package/src/tools/update-service-status.ts +0 -96
  512. package/templates/skills/artifacts/config.json +0 -14
  513. package/templates/skills/artifacts/content.md +0 -344
  514. package/templates/skills/artifacts/files/examples/approval-flow.ts +0 -34
  515. package/templates/skills/artifacts/files/examples/hono-dashboard.ts +0 -31
  516. package/templates/skills/artifacts/files/examples/multi-artifact.ts +0 -20
  517. package/templates/skills/artifacts/files/examples/static-report.sh +0 -17
  518. package/dist/{azure-openai-responses-ap1gwxgr.js → azure-openai-responses-ygfreq4t.js} +5 -5
  519. package/dist/{cli-5rqrj7q0.js → cli-pw7ebjdt.js} +0 -0
  520. package/dist/{openai-codex-responses-33qhkkdy.js → openai-codex-responses-3bk0grnr.js} +4 -4
  521. package/dist/{openai-completions-s88xm3ns.js → openai-completions-5d4jpakc.js} +9 -9
  522. package/dist/{openai-responses-7xd2f2e2.js → openai-responses-krb51f59.js} +5 -5
  523. package/dist/{pi-messages-xszk613n.js → pi-messages-x9gzynm1.js} +3 -3
  524. /package/src/{artifact-sdk → pages}/browser-sdk.ts +0 -0
  525. /package/src/{artifact-sdk → pages}/feedback-overlay.ts +0 -0
@@ -3,44 +3,50 @@ import {
3
3
  } from "./cli-ep19k3mb.js";
4
4
  import {
5
5
  agentFsProvisionSeeder
6
- } from "./cli-gj81e47t.js";
6
+ } from "./cli-r2pnvj8g.js";
7
7
  import {
8
8
  extractArgsJsonSchema,
9
9
  extractScriptSignature,
10
+ parseSkillContent,
10
11
  require_dist,
11
12
  typecheckScript,
12
13
  validateScriptImports
13
- } from "./cli-j6kpj9az.js";
14
+ } from "./cli-ee9gtz70.js";
14
15
  import"./cli-epan13p1.js";
15
16
  import"./cli-zt3w1cn1.js";
16
- import"./cli-388egyf6.js";
17
+ import"./cli-6razj6t4.js";
17
18
  import {
18
19
  validateDefinition
19
- } from "./cli-aspcaef3.js";
20
- import"./cli-xczpdp9e.js";
21
- import"./cli-7jykv7ef.js";
22
- import"./cli-cceskcvt.js";
23
- import"./cli-h6vtvt5c.js";
20
+ } from "./cli-v1yb0rqb.js";
21
+ import"./cli-se8w6wny.js";
22
+ import"./cli-e6mpjhed.js";
24
23
  import"./cli-2x0e561b.js";
24
+ import"./cli-txqb9s38.js";
25
+ import"./cli-h6vtvt5c.js";
25
26
  import"./cli-bsakbbkm.js";
26
27
  import"./cli-zxg80zs9.js";
27
28
  import"./cli-ydt3zkwv.js";
28
- import"./cli-9xsmdpdt.js";
29
- import"./cli-q51yprfq.js";
29
+ import"./cli-skxvvpap.js";
30
+ import"./cli-n2wq4j7m.js";
31
+ import"./cli-wmgrkscj.js";
30
32
  import"./cli-3er6gcsn.js";
31
- import"./cli-yatxqynm.js";
33
+ import"./cli-hjwmhcg1.js";
34
+ import"./cli-rhs9pz33.js";
35
+ import"./cli-9ha6hs2d.js";
36
+ import"./cli-k3dp5tpf.js";
32
37
  import"./cli-61625k3x.js";
33
- import"./cli-btbzvq95.js";
34
38
  import"./cli-1m2e086m.js";
39
+ import"./cli-btbzvq95.js";
35
40
  import"./cli-p7z6q2m6.js";
36
41
  import"./cli-wnb9b2g3.js";
37
- import"./cli-bd957dgb.js";
42
+ import"./cli-f527ce74.js";
38
43
  import"./cli-pzxba7gw.js";
39
44
  import {
40
45
  computeContentHash,
41
46
  createScheduledTask,
42
47
  createSkill,
43
48
  createWorkflow,
49
+ deleteSkill,
44
50
  deleteSkillFile,
45
51
  getDbClient,
46
52
  getScheduledTaskByName,
@@ -55,28 +61,30 @@ import {
55
61
  updateWorkflow,
56
62
  upsertScriptByName,
57
63
  upsertSkillFiles
58
- } from "./cli-tgpfnm6r.js";
59
- import"./cli-ywm1azg9.js";
60
- import"./cli-hsem16hb.js";
61
- import"./cli-nyhb1r6x.js";
62
- import"./cli-7mm2r1kp.js";
64
+ } from "./cli-ef1hg1wv.js";
65
+ import"./cli-hnmhtzvh.js";
66
+ import"./cli-cgtksawx.js";
67
+ import"./cli-s2sp957d.js";
63
68
  import"./cli-dmtrxqhg.js";
69
+ import"./cli-7mm2r1kp.js";
70
+ import"./cli-5hkm7whv.js";
64
71
  import"./cli-mvzyxwg3.js";
65
- import"./cli-tadfgd0t.js";
66
72
  import"./cli-fdvbjtbw.js";
67
73
  import"./cli-ywybs5as.js";
68
74
  import"./cli-f9w1semm.js";
69
75
  import"./cli-1a2j50m0.js";
70
76
  import"./cli-a35hzdve.js";
71
- import"./cli-014vns2y.js";
77
+ import"./cli-stfs0k5s.js";
72
78
  import"./cli-sd0mxa3c.js";
73
79
  import"./cli-w5e1d1vv.js";
74
- import"./cli-esnpehf5.js";
80
+ import"./cli-p0sg24zp.js";
81
+ import"./cli-k382tshb.js";
75
82
  import"./cli-zf8jjwex.js";
76
83
  import"./cli-gd4favx9.js";
77
84
  import"./cli-ej6gbxfb.js";
78
85
  import"./cli-a1b9b32n.js";
79
- import"./cli-d161g69q.js";
86
+ import"./cli-p1x85d2t.js";
87
+ import"./cli-e3g5mbkz.js";
80
88
  import {
81
89
  __toESM
82
90
  } from "./cli-bc05qbss.js";
@@ -3968,7 +3976,7 @@ const CONTRADICTORY_RE =
3968
3976
  /\\b(do not|don't|never)\\b[\\s\\S]{0,240}\\b(always|must|required)\\b|\\b(always|must|required)\\b[\\s\\S]{0,240}\\b(do not|don't|never)\\b/i;
3969
3977
 
3970
3978
  const CODE_REGISTRY_EVENTS =
3971
- "agentmail.email.followup agentmail.email.mapped_lead agentmail.email.mapped_worker agentmail.email.no_agent agentmail.email.unmapped common.command_suggestions.github_comment_issue common.command_suggestions.github_comment_pr common.command_suggestions.github_issue common.command_suggestions.github_pr common.command_suggestions.gitlab_issue common.command_suggestions.gitlab_mr common.delegation_instruction common.delegation_instruction.gitlab github.check_run.failed github.check_suite.failed github.comment.mentioned github.issue.assigned github.issue.labeled github.issue.mentioned github.pull_request.assigned github.pull_request.closed github.pull_request.labeled github.pull_request.mentioned github.pull_request.review_requested github.pull_request.review_submitted github.pull_request.synchronize github.workflow_run.failed gitlab.comment.mentioned gitlab.issue.assigned gitlab.merge_request.opened gitlab.pipeline.failed heartbeat.boot-triage heartbeat.checklist jira.issue.assigned jira.issue.commented jira.issue.followup kapso.message.received linear.issue.assigned linear.issue.followup linear.issue.reassigned slack.assistant.greeting slack.assistant.offline slack.assistant.suggested_prompts slack.message.attachment_failed slack.message.thread_context system.agent.communication system.agent.lead system.agent.memory system.agent.memory.remote system.agent.outputs system.agent.outputs.no_agent_fs system.agent.repository system.agent.role system.agent.secrets system.agent.slack system.agent.tools_skills system.agent.worker system.agent.worker.remote system.agent.workspace system.agent.workspace.remote system.session.lead system.session.lead.managed system.session.worker system.session.worker.managed system.session.worker.remote task.budget.refused task.requester.profile task.worker.completed task.worker.failed".split(
3979
+ "agentmail.email.followup agentmail.email.mapped_lead agentmail.email.mapped_worker agentmail.email.no_agent agentmail.email.unmapped chat.message.thread_context common.command_suggestions.github_comment_issue common.command_suggestions.github_comment_pr common.command_suggestions.github_issue common.command_suggestions.github_pr common.command_suggestions.gitlab_issue common.command_suggestions.gitlab_mr common.delegation_instruction common.delegation_instruction.gitlab github.check_run.failed github.check_suite.failed github.comment.mentioned github.issue.assigned github.issue.labeled github.issue.mentioned github.pull_request.assigned github.pull_request.closed github.pull_request.labeled github.pull_request.mentioned github.pull_request.review_requested github.pull_request.review_submitted github.pull_request.synchronize github.workflow_run.failed gitlab.comment.mentioned gitlab.issue.assigned gitlab.merge_request.opened gitlab.pipeline.failed heartbeat.boot-triage heartbeat.checklist jira.issue.assigned jira.issue.commented jira.issue.followup kapso.message.received linear.issue.assigned linear.issue.followup linear.issue.reassigned slack.assistant.greeting slack.assistant.offline slack.assistant.suggested_prompts slack.message.attachment_failed slack.message.thread_context system.agent.communication system.agent.lead system.agent.memory system.agent.memory.remote system.agent.outputs system.agent.outputs.no_agent_fs system.agent.repository system.agent.role system.agent.secrets system.agent.slack system.agent.tools_skills system.agent.worker system.agent.worker.remote system.agent.workspace system.agent.workspace.remote system.session.lead system.session.lead.managed system.session.worker system.session.worker.managed system.session.worker.remote task.budget.refused task.requester.profile task.worker.completed task.worker.failed".split(
3972
3980
  " ",
3973
3981
  );
3974
3982
 
@@ -5735,7 +5743,7 @@ var config_default = `{
5735
5743
  `;
5736
5744
 
5737
5745
  // templates/skills/agent-browser/content.md
5738
- var content_default = '# agent-browser\n\n`agent-browser` (vercel-labs) is the swarm\'s browser-automation CLI. It drives a headless Chromium over CDP and returns accessibility-tree snapshots with compact `@eN` refs. Use it for any browser task: verify a UI change, take a screenshot, walk a page, fill a form, extract page text, reproduce a frontend bug, or attach UI evidence to a PR, issue, Linear comment, or Slack thread.\n\nDo not use `qa-use`. It is not installed in the worker image any more.\n\n## Step 0: check that the CLI is present\n\nThe full worker image (`agent-swarm-worker:latest`) ships `agent-browser` and a headless Chromium. The slim image (`:slim`) has neither. The first command of any browser work is:\n\n```bash\ncommand -v agent-browser\n```\n\nIf it prints nothing, you are on the slim image. Say so in your progress report ("agent-browser is not available on this worker image") and fall back to manual QA: write the exact steps a human should run and mark the browser test cases as **Blocked**, not failed. Do not install a browser or the CLI yourself.\n\n## Step 1: load the version-matched guide\n\nThe CLI bundles its own usage guide. Load it once per task before the first browser command. Do not guess flags from memory.\n\n```bash\nagent-browser skills get core # overview + common patterns\nagent-browser skills get core --full # adds the full command reference\nagent-browser skills list # specialized guides (dogfood, electron, slack, ...)\n```\n\n## Step 2: the core loop\n\n```bash\nagent-browser open <url>\nagent-browser snapshot -i # accessibility tree, interactive elements only, @eN refs\nagent-browser click @e12 # act on refs from the snapshot\nagent-browser fill @e7 "text"\nagent-browser screenshot /tmp/<name>.png\nagent-browser close\n```\n\n- Re-run `snapshot` after every navigation or state change. Refs are only valid for the snapshot that produced them.\n- Run one browser session at a time. Call `store-progress` between page batches so the heartbeat watchdog sees you.\n- The browser is headless. Local servers inside the worker container are reachable at `http://localhost:<port>`.\n- The image launches Chromium with `--no-sandbox,--disable-dev-shm-usage` through `AGENT_BROWSER_ARGS`. If you pass your own `--args`, include `--no-sandbox` again or Chrome will not start.\n\n## Recording\n\nPass `record start` an absolute path (`/tmp/<topic>/raw.webm`). A relative path resolves against the agent-browser daemon\'s cwd, not yours, and fails with `ffmpeg write failed: Broken pipe` while `doctor` stays green.\n\nDefault: record with `--cursor`, drive every supported pointer action (`click`, `mouse move`, `drag`) with `--human`, and ship the **1.5x video**. Keep the 1x original only if a reviewer asks for it. Human pointer movement follows an eased curve; use `--seed <n>` on `mouse move` to make the take reproducible instead of using a bare move.\n\nOpen the page first, then record the interaction:\n\n```bash\nagent-browser record start ./in.mp4 --cursor\nagent-browser snapshot -i\nagent-browser mouse move 600 400 --human --seed 42 --duration 1400\nagent-browser click @e3 --human # use a ref from the current snapshot\n# Perform the remaining QA steps; use --human for drag actions too.\nagent-browser record stop\nffmpeg -y -i in.mp4 -filter:v "setpts=PTS/1.5,fps=30" -an \\\n -c:v libx264 -preset veryfast -crf 23 -pix_fmt yuv420p out-1.5x.mp4\n```\n\nSpeed-up is a post-process step: `record start` has no speed or human flag. Keep `fps=30` to preserve a steady 30 fps output rather than uneven frame drops at an inferred rate. `-an` disables audio because agent-browser recordings carry no audio track. The worker ships only `ffmpeg`, not `ffprobe`; inspect duration with `ffmpeg -i out-1.5x.mp4 2>&1 | grep Duration`.\n\nAdd `--contact-sheet` to the start command for a timestamped PNG summary of visual changes. Recording needs `ffmpeg` on PATH; check `agent-browser doctor` before starting. Stop the recording before closing the browser so the video is saved. Keep the 1.5x file on the same agent-fs QA path as screenshots below. In a PR, upload it as a GitHub user attachment and put the URL on its own line, so GitHub plays it inline.\n\n## Step 3: share the screenshot through agent-fs\n\nScreenshots on the worker disk disappear with the task. Upload them to agent-fs under the qa path convention: that copy is the durable original. Use `share-create` when a person needs to open the file. `--file` is binary-safe. `--content` is text-only and mangles PNGs.\n\n```bash\nagent-fs write thoughts/<agent-id>/qa/<topic>-screenshots/<name>.png \\\n --file /tmp/<name>.png -m "<what it shows>"\nagent-fs stat thoughts/<agent-id>/qa/<topic>-screenshots/<name>.png --json # confirm size > 0\nagent-fs share-create thoughts/<agent-id>/qa/<topic>-screenshots/<name>.png --json\n```\n\n- **GitHub PR body or comment**: upload the PNG, GIF or mp4 as a GitHub user attachment with the `github-attach` skill and embed `![caption](https://github.com/user-attachments/assets/<uuid>)`. Attachments never expire and mp4s play inline. A UI PR puts a before (main) and after pair in the `## Before / after` table. Fallback for images only, when the GitHub token is an App token (`ghu_`/`ghs_`): `agent-fs signed-url <path> --inline --json` (expires in 7 days). Never a presigned URL for an mp4 or a doc: it downloads.\n- **Linear comment or Slack message**: paste the `share-create` link.\n- In `store-progress`, list the upload in the `attachments` field with `kind: "agent-fs"` and the path, and paste the `share-create` link for people who need to open the file.\n- If `agent-fs auth whoami` fails, report the local path, say the upload was skipped, and continue.\n\nThe `artifacts` skill holds the full agent-fs recipe and the naming conventions.\n\n## Gotchas\n\n- `wait <selector>` waits for a VISIBLE match. When the first match is hidden it times out (about 25 s) even though the element exists. Wait on rendered text instead: `wait --text "<rendered text>"`.\n- Never run `pkill -f <pattern>` inside a Bash call: the pattern also matches the calling shell\'s command line and kills it. Record the PID when you start a background server (`echo $!`) and `kill <pid>`.\n';
5746
+ var content_default = '# agent-browser\n\n`agent-browser` (vercel-labs) is the swarm\'s browser-automation CLI. It drives a headless Chromium over CDP and returns accessibility-tree snapshots with compact `@eN` refs. Use it for any browser task: verify a UI change, take a screenshot, walk a page, fill a form, extract page text, reproduce a frontend bug, or attach UI evidence to a PR, issue, Linear comment, or Slack thread.\n\nDo not use `qa-use`. It is not installed in the worker image any more.\n\n## Step 0: check that the CLI is present\n\nThe full worker image (`agent-swarm-worker:latest`) ships `agent-browser` and a headless Chromium. The slim image (`:slim`) has neither. The first command of any browser work is:\n\n```bash\ncommand -v agent-browser\n```\n\nIf it prints nothing, you are on the slim image. Say so in your progress report ("agent-browser is not available on this worker image") and fall back to manual QA: write the exact steps a human should run and mark the browser test cases as **Blocked**, not failed. Do not install a browser or the CLI yourself.\n\n## Step 1: load the version-matched guide\n\nThe CLI bundles its own usage guide. Load it once per task before the first browser command. Do not guess flags from memory.\n\n```bash\nagent-browser skills get core # overview + common patterns\nagent-browser skills get core --full # adds the full command reference\nagent-browser skills list # specialized guides (dogfood, electron, slack, ...)\n```\n\n## Step 2: the core loop\n\n```bash\nagent-browser open <url>\nagent-browser snapshot -i # accessibility tree, interactive elements only, @eN refs\nagent-browser click @e12 # act on refs from the snapshot\nagent-browser fill @e7 "text"\nagent-browser screenshot /tmp/<name>.png\nagent-browser close\n```\n\n- Re-run `snapshot` after every navigation or state change. Refs are only valid for the snapshot that produced them.\n- Run one browser session at a time. Call `store-progress` between page batches so the heartbeat watchdog sees you.\n- The browser is headless. Local servers inside the worker container are reachable at `http://localhost:<port>`.\n- The image launches Chromium with `--no-sandbox,--disable-dev-shm-usage` through `AGENT_BROWSER_ARGS`. If you pass your own `--args`, include `--no-sandbox` again or Chrome will not start.\n\n## Recording\n\nPass `record start` an absolute path (`/tmp/<topic>/raw.webm`). A relative path resolves against the agent-browser daemon\'s cwd, not yours, and fails with `ffmpeg write failed: Broken pipe` while `doctor` stays green.\n\nDefault: record with `--cursor`, drive every supported pointer action (`click`, `mouse move`, `drag`) with `--human`, and ship the **1.5x video**. Keep the 1x original only if a reviewer asks for it. Human pointer movement follows an eased curve; use `--seed <n>` on `mouse move` to make the take reproducible instead of using a bare move.\n\nOpen the page first, then record the interaction:\n\n```bash\nagent-browser record start ./in.mp4 --cursor\nagent-browser snapshot -i\nagent-browser mouse move 600 400 --human --seed 42 --duration 1400\nagent-browser click @e3 --human # use a ref from the current snapshot\n# Perform the remaining QA steps; use --human for drag actions too.\nagent-browser record stop\nffmpeg -y -i in.mp4 -filter:v "setpts=PTS/1.5,fps=30" -an \\\n -c:v libx264 -preset veryfast -crf 23 -pix_fmt yuv420p out-1.5x.mp4\n```\n\nSpeed-up is a post-process step: `record start` has no speed or human flag. Keep `fps=30` to preserve a steady 30 fps output rather than uneven frame drops at an inferred rate. `-an` disables audio because agent-browser recordings carry no audio track. The worker ships only `ffmpeg`, not `ffprobe`; inspect duration with `ffmpeg -i out-1.5x.mp4 2>&1 | grep Duration`.\n\nAdd `--contact-sheet` to the start command for a timestamped PNG summary of visual changes. Recording needs `ffmpeg` on PATH; check `agent-browser doctor` before starting. Stop the recording before closing the browser so the video is saved. Keep the 1.5x file on the same agent-fs QA path as screenshots below. In a PR, upload it as a GitHub user attachment and put the URL on its own line, so GitHub plays it inline.\n\n## Step 3: share the screenshot through agent-fs\n\nScreenshots on the worker disk disappear with the task. Upload them to agent-fs under the qa path convention: that copy is the durable original. Use `share-create` when a person needs to open the file. `--file` is binary-safe. `--content` is text-only and mangles PNGs.\n\n```bash\nagent-fs write thoughts/<agent-id>/qa/<topic>-screenshots/<name>.png \\\n --file /tmp/<name>.png -m "<what it shows>"\nagent-fs stat thoughts/<agent-id>/qa/<topic>-screenshots/<name>.png --json # confirm size > 0\nagent-fs share-create thoughts/<agent-id>/qa/<topic>-screenshots/<name>.png --json\n```\n\n- **GitHub PR body or comment**: upload the PNG, GIF or mp4 as a GitHub user attachment with the `github-attach` skill and embed `![caption](https://github.com/user-attachments/assets/<uuid>)`. Attachments never expire and mp4s play inline. A UI PR puts a before (main) and after pair in the `## Before / after` table. Fallback for images only, when the GitHub token is an App token (`ghu_`/`ghs_`): `agent-fs signed-url <path> --inline --json` (expires in 7 days). Never a presigned URL for an mp4 or a doc: it downloads.\n- **Linear comment or Slack message**: paste the `share-create` link.\n- In `store-progress`, list the upload in the `attachments` field with `kind: "agent-fs"` and the path, and paste the `share-create` link for people who need to open the file.\n- If `agent-fs auth whoami` fails, report the local path, say the upload was skipped, and continue.\n\nThe `agent-fs` skill holds the full agent-fs recipe.\n\n## Gotchas\n\n- `wait <selector>` waits for a VISIBLE match. When the first match is hidden it times out (about 25 s) even though the element exists. Wait on rendered text instead: `wait --text "<rendered text>"`.\n- Never run `pkill -f <pattern>` inside a Bash call: the pattern also matches the calling shell\'s command line and kills it. Record the PID when you start a background server (`echo $!`) and `kill <pid>`.\n';
5739
5747
 
5740
5748
  // templates/skills/apps/config.json
5741
5749
  var config_default2 = `{
@@ -5755,374 +5763,10 @@ var config_default2 = `{
5755
5763
  `;
5756
5764
 
5757
5765
  // templates/skills/apps/content.md
5758
- var content_default2 = "# Swarm Apps\n\nSwarm Apps are persistent, agent-authored internal applications: a schema-backed row store, named queries, custom script/task actions, and a validated json-render interface served at `/apps/<id>`. Use an app when people need to view and change live records; use `create_page` for a static snapshot and `artifacts` for custom server logic or a UI outside this catalog.\n\n> **Capability gate**: app tools are available when the server's `CAPABILITIES` includes `pages` (for example `CAPABILITIES=core,task-pool,pages`). There is no separate `apps` capability. If these tools are absent from your MCP list, this is why.\n\n## Tools and iteration loop\n\n| Tool | Use it for |\n|---|---|\n| `app-list` | Discover apps by id, name, description, and timestamps. Definitions are omitted. |\n| `app-get` | Read one complete stored definition before changing it. |\n| `app-upsert` | Create an app, or replace an existing app's entire definition when you intentionally have the full desired state. |\n| `app-patch` | Make a focused change while preserving everything not mentioned in the patch. Prefer this for iteration. |\n| `app-query` | Run one declared named query and return its rows. Use this when an agent or saved script needs to consume app data rather than render the app UI. |\n| `app-history` | List definition snapshots and their compact model/column digests before choosing a restore point. |\n| `app-diff` | Show a unified definition diff; defaults to newest snapshot → CURRENT. |\n| `app-rollback` | Restore a snapshot through the normal schema-migration engine. |\n\nFor every edit: `app-get` -> modify the smallest coherent subtree -> `app-patch` -> if rejected, fix every returned `issues[]` entry (`path` + `message`) and retry. Validation happens before storage, so a rejected update leaves the saved app unchanged. Do not guess at the current definition or use `app-upsert` with a partial definition.\n\n`app-patch` can also change `name`; omit `description` to preserve it or pass `description: null` to clear it.\n\n### Reading an app's callable surface\n\nOne `app-get` returns everything needed to drive an app you did not write: `definition.queries` are runnable with `app-query` (supply every `$param` the query references), and `definition.actions` are invocable with `POST /api/apps/<id>/actions/<name>` (a `script` action runs its script, a `task` action spawns a task, a `sync` action refreshes sources). Read a query's `$param` placeholders and an action's kind/args from the definition before calling — do not guess parameter names.\n\n## Safe schema evolution and rollback\n\nPrefer **hiding** a column over deleting it. A hidden column's values stay on every existing row, so an unhide or rollback can make it visible again. Hiding is metadata-only: hidden columns cannot be used in queries, page bindings, `app.mutate` values, or new row writes, and `required` is ignored while hidden. Row GETs still expose the historical value for backward compatibility.\n\nDo not reuse a hidden column name. The name remains held until you either unhide it or explicitly purge its stored values. To permanently remove a column, delete its declaration with `null` and include `{ \"columnName\": { \"purge\": true } }` in the same `migration` object. That is deliberately one-shot and irreversible for row data.\n\nSchema-changing writes can carry this sibling `migration` object:\n\n```json\n{\n \"migration\": {\n \"priority\": { \"from\": \"flag\", \"map\": { \"urgent\": \"high\", \"watch\": \"low\" }, \"else\": \"none\" },\n \"status\": { \"from\": \"flag\", \"map\": { \"watch\": \"watching\", \"done\": \"done\" }, \"else\": \"open\" },\n \"oldColumn\": { \"purge\": true },\n \"count\": { \"coerce\": true, \"else\": 0 },\n \"owner\": { \"set\": \"unassigned\" }\n }\n}\n```\n\nUse `set` to backfill a changed column, `from` with optional `map`/`else` to derive it from one existing (including hidden) column, `coerce` for a kind change, and `purge` only for explicit destruction. Non-purge directives target a column changed in that same write. The server fails loudly before writing when a lossy change needs a directive: inspect every returned `issues[]` item, including its per-value row counts, then choose an explicit policy and retry. Never guess a migration just to get a 200.\n\n**Source lifecycle** rides the same engine with the same all-or-nothing guarantees. Free: adding a source, adding a NEW column with a `source` binding, binding an existing column that holds zero values, changing a binding's `field`/`transform`, and changing a source's `args`/`config`/`connection`/`scriptId` (window changes churn staleness on the next pass — expect `stale` flips when narrowing). Rejected with a path-bearing issue: changing `joinKey` (immutable — remove the source and add it again), changing `connector` while the source owns rows (row count in the message), and binding an existing column that already holds values (purge it — or add a fresh column — and bind that instead; a sync pass would otherwise overwrite the data). Removing a source **detaches** its rows: values survive as ordinary columns, the `source`/`syncedAt`/`stale` envelope is stripped, and the report counts them in `detachedRows`. A patch removing a source must drop its column bindings in the same call — a dangling `source.of` is rejected before anything is written.\n\nSync never adopts rows it does not own: a pulled record whose join key matches an unowned row creates a second, source-owned row. Adding a source over a join-key column that already holds values therefore duplicates those records on the first pass — and remove-source → re-add duplicates for the same reason, because detached rows keep their values. To convert a hand-maintained model into a synced one, purge the old rows first or use a fresh join-key column.\n\nFor example, split a legacy `flag` into `priority` and `status` without losing data in one patch: add the two columns, map both from `flag`, update every affected query/page binding, then hide `flag`.\n\n```json\n{\n \"appId\": \"<app-id>\",\n \"definition\": {\n \"models\": {\n \"ticket\": {\n \"columns\": {\n \"priority\": { \"kind\": \"enum\", \"enum\": [\"none\", \"low\", \"high\"] },\n \"status\": { \"kind\": \"enum\", \"enum\": [\"open\", \"watching\", \"done\"] },\n \"flag\": { \"kind\": \"string\", \"hidden\": true }\n }\n }\n }\n },\n \"migration\": {\n \"priority\": { \"from\": \"flag\", \"map\": { \"urgent\": \"high\", \"watch\": \"low\" }, \"else\": \"none\" },\n \"status\": { \"from\": \"flag\", \"map\": { \"watch\": \"watching\", \"done\": \"done\" }, \"else\": \"open\" }\n }\n}\n```\n\nEvery schema migration returns `{ scanned, backfilled, coerced, mapped, elsed, purgedValues, idxRebuilt, detachedRows, orphanFields }`. Read `orphanFields`: it reports extra row fields that predate or no longer appear in the definition; they are preserved until an explicit purge. `schemaVersion` is server-managed: never set it in an input patch or try to use it as application data.\n\nEvery successful definition write snapshots the previous definition. Use `app-history` to select a version, `app-diff({ appId, from: <version> })` to inspect it against CURRENT, then `app-rollback({ appId, version: <version>, migration? })` to restore it. Rollback is a forward migration over live rows, not row-level time travel: it creates a new pre-rollback snapshot and may reject a lossy restore with the same migration directives and counts. A rejected rollback changes nothing.\n\n### Per-user configuration\n\n`userConfig` declares up to 20 typed settings in the versioned definition; its values are deliberately stored outside the definition, separately for each user (and the operator dashboard). Each field is `{ \"kind\": \"string\" | \"number\" | \"boolean\" | \"date\" | \"enum\", \"default\"?: ..., \"enum\"?: [...], \"label\"?: \"...\" }`. Defaults follow column rules, including enum membership; `required` is not allowed. A missing or obsolete stored value reads as its default, or `null` when no default is declared.\n\nUse `{ \"$state\": \"/user/<field>\" }` on a page for the read-only current-user binding; the field must be declared and the path cannot continue below it. Pure and bound reusable elements cannot bind `/user` directly — pass the value through an element prop from the consuming page. An agent acting on an owned user-requested task reads and edits that requester's preferences. `userConfig` schema changes are always migration-compatible and appear in migration reports; no migration directives are needed. Rollback restores the historical schema but never alters saved per-user values, so values can resurface when a field returns.\n\n### Theming\n\nAn optional top-level `\"theme\"` sets the preset the dashboard applies to the app's rendered canvas — a lowercase slug validated by shape only. The preset catalog ships with the dashboard (`apps/ui/src/lib/themes.ts` + `theme-classics.ts` — update this list and the `app-upsert` tool description when it changes); current ids: `hive` (the stock look), the accent/field presets `meadow`, `iris`, `rose`, `cobalt`, `ember`, `carbon`, and the classic editor/platform presets `github`, `vscode`, `material`, `solarized`, `tokyo`, `monokai`, `gruvbox`, `catppuccin`. Action and destructive tones are never themed; status tones are themed ONLY by the classic presets (their identity includes a status palette — hue semantics stay fixed, success reads green and error reads red everywhere). An id the viewer's dashboard build does not know degrades to the dashboard's own theme, so unknown slugs are safe but pointless. Omit `theme` to inherit whatever theme the viewer runs; `\"hive\"` is an explicit reset to the stock look even inside a themed dashboard.\n\nViewers can override the preset per app from the app's settings drawer; the override rides the reserved `$theme` key of the user-config values (accepted on every app, even without a declared `userConfig` schema, and never visible at `/user/...`). A present override always beats the definition's `theme` — even an unrecognized one, which degrades to the dashboard theme — so treat `theme` as the app's default styling, not a guarantee.\n\nDistinguish the two rollback 400s. A lossy-migration 400 is fixable: copy the required `migration` entries from the message and retry. A target-snapshot validation 400 explicitly says that directives cannot repair that historical definition; use `app-history` and choose a different version.\n\nWhen a schema patch reports stale page/query bindings, repair them in that same patch. Page elements are atomic: replace the complete `pages.<page>.elements.<id>` declaration, removing any reference to a hidden or deleted column, instead of sending only the changed nested prop. This is also the repair move for older apps whose stored page already references an undeclared column; full-definition validation otherwise blocks every patch.\n\n## Definition reference\n\n```json\n{\n \"models\": {\n \"modelName\": {\n \"columns\": {\n \"externalId\": { \"kind\": \"string\" },\n \"title\": { \"kind\": \"string\" }\n }\n }\n },\n \"queries\": { \"queryName\": { \"model\": \"modelName\" } },\n \"actions\": { \"actionName\": { \"kind\": \"task\", \"prompt\": \"Review the current rows and report\" } },\n \"pages\": {\n \"main\": {\n \"root\": \"root\",\n \"elements\": { \"root\": { \"type\": \"Container\", \"props\": {} } }\n }\n },\n \"defaultPage\": \"main\",\n \"theme\": \"ember\"\n}\n```\n\nModel, reusable-element, element-prop, query, action, column, page, and page-param names start with a lowercase letter, contain only letters, numbers, or underscores, and are at most 40 characters. A definition has 0-10 models and at most 20 reusable elements; each declared model has 1-40 columns. Zero-model apps are valid for pure UI utilities as long as their pages and reusable elements validate.\n\n### Reusable elements\n\nTop-level `elements` are versioned inside the app definition and can be reused from the app's own pages or, when explicitly exported, from another app. They are private by default. Each entry declares a `mode`, optional typed `props`, one `root`, and a flat `elements` node map using the same validated tree vocabulary as a page:\n\n```json\n{\n \"elements\": {\n \"statCard\": {\n \"mode\": \"pure\",\n \"export\": true,\n \"props\": {\n \"label\": { \"kind\": \"string\", \"required\": true },\n \"value\": { \"kind\": \"number\", \"default\": 0 }\n },\n \"root\": \"card\",\n \"elements\": {\n \"card\": { \"type\": \"Card\", \"props\": {}, \"children\": [\"label\", \"slot\"] },\n \"label\": { \"type\": \"Metric\", \"props\": { \"label\": { \"$state\": \"/props/label\" }, \"value\": { \"$state\": \"/props/value\" } } },\n \"slot\": { \"type\": \"ElementSlot\", \"props\": {} }\n }\n }\n }\n}\n```\n\n- `pure` elements may read only their declared `/props/<name>` state; `$item` and `$index` are also legal inside repeated nodes. They cannot invoke any action step. A pure tree may contain at most one leaf `ElementSlot` where consumer children are inserted.\n- `bound` elements may additionally read the defining app's declared queries and actions and use its models. Those references are validated against the defining app, including hidden-column rules. Exported bound elements cannot use `app.navigate`; private bound elements may navigate within their defining app.\n- Prop kinds are `string`, `number`, `boolean`, `date`, or `enum`. An enum prop requires a non-empty `enum: [\"value\", ...]` values array. A literal default and each literal consumer value must match the kind and, for enums, be one of those values. A required prop without a default must be supplied.\n\nReference an element with an `ElementRef` node. Omit `app` for same-app reuse; cross-app references name the defining app id and require `export: true`. `instanceKey` is optional. Consumer `children` are accepted only when the target has an `ElementSlot`:\n\n```json\n{\n \"type\": \"ElementRef\",\n \"props\": {\n \"app\": \"<defining-app-id>\",\n \"element\": \"statCard\",\n \"props\": { \"label\": \"Open issues\", \"value\": 12 },\n \"instanceKey\": \"openIssues\"\n },\n \"children\": [\"details\"]\n}\n```\n\nElement references float to the defining app's current definition. `app` and `element` must be literal strings; dynamic references are not supported. The server rejects reference cycles and expansion deeper than five, missing/private targets, invalid props, and a breaking write to an exported element that other apps reference. Removing or unexporting an exported element, changing its mode, removing a declared prop, changing a prop kind, or adding a required prop without a default is breaking; publish the new contract under a new element name. If consumers are abandoned intentionally, retry the PUT, PATCH, or rollback with `\"forceElementBreak\": [\"elementName\"]`. The named consumers will then render an error once element rendering is available; deleting an entire app is deliberately not compatibility-gated.\n\n### Models\n\nEach column is `{ \"kind\": ..., \"required\"?: boolean, \"default\"?: ..., \"index\"?: boolean, \"enum\"?: string[], \"hidden\"?: boolean }`.\n\n| `kind` | Values and indexing |\n|---|---|\n| `string` | String value; set `index: true` only when equality lookups need it. |\n| `number` | Finite number; never indexed. |\n| `boolean` | Boolean; set `index: true` only when equality lookups need it. |\n| `date` | ISO-8601 string; never indexed. |\n| `enum` | String from the non-empty unique `enum` list; always indexed, without `index: true`. |\n\n`default` must match the column kind (and be one of the enum values). `required: true` rejects missing/null values unless a default supplies the value. `id`, `createdAt`, `updatedAt`, `createdBy`, `updatedBy` — and the sync envelope `source`, `syncedAt`, `stale` — are system columns and reserved names. An app created before the sync envelope that declared ordinary columns with these names surfaces `definitionError` after upgrade until they are renamed: patch in a fresh column with a `{from}` migration directive, then purge the old one. Rows always expose `id`/`createdAt`/`updatedAt`; `createdBy`/`updatedBy` record the acting principal (`user:<id>`, `agent:<id>`, `operator`, or `sync:<source>` for engine writes) and are system-managed on every row write.\n\n### Sources and sync\n\nA model may declare up to 4 named `sources` — one-way inbound projections of an external system into its rows. The `script` connector is THE way to bring external data in (GitHub, Linear, any API): a saved script owns the fetch, its auth placeholders, and its paging. `swarm-tasks` is the one native connector, projecting this swarm's own task pool (`config`: `status`/`tags` comma-lists, `agentId`, `assetKey` namespace prefix, `limit` up to 200 — default 100, `includeHeartbeat`). Synced rows are readable by EVERY principal with `app.use` on the app; an unscoped `swarm-tasks` source publishes task prompts (truncated, secret-scrubbed) to all of them, so scope it with `config.assetKey`/`config.agentId` unless that exposure is intended. A user-invoked sync only ever pulls the invoking user's own requested tasks. Script sources run with their run-as identity's credentials — an agent-owned script runs as its owner and only the owner may wire it; an owner-less catalog (global) script runs as the LEAD, so only the lead or the operator can wire or alter such a source (stored references stay valid with their args and connection pinned).\n\n```json\n{\n \"models\": {\n \"issue\": {\n \"columns\": {\n \"issueKey\": { \"kind\": \"string\" },\n \"title\": { \"kind\": \"string\", \"source\": { \"of\": \"gh\", \"field\": \"title\" } },\n \"openedAt\": { \"kind\": \"date\", \"source\": { \"of\": \"gh\", \"field\": \"created_at\", \"transform\": \"date-parse\" } },\n \"note\": { \"kind\": \"string\" }\n },\n \"sources\": {\n \"gh\": { \"connector\": \"script\", \"scriptId\": \"<script-uuid>\", \"joinKey\": \"issueKey\", \"args\": { \"repo\": \"owner/name\" }, \"connection\": \"github\" }\n }\n }\n }\n}\n```\n\n- **Join key**: each source names a `joinKey` — an owned, non-hidden `string` column carrying the external identity. It must not be `required`, must not declare a `default`, and cannot itself be source-bound. A row belongs to at most ONE source (`row.source`).\n- **Column bindings**: `{ \"of\": \"<sourceName>\", \"field\": \"user.login\", \"transform\"? }` projects a (dotted-path) field of the pulled record into the column on every pass. Transforms and their required column kinds: `slug`/`lower`/`upper` → `string`, `cents` → `number` (×100, rounded), `date-parse` → `date` (ISO). A failed transform nulls that one column and warns; it never fails the pass.\n- **Read-only contract**: source-bound columns and the join key reject every direct write (row create/patch/bulk) with a path-bearing issue — mutate them via the source or a sync refresh. Owned columns on the same model stay writable. Every `required` owned column on a sourced model must declare a `default`, because sync-created rows cannot supply it.\n- **Provenance**: synced rows carry `source` (owner), `syncedAt` (last confirmed presence — sortable), and `stale` (true when a complete pull no longer returned the record; clears on reappearance). `syncedAt` advances on every confirmed row WITHOUT touching `updatedAt`; `updatedAt` moves only when projected data actually changed.\n- **`connection`** (optional, script sources): names a connections-system slug the source depends on. It buys write-time validation plus a preflight before every pull; it does not by itself gate egress. Credentials resolve for the sync run-as identity — the script's owner when it has one, else the lead agent — so global connections always work and agent-scoped ones need matching script ownership. The slug is injected into the script's args as `connection`, letting one generic script serve N connections.\n\n**The canonical sync-script contract** (every sync script, always):\n\n```ts\nexport default async function run(args, ctx) {\n // args = { ...source.args, app: { id }, model: \"<model>\", source: \"<sourceName>\", connection?: \"<slug>\" }\n const records = [{ key: \"42\", fields: { title: \"…\", created_at: \"…\" } }];\n return { records, complete: true }; // or return the bare array — a bare array IS a complete snapshot\n}\n```\n\nReturn `Array<{ key, fields }>` or `{ records, complete? }`. `key` is the external identity the engine joins on `joinKey` (numbers are coerced to strings); `fields` is the bag bindings project from. Return `complete: false` whenever the pull window may have missed records — a paged fetch, a hit `limit`, or client-side filtering of a full page. An incomplete pull skips the stale sweep, so rows outside a narrow window are not falsely marked stale; compute `complete` BEFORE any client-side filtering. Hard cap 500 records per pull (over-cap pulls truncate and count as incomplete). An invalid return shape, thrown error, or non-zero exit fails the pass with zero row churn.\n\nThe seeded catalog script `github-issues-pull` is the worked example (placeholder auth via `[REDACTED:GITHUB_TOKEN]`, PR filtering, `complete` computed before filtering). For scheduled refreshes, point a `targetType: \"script\"` schedule at the seeded `app-sync-run` with `scriptArgs: { \"appId\": \"<id>\", \"model\"?, \"source\"? }`.\n\n**Three refresh doors, one engine**: `POST /api/apps/<id>/sync` (body `{ \"model\"?, \"source\"? }`), a `sync` action kind, and the `app-sync` MCP tool (`ctx.swarm.app_sync` from scripts). `{model?, source?}` fans out to every matching (model × source) pair; each pass reports `{ pulled, created, updated, refreshed, unchanged, markedStale, warnings }`. A pair already syncing in this API process short-circuits with `alreadyRunning: true` instead of pulling twice. Each pair's last-pass status (`lastFinishedAt`, counts, `error?`) comes back as `syncStatus` — keyed `\"<model>:<source>\"` — on `GET /api/apps/<id>` and in `app-get` data, so UI and agents can render \"last synced / last error\" without extra calls.\n\n### Freshness and the refresh button\n\nFreshness renders with the existing column kinds — no special components. Give the table `{ \"key\": \"syncedAt\", \"kind\": \"date\" }` and `{ \"key\": \"stale\", \"kind\": \"badge\", \"tones\": { \"true\": \"warning\" } }` columns, declare a `sync` action, and wire a Button to it:\n\n```json\n{\n \"actions\": { \"refresh\": { \"kind\": \"sync\" } },\n \"pages\": {\n \"main\": {\n \"root\": \"root\",\n \"elements\": {\n \"root\": { \"type\": \"Container\", \"props\": {}, \"children\": [\"refreshBtn\", \"issuesTable\"] },\n \"refreshBtn\": {\n \"type\": \"Button\",\n \"props\": { \"label\": \"Refresh\" },\n \"on\": { \"press\": [{ \"action\": \"app.action\", \"params\": { \"name\": \"refresh\" } }] }\n },\n \"issuesTable\": {\n \"type\": \"Table\",\n \"props\": {\n \"data\": { \"$state\": \"/queries/allIssues/data\" },\n \"columns\": [\n { \"key\": \"title\", \"label\": \"Issue\" },\n { \"key\": \"syncedAt\", \"label\": \"Synced\", \"kind\": \"date\" },\n { \"key\": \"stale\", \"label\": \"Stale\", \"kind\": \"badge\", \"tones\": { \"true\": \"warning\" } }\n ]\n }\n }\n }\n }\n }\n}\n```\n\nA `sync` action answers the script-action state shape (`/actions/refresh` → `running` → `ok`/`error`, result under `result.passes`), so the page's queries refetch when it completes. Use `app-query` to READ rows (named queries may filter on `stale`/`source` and sort by `syncedAt`); use `app-sync` to REFRESH sources; never poll a sync door in a loop — schedule it or give people the button.\n\n### Queries\n\nA named query is:\n\n```json\n{\n \"model\": \"idea\",\n \"filter\": { \"status\": \"open\" },\n \"sort\": { \"column\": \"createdAt\", \"dir\": \"desc\" },\n \"limit\": 100\n}\n```\n\n`filter` is a strict AND of equality checks. A filter may target a declared model column or a system column (`id`, `createdAt`, `updatedAt`, `createdBy`, `updatedBy`, and on synced models `source`, `syncedAt`, `stale`) — filtering on `id` is the universal way to select one row for a detail view, and `{ \"stale\": true }` selects rows a complete pull stopped confirming. A literal value must match the column's kind; date filters compare the stored raw ISO string, not parsed instants. Omit `limit` for the default 200 rows, or set an integer from 1 through 1000. `sort.column` is a model column, `createdAt`, `updatedAt`, or `syncedAt`; `dir` is `asc` or `desc`. Query runtime state is `{ data, loading, error }` under `/queries/<queryName>`.\n\nA filter value may instead be exactly `{ \"$param\": \"<name>\" }`. At execution, the caller supplies that name and the server coerces its value to the filtered column's kind before equality matching:\n\n```json\n{\n \"queries\": {\n \"issueDetail\": { \"model\": \"issue\", \"filter\": { \"issueId\": { \"$param\": \"issueId\" } }, \"limit\": 1 }\n }\n}\n```\n\nThe placeholder object must contain only `$param`. Missing caller values fail loudly rather than returning unfiltered rows: HTTP reports 400 and `app-query` returns a tool error listing every missing name. Supplying a param name the query does not reference is also an error. The app UI supplies current declared route params automatically. Direct calls use `app-query({ appId, query: \"issueDetail\", params: { issueId: 42 } })` or `ctx.swarm.app_query({ appId, query: \"issueDetail\", params: { issueId: 42 } })`.\n\n### Custom actions\n\nActions are optional (maximum 20) and are invoked from the page with the `app.action` runtime action.\n\n```json\n{\n \"actions\": {\n \"recalculate\": {\n \"kind\": \"script\",\n \"scriptId\": \"00000000-0000-4000-8000-000000000000\",\n \"args\": { \"mode\": \"fast\" }\n },\n \"triage\": {\n \"kind\": \"task\",\n \"prompt\": \"Triage this idea and report a recommendation\"\n }\n }\n}\n```\n\n| Kind | Contract |\n|---|---|\n| `script` | `scriptId` must identify an existing script; optional `args` are defaults. Invocation input overrides same-named defaults, and the runtime also supplies `app: { id }`. |\n| `task` | `prompt` must be non-empty; omit `agentId` to use default (lead) assignment. Only set `agentId` to a real agent UUID. Invocation `input` is included as context for the task prompt. Spawned tasks carry asset key `shared/app:<appId>/action:<name>/`, so a `swarm-tasks` source with `config.assetKey: \"shared/app:<appId>/\"` pulls back the tasks this app spawned. |\n| `sync` | Optional `model` and `source` narrow which (model × source) pairs refresh; omitting both syncs every pair the app declares. At least one pair must exist. Answers the script-kind state shape with per-pass results under `result.passes`. |\n\n- Invocation state lands at `/actions/<name>` as `{ status, result?, error?, taskId?, taskStatus? }`, where `status` is `\"running\"`, `\"ok\"`, or `\"error\"`.\n\nUse a task action named Tackle when a person should hand one issue row to the swarm. Pass the complete current row under `input.issue`; `{ \"$row\": \"\" }` resolves to the whole row and that context lands in the task prompt.\n\n```json\n{\n \"actions\": {\n \"tackle\": { \"kind\": \"task\", \"prompt\": \"Tackle this issue. Inspect the supplied issue context, do the work, and report the outcome.\" }\n },\n \"pages\": {\n \"main\": {\n \"root\": \"issuesTable\",\n \"elements\": {\n \"issuesTable\": {\n \"type\": \"Table\",\n \"props\": {\n \"data\": { \"$state\": \"/queries/allIssues/data\" },\n \"columns\": [{ \"key\": \"title\", \"label\": \"Issue\" }],\n \"rowActions\": [{\n \"label\": \"Tackle\",\n \"actions\": [{\n \"action\": \"app.action\",\n \"params\": { \"name\": \"tackle\", \"input\": { \"issue\": { \"$row\": \"\" } } }\n }]\n }]\n }\n }\n }\n }\n },\n \"defaultPage\": \"main\"\n}\n```\n\nFor direct MCP calls, `app-query` accepts `{ appId, query, params? }` and returns rows from that declared named query; every supplied param must be referenced by a `$param` filter in that query. Use it when an agent needs to read app state without scraping the UI.\n\nSaved scripts use the generated SDK name `ctx.swarm.app_query({ appId, query, params? })`. Use `app_query` in scripts or workflows that turn current app rows into reports, digests, or follow-up work. Saved scripts also get generated per-app types automatically: each app contributes an `App_<Name>` namespace with one row interface per model (enum columns as literal unions) and a typed `app_query` overload per named query, all visible via `script-query-types`. Actions stay REST-only from scripts — there is no SDK method to invoke them.\n\n### Pages, routes, and page trees\n\n`pages` is a map of independently validated page trees and `defaultPage` must name one entry. Each page may also declare a display `title` and typed route `params`. On every non-default page the runtime automatically renders breadcrumbs (`<default page title> › <current page title>`, first crumb navigates back) — including in chromeless embeds — so a detail page does NOT need its own back button; give pages good `title`s instead. This complete pattern connects a table row to a typed detail route and its parameterized query:\n\n```json\n{\n \"models\": {\n \"issue\": {\n \"columns\": {\n \"issueId\": { \"kind\": \"number\" },\n \"title\": { \"kind\": \"string\" },\n \"status\": { \"kind\": \"string\" }\n }\n }\n },\n \"queries\": {\n \"allIssues\": { \"model\": \"issue\" },\n \"issueDetail\": {\n \"model\": \"issue\",\n \"filter\": { \"issueId\": { \"$param\": \"issueId\" } },\n \"limit\": 1\n }\n },\n \"pages\": {\n \"issues\": {\n \"title\": \"Issues\",\n \"root\": \"issuesTable\",\n \"elements\": {\n \"issuesTable\": {\n \"type\": \"Table\",\n \"props\": {\n \"data\": { \"$state\": \"/queries/allIssues/data\" },\n \"columns\": [\n { \"key\": \"title\", \"label\": \"Issue\" },\n { \"key\": \"status\", \"label\": \"Status\", \"kind\": \"badge\" }\n ],\n \"rowActions\": [{\n \"label\": \"Open\",\n \"actions\": [{\n \"action\": \"app.navigate\",\n \"params\": {\n \"page\": \"detail\",\n \"params\": { \"issueId\": { \"$row\": \"issueId\" } }\n }\n }]\n }]\n }\n }\n }\n },\n \"detail\": {\n \"title\": \"Issue detail\",\n \"params\": { \"issueId\": { \"kind\": \"number\", \"required\": true } },\n \"root\": \"detail\",\n \"elements\": {\n \"detail\": {\n \"type\": \"DetailList\",\n \"props\": {\n \"data\": { \"$state\": \"/queries/issueDetail/data/0\" },\n \"fields\": [{ \"key\": \"title\", \"label\": \"Issue\" }]\n }\n }\n }\n }\n },\n \"defaultPage\": \"issues\"\n}\n```\n\nDrawer variant: declare a route param on the containing page, set the Drawer `props.param` to that literal param name, and use the same `app.navigate` shape targeting that page with the row id assigned to the Drawer's param. The Drawer then opens from route state without a separate action contract.\n\nParam `kind` is `string` by default, or `number` / `boolean`; `required` defaults to false. The names `mode`, `apiUrl`, `apiKey`, `email`, and `name` are reserved. `/apps/<id>` renders `defaultPage`; `/apps/<id>/p/<page>?issueId=42` is the shareable deep link for another page. Browser navigation is client-side, so app state and polled data stay warm across page changes.\n\nWithin each page, `root` names one entry in its non-empty `elements` map. Elements are a flat, single-parent tree: ids are map keys, `children` contains ids (not nested elements), all elements must be reachable from `root`, and cycles, missing children, or shared children are invalid. Element and Form/UI ids are page-local. Element keys are `type`, `props`, `children`, `on`, `visible`, `repeat`, and `watch`; only components with a `default` child slot accept `children` (Stack, Grid, Split, Tabs, Container, Card, and Drawer). `watch` maps state paths to an action step or chain.\n\n## Component catalog (all 23)\n\nProps reject unknown keys. A `{\"$state\":\"...\"}` binding may replace a literal prop value at any depth.\n\n| Component | Required props | Optional props / values |\n|---|---|---|\n| `Stack` | none | Primary layout; `direction: \"column\", \"row\"`, `gap`, `padding: \"none\", \"xs\", \"sm\", \"md\", \"lg\", \"xl\"`; `align: \"start\", \"center\", \"end\", \"stretch\"`, `justify: \"start\", \"center\", \"end\", \"between\"`, `wrap`; has the `default` child slot. |\n| `Grid` | none | Responsive `columns`: integer 1 through 6, or `{ base, sm, md, lg }` counts 1 through 6; `gap` uses the Stack spacing values; has the `default` child slot. |\n| `Split` | none | `ratio: \"1-1\", \"1-2\", \"2-1\", \"1-3\", \"3-1\"`, `gap`, `collapseBelow: \"sm\", \"md\", \"lg\"`, `reverse`; has the positional `default` child slot. |\n| `Divider` | none | `label`; no children. |\n| `Tabs` | `id`, `tabs` | `tabs` entries are `{ key, label? }`; `defaultTab`; has the positional `default` child slot. |\n| `Container` | none | Legacy layout primitive; prefer Stack. `direction: \"row\", \"column\"`, `gap: \"none\", \"sm\", \"md\", \"lg\"`; has the `default` child slot. |\n| `Card` | none | `title`, `description`; has the `default` child slot. |\n| `Heading` | `text` | `level: \"h1\", \"h2\", or \"h3\"`. |\n| `Text` | `content` | `tone: \"default\" or \"muted\"`. |\n| `Markdown` | `content` | Rendered Markdown for help, instructions, and rich prose; no children. |\n| `SearchInput` | `id` | `placeholder`, `label`; writes debounced text to `/ui/<id>/value`; no children. |\n| `Select` | `id`, `options` | `options` are strings or `{ value, label? }`; `placeholder`, `label`, `clearable`; writes a string or null to `/ui/<id>/value`; no children. |\n| `Button` | `label` | `variant: \"default\", \"secondary\", \"outline\", \"ghost\", \"destructive\", or \"destructive-outline\"` (use `destructive-outline` for standalone deletes); `disabled` (constant or bound boolean); `busyWith: \"<actionName>\"` disables and spins while that custom action's `/actions/<name>/status` is `\"running\"`; dispatch with element-level `on.press`. |\n| `Metric` | `label`, `value` | `value` is string or number; bind `loading` to `/queries/<name>/loading` for a skeleton while the query loads. |\n| `Alert` | `message` | `title`, `tone: \"info\", \"success\", \"warning\", or \"error\"`. |\n| `Badge` | `text` | `text` is string or number; `tone: \"neutral\", \"success\", \"active\", \"error\", \"info\", \"pending\", \"warning\", or \"paused\"`. |\n| `Table` | `columns` | `data`, `loading`, `error`, `emptyMessage`, `rowActions`, `search`, `filters`; see below. |\n| `Form` | `id`, `fields`, `onSubmit` | `title`, `submitLabel`; see below. |\n| `Drawer` | `param` | `title`, `description`, `side: \"right\" or \"left\"`, `size: \"sm\", \"md\", \"lg\", or \"xl\"`; has the `default` child slot. |\n| `DetailList` | `fields` | `data`, `emptyMessage`, `columns: 1 or 2`, `loading` (bind `/queries/<name>/loading` for skeleton fields while loading); renders one record without edit controls. |\n| `Kanban` | `columnField`, `columns`, `titleField` | `data`, `loading`, `cardFields`, `idField` (default `id`), `onMove`, `onCardClick`; see below. |\n| `Calendar` | `startField`, `titleField` | `data`, `loading`, `endField`, `idField`, `onSelect`; read-only month grid; see below. |\n| `ContributionGraph` | none | `data`, `loading`, `dateField` (default `date`), `countField`, `days` (7-730, default 365); read-only activity heatmap. |\n\nTable details:\n\n- `columns[]`: `{ key, label?, kind?, tones?: {value: badgeTone}, width?: number, pinned?: \"left\" or \"right\" }`. `kind` is `text`, `string`, `enum`, `number`, `boolean`, `date`, or `badge`; `string` and `enum` render as text. Pin id/actions columns so they survive horizontal scroll on narrow viewports.\n- `pagination`: boolean; defaults to auto-enabling past 200 rows (set `false` to force one scroll region). `density: \"compact\"` tightens row height for dense readouts.\n- `rowActions[]`: `{ label, variant?, confirm?, actions }`. Variants add `destructive-outline` to the Button variants. `destructive` and `destructive-outline` confirm by default; customize with `{ \"title\": ..., \"description\": ..., \"confirmLabel\": ... }`, or use a bare `confirm` string as the dialog description (use `confirm: false` only for a reversible action).\n- `search`: optional string; case-insensitive substring matching across the listed string and number columns. Bind a SearchInput value for client-side search.\n- `filters`: optional record of per-column string, number, boolean, or null values. Null, empty, or absent values disable one filter. Bind Select values for client-side equality filters.\n\nKanban details:\n\n- One card per `data` row, grouped by the row's `columnField` value. `columns[]` are plain strings or `{ id, label? }`, in display order; rows matching no column are hidden.\n- `cardFields[]` uses the DetailList field shape (`{ key, label?, kind?, tones? }`) for extra lines under the title.\n- `onMove` is an action chain run when a card is dropped in another column. `$row` is the moved row with `columnField` already set to the new column: `[{ \"action\": \"app.mutate\", \"params\": { \"model\": \"deal\", \"op\": \"update\", \"rowId\": { \"$row\": \"id\" }, \"values\": { \"stage\": { \"$row\": \"stage\" } } } }]`. Without `onMove`, cards cannot be dragged.\n- `onCardClick` is an action chain with `$row`; pair it with `app.navigate` and a `Drawer` to open a card.\n\nCalendar details:\n\n- `startField` / `endField` hold ISO dates, ISO datetimes, or epoch ms. An event shows on every day from start to end; without `endField` it shows on its start day.\n- Each Calendar keeps its own visible month (prev/next controls in its header). `onSelect` is an action chain with `$row`, run when an event is clicked.\n\nContributionGraph details:\n\n- Buckets `data` rows by the calendar day of `dateField` over the trailing `days`. Each row counts 1, or adds its numeric `countField` when set.\n\nForm details:\n\n- `fields[]`: `{ name, label?, kind?, options?: string[], placeholder?, required? }`; `kind` is `string`, `text`, `number`, `boolean`, `date`, or `enum`, and enum fields need `options`.\n- Values live at `/forms/<formId>/<fieldName>`. `onSubmit` is an action chain.\n- The submit button shows a pending spinner while the chain runs, and `app.mutate` failures in the chain render inline under the fields (not the page banner) — no extra wiring needed.\n\nDrawer details:\n\n- `param` must name a param declared on the containing page. The drawer is open exactly when `/route/params/<param>` has a non-empty value, so its open state survives refresh and is shareable. Its children mount only while open; closing it clears that param with history replacement.\n\nDetailList details:\n\n- `fields[]`: `{ key, label?, kind?, tones? }`. Kinds reuse Table formatting (`text`, `string`, `number`, `boolean`, `date`, `badge`, `enum`) and add `code` for monospace raw or JSON values. Bind `data` to one record, commonly `/queries/<detailQuery>/data/0`.\n\n## Layout & interactivity\n\nUse `Stack` as the primary page and section layout; `Container` is the legacy two-prop primitive retained for existing pages. Stack supports vertical or horizontal flow, shared spacing, alignment, justification, wrapping, and padding; row Stacks (filter bars, button rows) should set `collapseBelow: \"sm\"|\"md\"|\"lg\"` so they stack vertically on phones. Use `Grid` for responsive card or metric strips: a bare column count is responsive shorthand (1 column on phones, 2 from `sm`, the count from `md` up), or pin breakpoints explicitly with `{ \"base\": 1, \"md\": 2, \"lg\": 3 }`. `Grid` and `Split` also accept `padding` on the shared spacing scale.\n\n`Split` and `Tabs` children are positional. For Split, `children[0]` is the first pane, `children[1]` is the second, and extra children append inside the second pane. Below `collapseBelow`, panes stack; `reverse` changes only that narrow-layout stacking order. For Tabs, `children[i]` is the body for `tabs[i]`; keep both arrays in the same order and with the same count. Inactive tab children stay mounted but hidden, so Tables keep polling. Use `Divider` to separate sections and `Markdown` for richer explanatory content.\n\nSearchInput and Select are client-side controls: each needs a literal `id` and writes state under `/ui/<id>/value`; Tabs writes its active key to `/ui/<id>/tab`. Bind those values into a Table's `search` and `filters` props. This filters already-polled rows locally; it does not alter or re-run the named query.\n\n```json\n{\n \"filters\": { \"type\": \"Stack\", \"props\": { \"gap\": \"sm\" }, \"children\": [\"query\", \"status\"] },\n \"query\": { \"type\": \"SearchInput\", \"props\": { \"id\": \"ideaSearch\", \"placeholder\": \"Search ideas\" } },\n \"status\": { \"type\": \"Select\", \"props\": { \"id\": \"ideaStatus\", \"options\": [\"open\", \"done\"] } },\n \"ideas\": {\n \"type\": \"Table\",\n \"props\": {\n \"data\": { \"$state\": \"/queries/allIdeas/data\" },\n \"columns\": [{ \"key\": \"title\" }, { \"key\": \"status\" }],\n \"search\": { \"$state\": \"/ui/ideaSearch/value\" },\n \"filters\": { \"status\": { \"$state\": \"/ui/ideaStatus/value\" } }\n }\n }\n}\n```\n\n`visible` accepts a boolean, a `$state` truthiness binding, comparisons, and logical groups. Put exactly one comparison key in each condition object. The renderer evaluates the first matching key in fixed priority order (`eq`, `neq`, `gt`, `gte`, `lt`, `lte`; otherwise truthiness), so additional comparison keys in the same object are ignored. Use `$and` / `$or` arrays to combine conditions. `not` is a negation FLAG inside a condition object (`\"not\": true`), never a wrapper around one — `{ \"not\": { \"$state\": … } }` is invalid and the validator rejects it. The absent-record pattern (e.g. an alert when a detail query matched nothing) is `{ \"$state\": \"/queries/<q>/data/0/id\", \"not\": true }`:\n\n```json\n[\n { \"$state\": \"/route/params/issueId\", \"gt\": 0 },\n {\n \"$and\": [\n { \"$state\": \"/route/page\", \"eq\": \"detail\" },\n { \"$state\": \"/route/params/panel\", \"neq\": \"hidden\" }\n ]\n }\n]\n```\n\n## Bindings, sentinels, and action chains\n\nUse this exact query binding shape; do not invent `$query`, `$item`, or template strings:\n\n```json\n{\n \"data\": { \"$state\": \"/queries/allIdeas/data\" },\n \"loading\": { \"$state\": \"/queries/allIdeas/loading\" },\n \"error\": { \"$state\": \"/queries/allIdeas/error\" }\n}\n```\n\nValid `$state` roots are:\n\n- `/queries/<declaredQuery>/...`\n- `/forms/<formId>/...`, where a `Form` element has that literal `props.id`\n- `/ui/<id>/...`, where a SearchInput, Select, or Tabs element has that literal `props.id` (SearchInput and Select use `/value`; Tabs uses `/tab`)\n- `/actions/<declaredAction>/...`\n- `/route/page` for the active page name\n- `/route/params/<declaredParam>` for a param declared on the current page\n\n`/route` is mirrored as `{ page, params }`. Only declared params appear. Values are coerced by their declared kind: `number` becomes a number when possible; boolean strings `true`/`1` become true; failed number coercion remains the raw string.\n\nOnly inside action-chain `params` (`on.<event>`, `Table.rowActions[].actions`, or `Form.onSubmit`) these scoped sentinels are valid, recursively inside objects and arrays:\n\n| Sentinel | Resolves to |\n|---|---|\n| `{ \"$row\": \"id\" }` | Current Table row's `id`; another column name selects that field. |\n| `{ \"$row\": \"\" }` | Entire current Table row. |\n| `{ \"$rowIndex\": true }` | Current Table row index. |\n| `{ \"$form\": \"title\" }` | One current Form value. |\n| `{ \"$form\": \"\" }` | All current Form values. |\n| `{ \"$state\": \"/queries/allIdeas/data\" }` | Current value at a valid app state path, including action state such as `/actions/refreshIssues/status`. |\n\nEach sentinel object must contain exactly the single key shown.\n\nAn action-chain step is `{ \"action\": \"<type>\", \"params\": {...} }`. Available action types:\n\n- `app.mutate`: `{ model, op, rowId?, values?, formId? }`, where `op` is `\"create\"`, `\"update\"`, or `\"delete\"`. `update`/`delete` require `rowId` (usually `{ \"$row\": \"id\" }`); literal `values` keys must be model columns. Successful mutation refetches all queries on that model.\n- `app.refresh`: `{ query? }`; omit `query` to refetch all, or name a declared query.\n- `app.action`: `{ name, input? }`; `name` must be declared in definition `actions`. For a task-kind row action, pass whole-row context with `input: { issue: { \"$row\": \"\" } }`.\n- `app.navigate`: `{ page, params? }`; `page` must be declared, supplied keys must be params of the target page, and every required target param must be supplied. `params` replace the current route params wholesale and may contain action sentinels. Navigation pushes history, so browser Back returns to the previous page/params; only `mode` is preserved automatically.\n- `swarm.sdk`: `{ sdk, args? }`; invokes a catalog-supported Swarm browser SDK method with the viewer's bearer.\n- `swarm.call`: `{ method, endpoint: \"/api/...\", body? }`, where `method` is `\"GET\"`, `\"POST\"`, `\"PUT\"`, `\"DELETE\"`, or `\"PATCH\"`; raw authenticated API call.\n\n## Patch semantics\n\n`app-patch.definition` is RFC 7396 JSON Merge Patch applied to the stored definition:\n\n- omitted keys stay unchanged;\n- object keys merge recursively;\n- arrays and scalar values replace;\n- `null` deletes a key;\n- reusable-element disambiguation: a patch value for `elements.<name>` containing ONLY the `elements` key merges node-by-node; any other key present (`mode`, `root`, `props`, or `export`) makes it a full element replace — restate every field you want kept. In that node-by-node form, `elements.<name>.elements.<id>: null` deletes one node. Literal null nodes are rejected in a full element replace so they cannot be mistaken for deletions.\n- otherwise every supplied `pages.<page>.elements.<id>`, `pages.<page>.params.<param>`, `actions.<name>`, `models.<name>.columns.<col>`, and `models.<name>.sources.<src>` value is atomic and replaces that complete declaration; `null` deletes it (deleting a source detaches its rows — see the source lifecycle above).\n\n`pages.<page>: null` deletes a page, except the current `defaultPage` cannot be deleted. `root`, `title`, and top-level `defaultPage` use ordinary merge semantics. A top-level `page` patch is rejected with guidance to patch `pages.<name>` instead.\n\nBecause elements, param declarations, actions, and columns are atomic, include the complete desired object when changing one. For example, changing only a Table's `emptyMessage` requires sending its full `{ \"type\": \"Table\", \"props\": ... }` element. The merged result is validated as a whole; on failure, read `issues[]`, correct the paths, and retry without assuming anything was written.\n\n```json\n{\n \"appId\": \"<app-id>\",\n \"description\": null,\n \"definition\": {\n \"models\": {\n \"idea\": {\n \"columns\": {\n \"priority\": { \"kind\": \"enum\", \"enum\": [\"low\", \"high\"], \"default\": \"low\" }\n }\n }\n },\n \"queries\": { \"openIdeas\": null },\n \"pages\": {\n \"main\": {\n \"elements\": {\n \"title\": { \"type\": \"Heading\", \"props\": { \"text\": \"Prioritized ideas\", \"level\": \"h1\" } }\n }\n }\n }\n }\n}\n```\n\n## Worked example: ideas tracker\n\nCreate this with `app-upsert`:\n\n```json\n{\n \"name\": \"Ideas Tracker\",\n \"description\": \"Collect and manage product ideas\",\n \"definition\": {\n \"models\": {\n \"idea\": {\n \"columns\": {\n \"title\": { \"kind\": \"string\", \"required\": true },\n \"status\": { \"kind\": \"enum\", \"enum\": [\"open\", \"done\"], \"default\": \"open\" }\n }\n }\n },\n \"queries\": {\n \"allIdeas\": { \"model\": \"idea\", \"sort\": { \"column\": \"createdAt\", \"dir\": \"desc\" } }\n },\n \"actions\": {\n \"triage\": { \"kind\": \"task\", \"prompt\": \"Review current ideas and recommend the next one to pursue\" }\n },\n \"pages\": {\n \"main\": {\n \"root\": \"root\",\n \"elements\": {\n \"root\": {\n \"type\": \"Container\",\n \"props\": { \"direction\": \"column\", \"gap\": \"lg\" },\n \"children\": [\"heading\", \"formCard\", \"ideasCard\", \"triage\"]\n },\n \"heading\": { \"type\": \"Heading\", \"props\": { \"text\": \"Ideas\", \"level\": \"h1\" } },\n \"formCard\": {\n \"type\": \"Card\",\n \"props\": { \"title\": \"Add an idea\" },\n \"children\": [\"ideaForm\"]\n },\n \"ideaForm\": {\n \"type\": \"Form\",\n \"props\": {\n \"id\": \"newIdea\",\n \"fields\": [{ \"name\": \"title\", \"label\": \"Title\", \"required\": true }],\n \"submitLabel\": \"Add\",\n \"onSubmit\": [{\n \"action\": \"app.mutate\",\n \"params\": { \"model\": \"idea\", \"op\": \"create\", \"values\": { \"$form\": \"\" } }\n }]\n }\n },\n \"ideasCard\": {\n \"type\": \"Card\",\n \"props\": { \"title\": \"All ideas\" },\n \"children\": [\"ideasTable\"]\n },\n \"ideasTable\": {\n \"type\": \"Table\",\n \"props\": {\n \"data\": { \"$state\": \"/queries/allIdeas/data\" },\n \"loading\": { \"$state\": \"/queries/allIdeas/loading\" },\n \"error\": { \"$state\": \"/queries/allIdeas/error\" },\n \"columns\": [\n { \"key\": \"title\", \"label\": \"Title\" },\n { \"key\": \"status\", \"label\": \"Status\", \"kind\": \"badge\", \"tones\": { \"open\": \"info\", \"done\": \"success\" } }\n ],\n \"rowActions\": [{\n \"label\": \"Delete\",\n \"variant\": \"destructive-outline\",\n \"actions\": [{ \"action\": \"app.mutate\", \"params\": { \"model\": \"idea\", \"op\": \"delete\", \"rowId\": { \"$row\": \"id\" } } }]\n }]\n }\n },\n \"triage\": {\n \"type\": \"Button\",\n \"props\": { \"label\": \"Ask swarm to triage\", \"variant\": \"secondary\" },\n \"on\": { \"press\": [{ \"action\": \"app.action\", \"params\": { \"name\": \"triage\" } }] }\n }\n }\n }\n },\n \"defaultPage\": \"main\"\n }\n}\n```\n\nThe tool returns `{ appId, url: \"/apps/<id>\" }`. Open that URL to verify the live UI, then continue with the read-patch-validate loop above.\n";
5759
-
5760
- // templates/skills/artifacts/config.json
5761
- var config_default3 = `{
5762
- "kind": "skill",
5763
- "name": "artifacts",
5764
- "displayName": "Artifacts",
5765
- "slug": "artifacts",
5766
- "title": "Artifacts",
5767
- "description": "Store durable task outputs (reports, screenshots, exports, logs) in agent-fs or the shared workspace, and serve interactive web content to a public auth-protected URL via localtunnel. Use when asked to save evidence for a PR/ticket, share a file, \\"create an artifact for X\\", \\"host this for me\\", \\"spin up a web server\\", or \\"give me a live link\\". Covers agent-fs writes and verification, binary uploads, naming and attachment conventions, the \`agent-swarm artifact\` CLI, Hono apps, PM2 daemonization, HTTP Basic auth, and the injected browser SDK.",
5768
- "version": "2.0.0",
5769
- "category": "skills",
5770
- "placeholders": [],
5771
- "runAllSeedersCandidate": true,
5772
- "systemDefault": false,
5773
- "tags": ["artifacts", "evidence", "qa", "agent-fs", "serving", "tunnel"]
5774
- }
5775
- `;
5776
-
5777
- // templates/skills/artifacts/content.md
5778
- var content_default3 = `# Artifacts
5779
-
5780
- An artifact is any output that should outlive the session — a report,
5781
- screenshot, recording, log, data export, dashboard. There are two things you
5782
- can do with one, and this skill covers both:
5783
-
5784
- - **Store it** — write it to agent-fs or the shared workspace so humans and
5785
- other agents can find it later. This is the common case.
5786
- - **Serve it** — run a real web server behind a public, auth-protected URL so a
5787
- human can *interact* with it (custom routes, form posts, live data).
5788
-
5789
- | You need… | Use |
5790
- |---|---|
5791
- | A file, report, screenshot, or export someone will read later | **Store** (agent-fs) |
5792
- | A file another agent needs during *this* session | **Store** (\`/workspace/shared/\`) |
5793
- | A static HTML/JSON report shared by URL | The **\`pages\`** skill — cheaper, no process to babysit |
5794
- | Custom routes, websockets, file uploads, per-request computation | **Serve** (below) |
5795
-
5796
- Rule of thumb: if the content is a *snapshot*, store it or make a page. If it's
5797
- a *running program*, serve it.
5798
-
5799
- ---
5800
-
5801
- # Part 1 — Storing artifacts
5802
-
5803
- Two stores: **agent-fs** (structured, searchable, shareable, survives restarts)
5804
- and the **shared workspace filesystem** (\`/workspace/shared/\`, fast, same-session).
5805
-
5806
- ## When to create one
5807
-
5808
- - Your task produces a deliverable humans should review.
5809
- - Another agent or future session needs to pick up where you left off.
5810
- - You want to attach evidence to a PR, Linear ticket, or Slack message.
5811
- - The output is too large for \`store-progress.output\`.
5812
-
5813
- ## agent-fs (preferred for anything human-shareable)
5814
-
5815
- \`\`\`bash
5816
- # Write to your personal drive
5817
- agent-fs write thoughts/research/2026-05-28-topic.md --content "..." -m "description"
5818
-
5819
- # Write to the shared drive (humans + other agents can see it)
5820
- agent-fs --org <org-id> write \\
5821
- thoughts/<agent-id>/research/2026-05-28-topic.md \\
5822
- --content "..." -m "research findings"
5823
- \`\`\`
5824
-
5825
- Your org and drive context is already configured — you only need \`--org\` when
5826
- writing somewhere other than your default. Read the id from
5827
- \`agent-fs stat <path> --json\` rather than hardcoding one you saw in an example.
5828
-
5829
- Always verify the write landed — agent-fs writes can fail silently with an
5830
- empty payload:
5831
-
5832
- \`\`\`bash
5833
- agent-fs stat <path> --json | jq '.size'
5834
- # If size < 200 bytes on a non-trivial artifact, the write FAILED — re-do it.
5835
- \`\`\`
5836
-
5837
- ### Sharing agent-fs files with humans
5838
-
5839
- The deployment configures the live viewer host through \`AGENT_FS_LIVE_URL\`; the
5840
- documented fallback is \`https://live.agent-fs.dev\`. Resolve it in the shell and
5841
- combine it with the \`orgId\` and \`driveId\` returned by \`agent-fs stat\`:
5842
-
5843
- \`\`\`bash
5844
- FILE_PATH='thoughts/<agent-id>/research/<file>.md'
5845
- AGENT_FS_HOST=\${AGENT_FS_LIVE_URL:-https://live.agent-fs.dev}
5846
- FILE_STAT=$(agent-fs stat "$FILE_PATH" --json)
5847
- ORG_ID=$(printf '%s' "$FILE_STAT" | jq -r '.orgId')
5848
- DRIVE_ID=$(printf '%s' "$FILE_STAT" | jq -r '.driveId')
5849
- SHARE_URL="\${AGENT_FS_HOST%/}/file/~/$ORG_ID/$DRIVE_ID/$FILE_PATH"
5850
- printf '%s\\n' "$SHARE_URL"
5851
- \`\`\`
5852
-
5853
- Paste the printed concrete URL into Markdown or a message. Markdown does not
5854
- expand environment variables.
5855
-
5856
- ## Shared filesystem
5857
-
5858
- For non-text artifacts or files other agents need during the same session:
5859
-
5860
- - \`/workspace/shared/downloads/<agent-id>/\` — downloaded files
5861
- - \`/workspace/shared/misc/<agent-id>/\` — other shared files
5862
-
5863
- It does **not** survive container restarts. Anything that must outlive the
5864
- session goes to agent-fs.
5865
-
5866
- ## Binary artifacts (PNG, MP4)
5867
-
5868
- \`agent-fs write --content\` is text-only and mangles binaries. Upload binaries
5869
- with \`--file\` (or piped stdin), which uses the binary-safe raw route
5870
- (agent-fs CLI >= 0.7.1):
5871
-
5872
- \`\`\`bash
5873
- agent-fs write thoughts/<agent-id>/qa/<topic>-screenshots/login.png \\
5874
- --file /tmp/login.png -m "login page after fix"
5875
- agent-fs stat thoughts/<agent-id>/qa/<topic>-screenshots/login.png --json | jq '.size'
5876
- \`\`\`
5877
-
5878
- Capture with \`agent-browser screenshot <path>\` when it is installed, otherwise
5879
- Playwright, ffmpeg, or a system screenshot tool. The agent-fs copy is the
5880
- durable original. Where each kind of content goes:
5881
-
5882
- | Content | Use | Why |
5883
- |---|---|---|
5884
- | Image or GIF in GitHub | GitHub user attachment (\`github-attach\` skill) | Permanent, renders inline, short URL |
5885
- | Video (mp4) in GitHub | GitHub user attachment, else an \`agent-fs share-create\` link | GitHub plays it inline. A presigned link downloads it |
5886
- | Doc (plan, research, report) | \`agent-fs share-create <path> --expires-in 604800\` | Preview page, no download |
5887
- | Durable pointer | \`live.agent-fs.dev\` path, in a PR only under \`## Swarm provenance\` | Share links die after 7 days, this one does not (needs login) |
5888
- | Presigned \`signed-url\` | Image fallback only, with \`--inline\` | Downloads and expires after 7 days. Never for docs or mp4s |
5889
-
5890
- \`\`\`bash
5891
- agent-fs signed-url thoughts/<agent-id>/qa/<topic>-screenshots/login.png --inline --json # fallback only; 24h default, --expires-in up to 7d
5892
- \`\`\`
5893
-
5894
- ## Naming conventions
5895
-
5896
- Name paths predictably by task, date, and artifact type:
5897
-
5898
- \`\`\`
5899
- thoughts/<agent-id>/research/YYYY-MM-DD-<topic>.md
5900
- thoughts/<agent-id>/plans/YYYY-MM-DD-<topic>.md
5901
- thoughts/<agent-id>/qa/<topic>-screenshots/<filename>.png
5902
- misc/<agent-id>/<task-id>-<description>.ext
5903
- \`\`\`
5904
-
5905
- ## Attaching artifacts
5906
-
5907
- - **PR body** — embed images and mp4s as GitHub user attachments
5908
- (\`![caption](https://github.com/user-attachments/assets/<uuid>)\`), and link docs
5909
- with \`share-create\`. Never embed a \`share-create\` link as an image: it is an
5910
- HTML preview page. Swarm task links and durable agent-fs paths go only under
5911
- \`## Swarm provenance\`.
5912
- - **Slack** — link the agent-fs URL (team members only, the live viewer needs a login). Use a \`share-create\` link (up to 7 days) for anyone else.
5913
- - **\`store-progress\`** — use the \`attachments\` field with \`kind: "agent-fs"\` and the path.
5914
- - **Linear comments** — paste the live URL in the comment body.
5915
-
5916
- ## What NOT to store
5917
-
5918
- - Secrets, API keys, OAuth tokens
5919
- - Raw customer data without approval
5920
- - Oversized files without approval (check size before uploading)
5921
- - Ephemeral progress notes — those go in \`store-progress.progress\`
5922
-
5923
- ## Trade-offs
5924
-
5925
- **agent-fs vs shared filesystem** — agent-fs is persistent, versioned, and
5926
- searchable across sessions. The shared filesystem is faster for same-session
5927
- handoffs but doesn't survive container restarts. Use agent-fs for anything that
5928
- needs to outlive the current session or be reviewed by a human.
5929
-
5930
- ---
5931
-
5932
- # Part 2 — Serving interactive web content
5933
-
5934
- Serve a directory or a Hono app to a public, auth-protected URL via
5935
- localtunnel. Use this when a static page won't do: custom routes, form posts,
5936
- uploads, per-request computation.
5937
-
5938
- The CLI is a subcommand of \`agent-swarm\`. Always invoke it as
5939
- **\`agent-swarm artifact <subcommand>\`** — there is no top-level \`artifact\`
5940
- binary.
5941
-
5942
- ## Quick start
5943
-
5944
- ### Static content
5945
-
5946
- \`\`\`bash
5947
- # Create your content in a persisted directory
5948
- mkdir -p /workspace/personal/artifacts/my-report
5949
- echo '<h1>My Report</h1>' > /workspace/personal/artifacts/my-report/index.html
5950
-
5951
- # Serve it (auto-assigns a free port, creates tunnel, registers in service registry)
5952
- agent-swarm artifact serve /workspace/personal/artifacts/my-report --name my-report
5953
- # -> Copy the deployment URL printed by the command; it is authoritative.
5954
- \`\`\`
5955
-
5956
- ### Programmatic (custom Hono server)
5957
-
5958
- \`\`\`typescript
5959
- import { createArtifactServer } from '../artifact-sdk';
5960
- import { Hono } from 'hono';
5961
-
5962
- const app = new Hono();
5963
- app.get('/', (c) => c.html('<h1>Dashboard</h1>'));
5964
-
5965
- const server = createArtifactServer({ name: 'dashboard', app });
5966
- await server.start();
5967
- console.log(\`Live at: \${server.url}\`);
5968
- \`\`\`
5969
-
5970
- You can also run \`agent-swarm artifact serve ./server.ts --name dashboard\` if
5971
- \`server.ts\` exports a Hono instance as its default export.
5972
-
5973
- See the bundled \`examples/\` directory for complete working examples
5974
- (\`static-report.sh\`, \`hono-dashboard.ts\`, \`approval-flow.ts\`,
5975
- \`multi-artifact.ts\`).
5976
-
5977
- ## CLI commands
5978
-
5979
- | Command | Description |
5980
- |---|---|
5981
- | \`agent-swarm artifact serve <path> --name <name> [--port <port>] [--no-auth] [--subdomain <sub>]\` | Start serving content. \`<path>\` is a directory (static) or a \`.ts\`/\`.js\` file exporting a default Hono app. |
5982
- | \`agent-swarm artifact list\` | List active artifacts (name, agent, port, URL, status) from the service registry. |
5983
- | \`agent-swarm artifact stop <name>\` | Stop an artifact: deletes the matching PM2 process and unregisters it from the service registry. See "Known limitation" below. |
5984
-
5985
- Flags accepted by \`serve\`:
5986
-
5987
- - \`--name <name>\` — defaults to the basename of \`<path>\`. Used for the subdomain and PM2 process name.
5988
- - \`--port <port>\` — pin to a specific port. Default: auto-assigned ephemeral port.
5989
- - \`--no-auth\` — disable HTTP Basic auth on the tunnel (DANGEROUS — anyone with the URL can access).
5990
- - \`--subdomain <sub>\` — override the default \`\${agentId}-\${name}\` subdomain.
5991
-
5992
- ## Auth & URL pattern
5993
-
5994
- Tunnels are protected by **HTTP Basic auth** by default:
5995
-
5996
- - **Username:** \`hi\` (hardcoded MVP default in \`src/artifact-sdk/tunnel.ts\`)
5997
- - **Password:** the agent's \`API_KEY\`
5998
-
5999
- > **Never put the API key in the URL.** The Basic-auth password *is* the swarm
6000
- > \`API_KEY\` — it authenticates against the whole swarm API, not just this
6001
- > artifact. A \`https://user:key@host\` URL leaks that credential into shell
6002
- > history, browser history, proxy and tunnel access logs, referrer headers, and
6003
- > anything you paste it into. Treat a credential-bearing URL as a leaked key.
6004
-
6005
- The installed \`@desplega.ai/localtunnel\` package currently defaults to
6006
- \`lt.desplega.ai\`, but deployments may configure another tunnel host. The URL
6007
- printed by \`agent-swarm artifact serve\` (or exposed as \`server.url\`) is
6008
- authoritative. Save that plain URL as \`ARTIFACT_URL\`; a browser will prompt for
6009
- the credentials.
6010
-
6011
- For scripts and \`curl\`, pass the credential out-of-band instead of inlining it,
6012
- and read it from the environment so it never appears as a literal:
6013
-
6014
- \`\`\`bash
6015
- # -u keeps the credential out of the URL; --netrc-file keeps it out of argv too.
6016
- curl -u "hi:$API_KEY" "$ARTIFACT_URL"
6017
-
6018
- # Better for anything long-lived or logged — argv is visible to other processes:
6019
- ARTIFACT_HOST=\${ARTIFACT_URL#*://}
6020
- ARTIFACT_HOST=\${ARTIFACT_HOST%%/*}
6021
- printf 'machine %s login hi password %s\\n' \\
6022
- "$ARTIFACT_HOST" "$API_KEY" > /tmp/artifact-netrc
6023
- chmod 600 /tmp/artifact-netrc
6024
- curl --netrc-file /tmp/artifact-netrc "$ARTIFACT_URL"
6025
- \`\`\`
6026
-
6027
- Do not echo the assembled command, and do not paste the credential into a task
6028
- report, Slack message, PR body, or page — those are all persisted.
6029
-
6030
- Use \`--no-auth\` only for genuinely public content. Anyone who learns the
6031
- subdomain can read it.
6032
-
6033
- ## Running it as a daemon
6034
-
6035
- \`agent-swarm artifact serve\` blocks on a never-resolving promise to stay alive —
6036
- you cannot inline it in a script that needs to do other work. Pick one:
6037
-
6038
- ### Option A — \`nohup\` (quick, throwaway)
6039
-
6040
- \`\`\`bash
6041
- mkdir -p /workspace/personal/logs
6042
- nohup agent-swarm artifact serve /workspace/personal/artifacts/my-report \\
6043
- --name my-report \\
6044
- > /workspace/personal/logs/my-report.out 2>&1 &
6045
- echo $! > /workspace/personal/logs/my-report.pid
6046
-
6047
- # Later, kill it manually:
6048
- kill "$(cat /workspace/personal/logs/my-report.pid)"
6049
- \`\`\`
6050
-
6051
- ### Option B — PM2 (recommended for anything you'll come back to)
6052
-
6053
- PM2 gives you auto-restart on crash, a process name, log management, and —
6054
- crucially — lets \`agent-swarm artifact stop <name>\` actually kill it.
6055
-
6056
- \`\`\`bash
6057
- pm2 start agent-swarm \\
6058
- --name artifact-my-report \\
6059
- -- artifact serve /workspace/personal/artifacts/my-report --name my-report
6060
-
6061
- # Stop it cleanly later:
6062
- agent-swarm artifact stop my-report
6063
- \`\`\`
6064
-
6065
- The PM2 process name **must** be \`artifact-<name>\` (matching \`--name\`) — that's
6066
- exactly what \`artifact stop\` looks for.
6067
-
6068
- ### Known limitation — \`artifact stop\` only kills PM2-started processes
6069
-
6070
- \`agent-swarm artifact stop <name>\` runs \`pm2 delete artifact-<name>\` and then
6071
- unregisters the entry from the service registry. If you started the artifact
6072
- with \`nohup\` (or \`&\`, or any non-PM2 launcher), \`pm2 delete\` silently fails and
6073
- the server keeps running and serving — even though the command prints
6074
- \`Artifact '<name>' stopped.\` Until that's fixed:
6075
-
6076
- - Use **PM2** if you want \`artifact stop\` to do its job.
6077
- - For \`nohup\`/foreground processes, kill the PID yourself
6078
- (\`kill <pid>\` or \`pkill -f 'artifact serve.*<name>'\`) **and then** run
6079
- \`agent-swarm artifact stop <name>\` to clear the registry row.
6080
-
6081
- ## Multiple artifacts
6082
-
6083
- Each artifact gets its own port (auto-assigned) and subdomain
6084
- (\`<agentId>-<name>\`). You can run several simultaneously — see
6085
- \`examples/multi-artifact.ts\`.
6086
-
6087
- ## Browser SDK
6088
-
6089
- Served HTML gets the same injected browser SDK that pages get. Load it and use
6090
- the ready-made singleton:
6091
-
6092
- \`\`\`html
6093
- <script src="/@swarm/sdk.js"></script>
6094
- <script>
6095
- // \`window.swarmSdk\` is constructed for you — no \`new SwarmSDK()\` needed.
6096
- const tasks = await window.swarmSdk.tasks.list({ status: 'in_progress' });
6097
- const agents = await window.swarmSdk.agents.list();
6098
- </script>
6099
- \`\`\`
6100
-
6101
- The SDK is **domain-grouped** (\`swarmSdk.tasks\`, \`.agents\`, \`.events\`,
6102
- \`.memory\`, \`.repos\`, \`.schedules\`, \`.approvalRequests\`, \`.assets\`), each domain
6103
- mapping 1:1 onto the public REST API. The **\`pages\`** skill documents the full
6104
- method surface — it's the same object, so don't duplicate it here.
6105
-
6106
- Calls route through the \`/@swarm/api/*\` proxy, which injects auth server-side.
6107
- Browser code never sees the API key.
6108
-
6109
- ## Storage
6110
-
6111
- Always store served content in persisted directories — the working dir is wiped
6112
- between sessions:
6113
-
6114
- - \`/workspace/personal/artifacts/\` — per-agent, persists across sessions (default)
6115
- - \`/workspace/shared/artifacts/\` — shared across the swarm
6116
-
6117
- ## See also
6118
-
6119
- - The **\`pages\`** skill — DB-backed static HTML/JSON, no process to run. Prefer
6120
- it whenever the content is a snapshot.
6121
- - The **\`kv-storage\`** skill — for the small state a served page needs.
6122
- `;
5766
+ var content_default2 = "# Swarm Apps\n\nSwarm Apps are persistent, agent-authored internal applications: a schema-backed row store, named queries, custom script/task actions, and a validated json-render interface served at `/apps/<id>`. Use an app when people need to view and change live records; use `create_page` for a static snapshot.\n\n> **Capability gate**: app tools are available when the server's `CAPABILITIES` includes `pages` (for example `CAPABILITIES=core,task-pool,pages`). There is no separate `apps` capability. If these tools are absent from your MCP list, this is why.\n\n## Tools and iteration loop\n\n| Tool | Use it for |\n|---|---|\n| `app-list` | Discover apps by id, name, description, and timestamps. Definitions are omitted. |\n| `app-get` | Read one complete stored definition before changing it. |\n| `app-upsert` | Create an app, or replace an existing app's entire definition when you intentionally have the full desired state. |\n| `app-patch` | Make a focused change while preserving everything not mentioned in the patch. Prefer this for iteration. |\n| `app-query` | Run one declared named query and return its rows. Use this when an agent or saved script needs to consume app data rather than render the app UI. |\n| `app-history` | List definition snapshots and their compact model/column digests before choosing a restore point. |\n| `app-diff` | Show a unified definition diff; defaults to newest snapshot → CURRENT. |\n| `app-rollback` | Restore a snapshot through the normal schema-migration engine. |\n\nFor every edit: `app-get` -> modify the smallest coherent subtree -> `app-patch` -> if rejected, fix every returned `issues[]` entry (`path` + `message`) and retry. Validation happens before storage, so a rejected update leaves the saved app unchanged. Do not guess at the current definition or use `app-upsert` with a partial definition.\n\n`app-patch` can also change `name`; omit `description` to preserve it or pass `description: null` to clear it.\n\n### Reading an app's callable surface\n\nOne `app-get` returns everything needed to drive an app you did not write: `definition.queries` are runnable with `app-query` (supply every `$param` the query references), and `definition.actions` are invocable with `POST /api/apps/<id>/actions/<name>` (a `script` action runs its script, a `task` action spawns a task, a `sync` action refreshes sources). Read a query's `$param` placeholders and an action's kind/args from the definition before calling — do not guess parameter names.\n\n## Safe schema evolution and rollback\n\nPrefer **hiding** a column over deleting it. A hidden column's values stay on every existing row, so an unhide or rollback can make it visible again. Hiding is metadata-only: hidden columns cannot be used in queries, page bindings, `app.mutate` values, or new row writes, and `required` is ignored while hidden. Row GETs still expose the historical value for backward compatibility.\n\nDo not reuse a hidden column name. The name remains held until you either unhide it or explicitly purge its stored values. To permanently remove a column, delete its declaration with `null` and include `{ \"columnName\": { \"purge\": true } }` in the same `migration` object. That is deliberately one-shot and irreversible for row data.\n\nSchema-changing writes can carry this sibling `migration` object:\n\n```json\n{\n \"migration\": {\n \"priority\": { \"from\": \"flag\", \"map\": { \"urgent\": \"high\", \"watch\": \"low\" }, \"else\": \"none\" },\n \"status\": { \"from\": \"flag\", \"map\": { \"watch\": \"watching\", \"done\": \"done\" }, \"else\": \"open\" },\n \"oldColumn\": { \"purge\": true },\n \"count\": { \"coerce\": true, \"else\": 0 },\n \"owner\": { \"set\": \"unassigned\" }\n }\n}\n```\n\nUse `set` to backfill a changed column, `from` with optional `map`/`else` to derive it from one existing (including hidden) column, `coerce` for a kind change, and `purge` only for explicit destruction. Non-purge directives target a column changed in that same write. The server fails loudly before writing when a lossy change needs a directive: inspect every returned `issues[]` item, including its per-value row counts, then choose an explicit policy and retry. Never guess a migration just to get a 200.\n\n**Source lifecycle** rides the same engine with the same all-or-nothing guarantees. Free: adding a source, adding a NEW column with a `source` binding, binding an existing column that holds zero values, changing a binding's `field`/`transform`, and changing a source's `args`/`config`/`connection`/`scriptId` (window changes churn staleness on the next pass — expect `stale` flips when narrowing). Rejected with a path-bearing issue: changing `joinKey` (immutable — remove the source and add it again), changing `connector` while the source owns rows (row count in the message), and binding an existing column that already holds values (purge it — or add a fresh column — and bind that instead; a sync pass would otherwise overwrite the data). Removing a source **detaches** its rows: values survive as ordinary columns, the `source`/`syncedAt`/`stale` envelope is stripped, and the report counts them in `detachedRows`. A patch removing a source must drop its column bindings in the same call — a dangling `source.of` is rejected before anything is written.\n\nSync never adopts rows it does not own: a pulled record whose join key matches an unowned row creates a second, source-owned row. Adding a source over a join-key column that already holds values therefore duplicates those records on the first pass — and remove-source → re-add duplicates for the same reason, because detached rows keep their values. To convert a hand-maintained model into a synced one, purge the old rows first or use a fresh join-key column.\n\nFor example, split a legacy `flag` into `priority` and `status` without losing data in one patch: add the two columns, map both from `flag`, update every affected query/page binding, then hide `flag`.\n\n```json\n{\n \"appId\": \"<app-id>\",\n \"definition\": {\n \"models\": {\n \"ticket\": {\n \"columns\": {\n \"priority\": { \"kind\": \"enum\", \"enum\": [\"none\", \"low\", \"high\"] },\n \"status\": { \"kind\": \"enum\", \"enum\": [\"open\", \"watching\", \"done\"] },\n \"flag\": { \"kind\": \"string\", \"hidden\": true }\n }\n }\n }\n },\n \"migration\": {\n \"priority\": { \"from\": \"flag\", \"map\": { \"urgent\": \"high\", \"watch\": \"low\" }, \"else\": \"none\" },\n \"status\": { \"from\": \"flag\", \"map\": { \"watch\": \"watching\", \"done\": \"done\" }, \"else\": \"open\" }\n }\n}\n```\n\nEvery schema migration returns `{ scanned, backfilled, coerced, mapped, elsed, purgedValues, idxRebuilt, detachedRows, orphanFields }`. Read `orphanFields`: it reports extra row fields that predate or no longer appear in the definition; they are preserved until an explicit purge. `schemaVersion` is server-managed: never set it in an input patch or try to use it as application data.\n\nEvery successful definition write snapshots the previous definition. Use `app-history` to select a version, `app-diff({ appId, from: <version> })` to inspect it against CURRENT, then `app-rollback({ appId, version: <version>, migration? })` to restore it. Rollback is a forward migration over live rows, not row-level time travel: it creates a new pre-rollback snapshot and may reject a lossy restore with the same migration directives and counts. A rejected rollback changes nothing.\n\n### Per-user configuration\n\n`userConfig` declares up to 20 typed settings in the versioned definition; its values are deliberately stored outside the definition, separately for each user (and the operator dashboard). Each field is `{ \"kind\": \"string\" | \"number\" | \"boolean\" | \"date\" | \"enum\", \"default\"?: ..., \"enum\"?: [...], \"label\"?: \"...\" }`. Defaults follow column rules, including enum membership; `required` is not allowed. A missing or obsolete stored value reads as its default, or `null` when no default is declared.\n\nUse `{ \"$state\": \"/user/<field>\" }` on a page for the read-only current-user binding; the field must be declared and the path cannot continue below it. Pure and bound reusable elements cannot bind `/user` directly — pass the value through an element prop from the consuming page. An agent acting on an owned user-requested task reads and edits that requester's preferences. `userConfig` schema changes are always migration-compatible and appear in migration reports; no migration directives are needed. Rollback restores the historical schema but never alters saved per-user values, so values can resurface when a field returns.\n\n### Theming\n\nAn optional top-level `\"theme\"` sets the preset the dashboard applies to the app's rendered canvas — a lowercase slug validated by shape only. The preset catalog ships with the dashboard (`apps/ui/src/lib/themes.ts` + `theme-classics.ts` — update this list and the `app-upsert` tool description when it changes); current ids: `hive` (the stock look), the accent/field presets `meadow`, `iris`, `rose`, `cobalt`, `ember`, `carbon`, and the classic editor/platform presets `github`, `vscode`, `material`, `solarized`, `tokyo`, `monokai`, `gruvbox`, `catppuccin`. Action and destructive tones are never themed; status tones are themed ONLY by the classic presets (their identity includes a status palette — hue semantics stay fixed, success reads green and error reads red everywhere). An id the viewer's dashboard build does not know degrades to the dashboard's own theme, so unknown slugs are safe but pointless. Omit `theme` to inherit whatever theme the viewer runs; `\"hive\"` is an explicit reset to the stock look even inside a themed dashboard.\n\nViewers can override the preset per app from the app's settings drawer; the override rides the reserved `$theme` key of the user-config values (accepted on every app, even without a declared `userConfig` schema, and never visible at `/user/...`). A present override always beats the definition's `theme` — even an unrecognized one, which degrades to the dashboard theme — so treat `theme` as the app's default styling, not a guarantee.\n\nDistinguish the two rollback 400s. A lossy-migration 400 is fixable: copy the required `migration` entries from the message and retry. A target-snapshot validation 400 explicitly says that directives cannot repair that historical definition; use `app-history` and choose a different version.\n\nWhen a schema patch reports stale page/query bindings, repair them in that same patch. Page elements are atomic: replace the complete `pages.<page>.elements.<id>` declaration, removing any reference to a hidden or deleted column, instead of sending only the changed nested prop. This is also the repair move for older apps whose stored page already references an undeclared column; full-definition validation otherwise blocks every patch.\n\n## Definition reference\n\n```json\n{\n \"models\": {\n \"modelName\": {\n \"columns\": {\n \"externalId\": { \"kind\": \"string\" },\n \"title\": { \"kind\": \"string\" }\n }\n }\n },\n \"queries\": { \"queryName\": { \"model\": \"modelName\" } },\n \"actions\": { \"actionName\": { \"kind\": \"task\", \"prompt\": \"Review the current rows and report\" } },\n \"pages\": {\n \"main\": {\n \"root\": \"root\",\n \"elements\": { \"root\": { \"type\": \"Container\", \"props\": {} } }\n }\n },\n \"defaultPage\": \"main\",\n \"theme\": \"ember\"\n}\n```\n\nModel, reusable-element, element-prop, query, action, column, page, and page-param names start with a lowercase letter, contain only letters, numbers, or underscores, and are at most 40 characters. A definition has 0-10 models and at most 20 reusable elements; each declared model has 1-40 columns. Zero-model apps are valid for pure UI utilities as long as their pages and reusable elements validate.\n\n### Reusable elements\n\nTop-level `elements` are versioned inside the app definition and can be reused from the app's own pages or, when explicitly exported, from another app. They are private by default. Each entry declares a `mode`, optional typed `props`, one `root`, and a flat `elements` node map using the same validated tree vocabulary as a page:\n\n```json\n{\n \"elements\": {\n \"statCard\": {\n \"mode\": \"pure\",\n \"export\": true,\n \"props\": {\n \"label\": { \"kind\": \"string\", \"required\": true },\n \"value\": { \"kind\": \"number\", \"default\": 0 }\n },\n \"root\": \"card\",\n \"elements\": {\n \"card\": { \"type\": \"Card\", \"props\": {}, \"children\": [\"label\", \"slot\"] },\n \"label\": { \"type\": \"Metric\", \"props\": { \"label\": { \"$state\": \"/props/label\" }, \"value\": { \"$state\": \"/props/value\" } } },\n \"slot\": { \"type\": \"ElementSlot\", \"props\": {} }\n }\n }\n }\n}\n```\n\n- `pure` elements may read only their declared `/props/<name>` state; `$item` and `$index` are also legal inside repeated nodes. They cannot invoke any action step. A pure tree may contain at most one leaf `ElementSlot` where consumer children are inserted.\n- `bound` elements may additionally read the defining app's declared queries and actions and use its models. Those references are validated against the defining app, including hidden-column rules. Exported bound elements cannot use `app.navigate`; private bound elements may navigate within their defining app.\n- Prop kinds are `string`, `number`, `boolean`, `date`, or `enum`. An enum prop requires a non-empty `enum: [\"value\", ...]` values array. A literal default and each literal consumer value must match the kind and, for enums, be one of those values. A required prop without a default must be supplied.\n\nReference an element with an `ElementRef` node. Omit `app` for same-app reuse; cross-app references name the defining app id and require `export: true`. `instanceKey` is optional. Consumer `children` are accepted only when the target has an `ElementSlot`:\n\n```json\n{\n \"type\": \"ElementRef\",\n \"props\": {\n \"app\": \"<defining-app-id>\",\n \"element\": \"statCard\",\n \"props\": { \"label\": \"Open issues\", \"value\": 12 },\n \"instanceKey\": \"openIssues\"\n },\n \"children\": [\"details\"]\n}\n```\n\nElement references float to the defining app's current definition. `app` and `element` must be literal strings; dynamic references are not supported. The server rejects reference cycles and expansion deeper than five, missing/private targets, invalid props, and a breaking write to an exported element that other apps reference. Removing or unexporting an exported element, changing its mode, removing a declared prop, changing a prop kind, or adding a required prop without a default is breaking; publish the new contract under a new element name. If consumers are abandoned intentionally, retry the PUT, PATCH, or rollback with `\"forceElementBreak\": [\"elementName\"]`. The named consumers will then render an error once element rendering is available; deleting an entire app is deliberately not compatibility-gated.\n\n### Models\n\nEach column is `{ \"kind\": ..., \"required\"?: boolean, \"default\"?: ..., \"index\"?: boolean, \"enum\"?: string[], \"hidden\"?: boolean }`.\n\n| `kind` | Values and indexing |\n|---|---|\n| `string` | String value; set `index: true` only when equality lookups need it. |\n| `number` | Finite number; never indexed. |\n| `boolean` | Boolean; set `index: true` only when equality lookups need it. |\n| `date` | ISO-8601 string; never indexed. |\n| `enum` | String from the non-empty unique `enum` list; always indexed, without `index: true`. |\n\n`default` must match the column kind (and be one of the enum values). `required: true` rejects missing/null values unless a default supplies the value. `id`, `createdAt`, `updatedAt`, `createdBy`, `updatedBy` — and the sync envelope `source`, `syncedAt`, `stale` — are system columns and reserved names. An app created before the sync envelope that declared ordinary columns with these names surfaces `definitionError` after upgrade until they are renamed: patch in a fresh column with a `{from}` migration directive, then purge the old one. Rows always expose `id`/`createdAt`/`updatedAt`; `createdBy`/`updatedBy` record the acting principal (`user:<id>`, `agent:<id>`, `operator`, or `sync:<source>` for engine writes) and are system-managed on every row write.\n\n### Sources and sync\n\nA model may declare up to 4 named `sources` — one-way inbound projections of an external system into its rows. The `script` connector is THE way to bring external data in (GitHub, Linear, any API): a saved script owns the fetch, its auth placeholders, and its paging. `swarm-tasks` is the one native connector, projecting this swarm's own task pool (`config`: `status`/`tags` comma-lists, `agentId`, `assetKey` namespace prefix, `limit` up to 200 — default 100, `includeHeartbeat`). Synced rows are readable by EVERY principal with `app.use` on the app; an unscoped `swarm-tasks` source publishes task prompts (truncated, secret-scrubbed) to all of them, so scope it with `config.assetKey`/`config.agentId` unless that exposure is intended. A user-invoked sync only ever pulls the invoking user's own requested tasks. Script sources run with their run-as identity's credentials — an agent-owned script runs as its owner and only the owner may wire it; an owner-less catalog (global) script runs as the LEAD, so only the lead or the operator can wire or alter such a source (stored references stay valid with their args and connection pinned).\n\n```json\n{\n \"models\": {\n \"issue\": {\n \"columns\": {\n \"issueKey\": { \"kind\": \"string\" },\n \"title\": { \"kind\": \"string\", \"source\": { \"of\": \"gh\", \"field\": \"title\" } },\n \"openedAt\": { \"kind\": \"date\", \"source\": { \"of\": \"gh\", \"field\": \"created_at\", \"transform\": \"date-parse\" } },\n \"note\": { \"kind\": \"string\" }\n },\n \"sources\": {\n \"gh\": { \"connector\": \"script\", \"scriptId\": \"<script-uuid>\", \"joinKey\": \"issueKey\", \"args\": { \"repo\": \"owner/name\" }, \"connection\": \"github\" }\n }\n }\n }\n}\n```\n\n- **Join key**: each source names a `joinKey` — an owned, non-hidden `string` column carrying the external identity. It must not be `required`, must not declare a `default`, and cannot itself be source-bound. A row belongs to at most ONE source (`row.source`).\n- **Column bindings**: `{ \"of\": \"<sourceName>\", \"field\": \"user.login\", \"transform\"? }` projects a (dotted-path) field of the pulled record into the column on every pass. Transforms and their required column kinds: `slug`/`lower`/`upper` → `string`, `cents` → `number` (×100, rounded), `date-parse` → `date` (ISO). A failed transform nulls that one column and warns; it never fails the pass.\n- **Read-only contract**: source-bound columns and the join key reject every direct write (row create/patch/bulk) with a path-bearing issue — mutate them via the source or a sync refresh. Owned columns on the same model stay writable. Every `required` owned column on a sourced model must declare a `default`, because sync-created rows cannot supply it.\n- **Provenance**: synced rows carry `source` (owner), `syncedAt` (last confirmed presence — sortable), and `stale` (true when a complete pull no longer returned the record; clears on reappearance). `syncedAt` advances on every confirmed row WITHOUT touching `updatedAt`; `updatedAt` moves only when projected data actually changed.\n- **`connection`** (optional, script sources): names a connections-system slug the source depends on. It buys write-time validation plus a preflight before every pull; it does not by itself gate egress. Credentials resolve for the sync run-as identity — the script's owner when it has one, else the lead agent — so global connections always work and agent-scoped ones need matching script ownership. The slug is injected into the script's args as `connection`, letting one generic script serve N connections.\n\n**The canonical sync-script contract** (every sync script, always):\n\n```ts\nexport default async function run(args, ctx) {\n // args = { ...source.args, app: { id }, model: \"<model>\", source: \"<sourceName>\", connection?: \"<slug>\" }\n const records = [{ key: \"42\", fields: { title: \"…\", created_at: \"…\" } }];\n return { records, complete: true }; // or return the bare array — a bare array IS a complete snapshot\n}\n```\n\nReturn `Array<{ key, fields }>` or `{ records, complete? }`. `key` is the external identity the engine joins on `joinKey` (numbers are coerced to strings); `fields` is the bag bindings project from. Return `complete: false` whenever the pull window may have missed records — a paged fetch, a hit `limit`, or client-side filtering of a full page. An incomplete pull skips the stale sweep, so rows outside a narrow window are not falsely marked stale; compute `complete` BEFORE any client-side filtering. Hard cap 500 records per pull (over-cap pulls truncate and count as incomplete). An invalid return shape, thrown error, or non-zero exit fails the pass with zero row churn.\n\nThe seeded catalog script `github-issues-pull` is the worked example (placeholder auth via `[REDACTED:GITHUB_TOKEN]`, PR filtering, `complete` computed before filtering). For scheduled refreshes, point a `targetType: \"script\"` schedule at the seeded `app-sync-run` with `scriptArgs: { \"appId\": \"<id>\", \"model\"?, \"source\"? }`.\n\n**Three refresh doors, one engine**: `POST /api/apps/<id>/sync` (body `{ \"model\"?, \"source\"? }`), a `sync` action kind, and the `app-sync` MCP tool (`ctx.swarm.app_sync` from scripts). `{model?, source?}` fans out to every matching (model × source) pair; each pass reports `{ pulled, created, updated, refreshed, unchanged, markedStale, warnings }`. A pair already syncing in this API process short-circuits with `alreadyRunning: true` instead of pulling twice. Each pair's last-pass status (`lastFinishedAt`, counts, `error?`) comes back as `syncStatus` — keyed `\"<model>:<source>\"` — on `GET /api/apps/<id>` and in `app-get` data, so UI and agents can render \"last synced / last error\" without extra calls.\n\n### Freshness and the refresh button\n\nFreshness renders with the existing column kinds — no special components. Give the table `{ \"key\": \"syncedAt\", \"kind\": \"date\" }` and `{ \"key\": \"stale\", \"kind\": \"badge\", \"tones\": { \"true\": \"warning\" } }` columns, declare a `sync` action, and wire a Button to it:\n\n```json\n{\n \"actions\": { \"refresh\": { \"kind\": \"sync\" } },\n \"pages\": {\n \"main\": {\n \"root\": \"root\",\n \"elements\": {\n \"root\": { \"type\": \"Container\", \"props\": {}, \"children\": [\"refreshBtn\", \"issuesTable\"] },\n \"refreshBtn\": {\n \"type\": \"Button\",\n \"props\": { \"label\": \"Refresh\" },\n \"on\": { \"press\": [{ \"action\": \"app.action\", \"params\": { \"name\": \"refresh\" } }] }\n },\n \"issuesTable\": {\n \"type\": \"Table\",\n \"props\": {\n \"data\": { \"$state\": \"/queries/allIssues/data\" },\n \"columns\": [\n { \"key\": \"title\", \"label\": \"Issue\" },\n { \"key\": \"syncedAt\", \"label\": \"Synced\", \"kind\": \"date\" },\n { \"key\": \"stale\", \"label\": \"Stale\", \"kind\": \"badge\", \"tones\": { \"true\": \"warning\" } }\n ]\n }\n }\n }\n }\n }\n}\n```\n\nA `sync` action answers the script-action state shape (`/actions/refresh` → `running` → `ok`/`error`, result under `result.passes`), so the page's queries refetch when it completes. Use `app-query` to READ rows (named queries may filter on `stale`/`source` and sort by `syncedAt`); use `app-sync` to REFRESH sources; never poll a sync door in a loop — schedule it or give people the button.\n\n### Queries\n\nA named query is:\n\n```json\n{\n \"model\": \"idea\",\n \"filter\": { \"status\": \"open\" },\n \"sort\": { \"column\": \"createdAt\", \"dir\": \"desc\" },\n \"limit\": 100\n}\n```\n\n`filter` is a strict AND of equality checks. A filter may target a declared model column or a system column (`id`, `createdAt`, `updatedAt`, `createdBy`, `updatedBy`, and on synced models `source`, `syncedAt`, `stale`) — filtering on `id` is the universal way to select one row for a detail view, and `{ \"stale\": true }` selects rows a complete pull stopped confirming. A literal value must match the column's kind; date filters compare the stored raw ISO string, not parsed instants. Omit `limit` for the default 200 rows, or set an integer from 1 through 1000. `sort.column` is a model column, `createdAt`, `updatedAt`, or `syncedAt`; `dir` is `asc` or `desc`. Query runtime state is `{ data, loading, error }` under `/queries/<queryName>`.\n\nA filter value may instead be exactly `{ \"$param\": \"<name>\" }`. At execution, the caller supplies that name and the server coerces its value to the filtered column's kind before equality matching:\n\n```json\n{\n \"queries\": {\n \"issueDetail\": { \"model\": \"issue\", \"filter\": { \"issueId\": { \"$param\": \"issueId\" } }, \"limit\": 1 }\n }\n}\n```\n\nThe placeholder object must contain only `$param`. Missing caller values fail loudly rather than returning unfiltered rows: HTTP reports 400 and `app-query` returns a tool error listing every missing name. Supplying a param name the query does not reference is also an error. The app UI supplies current declared route params automatically. Direct calls use `app-query({ appId, query: \"issueDetail\", params: { issueId: 42 } })` or `ctx.swarm.app_query({ appId, query: \"issueDetail\", params: { issueId: 42 } })`.\n\n### Custom actions\n\nActions are optional (maximum 20) and are invoked from the page with the `app.action` runtime action.\n\n```json\n{\n \"actions\": {\n \"recalculate\": {\n \"kind\": \"script\",\n \"scriptId\": \"00000000-0000-4000-8000-000000000000\",\n \"args\": { \"mode\": \"fast\" }\n },\n \"triage\": {\n \"kind\": \"task\",\n \"prompt\": \"Triage this idea and report a recommendation\"\n }\n }\n}\n```\n\n| Kind | Contract |\n|---|---|\n| `script` | `scriptId` must identify an existing script; optional `args` are defaults. Invocation input overrides same-named defaults, and the runtime also supplies `app: { id }`. |\n| `task` | `prompt` must be non-empty; omit `agentId` to use default (lead) assignment. Only set `agentId` to a real agent UUID. Invocation `input` is included as context for the task prompt. Spawned tasks carry asset key `shared/app:<appId>/action:<name>/`, so a `swarm-tasks` source with `config.assetKey: \"shared/app:<appId>/\"` pulls back the tasks this app spawned. |\n| `sync` | Optional `model` and `source` narrow which (model × source) pairs refresh; omitting both syncs every pair the app declares. At least one pair must exist. Answers the script-kind state shape with per-pass results under `result.passes`. |\n\n- Invocation state lands at `/actions/<name>` as `{ status, result?, error?, taskId?, taskStatus? }`, where `status` is `\"running\"`, `\"ok\"`, or `\"error\"`.\n\nUse a task action named Tackle when a person should hand one issue row to the swarm. Pass the complete current row under `input.issue`; `{ \"$row\": \"\" }` resolves to the whole row and that context lands in the task prompt.\n\n```json\n{\n \"actions\": {\n \"tackle\": { \"kind\": \"task\", \"prompt\": \"Tackle this issue. Inspect the supplied issue context, do the work, and report the outcome.\" }\n },\n \"pages\": {\n \"main\": {\n \"root\": \"issuesTable\",\n \"elements\": {\n \"issuesTable\": {\n \"type\": \"Table\",\n \"props\": {\n \"data\": { \"$state\": \"/queries/allIssues/data\" },\n \"columns\": [{ \"key\": \"title\", \"label\": \"Issue\" }],\n \"rowActions\": [{\n \"label\": \"Tackle\",\n \"actions\": [{\n \"action\": \"app.action\",\n \"params\": { \"name\": \"tackle\", \"input\": { \"issue\": { \"$row\": \"\" } } }\n }]\n }]\n }\n }\n }\n }\n },\n \"defaultPage\": \"main\"\n}\n```\n\nFor direct MCP calls, `app-query` accepts `{ appId, query, params? }` and returns rows from that declared named query; every supplied param must be referenced by a `$param` filter in that query. Use it when an agent needs to read app state without scraping the UI.\n\nSaved scripts use the generated SDK name `ctx.swarm.app_query({ appId, query, params? })`. Use `app_query` in scripts or workflows that turn current app rows into reports, digests, or follow-up work. Saved scripts also get generated per-app types automatically: each app contributes an `App_<Name>` namespace with one row interface per model (enum columns as literal unions) and a typed `app_query` overload per named query, all visible via `script-query-types`. Actions stay REST-only from scripts — there is no SDK method to invoke them.\n\n### Pages, routes, and page trees\n\n`pages` is a map of independently validated page trees and `defaultPage` must name one entry. Each page may also declare a display `title` and typed route `params`. On every non-default page the runtime automatically renders breadcrumbs (`<default page title> › <current page title>`, first crumb navigates back) — including in chromeless embeds — so a detail page does NOT need its own back button; give pages good `title`s instead. This complete pattern connects a table row to a typed detail route and its parameterized query:\n\n```json\n{\n \"models\": {\n \"issue\": {\n \"columns\": {\n \"issueId\": { \"kind\": \"number\" },\n \"title\": { \"kind\": \"string\" },\n \"status\": { \"kind\": \"string\" }\n }\n }\n },\n \"queries\": {\n \"allIssues\": { \"model\": \"issue\" },\n \"issueDetail\": {\n \"model\": \"issue\",\n \"filter\": { \"issueId\": { \"$param\": \"issueId\" } },\n \"limit\": 1\n }\n },\n \"pages\": {\n \"issues\": {\n \"title\": \"Issues\",\n \"root\": \"issuesTable\",\n \"elements\": {\n \"issuesTable\": {\n \"type\": \"Table\",\n \"props\": {\n \"data\": { \"$state\": \"/queries/allIssues/data\" },\n \"columns\": [\n { \"key\": \"title\", \"label\": \"Issue\" },\n { \"key\": \"status\", \"label\": \"Status\", \"kind\": \"badge\" }\n ],\n \"rowActions\": [{\n \"label\": \"Open\",\n \"actions\": [{\n \"action\": \"app.navigate\",\n \"params\": {\n \"page\": \"detail\",\n \"params\": { \"issueId\": { \"$row\": \"issueId\" } }\n }\n }]\n }]\n }\n }\n }\n },\n \"detail\": {\n \"title\": \"Issue detail\",\n \"params\": { \"issueId\": { \"kind\": \"number\", \"required\": true } },\n \"root\": \"detail\",\n \"elements\": {\n \"detail\": {\n \"type\": \"DetailList\",\n \"props\": {\n \"data\": { \"$state\": \"/queries/issueDetail/data/0\" },\n \"fields\": [{ \"key\": \"title\", \"label\": \"Issue\" }]\n }\n }\n }\n }\n },\n \"defaultPage\": \"issues\"\n}\n```\n\nDrawer variant: declare a route param on the containing page, set the Drawer `props.param` to that literal param name, and use the same `app.navigate` shape targeting that page with the row id assigned to the Drawer's param. The Drawer then opens from route state without a separate action contract.\n\nParam `kind` is `string` by default, or `number` / `boolean`; `required` defaults to false. The names `mode`, `apiUrl`, `apiKey`, `email`, and `name` are reserved. `/apps/<id>` renders `defaultPage`; `/apps/<id>/p/<page>?issueId=42` is the shareable deep link for another page. Browser navigation is client-side, so app state and polled data stay warm across page changes.\n\nWithin each page, `root` names one entry in its non-empty `elements` map. Elements are a flat, single-parent tree: ids are map keys, `children` contains ids (not nested elements), all elements must be reachable from `root`, and cycles, missing children, or shared children are invalid. Element and Form/UI ids are page-local. Element keys are `type`, `props`, `children`, `on`, `visible`, `repeat`, and `watch`; only components with a `default` child slot accept `children` (Stack, Grid, Split, Tabs, Container, Card, Section, and Drawer). `watch` maps state paths to an action step or chain.\n\n## Component catalog (all 33)\n\nProps reject unknown keys. A `{\"$state\":\"...\"}` binding may replace a literal prop value at any depth.\n\nIcons: every `icon` prop (and the `Icon` block) takes one name from a curated lucide set: `plus`, `refresh-cw`, `search`, `filter`, `download`, `upload`, `external-link`, `copy`, `check`, `x`, `trash-2`, `pencil`, `eye`, `calendar`, `clock`, `user`, `users`, `bot`, `git-pull-request`, `git-branch`, `github`, `slack`, `mail`, `link`, `file-text`, `folder`, `database`, `zap`, `sparkles`, `flag`, `star`, `tag`, `alert-triangle`, `info`, `circle-check`, `trending-up`, `trending-down`, `dollar-sign`, `activity`, `rocket`. Any other name is rejected.\n\nTones: Badge, Metric and Progress `tone` take `neutral`, `success`, `active`, `error`, `info`, `pending`, `warning`, or `paused`.\n\n| Component | Required props | Optional props / values |\n|---|---|---|\n| `Stack` | none | Primary layout; `direction: \"column\", \"row\"`, `gap`, `padding: \"none\", \"xs\", \"sm\", \"md\", \"lg\", \"xl\"`; `align: \"start\", \"center\", \"end\", \"stretch\"`, `justify: \"start\", \"center\", \"end\", \"between\"`, `wrap`; `motion: \"rise\"` (default), `\"fade\"`, or `\"none\"` (see Motion below); has the `default` child slot. |\n| `Grid` | none | Responsive `columns`: integer 1 through 6, or `{ base, sm, md, lg }` counts 1 through 6; `gap` uses the Stack spacing values; has the `default` child slot. |\n| `Split` | none | `ratio: \"1-1\", \"1-2\", \"2-1\", \"1-3\", \"3-1\"`, `gap`, `collapseBelow: \"sm\", \"md\", \"lg\"`, `reverse`; has the positional `default` child slot. |\n| `Divider` | none | `label`; no children. |\n| `Tabs` | `id`, `tabs` | `tabs` entries are `{ key, label?, icon?, count? }` (`count` is a number shown as a small pill); `defaultTab`; `variant: \"line\"` (default) or `\"segmented\"` (a compact segmented control, same state and warm panels); has the positional `default` child slot. |\n| `Container` | none | Legacy layout primitive; prefer Stack. `direction: \"row\", \"column\"`, `gap: \"none\", \"sm\", \"md\", \"lg\"`; has the `default` child slot. |\n| `Card` | none | `title`, `description`; has the `default` child slot. |\n| `Heading` | `text` | `level: \"h1\", \"h2\", or \"h3\"`. |\n| `Text` | `content` | `tone: \"default\" or \"muted\"`. |\n| `Markdown` | `content` | Rendered Markdown for help, instructions, and rich prose; no children. |\n| `SearchInput` | `id` | `placeholder`, `label`, `shortcut` (one key, usually `\"/\"`: shows a keycap and focuses the field); writes debounced text to `/ui/<id>/value`; no children. |\n| `Select` | `id`, `options` | `options` are strings or `{ value, label?, icon?, hint? }`; `placeholder`, `label`, `clearable`, `searchable` (type to filter; on by default past 8 options), `searchPlaceholder`, `multiple`; writes a string or null to `/ui/<id>/value`, or with `multiple` a string array (empty = nothing picked); no children. |\n| `Chips` | `id`, `options` | Toggle chips for quick filters. `options` are strings or `{ value, label?, icon? }`; `multiple`; `label` (group name for screen readers). Writes a string or null to `/ui/<id>/value`, or a string array with `multiple`; pressing the selected chip clears it. |\n| `Button` | `label` | `variant: \"default\", \"secondary\", \"outline\", \"ghost\", \"destructive\", or \"destructive-outline\"` (use `destructive-outline` for standalone deletes); `disabled` (constant or bound boolean); `busyWith: \"<actionName>\"` swaps the label for a spinner while that custom action's `/actions/<name>/status` is `\"running\"` (presses are ignored), then shows a check and `successLabel` for about 1.5s when it turns `\"ok\"`; `icon`, `iconPosition: \"start\" or \"end\"`; `size: \"sm\", \"default\", or \"lg\"`; `shortcut` (one key, shown as a keycap, presses the button); `hint` (tooltip); dispatch with element-level `on.press`. |\n| `Metric` | `label`, `value` | The dashboard stat tile. `value` is string or number (empty renders an em dash); bind `loading` to `/queries/<name>/loading` for a skeleton while the query loads; `icon` with `tone` tinting its tile; `detail` (a line under the value); `trend: { direction: \"up\", \"down\", or \"flat\", label }`; `info` (explanation tooltip next to the label). |\n| `Alert` | `message` | `title`, `tone: \"info\", \"success\", \"warning\", or \"error\"`, `icon` (overrides the tone icon), `actionLabel` (a button inside the callout that emits `press`; wire it with `on.press`). |\n| `Badge` | `text` | `text` is string or number; `tone`; `icon` replaces the dot; `variant: \"tag\"` (default uppercase chip), `\"status\"` (sentence-case pill), `\"count\"` (numeric pill), or `\"beta\"` (the BETA tag); `status` (a lifecycle value renders the dashboard status chip); `hint` (tooltip; the beta tag's explanation). |\n| `Icon` | `name` | `size: \"sm\", \"md\", or \"lg\"`, `tone: \"default\", \"muted\", \"success\", \"warning\", \"error\", or \"info\"`, `label` (accessible name; decorative without it). |\n| `Progress` | `value` | `max` (default 100), `tone`, `label`, `variant: \"bar\"` (default) or `\"ring\"` (the dashboard progress ring; ignores `tone`), `showValue` (default true: percent for a bar, `value / max` for a ring). |\n| `EmptyState` | `title` | `icon` (default inbox), `body`, `actionLabel` (a button that emits `press`; wire it with `on.press`). |\n| `CopyButton` | `value` | `value` is string or number; `label` is the visible text, icon-only without it. Renders nothing for an empty value. |\n| `Kbd` | `keys` | Keycaps, one per string (`[\"⌘\", \"K\"]`). Display only. |\n| `Skeleton` | `shape` | `shape: \"line\", \"block\", \"rows\", \"metric\", or \"card\"`; `count` (1-20: lines, rows, tiles, or cards); `height` (px, `block` only, default 128). Show it with `visible` bound to a query's `loading`. |\n| `Spinner` | none | `variant: \"ring\", \"ellipsis\", or \"bars\"` (default: the dashboard spinner), `size: \"sm\" or \"md\"`, `label` (visible text and accessible name). |\n| `AvatarStack` | `users` | `users` are display names (initials avatars); `max` (default 4) before a `+N` count. Hover lists every name. |\n| `Section` | `title` | `description`, `collapsible`, `defaultOpen` (default true); has the `default` child slot. |\n| `Table` | `columns` | `data`, `loading`, `error`, `emptyMessage`, `emptyState`, `rowActions`, `onRowClick`, `selectedId`, `rowId`, `mobile`, `mobileFields`, `search`, `filters`; see below. |\n| `Form` | `id`, `fields`, `onSubmit` | `title`, `submitLabel`; see below. |\n| `Drawer` | `param` | `title`, `description`, `side: \"right\" or \"left\"`, `size: \"sm\", \"md\" (default), \"lg\", or \"xl\"` (about 480/660/900/1010px at 1440px; grows with the viewport up to 640/960/1280/1600px; full width on phones), `actions` (sticky footer buttons; see below); has the `default` child slot. |\n| `DetailList` | `fields` | `data`, `emptyMessage`, `columns: 1 or 2`, `loading` (bind `/queries/<name>/loading` for skeleton fields while loading); renders one record without edit controls; see below for field kinds. |\n| `Kanban` | `columnField`, `columns`, `titleField` | `data`, `loading`, `cardFields`, `idField` (default `id`), `onMove`, `onCardClick`; see below. |\n| `Calendar` | `startField`, `titleField` | `data`, `loading`, `endField`, `idField`, `onSelect`; read-only month grid; see below. |\n| `ContributionGraph` | none | `data`, `loading`, `dateField` (default `date`), `countField`, `days` (7-730, default 365); read-only activity heatmap. |\n\nTable details:\n\n- `columns[]`: `{ key, label?, kind?, tones?: {value: badgeTone}, width?: number, pinned?: \"left\" or \"right\", align?: \"start\" or \"end\", truncate?: \"end\" or \"middle\", icon?, tooltip?: \"<row field>\", max?: number, onChange?: chain }`. `kind` is `text`, `string`, `enum`, `number`, `boolean`, `date`, `badge`, `status`, `user`, `agent`, `relative`, `duration`, `progress`, `copy`, `switch`, or `link`; `string` and `enum` render as text. Pin id/actions columns so they survive horizontal scroll on narrow viewports.\n - `status`: lifecycle values (`pending`, `in_progress`, `running`, `completed`, `failed`, `cancelled`, `paused`, `reviewing`, ...) render the dashboard status chip with its icon; other values use `tones` like `badge`.\n - `user` / `agent`: avatar plus name. `relative` (and `date`): smart time, exact time on hover. `duration`: milliseconds as `1m 23s`. `progress`: a bar against `max` (default 100). `copy`: the value plus a copy button. `link`: an http(s) URL opening in a new tab (other values render as text).\n - `switch`: a toggle. `onChange` runs with `$row` = the row with this column already set to the new value, e.g. `[{ \"action\": \"app.mutate\", \"params\": { \"model\": \"m\", \"op\": \"update\", \"rowId\": { \"$row\": \"id\" }, \"values\": { \"enabled\": { \"$row\": \"enabled\" } } } }]`. Without `onChange` the switch is read-only.\n - Numbers, durations and progress right-align by default. `truncate: \"middle\"` keeps both ends of ids and paths. `icon` shows in the header; `tooltip` names a row field shown on hover.\n- `onRowClick`: action chain with `$row` / `$rowIndex`, run when a row is clicked (or Enter on a focused cell). Pair it with `app.navigate` that sets a Drawer param, and bind `selectedId` to `{ \"$state\": \"/route/params/<param>\" }` so the open row stays highlighted. `rowId` names the id field (default `id`).\n- Phones (below 768px): the table renders as a card list. `mobileFields: { title, subtitle?, meta?: string or string[], status? }` picks the columns; by default the first text column is the title and the first `status`/`badge` column the chip. `mobile: \"scroll\"` keeps the grid; it is the default when the table has `rowActions` but no `onRowClick`.\n- `emptyState: { icon?, title, body? }` replaces the plain empty message. A search or filter that hides every row shows its own \"No rows match\" state.\n- `pagination`: boolean; defaults to auto-enabling past 200 rows (set `false` to force one scroll region). `density: \"compact\"` tightens row height for dense readouts.\n- `rowActions[]`: `{ label, variant?, icon?, placement?: \"inline\" or \"menu\", confirm?, actions }`. `placement: \"menu\"` moves the action into the row's More menu. Variants add `destructive-outline` to the Button variants. `destructive` and `destructive-outline` confirm by default; customize with `{ \"title\": ..., \"description\": ..., \"confirmLabel\": ... }`, or use a bare `confirm` string as the dialog description (use `confirm: false` only for a reversible action).\n- `search`: optional string; case-insensitive substring matching across the listed string and number columns. Bind a SearchInput value for client-side search.\n- `filters`: optional record of per-column string, number, boolean, null, or array values. An array means \"the cell is one of these\" (what a `multiple` Chips or Select writes). Null, empty strings, empty arrays, or absent values disable one filter. Bind Select or Chips values for client-side filters.\n\nKanban details:\n\n- One card per `data` row, grouped by the row's `columnField` value. `columns[]` are plain strings or `{ id, label? }`, in display order; rows matching no column are hidden.\n- `cardFields[]` uses the DetailList field shape (`{ key, label?, kind?, tones? }`, same kinds) for extra lines under the title.\n- `onMove` is an action chain run when a card is dropped in another column. `$row` is the moved row with `columnField` already set to the new column: `[{ \"action\": \"app.mutate\", \"params\": { \"model\": \"deal\", \"op\": \"update\", \"rowId\": { \"$row\": \"id\" }, \"values\": { \"stage\": { \"$row\": \"stage\" } } } }]`. Without `onMove`, cards cannot be dragged.\n- `onCardClick` is an action chain with `$row`; pair it with `app.navigate` and a `Drawer` to open a card.\n\nCalendar details:\n\n- `startField` / `endField` hold ISO dates, ISO datetimes, or epoch ms. An event shows on every day from start to end; without `endField` it shows on its start day.\n- Each Calendar keeps its own visible month (prev/next controls in its header). `onSelect` is an action chain with `$row`, run when an event is clicked.\n\nContributionGraph details:\n\n- Buckets `data` rows by the calendar day of `dateField` over the trailing `days`. Each row counts 1, or adds its numeric `countField` when set.\n\nForm details:\n\n- `fields[]`: `{ name, label?, kind?, options?: string[], placeholder?, required? }`; `kind` is `string`, `text`, `number`, `boolean`, `date`, or `enum`, and enum fields need `options`.\n- Values live at `/forms/<formId>/<fieldName>`. `onSubmit` is an action chain.\n- The submit button shows a pending spinner while the chain runs, and `app.mutate` failures in the chain render inline under the fields (not the page banner) — no extra wiring needed.\n\nDrawer details:\n\n- `param` must name a param declared on the containing page. The drawer is open exactly when `/route/params/<param>` has a non-empty value, so its open state survives refresh and is shareable. Its children mount only while open; closing it clears that param with history replacement.\n- `actions[]`: `{ label, variant?, icon?, confirm?, actions }`, rendered as buttons in a sticky footer under the scrolling body. Confirmation follows Table `rowActions`: destructive variants confirm by default, and a chain with an `app.mutate` delete always confirms. Chains have no `$row`; address the open record with `{ \"$state\": \"/route/params/<param>\" }`, for example `{ \"action\": \"app.mutate\", \"params\": { \"model\": \"order\", \"op\": \"delete\", \"rowId\": { \"$state\": \"/route/params/panel\" } } }`. Prev/next record navigation is not available.\n\nDetailList details:\n\n- `fields[]`: `{ key, label?, kind?, tones? }`. Kinds reuse Table formatting (`text`, `string`, `number`, `boolean`, `date`, `badge`, `enum`) and the Table cell renderers (`status`: lifecycle chip or `tones` pill; `user`: avatar plus name; `relative`: smart time, exact on hover; `copy`: value plus copy button), and add `code` for a monospace block and `json` for a collapsible JSON tree (objects, or a string holding JSON). Bind `data` to one record, commonly `/queries/<detailQuery>/data/0`.\n\nMotion details:\n\n- Every `Stack` fades its direct children in and lifts them 4px over 200ms on first paint, staggered 30ms (capped at the 10th child). It never replays on the 5s data poll. A Stack nested inside another one does not add its own entrance. `motion: \"fade\"` drops the lift; `motion: \"none\"` turns it off. Viewers with reduced motion get a 150ms fade.\n\nShortcut details:\n\n- `Button.shortcut` and `SearchInput.shortcut` are one key. Letters and digits match without Shift; symbols such as `/` and `?` match as typed. They do nothing while focus is in a text field, while Ctrl, Cmd, or Alt is held, or while a dialog or menu is open (unless the control is inside it, such as a Button in an open Drawer).\n\n## Layout & interactivity\n\nUse `Stack` as the primary page and section layout; `Container` is the legacy two-prop primitive retained for existing pages. Stack supports vertical or horizontal flow, shared spacing, alignment, justification, wrapping, and padding; row Stacks (filter bars, button rows) should set `collapseBelow: \"sm\"|\"md\"|\"lg\"` so they stack vertically on phones. Use `Grid` for responsive card or metric strips: a bare column count is responsive shorthand (1 column on phones, 2 from `sm`, the count from `md` up), or pin breakpoints explicitly with `{ \"base\": 1, \"md\": 2, \"lg\": 3 }`. `Grid` and `Split` also accept `padding` on the shared spacing scale.\n\n`Split` and `Tabs` children are positional. For Split, `children[0]` is the first pane, `children[1]` is the second, and extra children append inside the second pane. Below `collapseBelow`, panes stack; `reverse` changes only that narrow-layout stacking order. For Tabs, `children[i]` is the body for `tabs[i]`; keep both arrays in the same order and with the same count. Inactive tab children stay mounted but hidden, so Tables keep polling. Use `Divider` to separate sections and `Markdown` for richer explanatory content.\n\nSearchInput, Select, and Chips are client-side controls: each needs a literal `id` and writes state under `/ui/<id>/value`; Tabs writes its active key to `/ui/<id>/tab`. Bind those values into a Table's `search` and `filters` props. This filters already-polled rows locally; it does not alter or re-run the named query.\n\n```json\n{\n \"filters\": { \"type\": \"Stack\", \"props\": { \"gap\": \"sm\" }, \"children\": [\"query\", \"status\"] },\n \"query\": { \"type\": \"SearchInput\", \"props\": { \"id\": \"ideaSearch\", \"placeholder\": \"Search ideas\" } },\n \"status\": { \"type\": \"Select\", \"props\": { \"id\": \"ideaStatus\", \"options\": [\"open\", \"done\"] } },\n \"ideas\": {\n \"type\": \"Table\",\n \"props\": {\n \"data\": { \"$state\": \"/queries/allIdeas/data\" },\n \"columns\": [{ \"key\": \"title\" }, { \"key\": \"status\" }],\n \"search\": { \"$state\": \"/ui/ideaSearch/value\" },\n \"filters\": { \"status\": { \"$state\": \"/ui/ideaStatus/value\" } }\n }\n }\n}\n```\n\n`visible` accepts a boolean, a `$state` truthiness binding, comparisons, and logical groups. Put exactly one comparison key in each condition object. The renderer evaluates the first matching key in fixed priority order (`eq`, `neq`, `gt`, `gte`, `lt`, `lte`; otherwise truthiness), so additional comparison keys in the same object are ignored. Use `$and` / `$or` arrays to combine conditions. `not` is a negation FLAG inside a condition object (`\"not\": true`), never a wrapper around one — `{ \"not\": { \"$state\": … } }` is invalid and the validator rejects it. The absent-record pattern (e.g. an alert when a detail query matched nothing) is `{ \"$state\": \"/queries/<q>/data/0/id\", \"not\": true }`:\n\n```json\n[\n { \"$state\": \"/route/params/issueId\", \"gt\": 0 },\n {\n \"$and\": [\n { \"$state\": \"/route/page\", \"eq\": \"detail\" },\n { \"$state\": \"/route/params/panel\", \"neq\": \"hidden\" }\n ]\n }\n]\n```\n\n## Bindings, sentinels, and action chains\n\nUse this exact query binding shape; do not invent `$query`, `$item`, or template strings:\n\n```json\n{\n \"data\": { \"$state\": \"/queries/allIdeas/data\" },\n \"loading\": { \"$state\": \"/queries/allIdeas/loading\" },\n \"error\": { \"$state\": \"/queries/allIdeas/error\" }\n}\n```\n\nValid `$state` roots are:\n\n- `/queries/<declaredQuery>/...`\n- `/forms/<formId>/...`, where a `Form` element has that literal `props.id`\n- `/ui/<id>/...`, where a SearchInput, Select, Chips, or Tabs element has that literal `props.id` (SearchInput, Select, and Chips use `/value`; Tabs uses `/tab`)\n- `/actions/<declaredAction>/...`\n- `/route/page` for the active page name\n- `/route/params/<declaredParam>` for a param declared on the current page\n\n`/route` is mirrored as `{ page, params }`. Only declared params appear. Values are coerced by their declared kind: `number` becomes a number when possible; boolean strings `true`/`1` become true; failed number coercion remains the raw string.\n\nOnly inside action-chain `params` (`on.<event>`, `Table.rowActions[].actions`, or `Form.onSubmit`) these scoped sentinels are valid, recursively inside objects and arrays:\n\n| Sentinel | Resolves to |\n|---|---|\n| `{ \"$row\": \"id\" }` | Current Table row's `id`; another column name selects that field. |\n| `{ \"$row\": \"\" }` | Entire current Table row. |\n| `{ \"$rowIndex\": true }` | Current Table row index. |\n| `{ \"$form\": \"title\" }` | One current Form value. |\n| `{ \"$form\": \"\" }` | All current Form values. |\n| `{ \"$state\": \"/queries/allIdeas/data\" }` | Current value at a valid app state path, including action state such as `/actions/refreshIssues/status`. |\n\nEach sentinel object must contain exactly the single key shown.\n\nAn action-chain step is `{ \"action\": \"<type>\", \"params\": {...} }`. Available action types:\n\n- `app.mutate`: `{ model, op, rowId?, values?, formId? }`, where `op` is `\"create\"`, `\"update\"`, or `\"delete\"`. `update`/`delete` require `rowId` (usually `{ \"$row\": \"id\" }`, or `{ \"$state\": \"/route/params/<param>\" }` in a Drawer footer); literal `values` keys must be model columns. Successful mutation refetches all queries on that model.\n- `app.refresh`: `{ query? }`; omit `query` to refetch all, or name a declared query.\n- `app.action`: `{ name, input? }`; `name` must be declared in definition `actions`. For a task-kind row action, pass whole-row context with `input: { issue: { \"$row\": \"\" } }`.\n- `app.navigate`: `{ page, params? }`; `page` must be declared, supplied keys must be params of the target page, and every required target param must be supplied. `params` replace the current route params wholesale and may contain action sentinels. Navigation pushes history, so browser Back returns to the previous page/params; only `mode` is preserved automatically.\n- `ui.toast`: `{ message, description?, tone?, undo? }`, where `tone` is `\"success\"`, `\"error\"`, `\"info\"` (default), or `\"warning\"`. `undo: { label?, actions }` adds an Undo button (default label `Undo`) that runs `actions` as a chain; its steps are validated like any chain. Put it after the step it confirms, e.g. a row delete followed by `{ \"action\": \"ui.toast\", \"params\": { \"message\": \"Order deleted\", \"undo\": { \"actions\": [{ \"action\": \"app.mutate\", \"params\": { \"model\": \"order\", \"op\": \"create\", \"values\": { \"title\": { \"$row\": \"title\" } } } }] } } }`. Undo params resolve `$state` when Undo is pressed; `$row` / `$form` resolve when the toast is shown.\n- `swarm.sdk`: `{ sdk, args? }`; invokes a catalog-supported Swarm browser SDK method with the viewer's bearer.\n- `swarm.call`: `{ method, endpoint: \"/api/...\", body? }`, where `method` is `\"GET\"`, `\"POST\"`, `\"PUT\"`, `\"DELETE\"`, or `\"PATCH\"`; raw authenticated API call.\n\n## Patch semantics\n\n`app-patch.definition` is RFC 7396 JSON Merge Patch applied to the stored definition:\n\n- omitted keys stay unchanged;\n- object keys merge recursively;\n- arrays and scalar values replace;\n- `null` deletes a key;\n- reusable-element disambiguation: a patch value for `elements.<name>` containing ONLY the `elements` key merges node-by-node; any other key present (`mode`, `root`, `props`, or `export`) makes it a full element replace — restate every field you want kept. In that node-by-node form, `elements.<name>.elements.<id>: null` deletes one node. Literal null nodes are rejected in a full element replace so they cannot be mistaken for deletions.\n- otherwise every supplied `pages.<page>.elements.<id>`, `pages.<page>.params.<param>`, `actions.<name>`, `models.<name>.columns.<col>`, and `models.<name>.sources.<src>` value is atomic and replaces that complete declaration; `null` deletes it (deleting a source detaches its rows — see the source lifecycle above).\n\n`pages.<page>: null` deletes a page, except the current `defaultPage` cannot be deleted. `root`, `title`, and top-level `defaultPage` use ordinary merge semantics. A top-level `page` patch is rejected with guidance to patch `pages.<name>` instead.\n\nBecause elements, param declarations, actions, and columns are atomic, include the complete desired object when changing one. For example, changing only a Table's `emptyMessage` requires sending its full `{ \"type\": \"Table\", \"props\": ... }` element. The merged result is validated as a whole; on failure, read `issues[]`, correct the paths, and retry without assuming anything was written.\n\n```json\n{\n \"appId\": \"<app-id>\",\n \"description\": null,\n \"definition\": {\n \"models\": {\n \"idea\": {\n \"columns\": {\n \"priority\": { \"kind\": \"enum\", \"enum\": [\"low\", \"high\"], \"default\": \"low\" }\n }\n }\n },\n \"queries\": { \"openIdeas\": null },\n \"pages\": {\n \"main\": {\n \"elements\": {\n \"title\": { \"type\": \"Heading\", \"props\": { \"text\": \"Prioritized ideas\", \"level\": \"h1\" } }\n }\n }\n }\n }\n}\n```\n\n## Worked example: ideas tracker\n\nCreate this with `app-upsert`:\n\n```json\n{\n \"name\": \"Ideas Tracker\",\n \"description\": \"Collect and manage product ideas\",\n \"definition\": {\n \"models\": {\n \"idea\": {\n \"columns\": {\n \"title\": { \"kind\": \"string\", \"required\": true },\n \"status\": { \"kind\": \"enum\", \"enum\": [\"open\", \"done\"], \"default\": \"open\" }\n }\n }\n },\n \"queries\": {\n \"allIdeas\": { \"model\": \"idea\", \"sort\": { \"column\": \"createdAt\", \"dir\": \"desc\" } }\n },\n \"actions\": {\n \"triage\": { \"kind\": \"task\", \"prompt\": \"Review current ideas and recommend the next one to pursue\" }\n },\n \"pages\": {\n \"main\": {\n \"root\": \"root\",\n \"elements\": {\n \"root\": {\n \"type\": \"Container\",\n \"props\": { \"direction\": \"column\", \"gap\": \"lg\" },\n \"children\": [\"heading\", \"formCard\", \"ideasCard\", \"triage\"]\n },\n \"heading\": { \"type\": \"Heading\", \"props\": { \"text\": \"Ideas\", \"level\": \"h1\" } },\n \"formCard\": {\n \"type\": \"Card\",\n \"props\": { \"title\": \"Add an idea\" },\n \"children\": [\"ideaForm\"]\n },\n \"ideaForm\": {\n \"type\": \"Form\",\n \"props\": {\n \"id\": \"newIdea\",\n \"fields\": [{ \"name\": \"title\", \"label\": \"Title\", \"required\": true }],\n \"submitLabel\": \"Add\",\n \"onSubmit\": [{\n \"action\": \"app.mutate\",\n \"params\": { \"model\": \"idea\", \"op\": \"create\", \"values\": { \"$form\": \"\" } }\n }]\n }\n },\n \"ideasCard\": {\n \"type\": \"Card\",\n \"props\": { \"title\": \"All ideas\" },\n \"children\": [\"ideasTable\"]\n },\n \"ideasTable\": {\n \"type\": \"Table\",\n \"props\": {\n \"data\": { \"$state\": \"/queries/allIdeas/data\" },\n \"loading\": { \"$state\": \"/queries/allIdeas/loading\" },\n \"error\": { \"$state\": \"/queries/allIdeas/error\" },\n \"columns\": [\n { \"key\": \"title\", \"label\": \"Title\" },\n { \"key\": \"status\", \"label\": \"Status\", \"kind\": \"badge\", \"tones\": { \"open\": \"info\", \"done\": \"success\" } }\n ],\n \"rowActions\": [{\n \"label\": \"Delete\",\n \"variant\": \"destructive-outline\",\n \"actions\": [{ \"action\": \"app.mutate\", \"params\": { \"model\": \"idea\", \"op\": \"delete\", \"rowId\": { \"$row\": \"id\" } } }]\n }]\n }\n },\n \"triage\": {\n \"type\": \"Button\",\n \"props\": { \"label\": \"Ask swarm to triage\", \"variant\": \"secondary\" },\n \"on\": { \"press\": [{ \"action\": \"app.action\", \"params\": { \"name\": \"triage\" } }] }\n }\n }\n }\n },\n \"defaultPage\": \"main\"\n }\n}\n```\n\nThe tool returns `{ appId, url: \"/apps/<id>\" }`. Open that URL to verify the live UI, then continue with the read-patch-validate loop above.\n\n## View and screenshot a live app\n\n- An app renders at `<dashboard>/apps/<id>`. Two view modes are set by query string: `?mode=full` (full-viewport overlay with a slim header) and `?mode=chromeless` (only the rendered page, the embed surface). `app.navigate` keeps only `?mode`.\n- Logged-in browser: a fresh dashboard has no connection and redirects to /setup. Seed the localStorage key `agent-swarm-connections` with `{connections:[{id,name,apiUrl,apiKey}],activeId}`. Build the value from an env var and pipe it to `agent-browser eval --stdin`, so the key never reaches a command line. Reload, then pick a user in the \"Who are you?\" dialog.\n- Apps scroll inside an inner canvas, so a full-page screenshot captures only the viewport. Scroll the canvas or raise the viewport height. For mobile, set a 375px-wide viewport before the screenshot.\n- App definitions: `GET /api/apps` and `GET /api/apps/<id>` with the API key as Bearer.\n";
6123
5767
 
6124
5768
  // templates/skills/ask-user/config.json
6125
- var config_default4 = `{
5769
+ var config_default3 = `{
6126
5770
  "category": "skills",
6127
5771
  "description": "Conventions for asking the user questions during a workflow via the AskUserQuestion tool. Other skills reference this to keep prompts consistent and structured.",
6128
5772
  "displayName": "Ask User",
@@ -6143,7 +5787,7 @@ var config_default4 = `{
6143
5787
  `;
6144
5788
 
6145
5789
  // templates/skills/ask-user/content.md
6146
- var content_default4 = `# Ask User
5790
+ var content_default3 = `# Ask User
6147
5791
 
6148
5792
  Thin helper that codifies how to ask the user questions during a workflow. Other skills (planning, research, brainstorming, implementing) reference this skill instead of duplicating AskUserQuestion conventions.
6149
5793
 
@@ -6219,7 +5863,7 @@ options:
6219
5863
  `;
6220
5864
 
6221
5865
  // templates/skills/asset-namespaces/config.json
6222
- var config_default5 = `{
5866
+ var config_default4 = `{
6223
5867
  "kind": "skill",
6224
5868
  "name": "asset-namespaces",
6225
5869
  "displayName": "Asset Namespaces",
@@ -6236,10 +5880,10 @@ var config_default5 = `{
6236
5880
  `;
6237
5881
 
6238
5882
  // templates/skills/asset-namespaces/content.md
6239
- var content_default5 = "# Asset Namespaces\n\nUse an asset `key` as directory-like grouping metadata across tasks, workflows, schedules, pages, apps, scripts, and mapped provider files. Keep identity separate: entity IDs remain authoritative, task `contextKey` remains routing context, and many assets may intentionally share one key.\n\n## Choose a namespace\n\n- Use `shared/` for general work and stable descendants such as `shared/releases/` or `shared/platform/migrations/` for team-visible groupings.\n- Use `personal/<user-id>/` or a descendant only for work owned by the trusted resolved user in the current request or task context.\n- Prefer durable organizational paths over temporary status labels. Put lifecycle state in task or workflow status fields, not paths such as `shared/in-progress/`.\n- Supply canonical values: lowercase relative paths, forward slashes, and a trailing slash. The server trims, Unicode-normalizes, lowercases, and adds the trailing slash, but rejects empty segments, `.`, `..`, backslashes, absolute paths, NUL bytes, unknown roots, and values longer than 255 characters.\n\n`personal/` is a write-ownership convention, **not** a privacy or read-visibility boundary. Continue to rely on each entity's authorization rules, and never place sensitive data under `personal/` merely because of the key.\n\n## Set and inherit keys\n\nPass `key` when creating a task, workflow, schedule, or page. Omitting it creates a deterministic shared resource key such as `shared/task:<id>/`, `shared/workflow:<id>/`, `shared/schedule:<id>/`, `shared/page:<id>/`, or `shared/fs:agent-fs:<id>/`.\n\n- Tasks sent from another task inherit the source task's key unless explicitly overridden.\n- Tasks launched by a workflow inherit the workflow key unless the agent-task node supplies a key.\n- Tasks launched by a schedule inherit the schedule key.\n- Agent-fs task attachments receive logical file mappings and inherit their task key.\n\nUse only a canonical user-registry ID in `personal/<user-id>/`. Do not substitute an agent ID, email address, Slack ID, or unverified caller-provided value. The API authorizes the destination against trusted request/task context and rejects mismatches.\n\n## Find and manage assets\n\nUse entity list tools with `key` for an exact namespace or `keyPrefix` for a subtree where those filters are exposed. For a lightweight cross-entity view, use `GET /api/assets` or the public page SDK:\n\n```js\nconst result = await window.swarmSdk.assets.list({\n keyPrefix: \"shared/releases/\",\n types: \"task,workflow,schedule,page,app,script,file\",\n limit: 100,\n});\n```\n\nMove an asset logically without changing its ID:\n\n```js\nawait window.swarmSdk.assets.move(\"workflow\", workflowId, \"shared/platform/\");\n```\n\nThe full `swarmSdk.assets` domain is:\n\n| Method | Purpose |\n|---|---|\n| `list(filters?)` | Return lightweight cross-entity summaries. |\n| `move(entityType, id, key)` | Change local namespace metadata. |\n| `registerMapping(body)` | Operator-only registration of a provider object under a logical key. |\n| `audit()` | Operator-only structural and mapping-drift audit. |\n\nProvider-backed moves are metadata-only: do not claim that a provider file, drive object, or agent-fs path was renamed or moved remotely. Repair audit warnings before retrying a blocked move.\n\n## Before writing\n\n1. Confirm whether the grouping is shared or owned by a resolved user.\n2. Keep the key non-unique and separate from entity identity or routing context.\n3. Pass the key at the highest-level parent so descendants inherit it.\n4. Use exact and prefix filters instead of scanning unrelated records.\n5. Treat personal namespaces and provider mappings according to their real authorization and remote-move limits.\n";
5883
+ var content_default4 = "# Asset Namespaces\n\nUse an asset `key` as directory-like grouping metadata across tasks, workflows, schedules, pages, apps, scripts, and mapped provider files. Keep identity separate: entity IDs remain authoritative, task `contextKey` remains routing context, and many assets may intentionally share one key.\n\n## Choose a namespace\n\n- Use `shared/` for general work and stable descendants such as `shared/releases/` or `shared/platform/migrations/` for team-visible groupings.\n- Use `personal/<user-id>/` or a descendant only for work owned by the trusted resolved user in the current request or task context.\n- Prefer durable organizational paths over temporary status labels. Put lifecycle state in task or workflow status fields, not paths such as `shared/in-progress/`.\n- Supply canonical values: lowercase relative paths, forward slashes, and a trailing slash. The server trims, Unicode-normalizes, lowercases, and adds the trailing slash, but rejects empty segments, `.`, `..`, backslashes, absolute paths, NUL bytes, unknown roots, and values longer than 255 characters.\n\n`personal/` is a write-ownership convention, **not** a privacy or read-visibility boundary. Continue to rely on each entity's authorization rules, and never place sensitive data under `personal/` merely because of the key.\n\n## Set and inherit keys\n\nPass `key` when creating a task, workflow, schedule, or page. Omitting it creates a deterministic shared resource key such as `shared/task:<id>/`, `shared/workflow:<id>/`, `shared/schedule:<id>/`, `shared/page:<id>/`, or `shared/fs:agent-fs:<id>/`.\n\n- Tasks sent from another task inherit the source task's key unless explicitly overridden.\n- Tasks launched by a workflow inherit the workflow key unless the agent-task node supplies a key.\n- Tasks launched by a schedule inherit the schedule key.\n- Agent-fs task attachments receive logical file mappings and inherit their task key.\n\nUse only a canonical user-registry ID in `personal/<user-id>/`. Do not substitute an agent ID, email address, Slack ID, or unverified caller-provided value. The API authorizes the destination against trusted request/task context and rejects mismatches.\n\n## Find and manage assets\n\nUse entity list tools with `key` for an exact namespace or `keyPrefix` for a subtree where those filters are exposed. For a lightweight cross-entity view, use `GET /api/assets` or the public page SDK:\n\n```js\nconst result = await window.swarmSdk.assets.list({\n keyPrefix: \"shared/releases/\",\n types: \"task,workflow,schedule,page,app,script,file\",\n limit: 100,\n});\n```\n\nMove an asset logically without changing its ID:\n\n```js\nawait window.swarmSdk.assets.move(\"workflow\", workflowId, \"shared/platform/\");\n```\n\nThe full `swarmSdk.assets` domain is:\n\n| Method | Purpose |\n|---|---|\n| `list(filters?)` | Return lightweight cross-entity summaries. |\n| `move(entityType, id, key)` | Change local namespace metadata. |\n| `registerMapping(body)` | Operator-only registration of a provider object under a logical key. |\n| `audit()` | Operator-only structural and mapping-drift audit. |\n\nProvider-backed moves are metadata-only: do not claim that a provider file, drive object, or agent-fs path was renamed or moved remotely. Repair audit warnings before retrying a blocked move.\n\n## Before writing\n\n1. Confirm whether the grouping is shared or owned by a resolved user.\n2. Keep the key non-unique and separate from entity identity or routing context.\n3. Pass the key at the highest-level parent so descendants inherit it.\n4. Use exact and prefix filters instead of scanning unrelated records.\n5. Treat personal namespaces and provider mappings according to their real authorization and remote-move limits.\n";
6240
5884
 
6241
5885
  // templates/skills/attio-interaction/config.json
6242
- var config_default6 = `{
5886
+ var config_default5 = `{
6243
5887
  "kind": "skill",
6244
5888
  "name": "attio-interaction",
6245
5889
  "displayName": "Attio Interaction",
@@ -6257,10 +5901,10 @@ var config_default6 = `{
6257
5901
  `;
6258
5902
 
6259
5903
  // templates/skills/attio-interaction/content.md
6260
- var content_default6 = '# Attio Interaction (Read + Write)\n\nUse this skill to read and write your Attio CRM through the REST API v2. Every read or write is a direct API call; agent-swarm does not maintain a separate Attio sync.\n\n## TL;DR\n\n1. Resolve `ATTIO_API_KEY` from swarm config before making calls.\n2. Base URL: `https://api.attio.com/v2/`\n3. Use `Authorization: Bearer $ATTIO_API_KEY`, `Content-Type: application/json`, and `Accept: application/json`.\n4. Prefer upsert over create for People, Companies, and Deals: `PUT /v2/objects/{slug}/records` with `matching_attribute`.\n5. Rate limits: 100 reads/sec, 25 writes/sec. Pace write bursts to roughly 15-20/sec.\n6. Attribute values are arrays, even for scalar values: `[{ "value": 42 }]`, never `42`.\n\n## Authentication\n\n```bash\nATTIO_API_KEY=$(get-config key="ATTIO_API_KEY" includeSecrets=true)\n\ncurl -sS "https://api.attio.com/v2/objects" \\\n -H "Authorization: Bearer $ATTIO_API_KEY" \\\n -H "Accept: application/json"\n```\n\nIf calls return `401`, re-fetch the key from config. If it still fails, notify the Lead; the key may need rotation. Do not retry silently.\n\n## Core object slugs\n\n| Object | API slug | Primary matching attribute |\n|---|---|---|\n| Companies | `companies` | `domains` |\n| People | `people` | `email_addresses` |\n| Deals | `deals` | Usually no global dedupe key; link to company/person records |\n\nCustom objects use their configured API slug. Discover them with `GET /v2/objects`.\n\n## Common operations\n\n### 1. Discover objects and slugs\n\n```bash\ncurl -sS "https://api.attio.com/v2/objects" \\\n -H "Authorization: Bearer $ATTIO_API_KEY" \\\n -H "Accept: application/json" \\\n | jq \'.data[] | {slug: .api_slug, name: .title}\'\n```\n\n### 2. Query records with filters and pagination\n\n```bash\ncurl -sS -X POST "https://api.attio.com/v2/objects/companies/records/query" \\\n -H "Authorization: Bearer $ATTIO_API_KEY" \\\n -H "Content-Type: application/json" \\\n -d \'{\n "filter": {\n "stage": { "$not_equal": "Won" }\n },\n "limit": 100,\n "offset": 0\n }\' | jq \'.data[] | {record_id: .id.record_id, name: .values.name[0].value}\'\n```\n\nPaginate by increasing `offset` until `data` is empty. For no filter, omit the `filter` key.\n\n### 3. Get a single record\n\n```bash\ncurl -sS "https://api.attio.com/v2/objects/companies/records/{RECORD_ID}" \\\n -H "Authorization: Bearer $ATTIO_API_KEY" \\\n -H "Accept: application/json" \\\n | jq \'.data.values\'\n```\n\n### 4. Upsert a company by domain\n\nUse `PUT` with `matching_attribute`. It creates if not found and updates if found, so it is safe to call repeatedly.\n\n```bash\ncurl -sS -X PUT "https://api.attio.com/v2/objects/companies/records" \\\n -H "Authorization: Bearer $ATTIO_API_KEY" \\\n -H "Content-Type: application/json" \\\n -d \'{\n "data": {\n "values": {\n "name": [{ "value": "Acme Corp" }],\n "domains": [{ "domain": "acme.com" }],\n "employee_count": [{ "value": 150 }]\n }\n },\n "matching_attribute": "domains"\n }\' | jq \'{record_id: .data.id.record_id}\'\n```\n\n### 5. Upsert a person by email\n\n```bash\ncurl -sS -X PUT "https://api.attio.com/v2/objects/people/records" \\\n -H "Authorization: Bearer $ATTIO_API_KEY" \\\n -H "Content-Type: application/json" \\\n -d \'{\n "data": {\n "values": {\n "name": [{ "first_name": "Jane", "last_name": "Doe" }],\n "email_addresses": [{ "email_address": "jane@acme.com" }],\n "job_title": [{ "value": "CTO" }]\n }\n },\n "matching_attribute": "email_addresses"\n }\' | jq \'{record_id: .data.id.record_id}\'\n```\n\nAn upsert returns HTTP `200` when it updates an existing record and `201` when it creates one. The response body has the same shape in both cases.\n\n### 6. Update specific attributes on an existing record\n\n```bash\ncurl -sS -X PATCH "https://api.attio.com/v2/objects/companies/records/{RECORD_ID}" \\\n -H "Authorization: Bearer $ATTIO_API_KEY" \\\n -H "Content-Type: application/json" \\\n -d \'{\n "data": {\n "values": {\n "icp_score": [{ "value": 85 }],\n "icp_tier": [{ "value": "Tier 1" }]\n }\n }\n }\'\n```\n\n### 7. Write a note to a record\n\nNotes are concise free-text entries linked to a record. Use them to preserve useful interaction context, enrichment findings, or an agent result that should remain visible in the CRM.\n\n```bash\ncurl -sS -X POST "https://api.attio.com/v2/notes" \\\n -H "Authorization: Bearer $ATTIO_API_KEY" \\\n -H "Content-Type: application/json" \\\n -d \'{\n "data": {\n "parent_object": "companies",\n "parent_record_id": "{RECORD_ID}",\n "title": "Enrichment - 2026-06-02",\n "content": "Employee count: 150. Funding stage: Series A. Tech stack: Node.js, React."\n }\n }\' | jq \'{note_id: .data.id.note_id}\'\n```\n\n### 8. Create a task linked to a record\n\n```bash\ncurl -sS "https://api.attio.com/v2/workspace_members" \\\n -H "Authorization: Bearer $ATTIO_API_KEY" \\\n -H "Accept: application/json" \\\n | jq \'.data[] | {member_id: .id.workspace_member_id, name: .name, email: .email_address}\'\n\ncurl -sS -X POST "https://api.attio.com/v2/tasks" \\\n -H "Authorization: Bearer $ATTIO_API_KEY" \\\n -H "Content-Type: application/json" \\\n -d \'{\n "data": {\n "content": "Follow up - no contact in 21 days",\n "is_completed": false,\n "assignees": [\n { "referenced_actor_type": "workspace-member", "referenced_actor_id": "{MEMBER_ID}" }\n ],\n "linked_records": [\n { "target_object": "deals", "target_record_id": "{DEAL_RECORD_ID}" }\n ]\n }\n }\' | jq \'{task_id: .data.id.task_id}\'\n```\n\n### 9. Post a comment on a record\n\n```bash\ncurl -sS -X POST "https://api.attio.com/v2/comments" \\\n -H "Authorization: Bearer $ATTIO_API_KEY" \\\n -H "Content-Type: application/json" \\\n -d \'{\n "data": {\n "record": { "target_object": "companies", "target_record_id": "{RECORD_ID}" },\n "content": [\n { "type": "text", "text": "Possible duplicate of acme-corp-old - please review and merge." }\n ]\n }\n }\' | jq \'{comment_id: .data.id.comment_id}\'\n```\n\n### 10. Query a list or pipeline view\n\n```bash\ncurl -sS "https://api.attio.com/v2/lists" \\\n -H "Authorization: Bearer $ATTIO_API_KEY" \\\n -H "Accept: application/json" \\\n | jq \'.data[] | {list_id: .id.list_id, name: .title}\'\n\ncurl -sS -X POST "https://api.attio.com/v2/lists/{LIST_ID}/entries/query" \\\n -H "Authorization: Bearer $ATTIO_API_KEY" \\\n -H "Content-Type: application/json" \\\n -d \'{ "limit": 100, "offset": 0 }\' \\\n | jq \'.data[] | {entry_id: .id.entry_id, record_id: .record_id}\'\n```\n\n### 11. Global search across objects\n\n```bash\ncurl -sS -X POST "https://api.attio.com/v2/records/search" \\\n -H "Authorization: Bearer $ATTIO_API_KEY" \\\n -H "Content-Type: application/json" \\\n -d \'{ "query": "acme", "limit": 10 }\' \\\n | jq \'.data[] | {object: .object_type, record_id: .id.record_id}\'\n```\n\n## Webhooks\n\nAttio delivers webhooks at least once. Payloads contain IDs only, so always re-fetch the full record via `GET` before acting.\n\nKey event types:\n\n- `record.created`, `record.updated`, `record.deleted`\n- `list-entry.created`, `list-entry.updated`, `list-entry.deleted`\n- `note.created`\n- `task.created`, `task.completed`\n\nWebhook timeout is 5 seconds. Respond `200` immediately and do async processing in a follow-up swarm task.\n\n## Rate limits\n\n| Operation | Hard limit | Safe working rate |\n|---|---|---|\n| Reads | 100 req/sec | ~80 req/sec |\n| Writes | 25 req/sec | ~15-20 req/sec |\n\nAdd `sleep 0.05` between write calls in loops. Attio does not provide a native batch endpoint for these operations.\n\n## Operational rules\n\n- Upsert first. Use `PUT /records` with `matching_attribute` for create-or-update. `POST /records` can create duplicates.\n- Re-fetch webhook records. Webhook payloads are event hints, not full source-of-truth records.\n- Values are arrays. Every attribute value must be wrapped in an array.\n- No merge endpoint. Attio has no API-level record merge; dedupe agents should flag duplicates as comments or tasks for human review.\n- Check config first. Fetch `ATTIO_API_KEY` via `get-config includeSecrets=true`; never hardcode it.\n\n## Error handling\n\n| Status | Likely cause | Action |\n|---|---|---|\n| 401 | API key invalid or expired | Re-fetch from config. If still failing, notify Lead for rotation. |\n| 403 | Key lacks permission | Check the Attio API key\'s workspace permissions. |\n| 404 | Wrong object slug or record ID | Re-discover slugs with `GET /v2/objects`. |\n| 400 | Malformed body | Ensure attribute values are wrapped in arrays. |\n| 422 | Validation or conflict error | Read the `errors` array for field-level details. |\n| 429 | Rate-limited | Back off and retry after `Retry-After` if provided. |\n\n## Worked example: stale deal reactivation\n\n```bash\nATTIO_API_KEY=$(get-config key="ATTIO_API_KEY" includeSecrets=true)\n\nDEALS=$(curl -sS -X POST "https://api.attio.com/v2/objects/deals/records/query" \\\n -H "Authorization: Bearer $ATTIO_API_KEY" \\\n -H "Content-Type: application/json" \\\n -d \'{"filter": {"stage": {"$not_equal": "Won"}}, "limit": 200}\')\n\necho "$DEALS" | jq -r \'.data[] | .id.record_id\' | while read -r RECORD_ID; do\n RECORD=$(curl -sS "https://api.attio.com/v2/objects/deals/records/$RECORD_ID" \\\n -H "Authorization: Bearer $ATTIO_API_KEY" \\\n -H "Accept: application/json")\n # Compute staleness from the relevant date attribute. If stale, create a task.\n sleep 0.05\ndone\n```\n\n## Related references\n\n- Official Attio REST API docs: https://developers.attio.com/reference\n- Official Attio MCP overview: https://docs.attio.com/mcp/overview\n';
5904
+ var content_default5 = '# Attio Interaction (Read + Write)\n\nUse this skill to read and write your Attio CRM through the REST API v2. Every read or write is a direct API call; agent-swarm does not maintain a separate Attio sync.\n\n## TL;DR\n\n1. Resolve `ATTIO_API_KEY` from swarm config before making calls.\n2. Base URL: `https://api.attio.com/v2/`\n3. Use `Authorization: Bearer $ATTIO_API_KEY`, `Content-Type: application/json`, and `Accept: application/json`.\n4. Prefer upsert over create for People, Companies, and Deals: `PUT /v2/objects/{slug}/records` with `matching_attribute`.\n5. Rate limits: 100 reads/sec, 25 writes/sec. Pace write bursts to roughly 15-20/sec.\n6. Attribute values are arrays, even for scalar values: `[{ "value": 42 }]`, never `42`.\n\n## Authentication\n\n```bash\nATTIO_API_KEY=$(get-config key="ATTIO_API_KEY" includeSecrets=true)\n\ncurl -sS "https://api.attio.com/v2/objects" \\\n -H "Authorization: Bearer $ATTIO_API_KEY" \\\n -H "Accept: application/json"\n```\n\nIf calls return `401`, re-fetch the key from config. If it still fails, notify the Lead; the key may need rotation. Do not retry silently.\n\n## Core object slugs\n\n| Object | API slug | Primary matching attribute |\n|---|---|---|\n| Companies | `companies` | `domains` |\n| People | `people` | `email_addresses` |\n| Deals | `deals` | Usually no global dedupe key; link to company/person records |\n\nCustom objects use their configured API slug. Discover them with `GET /v2/objects`.\n\n## Common operations\n\n### 1. Discover objects and slugs\n\n```bash\ncurl -sS "https://api.attio.com/v2/objects" \\\n -H "Authorization: Bearer $ATTIO_API_KEY" \\\n -H "Accept: application/json" \\\n | jq \'.data[] | {slug: .api_slug, name: .title}\'\n```\n\n### 2. Query records with filters and pagination\n\n```bash\ncurl -sS -X POST "https://api.attio.com/v2/objects/companies/records/query" \\\n -H "Authorization: Bearer $ATTIO_API_KEY" \\\n -H "Content-Type: application/json" \\\n -d \'{\n "filter": {\n "stage": { "$not_equal": "Won" }\n },\n "limit": 100,\n "offset": 0\n }\' | jq \'.data[] | {record_id: .id.record_id, name: .values.name[0].value}\'\n```\n\nPaginate by increasing `offset` until `data` is empty. For no filter, omit the `filter` key.\n\n### 3. Get a single record\n\n```bash\ncurl -sS "https://api.attio.com/v2/objects/companies/records/{RECORD_ID}" \\\n -H "Authorization: Bearer $ATTIO_API_KEY" \\\n -H "Accept: application/json" \\\n | jq \'.data.values\'\n```\n\n### 4. Upsert a company by domain\n\nUse `PUT` with `matching_attribute`. It creates if not found and updates if found, so it is safe to call repeatedly.\n\n```bash\ncurl -sS -X PUT "https://api.attio.com/v2/objects/companies/records" \\\n -H "Authorization: Bearer $ATTIO_API_KEY" \\\n -H "Content-Type: application/json" \\\n -d \'{\n "data": {\n "values": {\n "name": [{ "value": "Acme Corp" }],\n "domains": [{ "domain": "acme.com" }],\n "employee_count": [{ "value": 150 }]\n }\n },\n "matching_attribute": "domains"\n }\' | jq \'{record_id: .data.id.record_id}\'\n```\n\n### 5. Upsert a person by email\n\n```bash\ncurl -sS -X PUT "https://api.attio.com/v2/objects/people/records" \\\n -H "Authorization: Bearer $ATTIO_API_KEY" \\\n -H "Content-Type: application/json" \\\n -d \'{\n "data": {\n "values": {\n "name": [{ "first_name": "Jane", "last_name": "Doe" }],\n "email_addresses": [{ "email_address": "jane@acme.com" }],\n "job_title": [{ "value": "CTO" }]\n }\n },\n "matching_attribute": "email_addresses"\n }\' | jq \'{record_id: .data.id.record_id}\'\n```\n\nAn upsert returns HTTP `200` when it updates an existing record and `201` when it creates one. The response body has the same shape in both cases.\n\n### 6. Update specific attributes on an existing record\n\n```bash\ncurl -sS -X PATCH "https://api.attio.com/v2/objects/companies/records/{RECORD_ID}" \\\n -H "Authorization: Bearer $ATTIO_API_KEY" \\\n -H "Content-Type: application/json" \\\n -d \'{\n "data": {\n "values": {\n "icp_score": [{ "value": 85 }],\n "icp_tier": [{ "value": "Tier 1" }]\n }\n }\n }\'\n```\n\n### 7. Write a note to a record\n\nNotes are concise free-text entries linked to a record. Use them to preserve useful interaction context, enrichment findings, or an agent result that should remain visible in the CRM.\n\n```bash\ncurl -sS -X POST "https://api.attio.com/v2/notes" \\\n -H "Authorization: Bearer $ATTIO_API_KEY" \\\n -H "Content-Type: application/json" \\\n -d \'{\n "data": {\n "parent_object": "companies",\n "parent_record_id": "{RECORD_ID}",\n "title": "Enrichment - 2026-06-02",\n "content": "Employee count: 150. Funding stage: Series A. Tech stack: Node.js, React."\n }\n }\' | jq \'{note_id: .data.id.note_id}\'\n```\n\n### 8. Create a task linked to a record\n\n```bash\ncurl -sS "https://api.attio.com/v2/workspace_members" \\\n -H "Authorization: Bearer $ATTIO_API_KEY" \\\n -H "Accept: application/json" \\\n | jq \'.data[] | {member_id: .id.workspace_member_id, name: .name, email: .email_address}\'\n\ncurl -sS -X POST "https://api.attio.com/v2/tasks" \\\n -H "Authorization: Bearer $ATTIO_API_KEY" \\\n -H "Content-Type: application/json" \\\n -d \'{\n "data": {\n "content": "Follow up - no contact in 21 days",\n "is_completed": false,\n "assignees": [\n { "referenced_actor_type": "workspace-member", "referenced_actor_id": "{MEMBER_ID}" }\n ],\n "linked_records": [\n { "target_object": "deals", "target_record_id": "{DEAL_RECORD_ID}" }\n ]\n }\n }\' | jq \'{task_id: .data.id.task_id}\'\n```\n\n### 9. Post a comment on a record\n\n```bash\ncurl -sS -X POST "https://api.attio.com/v2/comments" \\\n -H "Authorization: Bearer $ATTIO_API_KEY" \\\n -H "Content-Type: application/json" \\\n -d \'{\n "data": {\n "record": { "target_object": "companies", "target_record_id": "{RECORD_ID}" },\n "content": [\n { "type": "text", "text": "Possible duplicate of acme-corp-old - please review and merge." }\n ]\n }\n }\' | jq \'{comment_id: .data.id.comment_id}\'\n```\n\n### 10. Query a list or pipeline view\n\n```bash\ncurl -sS "https://api.attio.com/v2/lists" \\\n -H "Authorization: Bearer $ATTIO_API_KEY" \\\n -H "Accept: application/json" \\\n | jq \'.data[] | {list_id: .id.list_id, name: .title}\'\n\ncurl -sS -X POST "https://api.attio.com/v2/lists/{LIST_ID}/entries/query" \\\n -H "Authorization: Bearer $ATTIO_API_KEY" \\\n -H "Content-Type: application/json" \\\n -d \'{ "limit": 100, "offset": 0 }\' \\\n | jq \'.data[] | {entry_id: .id.entry_id, record_id: .record_id}\'\n```\n\n### 11. Global search across objects\n\n```bash\ncurl -sS -X POST "https://api.attio.com/v2/records/search" \\\n -H "Authorization: Bearer $ATTIO_API_KEY" \\\n -H "Content-Type: application/json" \\\n -d \'{ "query": "acme", "limit": 10 }\' \\\n | jq \'.data[] | {object: .object_type, record_id: .id.record_id}\'\n```\n\n## Webhooks\n\nAttio delivers webhooks at least once. Payloads contain IDs only, so always re-fetch the full record via `GET` before acting.\n\nKey event types:\n\n- `record.created`, `record.updated`, `record.deleted`\n- `list-entry.created`, `list-entry.updated`, `list-entry.deleted`\n- `note.created`\n- `task.created`, `task.completed`\n\nWebhook timeout is 5 seconds. Respond `200` immediately and do async processing in a follow-up swarm task.\n\n## Rate limits\n\n| Operation | Hard limit | Safe working rate |\n|---|---|---|\n| Reads | 100 req/sec | ~80 req/sec |\n| Writes | 25 req/sec | ~15-20 req/sec |\n\nAdd `sleep 0.05` between write calls in loops. Attio does not provide a native batch endpoint for these operations.\n\n## Operational rules\n\n- Upsert first. Use `PUT /records` with `matching_attribute` for create-or-update. `POST /records` can create duplicates.\n- Re-fetch webhook records. Webhook payloads are event hints, not full source-of-truth records.\n- Values are arrays. Every attribute value must be wrapped in an array.\n- No merge endpoint. Attio has no API-level record merge; dedupe agents should flag duplicates as comments or tasks for human review.\n- Check config first. Fetch `ATTIO_API_KEY` via `get-config includeSecrets=true`; never hardcode it.\n\n## Error handling\n\n| Status | Likely cause | Action |\n|---|---|---|\n| 401 | API key invalid or expired | Re-fetch from config. If still failing, notify Lead for rotation. |\n| 403 | Key lacks permission | Check the Attio API key\'s workspace permissions. |\n| 404 | Wrong object slug or record ID | Re-discover slugs with `GET /v2/objects`. |\n| 400 | Malformed body | Ensure attribute values are wrapped in arrays. |\n| 422 | Validation or conflict error | Read the `errors` array for field-level details. |\n| 429 | Rate-limited | Back off and retry after `Retry-After` if provided. |\n\n## Worked example: stale deal reactivation\n\n```bash\nATTIO_API_KEY=$(get-config key="ATTIO_API_KEY" includeSecrets=true)\n\nDEALS=$(curl -sS -X POST "https://api.attio.com/v2/objects/deals/records/query" \\\n -H "Authorization: Bearer $ATTIO_API_KEY" \\\n -H "Content-Type: application/json" \\\n -d \'{"filter": {"stage": {"$not_equal": "Won"}}, "limit": 200}\')\n\necho "$DEALS" | jq -r \'.data[] | .id.record_id\' | while read -r RECORD_ID; do\n RECORD=$(curl -sS "https://api.attio.com/v2/objects/deals/records/$RECORD_ID" \\\n -H "Authorization: Bearer $ATTIO_API_KEY" \\\n -H "Accept: application/json")\n # Compute staleness from the relevant date attribute. If stale, create a task.\n sleep 0.05\ndone\n```\n\n## Related references\n\n- Official Attio REST API docs: https://developers.attio.com/reference\n- Official Attio MCP overview: https://docs.attio.com/mcp/overview\n';
6261
5905
 
6262
5906
  // templates/skills/brainstorming/config.json
6263
- var config_default7 = `{
5907
+ var config_default6 = `{
6264
5908
  "category": "skills",
6265
5909
  "description": "Interactive exploration of ideas through Socratic Q&A. Produces progressive documents that serve as lightweight pre-PRDs feeding into research.",
6266
5910
  "displayName": "Brainstorming",
@@ -6281,7 +5925,7 @@ var config_default7 = `{
6281
5925
  `;
6282
5926
 
6283
5927
  // templates/skills/brainstorming/content.md
6284
- var content_default7 = `# Brainstorming
5928
+ var content_default6 = `# Brainstorming
6285
5929
 
6286
5930
  You are facilitating interactive exploration of ideas through Socratic Q&A. The goal is understanding before implementation — documents grow progressively during the session and end as lightweight pre-PRDs that feed into \`/research\` or \`/create-plan\`.
6287
5931
 
@@ -6423,6 +6067,26 @@ File-review is on by default:
6423
6067
  - Process feedback with the \`file-review:process-review\` skill
6424
6068
  `;
6425
6069
 
6070
+ // templates/skills/chat-sdk/config.json
6071
+ var config_default7 = `{
6072
+ "kind": "skill",
6073
+ "name": "chat-sdk",
6074
+ "displayName": "Chat SDK",
6075
+ "slug": "chat-sdk",
6076
+ "title": "Chat SDK",
6077
+ "description": "Chat threads from Telegram, Slack (Chat SDK), Discord, and Teams. Use when a task carries a task:chat-sdk contextKey or a user asked through Telegram, Slack, Discord, or Teams. Covers what the swarm posts for you, \`chat-reply\`, markdown and length per platform, attachments, \`chat-read-thread\` limits, and channel routes with \`register-chat-channel\`.",
6078
+ "version": "1.0.0",
6079
+ "category": "skills",
6080
+ "placeholders": [],
6081
+ "runAllSeedersCandidate": true,
6082
+ "systemDefault": false,
6083
+ "tags": ["chat", "telegram", "discord", "teams", "slack", "communication"]
6084
+ }
6085
+ `;
6086
+
6087
+ // templates/skills/chat-sdk/content.md
6088
+ var content_default7 = "# Chat SDK\n\nThe swarm receives messages from Telegram, Slack, Discord, and Microsoft Teams through the Chat SDK channel. A person mentions the bot or sends it a direct message, and the swarm creates a task for it. This skill tells you how to work on such a task.\n\n## How you know you are on a chat task\n\n- The task has `source: \"chat\"`, or it is a lead follow-up of one.\n- The task `contextKey` starts with `task:chat-sdk:`. The full form is `task:chat-sdk:<adapter>:<encoded thread id>`, for example `task:chat-sdk:telegram:555001`.\n- The task text holds the user's message. Later replies in the same thread arrive as follow-up tasks with the same `contextKey`, or as steering messages on your running task.\n- The user has four slash commands that never reach you as a task: `/help`, `/status`, `/cancel` and `/reset`. `/cancel` ends the thread's active tasks, yours included, with status `cancelled`. After `/reset` the next task has no parent task and no earlier thread context, so treat it as a new conversation.\n\nUse `get-task-details` to read the `contextKey` when you are not sure.\n\n## Reply with `chat-reply`\n\n`chat-reply { text }` posts to the thread your current task came from. It finds the thread from your task, so you do not pass ids.\n\n- Use it to ask the user a question you cannot answer yourself. Ask once, with all your questions together.\n- Use it for one short progress note when work takes more than a few minutes, such as \"Started. This takes about 10 minutes.\"\n- Do not use it for receipts (\"Got it\", \"On it\"), and do not send many progress notes.\n- It works only on a task with a `task:chat-sdk:` contextKey. A session bound to a non-chat task does not see `chat-reply` at all, because the tool is left out of its tool list.\n\n## The final summary is posted automatically\n\nWhen your task ends, the swarm posts the task output to the thread:\n\n- Completed: your `store-progress` output.\n- Failed: a \"Task failed.\" line, then the failure reason.\n- Cancelled: a \"Task cancelled.\" line, then the reason when there is one.\n\nDo not post your final answer with `chat-reply` as well. The user would get it twice. Put the answer in the `store-progress` output and stop.\n\nThe post is durable. If the platform is down, the swarm retries with backoff for up to 24 hours.\n\n## Markdown per platform\n\nWrite plain, short markdown: bold, italic, inline code, code blocks, bullet lists, and links. Avoid tables, nested lists, and HTML.\n\n| Platform | What renders | Limit per message |\n|---|---|---|\n| Telegram | Bold, italic, code, code blocks, links, lists. No tables. | 4096 characters |\n| Discord | Bold, italic, code, code blocks, lists, headings, links. No tables. | 2000 characters |\n| Slack | Converted to Slack mrkdwn: bold, italic, code, lists, links. Tables do not render well. | 4000 characters |\n| Teams | Basic markdown: bold, italic, code, lists, links. | 20000 characters |\n\nThe swarm splits longer text into several messages at line breaks. Long answers are hard to read in chat, so keep the answer short and put long material in an artifact or page, then post the link.\n\n## Attachments arrive as task attachments\n\nFiles and images the user sends in the thread are stored as attachments of the task. The task text names them. Download them with the `download-task-attachment` skill. You cannot send files back to the thread in this version. Share a link instead.\n\n## `chat-read-thread` limits per platform\n\n`chat-read-thread { threadId, limit? }` returns the most recent messages of a thread, oldest first. The thread id is the adapter name plus the URI-decoded remainder of the contextKey. Telegram ids have no escaping, so `telegram:555001` stays as it is. Slack, Discord, and Teams ids contain `%3A` in the contextKey, so do not build the id by hand. `chat-reply` resolves the thread itself. For `chat-read-thread`, use the thread id returned by `chat-reply`, `chat-post`, or `chat-open-dm`. You can read the thread of your current task. Any other thread needs lead privileges.\n\n- Telegram has no history API. The SDK keeps the Telegram thread history it saw in the swarm database, capped at 100 messages and 7 days.\n- Teams needs the `Chat.Read.*` Graph permission on the bot. Without it, the call fails.\n- Slack and Discord return the platform history.\n\nThe task text already holds the earlier swarm messages of the thread, so read the thread only when you need more.\n\n## Other tools\n\n- `chat-post { adapter, threadId | channelId, text }` posts outside your task's thread. Lead only. Use it only when the task asks you to message another thread or channel.\n- `chat-open-dm { adapter, userId }` returns the direct-message thread id for a user. Lead only. Then post to it with `chat-post`.\n\n## Leads: `register-chat-channel`\n\nBy default every message addressed to the swarm goes to the lead. A lead can route one channel to a fixed agent or workflow:\n\n```\nregister-chat-channel { adapter: \"telegram\", channelId: \"telegram:-100123\", agentId: \"<agent id>\" }\nregister-chat-channel { adapter: \"slack\", channelId: \"slack:C123\", workflowId: \"<workflow id>\" }\nunregister-chat-channel { adapter: \"telegram\", channelId: \"telegram:-100123\" }\n```\n\nGive exactly one of `agentId` or `workflowId`. A `swarm#<agentId>` token in a message still wins over the route. A workflow mapping fires only for a bot mention or a DM; a plain reply in the subscribed thread does not start another run. Only the lead can call these tools.\n\n## Etiquette\n\n- Keep messages short. Chat is read on phones.\n- Never post secrets, tokens, internal URLs with credentials, or private data from other users. The swarm scrubs known secrets, but do not depend on it.\n- When you ask a question in a group, start with the user's name so they see it.\n- One question message is better than several.\n";
6089
+
6426
6090
  // templates/skills/code-quality/config.json
6427
6091
  var config_default8 = `{
6428
6092
  "kind": "skill",
@@ -6908,7 +6572,7 @@ var config_default12 = `{
6908
6572
  `;
6909
6573
 
6910
6574
  // templates/skills/composio-gmail/content.md
6911
- var content_default12 = '# Composio · Gmail\n\nToolkit slug: **`gmail`**. Read the [[composio]] hub first for the call model\n(`agent-swarm x composio …`, user_id, connected accounts, the 4302 gotcha).\nTool `arguments` go inside the request body; `user_id` defaults to `"me"` (the\nauthorized account) — you usually don\'t need to set it.\n\n```bash\n# Direct execute (reliable path — pin the ACTIVE ca_… from the hub Recipe B)\nagent-swarm x composio POST /tools/execute/<SLUG> \\\n --body \'{"user_id":"<connected-account-email>","connected_account_id":"<active-connected-account-id>","arguments":{ … }}\'\n```\n\nFollow the hub\'s **Resolve the user and connected account** procedure, then\nselect the resolved user\'s `gmail` account whose status is `ACTIVE`.\n\n## Headline tools\n\n| Slug | What | Key args |\n|---|---|---|\n| `GMAIL_FETCH_EMAILS` | List/search emails | `query`, `max_results` (def **1** — set it!), `include_payload` (def true), `verbose` (def true), `ids_only`, `label_ids`, `page_token` |\n| `GMAIL_FETCH_MESSAGE_BY_MESSAGE_ID` | Full single message | `message_id`, `include_payload` |\n| `GMAIL_FETCH_MESSAGE_BY_THREAD_ID` | All messages in a thread | `thread_id` |\n| `GMAIL_LIST_THREADS` | List threads | `query`, `max_results`, `page_token` |\n| `GMAIL_SEND_EMAIL` | Send | `recipient_email`, `subject`, `body`, `is_html` (def false), `cc`, `bcc`, `extra_recipients`, `attachment` |\n| `GMAIL_REPLY_TO_THREAD` | Reply in-thread | `thread_id`, `message_body`, `recipient_email` |\n| `GMAIL_CREATE_EMAIL_DRAFT` / `GMAIL_SEND_DRAFT` | Draft then send | `body`, `subject`, `recipient_email` / `draft_id` |\n| `GMAIL_GET_PROFILE` | Whose mailbox is this? | — |\n| `GMAIL_LIST_LABELS` / `GMAIL_CREATE_LABEL` / `GMAIL_ADD_LABEL_TO_EMAIL` | Labels | `message_id`, `label_ids` (use LIST_LABELS for custom IDs) |\n| `GMAIL_GET_CONTACTS` / `GMAIL_SEARCH_PEOPLE` | Contacts | `query` |\n| `GMAIL_GET_ATTACHMENT` | Download attachment | `message_id`, `attachment_id` |\n\nFull set: 63 tools — list with\n`agent-swarm x composio GET "/tools?toolkit_slug=gmail&limit=100" | jq -r \'.items[]|"\\(.slug)\\t\\(.name)"\'`.\nAvoid the ones marked Deprecated (`GMAIL_LIST_MESSAGES`, `GMAIL_REMOVE_LABEL`).\n\n## Read recipe (metadata-first)\n\n```bash\nagent-swarm x composio POST /tools/execute/GMAIL_FETCH_EMAILS \\\n --body \'{"connected_account_id":"ca_…","arguments":{"max_results":5,"include_payload":false,"verbose":false}}\'\n```\n- **Always set `max_results`** — the default is `1`.\n- Use Gmail `query` syntax: `"is:unread"`, `"from:foo@bar.com newer_than:7d"`,\n `"subject:invoice has:attachment"`.\n- Keep `include_payload:false` + `verbose:false` unless the user needs full bodies\n (token-heavy). Use `ids_only:true` for the cheapest listing.\n\n## Send recipe\n\n```bash\nagent-swarm x composio POST /tools/execute/GMAIL_SEND_EMAIL \\\n --body \'{"connected_account_id":"ca_…","arguments":{\n "recipient_email":"someone@example.com",\n "subject":"Hello",\n "body":"<p>Hi there</p>",\n "is_html":true,\n "cc":["cc@example.com"]\n }}\'\n```\n- Set `is_html:true` when `body` contains HTML, otherwise it sends as literal text.\n- `recipient_email` is the primary; add more via `extra_recipients` / `cc` / `bcc`.\n- **Sending is a write action** — only do it when the task explicitly asks.\n\n## Reply in a thread\n\n```bash\nagent-swarm x composio POST /tools/execute/GMAIL_REPLY_TO_THREAD \\\n --body \'{"connected_account_id":"ca_…","arguments":{\n "thread_id":"<thread_id>","recipient_email":"someone@example.com","message_body":"thanks!"\n }}\'\n```\n\n## Gotchas\n\n- Default `max_results` is `1` — forgetting it makes "list my emails" return a\n single message.\n- Bodies/attachments are token-heavy and may contain secrets — default to\n metadata; the secret-scrubber doesn\'t run on Composio tool output.\n- If you get `ToolRouterV2_NoActiveConnection`, switch to direct execute with the\n pinned `connected_account_id` (hub Gotchas).\n';
6575
+ var content_default12 = '# Composio · Gmail\n\nToolkit slug: **`gmail`**. Read the [[composio]] hub first for the call model\n(`agent-swarm x composio …`, user_id, connected accounts, the 4302 gotcha).\nTool `arguments` go inside the request body; `user_id` defaults to `"me"` (the\nauthorized account) — you usually don\'t need to set it.\n\n```bash\n# Direct execute (reliable path — pin the ACTIVE ca_… from the hub Recipe B)\nagent-swarm x composio POST /tools/execute/<SLUG> \\\n --body \'{"user_id":"<connected-account-email>","connected_account_id":"<active-connected-account-id>","arguments":{ … }}\'\n```\n\nFollow the hub\'s **Resolve the user and connected account** procedure, then\nselect the resolved user\'s `gmail` account whose status is `ACTIVE`.\n\n## Headline tools\n\n| Slug | What | Key args |\n|---|---|---|\n| `GMAIL_FETCH_EMAILS` | List/search emails | `query`, `max_results` (def **1** — set it!), `include_payload` (def true), `verbose` (def true), `ids_only`, `label_ids`, `page_token` |\n| `GMAIL_FETCH_MESSAGE_BY_MESSAGE_ID` | Full single message | `message_id`, `include_payload` |\n| `GMAIL_FETCH_MESSAGE_BY_THREAD_ID` | All messages in a thread | `thread_id` |\n| `GMAIL_LIST_THREADS` | List threads | `query`, `max_results`, `page_token` |\n| `GMAIL_SEND_EMAIL` | Send | `recipient_email`, `subject`, `body`, `is_html` (def false), `cc`, `bcc`, `extra_recipients`, `attachment` |\n| `GMAIL_REPLY_TO_THREAD` | Reply in-thread | `thread_id`, `message_body`, `recipient_email` |\n| `GMAIL_CREATE_EMAIL_DRAFT` / `GMAIL_SEND_DRAFT` | Draft then send | `body`, `subject`, `recipient_email` / `draft_id` |\n| `GMAIL_GET_PROFILE` | Whose mailbox is this? | — |\n| `GMAIL_LIST_LABELS` / `GMAIL_CREATE_LABEL` / `GMAIL_ADD_LABEL_TO_EMAIL` | Labels | `message_id`, `label_ids` (use LIST_LABELS for custom IDs) |\n| `GMAIL_GET_CONTACTS` / `GMAIL_SEARCH_PEOPLE` | Contacts | `query` |\n| `GMAIL_GET_ATTACHMENT` | Download attachment | `message_id`, `attachment_id` |\n\nFull set: 63 tools — list with\n`agent-swarm x composio GET "/tools?toolkit_slug=gmail&limit=100" | jq -r \'.items[]|"\\(.slug)\\t\\(.name)"\'`.\nAvoid the ones marked Deprecated (`GMAIL_LIST_MESSAGES`, `GMAIL_REMOVE_LABEL`).\n\n## Read recipe (metadata-first)\n\n```bash\nagent-swarm x composio POST /tools/execute/GMAIL_FETCH_EMAILS \\\n --body \'{"connected_account_id":"ca_…","arguments":{"max_results":5,"include_payload":false,"verbose":false}}\'\n```\n- **Always set `max_results`** — the default is `1`.\n- Use Gmail `query` syntax: `"is:unread"`, `"from:foo@bar.com newer_than:7d"`,\n `"subject:invoice has:attachment"`.\n- Keep `include_payload:false` + `verbose:false` unless the user needs full bodies\n (token-heavy). Use `ids_only:true` for the cheapest listing.\n\n## Send recipe\n\n```bash\nagent-swarm x composio POST /tools/execute/GMAIL_SEND_EMAIL \\\n --body \'{"connected_account_id":"ca_…","arguments":{\n "recipient_email":"someone@example.com",\n "subject":"Hello",\n "body":"<p>Hi there</p>",\n "is_html":true,\n "cc":["cc@example.com"]\n }}\'\n```\n- Set `is_html:true` when `body` contains HTML, otherwise it sends as literal text.\n- `recipient_email` is the primary; add more via `extra_recipients` / `cc` / `bcc`.\n- **Sending is a write action** — only do it when the task explicitly asks.\n\n## Sender (From) and footer\n\n- A draft defaults to the connected account\'s primary address. Before you create drafts, list the account\'s verified send-as aliases and set From to the alias the requester prefers (requester profile or brief). Take the footer and signature domain from the same source.\n- Never set From to an alias that is not verified. Gmail rewrites it to the primary address.\n- After you create the drafts, check From as well as To and Subject on each one.\n- To fix existing drafts, update each draft in place with the alias. Draft ids stay the same, so there is no need to delete and recreate.\n\n## Reply in a thread\n\n```bash\nagent-swarm x composio POST /tools/execute/GMAIL_REPLY_TO_THREAD \\\n --body \'{"connected_account_id":"ca_…","arguments":{\n "thread_id":"<thread_id>","recipient_email":"someone@example.com","message_body":"thanks!"\n }}\'\n```\n\n## Gotchas\n\n- Default `max_results` is `1` — forgetting it makes "list my emails" return a\n single message.\n- Bodies/attachments are token-heavy and may contain secrets — default to\n metadata; the secret-scrubber doesn\'t run on Composio tool output.\n- If you get `ToolRouterV2_NoActiveConnection`, switch to direct execute with the\n pinned `connected_account_id` (hub Gotchas).\n';
6912
6576
 
6913
6577
  // templates/skills/composio-google-calendar/config.json
6914
6578
  var config_default13 = `{
@@ -6948,7 +6612,7 @@ var config_default14 = `{
6948
6612
  `;
6949
6613
 
6950
6614
  // templates/skills/composio-google-docs/content.md
6951
- var content_default14 = '# Composio · Google Docs\n\nToolkit slug: **`googledocs`**. Read the [[composio]] hub first for the call model.\nA document is identified by its `document_id` (the id in the Docs URL).\n\n```bash\nagent-swarm x composio POST /tools/execute/<SLUG> \\\n --body \'{"user_id":"<connected-account-email>","connected_account_id":"<active-connected-account-id>","arguments":{ … }}\'\n```\n\nFollow the hub\'s **Resolve the user and connected account** procedure, then\nselect the resolved user\'s `googledocs` account whose status is `ACTIVE`.\n\n## Headline tools\n\n| Slug | What | Key args |\n|---|---|---|\n| `GOOGLEDOCS_SEARCH_DOCUMENTS` | Find docs (Drive search) | `query`, `max_results` (def 10), `order_by` (def `modifiedTime desc`), `modified_after`, `created_after`, `starred_only`, `shared_with_me`, `response_detail` (def `minimal`) |\n| `GOOGLEDOCS_GET_DOCUMENT_PLAINTEXT` | Read doc as text | **`document_id`**, `include_tables` (def true), `include_headers`, `include_footers`, `include_footnotes`, `include_tabs_content` |\n| `GOOGLEDOCS_GET_DOCUMENT_BY_ID` | Full structured doc JSON | `document_id` |\n| `GOOGLEDOCS_CREATE_DOCUMENT` | Create blank/with text | `title`, `text` |\n| `GOOGLEDOCS_CREATE_DOCUMENT_MARKDOWN` | Create from markdown | **`title`**, `markdown_text`, `image_assets` |\n| `GOOGLEDOCS_UPDATE_DOCUMENT_MARKDOWN` | Replace body with markdown | `document_id`, `markdown_text` |\n| `GOOGLEDOCS_INSERT_TEXT_ACTION` | Insert text at index | `document_id`, `text`, `index` |\n| `GOOGLEDOCS_REPLACE_ALL_TEXT` | Find & replace | `document_id`, `find`, `replace` |\n| `GOOGLEDOCS_COPY_DOCUMENT` | Duplicate a doc | `document_id`, `title` |\n| `GOOGLEDOCS_EXPORT_DOCUMENT_AS_PDF` | Export to PDF | `document_id` |\n\nFull set: 35 tools — `agent-swarm x composio GET "/tools?toolkit_slug=googledocs&limit=100" | jq -r \'.items[]|"\\(.slug)\\t\\(.name)"\'`.\nPrefer the `*_MARKDOWN` create/update tools for authoring; the granular\n`INSERT_*`/`DELETE_*`/table tools are for surgical structural edits.\n\n## Search recipe\n\n```bash\nagent-swarm x composio POST /tools/execute/GOOGLEDOCS_SEARCH_DOCUMENTS \\\n --body \'{"connected_account_id":"ca_…","arguments":{"query":"workshop","max_results":5}}\'\n# → results under .data.files[] (id, name, modifiedTime …)\n```\n- Results are Drive file entries at **`.data.files[]`** — grab `.id` to read.\n- `order_by` defaults to `modifiedTime desc` (most recent first).\n- Use `modified_after` / `shared_with_me` to narrow.\n\n## Read recipe\n\n```bash\nagent-swarm x composio POST /tools/execute/GOOGLEDOCS_GET_DOCUMENT_PLAINTEXT \\\n --body \'{"connected_account_id":"ca_…","arguments":{"document_id":"<id>","include_tables":true}}\'\n```\nUse `GET_DOCUMENT_PLAINTEXT` for reading content; only reach for\n`GET_DOCUMENT_BY_ID` when you need the structured JSON (styles, indices) for an\nedit.\n\n## Create-from-markdown recipe\n\n```bash\nagent-swarm x composio POST /tools/execute/GOOGLEDOCS_CREATE_DOCUMENT_MARKDOWN \\\n --body \'{"connected_account_id":"ca_…","arguments":{\n "title":"Weekly updates","markdown_text":"# Heading\\n\\n- point one\\n- point two"\n }}\'\n```\n\n## Google Doc to agent-fs\n\n1. Read the source with `GOOGLEDOCS_GET_DOCUMENT_PLAINTEXT` and retain its\n returned title and display URL as provenance.\n2. Write the converted content to your own agent-fs namespace with a short\n provenance header naming the source document and import method.\n3. Resolve the destination org and drive from the environment or\n `agent-fs stat <path> --json`; the `artifacts` skill documents the canonical\n storage and share-link workflow. Never copy org or drive IDs from an example.\n4. Read the stored file back and assert several markers: the title, a middle\n phrase, and the final sentence. A successful byte-count response alone does\n not prove fidelity.\n\nPlaintext is intentionally lossy. Inline images may become bare `[IMAGE]`\nmarkers, and headings may be flattened into adjacent paragraphs. If those\nfeatures matter, use `GOOGLEDOCS_GET_DOCUMENT_BY_ID` for structured JSON or a\nGoogle Drive export, then preserve or reconstruct the structure explicitly.\nThere is no `GET_DOCUMENT_MARKDOWN` tool; markdown exists on create/update, not\non read.\n\n## Gotchas\n\n- Search returns Drive metadata, not document content — do a second\n `GET_DOCUMENT_PLAINTEXT` call with the `.id` to read.\n- Doc bodies can be long/token-heavy and may contain secrets — read only what you\n need (`include_headers/footers/footnotes` default off for a reason).\n- Create/update/replace are **write actions** — only on explicit request.\n';
6615
+ var content_default14 = '# Composio · Google Docs\n\nToolkit slug: **`googledocs`**. Read the [[composio]] hub first for the call model.\nA document is identified by its `document_id` (the id in the Docs URL).\n\n```bash\nagent-swarm x composio POST /tools/execute/<SLUG> \\\n --body \'{"user_id":"<connected-account-email>","connected_account_id":"<active-connected-account-id>","arguments":{ … }}\'\n```\n\nFollow the hub\'s **Resolve the user and connected account** procedure, then\nselect the resolved user\'s `googledocs` account whose status is `ACTIVE`.\n\n## Headline tools\n\n| Slug | What | Key args |\n|---|---|---|\n| `GOOGLEDOCS_SEARCH_DOCUMENTS` | Find docs (Drive search) | `query`, `max_results` (def 10), `order_by` (def `modifiedTime desc`), `modified_after`, `created_after`, `starred_only`, `shared_with_me`, `response_detail` (def `minimal`) |\n| `GOOGLEDOCS_GET_DOCUMENT_PLAINTEXT` | Read doc as text | **`document_id`**, `include_tables` (def true), `include_headers`, `include_footers`, `include_footnotes`, `include_tabs_content` |\n| `GOOGLEDOCS_GET_DOCUMENT_BY_ID` | Full structured doc JSON | `document_id` |\n| `GOOGLEDOCS_CREATE_DOCUMENT` | Create blank/with text | `title`, `text` |\n| `GOOGLEDOCS_CREATE_DOCUMENT_MARKDOWN` | Create from markdown | **`title`**, `markdown_text`, `image_assets` |\n| `GOOGLEDOCS_UPDATE_DOCUMENT_MARKDOWN` | Replace body with markdown | `document_id`, `markdown_text` |\n| `GOOGLEDOCS_INSERT_TEXT_ACTION` | Insert text at index | `document_id`, `text`, `index` |\n| `GOOGLEDOCS_REPLACE_ALL_TEXT` | Find & replace | `document_id`, `find`, `replace` |\n| `GOOGLEDOCS_COPY_DOCUMENT` | Duplicate a doc | `document_id`, `title` |\n| `GOOGLEDOCS_EXPORT_DOCUMENT_AS_PDF` | Export to PDF | `document_id` |\n\nFull set: 35 tools — `agent-swarm x composio GET "/tools?toolkit_slug=googledocs&limit=100" | jq -r \'.items[]|"\\(.slug)\\t\\(.name)"\'`.\nPrefer the `*_MARKDOWN` create/update tools for authoring; the granular\n`INSERT_*`/`DELETE_*`/table tools are for surgical structural edits.\n\n## Search recipe\n\n```bash\nagent-swarm x composio POST /tools/execute/GOOGLEDOCS_SEARCH_DOCUMENTS \\\n --body \'{"connected_account_id":"ca_…","arguments":{"query":"workshop","max_results":5}}\'\n# → results under .data.files[] (id, name, modifiedTime …)\n```\n- Results are Drive file entries at **`.data.files[]`** — grab `.id` to read.\n- `order_by` defaults to `modifiedTime desc` (most recent first).\n- Use `modified_after` / `shared_with_me` to narrow.\n\n## Read recipe\n\n```bash\nagent-swarm x composio POST /tools/execute/GOOGLEDOCS_GET_DOCUMENT_PLAINTEXT \\\n --body \'{"connected_account_id":"ca_…","arguments":{"document_id":"<id>","include_tables":true}}\'\n```\nUse `GET_DOCUMENT_PLAINTEXT` for reading content; only reach for\n`GET_DOCUMENT_BY_ID` when you need the structured JSON (styles, indices) for an\nedit.\n\n## Create-from-markdown recipe\n\n```bash\nagent-swarm x composio POST /tools/execute/GOOGLEDOCS_CREATE_DOCUMENT_MARKDOWN \\\n --body \'{"connected_account_id":"ca_…","arguments":{\n "title":"Weekly updates","markdown_text":"# Heading\\n\\n- point one\\n- point two"\n }}\'\n```\n\n## Google Doc to agent-fs\n\n1. Read the source with `GOOGLEDOCS_GET_DOCUMENT_PLAINTEXT` and retain its\n returned title and display URL as provenance.\n2. Write the converted content to your own agent-fs namespace with a short\n provenance header naming the source document and import method.\n3. Resolve the destination org and drive from the environment or\n `agent-fs stat <path> --json`; the `agent-fs` skill documents the canonical\n storage and share-link workflow. Never copy org or drive IDs from an example.\n4. Read the stored file back and assert several markers: the title, a middle\n phrase, and the final sentence. A successful byte-count response alone does\n not prove fidelity.\n\nPlaintext is intentionally lossy. Inline images may become bare `[IMAGE]`\nmarkers, and headings may be flattened into adjacent paragraphs. If those\nfeatures matter, use `GOOGLEDOCS_GET_DOCUMENT_BY_ID` for structured JSON or a\nGoogle Drive export, then preserve or reconstruct the structure explicitly.\nThere is no `GET_DOCUMENT_MARKDOWN` tool; markdown exists on create/update, not\non read.\n\n## Gotchas\n\n- Search returns Drive metadata, not document content — do a second\n `GET_DOCUMENT_PLAINTEXT` call with the `.id` to read.\n- Doc bodies can be long/token-heavy and may contain secrets — read only what you\n need (`include_headers/footers/footnotes` default off for a reason).\n- Create/update/replace are **write actions** — only on explicit request.\n';
6952
6616
 
6953
6617
  // templates/skills/db-query-guidance/config.json
6954
6618
  var config_default15 = `{
@@ -7141,8 +6805,159 @@ var config_default18 = `{
7141
6805
  // templates/skills/download-task-attachment/content.md
7142
6806
  var content_default18 = '# Download a Task Attachment\n\nTask attachments (files uploaded via the UI or API onto a task) are served\nthrough a single **provider-agnostic** REST route — regardless of whether the\nbacking storage is `local-fs` or `agent-fs`. You do not need to know or care\nwhich provider is active, and you do not need `agent-fs org`/`agent-fs drive`\ndiscovery.\n\n## The one-call recipe\n\n```bash\ncurl "$MCP_BASE_URL/api/fs/tasks/$AGENT_SWARM_TASK_ID/files/<attachmentId>/raw" \\\n -H "X-Agent-ID: $AGENT_ID" \\\n -H "Authorization: Bearer ${AGENT_SWARM_API_KEY:-$API_KEY}" \\\n --create-dirs -o "/tmp/attachments/<attachmentId>/<name>"\n```\n\n- `$MCP_BASE_URL`, `$AGENT_ID`, `$AGENT_SWARM_TASK_ID` (or `$AGENT_SWARM_AGENT_ID`/`TASK_FILE`-derived taskId), and `$AGENT_SWARM_API_KEY`/`$API_KEY` are already present in every worker container\'s env — no new plumbing needed.\n- `<attachmentId>` and `<name>`/`mimeType` come from the task\'s attachment list (see below).\n- One directory per attachment: two attachments can share a name (every pasted screenshot is `image.png`), and `/tmp/<name>` would let one overwrite the other.\n- The route resolves the active file-storage provider (`local-fs` in dev, `agent-fs` in prod) server-side and streams the raw bytes back with the correct `Content-Type` — you get one call no matter which provider is behind it.\n\n## Finding the attachment ID\n\nIf the dispatch prompt already injected an `## Attachments` section with the\ncurl command pre-filled, just run it — that\'s the fast path (see\n`buildAttachmentsSection()` in `src/commands/runner.ts`).\n\nOtherwise, pull it from the task:\n\n```\nget-task-details taskId=<your task id>\n```\n\nThe response includes an `attachments` array: `{id, name, mimeType, sizeBytes}`.\nUse `id` as `<attachmentId>` in the curl above.\n\n## Why not the `agent-fs` CLI directly?\n\n`agent-fs` is a general-purpose CLI for the swarm\'s own agent-fs filesystem —\nit requires you to already know the org/drive a file lives in, and task\nattachments don\'t always carry that (older rows, or attachments stored via\n`local-fs` in dev have no org/drive at all). Reaching for `agent-fs cat` /\n`agent-fs download` on a task attachment means guessing subcommand names,\ndiscovering the right org via `agent-fs org list`, and re-trying — several\ntool calls where one `curl` suffices. This was root-caused from a real\nsession that burned 7 tool calls (~64s) doing exactly that detour before\nsucceeding; see `runbooks/harness-providers.md` and PR that added\n`buildAttachmentsSection()` for the full trace.\n\n## Gotchas\n\n- The route requires `X-Agent-ID` + bearer auth like any other swarm API call — same headers you\'d use for any MCP-adjacent REST call.\n- `mimeType` on the attachment record reflects the real upload `Content-Type` (not a filename-extension guess) — trust it when deciding how to handle the downloaded bytes (image vs PDF vs text).\n- If you get a 404, double-check you\'re using the **attachment ID** (from `attachments[].id`), not the display `name`.\n';
7143
6807
 
7144
- // templates/skills/engineering-standards/config.json
6808
+ // templates/skills/email-domain-health/config.json
7145
6809
  var config_default19 = `{
6810
+ "kind": "skill",
6811
+ "name": "email-domain-health",
6812
+ "displayName": "Email Domain Health",
6813
+ "slug": "email-domain-health",
6814
+ "title": "Email Domain Health",
6815
+ "description": "Build a scheduled email domain health monitor: ask the user for domains, providers, test inbox, schedule and alert rules, then build a checks script (SPF, DKIM, DMARC, MX, provider records, blocklists with positive controls), a daily test send with score capture, bounce rate, run history in a Swarm App, and alerts only on a new red or a score drop. Orchestrates the swarm-scripts, apps, scheduling and composio-gmail skills.",
6816
+ "version": "1.0.0",
6817
+ "category": "skills",
6818
+ "placeholders": [],
6819
+ "runAllSeedersCandidate": true,
6820
+ "systemDefault": true,
6821
+ "tags": ["email", "deliverability", "dns", "monitoring", "apps"]
6822
+ }
6823
+ `;
6824
+
6825
+ // templates/skills/email-domain-health/content.md
6826
+ var content_default19 = `# Email Domain Health
6827
+
6828
+ Build a monitor that tells a person, every working day, whether mail from their
6829
+ domains authenticates, lands, and stays off blocklists. It has four parts: one
6830
+ saved script (checks + test send), a Swarm App with run history, one or two
6831
+ schedules, and an alert that fires only when something gets worse.
6832
+
6833
+ This skill decides **what** to build. The mechanics live in other skills; read
6834
+ them, do not re-derive them:
6835
+
6836
+ - [[swarm-scripts]]: authoring, \`script-upsert\`, scope, connections, secrets.
6837
+ - [[apps]]: models, named queries, pages, row writes.
6838
+ - [[scheduling]]: \`targetType: "script"\` schedules, cron and timezone.
6839
+ - [[composio-gmail]] (or the user's own mail integration): the test send and
6840
+ the bounce search.
6841
+
6842
+ Full design notes and pitfalls: the "Email domain health" playbook on the docs
6843
+ site.
6844
+
6845
+ ## 1. Ask for requirements first
6846
+
6847
+ Ask before building (one batched question, see [[ask-user]]). Do not guess any
6848
+ of these; a wrong selector or From address makes every later check wrong.
6849
+
6850
+ | Ask | Why it matters |
6851
+ |---|---|
6852
+ | Domains to monitor, and which one is the envelope/bounce domain | One check set per domain; SPF is judged on the envelope domain. |
6853
+ | Sending providers per domain (Google Workspace, Microsoft 365, an ESP such as Resend, SES, Postmark) and each DKIM selector | DKIM is looked up at \`<selector>._domainkey.<domain>\`; there is no way to list selectors. |
6854
+ | The From address for the test send, and the integration that sends it (Composio Gmail, SMTP, ESP API) | The test must leave through the real path, alias included. |
6855
+ | Test inbox: mail-tester (free tier has a small daily cap) or an inbox the user owns | Decides how the score is captured. |
6856
+ | Schedule and timezone | Default: weekdays, morning, user's timezone. |
6857
+ | Alert channel and thresholds (score floor, bounce rate) | Defaults: score < 6/10, bounces >= 5% on >= 10 sends. |
6858
+ | History retention | Default 90 days of runs. |
6859
+
6860
+ Read the answers back in one short message before you write code.
6861
+
6862
+ ## 2. Build the checks script
6863
+
6864
+ One saved script with modes \`send\`, \`check\`, and optionally \`backfill\`. Keep
6865
+ domains, selectors, thresholds and ids in a config block at the top or in
6866
+ \`args\`, never scattered through the code.
6867
+
6868
+ **DNS over HTTPS, two resolvers.** Query every record on two public DoH
6869
+ resolvers (for example dns.google and Cloudflare's JSON endpoint).
6870
+ NOERROR and NXDOMAIN are answers; SERVFAIL, REFUSED and timeouts are not.
6871
+ Same verdict on both: use it. Different verdicts: \`warn\` ("resolvers
6872
+ disagree", often propagation). One failed: use the other, note it. Both
6873
+ failed: \`unknown\`.
6874
+
6875
+ **Checks per domain:**
6876
+
6877
+ - SPF: exactly one \`v=spf1\` record, includes each provider, total DNS lookups
6878
+ <= 10 (recurse \`include:\`/\`redirect=\`; warn at 9), ends \`~all\` or \`-all\`.
6879
+ - DKIM: TXT at each selector (follow a CNAME), non-empty \`p=\` (empty means
6880
+ revoked), key length.
6881
+ - DMARC: exactly one \`v=DMARC1\` at \`_dmarc.<domain>\`, a \`p=\` tag, \`rua=\` (warn
6882
+ without it).
6883
+ - MX: present and pointing at the expected provider.
6884
+ - Provider records the ESP documents (bounce subdomain SPF and MX, DKIM
6885
+ CNAMEs).
6886
+ - Blocklists: domain lists such as Spamhaus DBL, SURBL, URIBL (details below).
6887
+ - Test-send result: score plus the SPF, DKIM and DMARC verdicts from the
6888
+ receiver's headers.
6889
+ - Bounce rate: sent vs bounced in the last 24h from the sending mailbox,
6890
+ excluding test sends.
6891
+
6892
+ **Verdicts:** \`ok\`, \`warn\`, \`red\`, \`unknown\`. Overall = worst. \`unknown\` means
6893
+ "could not read", and it is **never** reported as clean.
6894
+
6895
+ **Controls, not hope.** Every negative result needs a positive control in the
6896
+ same run:
6897
+
6898
+ - Blocklists: before trusting a list, query its documented test entry
6899
+ (\`dbltest.com\` on \`dbl.spamhaus.org\`, \`test.surbl.org\` on \`multi.surbl.org\`,
6900
+ \`test.uribl.com\` on \`multi.uribl.com\`) on each resolver. Use the list only on
6901
+ a resolver where the test entry answers "listed". Refusal codes
6902
+ (\`127.255.255.x\` from Spamhaus, \`127.0.0.1\` from SURBL and URIBL) mean the
6903
+ resolver is blocked: \`unknown\`.
6904
+ - Bounce rate: a whole-mailbox 24h count of 0 means the search failed:
6905
+ \`unknown\`, not "0 bounces".
6906
+ - Test result page without a score yet: \`unknown\` for the first hour after the
6907
+ send, then \`red\`.
6908
+
6909
+ Return a \`controls.ok\` flag with the result so a run that read nothing cannot
6910
+ pass as green.
6911
+
6912
+ **Send mode.** Send one realistic message (the same template and links real
6913
+ mail uses) to a fresh test address, through the user's integration. Read the
6914
+ sent message back and confirm the From header kept the address the user asked
6915
+ for. Store the pending test id in KV with a per-day key so a retry never
6916
+ sends twice.
6917
+
6918
+ ## 3. Build the app
6919
+
6920
+ Use [[apps]]. Minimum models:
6921
+
6922
+ - \`run\`: one row per run (time, overall, red/warn/unknown counts, score,
6923
+ bounces, sent).
6924
+ - \`check\`: one row per check per run (history).
6925
+ - \`current\`: one row per \`domain|check\`, updated in place, with \`since\` = when
6926
+ the status last changed.
6927
+
6928
+ Optional per-domain views (\`domainRun\`, \`domainCheck\`) make tabs per domain
6929
+ cheap to render. Page: one tab per domain (problems first, raw record on
6930
+ hover), a score trend, and a collapsed "How it works". The script writes rows
6931
+ through the row endpoints; prune \`run\` and \`check\` rows older than the
6932
+ retention window.
6933
+
6934
+ ## 4. Schedule it
6935
+
6936
+ Use [[scheduling]] with \`targetType: "script"\`, not an agent task that says
6937
+ "run the script". Two schedules: \`send\`, then \`check\` 10 to 15 minutes later
6938
+ in the user's timezone, so the test inbox has the message before the check
6939
+ reads it.
6940
+
6941
+ ## 5. Alert only on change
6942
+
6943
+ Compare with the previous \`current\` rows. Alert when a check **turns** red, or
6944
+ the score drops below the floor after being above it. Never alert on a
6945
+ steady-state warn or an unknown. The message lists each new red with its raw
6946
+ detail and links the app. If the script's identity cannot post to the chosen
6947
+ channel, have it hand the message to an agent that can.
6948
+
6949
+ ## Done when
6950
+
6951
+ - A manual \`check\` run returns every check, \`controls.ok: true\`, and writes
6952
+ rows the app renders.
6953
+ - Every blocklist shows either a passing control or \`unknown\`, never a bare
6954
+ "not listed".
6955
+ - One full scheduled send + check cycle has run and the score landed in \`run\`.
6956
+ - You reported the app link, script name and scope, and schedule ids.
6957
+ `;
6958
+
6959
+ // templates/skills/engineering-standards/config.json
6960
+ var config_default20 = `{
7146
6961
  "category": "skills",
7147
6962
  "description": "Code-shape policy for ALL code that gets written or reviewed — the senior-engineer bar for minimal complexity. Use whenever you are about to write code, design a module or abstraction, draft a plan phase that shapes code, or review a diff; also when the user asks \\"is this over-engineered\\", \\"can this be simpler\\", or pushes back on complexity. Sibling of desplega:delegate-work (which picks WHO executes; this skill defines WHAT good output looks like).",
7148
6963
  "displayName": "Engineering Standards",
@@ -7163,7 +6978,7 @@ var config_default19 = `{
7163
6978
  `;
7164
6979
 
7165
6980
  // templates/skills/engineering-standards/content.md
7166
- var content_default19 = `# engineering-standards
6981
+ var content_default20 = `# engineering-standards
7167
6982
 
7168
6983
  The goal is always the same: **solve the problem with the least code and the least complexity that actually solves it.** Not the cleverest solution, not the most extensible one — the one a senior engineer would defend in review. Every rule below is phrased as a *checkable test*, not a slogan: if you can't run the test, the rule doesn't apply; if the test fails, change the code.
7169
6984
 
@@ -7234,7 +7049,7 @@ runbooks/<name>/<slug>.md # …or grouped by area when they multiply
7234
7049
  `;
7235
7050
 
7236
7051
  // templates/skills/heartbeat-runbook/config.json
7237
- var config_default20 = `{
7052
+ var config_default21 = `{
7238
7053
  "kind": "skill",
7239
7054
  "name": "heartbeat-runbook",
7240
7055
  "displayName": "Heartbeat Runbook",
@@ -7251,7 +7066,7 @@ var config_default20 = `{
7251
7066
  `;
7252
7067
 
7253
7068
  // templates/skills/heartbeat-runbook/content.md
7254
- var content_default20 = `# Heartbeat runbook
7069
+ var content_default21 = `# Heartbeat runbook
7255
7070
 
7256
7071
  Lead only. The heartbeat runbook is your \`heartbeatMd\` profile field. The server reads it every 30 minutes.
7257
7072
 
@@ -7318,7 +7133,7 @@ Two kinds of sections:
7318
7133
  `;
7319
7134
 
7320
7135
  // templates/skills/implementing/config.json
7321
- var config_default21 = `{
7136
+ var config_default22 = `{
7322
7137
  "category": "skills",
7323
7138
  "description": "Plan implementation skill. Executes approved technical plans phase by phase with verification checkpoints.",
7324
7139
  "displayName": "Implementing",
@@ -7339,7 +7154,7 @@ var config_default21 = `{
7339
7154
  `;
7340
7155
 
7341
7156
  // templates/skills/implementing/content.md
7342
- var content_default21 = `# Implementing
7157
+ var content_default22 = `# Implementing
7343
7158
 
7344
7159
  You are implementing an approved technical plan, executing it phase by phase with verification at each step. Each phase is executed as a background sub-agent via \`desplega:phase-running\` — the main session acts as an orchestrator.
7345
7160
 
@@ -7532,7 +7347,7 @@ File-review is on by default (unless Autopilot):
7532
7347
  `;
7533
7348
 
7534
7349
  // templates/skills/improve-agents-md/config.json
7535
- var config_default22 = `{
7350
+ var config_default23 = `{
7536
7351
  "category": "skills",
7537
7352
  "description": "Improve (or bootstrap) an AGENTS.md / CLAUDE.md file using \`<important if>\` conditional blocks so the agent actually attends to the right guidance at the right time. Use this skill whenever the user mentions AGENTS.md, CLAUDE.md, agent instructions, project rules for AI, \\"my claude config\\", onboarding docs for agents, or asks to tighten / shorten / audit / rewrite an existing one — even if they don't explicitly say the filename. Also use when the user complains that an agent keeps ignoring their project rules.",
7538
7353
  "displayName": "Improve AGENTS.md",
@@ -7553,7 +7368,7 @@ var config_default22 = `{
7553
7368
  `;
7554
7369
 
7555
7370
  // templates/skills/improve-agents-md/content.md
7556
- var content_default22 = `# improve-agents-md
7371
+ var content_default23 = `# improve-agents-md
7557
7372
 
7558
7373
  Progressively improve (or bootstrap from scratch) a project's agent-instruction file. Works on both \`AGENTS.md\` (vendor-neutral convention from OpenAI Codex, also read by Cursor, Claude Code, and others) and \`CLAUDE.md\` (Claude-specific). Keeps one canonical file on disk and symlinks the other, so every agent reads the same source of truth.
7559
7374
 
@@ -7973,7 +7788,7 @@ Run with \`turbo\` from the repo root.
7973
7788
  `;
7974
7789
 
7975
7790
  // templates/skills/kv-storage/config.json
7976
- var config_default23 = `{
7791
+ var config_default24 = `{
7977
7792
  "kind": "skill",
7978
7793
  "name": "kv-storage",
7979
7794
  "displayName": "KV Storage",
@@ -7990,7 +7805,7 @@ var config_default23 = `{
7990
7805
  `;
7991
7806
 
7992
7807
  // templates/skills/kv-storage/content.md
7993
- var content_default23 = `# KV Storage
7808
+ var content_default24 = `# KV Storage
7994
7809
 
7995
7810
  Namespaced key/value store inside the swarm SQLite DB. Auto-scoped to your
7996
7811
  calling context — same string used by \`agent_tasks.contextKey\`.
@@ -8279,15 +8094,15 @@ value so you can detect a clobber.
8279
8094
  ## See also
8280
8095
 
8281
8096
  - The **\`pages\`** skill — companion skill for authed pages and \`swarmSdk.kv\`.
8282
- - The **\`artifacts\`** skill — for anything too big or too binary for KV.
8097
+ - The **\`agent-fs\`** skill — for anything too big or too binary for KV.
8283
8098
  - \`src/be/migrations/061_kv_store.sql\` — schema (\`kv_entries\`)
8284
8099
  - \`src/http/kv.ts\` — REST handler + namespace resolution
8285
8100
  - \`src/tools/kv/*\` — MCP tool registrars
8286
- - \`src/artifact-sdk/browser-sdk.ts\` — \`swarmSdk.kv\` for pages
8101
+ - \`src/pages/browser-sdk.ts\` — \`swarmSdk.kv\` for pages
8287
8102
  `;
8288
8103
 
8289
8104
  // templates/skills/learning/config.json
8290
- var config_default24 = `{
8105
+ var config_default25 = `{
8291
8106
  "category": "skills",
8292
8107
  "description": "Compounding knowledge across projects and teams. Captures, searches, and promotes institutional learnings via tiered backends (local/qmd/agent-fs).",
8293
8108
  "displayName": "Learning",
@@ -8308,7 +8123,7 @@ var config_default24 = `{
8308
8123
  `;
8309
8124
 
8310
8125
  // templates/skills/learning/content.md
8311
- var content_default24 = `# Learning
8126
+ var content_default25 = `# Learning
8312
8127
 
8313
8128
  You are managing institutional knowledge — capturing insights, searching prior learnings, and promoting important patterns into CLAUDE.md for permanent reference.
8314
8129
 
@@ -8576,13 +8391,13 @@ The config file lives at \`~/.agentic-learnings.json\`:
8576
8391
  `;
8577
8392
 
8578
8393
  // templates/skills/memory/config.json
8579
- var config_default25 = '{\n "kind": "skill",\n "name": "memory",\n "displayName": "Memory",\n "slug": "memory",\n "title": "Memory",\n "description": "Remember, recall, learning, memory: how swarm memory works and how to store, search, edit, rate, and delete memories with `memory-store`, `memory-search`, `memory-get`, `memory-edit`, `memory_rate`, and `memory-delete`, plus logical paths (`key`, `newKey`, `keyPrefix`, the `/longterm` roots and the lead-only ones) and lead promotion with `inject-learning`. Use before you store, edit, or delete a memory.",\n "version": "1.1.0",\n "category": "skills",\n "placeholders": [],\n "runAllSeedersCandidate": true,\n "systemDefault": true,\n "tags": ["memory", "recall", "learning"]\n}\n';
8394
+ var config_default26 = '{\n "kind": "skill",\n "name": "memory",\n "displayName": "Memory",\n "slug": "memory",\n "title": "Memory",\n "description": "Remember, recall, learning, memory: how swarm memory works and how to store, search, edit, rate, and delete memories with `memory-store`, `memory-search`, `memory-get`, `memory-edit`, `memory_rate`, and `memory-delete`, plus logical paths (`key`, `newKey`, `keyPrefix`, the `/longterm` roots and the lead-only ones) and lead promotion with `inject-learning`. Use before you store, edit, or delete a memory.",\n "version": "1.1.0",\n "category": "skills",\n "placeholders": [],\n "runAllSeedersCandidate": true,\n "systemDefault": true,\n "tags": ["memory", "recall", "learning"]\n}\n';
8580
8395
 
8581
8396
  // templates/skills/memory/content.md
8582
- var content_default25 = '# Memory\n\nSwarm memory is a store of short texts with embeddings, searchable by meaning and by keyword. Recall is automatic: the runner puts the best matches for your task in the task message under "Relevant Past Knowledge". Everything else is a tool call.\n\n## What is stored without you\n\n| Source | When | Scope |\n|---|---|---|\n| `task_completion` | `store-progress` with status `completed` or `failed` | `agent`, or `swarm` for research tasks and tasks tagged `knowledge` or `shared` |\n| `session_summary` | your session ends | `agent` |\n| `file_index` | a file written under `/workspace/personal/memory/` or `/workspace/shared/memory/<agentId>/` on a harness with the file hook (claude, pi, opencode) | by path |\n\nAutomatic tasks (schedules, heartbeat, monitors) skip the `task_completion` write unless `store-progress` gets `persistMemory: true`.\n\nPrefer `memory-store` over memory files. It works on every harness, including the remote ones.\n\n## Tools\n\n| Tool | Use |\n|---|---|\n| `memory-store` | create a memory: `content`, `name`, `scope`, optional `tags`, `taskId`, `intent`, `key` (a logical path, see Logical paths) |\n| `memory-search` | find memories: `query`, `scope` (`all`, `agent`, `swarm`), `limit`, optional `intent` (why you search), `source`, `keyPrefix` (only memories whose key starts with it) |\n| `memory-get` | the full content of one memory by ID |\n| `memory-edit` | change a memory in place: mode `replace` (whole content) or `exact` (one unique substring), `intent` required. `newKey` alone moves it to another logical path |\n| `memory-delete` | remove a memory by ID |\n| `memory_rate` | mark a memory you used in this task as useful or misleading |\n| `inject-learning` | lead only: push a learning into a worker\'s memory at swarm scope |\n\nSeed scripts (`script-run` with `name` and `args`):\n\n- `task-context-gathering` `{ taskId, queries: [...] }`: the task plus a deduplicated multi-query recall in one call.\n- `smart-recall` `{ queries: [...] }`: multi-query recall without the task.\n- `memory-dedup-check` `{ text, threshold? }`: near-duplicates of a candidate memory, default threshold 0.85.\n\n## What makes a good memory\n\n- One fact per memory: a fix, a pattern, a gotcha, a preference of a person, a fact about a repo or a host.\n- The context it applies to: repo, host, tool, version.\n- The evidence: what you saw, where.\n- A searchable `name`: "Linear API rejects issue updates without teamId", not "notes".\n- Under 2,000 characters stores as one chunk. Longer content splits on headings, one memory per chunk.\n\nSkip what a tool returns on demand (paths, tool lists, task status), what the repo already records (README, CLAUDE.md, git history), and what only mattered for this one task.\n\nA memory must not contain a token, password, key, or connection string. Remove the value and keep the reference ("the Linear token lives in config key LINEAR_API_KEY").\n\n## Scope\n\n- `agent` (default): only you recall it. Your own setup, your working notes, your mistakes.\n- `swarm`: every agent recalls it. Facts about shared repos, hosts, people, and processes. Choose `swarm` when a second agent would hit the same thing.\n\n## Logical paths (`key`)\n\nEvery memory has a `key`. By default it is an auto key (`<scope>/<source>/<id>`, or the file path for `file_index`), and the memory is **inbox material**: it keeps the lifecycle of its `source` and expires. Give it a path under `/longterm` and it becomes curated memory: findable by folder, ranked higher, and kept.\n\n`name` is a title. `key` is the address. Use a key only for what should outlive the task: a durable fact, a decision, a person or customer. Leave working notes and task results without one; expiring is the right outcome for them.\n\n### The `/longterm` roots\n\nThe list is closed. A key under `/longterm` whose second segment is not in it is refused, and the error lists the roots.\n\n| Root | Holds | Rank weight | Who may write |\n|---|---|---|---|\n| `/longterm/company-story` | what the company is and does | 1.4 | lead only |\n| `/longterm/entities/people/<slug>`, `/longterm/entities/customers/<slug>` | one doc per person or customer | 1.3 | lead only |\n| `/longterm/facts/<topic>/<slug>` | a durable fact about a repo, host, tool, or process | 1.2 | any agent |\n| `/longterm/decisions/<slug>` | a decision and why | 1.1 (0.3 with the tag `superseded`) | any agent |\n| `/longterm/workstreams/<slug>` | an ongoing effort | 1.0 | any agent |\n| `/longterm/timeline/<period>/<slug>` | dated events | 0.8 | lead only |\n\nAgents and repos are not entity types: agent profiles and `get-repos` own them, so `/longterm/entities/agents/...` is refused.\n\nKey shape: lowercase letters, digits, `-`, and `.` or `_` after the first segment; segments joined by `/`; starts with `/`; no trailing `/`; at most 200 characters. A key outside `/longterm` is not root-checked but must still have this shape.\n\n### Write, move, find\n\n```\nmemory-store key="/longterm/facts/swarm-runtime/sqlite-busy-retry" scope="swarm"\n name="SQLite BUSY_SNAPSHOT needs BEGIN IMMEDIATE" content="..."\n\nmemory-edit memoryId="<id>" newKey="/longterm/decisions/sqlite-busy-retry" intent="promote the fact to a decision"\n\nmemory-search query="busy retry" keyPrefix="/longterm/facts/swarm-runtime/"\n```\n\n- `memory-store`: one key per owner and scope. A second memory under a key you already use fails: `memory-edit` that one. If you pass no `key` and the `name` starts with `/longterm/`, the name is the key, so you set one field. An invalid name of that form is refused, never stored under an auto key.\n- `memory-edit` `newKey`: pass it alone for a pure move. Every chunk of the document moves in one step. The ID, usefulness rating, access counts, and author stay. A key that is already taken is refused. Address the memory with `memoryId`, or with `key` plus `scope`.\n- `memory-search` `keyPrefix`: a literal, case-sensitive prefix, applied before the top results are cut, not after. End a folder with `/` (`/longterm/entities/`, not `/longterm/entities`) to stay inside it.\n\n### What a `/longterm` key changes\n\n`source` stays provenance: there is no `longterm` source. The key is the tier. A memory under `/longterm` follows a `manual` memory\'s lifecycle whatever its `source`:\n\n- It never expires. Storing under a `/longterm` key sets no expiry. Moving a memory in with `newKey` clears the expiry it had, on every chunk. Moving it back out does not restore one.\n- It has no recency decay, and its source-quality multiplier is 1.5.\n- Automated cleanup skips it.\n\nA memory under any other key keeps its `source` lifecycle: `task_completion` expires after 7 days, `session_summary` after 3, `file_index` after 30, `manual` never.\n\nRanking multiplies similarity by recency decay, access boost, source quality, **path weight** (the table above, by longest matching root; any other key weighs 1.0), and usefulness. Path weight is one factor: a weak match under `/longterm/company-story` does not beat a strong match elsewhere on weight alone.\n\n### Lead-only roots\n\nOnly the lead may write or move a key under `/longterm/company-story`, `/longterm/entities`, or `/longterm/timeline`. The check is the permission `memory.write.consolidated`. It applies to a `key`, to a `newKey`, to a `/longterm/` name, and to a `sourcePath` sent to `POST /api/memory/index`, so there is no side door. A worker that tries gets an error naming the lead-only paths. A worker puts durable knowledge under `/longterm/facts` or `/longterm/decisions`, or leaves the key off, and the lead consolidates the rest.\n\n## Before you store\n\n1. Run `memory-dedup-check` with the text, or `memory-search` with one or two queries and `intent: "dedup before store"`.\n2. A near-duplicate exists: `memory-edit` it. Mode `replace` for a rewrite, mode `exact` for a one-line correction. Say why in `intent`.\n3. Nothing close exists: `memory-store`. For a keyed memory, `memory-search` with `keyPrefix` set to the folder you will write into shows what is already there.\n\n## Triage\n\n- A memory is wrong: `memory-edit` with the correction.\n- A memory is stale and nobody needs it: `memory-delete`.\n- A recalled memory helped or misled you: `memory_rate` with `useful` true or false and a short `note`. The call needs a task context and counts once per memory per task. Ratings move the memory up or down in future searches.\n\n## Lead: promote a learning\n\nWhen a worker\'s output or failure holds a lesson other workers need, call `inject-learning` with the worker\'s `agentId`, the `learning`, and a `category`: `mistake-pattern`, `best-practice`, `codebase-knowledge`, or `preference`. It lands at swarm scope and every agent recalls it. It is stored as `manual`, so it never expires, but it takes no key: it has no path weight and `keyPrefix` does not find it. To file a learning by path, `memory-store` it with a `/longterm` key, or `memory-edit` it with `newKey`.\n';
8397
+ var content_default26 = '# Memory\n\nSwarm memory is a store of short texts with embeddings, searchable by meaning and by keyword. Recall is automatic: the runner puts the best matches for your task in the task message under "Relevant Past Knowledge". Everything else is a tool call.\n\n## What is stored without you\n\n| Source | When | Scope |\n|---|---|---|\n| `task_completion` | `store-progress` with status `completed` or `failed` | `agent`, or `swarm` for research tasks and tasks tagged `knowledge` or `shared` |\n| `session_summary` | your session ends | `agent` |\n| `file_index` | a file written under `/workspace/personal/memory/` or `/workspace/shared/memory/<agentId>/` on a harness with the file hook (claude, pi, opencode) | by path |\n\nAutomatic tasks (schedules, heartbeat, monitors) skip the `task_completion` write unless `store-progress` gets `persistMemory: true`.\n\nPrefer `memory-store` over memory files. It works on every harness, including the remote ones.\n\n## Tools\n\n| Tool | Use |\n|---|---|\n| `memory-store` | create a memory: `content`, `name`, `scope`, optional `tags`, `taskId`, `intent`, `key` (a logical path, see Logical paths) |\n| `memory-search` | find memories: `query`, `scope` (`all`, `agent`, `swarm`), `limit`, optional `intent` (why you search), `source`, `keyPrefix` (only memories whose key starts with it) |\n| `memory-get` | the full content of one memory by ID |\n| `memory-edit` | change a memory in place: mode `replace` (whole content) or `exact` (one unique substring), `intent` required. `newKey` alone moves it to another logical path |\n| `memory-delete` | remove a memory by ID |\n| `memory_rate` | mark a memory you used in this task as useful or misleading |\n| `inject-learning` | lead only: push a learning into a worker\'s memory at swarm scope |\n\nSeed scripts (`script-run` with `name` and `args`):\n\n- `task-context-gathering` `{ taskId, queries: [...] }`: the task plus a deduplicated multi-query recall in one call.\n- `smart-recall` `{ queries: [...] }`: multi-query recall without the task.\n- `memory-dedup-check` `{ text, threshold? }`: near-duplicates of a candidate memory, default threshold 0.85.\n\n## What makes a good memory\n\n- One fact per memory: a fix, a pattern, a gotcha, a preference of a person, a fact about a repo or a host.\n- The context it applies to: repo, host, tool, version.\n- The evidence: what you saw, where.\n- A searchable `name`: "Linear API rejects issue updates without teamId", not "notes".\n- Under 2,000 characters stores as one chunk. Longer content splits on headings, one memory per chunk.\n\nSkip what a tool returns on demand (paths, tool lists, task status), what the repo already records (README, CLAUDE.md, git history), and what only mattered for this one task.\n\nA memory must not contain a token, password, key, or connection string. Remove the value and keep the reference ("the Linear token lives in config key LINEAR_API_KEY").\n\n## Scope\n\n- `agent` (default): only you recall it. Your own setup, your working notes, your mistakes.\n- `swarm`: every agent recalls it. Facts about shared repos, hosts, people, and processes. Choose `swarm` when a second agent would hit the same thing.\n\n## Logical paths (`key`)\n\nEvery memory has a `key`. By default it is an auto key (`<scope>/<source>/<id>`, or the file path for `file_index`), and the memory is **inbox material**: it keeps the lifecycle of its `source` and expires. Give it a path under `/longterm` and it becomes curated memory: findable by folder, ranked higher, and kept.\n\n`name` is a title. `key` is the address. Use a key only for what should outlive the task: a durable fact, a decision, a person or customer. Leave working notes and task results without one; expiring is the right outcome for them.\n\n### The `/longterm` roots\n\nThe list is closed. A key under `/longterm` whose second segment is not in it is refused, and the error lists the roots.\n\n| Root | Holds | Rank weight | Who may write |\n|---|---|---|---|\n| `/longterm/company-story` | what the company is and does | 1.4 | lead only |\n| `/longterm/entities/people/<slug>`, `/longterm/entities/customers/<slug>` | one doc per person or customer | 1.3 | lead only |\n| `/longterm/facts/<topic>/<slug>` | a durable fact about a repo, host, tool, or process | 1.2 | any agent |\n| `/longterm/decisions/<slug>` | a decision and why | 1.1 (0.3 with the tag `superseded`) | any agent |\n| `/longterm/workstreams/<slug>` | an ongoing effort | 1.0 | any agent |\n| `/longterm/timeline/<period>/<slug>` | dated events | 0.8 | lead only |\n\nAgents and repos are not entity types: agent profiles and `get-repos` own them, so `/longterm/entities/agents/...` is refused.\n\nKey shape: lowercase letters, digits, `-`, and `.` or `_` after the first segment; segments joined by `/`; starts with `/`; no trailing `/`; at most 200 characters. A key outside `/longterm` is not root-checked but must still have this shape.\n\n### Write, move, find\n\n```\nmemory-store key="/longterm/facts/swarm-runtime/sqlite-busy-retry" scope="swarm"\n name="SQLite BUSY_SNAPSHOT needs BEGIN IMMEDIATE" content="..."\n\nmemory-edit memoryId="<id>" newKey="/longterm/decisions/sqlite-busy-retry" intent="promote the fact to a decision"\n\nmemory-search query="busy retry" keyPrefix="/longterm/facts/swarm-runtime/"\n```\n\n- `memory-store`: one key per owner and scope. A second memory under a key you already use fails: `memory-edit` that one. If you pass no `key` and the `name` starts with `/longterm/`, the name is the key, so you set one field. An invalid name of that form is refused, never stored under an auto key.\n- `memory-edit` `newKey`: pass it alone for a pure move. Every chunk of the document moves in one step. The ID, usefulness rating, access counts, and author stay. A key that is already taken is refused. Address the memory with `memoryId`, or with `key` plus `scope`.\n- `memory-search` `keyPrefix`: a literal, case-sensitive prefix, applied before the top results are cut, not after. End a folder with `/` (`/longterm/entities/`, not `/longterm/entities`) to stay inside it.\n\n### What a `/longterm` key changes\n\n`source` stays provenance: there is no `longterm` source. The key is the tier. A memory under `/longterm` follows a `manual` memory\'s lifecycle whatever its `source`:\n\n- It never expires. Storing under a `/longterm` key sets no expiry. Moving a memory in with `newKey` clears the expiry it had, on every chunk. Moving it back out does not restore one.\n- It has no recency decay, and its source-quality multiplier is 1.5.\n- Automated cleanup skips it.\n\nA memory under any other key keeps its `source` lifecycle: `task_completion` expires after 7 days, `session_summary` after 3, `file_index` after 30, `manual` never.\n\nRanking multiplies similarity by recency decay, access boost, source quality, **path weight** (the table above, by longest matching root; any other key weighs 1.0), and usefulness. Path weight is one factor: a weak match under `/longterm/company-story` does not beat a strong match elsewhere on weight alone.\n\n### Lead-only roots\n\nOnly the lead may write or move a key under `/longterm/company-story`, `/longterm/entities`, or `/longterm/timeline`. The check is the permission `memory.write.consolidated`. It applies to a `key`, to a `newKey`, to a `/longterm/` name, and to a `sourcePath` sent to `POST /api/memory/index`, so there is no side door. A worker that tries gets an error naming the lead-only paths. A worker puts durable knowledge under `/longterm/facts` or `/longterm/decisions`, or leaves the key off, and the lead consolidates the rest.\n\n## Before you store\n\n1. Run `memory-dedup-check` with the text, or `memory-search` with one or two queries and `intent: "dedup before store"`.\n2. A near-duplicate exists: `memory-edit` it. Mode `replace` for a rewrite, mode `exact` for a one-line correction. Say why in `intent`.\n3. Nothing close exists: `memory-store`. For a keyed memory, `memory-search` with `keyPrefix` set to the folder you will write into shows what is already there.\n\n## Triage\n\n- A memory is wrong: `memory-edit` with the correction.\n- A memory is stale and nobody needs it: `memory-delete`.\n- A recalled memory helped or misled you: `memory_rate` with `useful` true or false and a short `note`. The call needs a task context and counts once per memory per task. Ratings move the memory up or down in future searches.\n\n## Lead: promote a learning\n\nWhen a worker\'s output or failure holds a lesson other workers need, call `inject-learning` with the worker\'s `agentId`, the `learning`, and a `category`: `mistake-pattern`, `best-practice`, `codebase-knowledge`, or `preference`. It lands at swarm scope and every agent recalls it. It is stored as `manual`, so it never expires, but it takes no key: it has no path weight and `keyPrefix` does not find it. To file a learning by path, `memory-store` it with a `/longterm` key, or `memory-edit` it with `newKey`.\n';
8583
8398
 
8584
8399
  // templates/skills/one-shot/config.json
8585
- var config_default26 = `{
8400
+ var config_default27 = `{
8586
8401
  "category": "skills",
8587
8402
  "description": "Small-scope plan+implement in one session. Assess the prompt, optionally one bundled round of questions, maintain a lightweight yolo plan while implementing, verify, quick code review, commit. Use when the user invokes /one-shot, or asks to \\"just build/fix/add\\" something small without the full create-plan → implement-plan pipeline. Small means ≤ ~3 phases inside one subsystem — anything bigger escalates to /desplega:create-plan.",
8588
8403
  "displayName": "One Shot",
@@ -8603,7 +8418,7 @@ var config_default26 = `{
8603
8418
  `;
8604
8419
 
8605
8420
  // templates/skills/one-shot/content.md
8606
- var content_default26 = `# One-Shot
8421
+ var content_default27 = `# One-Shot
8607
8422
 
8608
8423
  Plan and implement a small task in a single session. This is the lightweight sibling of the \`create-plan\` → \`implement-plan\` pipeline: same discipline (written plan, verification, code review), a fraction of the ceremony.
8609
8424
 
@@ -8672,7 +8487,7 @@ Commit with a concise message referencing the change (only if the user hasn't sa
8672
8487
  `;
8673
8488
 
8674
8489
  // templates/skills/pages/config.json
8675
- var config_default27 = `{
8490
+ var config_default28 = `{
8676
8491
  "kind": "skill",
8677
8492
  "name": "pages",
8678
8493
  "displayName": "Pages",
@@ -8695,11 +8510,11 @@ var config_default27 = `{
8695
8510
  `;
8696
8511
 
8697
8512
  // templates/skills/pages/content.md
8698
- var content_default27 = `# Pages — DB-backed Static Artifacts
8513
+ var content_default28 = `# Pages — DB-backed Static Artifacts
8699
8514
 
8700
8515
  DB-backed static content (HTML or JSON) served by the API directly. Cheap,
8701
- versioned, share-able by URL. The lighter-weight cousin of \`artifacts\` —
8702
- no PM2, no tunnels, no port allocation, no \`services\` registry row.
8516
+ versioned, share-able by URL. No server process, no tunnel, no port
8517
+ allocation.
8703
8518
 
8704
8519
  A page should be a clean human-facing artifact, not a raw dump with a URL. This
8705
8520
  skill covers both halves: **how to publish** a page, and **the taste baseline**
@@ -8710,7 +8525,7 @@ every page should meet.
8710
8525
  > (e.g. \`CAPABILITIES=core,task-pool,pages\`). If the tool is missing from
8711
8526
  > your MCP list, this is why.
8712
8527
 
8713
- ## When to use Pages vs Artifacts
8528
+ ## When to use Pages vs Apps
8714
8529
 
8715
8530
  | You need… | Use |
8716
8531
  |---|---|
@@ -8718,12 +8533,11 @@ every page should meet.
8718
8533
  | A JSON status payload + a few buttons that call swarm APIs | **Pages** (\`contentType: 'application/json'\`) |
8719
8534
  | To share an output via a URL with no server logic | **Pages** |
8720
8535
  | Analysis that should be linkable and bookmarkable | **Pages** |
8721
- | Custom routes, websockets, server-side logic | The **\`artifacts\`** skill |
8722
- | File uploads or per-request computation | The **\`artifacts\`** skill |
8536
+ | Persistent records, queries, and server-side actions | The **\`apps\`** skill |
8537
+ | Large or binary files (PNG, MP4, exports) | The **\`agent-fs\`** skill |
8723
8538
 
8724
8539
  Rule of thumb: if the content is a snapshot (you can write the full HTML/JSON
8725
- in a single call), use pages. If the content is a *running program*, use
8726
- artifacts.
8540
+ in a single call), use pages. If it needs stored data and actions, use apps.
8727
8541
 
8728
8542
  Do **not** use pages for:
8729
8543
 
@@ -9230,9 +9044,8 @@ human-facing summary.
9230
9044
 
9231
9045
  ## See Also
9232
9046
 
9233
- - The **\`artifacts\`** skill — durable storage (agent-fs, shared workspace) plus
9234
- full custom Hono apps with PM2 + a tunneled subdomain. Use it for interactive
9235
- servers, not static content.
9047
+ - The **\`apps\`** skill — schema-backed records, named queries, and actions.
9048
+ - The **\`agent-fs\`** skill — durable file storage and share links.
9236
9049
  - The **\`kv-storage\`** skill — page-scoped state via \`swarmSdk.kv\`.
9237
9050
  - \`runbooks/secret-scrubbing.md\` — egress scrubbing details.
9238
9051
  - SPA listing: \`\${APP_URL}/pages\` — the same \`APP_URL\` used for share links above.
@@ -9387,7 +9200,7 @@ Channels cannot access internal workflow topics. The socket closes when its auth
9387
9200
  `;
9388
9201
 
9389
9202
  // templates/skills/phase-running/config.json
9390
- var config_default28 = `{
9203
+ var config_default29 = `{
9391
9204
  "category": "skills",
9392
9205
  "description": "Execute individual plan phases as background sub-agents for context-efficient implementation.",
9393
9206
  "displayName": "Phase Running",
@@ -9408,7 +9221,7 @@ var config_default28 = `{
9408
9221
  `;
9409
9222
 
9410
9223
  // templates/skills/phase-running/content.md
9411
- var content_default28 = `# Phase Running
9224
+ var content_default29 = `# Phase Running
9412
9225
 
9413
9226
  You are executing a single phase of an implementation plan as an atomic background sub-agent. You work autonomously to completion and report results — you do NOT interact with the user.
9414
9227
 
@@ -9540,7 +9353,7 @@ Phase agents are atomic — they run to completion or stop:
9540
9353
  `;
9541
9354
 
9542
9355
  // templates/skills/planning/config.json
9543
- var config_default29 = `{
9356
+ var config_default30 = `{
9544
9357
  "category": "skills",
9545
9358
  "description": "Implementation planning skill. Creates detailed technical plans through interactive research and iteration.",
9546
9359
  "displayName": "Planning",
@@ -9561,7 +9374,7 @@ var config_default29 = `{
9561
9374
  `;
9562
9375
 
9563
9376
  // templates/skills/planning/content.md
9564
- var content_default29 = `# Planning
9377
+ var content_default30 = `# Planning
9565
9378
 
9566
9379
  You create detailed implementation plans through an interactive, iterative process. Be skeptical, thorough, collaborative.
9567
9380
 
@@ -9636,7 +9449,7 @@ Canonical format and heading hierarchy lives in \`template.md\`. Structure valid
9636
9449
  `;
9637
9450
 
9638
9451
  // templates/skills/qa/config.json
9639
- var config_default30 = `{
9452
+ var config_default31 = `{
9640
9453
  "category": "skills",
9641
9454
  "description": "Functional validation skill. Captures test evidence (screenshots, recordings, links) and produces QA reports in thoughts/*/qa/.",
9642
9455
  "displayName": "QA",
@@ -9657,7 +9470,7 @@ var config_default30 = `{
9657
9470
  `;
9658
9471
 
9659
9472
  // templates/skills/qa/content.md
9660
- var content_default30 = `# QA
9473
+ var content_default31 = `# QA
9661
9474
 
9662
9475
  You are performing functional validation of a feature, bugfix, or deployment. Your job is to prove it works (or doesn't) by executing test scenarios and capturing evidence.
9663
9476
 
@@ -9842,85 +9655,292 @@ Based on the answer:
9842
9655
  - **Done**: Finalize the QA report
9843
9656
  `;
9844
9657
 
9845
- // templates/skills/questioning/config.json
9846
- var config_default31 = `{
9658
+ // templates/skills/qa-loom/config.json
9659
+ var config_default32 = `{
9847
9660
  "category": "skills",
9848
- "description": "One-shot question answering using the research process. Answers inline without generating documents, then offers handoff to brainstorm or research.",
9849
- "displayName": "Questioning",
9661
+ "description": "Narrated QA loom for UI work: record a UI QA pass with agent-browser and a timestamped step log, voice it with ElevenLabs, burn one-line subtitles with ffmpeg, and embed the narrated mp4 (plus an optional GIF preview) in the PR as GitHub user attachments. Use on UI-changing PRs and UI QA passes. Layers on the \`qa\` and \`agent-browser\` skills. Needs ELEVENLABS_API_KEY, ffmpeg with libass, and agent-browser.",
9662
+ "displayName": "QA Loom",
9850
9663
  "kind": "skill",
9851
- "name": "questioning",
9664
+ "name": "qa-loom",
9852
9665
  "placeholders": [],
9853
9666
  "runAllSeedersCandidate": true,
9854
- "slug": "questioning",
9667
+ "slug": "qa-loom",
9855
9668
  "systemDefault": true,
9856
- "tags": [
9857
- "research",
9858
- "questions",
9859
- "analysis"
9860
- ],
9861
- "title": "Questioning",
9669
+ "tags": ["qa", "video", "screen-recording", "evidence", "elevenlabs"],
9670
+ "title": "QA Loom",
9862
9671
  "version": "1.0.0"
9863
9672
  }
9864
9673
  `;
9865
9674
 
9866
- // templates/skills/questioning/content.md
9867
- var content_default31 = `# Questioning
9675
+ // templates/skills/qa-loom/content.md
9676
+ var content_default32 = `# QA loom
9868
9677
 
9869
- You are answering a question directly and concisely using the research process. No documents are created by default — the answer is the deliverable.
9678
+ A QA loom is a short screen recording of a real UI QA pass, with a voice-over and small one-line subtitles, embedded in the PR. The narrated mp4 plays inline on GitHub with audio. An optional subtitled GIF gives a silent preview.
9870
9679
 
9871
- ## When to Use
9680
+ Two bundled files ship next to this skill:
9872
9681
 
9873
- This skill activates when:
9874
- - User invokes \`/question\` command
9875
- - Another skill references \`desplega:questioning\`
9876
- - User asks a focused question that doesn't need a full research document
9682
+ - \`run-qa.sh\` records the QA pass with \`agent-browser\` and writes \`steps.json\`.
9683
+ - \`build-loom.mjs\` turns \`steps.json\` and the raw recording into \`qa-loom.mp4\`.
9877
9684
 
9878
- ## Philosophy
9685
+ Copy both into your working directory before you edit them.
9879
9686
 
9880
- This is the fastest path from question to answer. Unlike \`/research\` (which documents comprehensively) or \`/brainstorm\` (which explores interactively), \`/question\` gets straight to the point:
9687
+ ## When to use
9881
9688
 
9882
- 1. Hear the question
9883
- 2. Investigate as needed
9884
- 3. Answer inline
9885
- 4. Offer next steps
9689
+ - On every PR that changes visible UI, and on every UI QA pass.
9690
+ - Run your normal QA with the \`qa\` skill. Drive the browser with the \`agent-browser\` skill. This skill adds recording, narration and delivery on top.
9691
+ - Skip it when a PR has no visible UI change.
9886
9692
 
9887
- **No ceremony** — no autonomy prompts, no preference setup, no working agreement. Just answer the question.
9693
+ ## Requirements
9888
9694
 
9889
- When you do need to ask the user something (handoff, follow-up choice), use \`AskUserQuestion\` per \`desplega:ask-user\` conventions.
9695
+ Check these first. If one is missing, use the fallback in section 7.
9890
9696
 
9891
- ## Process
9697
+ | Requirement | Check |
9698
+ |---|---|
9699
+ | \`ELEVENLABS_API_KEY\` env var | \`test -n "$ELEVENLABS_API_KEY"\` (never echo the value) |
9700
+ | \`agent-browser\` with recording | \`command -v agent-browser\`; on errors, \`agent-browser doctor\` |
9701
+ | \`ffmpeg\` built with libass | \`ffmpeg -hide_banner -filters 2>/dev/null \\| grep -w ass\` |
9702
+ | Node 18+ or Bun | \`node --version\` (the script needs global \`fetch\` and top-level await) |
9703
+ | \`jq\` and GNU \`date\` | \`jq --version\`; \`date +%N\` prints digits, not \`N\` |
9704
+ | A subtitle font | \`fc-list \\| grep -i "DejaVu Sans"\`, or set \`FONT\` to an installed font |
9705
+ | \`GITHUB_TOKEN\` for delivery | a token that can upload user attachments to the repo (section 6) |
9892
9706
 
9893
- ### Step 1: Analyze the Question
9707
+ Portability gotchas:
9894
9708
 
9895
- Classify the question to determine what investigation is needed:
9709
+ - Some ffmpeg builds ship without \`ffprobe\`. Probe with \`ffmpeg -hide_banner -i f.mp4 2>&1 | grep -E "Duration|Stream"\`.
9710
+ - Some ffmpeg builds have no \`drawtext\` filter. Burn subtitles with the libass \`ass\` filter, as \`build-loom.mjs\` does.
9711
+ - libass ignores the alpha byte of \`BackColour\` for \`BorderStyle=3\`. The subtitle box is solid dark grey, not translucent.
9712
+ - Minimal containers may lack \`bc\`. Do time arithmetic in \`jq\`, as \`run-qa.sh\` does.
9896
9713
 
9897
- | Question Type | Investigation Needed | Example |
9898
- |---------------|---------------------|---------|
9899
- | **Factual/locational** | Quick codebase search | "Where is the auth middleware defined?" |
9900
- | **Conceptual/how** | Read relevant files | "How does the plugin system discover skills?" |
9901
- | **Why/root cause** | Deep read + history | "Why does brainstorming default to verbose?" |
9902
- | **Comparative** | Read multiple areas | "What's the difference between research and question skills?" |
9903
- | **External/library** | context7 or web search | "How does Bun's SQLite driver handle transactions?" |
9714
+ ## Style rules
9904
9715
 
9905
- ### Step 2: Investigate
9716
+ - **Speed 1.33x** on the raw footage (\`SPEED=1.33\`). 2x is too fast to follow.
9717
+ - **One short sentence per step**, present tense, describing what the viewer sees: "The editor opens a shared file." Aim for 6 to 12 words.
9718
+ - **Subtitles: one line, small, bottom centre.** 38 px on a 1080p frame, dark box, \`MarginV 30\`. Cues hold at most 6 words, split on punctuation, with no orphan cue under 3 words.
9719
+ - **Voice and subtitles start when the screen renders**, not when the click fires. That is why the step log carries \`visualAt\`.
9720
+ - **Letterbox, never stretch.** Mobile-viewport steps are fitted into 1920x1080 with padding.
9721
+ - **Keep it short.** 4 to 6 steps that prove the change, about 20 to 30 seconds final.
9906
9722
 
9907
- Based on the question type, use the appropriate tools. **Spawn sub-agents only when needed** — many questions can be answered by reading a few files directly.
9723
+ ## Context hygiene
9908
9724
 
9909
- **For codebase questions:**
9910
- - Read directly mentioned files first (use Read tool WITHOUT limit/offset)
9911
- - Use **codebase-locator** agent if you need to find WHERE something lives
9912
- - Use **codebase-analyzer** agent if you need to understand HOW something works
9913
- - Use **codebase-pattern-finder** agent if you need examples of a pattern
9725
+ The pipeline handles large binary payloads. Keep them out of your context:
9914
9726
 
9915
- **For library/framework questions:**
9916
- - Use context7 MCP to fetch documentation (\`resolve-library-id\` → \`query-docs\`)
9727
+ - Run every ffmpeg call with \`-loglevel error\`, or send its stderr to a file and \`tail -3\` it only on failure.
9728
+ - Never print an ElevenLabs response. \`audio_base64\` is hundreds of KB. \`build-loom.mjs\` writes it straight to disk.
9729
+ - Never \`cat\` or read an mp4 or GIF. To look at the result, extract one PNG frame and view that.
9917
9730
 
9918
- **For external/web questions:**
9919
- - Use **web-search-researcher** agent for documentation or examples
9731
+ ## 1. Record the QA pass with a step log
9920
9732
 
9921
- **Efficiency rule**: If you can answer by reading 1-3 files, just read them. Don't spawn sub-agents for simple lookups.
9733
+ Each step logs:
9922
9734
 
9923
- ### Step 3: Answer
9735
+ - \`start\`: just before the action fires (\`act\`).
9736
+ - \`visualAt\`: after the resulting screen has rendered, following an \`agent-browser wait <selector>\` (\`step\`).
9737
+ - \`verdict\` and one \`narration\` sentence.
9738
+ - \`end\`: the next step's \`start\`. The last step ends when recording stops.
9739
+
9740
+ All times are seconds from recording start. Replace the example steps in \`run-qa.sh\` with your QA steps, then run it:
9741
+
9742
+ \`\`\`bash
9743
+ BASE_URL=http://localhost:5274 OUT=./loom bash run-qa.sh
9744
+ \`\`\`
9745
+
9746
+ Notes:
9747
+
9748
+ - Log in and seed data **before** \`record start\`, so the loom shows only the change.
9749
+ - For a mobile check, run \`agent-browser set viewport 390 844\` inside a step. The build letterboxes it.
9750
+ - After each step, wait about 800 ms so the viewer sees the result before the next action.
9751
+ - Take your normal QA screenshots as well. They are the fallback.
9752
+
9753
+ ## 2. Build the narrated mp4
9754
+
9755
+ \`build-loom.mjs\` reads \`steps.json\` and the raw recording. For each step it:
9756
+
9757
+ 1. Calls ElevenLabs \`POST /v1/text-to-speech/{voice}/with-timestamps\` once and caches the mp3 and character alignment under \`work/\`. A re-run after a style tweak costs no characters.
9758
+ 2. Cuts the step's segment, speeds it up, and letterboxes it to 1920x1080 at 30 fps.
9759
+ 3. Freezes the last frame when the narration outlasts the segment.
9760
+ 4. Builds subtitle cues from the same character alignment, offset by \`visualAt\`, so voice and subtitles cannot drift.
9761
+
9762
+ Then it concatenates the segments with the concat demuxer, delays each step's audio to where its screen renders, mixes with \`amix normalize=0\`, burns the ASS subtitles, and writes \`qa-loom.mp4\` and \`qa-loom.ass\`.
9763
+
9764
+ \`\`\`bash
9765
+ node build-loom.mjs loom/steps.json loom/raw.webm loom/out > loom/build.log 2>&1 || tail -5 loom/build.log
9766
+ cat loom/build.log # one JSON line: {"durationS":..,"steps":..,"cues":..,"chars":..}
9767
+ \`\`\`
9768
+
9769
+ Optional env: \`SPEED\` (default \`1.33\`), \`VOICE\` (default \`21m00Tcm4TlvDq8ikWAM\`, an ElevenLabs premade voice), \`MODEL\` (default \`eleven_turbo_v2_5\`), \`FONT\` (default \`DejaVu Sans\`).
9770
+
9771
+ ## 3. Check the result
9772
+
9773
+ Look at one frame per cue, as PNG:
9774
+
9775
+ \`\`\`bash
9776
+ cd loom/out
9777
+ ffmpeg -hide_banner -i qa-loom.mp4 2>&1 | grep -E "Duration|Stream" # 1920x1080, 30 fps, h264 + aac
9778
+ grep Dialogue qa-loom.ass # one short line per cue
9779
+ ffmpeg -hide_banner -i qa-loom.mp4 -vn -af volumedetect -f null - 2>&1 | grep mean_volume # about -91 dB means silent
9780
+ ffmpeg -y -loglevel error -ss 2 -i qa-loom.mp4 -frames:v 1 check-2s.png # view it: the subtitle matches the screen
9781
+ \`\`\`
9782
+
9783
+ If the loom feels slow or fast, change \`SPEED\` and re-run. TTS is cached, so the re-run is free.
9784
+
9785
+ ## 4. Optional GIF preview
9786
+
9787
+ GitHub plays an mp4 inline only when it is a GitHub user attachment (section 6). The GIF is an optional silent preview with the subtitles already burned in.
9788
+
9789
+ \`\`\`bash
9790
+ ffmpeg -y -loglevel error -i qa-loom.mp4 \\
9791
+ -vf "fps=10,scale=960:-1:flags=lanczos,split[a][b];[a]palettegen=stats_mode=diff[p];[b][p]paletteuse=dither=bayer:bayer_scale=5:diff_mode=rectangle" \\
9792
+ -loop 0 qa-loom.gif 2>gif.err || tail -3 gif.err
9793
+ ffmpeg -hide_banner -i qa-loom.gif 2>&1 | grep -E "Duration|Stream" # 960x540, 10 fps
9794
+ head -c 6 qa-loom.gif; echo # GIF89a
9795
+ ffmpeg -y -loglevel error -ss 5 -i qa-loom.gif -frames:v 1 gif-check.png # view it: subtitle readable
9796
+ \`\`\`
9797
+
9798
+ At these settings a GIF costs about 30 KB per second of loom. Keep it under 5 MB. If it is bigger, drop to \`fps=8\`, then to \`scale=720:-1\`. Below 720 px wide the subtitles become unreadable.
9799
+
9800
+ ## 5. Keep the originals
9801
+
9802
+ Store \`qa-loom.mp4\` and \`qa-loom.gif\` wherever your team keeps QA evidence, next to the QA screenshots, under the same topic name. The GitHub attachment is the embed; your store is the durable original.
9803
+
9804
+ ## 6. Attach to the PR
9805
+
9806
+ Upload both files to GitHub as user attachments. This uses GitHub's user-attachments upload endpoint, the one the web editor uses. It is not part of the documented REST API.
9807
+
9808
+ \`\`\`bash
9809
+ REPO=<owner>/<repo>
9810
+ REPO_ID=$(gh api "repos/$REPO" --jq .id)
9811
+ attach() { # $1 local file, $2 content type; prints https://github.com/user-attachments/assets/<uuid>
9812
+ q=$(jq -rn --arg name "$(basename "$1")" --arg content_type "$2" --arg repository_id "$REPO_ID" \\
9813
+ '$ARGS.named | to_entries | map("\\(.key)=\\(.value | @uri)") | join("&")')
9814
+ curl --fail-with-body -sS -X POST "https://uploads.github.com/user-attachments/assets?$q" \\
9815
+ -H "Authorization: Bearer $GITHUB_TOKEN" -H 'Content-Type: application/octet-stream' \\
9816
+ --data-binary "@$1" | jq -er .url
9817
+ }
9818
+ MP4_URL=$(attach qa-loom.mp4 video/mp4)
9819
+ GIF_URL=$(attach qa-loom.gif image/gif)
9820
+ echo "$MP4_URL $GIF_URL"
9821
+ \`\`\`
9822
+
9823
+ Verification rules:
9824
+
9825
+ - Both URLs must match \`https://github.com/user-attachments/assets/<uuid>\`.
9826
+ - A 401 means the token type is not accepted (GitHub App installation tokens are rejected; a personal access token works). A 404 means a wrong repository ID or a token without write access to the repo.
9827
+ - Do not embed presigned storage URLs. A presigned mp4 usually downloads instead of playing, and it expires.
9828
+
9829
+ Put the loom in the PR body's \`## Before / after\` section. Write the section to a file:
9830
+
9831
+ \`\`\`bash
9832
+ cat > loom-section.md <<EOF
9833
+ QA loom: <one-line summary>. <n> steps, <verdicts>.
9834
+
9835
+ $MP4_URL
9836
+
9837
+ ![QA loom preview (silent, subtitled)]($GIF_URL)
9838
+ EOF
9839
+ \`\`\`
9840
+
9841
+ \`$MP4_URL\` must sit on its own line, so GitHub renders a player with audio. The GIF line is optional. Merge the section into the body (\`gh pr view <n> --json body\`, then \`gh pr edit <n> --body-file <file>\`) and leave every other line unchanged.
9842
+
9843
+ Then open the PR and confirm that the mp4 player and the GIF render. A successful upload response does not prove that the embed renders.
9844
+
9845
+ ## 7. Fallback: never block the PR
9846
+
9847
+ If ElevenLabs fails (401, 429, quota, or a 5xx after one retry with backoff), ffmpeg fails, or a requirement is missing:
9848
+
9849
+ 1. Post the QA screenshots in the PR as GitHub user attachments.
9850
+ 2. Add one line to the PR that says why there is no loom, for example "QA loom skipped: ElevenLabs 429 quota exceeded."
9851
+ 3. Ship the PR. The loom is evidence, never a merge gate.
9852
+
9853
+ A 4xx validation error is deterministic. Do not loop on it. Fix the input or fall back.
9854
+
9855
+ ## Deliverable checklist
9856
+
9857
+ - [ ] \`steps.json\` has \`start\`, \`visualAt\`, \`end\`, \`verdict\` and \`narration\` for every step
9858
+ - [ ] \`qa-loom.mp4\` is 1920x1080 at 30 fps with h264 and aac, is not silent, and shows one short subtitle line at the bottom
9859
+ - [ ] \`qa-loom.gif\`, if made, is 960 px wide at 10 fps, under 5 MB, and starts with \`GIF89a\`
9860
+ - [ ] The originals are stored with the QA screenshots
9861
+ - [ ] The PR body's \`## Before / after\` section has \`$MP4_URL\` on its own line, and every URL is \`https://github.com/user-attachments/assets/<uuid>\`
9862
+ - [ ] You opened the PR and saw the player render
9863
+ `;
9864
+
9865
+ // templates/skills/questioning/config.json
9866
+ var config_default33 = `{
9867
+ "category": "skills",
9868
+ "description": "One-shot question answering using the research process. Answers inline without generating documents, then offers handoff to brainstorm or research.",
9869
+ "displayName": "Questioning",
9870
+ "kind": "skill",
9871
+ "name": "questioning",
9872
+ "placeholders": [],
9873
+ "runAllSeedersCandidate": true,
9874
+ "slug": "questioning",
9875
+ "systemDefault": true,
9876
+ "tags": [
9877
+ "research",
9878
+ "questions",
9879
+ "analysis"
9880
+ ],
9881
+ "title": "Questioning",
9882
+ "version": "1.0.0"
9883
+ }
9884
+ `;
9885
+
9886
+ // templates/skills/questioning/content.md
9887
+ var content_default33 = `# Questioning
9888
+
9889
+ You are answering a question directly and concisely using the research process. No documents are created by default — the answer is the deliverable.
9890
+
9891
+ ## When to Use
9892
+
9893
+ This skill activates when:
9894
+ - User invokes \`/question\` command
9895
+ - Another skill references \`desplega:questioning\`
9896
+ - User asks a focused question that doesn't need a full research document
9897
+
9898
+ ## Philosophy
9899
+
9900
+ This is the fastest path from question to answer. Unlike \`/research\` (which documents comprehensively) or \`/brainstorm\` (which explores interactively), \`/question\` gets straight to the point:
9901
+
9902
+ 1. Hear the question
9903
+ 2. Investigate as needed
9904
+ 3. Answer inline
9905
+ 4. Offer next steps
9906
+
9907
+ **No ceremony** — no autonomy prompts, no preference setup, no working agreement. Just answer the question.
9908
+
9909
+ When you do need to ask the user something (handoff, follow-up choice), use \`AskUserQuestion\` per \`desplega:ask-user\` conventions.
9910
+
9911
+ ## Process
9912
+
9913
+ ### Step 1: Analyze the Question
9914
+
9915
+ Classify the question to determine what investigation is needed:
9916
+
9917
+ | Question Type | Investigation Needed | Example |
9918
+ |---------------|---------------------|---------|
9919
+ | **Factual/locational** | Quick codebase search | "Where is the auth middleware defined?" |
9920
+ | **Conceptual/how** | Read relevant files | "How does the plugin system discover skills?" |
9921
+ | **Why/root cause** | Deep read + history | "Why does brainstorming default to verbose?" |
9922
+ | **Comparative** | Read multiple areas | "What's the difference between research and question skills?" |
9923
+ | **External/library** | context7 or web search | "How does Bun's SQLite driver handle transactions?" |
9924
+
9925
+ ### Step 2: Investigate
9926
+
9927
+ Based on the question type, use the appropriate tools. **Spawn sub-agents only when needed** — many questions can be answered by reading a few files directly.
9928
+
9929
+ **For codebase questions:**
9930
+ - Read directly mentioned files first (use Read tool WITHOUT limit/offset)
9931
+ - Use **codebase-locator** agent if you need to find WHERE something lives
9932
+ - Use **codebase-analyzer** agent if you need to understand HOW something works
9933
+ - Use **codebase-pattern-finder** agent if you need examples of a pattern
9934
+
9935
+ **For library/framework questions:**
9936
+ - Use context7 MCP to fetch documentation (\`resolve-library-id\` → \`query-docs\`)
9937
+
9938
+ **For external/web questions:**
9939
+ - Use **web-search-researcher** agent for documentation or examples
9940
+
9941
+ **Efficiency rule**: If you can answer by reading 1-3 files, just read them. Don't spawn sub-agents for simple lookups.
9942
+
9943
+ ### Step 3: Answer
9924
9944
 
9925
9945
  Present the answer as **inline text** (not a document). Structure it naturally based on the question:
9926
9946
 
@@ -9975,7 +9995,7 @@ Each iteration is independent — no state accumulates between questions unless
9975
9995
  `;
9976
9996
 
9977
9997
  // templates/skills/researching/config.json
9978
- var config_default32 = `{
9998
+ var config_default34 = `{
9979
9999
  "category": "skills",
9980
10000
  "description": "Comprehensive codebase research skill. Documents codebase as-is by spawning parallel sub-agents and synthesizing findings into research documents.",
9981
10001
  "displayName": "Researching",
@@ -9996,7 +10016,7 @@ var config_default32 = `{
9996
10016
  `;
9997
10017
 
9998
10018
  // templates/skills/researching/content.md
9999
- var content_default32 = `# Researching
10019
+ var content_default34 = `# Researching
10000
10020
 
10001
10021
  You are conducting comprehensive research across the codebase to answer questions by spawning parallel sub-agents and synthesizing their findings.
10002
10022
 
@@ -10165,7 +10185,7 @@ File-review is on by default (unless Autopilot):
10165
10185
  `;
10166
10186
 
10167
10187
  // templates/skills/review-offered-task/config.json
10168
- var config_default33 = `{
10188
+ var config_default35 = `{
10169
10189
  "kind": "skill",
10170
10190
  "name": "review-offered-task",
10171
10191
  "displayName": "Review Offered Task",
@@ -10182,10 +10202,10 @@ var config_default33 = `{
10182
10202
  `;
10183
10203
 
10184
10204
  // templates/skills/review-offered-task/content.md
10185
- var content_default33 = '# Review Offered Task\n\nYou have been offered a task. Your job is to review it and decide whether to accept or reject it based on your capabilities and current workload.\n\n## Workflow\n\n1. **Get task details**: Call the `get-task-details` tool with the provided `taskId` to understand what the task involves.\n\n2. **Evaluate the task**: Consider:\n - Does this task match your capabilities?\n - Do you have the necessary context or access to complete it?\n - Is the task description clear enough to proceed?\n\n3. **Make a decision**:\n - **Accept**: If you can complete this task, call `task-action` with `action: "accept"` and `taskId: "<taskId>"`. Then immediately use the `work-on-task` skill with the taskId to start working on it.\n - **Reject**: If you cannot complete this task, call `task-action` with `action: "reject"`, `taskId: "<taskId>"`, and provide a `reason` explaining why you\'re rejecting it (e.g., "Task requires Python expertise which I don\'t have", "Task description is too vague").\n\n## Example Accept Flow\n\n```\n1. get-task-details taskId="abc-123"\n2. [Review the task details]\n3. task-action action="accept" taskId="abc-123"\n4. /work-on-task abc-123\n```\n\n## Example Reject Flow\n\n```\n1. get-task-details taskId="abc-123"\n2. [Review the task details]\n3. task-action action="reject" taskId="abc-123" reason="Task requires access to production database which I don\'t have"\n4. Stop\n```\n\n## Important Notes\n\n- Always provide a clear reason when rejecting a task - this helps the lead agent reassign it appropriately\n- If you accept, you must immediately start working on the task using the `work-on-task` skill\n- If you reject, the task returns to the unassigned pool for reassignment\n';
10205
+ var content_default35 = '# Review Offered Task\n\nYou have been offered a task. Your job is to review it and decide whether to accept or reject it based on your capabilities and current workload.\n\n## Workflow\n\n1. **Get task details**: Call the `get-task-details` tool with the provided `taskId` to understand what the task involves.\n\n2. **Evaluate the task**: Consider:\n - Does this task match your capabilities?\n - Do you have the necessary context or access to complete it?\n - Is the task description clear enough to proceed?\n\n3. **Make a decision**:\n - **Accept**: If you can complete this task, call `task-action` with `action: "accept"` and `taskId: "<taskId>"`. Then immediately use the `work-on-task` skill with the taskId to start working on it.\n - **Reject**: If you cannot complete this task, call `task-action` with `action: "reject"`, `taskId: "<taskId>"`, and provide a `reason` explaining why you\'re rejecting it (e.g., "Task requires Python expertise which I don\'t have", "Task description is too vague").\n\n## Example Accept Flow\n\n```\n1. get-task-details taskId="abc-123"\n2. [Review the task details]\n3. task-action action="accept" taskId="abc-123"\n4. /work-on-task abc-123\n```\n\n## Example Reject Flow\n\n```\n1. get-task-details taskId="abc-123"\n2. [Review the task details]\n3. task-action action="reject" taskId="abc-123" reason="Task requires access to production database which I don\'t have"\n4. Stop\n```\n\n## Important Notes\n\n- Always provide a clear reason when rejecting a task - this helps the lead agent reassign it appropriately\n- If you accept, you must immediately start working on the task using the `work-on-task` skill\n- If you reject, the task returns to the unassigned pool for reassignment\n';
10186
10206
 
10187
10207
  // templates/skills/reviewing/config.json
10188
- var config_default34 = `{
10208
+ var config_default36 = `{
10189
10209
  "category": "skills",
10190
10210
  "description": "Structured critique of research, plan, and brainstorm documents for completeness, gaps, and quality.",
10191
10211
  "displayName": "Reviewing",
@@ -10206,7 +10226,7 @@ var config_default34 = `{
10206
10226
  `;
10207
10227
 
10208
10228
  // templates/skills/reviewing/content.md
10209
- var content_default34 = `# Reviewing
10229
+ var content_default36 = `# Reviewing
10210
10230
 
10211
10231
  You are performing a structured critique of a document (research, plan, or brainstorm) to identify gaps, weaknesses, and quality issues. (For reviewing a code diff, use \`desplega:code-reviewing\` instead — this skill reviews documents.)
10212
10232
 
@@ -10458,7 +10478,7 @@ File-review is on by default (unless Autopilot):
10458
10478
  `;
10459
10479
 
10460
10480
  // templates/skills/scheduled-task-resilience/config.json
10461
- var config_default35 = `{
10481
+ var config_default37 = `{
10462
10482
  "kind": "skill",
10463
10483
  "name": "scheduled-task-resilience",
10464
10484
  "displayName": "Scheduled Task Resilience",
@@ -10475,7 +10495,7 @@ var config_default35 = `{
10475
10495
  `;
10476
10496
 
10477
10497
  // templates/skills/scheduled-task-resilience/content.md
10478
- var content_default35 = `# Scheduled Task Resilience
10498
+ var content_default37 = `# Scheduled Task Resilience
10479
10499
 
10480
10500
  Use these rules whenever a scheduled or regular task polls or waits on a long-running operation. They prevent heartbeat collisions, lost work, duplicate delivery, and sessions that fail after holding an external job open too long.
10481
10501
 
@@ -10575,7 +10595,7 @@ When historical incident detail is useful, search the deployment's memory regist
10575
10595
  `;
10576
10596
 
10577
10597
  // templates/skills/scheduling/config.json
10578
- var config_default36 = `{
10598
+ var config_default38 = `{
10579
10599
  "kind": "skill",
10580
10600
  "name": "scheduling",
10581
10601
  "displayName": "Scheduling",
@@ -10592,10 +10612,10 @@ var config_default36 = `{
10592
10612
  `;
10593
10613
 
10594
10614
  // templates/skills/scheduling/content.md
10595
- var content_default36 = '# Scheduling\n\nUse a schedule when work repeats on a clock, or when it must run once at a later time. On each tick the schedule creates one run. The run is a workflow, a catalog script, or an agent task.\n\n## Tools\n\n`list-schedules`, `create-schedule`, `update-schedule`, `patch-schedule`, `delete-schedule`, `run-schedule-now`. They are deferred. Load them with your harness tool search before the first call.\n\n## Pick the target type\n\n| The tick should | `targetType` | Required fields |\n|---|---|---|\n| Start a workflow | `workflow` | `workflowId` |\n| Run a catalog script | `script` | `scriptName`, optional `scriptArgs` |\n| Put a reasoning agent in the loop | `agent-task` (default) | `taskTemplate` |\n\nChoose `agent-task` only when the run needs judgment, open-ended work, or tool orchestration that no workflow or script covers. An `agent-task` whose template says "trigger workflow X" or "run script Y" is wrong: use the direct target instead.\n\nThe agent-task fields `targetAgentId`, `model`, `modelTier`, `taskTemplate`, `priority`, and `tags` do not apply to workflow or script targets. Workflow cooldowns still gate workflow targets.\n\n## Pick the timing\n\n| Shape | Fields |\n|---|---|\n| Recurring on a cron | `scheduleType: "recurring"`, `cronExpression` (for example `0 9 * * 1-5`), `timezone` (default `UTC`) |\n| Recurring on an interval | `scheduleType: "recurring"`, `intervalMs` (for example `3600000` for hourly) |\n| Once, at a time | `scheduleType: "one_time"`, `runAt` (ISO datetime) |\n| Once, after a delay | `scheduleType: "one_time"`, `delayMs` |\n\nGive the schedule a unique `name` and a one-line `description` that says what a human gets from it.\n\n## Write the task template (agent-task target)\n\nThe template is the whole task the agent receives. It must state:\n\n- the goal and the inputs (IDs, repo, channel),\n- where the result goes (a page, agent-fs, a Slack thread, a task output),\n- what done looks like.\n\n`targetAgentId` pins the task to one agent. Omit it to use the pool. `modelTier` (`smol`, `regular`, `smart`, `ultra`) is the portable way to pick a model. `model` is a provider-specific override.\n\nTasks created by a schedule are automatic tasks. Their completed output is not stored as memory unless the agent calls `store-progress` with `persistMemory: true`.\n\n## Check back later\n\nA task whose answer needs time (a build, a deploy, a reply) calls `defer-task` with `delayMs` or `runAt`, a `summary` of what you did so far, and a `note` that says what is pending. The tool completes the task and creates the one-off schedule for you. The task reaches its final state (`completed`) on this call, and the `summary` becomes its output. The wake-up task carries the deferred task as its parent, so you receive that task\'s context. Add `checks` to list what to verify on wake-up. Do not hand-roll this with `create-schedule`.\n\nWhen waiting on tasks, include `wakeOn:{event:"settled",taskIds:["<id>"],mode:"all"}` alongside `delayMs` or `runAt`. The displayed time is a ceiling; matching task outcomes wake you sooner. The task whose outcome wakes you creates no separate follow-up for you; the wake-up task is where you review it. In `mode:"all"`, a member that settles while others are pending still creates its follow-up.\n\n## Secrets\n\nA `taskTemplate` and `scriptArgs` are stored as plain text and replayed on every run. They must not contain a token, password, or key. Call the external API from a script through a registered connection or a credential binding instead. See the `swarm-scripts` skill, section Secrets.\n\n## Verify before you leave\n\n1. `run-schedule-now` once and read the run result.\n2. `list-schedules` with `name` to confirm `nextRunAt` and `enabled`.\n3. For a script target, the script must exist at global scope: `script-search` by name.\n\n## Repair a failing schedule\n\nSchedules fail in two places. A schedule whose spawned tasks fail: `get-tasks` with `scheduleId` and `status: "failed"`. A schedule the scheduler could not dispatch at all: `list-schedules` with `lastRunStatus: "failed"` or `consecutiveErrorsMin: 1`; these errors back off and auto-disable after 5. The `list-schedules` filters never see spawned-task failures. Fix the cause, then `run-schedule-now` to confirm. A schedule you cannot fix now: `patch-schedule` with `enabled: false` and say so in your task output. A second schedule next to a broken one is not a fix.\n\n## Related skills\n\n- `scheduled-task-resilience`: waiting on CI, builds, deploys, or other slow jobs inside a scheduled task.\n- `swarm-scripts`: writing the script a `script` target runs.\n- `workflow-iterate`: building and testing the workflow a `workflow` target starts.\n';
10615
+ var content_default38 = '# Scheduling\n\nUse a schedule when work repeats on a clock, or when it must run once at a later time. On each tick the schedule creates one run. The run is a workflow, a catalog script, or an agent task.\n\n## Tools\n\n`list-schedules`, `create-schedule`, `update-schedule`, `patch-schedule`, `delete-schedule`, `run-schedule-now`. They are deferred. Load them with your harness tool search before the first call.\n\n## Pick the target type\n\n| The tick should | `targetType` | Required fields |\n|---|---|---|\n| Start a workflow | `workflow` | `workflowId` |\n| Run a catalog script | `script` | `scriptName`, optional `scriptArgs` |\n| Put a reasoning agent in the loop | `agent-task` (default) | `taskTemplate` |\n\nChoose `agent-task` only when the run needs judgment, open-ended work, or tool orchestration that no workflow or script covers. An `agent-task` whose template says "trigger workflow X" or "run script Y" is wrong: use the direct target instead.\n\nThe agent-task fields `targetAgentId`, `model`, `modelTier`, `taskTemplate`, `priority`, and `tags` do not apply to workflow or script targets. Workflow cooldowns still gate workflow targets.\n\n## Pick the timing\n\n| Shape | Fields |\n|---|---|\n| Recurring on a cron | `scheduleType: "recurring"`, `cronExpression` (for example `0 9 * * 1-5`), `timezone` (default `UTC`) |\n| Recurring on an interval | `scheduleType: "recurring"`, `intervalMs` (for example `3600000` for hourly) |\n| Once, at a time | `scheduleType: "one_time"`, `runAt` (ISO datetime) |\n| Once, after a delay | `scheduleType: "one_time"`, `delayMs` |\n\nGive the schedule a unique `name` and a one-line `description` that says what a human gets from it.\n\n## Write the task template (agent-task target)\n\nThe template is the whole task the agent receives. It must state:\n\n- the goal and the inputs (IDs, repo, channel),\n- where the result goes (a page, agent-fs, a Slack thread, a task output),\n- what done looks like.\n\n`targetAgentId` pins the task to one agent. Omit it to use the pool. `modelTier` (`smol`, `regular`, `smart`, `ultra`) is the portable way to pick a model. `model` is a provider-specific override.\n\nTasks created by a schedule are automatic tasks. Their completed output is not stored as memory unless the agent calls `store-progress` with `persistMemory: true`.\n\n## Check back later\n\nA task whose answer needs time (a build, a deploy, a reply) calls `defer-task` with `delayMs` or `runAt`, a `summary` of what you did so far, and a `note` that says what is pending. The tool completes the task and creates the one-off schedule for you. The task reaches its final state (`completed`) on this call, and the `summary` becomes its output. The wake-up task carries the deferred task as its parent, so you receive that task\'s context. Add `checks` to list what to verify on wake-up. Do not hand-roll this with `create-schedule`.\n\nWhen waiting on tasks, include `wakeOn:{event:"settled",taskIds:["<id>"],mode:"all"}` alongside `delayMs` or `runAt`. The displayed time is a ceiling; matching task outcomes wake you sooner. The task whose outcome wakes you creates no separate follow-up for you; the wake-up task is where you review it. In `mode:"all"`, a member that settles while others are pending still creates its follow-up.\n\n## Secrets\n\nA `taskTemplate` and `scriptArgs` are stored as plain text and replayed on every run. They must not contain a token, password, or key. Call the external API from a script through a registered connection or a credential binding instead. See the `swarm-scripts` skill, section Secrets.\n\n## Verify before you leave\n\n1. `run-schedule-now` once and read the run result.\n2. `list-schedules` with `name` to confirm `nextRunAt` and `enabled`.\n3. For a script target, the script must exist at global scope: `script-search` by name.\n\n## Repair a failing schedule\n\nSchedules fail in two places. A schedule whose spawned tasks fail: `get-tasks` with `scheduleId` and `status: "failed"`. A schedule the scheduler could not dispatch at all: `list-schedules` with `lastRunStatus: "failed"` or `consecutiveErrorsMin: 1`; these errors back off and auto-disable after 5. The `list-schedules` filters never see spawned-task failures. Fix the cause, then `run-schedule-now` to confirm. A schedule you cannot fix now: `patch-schedule` with `enabled: false` and say so in your task output. A second schedule next to a broken one is not a fix.\n\n## Related skills\n\n- `scheduled-task-resilience`: waiting on CI, builds, deploys, or other slow jobs inside a scheduled task.\n- `swarm-scripts`: writing the script a `script` target runs.\n- `workflow-iterate`: building and testing the workflow a `workflow` target starts.\n';
10596
10616
 
10597
10617
  // templates/skills/script-builder/config.json
10598
- var config_default37 = `{
10618
+ var config_default39 = `{
10599
10619
  "category": "skills",
10600
10620
  "description": "Generate durable, re-runnable scripts from session intent — validation scripts (PASS/FAIL contract) AND gather/bulk scripts (many API/tool calls in code, one derived summary out). Supports TypeScript, Python, Bash with auto-detection, enforces context-optimal output, and auto-documents scripts in CLAUDE.md. Triggers on \\"turn this into a script\\", \\"I want to test/validate X end-to-end\\", \\"wrap this in a re-runnable script\\" — and, critically, whenever you are about to make (or just made) ~10+ similar tool/API calls or any bulk fan-out over a list: that mechanical middle belongs in a script, not in context.",
10601
10621
  "displayName": "Script Builder",
@@ -10616,10 +10636,10 @@ var config_default37 = `{
10616
10636
  `;
10617
10637
 
10618
10638
  // templates/skills/script-builder/content.md
10619
- var content_default37 = "# script-builder\n\nYou are converting session intent into a durable, re-runnable script committed to the target project's `scripts/` directory. Two script classes share one principle — **only the derived answer re-enters context; raw payloads never do**:\n\n- **Validation scripts** (`check-*`, `e2e-*`, `smoke-*`, …): a single PASS/FAIL line + `/tmp` log path on success, full verbose output redirected to a timestamped log file.\n- **Gather/bulk scripts** (`gather-*`, `bulk-*`): replace N similar tool/API calls with one script that loops, filters, and aggregates *in code* — stdout is one compact summary block (JSON or table), raw responses go to the `/tmp` log. Rubric (from production code-mode data): past ~10 items or any fan-out over a list, a script beats individual tool calls by ~100x on context and roughly halves end-to-end cost. Offer the script *before* the fan-out happens when you can see it coming, not just retrospectively.\n\nBoth classes get an `<important if>` block in `CLAUDE.md`/`AGENTS.md` so future agents discover the script when the intent recurs — durable scripts are reusable agent memory: import, don't re-derive.\n\n**Schema discovery happens in code, too**: when scripting against an unfamiliar API, grep/filter its OpenAPI spec or typed client programmatically to find the few relevant endpoints — never paste the full schema into context.\n\n## Working Agreement\n\nThese instructions establish a working agreement between you and the user. The key principles are:\n\n1. **AskUserQuestion is your primary communication tool** - Whenever you need to ask the user anything (clarifications, preferences, decisions, confirmations), use the **AskUserQuestion tool**. Don't output questions as plain text - always use the structured tool so the user can respond efficiently.\n\n2. **Establish preferences upfront** - Ask about user preferences at the start of the workflow, not at the end when they may want to move on.\n\n3. **Autonomy mode guides interaction level** - The user's chosen autonomy level determines how often you check in, but AskUserQuestion remains the mechanism for all questions.\n\n## When to Use\n\nThis skill activates when:\n- User invokes `/script-builder` command\n- Another skill references `**OPTIONAL SUB-SKILL:** desplega:script-builder`\n- A `planning`, `qa`, or `verifying` flow needs a re-runnable validation script that doesn't yet exist\n- The user expresses intent to \"turn X into a script\", \"wrap this validation into something reusable\", or \"I want to test X end-to-end\" with no existing script\n\n## Autonomy Mode\n\nAdapt your behavior based on the autonomy mode:\n\n| Mode | Behavior |\n|------|----------|\n| **Autopilot** | Detect mode, draft, syntax-check, document, and (if escalation tier) run + iterate without intermediate confirmations. Pause only at hard blockers or destructive side-effects. |\n| **Critical** (Default) | Confirm at each tier boundary (after draft, before doc edit, before run). Use AskUserQuestion for fix application during iterate loop. |\n| **Verbose** | Confirm before each step. Show every diff. Walk through detection reasoning out loud. |\n\nThe autonomy mode is passed by the invoking command. If not specified, default to **Critical**.\n\n## Process Steps\n\n### Step 1: Detect Mode (Retrospective vs Forward-Declared)\n\nDecide silently — **do not** ask the user \"which mode?\". Use the following heuristics:\n\n1. **Scan recent session tool-use history** (the last ~20 tool calls in the current conversation) for test/validation-shaped activity: `curl`/`fetch` calls, `bun run`/`python`/`pytest`, database queries, `agent-browser` actions, repeated `grep`/log inspection of a single endpoint or table. If ≥2 such actions targeting the same area exist → **retrospective mode**. Separately, if the history (or the task ahead) shows the *same call shape repeated ~10+ times or a fan-out over a list* → **gather-script mode**: propose replacing the repetition with one `gather-*` script before continuing.\n2. **Parse the user's invocation message** for cues:\n - Narrative cues → retrospective: *\"we just figured out\"*, *\"turn this into a script\"*, *\"wrap that in\"*, *\"that thing we just did\"*.\n - Intent cues → forward-declared: *\"I want to test\"*, *\"validate that\"*, *\"smoke check\"*, *\"check before deploy\"*.\n3. **Fallback**: when ambiguous, default to **forward-declared**.\n\n**Retrospective first action**: summarize the observed activity back to the user as plain text (≤6 lines: what was probed, against what, with what success signal), then go to Step 3 with a confirmation prompt instead of Q&A.\n\n**Forward first action**: skip directly to Step 3's Q&A.\n\n### Step 2: Scan Existing Scripts for Overlap\n\n**Resolve the scripts directory** in this order:\n1. Check `CLAUDE.md` for a `<!-- script-builder:dir=<path> -->` marker. If present, use that path.\n2. If `scripts/` exists at the repo root, use it.\n3. Use **AskUserQuestion** with options: `scripts/ (Recommended) | custom path | skip dir persistence for this run`.\n\n**Edge case — directory absent**: if the resolved path doesn't exist, skip the dedup scan, print a one-line note (`No existing scripts directory at <path> — proceeding to intent gathering`), and remember to optionally offer to create the directory before Step 5.\n\n**When the directory exists**:\n1. List files in the scripts directory (top-level only for v1).\n2. For each file, read the top of the file (header comment) and capture the file-name tokens.\n3. Read `CLAUDE.md`/`AGENTS.md` for `<important if=\"...\">` blocks that point at scripts in this directory and capture the trigger phrases.\n4. Fuzzy-match the current intent string against (file-name tokens ∪ header keywords ∪ trigger phrases). A \"plausible match\" is shared keyword + same area noun (e.g., `auth`, `health`, `webhook`).\n5. If ≥1 plausible match → use **AskUserQuestion** with options: `Reuse <name> as-is | Extend <name> | Generate new anyway`.\n\nNever silently skip dedup on borderline matches — surface and let the user decide. The **Extend** branch appends a sub-command/flag to the existing script (do not create a new file); document the new flag in the script's header comment and the existing `<important if>` block.\n\n### Step 3: Gather Intent\n\nBoth modes converge on the same internal intent structure: `{ what, success_signal, failure_signal, inputs, env, side_effects }`.\n\n**Forward mode** — use **AskUserQuestion** (one or two questions, not five):\n\n| Question | Options |\n|----------|---------|\n| \"What are we validating, and what's the success signal?\" | [Free text — collect what + success signal in one answer] |\n| \"Any required env vars, inputs, or side-effects to flag (e.g., writes to prod, costs money)?\" | [Free text — optional; skip if forward intent already specified them] |\n\n**Retrospective mode** — present the summary from Step 1 and use **AskUserQuestion** with: `That's the flow | Close but fix X | Start over`. On *Close but fix X*, ask for the correction inline; on *Start over*, fall through to forward-mode Q&A.\n\nPersist the resolved intent in working memory (do not write to disk yet).\n\n### Step 4: Detect Language\n\nPriority order — first match wins, with override paths:\n\n1. **TypeScript** — `package.json` + `tsconfig.json` exist.\n - **Bun** if `bun.lock` or `bunfig.toml` is present.\n - else **tsx** if `tsx` appears in `package.json` devDependencies.\n - else **compiled Node** (note in script header: requires `tsc` build).\n2. **Python** — `pyproject.toml` or `uv.lock` or `requirements.txt` exists.\n - **uv** if `uv.lock` exists or `pyproject.toml` has `[tool.uv]` configured (sets `{{UV_METADATA}}`).\n - else **vanilla python3** (`{{UV_METADATA}}` substituted with empty string).\n3. **Both TS and Python detected** — count source files in `src/`/top-level and ask **AskUserQuestion** tiebreaker with the dominant one Recommended.\n4. `Cargo.toml` / `go.mod` only → fall back to **bash** (note: TS/Python/Bash only in v1).\n5. Nothing matched → **bash**.\n\n**Task-driven override**: if the gathered intent is clearly shell-y (\"verify three docker containers respond\", \"tail this log file for an error\"), propose **bash** even in a TS/Python project.\n\nConfirm via **AskUserQuestion** with the detected language as the first option (Recommended). Skip in Autopilot.\n\n### Step 5: Draft the Script\n\n1. **Pick the template**: `templates/{typescript.ts.tmpl|python.py.tmpl|bash.sh.tmpl}`.\n2. **Resolve substitution markers** from gathered intent:\n - `{{SCRIPT_NAME}}` ← proposed file-name stem (see naming table).\n - `{{WHAT}}` / `{{WHEN}}` / `{{ENV}}` / `{{EXAMPLE}}` ← intent fields.\n - `{{UV_METADATA}}` ← per Step 4.\n3. **Generate `{{TEST_BODY}}`** from the intent. Keep it minimal: a single concrete probe + assertion, not a battery. Re-read the templates' README (`templates/README.md`) for the contract — the body must respect the PASS/FAIL surface. Throw/raise/`exit 1` on failure; let the template's outer try/trap convert it into the FAIL line.\n **Gather-class bodies** adapt the same template: the loop/aggregation replaces the probe, raw per-item responses go to the log only, and the final `console.log`/`print` emits one compact summary (JSON or aligned table) instead of the PASS line — exit non-zero only on operational failure (auth, network), not on \"found problems\" (problems ARE the output).\n4. **Propose a file name** matching the intent shape — see the prefix table below. Use **AskUserQuestion** with the proposed name first (Recommended) and `custom name` as the alternative.\n\n**Naming conventions** (advisory — skill proposes, user overrides):\n\n| Prefix | When | Example |\n|--------|------|---------|\n| `e2e-*` | End-to-end flows across multiple components | `e2e-auth-flow.ts` |\n| `check-*` | Idempotent single-probe verifications | `check-db-boundary.sh` |\n| `smoke-*` | Minimal-viability post-deploy checks | `smoke-prod-api.ts` |\n| `measure-*` | Performance / size / token measurements | `measure-tool-tokens.ts` |\n| `seed-*` / `generate-*` | Data seeding or artifact generation (rare for validation) | `seed-api-keys.sh` |\n| `gather-*` / `bulk-*` | Bulk data-gathering or bulk mutation replacing N tool calls; stdout = one summary block, not PASS/FAIL | `gather-workflow-health.ts` |\n\nIf the intent doesn't match any prefix cleanly, propose a free-form name like `validate-<area>.<ext>`.\n\n5. **Write the file** to the resolved scripts directory. Make it executable (`chmod +x`).\n\n### Step 6: Syntax/Type-Check\n\nRun the appropriate checker:\n\n| Language | Checker (in priority order) |\n|----------|-----------------------------|\n| TypeScript | `bunx tsc --noEmit <file>` if Bun present, else `npx tsc --noEmit <file>` |\n| Python | `python3 -m py_compile <file>` (always) + `ruff check <file>` if `ruff` is on PATH |\n| Bash | `shellcheck <file>` if available, else `bash -n <file>` |\n\n**On success**: log a single line (`syntax check OK`) and proceed to Step 7.\n\n**On failure**:\n1. Print the error (≤10 lines, not the full output).\n2. Propose a concrete one-edit fix (the exact `Edit` you'd apply).\n3. Use **AskUserQuestion** with `Apply fix | Investigate | Stop`.\n - **Apply fix** → edit the script and re-run the checker (loop, no hardcoded cap).\n - **Investigate** → drop into discussion with the user; do not auto-apply anything.\n - **Stop** → leave script in place at its path; print the path; do not delete.\n\n### Step 7: Document the Script\n\nAuto-edit the target project's `CLAUDE.md` and/or `AGENTS.md` so future agents discover this script when the matching intent recurs.\n\n**Block template** (the literal markdown the skill emits):\n\n```markdown\n<important if=\"[TRIGGER: e.g., you are testing the auth flow]\">\n\n## [Area] validation\n\nRun `scripts/<name>` to [one-liner]. Requires [env/deps]. Example: `<cmd>`. Full log at `/tmp/<name>-*.log`.\n\nGenerated/maintained via `/script-builder`.\n\n</important>\n```\n\n**Behavior**:\n\n1. **Target file selection**: edit `CLAUDE.md` and `AGENTS.md` if both exist; edit only what exists. **Never** create either file from scratch — if neither exists, print a one-line note (`No CLAUDE.md or AGENTS.md found — script generated but not documented`) and skip this step.\n2. **Placement heuristic**: search the file for the first heading matching `Test|Testing|Validation|Scripts` (case-insensitive). If found, append the new block within/after that section. Otherwise, append a new `## Scripts for testing & validation` section near the end of the file but **before** any heading matching `License|Acknowledg|Maintain|Contributors`.\n3. **Idempotency**: if a block referencing `scripts/<name>` already exists (i.e., a prior `/script-builder` run for the same script name), update it in place — replace the entire `<important if=...>...</important>` block, do not append a duplicate.\n4. **Scripts-dir marker**: if Step 2 resolved the scripts directory by user choice (not from an existing marker), insert `<!-- script-builder:dir=<path> -->` near the top of `CLAUDE.md` (after the title) so subsequent runs are silent. Skip if the marker already exists.\n5. **Scale gate — `scripts/index.md`**: if the scripts directory holds **>10 documented scripts**, per-script `<important if>` blocks become their own context bloat. Generate/maintain a `scripts/index.md` hub instead (one line + link per script, mirroring the `runbooks/` convention from `desplega:engineering-standards`), collapse the CLAUDE.md/AGENTS.md blocks into ONE pointer block referencing the hub, and add new scripts to the hub only.\n6. **Show the diff**: run the equivalent of `git diff CLAUDE.md AGENTS.md` and print a 5-line summary of what changed. **Never auto-stage** — the user commits.\n\n### Step 8: Offer Escalation\n\nAfter the doc edit, decide whether to proceed to the run-and-iterate tier.\n\n| Autonomy | Behavior |\n|----------|----------|\n| **Autopilot** | Auto-escalate to Step 9 unless the intent flagged side-effects (writes to prod, sends real money, mutates a shared resource). On flagged side-effects, fall through to Critical behavior. |\n| **Critical** (Default) | Use **AskUserQuestion**: `Run it now to confirm it works | I'll run it myself later | Just generate, don't run`. |\n| **Verbose** | Same as Critical, plus offer the proposed run command for review before executing. |\n\nIf the user picks \"I'll run it myself later\" or \"Just generate, don't run\" → skip directly to Step 10.\n\n### Step 9: Iterate on Failures (Escalated Tier Only)\n\nA bounded-by-the-human loop. **Never auto-apply a fix** outside Autopilot.\n\n1. **Run the script** with the proposed example invocation (no `--verbose`, no `--json`). Capture exit code and `tail -n 40` of the `/tmp` log.\n2. **If exit code 0** → report PASS (echo the script's PASS line) and proceed to Step 10.\n3. **If exit code non-zero**:\n - Summarize the failure in **1–3 lines**: error class (timeout / 4xx / 5xx / assertion / dependency-missing / etc.) + likely cause (grep the log for known signatures: `ECONNREFUSED`, `Traceback`, `non-zero exit`, `command not found`, etc.).\n - **Propose a concrete diff** — the exact `Edit` you would apply to the script. Show old → new.\n - Use **AskUserQuestion**: `Apply fix | Investigate differently | Stop`.\n - **Apply fix** → edit the script, log the change, loop back to (1).\n - **Investigate differently** → drop into discussion; do not auto-apply.\n - **Stop** → leave the script in place, print its path; do not revert.\n\n**No hardcoded retry cap** — the human is the implicit bound.\n\n**Side-effect flag**: if the intent declared side-effects (Step 3) and Autopilot is active, surface a one-line `WARNING: this script <does X> against <target>` before the first run and require explicit confirmation via **AskUserQuestion** even in Autopilot mode.\n\n### Step 10: Handoff\n\nHow the skill exits depends on how it was invoked:\n\n**Invoked as a sub-skill** (from `planning`, `qa`, `verifying`):\nReturn control to the parent skill with a structured summary: `{ script_path, status: \"pass\"|\"fail\"|\"unrun\", log_path?, doc_files_edited: [...] }`.\n\n**Invoked directly** (`/script-builder`):\nUse **AskUserQuestion**: `Run it with /qa | Run /verify-plan | Commit the script and doc changes | Done`.\n- **Run with /qa** → invoke `desplega:qa` with the script path as the source.\n- **Run /verify-plan** → invoke `desplega:verifying` if a plan path is in current context.\n- **Commit** → propose a commit message (`feat(scripts): add scripts/<name> for <area> validation`) and stage only the generated script + the CLAUDE.md/AGENTS.md edits. Do not auto-commit; show the proposed `git add` and `git commit` commands and require user confirmation.\n- **Done** → print the script path + log path (if escalation ran) and exit.\n\n**Abort path** (Stop selected at any earlier gate): leave the generated script in place, leave any CLAUDE.md/AGENTS.md edit in place if Step 7 ran, and print a one-liner: `Aborted. Script: <path>. Doc edits: <files or \"none\">. Re-run /script-builder or git checkout to discard.` Do not `git restore` on the user's behalf.\n\n## Learning Capture\n\n**OPTIONAL SUB-SKILL:** If significant insights, patterns, gotchas, or decisions emerged during this workflow, consider using `desplega:learning` to capture them via `/learning capture`. Focus on learnings that would help someone else in a future session.\n";
10639
+ var content_default39 = "# script-builder\n\nYou are converting session intent into a durable, re-runnable script committed to the target project's `scripts/` directory. Two script classes share one principle — **only the derived answer re-enters context; raw payloads never do**:\n\n- **Validation scripts** (`check-*`, `e2e-*`, `smoke-*`, …): a single PASS/FAIL line + `/tmp` log path on success, full verbose output redirected to a timestamped log file.\n- **Gather/bulk scripts** (`gather-*`, `bulk-*`): replace N similar tool/API calls with one script that loops, filters, and aggregates *in code* — stdout is one compact summary block (JSON or table), raw responses go to the `/tmp` log. Rubric (from production code-mode data): past ~10 items or any fan-out over a list, a script beats individual tool calls by ~100x on context and roughly halves end-to-end cost. Offer the script *before* the fan-out happens when you can see it coming, not just retrospectively.\n\nBoth classes get an `<important if>` block in `CLAUDE.md`/`AGENTS.md` so future agents discover the script when the intent recurs — durable scripts are reusable agent memory: import, don't re-derive.\n\n**Schema discovery happens in code, too**: when scripting against an unfamiliar API, grep/filter its OpenAPI spec or typed client programmatically to find the few relevant endpoints — never paste the full schema into context.\n\n## Working Agreement\n\nThese instructions establish a working agreement between you and the user. The key principles are:\n\n1. **AskUserQuestion is your primary communication tool** - Whenever you need to ask the user anything (clarifications, preferences, decisions, confirmations), use the **AskUserQuestion tool**. Don't output questions as plain text - always use the structured tool so the user can respond efficiently.\n\n2. **Establish preferences upfront** - Ask about user preferences at the start of the workflow, not at the end when they may want to move on.\n\n3. **Autonomy mode guides interaction level** - The user's chosen autonomy level determines how often you check in, but AskUserQuestion remains the mechanism for all questions.\n\n## When to Use\n\nThis skill activates when:\n- User invokes `/script-builder` command\n- Another skill references `**OPTIONAL SUB-SKILL:** desplega:script-builder`\n- A `planning`, `qa`, or `verifying` flow needs a re-runnable validation script that doesn't yet exist\n- The user expresses intent to \"turn X into a script\", \"wrap this validation into something reusable\", or \"I want to test X end-to-end\" with no existing script\n\n## Autonomy Mode\n\nAdapt your behavior based on the autonomy mode:\n\n| Mode | Behavior |\n|------|----------|\n| **Autopilot** | Detect mode, draft, syntax-check, document, and (if escalation tier) run + iterate without intermediate confirmations. Pause only at hard blockers or destructive side-effects. |\n| **Critical** (Default) | Confirm at each tier boundary (after draft, before doc edit, before run). Use AskUserQuestion for fix application during iterate loop. |\n| **Verbose** | Confirm before each step. Show every diff. Walk through detection reasoning out loud. |\n\nThe autonomy mode is passed by the invoking command. If not specified, default to **Critical**.\n\n## Process Steps\n\n### Step 1: Detect Mode (Retrospective vs Forward-Declared)\n\nDecide silently — **do not** ask the user \"which mode?\". Use the following heuristics:\n\n1. **Scan recent session tool-use history** (the last ~20 tool calls in the current conversation) for test/validation-shaped activity: `curl`/`fetch` calls, `bun run`/`python`/`pytest`, database queries, `agent-browser` actions, repeated `grep`/log inspection of a single endpoint or table. If ≥2 such actions targeting the same area exist → **retrospective mode**. Separately, if the history (or the task ahead) shows the *same call shape repeated ~10+ times or a fan-out over a list* → **gather-script mode**: propose replacing the repetition with one `gather-*` script before continuing.\n2. **Parse the user's invocation message** for cues:\n - Narrative cues → retrospective: *\"we just figured out\"*, *\"turn this into a script\"*, *\"wrap that in\"*, *\"that thing we just did\"*.\n - Intent cues → forward-declared: *\"I want to test\"*, *\"validate that\"*, *\"smoke check\"*, *\"check before deploy\"*.\n3. **Fallback**: when ambiguous, default to **forward-declared**.\n\n**Retrospective first action**: summarize the observed activity back to the user as plain text (≤6 lines: what was probed, against what, with what success signal), then go to Step 3 with a confirmation prompt instead of Q&A.\n\n**Forward first action**: skip directly to Step 3's Q&A.\n\n### Step 2: Scan Existing Scripts for Overlap\n\n**Resolve the scripts directory** in this order:\n1. Check `CLAUDE.md` for a `<!-- script-builder:dir=<path> -->` marker. If present, use that path.\n2. If `scripts/` exists at the repo root, use it.\n3. Use **AskUserQuestion** with options: `scripts/ (Recommended) | custom path | skip dir persistence for this run`.\n\n**Edge case — directory absent**: if the resolved path doesn't exist, skip the dedup scan, print a one-line note (`No existing scripts directory at <path> — proceeding to intent gathering`), and remember to optionally offer to create the directory before Step 5.\n\n**When the directory exists**:\n1. List files in the scripts directory (top-level only for v1).\n2. For each file, read the top of the file (header comment) and capture the file-name tokens.\n3. Read `CLAUDE.md`/`AGENTS.md` for `<important if=\"...\">` blocks that point at scripts in this directory and capture the trigger phrases.\n4. Fuzzy-match the current intent string against (file-name tokens ∪ header keywords ∪ trigger phrases). A \"plausible match\" is shared keyword + same area noun (e.g., `auth`, `health`, `webhook`).\n5. If ≥1 plausible match → use **AskUserQuestion** with options: `Reuse <name> as-is | Extend <name> | Generate new anyway`.\n\nNever silently skip dedup on borderline matches — surface and let the user decide. The **Extend** branch appends a sub-command/flag to the existing script (do not create a new file); document the new flag in the script's header comment and the existing `<important if>` block.\n\n### Step 3: Gather Intent\n\nBoth modes converge on the same internal intent structure: `{ what, success_signal, failure_signal, inputs, env, side_effects }`.\n\n**Forward mode** — use **AskUserQuestion** (one or two questions, not five):\n\n| Question | Options |\n|----------|---------|\n| \"What are we validating, and what's the success signal?\" | [Free text — collect what + success signal in one answer] |\n| \"Any required env vars, inputs, or side-effects to flag (e.g., writes to prod, costs money)?\" | [Free text — optional; skip if forward intent already specified them] |\n\n**Retrospective mode** — present the summary from Step 1 and use **AskUserQuestion** with: `That's the flow | Close but fix X | Start over`. On *Close but fix X*, ask for the correction inline; on *Start over*, fall through to forward-mode Q&A.\n\nPersist the resolved intent in working memory (do not write to disk yet).\n\n### Step 4: Detect Language\n\nPriority order — first match wins, with override paths:\n\n1. **TypeScript** — `package.json` + `tsconfig.json` exist.\n - **Bun** if `bun.lock` or `bunfig.toml` is present.\n - else **tsx** if `tsx` appears in `package.json` devDependencies.\n - else **compiled Node** (note in script header: requires `tsc` build).\n2. **Python** — `pyproject.toml` or `uv.lock` or `requirements.txt` exists.\n - **uv** if `uv.lock` exists or `pyproject.toml` has `[tool.uv]` configured (sets `{{UV_METADATA}}`).\n - else **vanilla python3** (`{{UV_METADATA}}` substituted with empty string).\n3. **Both TS and Python detected** — count source files in `src/`/top-level and ask **AskUserQuestion** tiebreaker with the dominant one Recommended.\n4. `Cargo.toml` / `go.mod` only → fall back to **bash** (note: TS/Python/Bash only in v1).\n5. Nothing matched → **bash**.\n\n**Task-driven override**: if the gathered intent is clearly shell-y (\"verify three docker containers respond\", \"tail this log file for an error\"), propose **bash** even in a TS/Python project.\n\nConfirm via **AskUserQuestion** with the detected language as the first option (Recommended). Skip in Autopilot.\n\n### Step 5: Draft the Script\n\n1. **Pick the template**: `templates/{typescript.ts.tmpl|python.py.tmpl|bash.sh.tmpl}`.\n2. **Resolve substitution markers** from gathered intent:\n - `{{SCRIPT_NAME}}` ← proposed file-name stem (see naming table).\n - `{{WHAT}}` / `{{WHEN}}` / `{{ENV}}` / `{{EXAMPLE}}` ← intent fields.\n - `{{UV_METADATA}}` ← per Step 4.\n3. **Generate `{{TEST_BODY}}`** from the intent. Keep it minimal: a single concrete probe + assertion, not a battery. Re-read the templates' README (`templates/README.md`) for the contract — the body must respect the PASS/FAIL surface. Throw/raise/`exit 1` on failure; let the template's outer try/trap convert it into the FAIL line.\n **Gather-class bodies** adapt the same template: the loop/aggregation replaces the probe, raw per-item responses go to the log only, and the final `console.log`/`print` emits one compact summary (JSON or aligned table) instead of the PASS line — exit non-zero only on operational failure (auth, network), not on \"found problems\" (problems ARE the output).\n4. **Propose a file name** matching the intent shape — see the prefix table below. Use **AskUserQuestion** with the proposed name first (Recommended) and `custom name` as the alternative.\n\n**Naming conventions** (advisory — skill proposes, user overrides):\n\n| Prefix | When | Example |\n|--------|------|---------|\n| `e2e-*` | End-to-end flows across multiple components | `e2e-auth-flow.ts` |\n| `check-*` | Idempotent single-probe verifications | `check-db-boundary.sh` |\n| `smoke-*` | Minimal-viability post-deploy checks | `smoke-prod-api.ts` |\n| `measure-*` | Performance / size / token measurements | `measure-tool-tokens.ts` |\n| `seed-*` / `generate-*` | Data seeding or artifact generation (rare for validation) | `seed-api-keys.sh` |\n| `gather-*` / `bulk-*` | Bulk data-gathering or bulk mutation replacing N tool calls; stdout = one summary block, not PASS/FAIL | `gather-workflow-health.ts` |\n\nIf the intent doesn't match any prefix cleanly, propose a free-form name like `validate-<area>.<ext>`.\n\n5. **Write the file** to the resolved scripts directory. Make it executable (`chmod +x`).\n\n### Step 6: Syntax/Type-Check\n\nRun the appropriate checker:\n\n| Language | Checker (in priority order) |\n|----------|-----------------------------|\n| TypeScript | `bunx tsc --noEmit <file>` if Bun present, else `npx tsc --noEmit <file>` |\n| Python | `python3 -m py_compile <file>` (always) + `ruff check <file>` if `ruff` is on PATH |\n| Bash | `shellcheck <file>` if available, else `bash -n <file>` |\n\n**On success**: log a single line (`syntax check OK`) and proceed to Step 7.\n\n**On failure**:\n1. Print the error (≤10 lines, not the full output).\n2. Propose a concrete one-edit fix (the exact `Edit` you'd apply).\n3. Use **AskUserQuestion** with `Apply fix | Investigate | Stop`.\n - **Apply fix** → edit the script and re-run the checker (loop, no hardcoded cap).\n - **Investigate** → drop into discussion with the user; do not auto-apply anything.\n - **Stop** → leave script in place at its path; print the path; do not delete.\n\n### Step 7: Document the Script\n\nAuto-edit the target project's `CLAUDE.md` and/or `AGENTS.md` so future agents discover this script when the matching intent recurs.\n\n**Block template** (the literal markdown the skill emits):\n\n```markdown\n<important if=\"[TRIGGER: e.g., you are testing the auth flow]\">\n\n## [Area] validation\n\nRun `scripts/<name>` to [one-liner]. Requires [env/deps]. Example: `<cmd>`. Full log at `/tmp/<name>-*.log`.\n\nGenerated/maintained via `/script-builder`.\n\n</important>\n```\n\n**Behavior**:\n\n1. **Target file selection**: edit `CLAUDE.md` and `AGENTS.md` if both exist; edit only what exists. **Never** create either file from scratch — if neither exists, print a one-line note (`No CLAUDE.md or AGENTS.md found — script generated but not documented`) and skip this step.\n2. **Placement heuristic**: search the file for the first heading matching `Test|Testing|Validation|Scripts` (case-insensitive). If found, append the new block within/after that section. Otherwise, append a new `## Scripts for testing & validation` section near the end of the file but **before** any heading matching `License|Acknowledg|Maintain|Contributors`.\n3. **Idempotency**: if a block referencing `scripts/<name>` already exists (i.e., a prior `/script-builder` run for the same script name), update it in place — replace the entire `<important if=...>...</important>` block, do not append a duplicate.\n4. **Scripts-dir marker**: if Step 2 resolved the scripts directory by user choice (not from an existing marker), insert `<!-- script-builder:dir=<path> -->` near the top of `CLAUDE.md` (after the title) so subsequent runs are silent. Skip if the marker already exists.\n5. **Scale gate — `scripts/index.md`**: if the scripts directory holds **>10 documented scripts**, per-script `<important if>` blocks become their own context bloat. Generate/maintain a `scripts/index.md` hub instead (one line + link per script, mirroring the `runbooks/` convention from `desplega:engineering-standards`), collapse the CLAUDE.md/AGENTS.md blocks into ONE pointer block referencing the hub, and add new scripts to the hub only.\n6. **Show the diff**: run the equivalent of `git diff CLAUDE.md AGENTS.md` and print a 5-line summary of what changed. **Never auto-stage** — the user commits.\n\n### Step 8: Offer Escalation\n\nAfter the doc edit, decide whether to proceed to the run-and-iterate tier.\n\n| Autonomy | Behavior |\n|----------|----------|\n| **Autopilot** | Auto-escalate to Step 9 unless the intent flagged side-effects (writes to prod, sends real money, mutates a shared resource). On flagged side-effects, fall through to Critical behavior. |\n| **Critical** (Default) | Use **AskUserQuestion**: `Run it now to confirm it works | I'll run it myself later | Just generate, don't run`. |\n| **Verbose** | Same as Critical, plus offer the proposed run command for review before executing. |\n\nIf the user picks \"I'll run it myself later\" or \"Just generate, don't run\" → skip directly to Step 10.\n\n### Step 9: Iterate on Failures (Escalated Tier Only)\n\nA bounded-by-the-human loop. **Never auto-apply a fix** outside Autopilot.\n\n1. **Run the script** with the proposed example invocation (no `--verbose`, no `--json`). Capture exit code and `tail -n 40` of the `/tmp` log.\n2. **If exit code 0** → report PASS (echo the script's PASS line) and proceed to Step 10.\n3. **If exit code non-zero**:\n - Summarize the failure in **1–3 lines**: error class (timeout / 4xx / 5xx / assertion / dependency-missing / etc.) + likely cause (grep the log for known signatures: `ECONNREFUSED`, `Traceback`, `non-zero exit`, `command not found`, etc.).\n - **Propose a concrete diff** — the exact `Edit` you would apply to the script. Show old → new.\n - Use **AskUserQuestion**: `Apply fix | Investigate differently | Stop`.\n - **Apply fix** → edit the script, log the change, loop back to (1).\n - **Investigate differently** → drop into discussion; do not auto-apply.\n - **Stop** → leave the script in place, print its path; do not revert.\n\n**No hardcoded retry cap** — the human is the implicit bound.\n\n**Side-effect flag**: if the intent declared side-effects (Step 3) and Autopilot is active, surface a one-line `WARNING: this script <does X> against <target>` before the first run and require explicit confirmation via **AskUserQuestion** even in Autopilot mode.\n\n### Step 10: Handoff\n\nHow the skill exits depends on how it was invoked:\n\n**Invoked as a sub-skill** (from `planning`, `qa`, `verifying`):\nReturn control to the parent skill with a structured summary: `{ script_path, status: \"pass\"|\"fail\"|\"unrun\", log_path?, doc_files_edited: [...] }`.\n\n**Invoked directly** (`/script-builder`):\nUse **AskUserQuestion**: `Run it with /qa | Run /verify-plan | Commit the script and doc changes | Done`.\n- **Run with /qa** → invoke `desplega:qa` with the script path as the source.\n- **Run /verify-plan** → invoke `desplega:verifying` if a plan path is in current context.\n- **Commit** → propose a commit message (`feat(scripts): add scripts/<name> for <area> validation`) and stage only the generated script + the CLAUDE.md/AGENTS.md edits. Do not auto-commit; show the proposed `git add` and `git commit` commands and require user confirmation.\n- **Done** → print the script path + log path (if escalation ran) and exit.\n\n**Abort path** (Stop selected at any earlier gate): leave the generated script in place, leave any CLAUDE.md/AGENTS.md edit in place if Step 7 ran, and print a one-liner: `Aborted. Script: <path>. Doc edits: <files or \"none\">. Re-run /script-builder or git checkout to discard.` Do not `git restore` on the user's behalf.\n\n## Learning Capture\n\n**OPTIONAL SUB-SKILL:** If significant insights, patterns, gotchas, or decisions emerged during this workflow, consider using `desplega:learning` to capture them via `/learning capture`. Focus on learnings that would help someone else in a future session.\n";
10620
10640
 
10621
10641
  // templates/skills/script-workflows/config.json
10622
- var config_default38 = `{
10642
+ var config_default40 = `{
10623
10643
  "kind": "skill",
10624
10644
  "name": "script-workflows",
10625
10645
  "displayName": "Script Workflows",
@@ -10636,16 +10656,16 @@ var config_default38 = `{
10636
10656
  `;
10637
10657
 
10638
10658
  // templates/skills/script-workflows/content.md
10639
- var content_default38 = "Use this skill when a user asks to launch, monitor, inspect, or debug a durable script workflow run. This is for the Script Workflows v1 runtime: one-off TypeScript workflow source with journaled `swarm-script`, `raw-llm`, and `agent-task` steps.\n\n## Tool Flow\n\nLoad the script workflow tools with ToolSearch when they are not already visible:\n\n```text\nlaunch-script-run\nget-script-run\nlist-script-runs\n```\n\nUse `launch-script-run` to start a one-off run. It calls the same `/api/script-runs` API as the dashboard, preserves the invoking agent identity, and starts the run in the background.\n\nUse `get-script-run` to read terminal status and journal entries. Poll it when needed, but keep polling bounded and report progress for long runs.\n\nUse `list-script-runs` to find recent runs or filter by `status` / `agentId`.\n\nDo not hand-roll raw HTTP for this flow unless the tool itself is broken and you are explicitly debugging the API. The tool handles auth and `X-Agent-ID` like the existing inline `script-run` tool family.\n\n## Source Shape\n\nAuthor TypeScript workflow source as a default export. The runtime provides `args` and `ctx`.\n\n```ts\nexport default async function main(args, ctx) {\n const lookup = await ctx.step.swarmScript(\"lookup-data\", {\n scriptName: \"fetch-readable\",\n args: { url: args.url },\n });\n\n const summary = await ctx.step.rawLlm(\"summarize\", {\n prompt: `Summarize this for an operator:\\n${JSON.stringify(lookup)}`,\n });\n\n const task = await ctx.step.agentTask(\"operator-review\", {\n task: `Review this summary and flag risks:\\n${JSON.stringify(summary)}`,\n tags: [\"script-run\"],\n priority: 50,\n });\n\n return { lookup, summary, task };\n}\n```\n\n`ctx.step.agentTask` blocks until the dispatched task reaches a terminal status (default `waitForCompletion: true`, default `timeoutMs` 2h) and journals its real output — a sequential `plan → implement → review` chain will not fan out. Pass `waitForCompletion: false` for the legacy fire-and-poll-yourself shape, or `failOnTaskFailure: false` to receive `{taskId, status: \"failed\", error}` instead of a throw when the task fails/is cancelled.\n\n## Label Rules\n\nStep labels are durability keys. They must be stable and unique for each logical step. Do not reuse the same literal label inside a loop; launch will fail with `label_lint_violation`.\n\nFor looped work, include an item identifier in the label:\n\n```ts\nfor (const item of args.items) {\n await ctx.step.agentTask(`review-${item.id}`, { task: item.prompt });\n}\n```\n\n## Statuses\n\nTerminal statuses to surface clearly:\n\n- `completed` — run finished and `output` may be present.\n- `failed` — run ended with `error`.\n- `cancelled` — run was cancelled before completion.\n- `aborted_limit` — runtime guardrail stopped the run, usually step count, agent-task count, or wall-clock cap.\n- `label_lint_violation` — launch-time rejection, not a persisted run status.\n\nWhen a run is not terminal, report the current status, journal count, and latest heartbeat if present.\n";
10659
+ var content_default40 = "Use this skill when a user asks to launch, monitor, inspect, or debug a durable script workflow run. This is for the Script Workflows v1 runtime: one-off TypeScript workflow source with journaled `swarm-script`, `raw-llm`, and `agent-task` steps.\n\n## Tool Flow\n\nLoad the script workflow tools with ToolSearch when they are not already visible:\n\n```text\nlaunch-script-run\nget-script-run\nlist-script-runs\n```\n\nUse `launch-script-run` to start a one-off run. It calls the same `/api/script-runs` API as the dashboard, preserves the invoking agent identity, and starts the run in the background.\n\nUse `get-script-run` to read terminal status and journal entries. Poll it when needed, but keep polling bounded and report progress for long runs.\n\nUse `list-script-runs` to find recent runs or filter by `status` / `agentId`.\n\nDo not hand-roll raw HTTP for this flow unless the tool itself is broken and you are explicitly debugging the API. The tool handles auth and `X-Agent-ID` like the existing inline `script-run` tool family.\n\n## Source Shape\n\nAuthor TypeScript workflow source as a default export. The runtime provides `args` and `ctx`.\n\n```ts\nexport default async function main(args, ctx) {\n const lookup = await ctx.step.swarmScript(\"lookup-data\", {\n scriptName: \"fetch-readable\",\n args: { url: args.url },\n });\n\n const summary = await ctx.step.rawLlm(\"summarize\", {\n prompt: `Summarize this for an operator:\\n${JSON.stringify(lookup)}`,\n });\n\n const task = await ctx.step.agentTask(\"operator-review\", {\n task: `Review this summary and flag risks:\\n${JSON.stringify(summary)}`,\n tags: [\"script-run\"],\n priority: 50,\n });\n\n return { lookup, summary, task };\n}\n```\n\n`ctx.step.agentTask` blocks until the dispatched task reaches a terminal status (default `waitForCompletion: true`, default `timeoutMs` 2h) and journals its real output — a sequential `plan → implement → review` chain will not fan out. Pass `waitForCompletion: false` for the legacy fire-and-poll-yourself shape, or `failOnTaskFailure: false` to receive `{taskId, status: \"failed\", error}` instead of a throw when the task fails/is cancelled.\n\n## Label Rules\n\nStep labels are durability keys. They must be stable and unique for each logical step. Do not reuse the same literal label inside a loop; launch will fail with `label_lint_violation`.\n\nFor looped work, include an item identifier in the label:\n\n```ts\nfor (const item of args.items) {\n await ctx.step.agentTask(`review-${item.id}`, { task: item.prompt });\n}\n```\n\n## Statuses\n\nTerminal statuses to surface clearly:\n\n- `completed` — run finished and `output` may be present.\n- `failed` — run ended with `error`.\n- `cancelled` — run was cancelled before completion.\n- `aborted_limit` — runtime guardrail stopped the run, usually step count, agent-task count, or wall-clock cap.\n- `label_lint_violation` — launch-time rejection, not a persisted run status.\n\nWhen a run is not terminal, report the current status, journal count, and latest heartbeat if present.\n";
10640
10660
 
10641
10661
  // templates/skills/slack-interaction/config.json
10642
- var config_default39 = '{\n "kind": "skill",\n "name": "slack-interaction",\n "displayName": "Slack Interaction",\n "slug": "slack-interaction",\n "title": "Slack Interaction",\n "description": "Slack: reply, post, read, upload, and the thread rules. What the engine already posts, the one-message rule, `slack-reply`, `slack-post`, `slack-start-thread`, `slack-read`, `slack-upload-file`, `slack-download-file`, `slack-list-channels`, `slack-update`, `slack-delete`, unknown-user registration with `manage-user`, and Slack standing orders. Use before any Slack message.",\n "version": "1.0.0",\n "category": "skills",\n "placeholders": [],\n "runAllSeedersCandidate": true,\n "systemDefault": true,\n "tags": ["slack", "communication"]\n}\n';
10662
+ var config_default41 = '{\n "kind": "skill",\n "name": "slack-interaction",\n "displayName": "Slack Interaction",\n "slug": "slack-interaction",\n "title": "Slack Interaction",\n "description": "Slack: reply, post, read, upload, and the thread rules. What the engine already posts, the one-message rule, `slack-reply`, `slack-post`, `slack-start-thread`, `slack-read`, `slack-upload-file`, `slack-download-file`, `slack-list-channels`, `slack-update`, `slack-delete`, unknown-user registration with `manage-user`, and Slack standing orders. Use before any Slack message.",\n "version": "1.0.0",\n "category": "skills",\n "placeholders": [],\n "runAllSeedersCandidate": true,\n "systemDefault": true,\n "tags": ["slack", "communication"]\n}\n';
10643
10663
 
10644
10664
  // templates/skills/slack-interaction/content.md
10645
- var content_default39 = '# Slack interaction\n\n## What the engine posts\n\nFor every task that came from Slack, the engine owns the thread tree and the top-level outcome card. It posts the start, the completion, and the failure. You do not.\n\n## Your one message\n\n- At most one message per task, and only when the outcome card will not carry it: a question, a decision the requester must make, a link to a page or file with one line of context.\n- Progress, receipts, acknowledgments, and relayed worker output stay out of Slack.\n- Concrete content. The length matches what the requester asked for.\n- The "How you write" rules of your system prompt apply. Slack renders mrkdwn: `*bold*`, `_italic_`, `` `code` ``, `<url|text>` for links. Markdown headings and tables do not render.\n\n## Where to post\n\nThe task carries `slackChannelId` and `slackThreadTs` in its metadata. A follow-up task with `parentTaskId` inherits them.\n\n- Reply in the task\'s thread: `slack-reply` with `taskId` and `message`.\n- Reply to an inbox message: `slack-reply` with `inboxMessageId`.\n- New top-level message in a channel (lead): `slack-start-thread` with `channelId`, then `slack-post` with the returned `ts` as `threadTs` for replies under it.\n- Fix your own message: `slack-update` (text) or `slack-delete` (lead). Both work only on messages this bot authored.\n\n## Tools\n\n| Tool | Use |\n|---|---|\n| `slack-reply` | reply in a thread by `taskId` or `inboxMessageId` |\n| `slack-post` | post to a channel, optional `threadTs` (lead) |\n| `slack-start-thread` | post a top-level message and get its `ts` (lead) |\n| `slack-read` | read a thread by `taskId` or `inboxMessageId`, or a channel by `channelId` (lead) |\n| `slack-list-channels` | channels the bot is a member of |\n| `slack-upload-file` | upload a file to a thread or channel, up to 1 GB |\n| `slack-download-file` | attach a Slack file (by ID or URL) to your task; run the `fetchCommand` it returns to get the bytes |\n| `slack-update` | edit your own message |\n| `slack-delete` | delete your own message (lead) |\n| `slack-create-channel`, `slack-invite-to-channel`, `slack-archive-channel` | channel lifecycle (lead) |\n\nFiles attached to your task already come with download commands in the task message. Use those before `slack-download-file`. `slack-read` on your task also attaches the files in the thread and gives each one a `fetchCommand`. Don\'t look for Slack files under `/app/shared` or `/workspace/shared/downloads`: those paths are the API server\'s disk.\n\n## Unknown user\n\nA task without `requestedByUserId` came from a Slack user the swarm does not know. Register them with `manage-user`, using the Slack user ID and display name from the task metadata, then continue. When you learn a requester\'s stable preferences (language, tone, verbosity), the lead stores them in the user\'s `comms` field with `manage-user`.\n\n## Lead standing orders\n\nWhen your heartbeat runbook says so, check for unaddressed requests older than one hour: `slack-list-channels`, then `slack-read` per channel, then create a task for each open request.\n\n## Scripts-only mode\n\nThe named Slack tools are not registered. Use `script-run` with inline source: `ctx.swarm.slack_reply({ taskId, message })`. The task ID carries the thread context.\n';
10665
+ var content_default41 = '# Slack interaction\n\n## What the engine posts\n\nFor every task that came from Slack, the engine owns the thread tree and the top-level outcome card. It posts the start, the completion, and the failure. You do not.\n\n## Your one message\n\n- At most one message per task, and only when the outcome card will not carry it: a question, a decision the requester must make, a link to a page or file with one line of context.\n- Progress, receipts, acknowledgments, and relayed worker output stay out of Slack.\n- Concrete content. The length matches what the requester asked for.\n- The "How you write" rules of your system prompt apply. Slack renders mrkdwn: `*bold*`, `_italic_`, `` `code` ``, `<url|text>` for links. Markdown headings and tables do not render.\n\n## Where to post\n\nThe task carries `slackChannelId` and `slackThreadTs` in its metadata. A follow-up task with `parentTaskId` inherits them.\n\n- Reply in the task\'s thread: `slack-reply` with `taskId` and `message`.\n- Reply to an inbox message: `slack-reply` with `inboxMessageId`.\n- New top-level message in a channel (lead): `slack-start-thread` with `channelId`, then `slack-post` with the returned `ts` as `threadTs` for replies under it.\n- Fix your own message: `slack-update` (text) or `slack-delete` (lead). Both work only on messages this bot authored.\n\n## Tools\n\n| Tool | Use |\n|---|---|\n| `slack-reply` | reply in a thread by `taskId` or `inboxMessageId` |\n| `slack-post` | post to a channel, optional `threadTs` (lead) |\n| `slack-start-thread` | post a top-level message and get its `ts` (lead) |\n| `slack-read` | read a thread by `taskId` or `inboxMessageId`, or a channel by `channelId` (lead) |\n| `slack-list-channels` | channels the bot is a member of |\n| `slack-upload-file` | upload a file to a thread or channel, up to 1 GB |\n| `slack-download-file` | attach a Slack file (by ID or URL) to your task; run the `fetchCommand` it returns to get the bytes |\n| `slack-update` | edit your own message |\n| `slack-delete` | delete your own message (lead) |\n| `slack-create-channel`, `slack-invite-to-channel`, `slack-archive-channel` | channel lifecycle (lead) |\n\nFiles attached to your task already come with download commands in the task message. Use those before `slack-download-file`. `slack-read` on your task also attaches the files in the thread and gives each one a `fetchCommand`. Don\'t look for Slack files under `/app/shared` or `/workspace/shared/downloads`: those paths are the API server\'s disk.\n\n## Unknown user\n\nA task without `requestedByUserId` came from a Slack user the swarm does not know. Register them with `manage-user`, using the Slack user ID and display name from the task metadata, then continue. When you learn a requester\'s stable preferences (language, tone, verbosity), the lead stores them in the user\'s `comms` field with `manage-user`.\n\n## Lead standing orders\n\nWhen your heartbeat runbook says so, check for unaddressed requests older than one hour: `slack-list-channels`, then `slack-read` per channel, then create a task for each open request.\n\n## Scripts-only mode\n\nThe named Slack tools are not registered. Use `script-run` with inline source: `ctx.swarm.slack_reply({ taskId, message })`. The task ID carries the thread context.\n\n## Status and blocker questions\n\nAnswer for the scope of the place the question was asked. In a customer or project channel or thread, "current blockers" or "what\'s the status" means that customer\'s or project\'s work: its tracker, its PRs, its open asks. Answer swarm-wide only in a DM or an ops channel, or when the person asks for it. Name the scope you used in the first line.\n';
10646
10666
 
10647
10667
  // templates/skills/step-running/config.json
10648
- var config_default40 = `{
10668
+ var config_default42 = `{
10649
10669
  "category": "skills",
10650
10670
  "description": "Execute a single DAG step as an autonomous background sub-agent. Sibling of phase-running for DAG plans produced by v-planning. Reads a step-<n>.md file directly, atomically claims it via frontmatter status, runs the three-bucket Success Criteria, and reports back. Spawned by v-implementing or by /run-step.",
10651
10671
  "displayName": "Step Running",
@@ -10666,10 +10686,10 @@ var config_default40 = `{
10666
10686
  `;
10667
10687
 
10668
10688
  // templates/skills/step-running/content.md
10669
- var content_default40 = "# Step Running\n\nYou execute a single step of a DAG plan as an atomic background sub-agent. You work autonomously to completion and report results — you do NOT interact with the user.\n\nThis is the DAG sibling of `phase-running`. The execution model and atomicity contract are identical; the differences are:\n- You receive a **step file path** (`step-<n>.md`), not a plan path + phase number.\n- You **claim the step atomically** via the step's frontmatter `status` field before doing work, and release it on completion / failure. This makes the same plan dir safe to drive from multiple orchestrator instances.\n\n## Execution Model\n\nThis skill runs inside a background `Agent` (sub-agent). v-implementing (or a user invoking `/run-step`) spawns it via the Agent tool with `run_in_background: true`.\n\nThe step agent receives:\n- **Step path**: full path to `step-<n>.md`\n- **Plan dir**: full path to the parent plan directory (read `root.md` from here for plan-level context)\n- **Agent ID**: unique identifier for this sub-agent (e.g. orchestrator session ID + step ID + timestamp). Used for atomic claim + stale-claim detection.\n- **Relevant context**: any extra context from the caller\n\n## Concurrency: Atomic Claim\n\nBefore doing any work, claim the step:\n\n1. Read the step file's frontmatter.\n2. **If `status: done`** — report `Status: skipped` and exit. The step is already finished.\n3. **If `status: claimed`** and `assignee` differs from your agent ID:\n - Fresh claim (`claimed_at` within last hour): report `Status: blocked` (reason: `claimed by <other-id>`) and exit.\n - Stale claim (>1h): proceed and overwrite — the previous worker likely died.\n4. **Otherwise** (`status: ready`, or stale `claimed`): rewrite the frontmatter block in a single `Edit` so the write is atomic-enough for filesystem-based coordination. Set:\n - `status: claimed`\n - `assignee: <your-agent-id>`\n - `claimed_at: <ISO timestamp UTC>`\n5. Re-read the file. If `assignee` is not yours, another worker raced you — report `Status: blocked` (reason: `lost claim race`) and exit.\n\nOn terminal transitions, set frontmatter:\n- **Completed**: `status: done`, clear `assignee` and `claimed_at`.\n- **Blocked or failed (retry-able)**: `status: ready`, clear `assignee` and `claimed_at`. The orchestrator (or another worker) can retry.\n- **Failed (non-retry-able)**: leave `status: claimed` so the orchestrator can investigate; include the reason in the report.\n\nFor true cross-machine atomicity (NFS, etc.), the caller is responsible for rendezvous (e.g. a shared lock service). The single-Edit pattern is good-enough for local filesystem and well-behaved network filesystems.\n\n## Autonomy\n\nStep agents always run as **Autopilot** within the sub-agent. The calling context controls outer autonomy and human checkpoints.\n\n**CRITICAL**: Step agents do NOT use AskUserQuestion. If something is ambiguous, report `Status: blocked` with details. The caller handles all user interaction.\n\n## Process Steps\n\n### Step 1: Load Context\n\n1. **Claim the step** (see \"Concurrency: Atomic Claim\"). Stop here on any non-claim outcome.\n2. Read the full step file.\n3. Read `root.md` from the plan dir for plan-level context: Overview, Current State, Desired End State, Implementation Approach, Global Verification.\n4. Read all files mentioned in the step's \"Changes Required\" section.\n\n### Step 2: Pre-flight Check\n\n| Check | Action if failed |\n|-------|-----------------|\n| All `depends_on` steps have `status: done` | Scheduler bug — release claim, report `blocked` |\n| No merge conflicts in target files | Release claim, report `blocked` |\n| Files/dirs from depended-on steps exist | Release claim, report `blocked` |\n\n### Step 3: Execute Step\n\nImplement the changes described in the step's \"Changes Required\" section. Adapt to minor mismatches; report `blocked` for significant ones.\n\n### Step 4: Run Verification\n\nSame three-bucket pattern as `phase-running`:\n1. **Automated Verification** (runnable commands): run each, record pass/fail. On failure, attempt one fix-and-retry.\n2. **Automated QA** (agent-driven scenarios — browser-use, screenshot diff, CLI walkthrough): execute, record pass/fail per item.\n3. **Manual Verification**: leave unchecked. Caller handles with the user.\n4. **QA Spec (linked doc)**: if present, report `QA Doc: <path>`. Do not execute scenarios inline.\n\n### Step 5: Update Step File\n\n- Check off (`- [x]`) Automated Verification + Automated QA items that passed.\n- Do NOT check off Manual Verification items.\n- Update frontmatter `status` per \"Concurrency: Atomic Claim\" terminal-transition rules.\n- Update `last_updated` / `last_updated_by` frontmatter fields if present.\n\n### Step 6: Report Results\n\n**Completed:**\n```\nStatus: completed\nStep: step-N - <Name>\nFinal frontmatter status: done\nFiles changed: [list]\nAutomated Verification: N/M passed\n- [x] [Check 1] — passed\nAutomated QA: N/M passed\n- [x] [Scenario 1] — passed\nQA Doc: <path> | n/a\nManual verification needed:\n- [ ] [Manual check 1]\n```\n\n**Blocked / failed:** mirror `phase-running`'s shape, plus a `Final frontmatter status:` line so the orchestrator knows whether the step is released for retry (`ready`) or held for investigation (`claimed`).\n\n## Atomicity Contract\n\n- No interactive questions.\n- No partial states left unexplained.\n- Frontmatter `status` always reflects the final state at exit.\n- Report includes enough detail for the caller to decide next steps.\n\n## Context Handoff Pattern\n\n| Direction | Data |\n|-----------|------|\n| **Caller → Step Agent** | Step path + plan dir + agent ID + autonomy mode |\n| **Step Agent reads** | Step file + root.md + files in Changes Required |\n| **Step Agent writes** | Frontmatter status transitions + checkboxes + code changes |\n| **Step Agent → Caller** | Status + changed files + verification results + final frontmatter status |\n| **Caller handles** | Manual verification, cross-step coordination, QA doc execution, human checkpoints |\n";
10689
+ var content_default42 = "# Step Running\n\nYou execute a single step of a DAG plan as an atomic background sub-agent. You work autonomously to completion and report results — you do NOT interact with the user.\n\nThis is the DAG sibling of `phase-running`. The execution model and atomicity contract are identical; the differences are:\n- You receive a **step file path** (`step-<n>.md`), not a plan path + phase number.\n- You **claim the step atomically** via the step's frontmatter `status` field before doing work, and release it on completion / failure. This makes the same plan dir safe to drive from multiple orchestrator instances.\n\n## Execution Model\n\nThis skill runs inside a background `Agent` (sub-agent). v-implementing (or a user invoking `/run-step`) spawns it via the Agent tool with `run_in_background: true`.\n\nThe step agent receives:\n- **Step path**: full path to `step-<n>.md`\n- **Plan dir**: full path to the parent plan directory (read `root.md` from here for plan-level context)\n- **Agent ID**: unique identifier for this sub-agent (e.g. orchestrator session ID + step ID + timestamp). Used for atomic claim + stale-claim detection.\n- **Relevant context**: any extra context from the caller\n\n## Concurrency: Atomic Claim\n\nBefore doing any work, claim the step:\n\n1. Read the step file's frontmatter.\n2. **If `status: done`** — report `Status: skipped` and exit. The step is already finished.\n3. **If `status: claimed`** and `assignee` differs from your agent ID:\n - Fresh claim (`claimed_at` within last hour): report `Status: blocked` (reason: `claimed by <other-id>`) and exit.\n - Stale claim (>1h): proceed and overwrite — the previous worker likely died.\n4. **Otherwise** (`status: ready`, or stale `claimed`): rewrite the frontmatter block in a single `Edit` so the write is atomic-enough for filesystem-based coordination. Set:\n - `status: claimed`\n - `assignee: <your-agent-id>`\n - `claimed_at: <ISO timestamp UTC>`\n5. Re-read the file. If `assignee` is not yours, another worker raced you — report `Status: blocked` (reason: `lost claim race`) and exit.\n\nOn terminal transitions, set frontmatter:\n- **Completed**: `status: done`, clear `assignee` and `claimed_at`.\n- **Blocked or failed (retry-able)**: `status: ready`, clear `assignee` and `claimed_at`. The orchestrator (or another worker) can retry.\n- **Failed (non-retry-able)**: leave `status: claimed` so the orchestrator can investigate; include the reason in the report.\n\nFor true cross-machine atomicity (NFS, etc.), the caller is responsible for rendezvous (e.g. a shared lock service). The single-Edit pattern is good-enough for local filesystem and well-behaved network filesystems.\n\n## Autonomy\n\nStep agents always run as **Autopilot** within the sub-agent. The calling context controls outer autonomy and human checkpoints.\n\n**CRITICAL**: Step agents do NOT use AskUserQuestion. If something is ambiguous, report `Status: blocked` with details. The caller handles all user interaction.\n\n## Process Steps\n\n### Step 1: Load Context\n\n1. **Claim the step** (see \"Concurrency: Atomic Claim\"). Stop here on any non-claim outcome.\n2. Read the full step file.\n3. Read `root.md` from the plan dir for plan-level context: Overview, Current State, Desired End State, Implementation Approach, Global Verification.\n4. Read all files mentioned in the step's \"Changes Required\" section.\n\n### Step 2: Pre-flight Check\n\n| Check | Action if failed |\n|-------|-----------------|\n| All `depends_on` steps have `status: done` | Scheduler bug — release claim, report `blocked` |\n| No merge conflicts in target files | Release claim, report `blocked` |\n| Files/dirs from depended-on steps exist | Release claim, report `blocked` |\n\n### Step 3: Execute Step\n\nImplement the changes described in the step's \"Changes Required\" section. Adapt to minor mismatches; report `blocked` for significant ones.\n\n### Step 4: Run Verification\n\nSame three-bucket pattern as `phase-running`:\n1. **Automated Verification** (runnable commands): run each, record pass/fail. On failure, attempt one fix-and-retry.\n2. **Automated QA** (agent-driven scenarios — browser-use, screenshot diff, CLI walkthrough): execute, record pass/fail per item.\n3. **Manual Verification**: leave unchecked. Caller handles with the user.\n4. **QA Spec (linked doc)**: if present, report `QA Doc: <path>`. Do not execute scenarios inline.\n\n### Step 5: Update Step File\n\n- Check off (`- [x]`) Automated Verification + Automated QA items that passed.\n- Do NOT check off Manual Verification items.\n- Update frontmatter `status` per \"Concurrency: Atomic Claim\" terminal-transition rules.\n- Update `last_updated` / `last_updated_by` frontmatter fields if present.\n\n### Step 6: Report Results\n\n**Completed:**\n```\nStatus: completed\nStep: step-N - <Name>\nFinal frontmatter status: done\nFiles changed: [list]\nAutomated Verification: N/M passed\n- [x] [Check 1] — passed\nAutomated QA: N/M passed\n- [x] [Scenario 1] — passed\nQA Doc: <path> | n/a\nManual verification needed:\n- [ ] [Manual check 1]\n```\n\n**Blocked / failed:** mirror `phase-running`'s shape, plus a `Final frontmatter status:` line so the orchestrator knows whether the step is released for retry (`ready`) or held for investigation (`claimed`).\n\n## Atomicity Contract\n\n- No interactive questions.\n- No partial states left unexplained.\n- Frontmatter `status` always reflects the final state at exit.\n- Report includes enough detail for the caller to decide next steps.\n\n## Context Handoff Pattern\n\n| Direction | Data |\n|-----------|------|\n| **Caller → Step Agent** | Step path + plan dir + agent ID + autonomy mode |\n| **Step Agent reads** | Step file + root.md + files in Changes Required |\n| **Step Agent writes** | Frontmatter status transitions + checkboxes + code changes |\n| **Step Agent → Caller** | Status + changed files + verification results + final frontmatter status |\n| **Caller handles** | Manual verification, cross-step coordination, QA doc execution, human checkpoints |\n";
10670
10690
 
10671
10691
  // templates/skills/swarm-extensions/config.json
10672
- var config_default41 = `{
10692
+ var config_default43 = `{
10673
10693
  "kind": "skill",
10674
10694
  "name": "swarm-extensions",
10675
10695
  "displayName": "Swarm Extensions",
@@ -10686,7 +10706,7 @@ var config_default41 = `{
10686
10706
  `;
10687
10707
 
10688
10708
  // templates/skills/swarm-extensions/content.md
10689
- var content_default41 = `# Swarm Extensions
10709
+ var content_default43 = `# Swarm Extensions
10690
10710
 
10691
10711
  An extension is a trusted TypeScript hook bundle that runs inside the swarm API server. Use one when a rule must apply to the whole swarm: block or rewrite new tasks, route Slack channels, suppress follow-ups, change stalled-task remediation, guard tool calls, or react after a task or tool call finishes.
10692
10712
 
@@ -10735,10 +10755,12 @@ curl -s "$MCP_BASE_URL/api/extensions/type-defs" \\
10735
10755
  | \`pre.task.create\` | before any task is stored (REST, MCP, Slack, schedule, workflow, webhook, follow-up) | \`block(reason)\`, \`modify({ priority, agentId, tags, description, ... })\`, or nothing |
10736
10756
  | \`pre.task.followUp\` | before the lead follow-up task for a finished worker task | \`block\`, \`modify({ description, priority, agentId })\`, or nothing |
10737
10757
  | \`pre.slack.route\` | before a Slack message becomes a task | \`modify({ target: { kind: "agent", agentId } })\`, \`{ kind: "lead" }\`, \`{ kind: "broadcast" }\` |
10758
+ | \`pre.chat.route\` | before buffered Chat SDK messages (Telegram, Slack, Discord, Teams) become a task | \`block(reason)\`, or \`modify({ target: { kind: "agent", agentId } })\`, \`{ kind: "lead" }\`, \`{ kind: "broadcast" }\`, \`{ kind: "workflow", workflowId }\` |
10738
10759
  | \`pre.heartbeat.remediate\` | after the heartbeat sweep finds a stalled task | \`modify({ proposedAction: "record" | "fail" | "supersede-resume" })\` |
10739
10760
  | \`pre.tool.call\` | before an agent MCP tool call runs | \`block(reason)\` or \`modify({ args })\` |
10740
10761
  | \`post.task.created\`, \`post.task.completed\`, \`post.task.failed\`, \`post.task.cancelled\`, \`post.task.superseded\`, \`post.task.progress\` | after the change is committed | none |
10741
10762
  | \`post.slack.message\`, \`post.tool.call\` | after the message or tool call finished | none |
10763
+ | \`post.chat.message\` | after a Chat SDK message is received (before buffering) or sent; \`direction\` is \`inbound\` or \`outbound\` | none |
10742
10764
  | \`post.approval.resolved\` | after a person answers an approval request, workflow or standalone (not on timeout or cancel) | none |
10743
10765
  | \`post.task.budgetRefused\` | after a task is refused by a daily spend budget | none |
10744
10766
  | \`post.email.received\`, \`post.kapso.message\` | after an inbound AgentMail or Kapso message is received | none |
@@ -10865,14 +10887,14 @@ export default extension;
10865
10887
  `;
10866
10888
 
10867
10889
  // templates/skills/swarm-scripts/config.json
10868
- var config_default42 = `{
10890
+ var config_default44 = `{
10869
10891
  "kind": "skill",
10870
10892
  "name": "swarm-scripts",
10871
10893
  "displayName": "Swarm Scripts",
10872
10894
  "slug": "swarm-scripts",
10873
10895
  "title": "Swarm Scripts",
10874
10896
  "description": "Bulk, repeat, fan-out, or data-heavy work: write and run swarm scripts (inline \`script-run\`, named \`script-upsert\`, durable \`launch-script-run\`). Covers the script-vs-tool rubric, the authoring contract (\`args\` first, \`ctx\` second), the seed catalog, connections and secrets, \`db_query\`, and exposing a script as an API.",
10875
- "version": "1.1.0",
10897
+ "version": "1.2.0",
10876
10898
  "category": "skills",
10877
10899
  "placeholders": [],
10878
10900
  "runAllSeedersCandidate": true,
@@ -10882,7 +10904,7 @@ var config_default42 = `{
10882
10904
  `;
10883
10905
 
10884
10906
  // templates/skills/swarm-scripts/content.md
10885
- var content_default42 = `# Swarm Scripts
10907
+ var content_default44 = `# Swarm Scripts
10886
10908
 
10887
10909
  A swarm script is TypeScript that runs out of process with a typed Swarm SDK. Only its return value enters your context. Use one when direct tool calls would repeat, flood your context, or need deterministic processing over many records.
10888
10910
 
@@ -10955,7 +10977,7 @@ export default async function (args: z.infer<typeof argsSchema>, ctx: ScriptCont
10955
10977
 
10956
10978
  - \`ctx.swarm.*\`: the swarm SDK. \`task_get\`, \`task_send\`, \`task_storeProgress\`, \`task_action\`, \`task_list\`, \`slack_reply\`, \`memory_search\`, \`memory_store\`, \`kv_get\`, \`kv_getOrNull\`, \`kv_set\`, \`kv_delete\`, \`kv_incr\`, \`kv_list\`, \`swarm_get\`, \`agent_info\`, \`db_query\`, and more. \`kv_getOrNull\` returns the entry, \`null\` on a missing key, and throws on other errors.
10957
10979
  - \`ctx.swarm.task_send\` requires \`routingReason\` and \`routingNote\` whenever \`agentId\` is present. The note must explain the worker choice in at least 10 characters after trimming whitespace (maximum 200 characters). Use \`human_pinned\` for an explicit/configured choice, \`skill\` for a role or specialization match, \`continuity\` for the same worker/session, \`reroute_fault\` for a fault handoff, or \`overflow\` for capacity/pool escalation. Omit both \`agentId\` and routing fields for the pool.
10958
- - \`ctx.swarm.config\`: \`apiKey\`, \`agentId\`, \`mcpBaseUrl\`, and \`ctx.swarm.config.get("KEY")\` for user config values. All are \`Redacted\` wrappers that stringify to \`<redacted>\`. Never unwrap one into a return value, a log line, or a request body you build by hand.
10980
+ - \`ctx.swarm.config\`: \`apiKey\`, \`agentId\`, \`mcpBaseUrl\`, and \`ctx.swarm.config.get("KEY")\` for swarm config values, resolved for the agent running the script (agent scope over global, the same rows \`get-config\` returns for that agent). A key that is not set returns \`undefined\`, so \`config.get("KEY") ?? fallback\` works. All are \`Redacted\` wrappers that stringify to \`<redacted>\`. Never unwrap one into a return value, a log line, or a request body you build by hand.
10959
10981
  - \`ctx.api.<slug>\` and \`ctx.mcp.<slug>\`: typed clients for registered connections. They exist only for registered connections. Introspect with \`Object.keys(ctx.api ?? {})\` and \`Object.keys(ctx.mcp ?? {})\`.
10960
10982
  - \`ctx.stdlib\`: \`fetch\`, \`fetchJson\` (retries, 30 s timeout), \`grep\`, \`glob\`, \`table\`, \`Redacted\`.
10961
10983
  - \`ctx.logger\`: \`log\`, \`warn\`, \`error\`. Keep logs short.
@@ -10986,11 +11008,13 @@ export default async function main(args: { status?: string; limit?: number } | u
10986
11008
 
10987
11009
  ## Secrets
10988
11010
 
11011
+ A config row reaches your shell env only when its \`exportToEnv\` flag is on. If \`$KEY\` is empty in your shell but \`get-config\` or \`list-config\` shows the row marked \`not in agent env\`, the row stays server-side on purpose: do not copy the value into a file, an env var, or a command. Use it from a script instead, through one of the paths below. Harness, git, and agent-fs credentials stay exported. Keys only the API server reads (Slack app token and signing secret, GitHub App private key, webhook and OAuth client secrets) can never be exported. If a recipe genuinely needs the variable in a shell, ask the lead to turn \`exportToEnv\` on for that row (\`set-config\` with \`exportToEnv: true\`); it applies to your next task.
11012
+
10989
11013
  Order of preference:
10990
11014
 
10991
11015
  1. A registered connection: \`ctx.api.<slug>.<operationId>({ path, query, header, body })\` (OpenAPI), \`ctx.api.<slug>.graphql(query, variables)\` (GraphQL), \`ctx.mcp.<slug>.<toolName>(args)\` (MCP, proxied server-side, returns the raw MCP envelope: read \`res.structuredContent ?? res.content?.[0]?.text\`). Credentials attach at egress. The script never sees them. OAuth tokens refresh on their own.
10992
11016
  2. A credential binding placeholder in a hand-written request: \`Authorization: Bearer [REDACTED:GITHUB_TOKEN]\`. The runtime substitutes the value at egress, only for the binding's allowed hosts.
10993
- 3. \`ctx.swarm.config.get("KEY")\` for a config value you pass to \`ctx.stdlib.fetch\` as a header value. Still \`Redacted\`; never log it.
11017
+ 3. \`ctx.swarm.config.get("KEY")\` for a config value you pass to \`ctx.stdlib.fetch\` as a header value. Unwrap it with \`ctx.stdlib.Redacted.value(...)\` inside the header itself (a wrapped value sends the literal \`<redacted>\`), and never log it. Secret values are scrubbed from run output.
10994
11018
 
10995
11019
  A raw secret, or the output of \`get-config\` with \`includeSecrets: true\`, must not appear in script source, script \`args\`, a schedule's \`taskTemplate\` or \`scriptArgs\`, or a task description. Those are stored as plain text and replayed on every run.
10996
11020
 
@@ -11049,7 +11073,7 @@ A named script can serve \`POST /api/x/script/<id>\` for callers outside the swa
11049
11073
  `;
11050
11074
 
11051
11075
  // templates/skills/tackle-gh-comments/config.json
11052
- var config_default43 = `{
11076
+ var config_default45 = `{
11053
11077
  "category": "skills",
11054
11078
  "description": "Work through every review comment on a GitHub PR — fetch the threads, verify each claim against the code, fix what is real, then reply and mark each thread resolved before pushing. Use when the user says \\"tackle the PR comments\\", \\"address the review\\", \\"handle the bot comments\\", \\"check the comments on the PR\\", \\"resolve the review threads\\", or invokes /tackle-gh-comments. Project-agnostic; works in any repo with a GitHub remote.",
11055
11079
  "displayName": "Tackle GH Comments",
@@ -11070,7 +11094,7 @@ var config_default43 = `{
11070
11094
  `;
11071
11095
 
11072
11096
  // templates/skills/tackle-gh-comments/content.md
11073
- var content_default43 = `# Tackle GitHub PR comments
11097
+ var content_default45 = `# Tackle GitHub PR comments
11074
11098
 
11075
11099
  Review threads and their resolved state live **only in GraphQL**. The REST API
11076
11100
  cannot read \`isResolved\` and cannot resolve a thread, and \`gh pr view --comments\`
@@ -11189,7 +11213,7 @@ committed, and the branch is pushed.
11189
11213
  `;
11190
11214
 
11191
11215
  // templates/skills/taste-minimalist-skill/config.json
11192
- var config_default44 = `{
11216
+ var config_default46 = `{
11193
11217
  "kind": "skill",
11194
11218
  "name": "taste-minimalist-skill",
11195
11219
  "displayName": "Taste Minimalist Skill",
@@ -11211,7 +11235,7 @@ var config_default44 = `{
11211
11235
  `;
11212
11236
 
11213
11237
  // templates/skills/taste-minimalist-skill/content.md
11214
- var content_default44 = `> Vendored from [taste-skill](https://github.com/Leonxlnx/taste-skill) / [tasteskill.dev](https://www.tasteskill.dev/) at commit 06d6028b5c623016c59ce8536f578e5a1127b499.
11238
+ var content_default46 = `> Vendored from [taste-skill](https://github.com/Leonxlnx/taste-skill) / [tasteskill.dev](https://www.tasteskill.dev/) at commit 06d6028b5c623016c59ce8536f578e5a1127b499.
11215
11239
  > Upstream license: MIT, Copyright (c) 2026 Leonxlnx. See ../TASTE-SKILL-LICENSE.
11216
11240
 
11217
11241
  # Protocol: Premium Utilitarian Minimalism UI Architect
@@ -11297,7 +11321,7 @@ When tasked with writing frontend code (HTML, React, Tailwind, Vue) or designing
11297
11321
  `;
11298
11322
 
11299
11323
  // templates/skills/tdd-planning/config.json
11300
- var config_default45 = `{
11324
+ var config_default47 = `{
11301
11325
  "category": "skills",
11302
11326
  "description": "TDD-focused implementation planning. Creates plans with strict Red-Green-Commit/Rollback cycles for each step.",
11303
11327
  "displayName": "TDD Planning",
@@ -11318,7 +11342,7 @@ var config_default45 = `{
11318
11342
  `;
11319
11343
 
11320
11344
  // templates/skills/tdd-planning/content.md
11321
- var content_default45 = `# TDD Planning
11345
+ var content_default47 = `# TDD Planning
11322
11346
 
11323
11347
  You are creating implementation plans that follow a strict Test-Driven Development approach. Every implementation step follows the TDD cycle: **RED → GREEN → COMMIT/ROLLBACK**.
11324
11348
 
@@ -11580,7 +11604,7 @@ Before finalizing any TDD plan, verify:
11580
11604
  `;
11581
11605
 
11582
11606
  // templates/skills/v-implementing/config.json
11583
- var config_default46 = `{
11607
+ var config_default48 = `{
11584
11608
  "category": "skills",
11585
11609
  "description": "Parallel DAG-plan implementation skill. Reads a v-planning plan directory (root.md + step-<n>.md files), topologically schedules ready steps, and fans them out as parallel sub-agents. Use whenever the user invokes /v-implement, points at a plan directory produced by /v-plan, or asks to \\"run the parallel plan\\", \\"implement the DAG\\", or \\"fan out the steps\\" — even without those exact words. For linear plans (single .md file), use \`implementing\` instead.",
11586
11610
  "displayName": "V-Implementing",
@@ -11601,10 +11625,10 @@ var config_default46 = `{
11601
11625
  `;
11602
11626
 
11603
11627
  // templates/skills/v-implementing/content.md
11604
- var content_default46 = "# v-implementing\n\nYou are implementing an approved DAG-structured plan produced by `v-planning`. The plan is a directory with `root.md` + one `step-<n>.md` per node. Your job is to act as a **topological scheduler**: at each tick, find steps whose dependencies are all done, fan them out as parallel sub-agents (one per ready step), wait, repeat — until the DAG is drained. Then run Global Verification.\n\nThis is the parallel sibling of `implementing`. The orchestration model is the same (sub-agents do the work, main session coordinates), only the scheduler is different.\n\n## Working Agreement\n\n**All user-facing questions go through `AskUserQuestion`** — see `desplega:ask-user` for conventions. Never ask in chat as plain bullets.\n\n**All read/research/validation work goes through sub-agents** — keep raw tool output out of the main session. Default to `run_in_background: true`. The fan-out scheduler below is built on this.\n\nThe autonomy mode (below) controls how often you check in. AskUserQuestion is always the mechanism.\n\nFile-review is on by default — when significant changes land in a step (or wave), invoke `/file-review:file-review <path>` for inline feedback (skip only if Autopilot).\n\n## Autonomy Mode\n\n| Mode | Behavior |\n|------|----------|\n| **Autopilot** | Drain the DAG without pausing. Only stop on blocker / failed step. |\n| **Critical** (Default) | Pause when each *wave* of parallel steps completes; wait for manual verification before unlocking the next wave. |\n| **Verbose** | Pause after each individual step (even within a wave). |\n\n## Initial Setup Questions\n\nAfter understanding the plan and before the scheduler loop starts, gather implementation-specific details (skip in Autopilot):\n\n### 1. Branch / Worktree Setup\n\nCheck the current branch: `git branch --show-current`. Then check if the `wts` plugin is installed (look for `wts:wts` in available skills).\n\n**If wts is installed**, use **AskUserQuestion**:\n\n| Question | Options |\n|----------|---------|\n| \"You're on `<current-branch>`. Where would you like to implement?\" | 1. Continue on current branch, 2. Create a new branch, 3. Create a wts worktree |\n\n**If wts is not installed**, drop the worktree option.\n\n### 2. Commit Strategy\n\nUse **AskUserQuestion**:\n\n| Question | Options |\n|----------|---------|\n| \"How would you like to handle commits during parallel implementation?\" | 1. Commit after each step completes (Recommended), 2. Commit at the end (single commit), 3. Let me decide as I go |\n\nIf \"Commit after each step\" is selected, after a step's manual verification passes, create a commit: `[step-N] <step name>`.\n\n### 3. Orchestration Mode (only if the `Workflow` tool is available)\n\n| Question | Options |\n|----------|---------|\n| \"Run the DAG waves as Workflow scripts? (deterministic scheduling, parallel steps as agent() calls)\" | 1. Yes — Workflow waves, 2. No — Agent fan-out (Default) |\n\nA \"yes\" is the explicit opt-in the Workflow tool requires. If opted in, each wave of the scheduler loop below runs as one Workflow script — a `parallel()` of `agent()` calls, one per ready step, with `model`/`effort` per `desplega:delegate-work`. Everything else is unchanged: step agents still follow the `step-running` contract (atomic frontmatter claim, three-bucket verification), and autonomy-mode pause points sit **between** Workflow invocations, never inside one. In Autopilot these questions are skipped, so there is no opt-in — use Agent fan-out unless the user pre-authorized workflows.\n\n### 4. Code Review Mode\n\n| Question | Options |\n|----------|---------|\n| \"Run an automatic code review after each wave?\" | 1. Automatic two-axis review per wave (Recommended), 2. Only at the end (single review after the DAG drains), 3. Off — I'll review myself |\n\nIn **Autopilot**, skip the question and default to automatic per-wave review.\n\n## Getting Started\n\nGiven a plan directory path:\n\n1. **Read `root.md` fully** (no `limit`/`offset`). Capture: Overview, Current State, Desired End State, Implementation Approach, Global Verification.\n2. **Read every `step-<n>.md`'s frontmatter** to build the dependency graph (`id`, `depends_on`). You don't need to read step bodies up front — the sub-agents will do that.\n3. **Validate the DAG:**\n - No cycles\n - Every `depends_on` ID exists as a step file\n - At least one step has `depends_on: []` (otherwise nothing can start)\n4. **Set `root.md` frontmatter `status: in-progress`.**\n5. **Build a TodoWrite list** with one entry per step, marked `pending`.\n6. **Resume support:** if any step's body has `- [x]` boxes already (or its frontmatter says `status: completed` if the user added it), trust them and treat that step as `done`.\n\nIf no path given, ask via AskUserQuestion.\n\n## The Scheduler Loop\n\nThe scheduler reads each step's frontmatter `status` (`ready` | `claimed` | `done`) and `depends_on`. The frontmatter `status` field is the **single source of truth** — multiple orchestrator instances on the same plan dir coordinate through it (each `desplega:step-running` sub-agent atomically claims its step before doing work).\n\n```\nwhile any step has status != done:\n ready = [step for step in steps\n if step.status == \"ready\"\n and all(dep.status == \"done\" for dep in step.depends_on)]\n if not ready:\n # DAG drained, OR every remaining undone step is claimed by another worker\n if any step has status == claimed: wait for in-flight claims to resolve\n else: report stuck (likely cycle or unrecoverable failure)\n continue\n fan out each step in `ready` as a parallel `desplega:step-running` sub-agent\n wait for the wave to complete\n review reports; step-running has already updated frontmatter\n (status: done on success, status: ready on retry-able failure, status: claimed if held for investigation)\n if Critical mode: pause for manual verification of newly-completed steps\n if any failed: stop and ask user how to proceed\n```\n\n### Spawning a Step Sub-agent\n\nUse the `Agent` tool with `run_in_background: true`, invoking `desplega:step-running`. Pass:\n\n- **Step path**: full path to `step-<n>.md`\n- **Plan dir**: full path to the parent plan directory\n- **Agent ID**: unique ID for this sub-agent (e.g. orchestrator session ID + step ID + timestamp). step-running uses this for atomic claim + stale-claim detection.\n- **Plan-level context** (optional): a quick brief from `root.md`. step-running re-reads `root.md` itself, so this is just to reduce round-trips.\n\n`step-running` owns:\n- Atomic claim (rewrite frontmatter `status: ready` → `status: claimed, assignee: <agent-id>, claimed_at: <ts>`)\n- Three-bucket verification (Automated Verification, Automated QA, QA Doc identification)\n- Terminal status transition (`status: done` on success, `status: ready` on retry-able failure)\n- Reporting back\n\nThe orchestrator does NOT do step work itself — it delegates. See `desplega:step-running` for the full sub-agent contract.\n\n**Executor routing**: if the `desplega:delegate-work` skill is available, pick each step's executor per its routing matrix instead of defaulting to a `step-running` sub-agent — a step may route to a Codex variant (one worktree per parallel slice) or a specific Claude model tier. Only the executor choice changes; the scheduler, frontmatter claim protocol, and all other semantics here stay unchanged. When a step routes to Codex, the orchestrator owns the frontmatter bookkeeping that `step-running` would normally do (Codex must not edit plan files).\n\n### Wave Completion\n\nWhen a wave finishes:\n1. **Review each agent's report.** Mark steps `done` only if `completed` was reported.\n2. **Run the wave code review** (unless review mode is Off) — invoke `desplega:code-reviewing` on the wave's combined diff: Standards + Spec axes as parallel background sub-agents (routed per `desplega:delegate-work`), spec source = the completed steps' bodies and Success Criteria. Critical findings block those steps' commits — fix and re-verify first. If \"Only at the end\" was selected, run one review over the full diff after the DAG drains instead.\n3. **Handle `QA Doc: <path>`** — for each step that reported a QA doc, invoke `desplega:qa` against that path. The Automated QA bucket inside the step is already handled by the sub-agent; only the linked doc needs separate orchestration here. (`QA: n/a` → proceed normally.)\n4. **Manual verification (if not Autopilot)** — present manual verification items from each completed step's body. Wait for user confirmation. Don't tick manual boxes until confirmed.\n5. **Commits (if commit-per-step was selected)** — after a step's manual verification passes, create a commit: `[step-N] <step name>`.\n6. **Loop** — recompute `ready` and start the next wave.\n\n## Handling Failures and Mismatches\n\nIf a sub-agent reports `failed` or `blocked`, or you spot a mismatch between plan and reality:\n\n| Question | Options |\n|----------|---------|\n| \"[step-N] [issue]. How should I proceed?\" | 1. Adapt step (edit step-N.md and retry), 2. Retry as-is, 3. Skip and continue (mark step blocked), 4. Stop the run |\n\nIn Autopilot mode, use best judgment, document the decision in the step file, and continue if non-fatal.\n\n## After the DAG Drains\n\nWhen every step is `done`:\n\n1. Run **Global Verification** from `root.md`. Tick automated checks; surface manual checks to the user.\n2. Set `root.md` frontmatter `status: completed`.\n3. Offer post-implementation auditing: \"Would you like me to run `/verify-plan` and `/review` on the plan directory?\"\n4. If commit-per-step was off, offer to create a single bundled commit now.\n5. If the work ships as a GitHub PR: once review comments land, address them with `desplega:tackle-gh-comments` (fetch threads, verify each claim, fix, reply, resolve — push last).\n\n## Important Guidelines\n\n1. **One step = one sub-agent.** Don't bundle steps into a single agent even if they're in the same wave — fan-out is the whole point.\n2. **Plan-level context goes to every sub-agent.** Sibling step bodies do not.\n3. **The DAG is canonical via step frontmatter** — if `root.md`'s table is out of sync, trust the frontmatter.\n4. **Resume works step-granular.** If the user re-runs `/v-implement` mid-DAG, completed steps stay completed; only undone ones rerun.\n5. **Don't tick manual checkboxes** until the user confirms — automated boxes can be ticked by the sub-agent or the orchestrator.\n\n## Review Integration\n\nIf `file-review` is available and the user opted in:\n- After each step's significant code changes, invoke `/file-review:file-review <changed-file>`.\n- Process feedback with `file-review:process-review` before treating the step as done.\n- Skip in Autopilot.\n";
11628
+ var content_default48 = "# v-implementing\n\nYou are implementing an approved DAG-structured plan produced by `v-planning`. The plan is a directory with `root.md` + one `step-<n>.md` per node. Your job is to act as a **topological scheduler**: at each tick, find steps whose dependencies are all done, fan them out as parallel sub-agents (one per ready step), wait, repeat — until the DAG is drained. Then run Global Verification.\n\nThis is the parallel sibling of `implementing`. The orchestration model is the same (sub-agents do the work, main session coordinates), only the scheduler is different.\n\n## Working Agreement\n\n**All user-facing questions go through `AskUserQuestion`** — see `desplega:ask-user` for conventions. Never ask in chat as plain bullets.\n\n**All read/research/validation work goes through sub-agents** — keep raw tool output out of the main session. Default to `run_in_background: true`. The fan-out scheduler below is built on this.\n\nThe autonomy mode (below) controls how often you check in. AskUserQuestion is always the mechanism.\n\nFile-review is on by default — when significant changes land in a step (or wave), invoke `/file-review:file-review <path>` for inline feedback (skip only if Autopilot).\n\n## Autonomy Mode\n\n| Mode | Behavior |\n|------|----------|\n| **Autopilot** | Drain the DAG without pausing. Only stop on blocker / failed step. |\n| **Critical** (Default) | Pause when each *wave* of parallel steps completes; wait for manual verification before unlocking the next wave. |\n| **Verbose** | Pause after each individual step (even within a wave). |\n\n## Initial Setup Questions\n\nAfter understanding the plan and before the scheduler loop starts, gather implementation-specific details (skip in Autopilot):\n\n### 1. Branch / Worktree Setup\n\nCheck the current branch: `git branch --show-current`. Then check if the `wts` plugin is installed (look for `wts:wts` in available skills).\n\n**If wts is installed**, use **AskUserQuestion**:\n\n| Question | Options |\n|----------|---------|\n| \"You're on `<current-branch>`. Where would you like to implement?\" | 1. Continue on current branch, 2. Create a new branch, 3. Create a wts worktree |\n\n**If wts is not installed**, drop the worktree option.\n\n### 2. Commit Strategy\n\nUse **AskUserQuestion**:\n\n| Question | Options |\n|----------|---------|\n| \"How would you like to handle commits during parallel implementation?\" | 1. Commit after each step completes (Recommended), 2. Commit at the end (single commit), 3. Let me decide as I go |\n\nIf \"Commit after each step\" is selected, after a step's manual verification passes, create a commit: `[step-N] <step name>`.\n\n### 3. Orchestration Mode (only if the `Workflow` tool is available)\n\n| Question | Options |\n|----------|---------|\n| \"Run the DAG waves as Workflow scripts? (deterministic scheduling, parallel steps as agent() calls)\" | 1. Yes — Workflow waves, 2. No — Agent fan-out (Default) |\n\nA \"yes\" is the explicit opt-in the Workflow tool requires. If opted in, each wave of the scheduler loop below runs as one Workflow script — a `parallel()` of `agent()` calls, one per ready step, with `model`/`effort` per `desplega:delegate-work`. Everything else is unchanged: step agents still follow the `step-running` contract (atomic frontmatter claim, three-bucket verification), and autonomy-mode pause points sit **between** Workflow invocations, never inside one. In Autopilot these questions are skipped, so there is no opt-in — use Agent fan-out unless the user pre-authorized workflows.\n\n### 4. Code Review Mode\n\n| Question | Options |\n|----------|---------|\n| \"Run an automatic code review after each wave?\" | 1. Automatic two-axis review per wave (Recommended), 2. Only at the end (single review after the DAG drains), 3. Off — I'll review myself |\n\nIn **Autopilot**, skip the question and default to automatic per-wave review.\n\n## Getting Started\n\nGiven a plan directory path:\n\n1. **Read `root.md` fully** (no `limit`/`offset`). Capture: Overview, Current State, Desired End State, Implementation Approach, Global Verification.\n2. **Read every `step-<n>.md`'s frontmatter** to build the dependency graph (`id`, `depends_on`). You don't need to read step bodies up front — the sub-agents will do that.\n3. **Validate the DAG:**\n - No cycles\n - Every `depends_on` ID exists as a step file\n - At least one step has `depends_on: []` (otherwise nothing can start)\n4. **Set `root.md` frontmatter `status: in-progress`.**\n5. **Build a TodoWrite list** with one entry per step, marked `pending`.\n6. **Resume support:** if any step's body has `- [x]` boxes already (or its frontmatter says `status: completed` if the user added it), trust them and treat that step as `done`.\n\nIf no path given, ask via AskUserQuestion.\n\n## The Scheduler Loop\n\nThe scheduler reads each step's frontmatter `status` (`ready` | `claimed` | `done`) and `depends_on`. The frontmatter `status` field is the **single source of truth** — multiple orchestrator instances on the same plan dir coordinate through it (each `desplega:step-running` sub-agent atomically claims its step before doing work).\n\n```\nwhile any step has status != done:\n ready = [step for step in steps\n if step.status == \"ready\"\n and all(dep.status == \"done\" for dep in step.depends_on)]\n if not ready:\n # DAG drained, OR every remaining undone step is claimed by another worker\n if any step has status == claimed: wait for in-flight claims to resolve\n else: report stuck (likely cycle or unrecoverable failure)\n continue\n fan out each step in `ready` as a parallel `desplega:step-running` sub-agent\n wait for the wave to complete\n review reports; step-running has already updated frontmatter\n (status: done on success, status: ready on retry-able failure, status: claimed if held for investigation)\n if Critical mode: pause for manual verification of newly-completed steps\n if any failed: stop and ask user how to proceed\n```\n\n### Spawning a Step Sub-agent\n\nUse the `Agent` tool with `run_in_background: true`, invoking `desplega:step-running`. Pass:\n\n- **Step path**: full path to `step-<n>.md`\n- **Plan dir**: full path to the parent plan directory\n- **Agent ID**: unique ID for this sub-agent (e.g. orchestrator session ID + step ID + timestamp). step-running uses this for atomic claim + stale-claim detection.\n- **Plan-level context** (optional): a quick brief from `root.md`. step-running re-reads `root.md` itself, so this is just to reduce round-trips.\n\n`step-running` owns:\n- Atomic claim (rewrite frontmatter `status: ready` → `status: claimed, assignee: <agent-id>, claimed_at: <ts>`)\n- Three-bucket verification (Automated Verification, Automated QA, QA Doc identification)\n- Terminal status transition (`status: done` on success, `status: ready` on retry-able failure)\n- Reporting back\n\nThe orchestrator does NOT do step work itself — it delegates. See `desplega:step-running` for the full sub-agent contract.\n\n**Executor routing**: if the `desplega:delegate-work` skill is available, pick each step's executor per its routing matrix instead of defaulting to a `step-running` sub-agent — a step may route to a Codex variant (one worktree per parallel slice) or a specific Claude model tier. Only the executor choice changes; the scheduler, frontmatter claim protocol, and all other semantics here stay unchanged. When a step routes to Codex, the orchestrator owns the frontmatter bookkeeping that `step-running` would normally do (Codex must not edit plan files).\n\n### Wave Completion\n\nWhen a wave finishes:\n1. **Review each agent's report.** Mark steps `done` only if `completed` was reported.\n2. **Run the wave code review** (unless review mode is Off) — invoke `desplega:code-reviewing` on the wave's combined diff: Standards + Spec axes as parallel background sub-agents (routed per `desplega:delegate-work`), spec source = the completed steps' bodies and Success Criteria. Critical findings block those steps' commits — fix and re-verify first. If \"Only at the end\" was selected, run one review over the full diff after the DAG drains instead.\n3. **Handle `QA Doc: <path>`** — for each step that reported a QA doc, invoke `desplega:qa` against that path. The Automated QA bucket inside the step is already handled by the sub-agent; only the linked doc needs separate orchestration here. (`QA: n/a` → proceed normally.)\n4. **Manual verification (if not Autopilot)** — present manual verification items from each completed step's body. Wait for user confirmation. Don't tick manual boxes until confirmed.\n5. **Commits (if commit-per-step was selected)** — after a step's manual verification passes, create a commit: `[step-N] <step name>`.\n6. **Loop** — recompute `ready` and start the next wave.\n\n## Handling Failures and Mismatches\n\nIf a sub-agent reports `failed` or `blocked`, or you spot a mismatch between plan and reality:\n\n| Question | Options |\n|----------|---------|\n| \"[step-N] [issue]. How should I proceed?\" | 1. Adapt step (edit step-N.md and retry), 2. Retry as-is, 3. Skip and continue (mark step blocked), 4. Stop the run |\n\nIn Autopilot mode, use best judgment, document the decision in the step file, and continue if non-fatal.\n\n## After the DAG Drains\n\nWhen every step is `done`:\n\n1. Run **Global Verification** from `root.md`. Tick automated checks; surface manual checks to the user.\n2. Set `root.md` frontmatter `status: completed`.\n3. Offer post-implementation auditing: \"Would you like me to run `/verify-plan` and `/review` on the plan directory?\"\n4. If commit-per-step was off, offer to create a single bundled commit now.\n5. If the work ships as a GitHub PR: once review comments land, address them with `desplega:tackle-gh-comments` (fetch threads, verify each claim, fix, reply, resolve — push last).\n\n## Important Guidelines\n\n1. **One step = one sub-agent.** Don't bundle steps into a single agent even if they're in the same wave — fan-out is the whole point.\n2. **Plan-level context goes to every sub-agent.** Sibling step bodies do not.\n3. **The DAG is canonical via step frontmatter** — if `root.md`'s table is out of sync, trust the frontmatter.\n4. **Resume works step-granular.** If the user re-runs `/v-implement` mid-DAG, completed steps stay completed; only undone ones rerun.\n5. **Don't tick manual checkboxes** until the user confirms — automated boxes can be ticked by the sub-agent or the orchestrator.\n\n## Review Integration\n\nIf `file-review` is available and the user opted in:\n- After each step's significant code changes, invoke `/file-review:file-review <changed-file>`.\n- Process feedback with `file-review:process-review` before treating the step as done.\n- Skip in Autopilot.\n";
11605
11629
 
11606
11630
  // templates/skills/v-planning/config.json
11607
- var config_default47 = `{
11631
+ var config_default49 = `{
11608
11632
  "category": "skills",
11609
11633
  "description": "Vertical / parallel implementation planning skill. Creates DAG-structured plan directories where each step is an independent, QA-able vertical slice that sub-agents can pick up and implement in parallel. Use whenever the user wants a plan that fans out (multiple independent features), invokes /v-plan, or asks for a \\"parallel plan\\", \\"DAG plan\\", \\"vertical plan\\", or \\"plan that can be parallelized\\" — even if they don't say those exact words. Prefer the linear \`planning\` skill for strictly sequential work.",
11610
11634
  "displayName": "V-Planning",
@@ -11625,7 +11649,7 @@ var config_default47 = `{
11625
11649
  `;
11626
11650
 
11627
11651
  // templates/skills/v-planning/content.md
11628
- var content_default47 = `# v-planning
11652
+ var content_default49 = `# v-planning
11629
11653
 
11630
11654
  You create implementation plans as a **DAG of vertical steps** — each step a complete, QA-able slice of value (DB + API + UI + tests for one feature). The DAG captures feature-level dependencies, so independent steps can be implemented in parallel by sub-agents.
11631
11655
 
@@ -11716,7 +11740,7 @@ Structure validation runs automatically (rule 9, Haiku sub-agent).
11716
11740
  `;
11717
11741
 
11718
11742
  // templates/skills/verifying/config.json
11719
- var config_default48 = `{
11743
+ var config_default50 = `{
11720
11744
  "category": "skills",
11721
11745
  "description": "Post-implementation plan verification. Cross-references plans against actual changes for completeness and accuracy.",
11722
11746
  "displayName": "Verifying",
@@ -11737,7 +11761,7 @@ var config_default48 = `{
11737
11761
  `;
11738
11762
 
11739
11763
  // templates/skills/verifying/content.md
11740
- var content_default48 = `# Verifying
11764
+ var content_default50 = `# Verifying
11741
11765
 
11742
11766
  You are performing a post-implementation audit of a plan, cross-referencing it against actual changes to ensure nothing was missed, nothing is stale, and the implementation matches what was planned.
11743
11767
 
@@ -11885,7 +11909,7 @@ File-review is on by default (unless Autopilot):
11885
11909
  `;
11886
11910
 
11887
11911
  // templates/skills/work-on-task/config.json
11888
- var config_default49 = `{
11912
+ var config_default51 = `{
11889
11913
  "kind": "skill",
11890
11914
  "name": "work-on-task",
11891
11915
  "displayName": "Work On Task",
@@ -11902,10 +11926,10 @@ var config_default49 = `{
11902
11926
  `;
11903
11927
 
11904
11928
  // templates/skills/work-on-task/content.md
11905
- var content_default49 = "# Working on a task\n\nThe taskId follows the command. Without one, call `get-tasks` with `mineOnly: true` and take your `pending` or `in_progress` task. If there is none, say so and stop.\n\nThis message carries the task text, its attachments, its output format, and memories from past sessions. If it does not (you invoked this command yourself, or the context was compacted), run the `task-context-gathering` script with the taskId.\n\nWhen the task names a skill (`researching`, `planning`, `implementing`), use it. Otherwise work directly.\n\nFinish the task with one of the four endings in your operating contract: `completed`, `defer-task`, `request-human-input`, or `failed`. Then stop.\n\nResuming (a resume task, or work cut by a graceful shutdown): read the parent's last progress line, then re-read live state once (PR state and head SHA, branch, `git status`) before new work. Re-run controls only if the head moved. Never redo a pushed commit or open a second PR for the same branch. Store a progress line with branch, PR number and head SHA each time you push. A document task has no branch: its live state is the brief's agent-fs output path and the task attachments. Run `agent-fs ls` on that path, download any partial and extend it; never re-run collection steps that already produced artifacts. If your task's `model` differs from the parent's, say so in the output, because the result's provenance changed.\n\nWrite times in replies, outputs and defer notes in the requester's timezone when the Requester Profile names one, never raw UTC.\n\nIf the user interrupts, follow their instructions. To resume, call `/work-on-task <taskId>` again.\n";
11929
+ var content_default51 = "# Working on a task\n\nThe taskId follows the command. Without one, call `get-tasks` with `mineOnly: true` and take your `pending` or `in_progress` task. If there is none, say so and stop.\n\nThis message carries the task text, its attachments, its output format, and memories from past sessions. If it does not (you invoked this command yourself, or the context was compacted), run the `task-context-gathering` script with the taskId.\n\nWhen the task names a skill (`researching`, `planning`, `implementing`), use it. Otherwise work directly.\n\nFinish the task with one of the four endings in your operating contract: `completed`, `defer-task`, `request-human-input`, or `failed`. Then stop.\n\nResuming (a resume task, or work cut by a graceful shutdown): read the parent's last progress line, then re-read live state once (PR state and head SHA, branch, `git status`) before new work. Re-run controls only if the head moved. Never redo a pushed commit or open a second PR for the same branch. Store a progress line with branch, PR number and head SHA each time you push. A document task has no branch: its live state is the brief's agent-fs output path and the task attachments. Run `agent-fs ls` on that path, download any partial and extend it; never re-run collection steps that already produced artifacts. If your task's `model` differs from the parent's, say so in the output, because the result's provenance changed.\n\nWrite times in replies, outputs and defer notes in the requester's timezone when the Requester Profile names one, never raw UTC.\n\nIf the user interrupts, follow their instructions. To resume, call `/work-on-task <taskId>` again.\n";
11906
11930
 
11907
11931
  // templates/skills/workflow-iterate/config.json
11908
- var config_default50 = `{
11932
+ var config_default52 = `{
11909
11933
  "kind": "skill",
11910
11934
  "name": "workflow-iterate",
11911
11935
  "displayName": "Workflow Iteration",
@@ -11927,7 +11951,7 @@ var config_default50 = `{
11927
11951
  `;
11928
11952
 
11929
11953
  // templates/skills/workflow-iterate/content.md
11930
- var content_default50 = `# Workflow Iteration
11954
+ var content_default52 = `# Workflow Iteration
11931
11955
 
11932
11956
  Use this skill when you need to change an existing workflow without breaking live runs. The goal is to make small, verified revisions: inspect the current workflow, diagnose the failing step, patch only the required node or edge, trigger a realistic run, and keep iterating until the run reaches the intended terminal state.
11933
11957
 
@@ -12102,6 +12126,44 @@ Afterwards, a \`warnings\` entry on \`create-workflow\` or a patch tool means th
12102
12126
  - Do not set \`retry\` or \`validation.retry\` on a \`system-one-decision\` node. It retries connection errors, 408, 429, and 5xx itself (\`config.maxRetries\`, 0 to 3, default 2) inside \`config.timeoutMs\` (default 30000). A 401, a 422, or an answer that fails validation ends after one attempt.
12103
12127
  - The node reads its provider's key (\`TYPESAFE_API_KEY\`, \`OPENROUTER_API_KEY\`, \`LAYA_API_KEY\`, \`OPENAI_DECISIONS_API_KEY\`, or \`CLOUDFLARE_API_TOKEN\`, plus \`LAYA_URL\` for \`laya\` and \`CLOUDFLARE_ACCOUNT_ID\` for \`cloudflare\`) server-side. The key never appears in the definition, the step output, or an error. A run whose key or \`LAYA_URL\` is missing fails before any node executes, and a 401 or 403 fails the step as \`<KEY> was rejected\`. An unresolved \`{{token}}\` fails the step before any request.
12104
12128
 
12129
+ #### Jev images (direct TypeSafe API)
12130
+
12131
+ \`system-one-decision\` cannot send images today: its strict config has no \`assets\` field, its wire sends only \`state\`, \`model\`, and \`questions\`, and it does not send the image opt-in header. A URL or base64 string in \`state\` stays text. Use a \`swarm-script\` calling \`POST https://api.typesafe.ai/v1/systemone\` with the credential kept server-side until the node supports assets.
12132
+
12133
+ - Send \`TypeSafe-Danger-Zone-Images: true\`. Both \`jev-1.13.0\` and \`jev-1.14.0\` accept images; Box questions require 1.14. Pin the model you tested.
12134
+ - Upload PNG or JPEG bytes, not a URL. Prefer multipart: first part \`request\` is the JSON with content type \`application/json\`; an asset such as \`assets[uuid] = { type: "image", part: "photo-file" }\` names its file part. For JSON-only calls, use the base64 shape below: standard padded base64, no whitespace or \`data:\` prefix.
12135
+ - Each image must be still, at most 16 MiB decoded, 256–1568 pixels on each side, and 78,400–2,458,624 pixels total (256×256 is too small). Request body, all processed images together, and the expanded request each have a 32 MiB limit. Limits reject the whole request; the API does not resize. Rotate pixels upright before sending because EXIF orientation is not applied.
12136
+ - Generate a fresh UUID reference per image. References are replaced wherever they occur in string values in \`state\`, \`instructions\`, or \`criteria\`; object keys are not replaced. Every asset must be referenced. Repeating the same reference inserts and bills another image copy.
12137
+
12138
+ \`\`\`json
12139
+ {
12140
+ "model": "jev-1.13.0",
12141
+ "state": { "photo": "d1369c6a-538e-4ab3-91cd-f76f2f67b651" },
12142
+ "assets": {
12143
+ "d1369c6a-538e-4ab3-91cd-f76f2f67b651": {
12144
+ "type": "image", "format": "png", "data_base64": "<encoded PNG bytes>"
12145
+ }
12146
+ },
12147
+ "questions": {
12148
+ "ready": { "type": "noul", "instructions": "Is the product in \`photo\` fully assembled?" }
12149
+ }
12150
+ }
12151
+ \`\`\`
12152
+
12153
+ Measured 2026-10-15 on \`jev-1.13.0\`, using an 850×850 RGB PNG with a red square on white. Cost is the complete request's \`usage.input_tokens × $0.042 / 1,000,000\`; output is free:
12154
+
12155
+ | Placement | Questions | Input tokens | Request cost (USD) |
12156
+ |---|---:|---:|---:|
12157
+ | Once in \`state\` | 1 | 2,273 | $0.000095466 |
12158
+ | Once in \`state\`, questions use the \`photo\` path | 2 | 2,289 | $0.000096138 |
12159
+ | Twice in \`state\` | 1 | 4,237 | $0.000177954 |
12160
+ | Once in each question's \`instructions\` | 2 | 4,203 | $0.000176526 |
12161
+ | Once in \`state\`, resized to 425×425 | 1 | 863 | $0.000036246 |
12162
+
12163
+ The roughly $0.0001 claim (about 10,000 one-image requests per dollar) holds for this example; the exact cost depends on image size and prompt. **Question count alone does not multiply a shared state image's tokens.** Put shared images in \`state\` once and have questions use its field path, not the asset UUID. Image duplication scales with repeated references, including one copy per question when embedded in each question's instructions. If several judgments form one classification, a single \`choice\` can avoid repeating question-local images. Downscale when the detail permits: halving both dimensions reduced this request's billed tokens from 2,273 to 863; recheck accuracy on your images.
12164
+
12165
+ Contract and current pricing: [TypeSafe images](https://docs.typesafe.ai/images), [models](https://docs.typesafe.ai/models). Re-measure before budgeting a different model, resolution, or prompt.
12166
+
12105
12167
  ### \`human-in-the-loop\` (how approvals render)
12106
12168
 
12107
12169
  The same rules apply to a standalone \`request-human-input\` call.
@@ -12159,7 +12221,7 @@ The same rules apply to a standalone \`request-human-input\` call.
12159
12221
  `;
12160
12222
 
12161
12223
  // templates/skills/workflow-structured-output/config.json
12162
- var config_default51 = `{
12224
+ var config_default53 = `{
12163
12225
  "kind": "skill",
12164
12226
  "name": "workflow-structured-output",
12165
12227
  "displayName": "Workflow Structured Output",
@@ -12176,10 +12238,10 @@ var config_default51 = `{
12176
12238
  `;
12177
12239
 
12178
12240
  // templates/skills/workflow-structured-output/content.md
12179
- var content_default51 = '# Workflow Structured Output\n\n> **Companion skill — `workflow-iterate` (author-side).** This skill is for *workers* assigned a task spawned by an `agent-task` workflow node with an `outputSchema`. The author-side counterpart `workflow-iterate` covers how the workflow defines that schema, why gates downstream depend on it, and how to debug failed runs. If you ever need to understand *why* a particular schema is shaped the way it is — or you\'re switching from worker to author mode — read `workflow-iterate`.\n\n## Failure reason → fix (read this first)\n\nIf you see this failure reason on a task, the fix is always the same:\n\n| failureReason contains | What it means | Fix |\n|---|---|---|\n| `Task has an outputSchema but no output was provided` | You called `store-progress` without an `output` when the task required a JSON object matching `outputSchema` | Re-call `store-progress` with `status: "completed"` and `output` = a **stringified JSON** matching the schema exactly |\n| `Task output must be valid JSON` / `Task output does not match the outputSchema` | You passed invalid JSON, omitted required fields, or used the wrong types | Re-read the schema, include every `required` field with the exact key names, re-call `store-progress` |\n\n**You can re-call `store-progress` even after a rejection.** Your prior progress updates do not count as the final output. Fix the JSON and try again.\n\n## Pre-flight checklist (run through before calling store-progress)\n\n1. **Do the task details contain an actual `outputSchema`?** If yes, runtime\n enforcement is active: continue with that exact schema.\n2. **If there is no `outputSchema`, does the user provide an explicit JSON Output\n Format or interface?** Honor it as the requested output contract even though\n the runner is not enforcing an `outputSchema`.\n3. **Are workflow origin or tags such as `deterministic`, `litmus`, `validation`,\n or `context` the only signals?** They are heuristics only. Re-read the task\n details for an actual schema or explicit format. If neither exists, do not\n invent one; plain-text output is allowed.\n4. **Can I quote the exact required shape from the task details?** Use its exact\n keys and types, with every required field and no guessed renames.\n5. **Is my `output` a string (stringified JSON), not an object?** For a structured\n contract, `store-progress.output` should be a JSON string.\n6. **Only after the applicable contract is clear:** call `store-progress` with\n `status: "completed"` and the stringified JSON, or use plain text when no\n structured contract exists.\n\nIf the required shape is unclear, re-read the task details. Never synthesize a\nschema from tags or workflow origin.\n\n## Why this exists\n\nWhen a task has an actual `outputSchema`, the runner validates\n`store-progress.output` against it and rejects missing output, invalid JSON, or\nschema mismatches with one of the current messages listed above. A JSON Output\nFormat or interface without `task.outputSchema` remains a user instruction, but\nit is not the same runtime enforcement mechanism. Missing the distinction can\nturn completed work into a rejected terminal update, so check the task contract\nbefore reporting completion.\n\n## How to spot a structured-output task\n\nTreat these signals differently:\n\n- An actual `task.outputSchema` means JSON is runtime-enforced.\n- An explicit "Output Format", "Return a JSON object", or TypeScript/JSON\n interface is a user contract to return that shape, even without runtime\n enforcement.\n- Tags such as `deterministic`, `litmus`, `validation`, `releases`, or `context`,\n and `source = workflow`, only tell you to inspect the task details carefully.\n They do not prove a schema exists.\n\nWhen in doubt, re-read the task details and its `outputSchema`. Do not invent a\nschema or required fields.\n\n## How to complete correctly\n\n1. **Build the JSON object** that matches the actual `outputSchema` or explicit\n user-provided JSON format. Include every required field and use the exact key\n names from that contract.\n2. **Stringify it** — `store-progress.output` must be a string, not an object. Use `JSON.stringify(obj)` in your head.\n3. **Call store-progress** with `status: "completed"` and that JSON string as `output`.\n\n### Example — skip case\n\nTask has schema `{skip: bool, reason?: str, contextPath?: str, ...}` and you\'re skipping because the release already exists:\n\n```\nstore-progress(\n taskId,\n status="completed",\n output=\'{"skip":true,"reason":"Release already exists for this week"}\'\n)\n```\n\n### Example — full run\n\n```\nstore-progress(\n taskId,\n status="completed",\n output=\'{"skip":false,"contextPath":"release-runs/2026-04-20/context.json","commitCount":42,"repos":["example-repo"],"repoPatternsSource":"cache","dateRange":"2026-04-13 to 2026-04-20"}\'\n)\n```\n\n### Example — litmus / validation verdict\n\n```\nstore-progress(\n taskId,\n status="completed",\n output=\'{"verdict":"publish","reason":"5 user-facing changes; threshold met"}\'\n)\n```\n\n## Anti-patterns for an `outputSchema` task\n\n- `output: "Done. Context written to agent-fs at release-runs/2026-04-20/context.json"`\n- `output: "Published release notes for week of 2026-04-20"`\n- `output: "Verdict: publish"`\n- Calling `store-progress` with `status: "completed"` and no `output` at all\n- JSON that\'s missing any `required` field from the schema\n- JSON with extra keys instead of the schema\'s exact keys\n\n## Recovery if you realize mid-task\n\nIf you\'ve done all the work but forgot the JSON contract, just call `store-progress` again with the correct JSON string in `output`. Your prior progress updates don\'t count as the final output.\n\n## Verify before completing\n\nRead your task description once more. Find the schema. Build the JSON. Then complete. Takes 30 seconds. Saves a workflow re-run.\n\n## See also\n\n- **`workflow-iterate`** — the author-side counterpart of this skill. Read it if you\'re editing the workflow that produced this task, or want to understand why this particular schema is shaped the way it is (which gates downstream depend on it, etc.).\n';
12241
+ var content_default53 = '# Workflow Structured Output\n\n> **Companion skill — `workflow-iterate` (author-side).** This skill is for *workers* assigned a task spawned by an `agent-task` workflow node with an `outputSchema`. The author-side counterpart `workflow-iterate` covers how the workflow defines that schema, why gates downstream depend on it, and how to debug failed runs. If you ever need to understand *why* a particular schema is shaped the way it is — or you\'re switching from worker to author mode — read `workflow-iterate`.\n\n## Failure reason → fix (read this first)\n\nIf you see this failure reason on a task, the fix is always the same:\n\n| failureReason contains | What it means | Fix |\n|---|---|---|\n| `Task has an outputSchema but no output was provided` | You called `store-progress` without an `output` when the task required a JSON object matching `outputSchema` | Re-call `store-progress` with `status: "completed"` and `output` = a **stringified JSON** matching the schema exactly |\n| `Task output must be valid JSON` / `Task output does not match the outputSchema` | You passed invalid JSON, omitted required fields, or used the wrong types | Re-read the schema, include every `required` field with the exact key names, re-call `store-progress` |\n\n**You can re-call `store-progress` even after a rejection.** Your prior progress updates do not count as the final output. Fix the JSON and try again.\n\n## Pre-flight checklist (run through before calling store-progress)\n\n1. **Do the task details contain an actual `outputSchema`?** If yes, runtime\n enforcement is active: continue with that exact schema.\n2. **If there is no `outputSchema`, does the user provide an explicit JSON Output\n Format or interface?** Honor it as the requested output contract even though\n the runner is not enforcing an `outputSchema`.\n3. **Are workflow origin or tags such as `deterministic`, `litmus`, `validation`,\n or `context` the only signals?** They are heuristics only. Re-read the task\n details for an actual schema or explicit format. If neither exists, do not\n invent one; plain-text output is allowed.\n4. **Can I quote the exact required shape from the task details?** Use its exact\n keys and types, with every required field and no guessed renames.\n5. **Is my `output` a string (stringified JSON), not an object?** For a structured\n contract, `store-progress.output` should be a JSON string.\n6. **Only after the applicable contract is clear:** call `store-progress` with\n `status: "completed"` and the stringified JSON, or use plain text when no\n structured contract exists.\n\nIf the required shape is unclear, re-read the task details. Never synthesize a\nschema from tags or workflow origin.\n\n## Why this exists\n\nWhen a task has an actual `outputSchema`, the runner validates\n`store-progress.output` against it and rejects missing output, invalid JSON, or\nschema mismatches with one of the current messages listed above. A JSON Output\nFormat or interface without `task.outputSchema` remains a user instruction, but\nit is not the same runtime enforcement mechanism. Missing the distinction can\nturn completed work into a rejected terminal update, so check the task contract\nbefore reporting completion.\n\n## How to spot a structured-output task\n\nTreat these signals differently:\n\n- An actual `task.outputSchema` means JSON is runtime-enforced.\n- An explicit "Output Format", "Return a JSON object", or TypeScript/JSON\n interface is a user contract to return that shape, even without runtime\n enforcement.\n- Tags such as `deterministic`, `litmus`, `validation`, `releases`, or `context`,\n and `source = workflow`, only tell you to inspect the task details carefully.\n They do not prove a schema exists.\n\nWhen in doubt, re-read the task details and its `outputSchema`. Do not invent a\nschema or required fields.\n\n## How to complete correctly\n\n1. **Build the JSON object** that matches the actual `outputSchema` or explicit\n user-provided JSON format. Include every required field and use the exact key\n names from that contract.\n2. **Stringify it** — `store-progress.output` must be a string, not an object. Use `JSON.stringify(obj)` in your head.\n3. **Call store-progress** with `status: "completed"` and that JSON string as `output`.\n\n### Example — skip case\n\nTask has schema `{skip: bool, reason?: str, contextPath?: str, ...}` and you\'re skipping because the release already exists:\n\n```\nstore-progress(\n taskId,\n status="completed",\n output=\'{"skip":true,"reason":"Release already exists for this week"}\'\n)\n```\n\n### Example — full run\n\n```\nstore-progress(\n taskId,\n status="completed",\n output=\'{"skip":false,"contextPath":"release-runs/2026-04-20/context.json","commitCount":42,"repos":["example-repo"],"repoPatternsSource":"cache","dateRange":"2026-04-13 to 2026-04-20"}\'\n)\n```\n\n### Example — litmus / validation verdict\n\n```\nstore-progress(\n taskId,\n status="completed",\n output=\'{"verdict":"publish","reason":"5 user-facing changes; threshold met"}\'\n)\n```\n\n## Anti-patterns for an `outputSchema` task\n\n- `output: "Done. Context written to agent-fs at release-runs/2026-04-20/context.json"`\n- `output: "Published release notes for week of 2026-04-20"`\n- `output: "Verdict: publish"`\n- Calling `store-progress` with `status: "completed"` and no `output` at all\n- JSON that\'s missing any `required` field from the schema\n- JSON with extra keys instead of the schema\'s exact keys\n\n## Recovery if you realize mid-task\n\nIf you\'ve done all the work but forgot the JSON contract, just call `store-progress` again with the correct JSON string in `output`. Your prior progress updates don\'t count as the final output.\n\n## Verify before completing\n\nRead your task description once more. Find the schema. Build the JSON. Then complete. Takes 30 seconds. Saves a workflow re-run.\n\n## See also\n\n- **`workflow-iterate`** — the author-side counterpart of this skill. Read it if you\'re editing the workflow that produced this task, or want to understand why this particular schema is shaped the way it is (which gates downstream depend on it, etc.).\n';
12180
12242
 
12181
12243
  // templates/skills/wts-expert/config.json
12182
- var config_default52 = `{
12244
+ var config_default54 = `{
12183
12245
  "category": "skills",
12184
12246
  "description": "Git worktree management expert for @desplega.ai/wts. Use when the user asks about git worktrees, wts commands, worktree workflows, or wants help managing multiple branches simultaneously.",
12185
12247
  "displayName": "WTS Expert",
@@ -12200,7 +12262,7 @@ var config_default52 = `{
12200
12262
  `;
12201
12263
 
12202
12264
  // templates/skills/wts-expert/content.md
12203
- var content_default52 = `# WTS Expert
12265
+ var content_default54 = `# WTS Expert
12204
12266
 
12205
12267
  You are an expert on \`@desplega.ai/wts\`, a CLI tool for managing Git worktrees with tmux integration, Claude Code launcher support, and GitHub PR creation.
12206
12268
 
@@ -12350,128 +12412,24 @@ For complete command documentation with all flags and options, see [COMMANDS.md]
12350
12412
 
12351
12413
  // src/be/seed-skills/index.ts
12352
12414
  init_db2();
12415
+
12416
+ // src/be/seed/state-db.ts
12417
+ init_db2();
12418
+ async function getSeedState(kind, key) {
12419
+ return getDbClient().get("SELECT kind, key, seededHash, seededAt FROM seed_state WHERE kind = ? AND key = ?", [kind, key]);
12420
+ }
12421
+ async function recordSeedState(kind, key, seededHash) {
12422
+ await getDbClient().run(`INSERT INTO seed_state (kind, key, seededHash, seededAt)
12423
+ VALUES (?, ?, ?, ?)
12424
+ ON CONFLICT(kind, key) DO UPDATE SET
12425
+ seededHash = excluded.seededHash,
12426
+ seededAt = excluded.seededAt`, [kind, key, seededHash, new Date().toISOString()]);
12427
+ }
12428
+ async function deleteSeedState(kind, key) {
12429
+ await getDbClient().run("DELETE FROM seed_state WHERE kind = ? AND key = ?", [kind, key]);
12430
+ }
12353
12431
  // src/be/seed-skills/bundled-files.generated.json
12354
12432
  var bundled_files_generated_default = {
12355
- artifacts: [
12356
- {
12357
- path: "examples/approval-flow.ts",
12358
- content: `import { createArtifactServer } from "../../src/artifact-sdk";
12359
- import { Hono } from "hono";
12360
-
12361
- const app = new Hono();
12362
- app.get("/", (c) =>
12363
- c.html(\`
12364
- <!DOCTYPE html>
12365
- <html>
12366
- <head><title>Approval Required</title></head>
12367
- <body>
12368
- <h1>PR #42 — Review Required</h1>
12369
- <p>Agent wants to merge this PR. Please review.</p>
12370
- <button id="approve">Approve</button>
12371
- <button id="reject">Reject</button>
12372
- <script src="/@swarm/sdk.js"></script>
12373
- <script>
12374
- const swarm = new SwarmSDK();
12375
- document.getElementById('approve').onclick = async () => {
12376
- await swarm.createTask({ task: 'Merge PR #42 — human approved' });
12377
- document.body.innerHTML = '<h1>Approved! Task created.</h1>';
12378
- };
12379
- document.getElementById('reject').onclick = async () => {
12380
- await swarm.createTask({ task: 'PR #42 rejected by human — needs changes' });
12381
- document.body.innerHTML = '<h1>Rejected. Agent notified.</h1>';
12382
- };
12383
- </script>
12384
- </body>
12385
- </html>
12386
- \`),
12387
- );
12388
-
12389
- const server = createArtifactServer({ name: "approval-pr-42", app });
12390
- await server.start();
12391
- console.log(\`Approval artifact at: \${server.url}\`);
12392
- `
12393
- },
12394
- {
12395
- path: "examples/hono-dashboard.ts",
12396
- content: `import { createArtifactServer } from "../../src/artifact-sdk";
12397
- import { Hono } from "hono";
12398
-
12399
- const app = new Hono();
12400
-
12401
- app.get("/", (c) =>
12402
- c.html(\`
12403
- <!DOCTYPE html>
12404
- <html>
12405
- <head><title>Dashboard</title></head>
12406
- <body>
12407
- <h1>Agent Dashboard</h1>
12408
- <div id="agents"></div>
12409
- <script src="/@swarm/sdk.js"></script>
12410
- <script>
12411
- const swarm = new SwarmSDK();
12412
- swarm.getSwarm().then(agents => {
12413
- document.getElementById('agents').innerHTML =
12414
- '<ul>' + agents.map(a => '<li>' + a.name + ' (' + a.status + ')</li>').join('') + '</ul>';
12415
- });
12416
- </script>
12417
- </body>
12418
- </html>
12419
- \`),
12420
- );
12421
-
12422
- app.get("/api/status", (c) => c.json({ status: "ok", timestamp: Date.now() }));
12423
-
12424
- const server = createArtifactServer({ name: "dashboard", app });
12425
- await server.start();
12426
- console.log(\`Dashboard live at: \${server.url}\`);
12427
- `
12428
- },
12429
- {
12430
- path: "examples/multi-artifact.ts",
12431
- content: `import { createArtifactServer } from "../../src/artifact-sdk";
12432
- import { Hono } from "hono";
12433
-
12434
- // Artifact 1: Status page
12435
- const statusApp = new Hono();
12436
- statusApp.get("/", (c) => c.html("<h1>Status: All systems operational</h1>"));
12437
-
12438
- // Artifact 2: Data viewer
12439
- const dataApp = new Hono();
12440
- dataApp.get("/", (c) => c.json({ items: [1, 2, 3], generated: new Date().toISOString() }));
12441
-
12442
- const status = createArtifactServer({ name: "status", app: statusApp });
12443
- const data = createArtifactServer({ name: "data", app: dataApp });
12444
-
12445
- await status.start();
12446
- await data.start();
12447
-
12448
- console.log(\`Status page: \${status.url}\`);
12449
- console.log(\`Data viewer: \${data.url}\`);
12450
- console.log(\`Both artifacts running on different ports (\${status.port}, \${data.port})\`);
12451
- `
12452
- },
12453
- {
12454
- path: "examples/static-report.sh",
12455
- content: `#!/bin/bash
12456
- # Serve a static HTML report as an artifact
12457
- ARTIFACT_DIR="/workspace/personal/artifacts/my-report"
12458
- mkdir -p "$ARTIFACT_DIR"
12459
-
12460
- cat > "$ARTIFACT_DIR/index.html" << 'HTML'
12461
- <!DOCTYPE html>
12462
- <html>
12463
- <head><title>Agent Report</title></head>
12464
- <body>
12465
- <h1>Analysis Report</h1>
12466
- <p>Generated by agent</p>
12467
- </body>
12468
- </html>
12469
- HTML
12470
-
12471
- agent-swarm artifact serve "$ARTIFACT_DIR" --name "my-report"
12472
- `
12473
- }
12474
- ],
12475
12433
  brainstorming: [
12476
12434
  {
12477
12435
  path: "template.md",
@@ -13703,6 +13661,294 @@ last_updated_by: [Name]
13703
13661
  - **Verification**: \`thoughts/.../plans/YYYY-MM-DD-verification-report.md\`
13704
13662
  - **Related documents**: [links]
13705
13663
  - **Notes**: [follow-up QA, derail items, environment caveats]
13664
+ `
13665
+ }
13666
+ ],
13667
+ "qa-loom": [
13668
+ {
13669
+ path: "build-loom.mjs",
13670
+ content: `// build-loom.mjs: steps.json + raw recording -> narrated qa-loom.mp4 (+ qa-loom.ass, work/).
13671
+ // Usage: node build-loom.mjs <steps.json> <raw.webm> <outDir> (bun works too)
13672
+ // Env: ELEVENLABS_API_KEY (required), SPEED=1.33, VOICE=21m00Tcm4TlvDq8ikWAM,
13673
+ // MODEL=eleven_turbo_v2_5, FONT="DejaVu Sans"
13674
+ // Needs: Node 18+ (global fetch, top-level await) and ffmpeg built with libass (\`ass\` filter).
13675
+ import { execFileSync } from "node:child_process";
13676
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
13677
+ import { join, resolve } from "node:path";
13678
+
13679
+ const [stepsPath, raw, outArg] = process.argv.slice(2);
13680
+ if (!stepsPath || !raw || !outArg) {
13681
+ throw new Error("usage: node build-loom.mjs <steps.json> <raw.webm> <outDir>");
13682
+ }
13683
+ const out = resolve(outArg);
13684
+ const SPEED = Number(process.env.SPEED ?? 1.33);
13685
+ const VOICE = process.env.VOICE ?? "21m00Tcm4TlvDq8ikWAM";
13686
+ const MODEL = process.env.MODEL ?? "eleven_turbo_v2_5";
13687
+ const FONT = process.env.FONT ?? "DejaVu Sans";
13688
+ const KEY = process.env.ELEVENLABS_API_KEY;
13689
+ if (!KEY) throw new Error("ELEVENLABS_API_KEY is not set");
13690
+ mkdirSync(join(out, "work"), { recursive: true });
13691
+ const W = (f) => join(out, "work", f);
13692
+
13693
+ // -loglevel error keeps ffmpeg's banner and progress output out of the logs.
13694
+ const ff = (...args) => {
13695
+ try {
13696
+ execFileSync("ffmpeg", ["-y", "-hide_banner", "-loglevel", "error", ...args], {
13697
+ stdio: ["ignore", "ignore", "pipe"],
13698
+ });
13699
+ } catch (e) {
13700
+ throw new Error(\`ffmpeg failed: \${String(e.stderr).slice(0, 400)}\`);
13701
+ }
13702
+ };
13703
+ const ENC = [
13704
+ "-c:v",
13705
+ "libx264",
13706
+ "-preset",
13707
+ "veryfast",
13708
+ "-crf",
13709
+ "23",
13710
+ "-pix_fmt",
13711
+ "yuv420p",
13712
+ "-r",
13713
+ "30",
13714
+ "-video_track_timescale",
13715
+ "15360",
13716
+ ];
13717
+ // Keep the aspect ratio: letterbox or pillarbox into 1920x1080, never stretch (mobile viewports).
13718
+ const FIT =
13719
+ "scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2:color=white";
13720
+
13721
+ const { steps } = JSON.parse(readFileSync(stepsPath, "utf8"));
13722
+ const concat = [];
13723
+ const cues = [];
13724
+ const audio = [];
13725
+ let T = 0; // running output timeline, in seconds
13726
+
13727
+ for (const [i, s] of steps.entries()) {
13728
+ const visualAt = s.visualAt ?? s.start;
13729
+ const segLen = (s.end - s.start) / SPEED;
13730
+ const v = (visualAt - s.start) / SPEED; // voice and subtitles start when the screen renders
13731
+
13732
+ // 1. TTS with character timestamps, one request per step. Cached per narration text,
13733
+ // so a re-run after a style tweak costs no ElevenLabs characters.
13734
+ const mp3 = W(\`audio-\${i + 1}.mp3\`);
13735
+ const alPath = W(\`audio-\${i + 1}.json\`);
13736
+ let cached = existsSync(alPath) ? JSON.parse(readFileSync(alPath, "utf8")) : null;
13737
+ if (cached?.text !== s.narration) {
13738
+ const res = await fetch(
13739
+ \`https://api.elevenlabs.io/v1/text-to-speech/\${VOICE}/with-timestamps\`,
13740
+ {
13741
+ method: "POST",
13742
+ headers: { "xi-api-key": KEY, "content-type": "application/json" },
13743
+ body: JSON.stringify({ text: s.narration, model_id: MODEL }),
13744
+ },
13745
+ );
13746
+ if (!res.ok) throw new Error(\`ElevenLabs \${res.status}: \${(await res.text()).slice(0, 200)}\`);
13747
+ const tts = await res.json();
13748
+ // audio_base64 is large: write it straight to disk, never print it.
13749
+ writeFileSync(mp3, Buffer.from(tts.audio_base64, "base64"));
13750
+ cached = { text: s.narration, alignment: tts.alignment };
13751
+ writeFileSync(alPath, JSON.stringify(cached));
13752
+ }
13753
+ const al = cached.alignment;
13754
+ const aDur = al.character_end_times_seconds.at(-1);
13755
+
13756
+ // 2. Sped-up segment. Freeze the last frame when the narration outlasts it.
13757
+ const seg = W(\`seg-\${i + 1}.mp4\`);
13758
+ ff(
13759
+ "-ss",
13760
+ String(s.start),
13761
+ "-t",
13762
+ String(s.end - s.start),
13763
+ "-i",
13764
+ raw,
13765
+ "-an",
13766
+ "-vf",
13767
+ \`setpts=(PTS-STARTPTS)/\${SPEED},\${FIT},fps=30\`,
13768
+ ...ENC,
13769
+ seg,
13770
+ );
13771
+ concat.push(seg);
13772
+ const need = v + aDur + 0.35;
13773
+ let len = segLen;
13774
+ if (need > segLen) {
13775
+ const png = W(\`last-\${i + 1}.png\`);
13776
+ ff("-sseof", "-0.2", "-i", seg, "-update", "1", "-frames:v", "1", png);
13777
+ const pad = W(\`pad-\${i + 1}.mp4\`);
13778
+ const padLen = (need - segLen).toFixed(3);
13779
+ ff("-loop", "1", "-framerate", "30", "-t", padLen, "-i", png, "-vf", FIT, ...ENC, pad);
13780
+ concat.push(pad);
13781
+ len = need;
13782
+ }
13783
+ audio.push({ mp3, delayMs: Math.round((T + v) * 1000) });
13784
+
13785
+ // 3. Subtitle cues from the same alignment, so voice and subtitles cannot drift.
13786
+ // At most 6 words per cue, break on punctuation, merge orphans under 3 words.
13787
+ const words = [];
13788
+ let cur = null;
13789
+ al.characters.forEach((ch, k) => {
13790
+ if (/\\s/.test(ch)) {
13791
+ if (cur) words.push(cur);
13792
+ cur = null;
13793
+ return;
13794
+ }
13795
+ if (!cur) cur = { text: "", t0: al.character_start_times_seconds[k] };
13796
+ cur.text += ch;
13797
+ cur.t1 = al.character_end_times_seconds[k];
13798
+ });
13799
+ if (cur) words.push(cur);
13800
+ const groups = [];
13801
+ let g = [];
13802
+ for (const w of words) {
13803
+ g.push(w);
13804
+ if (g.length >= 6 || /[.,;:!?]$/.test(w.text)) {
13805
+ groups.push(g);
13806
+ g = [];
13807
+ }
13808
+ }
13809
+ if (g.length) groups.push(g);
13810
+ for (let k = groups.length - 1; k > 0; k--) {
13811
+ if (groups[k].length < 3) {
13812
+ groups[k - 1].push(...groups[k]);
13813
+ groups.splice(k, 1);
13814
+ }
13815
+ }
13816
+ for (const grp of groups) {
13817
+ cues.push({
13818
+ t0: T + v + grp[0].t0,
13819
+ t1: T + v + grp.at(-1).t1 + 0.1,
13820
+ text: grp.map((w) => w.text).join(" "),
13821
+ });
13822
+ }
13823
+ T += len;
13824
+ }
13825
+
13826
+ // 4. Concat with the concat demuxer. The concat filter in one big filtergraph can
13827
+ // truncate the video stream.
13828
+ writeFileSync(W("concat.txt"), concat.map((f) => \`file '\${f}'\`).join("\\n"));
13829
+ ff("-f", "concat", "-safe", "0", "-i", W("concat.txt"), "-c", "copy", W("video.mp4"));
13830
+
13831
+ // 5. ASS subtitles: one small line, bottom centre, dark box. libass ignores the
13832
+ // BackColour alpha with BorderStyle=3, so the box is solid #303030.
13833
+ const ts = (t) => {
13834
+ const cs = Math.round(t * 100);
13835
+ const h = Math.floor(cs / 360000);
13836
+ const m = String(Math.floor(cs / 6000) % 60).padStart(2, "0");
13837
+ const sec = String(Math.floor(cs / 100) % 60).padStart(2, "0");
13838
+ return \`\${h}:\${m}:\${sec}.\${String(cs % 100).padStart(2, "0")}\`;
13839
+ };
13840
+ for (let k = 0; k < cues.length - 1; k++) cues[k].t1 = Math.min(cues[k].t1, cues[k + 1].t0);
13841
+ const dialogue = cues
13842
+ .map((c) => \`Dialogue: 0,\${ts(c.t0)},\${ts(c.t1)},Default,,0,0,0,,\${c.text}\`)
13843
+ .join("\\n");
13844
+ const ass = \`[Script Info]
13845
+ ScriptType: v4.00+
13846
+ PlayResX: 1920
13847
+ PlayResY: 1080
13848
+
13849
+ [V4+ Styles]
13850
+ Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
13851
+ Style: Default,\${FONT},38,&H00FFFFFF,&H00FFFFFF,&H303030,&H00000000,0,0,0,0,100,100,0,0,3,2,0,2,40,40,30,1
13852
+
13853
+ [Events]
13854
+ Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
13855
+ \${dialogue}
13856
+ \`;
13857
+ const assPath = join(out, "qa-loom.ass");
13858
+ writeFileSync(assPath, ass);
13859
+
13860
+ // 6. Mux: delay each step's audio to where its screen renders, mix, burn subtitles.
13861
+ // normalize=0 stops amix from dividing the volume by the number of inputs.
13862
+ const inputs = audio.flatMap((a) => ["-i", a.mp3]);
13863
+ const delays = audio.map((a, k) => \`[\${k + 1}:a]adelay=\${a.delayMs}:all=1[a\${k}]\`).join(";");
13864
+ const labels = audio.map((_, k) => \`[a\${k}]\`).join("");
13865
+ const mix = \`\${labels}amix=inputs=\${audio.length}:normalize=0:duration=longest[a]\`;
13866
+ ff(
13867
+ "-i",
13868
+ W("video.mp4"),
13869
+ ...inputs,
13870
+ "-filter_complex",
13871
+ \`\${delays};\${mix};[0:v]ass=\${assPath}[v]\`,
13872
+ "-map",
13873
+ "[v]",
13874
+ "-map",
13875
+ "[a]",
13876
+ ...ENC,
13877
+ "-c:a",
13878
+ "aac",
13879
+ "-b:a",
13880
+ "128k",
13881
+ "-movflags",
13882
+ "+faststart",
13883
+ "-t",
13884
+ T.toFixed(3),
13885
+ join(out, "qa-loom.mp4"),
13886
+ );
13887
+ const chars = steps.reduce((n, s) => n + s.narration.length, 0);
13888
+ console.log(
13889
+ JSON.stringify({ durationS: +T.toFixed(2), steps: steps.length, cues: cues.length, chars }),
13890
+ );
13891
+ `
13892
+ },
13893
+ {
13894
+ path: "run-qa.sh",
13895
+ content: `#!/usr/bin/env bash
13896
+ # run-qa.sh: record a UI QA pass with agent-browser and write steps.json.
13897
+ # Every step offset is in seconds from the moment recording starts.
13898
+ #
13899
+ # Usage: BASE_URL=http://localhost:5274 OUT=./loom bash run-qa.sh
13900
+ # Needs: agent-browser (with \`record\`), jq, GNU date (%N). No bc: time math runs in jq.
13901
+ #
13902
+ # Replace the two example steps with the steps that prove your change.
13903
+ set -euo pipefail
13904
+ BASE_URL=\${BASE_URL:-http://localhost:5274}
13905
+ OUT=\${OUT:-./loom}
13906
+ mkdir -p "$OUT"
13907
+ LOG="$OUT/steps.ndjson"
13908
+ : >"$LOG"
13909
+
13910
+ now() { date +%s.%N; }
13911
+ # act: call right before the action fires. It marks the step start.
13912
+ act() { STEP_START=$(now); }
13913
+ # step <action> <observed> <verdict> <narration>: call AFTER the resulting screen has
13914
+ # rendered (after an \`agent-browser wait <selector>\`). That moment becomes visualAt.
13915
+ step() {
13916
+ jq -nc --arg a "$1" --arg o "$2" --arg v "$3" --arg n "$4" \\
13917
+ --arg t0 "$T0" --arg s "$STEP_START" --arg va "$(now)" \\
13918
+ '{action:$a, observed:$o, verdict:$v, narration:$n,
13919
+ start:(($s|tonumber)-($t0|tonumber)), visualAt:(($va|tonumber)-($t0|tonumber))}' >>"$LOG"
13920
+ }
13921
+
13922
+ # Log in and seed data BEFORE recording starts.
13923
+ agent-browser set viewport 1920 1080 >/dev/null
13924
+ agent-browser open "$BASE_URL" >/dev/null
13925
+ agent-browser wait 1500 >/dev/null
13926
+ agent-browser record start "$OUT/raw.webm" --cursor --fps 30 >/dev/null
13927
+ T0=$(now)
13928
+
13929
+ act
13930
+ agent-browser reload >/dev/null
13931
+ agent-browser wait h1 >/dev/null
13932
+ step "Load home page" "Heading and nav render" pass "The home page loads with its heading and nav."
13933
+ agent-browser wait 800 >/dev/null # dwell so the viewer sees the result before the next action
13934
+
13935
+ act
13936
+ agent-browser open "$BASE_URL/settings" >/dev/null
13937
+ agent-browser wait h1 >/dev/null
13938
+ step "Open settings" "Settings page renders" pass "The settings page opens."
13939
+ agent-browser wait 800 >/dev/null
13940
+
13941
+ END=$(jq -n --arg a "$(now)" --arg b "$T0" '($a|tonumber)-($b|tonumber)')
13942
+ agent-browser record stop >/dev/null
13943
+
13944
+ # Each step ends where the next one starts; the last one ends at record stop.
13945
+ jq -s --argjson end "$END" '
13946
+ {steps: ([to_entries[] | .value + {idx: (.key + 1)}]
13947
+ | . as $s
13948
+ | [range(0; length) as $i
13949
+ | $s[$i] + {end: (if $i + 1 < ($s | length) then $s[$i + 1].start else $end end)}])}
13950
+ ' "$LOG" >"$OUT/steps.json"
13951
+ jq -c '.steps[] | {idx, start, visualAt, end, verdict}' "$OUT/steps.json"
13706
13952
  `
13707
13953
  }
13708
13954
  ],
@@ -14993,13 +15239,13 @@ var BUILT_IN_SKILL_SOURCES = [
14993
15239
  { config: config_default34, body: content_default34 },
14994
15240
  { config: config_default35, body: content_default35 },
14995
15241
  { config: config_default36, body: content_default36 },
14996
- { config: config_default41, body: content_default41 },
14997
15242
  { config: config_default37, body: content_default37 },
14998
15243
  { config: config_default38, body: content_default38 },
15244
+ { config: config_default43, body: content_default43 },
14999
15245
  { config: config_default39, body: content_default39 },
15000
15246
  { config: config_default40, body: content_default40 },
15247
+ { config: config_default41, body: content_default41 },
15001
15248
  { config: config_default42, body: content_default42 },
15002
- { config: config_default43, body: content_default43 },
15003
15249
  { config: config_default44, body: content_default44 },
15004
15250
  { config: config_default45, body: content_default45 },
15005
15251
  { config: config_default46, body: content_default46 },
@@ -15008,7 +15254,9 @@ var BUILT_IN_SKILL_SOURCES = [
15008
15254
  { config: config_default49, body: content_default49 },
15009
15255
  { config: config_default50, body: content_default50 },
15010
15256
  { config: config_default51, body: content_default51 },
15011
- { config: config_default52, body: content_default52 }
15257
+ { config: config_default52, body: content_default52 },
15258
+ { config: config_default53, body: content_default53 },
15259
+ { config: config_default54, body: content_default54 }
15012
15260
  ];
15013
15261
  function canonicalFiles(files) {
15014
15262
  return [...files].sort((a, b) => a.path.localeCompare(b.path)).map((file) => `${file.path}
@@ -15098,6 +15346,42 @@ async function syncSeededSkillFiles(skillId, files) {
15098
15346
  if (files.length > 0)
15099
15347
  await upsertSkillFiles(skillId, files);
15100
15348
  }
15349
+ var RETIRED_SEED_SKILLS = ["artifacts"];
15350
+ function metadataAsSeeded(skill, fileCount) {
15351
+ let seededDescription;
15352
+ try {
15353
+ seededDescription = parseSkillContent(skill.content).description;
15354
+ } catch {
15355
+ return false;
15356
+ }
15357
+ return skill.description === seededDescription && skill.type === "personal" && skill.isComplex === fileCount > 0 && skill.isEnabled && !skill.disableModelInvocation && skill.allowedTools === null && skill.model === null && skill.effort === null && skill.context === null && skill.agent === null && skill.sourceUrl === null && skill.sourceRepo === null && skill.sourcePath === null && skill.sourceBranch === "main" && skill.sourceHash === null && skill.lastFetchedAt === null;
15358
+ }
15359
+ async function retireSeedSkills(names = RETIRED_SEED_SKILLS) {
15360
+ const retired = [];
15361
+ for (const name of names) {
15362
+ const existing = await getSkillByName(name, "swarm");
15363
+ if (!existing)
15364
+ continue;
15365
+ const state = await getSeedState("skill", name);
15366
+ if (!state)
15367
+ continue;
15368
+ const liveFiles = (await getSkillFiles(existing.id)).map((file) => ({
15369
+ path: file.path,
15370
+ content: file.content
15371
+ }));
15372
+ const liveHash = skillSeedHash(existing.content, existing.systemDefault, liveFiles, existing.userInvocable);
15373
+ if (liveHash !== state.seededHash || !metadataAsSeeded(existing, liveFiles.length)) {
15374
+ console.warn(`[seed:skill] kept retired skill "${name}": edited since it was seeded`);
15375
+ continue;
15376
+ }
15377
+ await getDbClient().transaction(async () => {
15378
+ await deleteSkill(existing.id);
15379
+ await deleteSeedState("skill", name);
15380
+ });
15381
+ retired.push(name);
15382
+ }
15383
+ return retired;
15384
+ }
15101
15385
  var skillsSeeder = {
15102
15386
  kind: "skill",
15103
15387
  async items() {
@@ -15148,22 +15432,10 @@ var skillsSeeder = {
15148
15432
  });
15149
15433
  await syncSeededSkillFiles(created.id, skill.files);
15150
15434
  });
15151
- }
15435
+ },
15436
+ retire: () => retireSeedSkills()
15152
15437
  };
15153
15438
 
15154
- // src/be/seed/state-db.ts
15155
- init_db2();
15156
- async function getSeedState(kind, key) {
15157
- return getDbClient().get("SELECT kind, key, seededHash, seededAt FROM seed_state WHERE kind = ? AND key = ?", [kind, key]);
15158
- }
15159
- async function recordSeedState(kind, key, seededHash) {
15160
- await getDbClient().run(`INSERT INTO seed_state (kind, key, seededHash, seededAt)
15161
- VALUES (?, ?, ?, ?)
15162
- ON CONFLICT(kind, key) DO UPDATE SET
15163
- seededHash = excluded.seededHash,
15164
- seededAt = excluded.seededAt`, [kind, key, seededHash, new Date().toISOString()]);
15165
- }
15166
-
15167
15439
  // src/be/seed/runner.ts
15168
15440
  async function runSeeder(seeder, opts) {
15169
15441
  const result = {
@@ -15172,6 +15444,7 @@ async function runSeeder(seeder, opts) {
15172
15444
  updated: 0,
15173
15445
  skippedUnchanged: 0,
15174
15446
  skippedUserModified: 0,
15447
+ retired: 0,
15175
15448
  failed: []
15176
15449
  };
15177
15450
  const items = await seeder.items();
@@ -15207,8 +15480,18 @@ async function runSeeder(seeder, opts) {
15207
15480
  });
15208
15481
  }
15209
15482
  }
15483
+ if (seeder.retire) {
15484
+ try {
15485
+ result.retired = (await seeder.retire()).length;
15486
+ } catch (err) {
15487
+ result.failed.push({
15488
+ key: "(retire)",
15489
+ error: err instanceof Error ? err.message : String(err)
15490
+ });
15491
+ }
15492
+ }
15210
15493
  if (!opts?.quiet) {
15211
- console.log(`[seed:${result.kind}] created=${result.created} updated=${result.updated} ` + `unchanged=${result.skippedUnchanged} preserved=${result.skippedUserModified} ` + `failed=${result.failed.length}`);
15494
+ console.log(`[seed:${result.kind}] created=${result.created} updated=${result.updated} ` + `unchanged=${result.skippedUnchanged} preserved=${result.skippedUserModified} ` + `retired=${result.retired} failed=${result.failed.length}`);
15212
15495
  for (const f of result.failed) {
15213
15496
  console.error(`[seed:${result.kind}] FAILED ${f.key}: ${f.error}`);
15214
15497
  }
@@ -15227,7 +15510,7 @@ async function runSeeders(seeders, opts) {
15227
15510
  var import_cron_parser = __toESM(require_dist(), 1);
15228
15511
 
15229
15512
  // templates/schedules/daily-blocker-digest/config.json
15230
- var config_default53 = `{
15513
+ var config_default55 = `{
15231
15514
  "kind": "schedule",
15232
15515
  "name": "daily-blocker-digest",
15233
15516
  "displayName": "Daily Blocker Digest",
@@ -15249,7 +15532,7 @@ var config_default53 = `{
15249
15532
  `;
15250
15533
 
15251
15534
  // templates/schedules/daily-blocker-digest/content.md
15252
- var content_default53 = `# Daily Blocker Digest
15535
+ var content_default55 = `# Daily Blocker Digest
15253
15536
 
15254
15537
  Delivery uses configured admin channels with an in-app fallback.
15255
15538
 
@@ -15418,7 +15701,7 @@ Call \`store-progress\` with status \`completed\` and an output paragraph coveri
15418
15701
  `;
15419
15702
 
15420
15703
  // templates/schedules/daily-compounding-reflection/config.json
15421
- var config_default54 = `{
15704
+ var config_default56 = `{
15422
15705
  "kind": "schedule",
15423
15706
  "name": "daily-compounding-reflection",
15424
15707
  "displayName": "Daily Evolution",
@@ -15441,7 +15724,7 @@ var config_default54 = `{
15441
15724
  `;
15442
15725
 
15443
15726
  // templates/schedules/daily-compounding-reflection/content.md
15444
- var content_default54 = `# Daily Compounding Reflection
15727
+ var content_default56 = `# Daily Compounding Reflection
15445
15728
 
15446
15729
  Delivery uses configured admin channels with an in-app fallback.
15447
15730
 
@@ -15666,7 +15949,7 @@ If you have zero changes across all three folds and zero deferred items, somethi
15666
15949
  `;
15667
15950
 
15668
15951
  // templates/schedules/daily-hn-briefing/config.json
15669
- var config_default55 = `{
15952
+ var config_default57 = `{
15670
15953
  "kind": "schedule",
15671
15954
  "name": "daily-hn-briefing",
15672
15955
  "displayName": "Daily Hacker News Briefing",
@@ -15683,7 +15966,7 @@ var config_default55 = `{
15683
15966
  `;
15684
15967
 
15685
15968
  // templates/schedules/daily-hn-briefing/content.md
15686
- var content_default55 = `# Daily Hacker News Briefing
15969
+ var content_default57 = `# Daily Hacker News Briefing
15687
15970
 
15688
15971
  Template parameters: {{REPORT_EMAIL}}
15689
15972
 
@@ -15785,7 +16068,7 @@ IMPORTANT:
15785
16068
  `;
15786
16069
 
15787
16070
  // templates/schedules/daily-status-report/config.json
15788
- var config_default56 = `{
16071
+ var config_default58 = `{
15789
16072
  "kind": "schedule",
15790
16073
  "name": "daily-status-report",
15791
16074
  "displayName": "Daily Status Report",
@@ -15807,7 +16090,7 @@ var config_default56 = `{
15807
16090
  `;
15808
16091
 
15809
16092
  // templates/schedules/daily-status-report/content.md
15810
- var content_default56 = `# Daily Status Report
16093
+ var content_default58 = `# Daily Status Report
15811
16094
 
15812
16095
  Give the operator one daily read on whether the swarm is healthy: what's running, what's failed, what's waiting on them.
15813
16096
 
@@ -15903,7 +16186,7 @@ More than 3 qualify ⇒ post the 3 that matter and put the rest in the task outp
15903
16186
  `;
15904
16187
 
15905
16188
  // templates/schedules/daily-swarm-update-check/config.json
15906
- var config_default57 = `{
16189
+ var config_default59 = `{
15907
16190
  "kind": "schedule",
15908
16191
  "name": "daily-swarm-update-check",
15909
16192
  "displayName": "Daily Agent-Swarm Update Check",
@@ -15925,7 +16208,7 @@ var config_default57 = `{
15925
16208
  `;
15926
16209
 
15927
16210
  // templates/schedules/daily-swarm-update-check/content.md
15928
- var content_default57 = `# Daily Agent-Swarm Update Check
16211
+ var content_default59 = `# Daily Agent-Swarm Update Check
15929
16212
 
15930
16213
  Zero-config auto-update watchdog: compares the swarm's own running version against the latest published GitHub release of \`desplega-ai/agent-swarm\`, and flags the operator when an upgrade is available. No placeholders and no integration required — every install gets this by default.
15931
16214
 
@@ -15977,7 +16260,7 @@ Do not attempt the upgrade yourself — this schedule only detects and reports.
15977
16260
  `;
15978
16261
 
15979
16262
  // templates/schedules/daily-workflow-health-audit/config.json
15980
- var config_default58 = `{
16263
+ var config_default60 = `{
15981
16264
  "kind": "schedule",
15982
16265
  "name": "daily-workflow-health-audit",
15983
16266
  "displayName": "Daily Workflow Health Audit",
@@ -15999,7 +16282,7 @@ var config_default58 = `{
15999
16282
  `;
16000
16283
 
16001
16284
  // templates/schedules/daily-workflow-health-audit/content.md
16002
- var content_default58 = `# Daily Workflow Health Audit
16285
+ var content_default60 = `# Daily Workflow Health Audit
16003
16286
 
16004
16287
  Delivery uses configured admin channels with an in-app fallback.
16005
16288
 
@@ -16193,7 +16476,7 @@ Omit any section whose count is 0. **HARD CEILING 700 characters — not the 400
16193
16476
  `;
16194
16477
 
16195
16478
  // templates/schedules/gtm-weekly-review/config.json
16196
- var config_default59 = `{
16479
+ var config_default61 = `{
16197
16480
  "kind": "schedule",
16198
16481
  "name": "gtm-weekly-review",
16199
16482
  "displayName": "Weekly GTM Metrics Review",
@@ -16210,7 +16493,7 @@ var config_default59 = `{
16210
16493
  `;
16211
16494
 
16212
16495
  // templates/schedules/gtm-weekly-review/content.md
16213
- var content_default59 = `Template parameters: {{REPO_URL}}, {{GSC_PROPERTY}}
16496
+ var content_default61 = `Template parameters: {{REPO_URL}}, {{GSC_PROPERTY}}
16214
16497
 
16215
16498
  # Weekly GTM Metrics Review
16216
16499
 
@@ -16291,7 +16574,7 @@ This is part of your team's GTM goal; update the goal statement before enabling
16291
16574
  `;
16292
16575
 
16293
16576
  // templates/schedules/weekly-code-health-reports/config.json
16294
- var config_default60 = `{
16577
+ var config_default62 = `{
16295
16578
  "kind": "schedule",
16296
16579
  "name": "weekly-code-health-reports",
16297
16580
  "displayName": "Weekly Code Health Reports",
@@ -16308,7 +16591,7 @@ var config_default60 = `{
16308
16591
  `;
16309
16592
 
16310
16593
  // templates/schedules/weekly-code-health-reports/content.md
16311
- var content_default60 = `# Weekly Code Health Reports
16594
+ var content_default62 = `# Weekly Code Health Reports
16312
16595
 
16313
16596
  Template parameters: {{REPO_URL}}, {{BRANCH}}, {{SCOPE_PATH}}, {{REPORT_NAME}}, {{PAGE_ID}}
16314
16597
 
@@ -16373,7 +16656,7 @@ Call \`store-progress\` with status \`completed\` and an output summary that inc
16373
16656
  `;
16374
16657
 
16375
16658
  // templates/schedules/weekly-dependabot-triage/config.json
16376
- var config_default61 = `{
16659
+ var config_default63 = `{
16377
16660
  "kind": "schedule",
16378
16661
  "name": "weekly-dependabot-triage",
16379
16662
  "displayName": "Weekly Dependency Triage",
@@ -16390,7 +16673,7 @@ var config_default61 = `{
16390
16673
  `;
16391
16674
 
16392
16675
  // templates/schedules/weekly-dependabot-triage/content.md
16393
- var content_default61 = `# Weekly Dependency Triage
16676
+ var content_default63 = `# Weekly Dependency Triage
16394
16677
 
16395
16678
  Template parameters: {{SLACK_CHANNEL_ID}}, {{REPO_URL}}, {{TIMEZONE}}
16396
16679
 
@@ -16444,7 +16727,7 @@ Call \`store-progress\` with status \`completed\` and an output summary listing
16444
16727
  `;
16445
16728
 
16446
16729
  // templates/schedules/weekly-dora-metrics/config.json
16447
- var config_default62 = `{
16730
+ var config_default64 = `{
16448
16731
  "kind": "schedule",
16449
16732
  "name": "weekly-dora-metrics",
16450
16733
  "displayName": "Weekly DORA Metrics",
@@ -16461,7 +16744,7 @@ var config_default62 = `{
16461
16744
  `;
16462
16745
 
16463
16746
  // templates/schedules/weekly-dora-metrics/content.md
16464
- var content_default62 = `# Weekly DORA Metrics
16747
+ var content_default64 = `# Weekly DORA Metrics
16465
16748
 
16466
16749
  Template parameters: {{REPO_URL}}, {{BRANCH}}, {{TAG_PATTERN}}, {{REPORT_NAME}}, {{PAGE_ID}}
16467
16750
 
@@ -16527,7 +16810,7 @@ Call \`store-progress\` with status \`completed\` and an output summary that inc
16527
16810
  `;
16528
16811
 
16529
16812
  // templates/schedules/weekly-harness-upgrade-check/config.json
16530
- var config_default63 = `{
16813
+ var config_default65 = `{
16531
16814
  "kind": "schedule",
16532
16815
  "name": "weekly-harness-upgrade-check",
16533
16816
  "displayName": "Weekly Harness Upgrade Check",
@@ -16544,7 +16827,7 @@ var config_default63 = `{
16544
16827
  `;
16545
16828
 
16546
16829
  // templates/schedules/weekly-harness-upgrade-check/content.md
16547
- var content_default63 = '# Weekly Harness Upgrade Check\n\nThis is a topology-preserving, anonymized port of the live Sunday schedule. It keeps its bundle, deduplication, baseline, partial-fallback, and CI acceptance behavior.\n\n```json\n{"cron":"0 16 * * 0","timezone":"UTC","agentRole":"lead","enabled":true}\n```\n\n**Weekly Harness Upgrade — self-hosted repository Dockerfile.worker**\n\nRepo: `{{REPO_URL}}` (working dir: `/workspace/repo`)\nFile: `Dockerfile.worker`\n\nThe worker image pins these harnesses via `ARG`. Each week, check upstream for new versions and open ONE PR bundling ALL outdated harness bumps together ({{PR_REVIEWER}}\'s preference — bump ALL in one PR per run, do NOT stagger one-per-run).\n\n| ARG | Source | Sync requirement |\n|---|---|---|\n| `CLAUDE_CODE_VERSION` | npm `@anthropic-ai/claude-code` | none |\n| `PI_CODING_AGENT_VERSION` | npm `@earendil-works/pi-coding-agent` | MUST match `@earendil-works/pi-agent-core`, `@earendil-works/pi-ai`, and `@earendil-works/pi-coding-agent` in `package.json` (family-pin, all same version) — bump all together |\n| `CODEX_VERSION` | npm `@openai/codex` | MUST match direct `@openai/codex` in `package.json` (enforced by `scripts/check-codex-default-model.sh`) |\n| `OPENCODE_VERSION` | npm `@opencode/sdk` | Installs both `@opencode/cli` and `@opencode/sdk`. MUST match `@opencode/sdk` and `@opencode/plugin` in `package.json` devDependencies. OpenCode 2 ships under the `@opencode/*` scope; never take a version from `opencode-ai`, `@opencode-ai/sdk`, or the `sst/opencode` GitHub releases (still the 1.x line, which would be a downgrade) |\n\n## Steps\n\n1. `cd /workspace/repo && git fetch origin && git checkout main && git pull --ff-only`\n2. Read current pinned versions from `Dockerfile.worker` (the `ARG` lines) and from `package.json` (for the pi family + `@openai/codex` + `@opencode/sdk`).\n3. For each harness, fetch the latest version:\n - npm claude-code: `npm view @anthropic-ai/claude-code version`\n - npm pi: `npm view @earendil-works/pi-coding-agent version` (use the SAME version for `@earendil-works/pi-agent-core` and `@earendil-works/pi-ai` — they\'re a family pin)\n - npm codex: `npm view @openai/codex version`\n - npm opencode: `npm view @opencode/sdk version` (one version for `@opencode/cli`, `@opencode/sdk`, `@opencode/plugin`)\n4. Check for an existing open bundled PR — `gh pr list --search "harness upgrade in:title" --state open --repo {{REPO_URL}}`. Look at the branch prefix `harness-upgrade/`. If one is already open AND was created within the last 48h (sibling schedule may have just opened it), skip and complete with `"Skipped: sibling harness upgrade PR already open"` + the existing PR URL.\n5. Collect ALL outdated harnesses. If none are outdated, complete with output `"All harnesses up to date as of YYYY-MM-DD"` (no PR).\n6. Branch: `harness-upgrade/weekly-YYYY-MM-DD` (e.g. `harness-upgrade/weekly-2026-06-03`).\n7. Edit `Dockerfile.worker` — bump every outdated ARG. Also update `package.json`:\n - Codex: bump direct `@openai/codex` to match `CODEX_VERSION`.\n - Pi family: bump `@earendil-works/pi-agent-core`, `@earendil-works/pi-ai`, and `@earendil-works/pi-coding-agent` to the same new version as `PI_CODING_AGENT_VERSION`.\n - Opencode: bump `@opencode/sdk` and `@opencode/plugin` (devDependencies) to match `OPENCODE_VERSION`.\n Then re-run `bun install` so `bun.lock` updates.\n8. Local verification (all must pass before pushing):\n ```bash\n bun install --frozen-lockfile\n bun run lint\n bun run tsc:check\n bun test\n bash scripts/check-db-boundary.sh\n ```\n For Codex bumps: also run `bash scripts/check-codex-default-model.sh`.\n Note: do NOT run `docker build -f Dockerfile.worker .` locally — the runner lacks docker. CI\'s `docker-worker-build` job is the acceptance gate.\n9. If any local check fails, apply the **partial-bundle fallback** before giving up — one harness must never block the others:\n **BASELINE FIRST — do this before ANY isolation work.** A red gate does NOT mean the bumps broke anything. Re-run the EXACT failing command on a pristine `origin/main` worktree with zero bumps applied (`git worktree add /tmp/hb-baseline origin/main && cd /tmp/hb-baseline && bun install --frozen-lockfile`) and compare. If the SAME checks fail with the SAME assertions and the same pass/skip/fail totals, the failure is **PRE-EXISTING and unrelated to the bumps** ⇒ it does NOT block this PR, and you must NOT attempt isolation for it. Proceed to step 10, open the PR, and add a `## Pre-existing failures (also fail on origin/main)` section quoting the verbatim error plus both totals (baseline vs branch) so the reviewer can see they match. Only a failure that is RED on your branch and GREEN on baseline was caused by a bump — apply a–d below to that failure ONLY. A gate that is red on origin/main is never a reason to skip the weekly bump.\n a. Isolate which harness\'s bump caused it. The error usually names the package; if not, revert bumps one at a time until the step-8 gate passes.\n b. Revert ONLY that harness — its `Dockerfile.worker` ARG **and** its `package.json` pins (respect the family/sync requirements in the table above) — then re-run `bun install` and the FULL step-8 gate.\n c. Gate green ⇒ open the PR with the remaining harnesses and add a `## Held back` section naming the excluded harness, current → attempted version, and the VERBATIM error. Name it in your `store-progress` output too.\n d. Gate still red with that harness reverted, OR the failure cannot be isolated to one harness ⇒ do NOT open a PR; report via `store-progress` with `failureReason`.\n Migrating an upstream API break (a removed option, a renamed export, a moved type) is still OUT OF SCOPE for this routine — hold that harness back per (c) rather than changing code here.\n10. Push the branch and open a PR:\n - Title: `chore(docker): bump harnesses (weekly YYYY-MM-DD)` — or list the bumps, e.g. `chore(docker): bump claude-code, pi, opencode (weekly YYYY-MM-DD)`.\n - Body: include a table of current → new versions for every harness bumped, link to each upstream changelog (e.g. https://github.com/anthropics/claude-code/releases/tag/v<X.Y.Z>), and the local verification output (or "all green").\n - Add `{{PR_REVIEWER}}` as a reviewer.\n - DO NOT merge — {{PR_REVIEWER}} reviews and merges.\n11. Verify CI on the PR — wait for the merge-gate workflow to start, confirm at least the `docker-worker-build` job is queued/passing. If CI fails, comment on the PR with the failure log and leave the branch for human review.\n\n## Constraints\n\n- **ONE bundled PR per run.** Bump every outdated harness in the same PR — do not stagger one-per-run. The ONLY permitted exclusion is a harness held back by the step-9 partial-bundle fallback, which must be declared in the PR body.\n- **No `--no-verify`, no force pushes.**\n- If a harness has a major-version bump (e.g. 2.x → 3.x), still bundle it but flag clearly in the PR description that this is a major bump and may need extra review.\n- Pi family-pin: all three pi packages MUST be the same version. If they drift in the lockfile, fix the drift in the same PR.\n\n## Sibling-schedule coordination\nThis schedule has a sibling — `weekly-harness-upgrade-check-wed` (Wednesday 05:00 BCN). If a sibling-opened PR is OPEN and `< 48h` old, step 4 skips. This is the dedup gate. Don\'t change branch-naming heuristics in a way that breaks the dedup.\n\n## Output\n\n`store-progress` with `status: "completed"` and `output` containing:\n- The PR URL (if one was opened) + the table of harnesses bumped, OR\n- "No-op: all harnesses up to date as of YYYY-MM-DD" with the version table, OR\n- "Skipped: sibling harness upgrade PR already open" with the existing PR URL.\n';
16830
+ var content_default65 = '# Weekly Harness Upgrade Check\n\nThis is a topology-preserving, anonymized port of the live Sunday schedule. It keeps its bundle, deduplication, baseline, partial-fallback, and CI acceptance behavior.\n\n```json\n{"cron":"0 16 * * 0","timezone":"UTC","agentRole":"lead","enabled":true}\n```\n\n**Weekly Harness Upgrade — self-hosted repository Dockerfile.worker**\n\nRepo: `{{REPO_URL}}` (working dir: `/workspace/repo`)\nFile: `Dockerfile.worker`\n\nThe worker image pins these harnesses via `ARG`. Each week, check upstream for new versions and open ONE PR bundling ALL outdated harness bumps together ({{PR_REVIEWER}}\'s preference — bump ALL in one PR per run, do NOT stagger one-per-run).\n\n| ARG | Source | Sync requirement |\n|---|---|---|\n| `CLAUDE_CODE_VERSION` | npm `@anthropic-ai/claude-code` | none |\n| `PI_CODING_AGENT_VERSION` | npm `@earendil-works/pi-coding-agent` | MUST match `@earendil-works/pi-agent-core`, `@earendil-works/pi-ai`, and `@earendil-works/pi-coding-agent` in `package.json` (family-pin, all same version) — bump all together |\n| `CODEX_VERSION` | npm `@openai/codex` | MUST match direct `@openai/codex` in `package.json` (enforced by `scripts/check-codex-default-model.sh`) |\n| `OPENCODE_VERSION` | npm `@opencode/sdk` | Installs both `@opencode/cli` and `@opencode/sdk`. MUST match `@opencode/sdk` and `@opencode/plugin` in `package.json` devDependencies. OpenCode 2 ships under the `@opencode/*` scope; never take a version from `opencode-ai`, `@opencode-ai/sdk`, or the `sst/opencode` GitHub releases (still the 1.x line, which would be a downgrade) |\n\n## Steps\n\n1. `cd /workspace/repo && git fetch origin && git checkout main && git pull --ff-only`\n2. Read current pinned versions from `Dockerfile.worker` (the `ARG` lines) and from `package.json` (for the pi family + `@openai/codex` + `@opencode/sdk`).\n3. For each harness, fetch the latest version:\n - npm claude-code: `npm view @anthropic-ai/claude-code version`\n - npm pi: `npm view @earendil-works/pi-coding-agent version` (use the SAME version for `@earendil-works/pi-agent-core` and `@earendil-works/pi-ai` — they\'re a family pin)\n - npm codex: `npm view @openai/codex version`\n - npm opencode: `npm view @opencode/sdk version` (one version for `@opencode/cli`, `@opencode/sdk`, `@opencode/plugin`)\n4. Check for an existing open bundled PR — `gh pr list --search "harness upgrade in:title" --state open --repo {{REPO_URL}}`. Look at the branch prefix `harness-upgrade/`. If one is already open AND was created within the last 48h (sibling schedule may have just opened it), skip and complete with `"Skipped: sibling harness upgrade PR already open"` + the existing PR URL.\n5. Collect ALL outdated harnesses. If none are outdated, complete with output `"All harnesses up to date as of YYYY-MM-DD"` (no PR).\n6. Branch: `harness-upgrade/weekly-YYYY-MM-DD` (e.g. `harness-upgrade/weekly-2026-06-03`).\n7. Edit `Dockerfile.worker` — bump every outdated ARG. Also update `package.json`:\n - Codex: bump direct `@openai/codex` to match `CODEX_VERSION`.\n - Pi family: bump `@earendil-works/pi-agent-core`, `@earendil-works/pi-ai`, and `@earendil-works/pi-coding-agent` to the same new version as `PI_CODING_AGENT_VERSION`.\n - Opencode: bump `@opencode/sdk` and `@opencode/plugin` (devDependencies) to match `OPENCODE_VERSION`.\n Then re-run `bun install` so `bun.lock` updates.\n8. Local verification (all must pass before pushing):\n ```bash\n bun install --frozen-lockfile\n bun run lint\n bun run tsc:check\n bun test\n bash scripts/check-db-boundary.sh\n ```\n For Codex bumps: also run `bash scripts/check-codex-default-model.sh`.\n Note: do NOT run `docker build -f Dockerfile.worker .` locally — the runner lacks docker. CI\'s `docker-worker-build` job is the acceptance gate.\n9. If any local check fails, apply the **partial-bundle fallback** before giving up — one harness must never block the others:\n **BASELINE FIRST — do this before ANY isolation work.** A red gate does NOT mean the bumps broke anything. Re-run the EXACT failing command on a pristine `origin/main` worktree with zero bumps applied (`git worktree add /tmp/hb-baseline origin/main && cd /tmp/hb-baseline && bun install --frozen-lockfile`) and compare. If the SAME checks fail with the SAME assertions and the same pass/skip/fail totals, the failure is **PRE-EXISTING and unrelated to the bumps** ⇒ it does NOT block this PR, and you must NOT attempt isolation for it. Proceed to step 10, open the PR, and add a `## Pre-existing failures (also fail on origin/main)` section quoting the verbatim error plus both totals (baseline vs branch) so the reviewer can see they match. Only a failure that is RED on your branch and GREEN on baseline was caused by a bump — apply a–d below to that failure ONLY. A gate that is red on origin/main is never a reason to skip the weekly bump.\n a. Isolate which harness\'s bump caused it. The error usually names the package; if not, revert bumps one at a time until the step-8 gate passes.\n b. Revert ONLY that harness — its `Dockerfile.worker` ARG **and** its `package.json` pins (respect the family/sync requirements in the table above) — then re-run `bun install` and the FULL step-8 gate.\n c. Gate green ⇒ open the PR with the remaining harnesses and add a `## Held back` section naming the excluded harness, current → attempted version, and the VERBATIM error. Name it in your `store-progress` output too.\n d. Gate still red with that harness reverted, OR the failure cannot be isolated to one harness ⇒ do NOT open a PR; report via `store-progress` with `failureReason`.\n Migrating an upstream API break (a removed option, a renamed export, a moved type) is still OUT OF SCOPE for this routine — hold that harness back per (c) rather than changing code here.\n10. Push the branch and open a PR:\n - Title: `chore(docker): bump harnesses (weekly YYYY-MM-DD)` — or list the bumps, e.g. `chore(docker): bump claude-code, pi, opencode (weekly YYYY-MM-DD)`.\n - Body: include a table of current → new versions for every harness bumped, link to each upstream changelog (e.g. https://github.com/anthropics/claude-code/releases/tag/v<X.Y.Z>), and the local verification output (or "all green").\n - Add `{{PR_REVIEWER}}` as a reviewer.\n - DO NOT merge — {{PR_REVIEWER}} reviews and merges.\n11. Verify CI on the PR — wait for the merge-gate workflow to start, confirm at least the `docker-worker-build` job is queued/passing. If CI fails, comment on the PR with the failure log and leave the branch for human review.\n\n## Constraints\n\n- **ONE bundled PR per run.** Bump every outdated harness in the same PR — do not stagger one-per-run. The ONLY permitted exclusion is a harness held back by the step-9 partial-bundle fallback, which must be declared in the PR body.\n- **No `--no-verify`, no force pushes.**\n- If a harness has a major-version bump (e.g. 2.x → 3.x), still bundle it but flag clearly in the PR description that this is a major bump and may need extra review.\n- Pi family-pin: all three pi packages MUST be the same version. If they drift in the lockfile, fix the drift in the same PR.\n\n## Sibling-schedule coordination\nThis schedule has a sibling — `weekly-harness-upgrade-check-wed` (Wednesday 05:00 BCN). If a sibling-opened PR is OPEN and `< 48h` old, step 4 skips. This is the dedup gate. Don\'t change branch-naming heuristics in a way that breaks the dedup.\n\n## Output\n\n`store-progress` with `status: "completed"` and `output` containing:\n- The PR URL (if one was opened) + the table of harnesses bumped, OR\n- "No-op: all harnesses up to date as of YYYY-MM-DD" with the version table, OR\n- "Skipped: sibling harness upgrade PR already open" with the existing PR URL.\n';
16548
16831
 
16549
16832
  // src/be/seed/schedules-seeder.ts
16550
16833
  init_db2();
@@ -16566,7 +16849,7 @@ function resolveSeededEnabled(params) {
16566
16849
  }
16567
16850
 
16568
16851
  // templates/workflows/alerts-triage/config.json
16569
- var config_default64 = `{
16852
+ var config_default66 = `{
16570
16853
  "kind": "workflow",
16571
16854
  "name": "alerts-triage",
16572
16855
  "displayName": "Alerts Triage",
@@ -16583,7 +16866,7 @@ var config_default64 = `{
16583
16866
  `;
16584
16867
 
16585
16868
  // templates/workflows/alerts-triage/content.md
16586
- var content_default64 = `# Alerts Triage
16869
+ var content_default66 = `# Alerts Triage
16587
16870
 
16588
16871
  The Slack handler emits \`slack.message\`; this workflow keeps the event-driven path and deduplicates work after the channel filter.
16589
16872
 
@@ -16603,7 +16886,7 @@ The Slack handler emits \`slack.message\`; this workflow keeps the event-driven
16603
16886
  `;
16604
16887
 
16605
16888
  // templates/workflows/autopilot/config.json
16606
- var config_default65 = `{
16889
+ var config_default67 = `{
16607
16890
  "kind": "workflow",
16608
16891
  "name": "autopilot",
16609
16892
  "displayName": "Autopilot Feature Pipeline",
@@ -16620,7 +16903,7 @@ var config_default65 = `{
16620
16903
  `;
16621
16904
 
16622
16905
  // templates/workflows/autopilot/content.md
16623
- var content_default65 = `# Autopilot Feature Pipeline
16906
+ var content_default67 = `# Autopilot Feature Pipeline
16624
16907
 
16625
16908
  Template parameter: {{REPO_URL}}
16626
16909
 
@@ -16632,7 +16915,7 @@ Use this workflow as a canonical feature delivery pipeline. Customize roles and
16632
16915
  `;
16633
16916
 
16634
16917
  // templates/workflows/claude-code-changelog-watch/config.json
16635
- var config_default66 = `{
16918
+ var config_default68 = `{
16636
16919
  "kind": "workflow",
16637
16920
  "name": "claude-code-changelog-watch",
16638
16921
  "displayName": "Claude Code Changelog Watch",
@@ -16649,10 +16932,10 @@ var config_default66 = `{
16649
16932
  `;
16650
16933
 
16651
16934
  // templates/workflows/claude-code-changelog-watch/content.md
16652
- var content_default66 = '# Claude Code Changelog Watch\n\nThis topology-preserving self-hosted port removes installation-specific identities, endpoints, and credentials while retaining the live workflow\'s nodes, edges, input mappings, guards, retries, cooldown, and triggers. Configure the declared template parameters and integrations before enabling it.\n\nPrerequisite: the `check` and `commit` nodes run the global script `claude-code-changelog-check`, which is not seeded. Author it before enabling the workflow, or every run fails at the first node.\n\n```json\n{"name":"claude-code-changelog-watch","description":"Changelog watcher with deduplication: fetch, gate on a new version, publish an in-app receipt plus any configured delivery providers, then commit the version marker.","nodes":[{"id":"check","type":"swarm-script","label":"Fetch changelog + compare vs KV","config":{"scriptName":"claude-code-changelog-check","scope":"global","args":{"mode":"check"}},"next":"gate"},{"id":"gate","type":"property-match","label":"Gate: only continue on a new version","config":{"mode":"all","conditions":[{"field":"check.result.isNew","op":"eq","value":true}]},"next":{"true":"post"}},{"id":"post","type":"agent-task","label":"Post new version to Slack","config":{"model":"sonnet","tags":["changelog","claude-code","workflow"],"priority":50,"template":"**Claude Code new version announcement**\\n\\nA new Claude Code version was detected by the claude-code-changelog-watch workflow.\\n\\nNew version: {{check.result.latestVersion}} (released {{check.result.releaseDate}}, previous known: {{check.result.lastKnown}})\\n\\nRaw release notes:\\n{{check.result.section}}\\n\\nSteps:\\n1. ONE `slack-post` to the configured Slack notification target, **max 400 chars**:\\n\\n```\\n:rocket: *Claude Code {{check.result.latestVersion}}* — <the one change that matters to us, one line>\\n<https://code.claude.com/docs/en/changelog|changelog>\\n```\\n\\nRules:\\n- **One line.** Pick the single item that affects how we run the swarm (a fix we were hitting, a flag we use, a breaking change). Not 3-5 bullets.\\n- If a second item genuinely matters, one more bullet. Never more than two.\\n- **Routine release with nothing relevant to us ⇒ still post, but the one line is just the headline feature.** Never paste the release notes.\\n- No preamble, no \\"is out!\\", no per-surface breakdown (VSCode/CLI/SDK), no closing line.\\n\\n2. Complete with output \\"Posted Claude Code {{check.result.latestVersion}} to in-app activity and configured delivery providers.\\" plus the charCount.\\n\\nDo NOT update any version state — the workflow\'s commit node handles that after you complete. Do NOT post more than once."},"next":"commit","inputs":{"check":"check"}},{"id":"commit","type":"swarm-script","label":"Commit new version to KV","config":{"scriptName":"claude-code-changelog-check","scope":"global","args":{"mode":"commit","version":"{{check.result.latestVersion}}"}},"inputs":{"check":"check"}}],"onNodeFailure":"fail"}\n```\n\n\n\n\n';
16935
+ var content_default68 = '# Claude Code Changelog Watch\n\nThis topology-preserving self-hosted port removes installation-specific identities, endpoints, and credentials while retaining the live workflow\'s nodes, edges, input mappings, guards, retries, cooldown, and triggers. Configure the declared template parameters and integrations before enabling it.\n\nPrerequisite: the `check` and `commit` nodes run the global script `claude-code-changelog-check`, which is not seeded. Author it before enabling the workflow, or every run fails at the first node.\n\n```json\n{"name":"claude-code-changelog-watch","description":"Changelog watcher with deduplication: fetch, gate on a new version, publish an in-app receipt plus any configured delivery providers, then commit the version marker.","nodes":[{"id":"check","type":"swarm-script","label":"Fetch changelog + compare vs KV","config":{"scriptName":"claude-code-changelog-check","scope":"global","args":{"mode":"check"}},"next":"gate"},{"id":"gate","type":"property-match","label":"Gate: only continue on a new version","config":{"mode":"all","conditions":[{"field":"check.result.isNew","op":"eq","value":true}]},"next":{"true":"post"}},{"id":"post","type":"agent-task","label":"Post new version to Slack","config":{"model":"sonnet","tags":["changelog","claude-code","workflow"],"priority":50,"template":"**Claude Code new version announcement**\\n\\nA new Claude Code version was detected by the claude-code-changelog-watch workflow.\\n\\nNew version: {{check.result.latestVersion}} (released {{check.result.releaseDate}}, previous known: {{check.result.lastKnown}})\\n\\nRaw release notes:\\n{{check.result.section}}\\n\\nSteps:\\n1. ONE `slack-post` to the configured Slack notification target, **max 400 chars**:\\n\\n```\\n:rocket: *Claude Code {{check.result.latestVersion}}* — <the one change that matters to us, one line>\\n<https://code.claude.com/docs/en/changelog|changelog>\\n```\\n\\nRules:\\n- **One line.** Pick the single item that affects how we run the swarm (a fix we were hitting, a flag we use, a breaking change). Not 3-5 bullets.\\n- If a second item genuinely matters, one more bullet. Never more than two.\\n- **Routine release with nothing relevant to us ⇒ still post, but the one line is just the headline feature.** Never paste the release notes.\\n- No preamble, no \\"is out!\\", no per-surface breakdown (VSCode/CLI/SDK), no closing line.\\n\\n2. Complete with output \\"Posted Claude Code {{check.result.latestVersion}} to in-app activity and configured delivery providers.\\" plus the charCount.\\n\\nDo NOT update any version state — the workflow\'s commit node handles that after you complete. Do NOT post more than once."},"next":"commit","inputs":{"check":"check"}},{"id":"commit","type":"swarm-script","label":"Commit new version to KV","config":{"scriptName":"claude-code-changelog-check","scope":"global","args":{"mode":"commit","version":"{{check.result.latestVersion}}"}},"inputs":{"check":"check"}}],"onNodeFailure":"fail"}\n```\n\n\n\n\n';
16653
16936
 
16654
16937
  // templates/workflows/competitor-radar/config.json
16655
- var config_default67 = `{
16938
+ var config_default69 = `{
16656
16939
  "kind": "workflow",
16657
16940
  "name": "competitor-radar",
16658
16941
  "displayName": "Competitor Radar",
@@ -16669,7 +16952,7 @@ var config_default67 = `{
16669
16952
  `;
16670
16953
 
16671
16954
  // templates/workflows/competitor-radar/content.md
16672
- var content_default67 = `# Competitor Radar
16955
+ var content_default69 = `# Competitor Radar
16673
16956
 
16674
16957
  This self-hosted port retains the scheduled, fixture, extraction, gap-analysis, litmus, cooldown, and bounded-report behavior. Its pinned GSC helper is embedded in the workflow, so it does not fetch executable code from Desplega infrastructure.
16675
16958
 
@@ -16679,7 +16962,7 @@ This self-hosted port retains the scheduled, fixture, extraction, gap-analysis,
16679
16962
  `;
16680
16963
 
16681
16964
  // templates/workflows/docs-site-releases/config.json
16682
- var config_default68 = `{
16965
+ var config_default70 = `{
16683
16966
  "kind": "workflow",
16684
16967
  "name": "docs-site-releases",
16685
16968
  "displayName": "Documentation Release Notes",
@@ -16696,7 +16979,7 @@ var config_default68 = `{
16696
16979
  `;
16697
16980
 
16698
16981
  // templates/workflows/docs-site-releases/content.md
16699
- var content_default68 = `# Documentation Release Notes
16982
+ var content_default70 = `# Documentation Release Notes
16700
16983
 
16701
16984
  This topology-preserving self-hosted port removes installation-specific identities, endpoints, and credentials while retaining the live workflow's nodes, edges, input mappings, guards, retries, cooldown, and triggers. Configure the declared template parameters and integrations before enabling it.
16702
16985
 
@@ -16711,7 +16994,7 @@ This topology-preserving self-hosted port removes installation-specific identiti
16711
16994
  `;
16712
16995
 
16713
16996
  // templates/workflows/gsc-topic-miner/config.json
16714
- var config_default69 = `{
16997
+ var config_default71 = `{
16715
16998
  "kind": "workflow",
16716
16999
  "name": "gsc-topic-miner",
16717
17000
  "displayName": "Search Topic Miner",
@@ -16728,7 +17011,7 @@ var config_default69 = `{
16728
17011
  `;
16729
17012
 
16730
17013
  // templates/workflows/gsc-topic-miner/content.md
16731
- var content_default69 = `# Search Topic Miner
17014
+ var content_default71 = `# Search Topic Miner
16732
17015
 
16733
17016
  This template uses only the configured GSC and agent-fs integrations. The deferred seeder creates the weekly schedule binding; no installation-specific schedule ID is shipped.
16734
17017
 
@@ -16738,7 +17021,7 @@ This template uses only the configured GSC and agent-fs integrations. The deferr
16738
17021
  `;
16739
17022
 
16740
17023
  // templates/workflows/linear-drain-loop/config.json
16741
- var config_default70 = `{
17024
+ var config_default72 = `{
16742
17025
  "kind": "workflow",
16743
17026
  "name": "linear-drain-loop",
16744
17027
  "displayName": "Linear Drain Loop",
@@ -16760,7 +17043,7 @@ var config_default70 = `{
16760
17043
  `;
16761
17044
 
16762
17045
  // templates/workflows/linear-drain-loop/content.md
16763
- var content_default70 = `# Linear Drain Loop
17046
+ var content_default72 = `# Linear Drain Loop
16764
17047
 
16765
17048
  Template parameter: {{LINEAR_PROJECT_ID}}
16766
17049
 
@@ -16805,7 +17088,7 @@ Blocked and ambiguous items are explicitly left alone — the dispatch node surf
16805
17088
  `;
16806
17089
 
16807
17090
  // templates/workflows/llm-safe-release-context/config.json
16808
- var config_default71 = `{
17091
+ var config_default73 = `{
16809
17092
  "kind": "workflow",
16810
17093
  "name": "llm-safe-release-context",
16811
17094
  "displayName": "LLM-Safe Release Context",
@@ -16822,7 +17105,7 @@ var config_default71 = `{
16822
17105
  `;
16823
17106
 
16824
17107
  // templates/workflows/llm-safe-release-context/content.md
16825
- var content_default71 = `# LLM-Safe Release Context
17108
+ var content_default73 = `# LLM-Safe Release Context
16826
17109
 
16827
17110
  Template parameters: {{ORG_ID}}, {{REPO_URL}}
16828
17111
 
@@ -16896,7 +17179,7 @@ This avoids the common failure mode where a large \`context.json\` fills the mod
16896
17179
  `;
16897
17180
 
16898
17181
  // templates/workflows/pr-review-status-sweep/config.json
16899
- var config_default72 = `{
17182
+ var config_default74 = `{
16900
17183
  "kind": "workflow",
16901
17184
  "name": "pr-review-status-sweep",
16902
17185
  "displayName": "PR Review Status Sweep",
@@ -16913,7 +17196,7 @@ var config_default72 = `{
16913
17196
  `;
16914
17197
 
16915
17198
  // templates/workflows/pr-review-status-sweep/content.md
16916
- var content_default72 = `# PR Review Status Sweep
17199
+ var content_default74 = `# PR Review Status Sweep
16917
17200
 
16918
17201
  This topology-preserving self-hosted port removes installation-specific identities, endpoints, and credentials while retaining the live workflow's nodes, edges, input mappings, guards, retries, cooldown, and triggers. Configure the declared template parameters and integrations before enabling it.
16919
17202
 
@@ -16928,7 +17211,7 @@ Prerequisite: the \`sweep\` node runs the agent-scoped script \`pr-review-status
16928
17211
  `;
16929
17212
 
16930
17213
  // templates/workflows/ralph-loop/config.json
16931
- var config_default73 = `{
17214
+ var config_default75 = `{
16932
17215
  "kind": "workflow",
16933
17216
  "name": "ralph-loop",
16934
17217
  "displayName": "Iterative Review Loop",
@@ -16945,7 +17228,7 @@ var config_default73 = `{
16945
17228
  `;
16946
17229
 
16947
17230
  // templates/workflows/ralph-loop/content.md
16948
- var content_default73 = `# Iterative Review Loop
17231
+ var content_default75 = `# Iterative Review Loop
16949
17232
 
16950
17233
  Template parameter: {{REPO_URL}}
16951
17234
 
@@ -16996,19 +17279,19 @@ The \`maxIterations\` parameter is declarative: the reviewer signals PASS when d
16996
17279
  init_db2();
16997
17280
  var asText2 = (value) => value;
16998
17281
  var BUILT_IN_WORKFLOW_SOURCES = [
16999
- { config: asText2(config_default64), content: asText2(content_default64) },
17000
- { config: asText2(config_default65), content: asText2(content_default65) },
17282
+ { config: asText2(config_default66), content: asText2(content_default66) },
17283
+ { config: asText2(config_default67), content: asText2(content_default67) },
17001
17284
  {
17002
- config: asText2(config_default66),
17003
- content: asText2(content_default66)
17285
+ config: asText2(config_default68),
17286
+ content: asText2(content_default68)
17004
17287
  },
17005
- { config: asText2(config_default67), content: asText2(content_default67) },
17006
- { config: asText2(config_default68), content: asText2(content_default68) },
17007
17288
  { config: asText2(config_default69), content: asText2(content_default69) },
17008
17289
  { config: asText2(config_default70), content: asText2(content_default70) },
17009
17290
  { config: asText2(config_default71), content: asText2(content_default71) },
17010
17291
  { config: asText2(config_default72), content: asText2(content_default72) },
17011
- { config: asText2(config_default73), content: asText2(content_default73) }
17292
+ { config: asText2(config_default73), content: asText2(content_default73) },
17293
+ { config: asText2(config_default74), content: asText2(content_default74) },
17294
+ { config: asText2(config_default75), content: asText2(content_default75) }
17012
17295
  ];
17013
17296
  function sortJson(value) {
17014
17297
  if (Array.isArray(value))
@@ -17124,34 +17407,34 @@ var workflowsSeeder = createWorkflowsSeeder();
17124
17407
  // src/be/seed/schedules-seeder.ts
17125
17408
  var asText3 = (value) => value;
17126
17409
  var BUILT_IN_SCHEDULE_SOURCES = [
17127
- { config: asText3(config_default53), content: asText3(content_default53) },
17128
- {
17129
- config: asText3(config_default54),
17130
- content: asText3(content_default54)
17131
- },
17132
17410
  { config: asText3(config_default55), content: asText3(content_default55) },
17133
- { config: asText3(config_default56), content: asText3(content_default56) },
17134
17411
  {
17135
- config: asText3(config_default57),
17136
- content: asText3(content_default57)
17412
+ config: asText3(config_default56),
17413
+ content: asText3(content_default56)
17137
17414
  },
17415
+ { config: asText3(config_default57), content: asText3(content_default57) },
17416
+ { config: asText3(config_default58), content: asText3(content_default58) },
17138
17417
  {
17139
- config: asText3(config_default58),
17140
- content: asText3(content_default58)
17418
+ config: asText3(config_default59),
17419
+ content: asText3(content_default59)
17141
17420
  },
17142
- { config: asText3(config_default59), content: asText3(content_default59) },
17143
17421
  {
17144
17422
  config: asText3(config_default60),
17145
17423
  content: asText3(content_default60)
17146
17424
  },
17425
+ { config: asText3(config_default61), content: asText3(content_default61) },
17147
17426
  {
17148
- config: asText3(config_default61),
17149
- content: asText3(content_default61)
17427
+ config: asText3(config_default62),
17428
+ content: asText3(content_default62)
17150
17429
  },
17151
- { config: asText3(config_default62), content: asText3(content_default62) },
17152
17430
  {
17153
17431
  config: asText3(config_default63),
17154
17432
  content: asText3(content_default63)
17433
+ },
17434
+ { config: asText3(config_default64), content: asText3(content_default64) },
17435
+ {
17436
+ config: asText3(config_default65),
17437
+ content: asText3(content_default65)
17155
17438
  }
17156
17439
  ];
17157
17440
  function parseScheduleSource(source) {