@desplega.ai/agent-swarm 2.7.5 → 2.7.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (319) hide show
  1. package/dist/{acp-adapter-g3ads55s.js → acp-adapter-7b4vk3h5.js} +5 -4
  2. package/dist/{actions-zwfykqqk.js → actions-3tt153af.js} +18 -17
  3. package/dist/{amp-adapter-z3s0ssdt.js → amp-adapter-njpts0yr.js} +1 -1
  4. package/dist/{anthropic-messages-x4q6rfq5.js → anthropic-messages-xey2skwx.js} +7 -7
  5. package/dist/{app-vmp4fgep.js → app-nky4378f.js} +3 -3
  6. package/dist/{assistant-vwde653f.js → assistant-jnb669pa.js} +24 -22
  7. package/dist/{azure-openai-responses-s1w5n70z.js → azure-openai-responses-26t7j9ra.js} +5 -5
  8. package/dist/{boot-reembed-375t7tbk.js → boot-reembed-9mgvaztt.js} +1 -1
  9. package/dist/{boot-reembed-gq9rcpnj.js → boot-reembed-qs2djdej.js} +1 -1
  10. package/dist/{boot-scrub-sweep-nd938kyf.js → boot-scrub-sweep-yjpdj7s1.js} +3 -3
  11. package/dist/{boot-seeds-aykx51v3.js → boot-seeds-5cm6ambd.js} +4 -4
  12. package/dist/{boxd-k1j26g3h.js → boxd-nv507va8.js} +5 -5
  13. package/dist/{claude-adapter-8j4hm099.js → claude-adapter-fn287zmm.js} +5 -4
  14. package/dist/{claude-managed-setup-0wyynpn1.js → claude-managed-setup-wtdz2mj2.js} +1 -1
  15. package/dist/{claude-sdk-session-q6wj5hmy.js → claude-sdk-session-x91kqyhm.js} +9677 -9514
  16. package/dist/{cli-j3jvnpe8.js → cli-08bmz9z9.js} +1 -1
  17. package/dist/{cli-gjr2y2xk.js → cli-0agcv1v5.js} +4 -1
  18. package/dist/{cli-y24sc29k.js → cli-1c6tzt8g.js} +30 -8
  19. package/dist/{cli-mjzynrse.js → cli-35rsp8ye.js} +1 -1
  20. package/dist/{cli-rpm2sgv0.js → cli-3fg0bdcv.js} +19 -2
  21. package/dist/{cli-nn4d8dne.js → cli-5b456bpf.js} +2 -2
  22. package/dist/{cli-bhjt02p8.js → cli-5jnhv72d.js} +1 -1
  23. package/dist/{cli-fqzx7gwa.js → cli-5zx699j9.js} +2 -2
  24. package/dist/{cli-hf6zt5m6.js → cli-69szn7w7.js} +17 -1
  25. package/dist/{cli-vqypxzh4.js → cli-6g0hb4cp.js} +8 -8
  26. package/dist/{cli-tqgg40ek.js → cli-6pfd54vx.js} +9 -8
  27. package/dist/{cli-bsn8nhrq.js → cli-6vcp76qk.js} +3 -3
  28. package/dist/{cli-zppwg9p9.js → cli-72v799xn.js} +1 -1
  29. package/dist/cli-82j36f2m.js +45 -0
  30. package/dist/{cli-hn9hxmz9.js → cli-8305gbq0.js} +29 -16
  31. package/dist/{cli-z8eh3ex8.js → cli-8sckt74b.js} +8 -8
  32. package/dist/{cli-bnz7a7qf.js → cli-95jzxcp3.js} +1 -1
  33. package/dist/{cli-0bxqnvq9.js → cli-adeegh7d.js} +29 -7
  34. package/dist/{cli-g61qxmge.js → cli-afkqkrtw.js} +3 -3
  35. package/dist/{cli-7a42nfy0.js → cli-ayayvfg4.js} +24 -4
  36. package/dist/{cli-bqt1mptt.js → cli-az48n0nr.js} +4 -4
  37. package/dist/{cli-z6jkx23q.js → cli-bfv7vdg6.js} +9 -9
  38. package/dist/{cli-pywf3eh5.js → cli-caj33vcq.js} +5 -5
  39. package/dist/{cli-5pag6rmn.js → cli-cb2a8pna.js} +1 -1
  40. package/dist/{cli-wvaawacd.js → cli-cppkf404.js} +5236 -72
  41. package/dist/{cli-mry0qzym.js → cli-ddaanqkd.js} +123 -10
  42. package/dist/{cli-0gey5fzy.js → cli-e0kmphd6.js} +2 -2
  43. package/dist/{cli-zh3fp8v2.js → cli-e46hg0xw.js} +1 -1
  44. package/dist/{cli-jm2vfpfd.js → cli-e5bzrn01.js} +1 -1
  45. package/dist/{cli-qxnf8z2z.js → cli-excnnjg2.js} +165 -153
  46. package/dist/{cli-71ptav7x.js → cli-ferkkrg9.js} +1 -1
  47. package/dist/{cli-vpf89f34.js → cli-fmjd0t1y.js} +3 -3
  48. package/dist/{cli-rm6hqj3z.js → cli-fyt2g4wd.js} +33 -139
  49. package/dist/cli-g08zdaec.js +55 -0
  50. package/dist/{cli-ehq736cn.js → cli-g7s47cc5.js} +4 -4
  51. package/dist/{cli-82ae98pz.js → cli-gz6q4eve.js} +3 -3
  52. package/dist/{cli-82jg7fp9.js → cli-he5nxaa8.js} +1 -1
  53. package/dist/{cli-rqkxrmgw.js → cli-je518txm.js} +39 -14
  54. package/dist/{cli-8hs41zy2.js → cli-jtjtabxe.js} +1 -1
  55. package/dist/{cli-611a1294.js → cli-k0wwfqbg.js} +18 -1
  56. package/dist/{cli-nzxr556s.js → cli-k1nje3v3.js} +38 -43
  57. package/dist/{cli-3h4nhjn8.js → cli-k8196sh2.js} +9 -7
  58. package/dist/{cli-afrrppsw.js → cli-k899ehvh.js} +2 -2
  59. package/dist/{cli-0731hvbw.js → cli-m0da8j35.js} +23 -9
  60. package/dist/cli-m2z8jagn.js +12078 -0
  61. package/dist/{cli-8veytvbh.js → cli-mkk9rnzz.js} +5 -5
  62. package/dist/{cli-7wspm5gm.js → cli-n6bv12jn.js} +528 -6
  63. package/dist/{cli-spknvxbq.js → cli-nmzhp1b5.js} +14 -12
  64. package/dist/{cli-p599f1qm.js → cli-p82cnnjq.js} +1 -1
  65. package/dist/{cli-xtjhfmpc.js → cli-pc6n7fre.js} +1 -1
  66. package/dist/{cli-vbyfwh92.js → cli-pjctyea3.js} +2 -2
  67. package/dist/{cli-nmbmy64z.js → cli-pq73h5mz.js} +3 -3
  68. package/dist/{cli-aa6yxc7m.js → cli-pz4rk021.js} +1 -1
  69. package/dist/{cli-7wcyk2s5.js → cli-q3tkegby.js} +1 -1
  70. package/dist/cli-qghe0h66.js +67 -0
  71. package/dist/{cli-6s7vdcta.js → cli-qh4d4tvk.js} +4 -4
  72. package/dist/{cli-mmwm7zbf.js → cli-rtt8g4v9.js} +1 -1
  73. package/dist/{cli-kg8hchyg.js → cli-sa02jsqt.js} +1 -1
  74. package/dist/{cli-1h3wcpbw.js → cli-snpj5zbj.js} +2 -2
  75. package/dist/{cli-a7v854rf.js → cli-tgrv9cjx.js} +263 -12
  76. package/dist/{cli-a47xy8eq.js → cli-vz22fyqv.js} +186 -54
  77. package/dist/{cli-1ayk1eas.js → cli-wk5yrfwd.js} +285 -42
  78. package/dist/{cli-1338796d.js → cli-ws19768n.js} +1755 -119
  79. package/dist/{cli-70r77n04.js → cli-xae4g51r.js} +1 -1
  80. package/dist/{cli-gf5hbmdr.js → cli-yjdgea32.js} +3 -3
  81. package/dist/{cli-xbkdm710.js → cli-yndmfxb3.js} +3 -3
  82. package/dist/{cli-rhw40jgb.js → cli-yyj3j5m4.js} +2 -2
  83. package/dist/cli.js +26 -25
  84. package/dist/{cloudflare-workers-ai-system-one-bm60fjm0.js → cloudflare-workers-ai-system-one-ef2a980q.js} +2 -2
  85. package/dist/{codex-adapter-q6k3gnyg.js → codex-adapter-cf2yxe9m.js} +3 -2
  86. package/dist/{codex-hook-9efm2x1m.js → codex-hook-a0c6g6t6.js} +1 -1
  87. package/dist/{codex-session-runner-3mwmk3d9.js → codex-session-runner-6vfzdgbd.js} +3 -2
  88. package/dist/{commands-evrenjn9.js → commands-t3mfy7vn.js} +9 -3
  89. package/dist/{controller-xjp8vswh.js → controller-q7xrbaz0.js} +8 -3
  90. package/dist/{controllers-connect-pp5jnxvc.js → controllers-connect-c6nm70kg.js} +20 -19
  91. package/dist/{cursor-adapter-yf47w4t0.js → cursor-adapter-ftd81ga8.js} +1 -1
  92. package/dist/{db-zxpw6aht.js → db-dk572x25.js} +3 -3
  93. package/dist/{db-rgfsh91f.js → db-vemtpmcc.js} +14 -4
  94. package/dist/{devtools-82fzqra8.js → devtools-qxfgbsqz.js} +1 -1
  95. package/dist/{dispatcher-smasgws7.js → dispatcher-kh7ecxh9.js} +17 -16
  96. package/dist/{docker-rrjhaqt9.js → docker-c1a3gqmf.js} +89 -22
  97. package/dist/{dsh-adapter-ftyy8dnp.js → dsh-adapter-v8628r89.js} +1 -1
  98. package/dist/{e2b-w1p0bnpm.js → e2b-fr12yccx.js} +5 -5
  99. package/dist/{e2b-qcaj675y.js → e2b-x6ywzak3.js} +3 -3
  100. package/dist/{easter-egg-3d-yyts7p16.js → easter-egg-3d-nenw4x63.js} +1 -1
  101. package/dist/{embedding-space-1z0mbg95.js → embedding-space-0jepb239.js} +6 -6
  102. package/dist/{embeddings-ygeg7tm7.js → embeddings-xxt49yxg.js} +1 -1
  103. package/dist/{execute-14qhqp4d.js → execute-1mhw6e4x.js} +1 -1
  104. package/dist/{external-channel-ttgrs3sx.js → external-channel-55v12e60.js} +2 -2
  105. package/dist/{fx-adapter-gt6pncj2.js → fx-adapter-bs8wqrk5.js} +6 -5
  106. package/dist/{google-generative-ai-yvmdxyxa.js → google-generative-ai-v94hr7h0.js} +5 -5
  107. package/dist/{google-vertex-jrtdh9gs.js → google-vertex-g7vr6cf8.js} +5 -5
  108. package/dist/{grok-adapter-sm4m5k4g.js → grok-adapter-gpjqmkcq.js} +6 -5
  109. package/dist/handlers-zxmp1ta4.js +32 -0
  110. package/dist/{heartbeat-qahz4p62.js → heartbeat-k42svnr0.js} +20 -19
  111. package/dist/{hook-4fd5wq5w.js → hook-114m7hap.js} +4 -3
  112. package/dist/{hook-kj3d398m.js → hook-6nphweh0.js} +24 -9
  113. package/dist/{http-1ted0j1k.js → http-r8cc4wz7.js} +1196 -211
  114. package/dist/index-a8phxqnk.js +37 -0
  115. package/dist/{index-q4b917zs.js → index-e3s8wmxy.js} +1 -1
  116. package/dist/{index-6rq3r5zb.js → index-fpk4bcd9.js} +1 -1
  117. package/dist/{index-5rk6rfzr.js → index-fvzke5kx.js} +1 -1
  118. package/dist/index-k6h00t4c.js +30 -0
  119. package/dist/{index-7v51awbn.js → index-nh3wyqdt.js} +1027 -577
  120. package/dist/{index-yqhz88yy.js → index-pxn30mse.js} +8 -8
  121. package/dist/index-xrz7te8h.js +33 -0
  122. package/dist/{keepalive-6cf311t5.js → keepalive-x060nt9p.js} +3 -3
  123. package/dist/lead-956sd2yc.js +71 -0
  124. package/dist/{maintenance-a4bm1bmt.js → maintenance-2xxfexhf.js} +1 -1
  125. package/dist/{mistral-conversations-4pmwj1c0.js → mistral-conversations-3gjpdb4b.js} +4 -4
  126. package/dist/{oauth-refresh-sweep-3h0dfw1k.js → oauth-refresh-sweep-fckc4ar5.js} +2 -2
  127. package/dist/{onboard-myn5hraa.js → onboard-zc48j237.js} +5 -5
  128. package/dist/{openai-codex-responses-44j573nm.js → openai-codex-responses-8pbxq9h5.js} +5 -5
  129. package/dist/{openai-completions-a4gee2a8.js → openai-completions-rm00s912.js} +4 -4
  130. package/dist/{openai-decisions-w4qkrcar.js → openai-decisions-8v957r2r.js} +1 -1
  131. package/dist/{openai-responses-rbp6h5kr.js → openai-responses-agxpf96k.js} +5 -5
  132. package/dist/{opencode-adapter-p2nkb5am.js → opencode-adapter-r690h1cr.js} +4 -3
  133. package/dist/{otel-impl-a1a0ef6b.js → otel-impl-r5xbh81q.js} +4 -4
  134. package/dist/{pi-mono-adapter-rzmpb72a.js → pi-mono-adapter-c7v8sdec.js} +7 -6
  135. package/dist/{pricing-refresh-83drrnxj.js → pricing-refresh-3pmre11v.js} +3 -3
  136. package/dist/{rbac-roles-s8y6ecb5.js → rbac-roles-3vbshwsb.js} +1 -1
  137. package/dist/{rbac-roles-0fb6e7pj.js → rbac-roles-910mmkgz.js} +2 -2
  138. package/dist/{render-v2-twmbgrz4.js → render-v2-pxykkbab.js} +3 -3
  139. package/dist/{resume-zfqc36xj.js → resume-adda0bwy.js} +6 -6
  140. package/dist/{runtime-qwggdpqr.js → runtime-fnbm7stk.js} +1 -1
  141. package/dist/{session-access-sc9pn9da.js → session-access-2fvamrdc.js} +4 -4
  142. package/dist/{session-source-backfill-qh07xr8g.js → session-source-backfill-08xvv0sm.js} +2 -2
  143. package/dist/{setup-4fr0m7a1.js → setup-1aqmvmh1.js} +3 -3
  144. package/dist/{typesafe-system-one-5mavm259.js → typesafe-system-one-0bmqcxfg.js} +2 -2
  145. package/dist/{virtual-modules-fh4ha15b.js → virtual-modules-d2pjk9j1.js} +5 -5
  146. package/dist/{work-objects-0wr8zxct.js → work-objects-9s0yrm5j.js} +20 -19
  147. package/dist/{worker-follow-up-ktyyazyp.js → worker-follow-up-3q78fqmr.js} +17 -16
  148. package/dist/worker-y15hy08h.js +71 -0
  149. package/openapi.json +289 -4
  150. package/package.json +9 -8
  151. package/src/apps/definition.ts +6 -5
  152. package/src/be/db/page-theme-tokens.ts +12 -0
  153. package/src/be/db/script-runs.ts +23 -0
  154. package/src/be/db/tasks/read.ts +131 -4
  155. package/src/be/db/tasks/write.ts +19 -0
  156. package/src/be/db-client.ts +114 -11
  157. package/src/be/db.ts +49 -6
  158. package/src/be/memory/providers/sqlite-store.ts +49 -12
  159. package/src/be/migrations/260_runtime_pool_cache.sql +20 -0
  160. package/src/be/migrations/262_session_visibility_indexes.sql +18 -0
  161. package/src/be/migrations/263_task_list_filter_index.sql +11 -0
  162. package/src/be/migrations/264_slack_thread_cover_index.sql +9 -0
  163. package/src/be/migrations/266_script_runs_source_task.sql +11 -0
  164. package/src/be/migrations/269_pages_design_system.sql +23 -0
  165. package/src/be/runtime-pools/cache-cleanup.ts +109 -0
  166. package/src/be/runtime-pools/db.ts +65 -6
  167. package/src/be/runtime-pools/remote-controller.ts +5 -0
  168. package/src/be/runtime-pools/supervisor.ts +33 -2
  169. package/src/be/scripts/typecheck.ts +22 -0
  170. package/src/be/seed/schedules-seeder.ts +0 -10
  171. package/src/be/seed-scripts/catalog/catalog-report.inline.ts +2 -0
  172. package/src/be/seed-scripts/catalog/catalog-report.ts +2 -0
  173. package/src/be/seed-scripts/catalog/memory-eval.ts +2 -0
  174. package/src/be/seed-scripts/catalog/swarm-update-check.ts +150 -0
  175. package/src/be/seed-scripts/catalog/task-failure-audit.ts +5 -1
  176. package/src/be/seed-scripts/index.ts +9 -0
  177. package/src/be/seed-skills/bundled-files.generated.json +3 -3
  178. package/src/be/swarm-config-guard.ts +32 -0
  179. package/src/be/task-auto-title-listener.ts +127 -0
  180. package/src/commands/controller.ts +4 -0
  181. package/src/commands/entrypoint-api-readiness.ts +74 -0
  182. package/src/commands/onboard/agent-fs-version.ts +1 -1
  183. package/src/commands/profile-sync.ts +151 -16
  184. package/src/commands/runner.ts +15 -7
  185. package/src/hooks/hook.ts +20 -9
  186. package/src/http/index.ts +4 -1
  187. package/src/http/list-total-cache.ts +14 -7
  188. package/src/http/memory.ts +38 -9
  189. package/src/http/pages-public.ts +93 -129
  190. package/src/http/pages.ts +94 -21
  191. package/src/http/script-runs.ts +174 -5
  192. package/src/http/status.ts +10 -2
  193. package/src/http/tasks.ts +4 -2
  194. package/src/pages/browser-sdk.ts +48 -21
  195. package/src/pages/color-literals.ts +115 -0
  196. package/src/pages/page-head.ts +766 -0
  197. package/src/pages/version.ts +3 -0
  198. package/src/providers/amp-adapter.ts +1 -1
  199. package/src/providers/codex-adapter.ts +1 -0
  200. package/src/providers/codex-app-server.ts +27 -1
  201. package/src/providers/fx-adapter.ts +1 -1
  202. package/src/providers/pi-mono-extension.ts +30 -15
  203. package/src/runtime-providers/controller-protocol.ts +14 -1
  204. package/src/runtime-providers/controller.ts +5 -0
  205. package/src/runtime-providers/docker.ts +108 -23
  206. package/src/runtime-providers/types.ts +16 -0
  207. package/src/script-workflows/workflow-ctx.ts +8 -0
  208. package/src/scripts-runtime/types/stdlib.d.ts +22 -0
  209. package/src/scripts-runtime/types/swarm-sdk.d.ts +22 -0
  210. package/src/server-user.ts +1 -0
  211. package/src/slack/commands.ts +5 -1
  212. package/src/slack/handlers.ts +37 -2
  213. package/src/slack/left-threads.ts +27 -4
  214. package/src/tasks/follow-up-gate.ts +318 -0
  215. package/src/tasks/follow-up-redelegation-guard.ts +155 -0
  216. package/src/tasks/worker-follow-up.ts +21 -1
  217. package/src/tests/claude-artifacts-read-script.test.ts +209 -0
  218. package/src/tests/codex-app-server.test.ts +59 -0
  219. package/src/tests/codex-gateway-route.test.ts +50 -1
  220. package/src/tests/create-page-tool.test.ts +501 -0
  221. package/src/tests/db-client-busy-retry.test.ts +81 -0
  222. package/src/tests/db-client.test.ts +21 -0
  223. package/src/tests/db-task-read-contract.test.ts +9 -2
  224. package/src/tests/db-task-write-contract.test.ts +7 -1
  225. package/src/tests/db-tasks-read.test.ts +21 -0
  226. package/src/tests/entrypoint-api-readiness.test.ts +416 -210
  227. package/src/tests/explicit-model-validation.test.ts +14 -8
  228. package/src/tests/fixtures/pages/head-v1.html +56 -0
  229. package/src/tests/follow-up-gate-shadow.test.ts +513 -0
  230. package/src/tests/follow-up-redelivery-guard.test.ts +432 -202
  231. package/src/tests/identity-field-budget.test.ts +1 -1
  232. package/src/tests/internal-ai/credentials.test.ts +62 -0
  233. package/src/tests/internal-ai/task-title.test.ts +283 -0
  234. package/src/tests/list-total-cache.test.ts +27 -2
  235. package/src/tests/memory-fts-populate.test.ts +11 -9
  236. package/src/tests/memory-retrieval-purge.test.ts +80 -0
  237. package/src/tests/merge-gate-detect-changes.test.ts +145 -0
  238. package/src/tests/migration-260-runtime-pool-cache.test.ts +118 -0
  239. package/src/tests/page-proxy.test.ts +1 -1
  240. package/src/tests/page-session-dev-cookie.test.ts +188 -0
  241. package/src/tests/pages-frame-ancestors.test.ts +28 -1
  242. package/src/tests/pages-head-v2.test.ts +534 -0
  243. package/src/tests/pages-http.test.ts +330 -0
  244. package/src/tests/pages-public-html.test.ts +43 -0
  245. package/src/tests/pages-storage.test.ts +182 -0
  246. package/src/tests/pages-versioning.test.ts +72 -0
  247. package/src/tests/profile-sync-stale-writeback.test.ts +334 -0
  248. package/src/tests/profile-sync.test.ts +11 -15
  249. package/src/tests/query-audit-plans.test.ts +27 -0
  250. package/src/tests/runtime-cache-cleanup.test.ts +367 -0
  251. package/src/tests/runtime-e2b-pools.test.ts +41 -0
  252. package/src/tests/runtime-pools-db.test.ts +98 -0
  253. package/src/tests/runtime-pools-http.test.ts +29 -0
  254. package/src/tests/runtime-providers-docker.test.ts +253 -0
  255. package/src/tests/runtime-remote-controller.test.ts +66 -2
  256. package/src/tests/runtime-supervisor.test.ts +66 -2
  257. package/src/tests/scheduled-task-required-params-migration.test.ts +3 -1
  258. package/src/tests/script-connections.test.ts +13 -0
  259. package/src/tests/script-runs-http.test.ts +684 -2
  260. package/src/tests/seed-registry.test.ts +28 -6
  261. package/src/tests/seed-schedules.test.ts +61 -8
  262. package/src/tests/seed-scripts.test.ts +168 -2
  263. package/src/tests/slack-leave-thread.test.ts +145 -5
  264. package/src/tests/slack-render-v2-query-plans.test.ts +24 -2
  265. package/src/tests/status.test.ts +11 -0
  266. package/src/tests/swarm-diff.test.ts +121 -2
  267. package/src/tests/task-auto-title-config-guard.test.ts +37 -0
  268. package/src/tests/task-auto-title-listener.test.ts +165 -0
  269. package/src/tests/tasks-readable-row.test.ts +155 -0
  270. package/src/tests/theme-import.test.ts +758 -0
  271. package/src/tests/themes-page-theme.test.ts +155 -0
  272. package/src/tests/themes-presets.test.ts +172 -0
  273. package/src/tests/workflow-script-principal.test.ts +2 -1
  274. package/src/themes/classics.ts +949 -0
  275. package/src/themes/import.ts +420 -0
  276. package/src/themes/page-fields.ts +40 -0
  277. package/src/themes/page-theme.ts +102 -0
  278. package/src/themes/presets.ts +561 -0
  279. package/src/themes/tokens.ts +218 -0
  280. package/src/tools/app-upsert.ts +1 -1
  281. package/src/tools/create-page.ts +118 -12
  282. package/src/tools/send-task.ts +35 -47
  283. package/src/tools/slack-leave-thread.ts +1 -1
  284. package/src/tools/task-dedup.ts +45 -21
  285. package/src/tools/utils.ts +33 -0
  286. package/src/types.ts +83 -3
  287. package/src/utils/constants.ts +19 -0
  288. package/src/utils/follow-up-gate-schedule-ids.ts +13 -0
  289. package/src/utils/internal-ai/credentials.ts +146 -93
  290. package/src/utils/internal-ai/index.ts +10 -0
  291. package/src/utils/internal-ai/models.ts +18 -0
  292. package/src/utils/internal-ai/task-title.ts +282 -0
  293. package/src/utils/page-session.ts +37 -0
  294. package/templates/schedules/daily-status-report/content.md +88 -2
  295. package/templates/schedules/daily-swarm-update-check/content.md +16 -40
  296. package/templates/skills/apps/SKILL.md +1 -1
  297. package/templates/skills/apps/content.md +1 -1
  298. package/templates/skills/composio-google-docs/content.md +10 -0
  299. package/templates/skills/heartbeat-runbook/content.md +1 -1
  300. package/templates/skills/pages/config.json +1 -1
  301. package/templates/skills/pages/content.md +539 -86
  302. package/templates/skills/pages/files/examples/annotated-pr.html +21 -12
  303. package/templates/skills/pages/files/examples/multiplayer-board.html +41 -41
  304. package/templates/skills/pages/files/examples/report-page.html +125 -124
  305. package/templates/skills/script-workflows/content.md +19 -1
  306. package/templates/skills/swarm-docs/content.md +1 -0
  307. package/templates/skills/swarm-scripts/SKILL.md +2 -0
  308. package/templates/skills/swarm-scripts/content.md +2 -0
  309. package/dist/cli-2pnswgh8.js +0 -189
  310. package/dist/cli-dznrgaep.js +0 -30
  311. package/dist/cli-w3hcrxht.js +0 -6985
  312. package/dist/handlers-vwmba1m2.js +0 -31
  313. package/dist/index-5r71rf98.js +0 -32
  314. package/dist/index-anmwxs87.js +0 -29
  315. package/dist/index-spp0ra5c.js +0 -36
  316. package/dist/lead-fjy418cw.js +0 -69
  317. package/dist/worker-0c5tyzqe.js +0 -69
  318. package/templates/schedules/daily-workflow-health-audit/config.json +0 -19
  319. package/templates/schedules/daily-workflow-health-audit/content.md +0 -191
@@ -2,7 +2,8 @@ import { deleteKv, getKv, upsertKv } from "../be/db";
2
2
  import { isEnvFlagEnabled } from "../utils/env-flag";
3
3
 
4
4
  /**
5
- * Slack threads the swarm left via `slack-leave-thread`.
5
+ * Slack threads the swarm left, via the `slack-leave-thread` tool or a
6
+ * human's `!leave` in the thread.
6
7
  *
7
8
  * A left thread drops every inbound message that does not @mention the bot,
8
9
  * in code, before buffering, routing, or any AI call. A mention clears the
@@ -11,7 +12,7 @@ import { isEnvFlagEnabled } from "../utils/env-flag";
11
12
  */
12
13
  export const SLACK_LEFT_THREAD_KV_NAMESPACE = "slack-left-thread";
13
14
 
14
- /** Operator flag that turns the `slack-leave-thread` tool off. */
15
+ /** Operator flag that turns leaving threads off: the tool and `!leave`. */
15
16
  export const SLACK_LEAVE_THREAD_DISABLE = "SLACK_LEAVE_THREAD_DISABLE";
16
17
 
17
18
  /**
@@ -22,19 +23,41 @@ export function isSlackLeaveThreadDisabled(): boolean {
22
23
  return isEnvFlagEnabled(SLACK_LEAVE_THREAD_DISABLE, false);
23
24
  }
24
25
 
26
+ /** Ephemeral reply to the human who sent `!leave`. */
27
+ export const SLACK_LEFT_THREAD_CONFIRMATION =
28
+ "Left this thread. @mention the swarm to bring it back.";
29
+
30
+ /**
31
+ * True when a thread message is the `!leave` command: exactly `!leave`,
32
+ * optionally after @mentions of the bot. A message that mentions anyone else
33
+ * is not addressed to the swarm and does not match.
34
+ */
35
+ export function isSlackLeaveThreadCommand(
36
+ text: string | undefined,
37
+ botUserId: string | null,
38
+ ): boolean {
39
+ const withoutBotMentions = botUserId
40
+ ? (text ?? "").replaceAll(`<@${botUserId}>`, "")
41
+ : (text ?? "");
42
+ return withoutBotMentions.trim() === "!leave";
43
+ }
44
+
25
45
  function threadKey(channelId: string, threadTs: string): string {
26
46
  return `${channelId}:${threadTs}`;
27
47
  }
28
48
 
49
+ /** Who left the thread: an agent via the tool, or a Slack user via `!leave`. */
50
+ export type SlackThreadLeftBy = { agentId: string } | { slackUserId: string };
51
+
29
52
  export async function leaveSlackThread(
30
53
  channelId: string,
31
54
  threadTs: string,
32
- agentId: string,
55
+ leftBy: SlackThreadLeftBy,
33
56
  ): Promise<void> {
34
57
  await upsertKv({
35
58
  namespace: SLACK_LEFT_THREAD_KV_NAMESPACE,
36
59
  key: threadKey(channelId, threadTs),
37
- value: { agentId, leftAt: new Date().toISOString() },
60
+ value: { ...leftBy, leftAt: new Date().toISOString() },
38
61
  valueType: "json",
39
62
  });
40
63
  }
@@ -0,0 +1,318 @@
1
+ import { createLogEntry, getDbClient } from "../be/db";
2
+ import { getScript } from "../be/scripts/db";
3
+ import { runSavedScriptAsAgent } from "../be/scripts/run-saved";
4
+ import type { AgentTask, ScriptRecord } from "../types";
5
+ import {
6
+ isFollowUpGateScheduleId,
7
+ parseFollowUpGateScheduleIds,
8
+ } from "../utils/follow-up-gate-schedule-ids";
9
+ import { scrubSecrets } from "../utils/secret-scrubber";
10
+
11
+ /**
12
+ * Follow-up gate, shadow mode. When a gated scheduled run's Lead follow-up is
13
+ * created, a configured verifier script checks the run after commit and the
14
+ * server records whether the follow-up could have been skipped. Shadow mode
15
+ * never skips or delays a follow-up.
16
+ */
17
+
18
+ export const FOLLOW_UP_GATE_SHADOW_EVENT = "task_follow_up_gate_shadow" as const;
19
+
20
+ export type FollowUpGateConfig = {
21
+ mode: "off" | "shadow";
22
+ scheduleIds: ReadonlySet<string>;
23
+ verifierScript: string;
24
+ };
25
+
26
+ export type GateIneligibleReason =
27
+ | "mode_off"
28
+ | "not_scheduled"
29
+ | "schedule_not_gated"
30
+ | "status_failed"
31
+ | "follow_up_instructions";
32
+
33
+ /**
34
+ * Read the gate config on every call so a `swarm_config` reload applies. Any
35
+ * mode other than `shadow` reads as `off`, and malformed ids are ignored.
36
+ */
37
+ export function readFollowUpGateConfig(env: NodeJS.ProcessEnv = process.env): FollowUpGateConfig {
38
+ const mode = env.FOLLOW_UP_GATE_MODE?.trim().toLowerCase() === "shadow" ? "shadow" : "off";
39
+ const scheduleIds = new Set(
40
+ parseFollowUpGateScheduleIds(env.FOLLOW_UP_GATE_SCHEDULE_IDS)
41
+ .filter(isFollowUpGateScheduleId)
42
+ .map((id) => id.toLowerCase()),
43
+ );
44
+ return { mode, scheduleIds, verifierScript: env.FOLLOW_UP_GATE_VERIFIER_SCRIPT?.trim() ?? "" };
45
+ }
46
+
47
+ /** Pure decision on whether a follow-up's worker task may be verified. */
48
+ export function classifyFollowUpGate(input: {
49
+ task: Pick<AgentTask, "scheduleId" | "followUpConfig">;
50
+ status: "completed" | "failed";
51
+ config: FollowUpGateConfig;
52
+ }): { eligible: true } | { eligible: false; reason: GateIneligibleReason } {
53
+ const { task, status, config } = input;
54
+ if (config.mode === "off") return { eligible: false, reason: "mode_off" };
55
+ if (!task.scheduleId) return { eligible: false, reason: "not_scheduled" };
56
+ if (!config.scheduleIds.has(task.scheduleId.toLowerCase())) {
57
+ return { eligible: false, reason: "schedule_not_gated" };
58
+ }
59
+ if (status === "failed") return { eligible: false, reason: "status_failed" };
60
+ if (task.followUpConfig?.onCompleted?.trim() || task.followUpConfig?.onFailed?.trim()) {
61
+ return { eligible: false, reason: "follow_up_instructions" };
62
+ }
63
+ return { eligible: true };
64
+ }
65
+
66
+ type VerifierScript = Pick<ScriptRecord, "name" | "version" | "contentHash">;
67
+
68
+ export type FollowUpGateDeps = {
69
+ /** Resolve the global verifier script, or null when it does not exist. */
70
+ getVerifier: (name: string) => Promise<VerifierScript | null>;
71
+ /** Run the verifier as `agentId` and return its result, or throw. */
72
+ runVerifier: (
73
+ script: VerifierScript,
74
+ input: { taskId: string },
75
+ agentId: string,
76
+ signal: AbortSignal,
77
+ ) => Promise<unknown>;
78
+ };
79
+
80
+ const FOLLOW_UP_GATE_TIMEOUT_MS = 60_000;
81
+ const FOLLOW_UP_GATE_MAX_IN_FLIGHT = 2;
82
+ const TEXT_LIMIT = 300;
83
+ const MAX_UNVERIFIED = 20;
84
+
85
+ const defaultDeps: FollowUpGateDeps = {
86
+ getVerifier: (name) => getScript({ name, scope: "global" }),
87
+ runVerifier: async (script, input, agentId, signal) => {
88
+ const output = await runSavedScriptAsAgent({
89
+ script: script as ScriptRecord,
90
+ input,
91
+ agentId,
92
+ timeoutMs,
93
+ signal,
94
+ });
95
+ if (output.exitCode !== 0 || output.error || output.runtimeError) {
96
+ throw new Error(
97
+ output.runtimeError?.message ??
98
+ output.error ??
99
+ `script exited with code ${output.exitCode}`,
100
+ );
101
+ }
102
+ return output.result;
103
+ },
104
+ };
105
+
106
+ let timeoutMs = FOLLOW_UP_GATE_TIMEOUT_MS;
107
+ let depsForTests: FollowUpGateDeps | null = null;
108
+ let inFlight = 0;
109
+ const pending = new Set<Promise<void>>();
110
+
111
+ /** Replace the verifier lookup and runner. `null` restores the defaults. */
112
+ export function setFollowUpGateDepsForTests(deps: FollowUpGateDeps | null): void {
113
+ depsForTests = deps;
114
+ }
115
+
116
+ /** Override the verifier deadline. `null` restores the 60s default. */
117
+ export function setFollowUpGateTimeoutMsForTests(ms: number | null): void {
118
+ timeoutMs = ms ?? FOLLOW_UP_GATE_TIMEOUT_MS;
119
+ }
120
+
121
+ /** Wait until every scheduled shadow hook has written its row. */
122
+ export async function drainFollowUpGateShadowForTest(): Promise<void> {
123
+ // Hooks run in FIFO order behind the client lock, so once this marker runs,
124
+ // every earlier hook has started and registered its promise.
125
+ await new Promise<void>((resolve) => getDbClient().afterCommit(resolve));
126
+ while (pending.size > 0) await Promise.allSettled([...pending]);
127
+ }
128
+
129
+ type ShadowArgs = {
130
+ task: Pick<AgentTask, "id" | "scheduleId" | "followUpConfig">;
131
+ status: "completed" | "failed";
132
+ followUpTaskId: string;
133
+ leadAgentId: string;
134
+ };
135
+
136
+ type ShadowRecord = {
137
+ decision: "would_skip" | "would_keep" | "error" | "ineligible";
138
+ reason: string | null;
139
+ verdict?: { ok: boolean | null; check: string | null; detail: string; controlsOk: boolean };
140
+ unverified?: string[];
141
+ verifier?: VerifierScript | null;
142
+ latencyMs?: number | null;
143
+ error?: string;
144
+ };
145
+
146
+ /**
147
+ * Record a shadow gate decision for a just-created follow-up. Returns
148
+ * synchronously: all work, including the row write, runs after commit. With
149
+ * the gate off or the schedule not gated, nothing is registered.
150
+ */
151
+ export function scheduleFollowUpGateShadow(args: ShadowArgs): void {
152
+ const config = readFollowUpGateConfig();
153
+ const verdict = classifyFollowUpGate({ task: args.task, status: args.status, config });
154
+ if (
155
+ !verdict.eligible &&
156
+ (verdict.reason === "mode_off" ||
157
+ verdict.reason === "not_scheduled" ||
158
+ verdict.reason === "schedule_not_gated")
159
+ ) {
160
+ return;
161
+ }
162
+ const deps = depsForTests ?? defaultDeps;
163
+ getDbClient().afterCommit(() => {
164
+ const run = verdict.eligible
165
+ ? runShadow(args, config, deps)
166
+ : writeRecord(args, { decision: "ineligible", reason: verdict.reason });
167
+ pending.add(run);
168
+ const settle = () => {
169
+ pending.delete(run);
170
+ };
171
+ void run.then(settle, settle);
172
+ });
173
+ }
174
+
175
+ async function runShadow(
176
+ args: ShadowArgs,
177
+ config: FollowUpGateConfig,
178
+ deps: FollowUpGateDeps,
179
+ ): Promise<void> {
180
+ if (inFlight >= FOLLOW_UP_GATE_MAX_IN_FLIGHT) {
181
+ await writeRecord(args, { decision: "error", reason: "concurrency_cap" });
182
+ return;
183
+ }
184
+ inFlight++;
185
+ try {
186
+ await writeRecord(args, await verify(args, config, deps));
187
+ } finally {
188
+ inFlight--;
189
+ }
190
+ }
191
+
192
+ async function verify(
193
+ args: ShadowArgs,
194
+ config: FollowUpGateConfig,
195
+ deps: FollowUpGateDeps,
196
+ ): Promise<ShadowRecord> {
197
+ if (!config.verifierScript) return { decision: "error", reason: "no_verifier" };
198
+ const started = performance.now();
199
+ const latency = () => Math.round(performance.now() - started);
200
+ const controller = new AbortController();
201
+ const deadlineMs = timeoutMs;
202
+ let timer: ReturnType<typeof setTimeout> | undefined;
203
+ let script: VerifierScript | null = null;
204
+ let timedOut = false;
205
+ try {
206
+ const run = (async () => {
207
+ script = await deps.getVerifier(config.verifierScript);
208
+ if (!script) return null;
209
+ return {
210
+ result: await deps.runVerifier(
211
+ script,
212
+ { taskId: args.task.id },
213
+ args.leadAgentId,
214
+ controller.signal,
215
+ ),
216
+ };
217
+ })();
218
+ // A run that settles after the deadline must not surface as an unhandled rejection.
219
+ run.catch(() => {});
220
+ const deadline = new Promise<never>((_, reject) => {
221
+ timer = setTimeout(() => {
222
+ timedOut = true;
223
+ controller.abort();
224
+ reject(new Error(`verifier timed out after ${deadlineMs}ms`));
225
+ }, deadlineMs);
226
+ });
227
+ const outcome = await Promise.race([run, deadline]);
228
+ const verifier = verifierInfo(script);
229
+ if (!outcome) {
230
+ return { decision: "error", reason: "no_verifier", verifier, latencyMs: latency() };
231
+ }
232
+ const result = outcome.result;
233
+ if (!result || typeof result !== "object") {
234
+ return {
235
+ decision: "error",
236
+ reason: "verifier_error",
237
+ verifier,
238
+ latencyMs: latency(),
239
+ error: "verifier result is not an object",
240
+ };
241
+ }
242
+ return { ...mapVerdict(result as Record<string, unknown>), verifier, latencyMs: latency() };
243
+ } catch (err) {
244
+ const message = err instanceof Error ? err.message : String(err);
245
+ return {
246
+ decision: "error",
247
+ reason: timedOut ? "timeout" : "verifier_error",
248
+ verifier: verifierInfo(script),
249
+ latencyMs: latency(),
250
+ error: clip(message),
251
+ };
252
+ } finally {
253
+ clearTimeout(timer);
254
+ }
255
+ }
256
+
257
+ function verifierInfo(script: VerifierScript | null): VerifierScript | null {
258
+ return script
259
+ ? { name: script.name, version: script.version, contentHash: script.contentHash }
260
+ : null;
261
+ }
262
+
263
+ /** `ok:true` with passing controls is `would_skip`; anything else keeps the follow-up. */
264
+ function mapVerdict(result: Record<string, unknown>): ShadowRecord {
265
+ const ok = typeof result.ok === "boolean" ? result.ok : null;
266
+ const controls = result.controls as { ok?: unknown } | null | undefined;
267
+ const controlsOk = controls?.ok === true;
268
+ const unverified = Array.isArray(result.unverified)
269
+ ? result.unverified
270
+ .filter((name): name is string => typeof name === "string")
271
+ .slice(0, MAX_UNVERIFIED)
272
+ .map((name) => name.slice(0, 100))
273
+ : [];
274
+ return {
275
+ decision: ok === true && controlsOk ? "would_skip" : "would_keep",
276
+ reason: null,
277
+ verdict: {
278
+ ok,
279
+ check: typeof result.check === "string" ? result.check.slice(0, 100) : null,
280
+ detail: typeof result.detail === "string" ? clip(result.detail) : "",
281
+ controlsOk,
282
+ },
283
+ unverified,
284
+ };
285
+ }
286
+
287
+ function clip(text: string): string {
288
+ return scrubSecrets(text).slice(0, TEXT_LIMIT);
289
+ }
290
+
291
+ async function writeRecord(args: ShadowArgs, record: ShadowRecord): Promise<void> {
292
+ try {
293
+ await createLogEntry({
294
+ eventType: FOLLOW_UP_GATE_SHADOW_EVENT,
295
+ taskId: args.task.id,
296
+ agentId: args.leadAgentId,
297
+ metadata: {
298
+ v: 1,
299
+ mode: "shadow",
300
+ followUpTaskId: args.followUpTaskId,
301
+ scheduleId: args.task.scheduleId ?? null,
302
+ status: args.status,
303
+ decision: record.decision,
304
+ reason: record.reason,
305
+ verdict: record.verdict ?? null,
306
+ unverified: record.unverified ?? [],
307
+ verifier: record.verifier ?? null,
308
+ latencyMs: record.latencyMs ?? null,
309
+ ...(record.error ? { error: record.error } : {}),
310
+ },
311
+ });
312
+ } catch (err) {
313
+ console.warn(
314
+ `[follow-up-gate] Could not record shadow decision for task ${args.task.id.slice(0, 8)}:`,
315
+ scrubSecrets(err instanceof Error ? err.message : String(err)),
316
+ );
317
+ }
318
+ }
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Follow-up re-delegation guard, shared by every agent dispatch path that can
3
+ * run inside a follow-up session: MCP `send-task` and durable-run
4
+ * `ctx.step.agentTask` steps (`src/http/script-runs.ts`).
5
+ *
6
+ * When the dispatching session's source task is a `follow-up` in a Slack
7
+ * thread, two rules stop the cycle
8
+ * worker completes → follow-up → Lead re-delegates → repeat,
9
+ * while a distinct next step (step 2 of a chain, a fix round, a retry) passes:
10
+ *
11
+ * 1. same-work: the new task matches a completed task in the thread under the
12
+ * dedup similarity rules. Bypassed when a task in the thread was cancelled
13
+ * or failed after that completion: the work was interrupted or went wrong,
14
+ * so re-dispatch is the correct response, not a deduped no-op.
15
+ * 2. loop-cap: the caller already made FOLLOW_UP_DISPATCH_CAP dispatches into
16
+ * the thread within FOLLOW_UP_DISPATCH_WINDOW_MINUTES. A durable script run
17
+ * counts once however many steps it dispatched. Independent of similarity,
18
+ * so a loop that rewords each re-delegation still stops.
19
+ *
20
+ * Callers bypass both with `allowRedelegation` and log the override. Each
21
+ * caller evaluates the rules inside the write transaction that inserts the
22
+ * task, so two concurrent dispatches into one thread (send-task or a run's
23
+ * first step, in any mix) cannot both read the count below the cap.
24
+ */
25
+ import {
26
+ countAgentDispatchesInThread,
27
+ findCompletedTasksInThread,
28
+ findRecentCancelledTaskInThread,
29
+ findRecentFailedTaskInThread,
30
+ getTaskById,
31
+ } from "../be/db";
32
+ import { type DedupMatch, matchSimilarTask } from "../tools/task-dedup";
33
+
34
+ /** How far back the same-work rule looks for a completed task in the thread. */
35
+ export const FOLLOW_UP_SAME_WORK_WINDOW_MINUTES = 2880; // 48 hours
36
+ /**
37
+ * Loop cap: dispatches one agent may make into one Slack thread per window
38
+ * before a follow-up session needs allowRedelegation. A long legitimate chain
39
+ * (plan, implement, review, fix, re-review, fix, CI fix, retry) is about 8
40
+ * dispatches; a runaway loop makes one per worker run (minutes), so it hits
41
+ * 10 within a few hours and burns at most 10 worker sessions.
42
+ */
43
+ export const FOLLOW_UP_DISPATCH_CAP = 10;
44
+ export const FOLLOW_UP_DISPATCH_WINDOW_MINUTES = 360; // 6 hours
45
+
46
+ /** Rows per page when the same-work rule scans a thread's completions. */
47
+ export const FOLLOW_UP_SAME_WORK_PAGE_SIZE = 200;
48
+
49
+ export type FollowUpThread = { channelId: string; threadTs: string; userId?: string };
50
+
51
+ export type FollowUpRedelegationBlock = {
52
+ rule: "same-work" | "loop-cap";
53
+ message: string;
54
+ };
55
+
56
+ /**
57
+ * The Slack thread the guard applies to, or null when the source task is not
58
+ * a `follow-up` in a Slack thread. A `resume` source is intentionally exempt:
59
+ * a resume IS the legitimate re-dispatch.
60
+ */
61
+ export async function followUpThreadForSourceTask(
62
+ sourceTaskId: string | null | undefined,
63
+ ): Promise<FollowUpThread | null> {
64
+ if (!sourceTaskId) return null;
65
+ const sourceTask = await getTaskById(sourceTaskId);
66
+ if (
67
+ sourceTask?.taskType !== "follow-up" ||
68
+ !sourceTask.slackChannelId ||
69
+ !sourceTask.slackThreadTs
70
+ ) {
71
+ return null;
72
+ }
73
+ return {
74
+ channelId: sourceTask.slackChannelId,
75
+ threadTs: sourceTask.slackThreadTs,
76
+ userId: sourceTask.slackUserId,
77
+ };
78
+ }
79
+
80
+ /**
81
+ * The most recent completed task in the thread's same-work window whose
82
+ * description matches this dispatch. Pages through every completion in the
83
+ * window (an older match can sit behind any number of newer unrelated ones),
84
+ * one bounded query per page, and stops at the first match.
85
+ */
86
+ async function findMatchingCompletedWork(args: {
87
+ channelId: string;
88
+ threadTs: string;
89
+ taskDescription: string;
90
+ targetAgentId?: string;
91
+ }): Promise<DedupMatch | null> {
92
+ let before: { lastUpdatedAt: string; id: string } | undefined;
93
+ for (;;) {
94
+ const page = await findCompletedTasksInThread(
95
+ args.channelId,
96
+ args.threadTs,
97
+ FOLLOW_UP_SAME_WORK_WINDOW_MINUTES,
98
+ FOLLOW_UP_SAME_WORK_PAGE_SIZE,
99
+ before,
100
+ );
101
+ for (const existing of page) {
102
+ const match = matchSimilarTask(existing, args);
103
+ if (match) return match;
104
+ }
105
+ const last = page.at(-1);
106
+ if (!last || page.length < FOLLOW_UP_SAME_WORK_PAGE_SIZE) return null;
107
+ before = { lastUpdatedAt: last.lastUpdatedAt, id: last.id };
108
+ }
109
+ }
110
+
111
+ /**
112
+ * The follow-up re-delegation rules. Returns the rule that blocks this
113
+ * dispatch, or null when it may proceed.
114
+ */
115
+ export async function evaluateFollowUpRedelegation(args: {
116
+ channelId: string;
117
+ threadTs: string;
118
+ creatorAgentId: string;
119
+ taskDescription: string;
120
+ targetAgentId?: string;
121
+ }): Promise<FollowUpRedelegationBlock | null> {
122
+ const { channelId, threadTs } = args;
123
+ const match = await findMatchingCompletedWork(args);
124
+ if (match) {
125
+ const existing = match.task;
126
+ const [cancelled, failed] = await Promise.all([
127
+ findRecentCancelledTaskInThread(channelId, threadTs, FOLLOW_UP_SAME_WORK_WINDOW_MINUTES),
128
+ findRecentFailedTaskInThread(channelId, threadTs, FOLLOW_UP_SAME_WORK_WINDOW_MINUTES),
129
+ ]);
130
+ const completedAt = new Date(existing.lastUpdatedAt).getTime();
131
+ const interruptedLater = [cancelled, failed].some(
132
+ (t) => t && new Date(t.lastUpdatedAt).getTime() > completedAt,
133
+ );
134
+ if (!interruptedLater) {
135
+ return {
136
+ rule: "same-work",
137
+ message: `Blocked by the follow-up re-delegation guard (same-work rule): this task matches completed task ${existing.id.slice(0, 8)} in this Slack thread (${match.reason}), so the work was already done. If it must run again, pass allowRedelegation: true.`,
138
+ };
139
+ }
140
+ }
141
+
142
+ const dispatched = await countAgentDispatchesInThread(
143
+ channelId,
144
+ threadTs,
145
+ args.creatorAgentId,
146
+ FOLLOW_UP_DISPATCH_WINDOW_MINUTES,
147
+ );
148
+ if (dispatched >= FOLLOW_UP_DISPATCH_CAP) {
149
+ return {
150
+ rule: "loop-cap",
151
+ message: `Blocked by the follow-up re-delegation guard (loop-cap rule): you already dispatched ${dispatched} times into this Slack thread in the last ${FOLLOW_UP_DISPATCH_WINDOW_MINUTES / 60} hours (cap ${FOLLOW_UP_DISPATCH_CAP}; a durable script run counts once). If this is a deliberate next step and not a re-delegation loop, pass allowRedelegation: true.`,
152
+ };
153
+ }
154
+ return null;
155
+ }
@@ -34,6 +34,7 @@ import {
34
34
  import { isEnvFlagEnabled } from "../utils/env-flag";
35
35
  import { taskAttachmentDisplayUrl } from "../utils/task-attachment-links";
36
36
  import { chatThreadOfContextKey } from "./context-key";
37
+ import { scheduleFollowUpGateShadow } from "./follow-up-gate";
37
38
  import { taskModelPin } from "./model-pin";
38
39
  import { createTaskWithSiblingAwareness } from "./sibling-awareness";
39
40
  // Side-effect import: registers task lifecycle templates in the in-memory registry.
@@ -261,6 +262,9 @@ async function findDeferredWakeFor(taskId: string, agentId: string): Promise<Def
261
262
  * `all` wait with siblings still pending wakes nobody, so the follow-up stays.
262
263
  * A `followUpConfig.onCompleted` / `onFailed` instruction for this status also
263
264
  * keeps the follow-up, because the wake-up task does not carry that text.
265
+ *
266
+ * With `FOLLOW_UP_GATE_MODE=shadow`, a gated scheduled run also records a
267
+ * verifier's would-skip decision after commit; the follow-up is never skipped.
264
268
  */
265
269
  export async function createWorkerTaskFollowUp(args: {
266
270
  task: AgentTask;
@@ -392,7 +396,7 @@ export async function createWorkerTaskFollowUp(args: {
392
396
  );
393
397
  return null;
394
398
  }
395
- return await createTaskWithSiblingAwareness(
399
+ const followUp = await createTaskWithSiblingAwareness(
396
400
  changes.description ?? followUpDescription,
397
401
  {
398
402
  agentId: followUpAgentId,
@@ -413,6 +417,22 @@ export async function createWorkerTaskFollowUp(args: {
413
417
  },
414
418
  { origin: "followUp" },
415
419
  );
420
+ if (followUp) {
421
+ try {
422
+ scheduleFollowUpGateShadow({
423
+ task,
424
+ status,
425
+ followUpTaskId: followUp.id,
426
+ leadAgentId: leadAgent.id,
427
+ });
428
+ } catch (err) {
429
+ console.warn(
430
+ `[worker-follow-up] Follow-up gate shadow failed for task ${task.id.slice(0, 8)}:`,
431
+ err instanceof Error ? err.message : String(err),
432
+ );
433
+ }
434
+ }
435
+ return followUp;
416
436
  }
417
437
 
418
438
  /** Hop cap for `deferredWorkChatContextKey`; a defer/resume chain is short. */