@desplega.ai/agent-swarm 1.158.0 → 1.159.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 (272) hide show
  1. package/dist/{acp-adapter-xaq4n6pc.js → acp-adapter-qhch0n2v.js} +1 -1
  2. package/dist/{actions-p9hz5yat.js → actions-dtsh1mfb.js} +33 -29
  3. package/dist/{anthropic-messages-51pz26qe.js → anthropic-messages-f2h06k64.js} +8 -3
  4. package/dist/{app-g26wdcz6.js → app-y1b9srva.js} +10 -9
  5. package/dist/{assistant-wydfqkb3.js → assistant-31hwy5xj.js} +34 -30
  6. package/dist/{azure-openai-responses-8gksqtmz.js → azure-openai-responses-0k1mfax5.js} +12 -13
  7. package/dist/{boot-reembed-se8zmdcp.js → boot-reembed-48d2qpkt.js} +11 -11
  8. package/dist/{boot-reembed-g5ckg9yb.js → boot-reembed-w0bryhkr.js} +12 -12
  9. package/dist/{boot-scrub-logs-wfxqan61.js → boot-scrub-logs-t65wbrn7.js} +9 -8
  10. package/dist/{boot-seeds-s3q01xj3.js → boot-seeds-h7c32hvm.js} +12 -11
  11. package/dist/{claude-adapter-tv2x4f36.js → claude-adapter-1t6611ne.js} +26 -21
  12. package/dist/{claude-managed-adapter-41b2s7fd.js → claude-managed-adapter-werp659j.js} +4 -2
  13. package/dist/{claude-managed-setup-60g8aazx.js → claude-managed-setup-y6rdgbc1.js} +11 -2
  14. package/dist/claude-sdk-session-zhkyk8yw.js +29319 -0
  15. package/dist/{cli-vj6h9kc2.js → cli-0m7wct0d.js} +59 -1
  16. package/dist/{cli-wscdvcqz.js → cli-0n2e4x49.js} +1 -1
  17. package/dist/cli-0xqde53b.js +176 -0
  18. package/dist/{cli-xberxa1r.js → cli-10ym38qc.js} +2 -2
  19. package/dist/{cli-5b6b62tz.js → cli-13hw4z91.js} +1 -1
  20. package/dist/{cli-rqz8qbw7.js → cli-20035trh.js} +50 -18
  21. package/dist/{cli-k0g1sjzm.js → cli-31nkw3g3.js} +3 -3
  22. package/dist/cli-3gvrr8vj.js +1422 -0
  23. package/dist/{cli-c07r9a2m.js → cli-48527fpw.js} +9 -8
  24. package/dist/{cli-24k806sc.js → cli-4aj25szq.js} +2 -2
  25. package/dist/{cli-7gtvvpbh.js → cli-4tvgz99m.js} +7 -7
  26. package/dist/cli-503ya656.js +455 -0
  27. package/dist/{cli-cw9zck8j.js → cli-5f2hq128.js} +8 -4
  28. package/dist/{cli-s8eet254.js → cli-68bmme5p.js} +11078 -27833
  29. package/dist/{cli-ftfvggzx.js → cli-79aq9cgb.js} +794 -563
  30. package/dist/cli-7cxw8b60.js +14910 -0
  31. package/dist/cli-7kyk9jr3.js +22 -0
  32. package/dist/cli-7zttzetj.js +324 -0
  33. package/dist/{cli-y95wp3tg.js → cli-96qd5psa.js} +1 -1
  34. package/dist/{cli-g8djp6qe.js → cli-9yg65c1j.js} +22 -0
  35. package/dist/{cli-3gmcwfyx.js → cli-ardpzhjm.js} +5 -5
  36. package/dist/{cli-7s3syk22.js → cli-b6thtzac.js} +5 -5
  37. package/dist/{cli-00w2c66p.js → cli-bgv70kym.js} +13 -2
  38. package/dist/{cli-j55n64b6.js → cli-bv0a38v9.js} +163 -44
  39. package/dist/cli-ca2w7zv7.js +115 -0
  40. package/dist/cli-cpvsaqsw.js +333 -0
  41. package/dist/cli-cygyqj3b.js +754 -0
  42. package/dist/{cli-x24pshtq.js → cli-dvjpkkjd.js} +6 -6
  43. package/dist/{cli-5d10vd2m.js → cli-e0ndxhtk.js} +4 -4
  44. package/dist/{cli-4198qc9v.js → cli-e4xnc0qp.js} +15 -3
  45. package/dist/{cli-rjehvrts.js → cli-eg7cdg68.js} +8 -8
  46. package/dist/cli-ejpygnqz.js +3815 -0
  47. package/dist/{cli-fgqxwrpv.js → cli-fjtp3463.js} +1631 -127
  48. package/dist/{cli-zt1227k1.js → cli-fkqhbg8g.js} +5 -3
  49. package/dist/{cli-47m6dkd3.js → cli-g9df2m37.js} +409 -168
  50. package/dist/cli-gzyh0j0g.js +8455 -0
  51. package/dist/cli-hgqfttfc.js +183 -0
  52. package/dist/cli-hj65wsts.js +17755 -0
  53. package/dist/cli-j9z8mfx3.js +476 -0
  54. package/dist/{cli-aehm1rbq.js → cli-kc3dp84d.js} +1135 -61
  55. package/dist/{cli-h38x0qhg.js → cli-kdnehe0t.js} +4 -3
  56. package/dist/{cli-20cj37vt.js → cli-m4excttz.js} +136 -2216
  57. package/dist/{cli-8qmme6vd.js → cli-mg24hkt4.js} +2 -2
  58. package/dist/{cli-xhaeg2nc.js → cli-mzkqpxee.js} +1 -1
  59. package/dist/{cli-q67y4wzm.js → cli-phsaj4xq.js} +24 -2
  60. package/dist/{cli-pw9rj5t9.js → cli-pqetb83f.js} +1 -176
  61. package/dist/{cli-rj19hc34.js → cli-qjxy52w7.js} +1 -1
  62. package/dist/{cli-53sz8gkz.js → cli-qv4jv8bx.js} +1 -1
  63. package/dist/cli-qvnnx4ct.js +1051 -0
  64. package/dist/cli-rb9m6rpe.js +4980 -0
  65. package/dist/{cli-tt9k8r35.js → cli-rfezr056.js} +476 -909
  66. package/dist/{cli-ggm92wcn.js → cli-rgjprct3.js} +1 -0
  67. package/dist/{cli-9httdpyq.js → cli-rrqmqr1s.js} +1 -1
  68. package/dist/cli-rykfmgwg.js +102 -0
  69. package/dist/{cli-eetrvv91.js → cli-t3p77qk1.js} +1 -1
  70. package/dist/{cli-68w09sjw.js → cli-vn0qv9cj.js} +25 -88
  71. package/dist/{cli-987hg0aa.js → cli-vwbbaecp.js} +2 -2
  72. package/dist/cli-wd9hz40d.js +69 -0
  73. package/dist/{cli-j57qkrpq.js → cli-wgjx0q33.js} +13 -13
  74. package/dist/cli-xgpf00sk.js +2149 -0
  75. package/dist/{cli-mcnhgnn1.js → cli-ykk5jaax.js} +184 -26
  76. package/dist/{cli-aky8tctm.js → cli-z9x0sh1t.js} +3 -3
  77. package/dist/{cli-1hkrch3k.js → cli-zw65mab6.js} +5 -5
  78. package/dist/cli.js +38 -29
  79. package/dist/cloudflare-workers-ai-system-one-1sg586qv.js +46 -0
  80. package/dist/{codex-adapter-cnm8wk11.js → codex-adapter-jqfqqa21.js} +21 -14
  81. package/dist/{codex-hook-9hptmdhj.js → codex-hook-5eys3dya.js} +2 -2
  82. package/dist/{codex-session-runner-gfg547zj.js → codex-session-runner-zd341dsm.js} +21 -14
  83. package/dist/{commands-e3bn0p75.js → commands-vfsysz4s.js} +9 -8
  84. package/dist/{db-03v9kzr2.js → db-s3znhvbn.js} +15 -10
  85. package/dist/{dispatcher-nrmg8yht.js → dispatcher-5xn0c7rf.js} +32 -28
  86. package/dist/{dsh-adapter-k03nfqa6.js → dsh-adapter-894mgfvs.js} +3 -3
  87. package/dist/{e2b-93x1k0j5.js → e2b-ez07znam.js} +4 -3
  88. package/dist/execute-71ysgv35.js +965 -0
  89. package/dist/{google-generative-ai-bxmv50nc.js → google-generative-ai-1nfcdphp.js} +5 -4
  90. package/dist/{google-vertex-809af514.js → google-vertex-1tqcd7b1.js} +5 -4
  91. package/dist/{handlers-aqcedbyb.js → handlers-5w4jwycr.js} +35 -31
  92. package/dist/{hook-7vkskkre.js → hook-853bxrr6.js} +16 -13
  93. package/dist/{hook-ybvmrmhq.js → hook-z0hbxj85.js} +16 -13
  94. package/dist/{http-2eqa8t24.js → http-akc3wy3m.js} +600 -123
  95. package/dist/{index-f6cryxwe.js → index-4a5njnm1.js} +2 -59
  96. package/dist/{index-a3vn96n8.js → index-5sz16ksh.js} +187 -47
  97. package/dist/{index-c6zsv1bd.js → index-bjmjh9h4.js} +11 -11
  98. package/dist/{index-chgdkq36.js → index-cd151qkq.js} +32 -28
  99. package/dist/{index-x71bvq0p.js → index-k661dzzr.js} +33 -29
  100. package/dist/{index-1yks3qcr.js → index-ttpbhnjg.js} +32 -28
  101. package/dist/{keepalive-vtkf5n99.js → keepalive-8p9rpvz5.js} +11 -10
  102. package/dist/{lead-h3sp357x.js → lead-jp69z5h3.js} +49 -45
  103. package/dist/llama-cpp-classify-t2tccams.js +335 -0
  104. package/dist/{maintenance-jzeqtk3q.js → maintenance-fwx4ev0k.js} +12 -12
  105. package/dist/{mistral-conversations-zx0kn554.js → mistral-conversations-bhvwqcnk.js} +14 -17
  106. package/dist/{oauth-refresh-sweep-qp3dzmdp.js → oauth-refresh-sweep-cwcq1ctx.js} +10 -9
  107. package/dist/{onboard-tmsp0qbz.js → onboard-2b4q0t84.js} +6 -5
  108. package/dist/{openai-codex-responses-4vjqzvjp.js → openai-codex-responses-nekthgks.js} +21 -8
  109. package/dist/{openai-completions-9hjrqbw8.js → openai-completions-hxtzwj0s.js} +8 -11
  110. package/dist/{openai-responses-7b1pn0xt.js → openai-responses-w4r118w7.js} +22 -17
  111. package/dist/{opencode-adapter-x6x2da1z.js → opencode-adapter-zz7xvjj6.js} +6 -4
  112. package/dist/{openrouter-images-9f9ek4rw.js → openrouter-images-b6sqqjgn.js} +4 -6
  113. package/dist/{otel-impl-9mz73q65.js → otel-impl-g8rs4vwr.js} +1 -1
  114. package/dist/{pi-messages-txpjj39q.js → pi-messages-vxdt8v7j.js} +2 -1
  115. package/dist/{pi-mono-adapter-m6dvv0dz.js → pi-mono-adapter-n0me28wt.js} +29 -17
  116. package/dist/pricing-refresh-0v77385v.js +39 -0
  117. package/dist/{rbac-roles-c9wqz83x.js → rbac-roles-krjyazbj.js} +10 -9
  118. package/dist/{render-v2-gryrh9h8.js → render-v2-3wjsp9aw.js} +10 -9
  119. package/dist/runtime-ktjysacx.js +24 -0
  120. package/dist/{setup-q9720bt6.js → setup-yeqryke6.js} +6 -5
  121. package/dist/typesafe-system-one-96960jxp.js +27 -0
  122. package/dist/{virtual-modules-3htpgn8x.js → virtual-modules-dzdeq1sf.js} +21 -13
  123. package/dist/{work-objects-b76x7deb.js → work-objects-3pakpkw0.js} +35 -31
  124. package/dist/{worker-6vrxbv41.js → worker-85testgj.js} +49 -45
  125. package/openapi.json +863 -14
  126. package/package.json +9 -8
  127. package/src/be/db/agents.ts +20 -0
  128. package/src/be/db/tasks/human-free.ts +442 -0
  129. package/src/be/db/tasks/read.ts +56 -1
  130. package/src/be/db/tasks/write.ts +107 -11
  131. package/src/be/db.ts +89 -165
  132. package/src/be/harness-model-support.ts +182 -0
  133. package/src/be/human-free-drain.ts +67 -0
  134. package/src/be/migrations/182_task_human_free_flag.sql +63 -0
  135. package/src/be/migrations/183_human_free_reclassify_queue.sql +25 -0
  136. package/src/be/migrations/184_model_catalog.sql +120 -0
  137. package/src/be/model-catalog-store.ts +644 -0
  138. package/src/be/model-tier-keys.ts +31 -0
  139. package/src/be/model-tier-resolution.ts +489 -0
  140. package/src/be/model-validation.ts +112 -0
  141. package/src/be/models-catalog.ts +61 -7
  142. package/src/be/modelsdev-cache.json +82 -0
  143. package/src/be/modelsdev-cache.ts +2 -0
  144. package/src/be/pricing-refresh.ts +154 -12
  145. package/src/be/scripts/typecheck.ts +15 -0
  146. package/src/be/seed-scripts/catalog/task-failure-audit.ts +61 -9
  147. package/src/be/swarm-config-guard.ts +6 -0
  148. package/src/cli.tsx +10 -0
  149. package/src/commands/claude-managed-setup.ts +3 -1
  150. package/src/commands/runner.ts +76 -2
  151. package/src/extensions/contract-types.generated.ts +1 -1
  152. package/src/heartbeat/heartbeat.ts +64 -15
  153. package/src/heartbeat/templates.ts +2 -2
  154. package/src/http/agents.ts +49 -1
  155. package/src/http/index.ts +8 -0
  156. package/src/http/models-catalog.ts +357 -8
  157. package/src/http/poll.ts +45 -2
  158. package/src/http/schedules.ts +36 -6
  159. package/src/http/session-data.ts +2 -5
  160. package/src/http/stats.ts +6 -8
  161. package/src/http/tasks.ts +26 -1
  162. package/src/http/usage-cache.ts +56 -15
  163. package/src/http/workflows.ts +35 -8
  164. package/src/prompts/session-templates.ts +1 -0
  165. package/src/providers/claude-managed-models.ts +47 -161
  166. package/src/providers/codex-adapter.ts +4 -4
  167. package/src/providers/codex-models.ts +65 -198
  168. package/src/providers/reasoning-effort.ts +44 -140
  169. package/src/rbac/legacy-policy.ts +25 -0
  170. package/src/rbac/permissions.ts +10 -0
  171. package/src/scripts-runtime/sdk-allowlist.ts +4 -0
  172. package/src/scripts-runtime/types/stdlib.d.ts +15 -0
  173. package/src/scripts-runtime/types/swarm-extension.d.ts +3 -0
  174. package/src/scripts-runtime/types/swarm-sdk.d.ts +15 -0
  175. package/src/server.ts +6 -0
  176. package/src/slack/message-text.ts +56 -2
  177. package/src/slack/thread-buffer.ts +10 -2
  178. package/src/tests/agents-list-model-display.test.ts +4 -6
  179. package/src/tests/base-prompt.test.ts +3 -0
  180. package/src/tests/claude-adapter.test.ts +10 -0
  181. package/src/tests/claude-managed-adapter.test.ts +12 -18
  182. package/src/tests/codex-adapter.test.ts +2 -1
  183. package/src/tests/codex-gpt6-release.test.ts +66 -5
  184. package/src/tests/context-window.test.ts +7 -5
  185. package/src/tests/db-extraction-contract.test.ts +1 -0
  186. package/src/tests/db-task-read-contract.test.ts +5 -1
  187. package/src/tests/db-task-write-contract.test.ts +4 -1
  188. package/src/tests/docs-index-footprint.test.js +3 -3
  189. package/src/tests/explicit-model-validation.test.ts +332 -0
  190. package/src/tests/harness-model-support.test.ts +374 -0
  191. package/src/tests/heartbeat-checklist-config.test.ts +71 -0
  192. package/src/tests/heartbeat-checklist.test.ts +4 -2
  193. package/src/tests/heartbeat-orphan-resume.test.ts +48 -0
  194. package/src/tests/human-free-flag.test.ts +279 -0
  195. package/src/tests/human-free-oracle.ts +57 -0
  196. package/src/tests/human-free-reclassify.test.ts +669 -0
  197. package/src/tests/migration-046-budgets.test.ts +3 -3
  198. package/src/tests/model-catalog-migration.test.ts +134 -0
  199. package/src/tests/model-catalog-table.test.ts +249 -0
  200. package/src/tests/model-catalog-write-access.test.ts +387 -0
  201. package/src/tests/model-tier-resolution.test.ts +519 -0
  202. package/src/tests/models-catalog.test.ts +1 -1
  203. package/src/tests/multi-runtime-enabled-stats.test.ts +65 -1
  204. package/src/tests/package-publish.test.ts +137 -36
  205. package/src/tests/pricing-routes.test.ts +2 -2
  206. package/src/tests/rbac-engine.test.ts +42 -2
  207. package/src/tests/reasoning-effort.test.ts +109 -0
  208. package/src/tests/refresh-modelsdev-carryforward.test.ts +5 -5
  209. package/src/tests/runtime-model-catalog.test.ts +86 -0
  210. package/src/tests/script-connections.test.ts +5 -0
  211. package/src/tests/seed-scripts.test.ts +89 -0
  212. package/src/tests/session-costs.test.ts +14 -11
  213. package/src/tests/slack-message-text.test.ts +101 -0
  214. package/src/tests/slack-thread-buffer-context.test.ts +93 -0
  215. package/src/tests/task-cascade-fail.test.ts +148 -2
  216. package/src/tests/tool-annotations.test.ts +1 -1
  217. package/src/tests/usage-cache.test.ts +166 -0
  218. package/src/tests/workflow-executors.test.ts +5 -2
  219. package/src/tests/workflow-system-one-decision-review.test.ts +1021 -0
  220. package/src/tests/workflow-system-one-decision-save-warnings.test.ts +365 -0
  221. package/src/tests/workflow-system-one-decision.test.ts +2063 -0
  222. package/src/tools/model-catalog/index.ts +137 -0
  223. package/src/tools/schedules/create-schedule.ts +16 -1
  224. package/src/tools/schedules/patch-schedule.ts +17 -0
  225. package/src/tools/schedules/update-schedule.ts +17 -0
  226. package/src/tools/send-task.ts +14 -0
  227. package/src/tools/task-action.ts +12 -0
  228. package/src/tools/tool-config.ts +3 -1
  229. package/src/tools/workflows/create-workflow.ts +9 -2
  230. package/src/tools/workflows/patch-workflow-node.ts +17 -7
  231. package/src/tools/workflows/patch-workflow.ts +9 -2
  232. package/src/tools/workflows/update-workflow.ts +9 -2
  233. package/src/types.ts +30 -1
  234. package/src/utils/context-window.ts +47 -53
  235. package/src/utils/harness-cli-version.ts +115 -0
  236. package/src/utils/harness-model-error.ts +33 -0
  237. package/src/utils/runtime-model-catalog.ts +137 -0
  238. package/src/workflows/approval-resolution.ts +39 -0
  239. package/src/workflows/definition.ts +7 -0
  240. package/src/workflows/engine.ts +69 -1
  241. package/src/workflows/executors/agent-task.ts +9 -0
  242. package/src/workflows/executors/base.ts +53 -0
  243. package/src/workflows/executors/human-in-the-loop.ts +8 -8
  244. package/src/workflows/executors/index.ts +1 -0
  245. package/src/workflows/executors/registry.ts +2 -0
  246. package/src/workflows/executors/system-one-decision.ts +1232 -0
  247. package/src/workflows/executors/system-one-providers.ts +301 -0
  248. package/src/workflows/executors/workflow-llm.ts +13 -1
  249. package/src/workflows/readiness.ts +86 -0
  250. package/src/workflows/recovery.ts +12 -12
  251. package/src/workflows/resume.ts +23 -9
  252. package/templates/schedules/daily-blocker-digest/content.md +4 -5
  253. package/templates/skills/agent-browser/SKILL.md +6 -5
  254. package/templates/skills/agent-browser/content.md +6 -5
  255. package/templates/skills/heartbeat-runbook/config.json +1 -1
  256. package/templates/skills/heartbeat-runbook/content.md +2 -2
  257. package/templates/skills/swarm-scripts/SKILL.md +1 -1
  258. package/templates/skills/swarm-scripts/content.md +1 -1
  259. package/templates/skills/workflow-iterate/content.md +81 -1
  260. package/templates/workflows/claude-code-changelog-watch/content.md +2 -0
  261. package/templates/workflows/pr-review-status-sweep/content.md +2 -0
  262. package/dist/claude-sdk-session-t442qz6f.js +0 -39108
  263. package/dist/cli-0xjkj1j7.js +0 -61
  264. package/dist/cli-65hjmev5.js +0 -1141
  265. package/dist/cli-aw1jwp1r.js +0 -14075
  266. package/dist/cli-h967nb6r.js +0 -6603
  267. package/dist/cli-vapyk8sw.js +0 -20
  268. package/dist/cli-ws6rqycm.js +0 -247
  269. package/dist/pricing-refresh-v1cbr9te.js +0 -27
  270. package/src/providers/modelsdev-reasoning.json +0 -6453
  271. package/dist/{codex-login-x3jk4etn.js → codex-login-he9vrn90.js} +3 -3
  272. package/dist/{index-f24115ge.js → index-2wsdsnmm.js} +3 -3
@@ -0,0 +1,301 @@
1
+ import { DEFAULT_OPENROUTER_BASE_URL, getOpenRouterBaseUrl } from "../../utils/openrouter-base-url";
2
+ import type { ExecutorDependencies } from "./base";
3
+ import { resolveWorkflowLlmConfig } from "./workflow-llm";
4
+
5
+ /**
6
+ * Hosts that serve typed decisions (Jev, laya). A `system-one-decision` node names one by id
7
+ * (`config.provider`); the endpoint, header shape, and key name live here, never
8
+ * in a workflow definition. Every host takes the same `{ state, model, questions }`
9
+ * request and returns the same `{ model, answers, usage }` body, so the answer
10
+ * contract in `system-one-decision.ts` is shared. Adding a host is one entry in `SYSTEM_ONE_PROVIDERS`.
11
+ *
12
+ * Do not add a chat-completions route. OpenRouter's `typesafe/jev-router` is a
13
+ * router that forwards a chat request to another model, and `~typesafe/jev-latest`
14
+ * refuses chat/completions ("is a decisions model"). Only the decisions endpoint
15
+ * runs Jev itself.
16
+ */
17
+
18
+ export interface SystemOneProviderContext {
19
+ db: ExecutorDependencies["db"];
20
+ env: NodeJS.ProcessEnv;
21
+ }
22
+
23
+ /**
24
+ * A host with no fixed address: the base URL is a global config value, and the
25
+ * route lives under it. The workflow still carries no URL.
26
+ */
27
+ export interface SystemOneConfiguredEndpoint {
28
+ /** Global config key holding the server's base URL, e.g. `LAYA_URL`. */
29
+ readonly configKey: string;
30
+ /** Route under the base URL, starting with `/`. */
31
+ readonly path: string;
32
+ }
33
+
34
+ export interface SystemOneProvider {
35
+ /** Name used in messages, e.g. "TypeSafe". */
36
+ readonly label: string;
37
+ /** The single credential this host needs: a global secret name. */
38
+ readonly keyName: string;
39
+ /** A fixed URL, or the global config key that names the server. */
40
+ readonly endpoint: string | SystemOneConfiguredEndpoint;
41
+ /**
42
+ * Model used when the node sets none. Absent means no `model` is sent and the
43
+ * host picks, for a host that ignores a model id it does not know.
44
+ */
45
+ readonly defaultModel?: string;
46
+ /**
47
+ * The field of a `choice` or `score` answer that is the answer's confidence: what
48
+ * the output reports and what a `humanReview` band is tested against.
49
+ */
50
+ readonly confidenceField: "confidence" | "answer_confidence";
51
+ /** A deployment setting that makes this host unusable, or null. */
52
+ readonly deploymentProblem?: (env: NodeJS.ProcessEnv) => string | null;
53
+ /** The credential value, or null/blank when absent. May throw; callers never relay the cause. */
54
+ readonly readKey: (ctx: SystemOneProviderContext) => Promise<string | null | undefined>;
55
+ }
56
+
57
+ const TYPESAFE_KEY = "TYPESAFE_API_KEY";
58
+ const OPENROUTER_KEY = "OPENROUTER_API_KEY";
59
+ const LAYA_KEY = "LAYA_API_KEY";
60
+ const LAYA_URL_KEY = "LAYA_URL";
61
+
62
+ /** Filter by key and scope in SQL so no other config row is read or decrypted. */
63
+ async function readGlobalConfig(
64
+ db: ExecutorDependencies["db"],
65
+ key: string,
66
+ ): Promise<string | undefined> {
67
+ const rows = await db.getSwarmConfigs({ scope: "global", key });
68
+ return rows.find((row) => row.scope === "global" && row.key === key)?.value;
69
+ }
70
+
71
+ export const SYSTEM_ONE_PROVIDERS = {
72
+ typesafe: {
73
+ label: "TypeSafe",
74
+ keyName: TYPESAFE_KEY,
75
+ endpoint: "https://api.typesafe.ai/v1/systemone",
76
+ defaultModel: "jev-latest",
77
+ confidenceField: "confidence",
78
+ readKey: ({ db }) => readGlobalConfig(db, TYPESAFE_KEY),
79
+ },
80
+ openrouter: {
81
+ label: "OpenRouter",
82
+ keyName: OPENROUTER_KEY,
83
+ endpoint: "https://openrouter.ai/api/alpha/decisions",
84
+ defaultModel: "~typesafe/jev-latest",
85
+ confidenceField: "confidence",
86
+ deploymentProblem(env) {
87
+ // A gateway URL means OPENROUTER_API_KEY may be a gateway token, and the
88
+ // decisions API is a sibling of /v1 that a chat gateway need not forward.
89
+ // Refuse rather than send a credential to a host it was not issued for.
90
+ return getOpenRouterBaseUrl(env) === DEFAULT_OPENROUTER_BASE_URL
91
+ ? null
92
+ : 'OPENROUTER_BASE_URL points at a gateway. SystemOne provider "openrouter" calls openrouter.ai directly and is not supported through a gateway; unset OPENROUTER_BASE_URL or use provider "typesafe".';
93
+ },
94
+ async readKey({ env }) {
95
+ try {
96
+ // Same resolver raw-llm uses. requireKind stops it falling through to OPENAI_API_KEY.
97
+ return (await resolveWorkflowLlmConfig(undefined, env, { requireKind: "openrouter" }))
98
+ .apiKey;
99
+ } catch {
100
+ return null;
101
+ }
102
+ },
103
+ },
104
+ laya: {
105
+ label: "laya",
106
+ keyName: LAYA_KEY,
107
+ endpoint: { configKey: LAYA_URL_KEY, path: "/v1/systemone" },
108
+ // No default model. laya answers an id it does not know (`jev-latest` included) by
109
+ // routing on the text instead, with HTTP 200, so a default would be sent as if it meant something.
110
+ // `confidence` on a choice or score is entropy-based (0.47 top probability reads
111
+ // ~0.15). `answer_confidence` is the top probability, the number a `noul` band
112
+ // already uses, so one band means the same thing on every question type.
113
+ confidenceField: "answer_confidence",
114
+ readKey: ({ db }) => readGlobalConfig(db, LAYA_KEY),
115
+ },
116
+ } as const satisfies Record<string, SystemOneProvider>;
117
+
118
+ export type SystemOneProviderId = keyof typeof SYSTEM_ONE_PROVIDERS;
119
+
120
+ export const SYSTEM_ONE_PROVIDER_IDS = Object.keys(SYSTEM_ONE_PROVIDERS) as [
121
+ SystemOneProviderId,
122
+ ...SystemOneProviderId[],
123
+ ];
124
+
125
+ export const SYSTEM_ONE_DEFAULT_PROVIDER: SystemOneProviderId = "typesafe";
126
+
127
+ export function isSystemOneProviderId(value: unknown): value is SystemOneProviderId {
128
+ return typeof value === "string" && Object.hasOwn(SYSTEM_ONE_PROVIDERS, value);
129
+ }
130
+
131
+ /** Provider a raw node config selects. Undefined when the field is present but not a known id. */
132
+ export function systemOneProviderOf(
133
+ config: Record<string, unknown>,
134
+ ): SystemOneProviderId | undefined {
135
+ if (config.provider === undefined) return SYSTEM_ONE_DEFAULT_PROVIDER;
136
+ return isSystemOneProviderId(config.provider) ? config.provider : undefined;
137
+ }
138
+
139
+ // ─── Messages ───────────────────────────────────────────────
140
+ //
141
+ // Every message names the exact credential and where to set it. They are shared by
142
+ // the save-time warning, the run-start stop, and the step itself, so an author
143
+ // sees the same sentence wherever the problem surfaces.
144
+
145
+ const SET_IT = (keyName: string) =>
146
+ `Set ${keyName} as a global secret on the Secrets page (Settings > Secrets).`;
147
+
148
+ const SET_URL = (configKey: string) =>
149
+ `Set ${configKey} as a global config value (set-config with scope global).`;
150
+
151
+ export function systemOneKeyMissingMessage(id: SystemOneProviderId): string {
152
+ const { label, keyName } = SYSTEM_ONE_PROVIDERS[id];
153
+ return `${keyName} is not configured. SystemOne provider "${id}" needs a working ${label} API key. ${SET_IT(keyName)}`;
154
+ }
155
+
156
+ export function systemOneKeyUnreadableMessage(id: SystemOneProviderId): string {
157
+ // The underlying error can name a config row id; keep the message generic.
158
+ return `Could not read ${SYSTEM_ONE_PROVIDERS[id].keyName} from swarm config`;
159
+ }
160
+
161
+ export function systemOneKeyMalformedMessage(id: SystemOneProviderId): string {
162
+ return `${SYSTEM_ONE_PROVIDERS[id].keyName} is not a valid bearer token value. ${SET_IT(SYSTEM_ONE_PROVIDERS[id].keyName)}`;
163
+ }
164
+
165
+ /** A 401 or 403 from the host: the key exists but the host does not accept it. */
166
+ export function systemOneKeyRejectedMessage(
167
+ id: SystemOneProviderId,
168
+ status: number,
169
+ code?: string,
170
+ ): string {
171
+ const { label, keyName } = SYSTEM_ONE_PROVIDERS[id];
172
+ const detail = code ? ` (${code})` : "";
173
+ return (
174
+ `${keyName} was rejected by ${label} (HTTP ${status}${detail}). ` +
175
+ `The key is invalid, revoked, or lacks access to the model. Replace it on the Secrets page (Settings > Secrets). No decision was made.`
176
+ );
177
+ }
178
+
179
+ function urlKeyOf(provider: SystemOneProvider): string | null {
180
+ return typeof provider.endpoint === "string" ? null : provider.endpoint.configKey;
181
+ }
182
+
183
+ export function systemOneUrlMissingMessage(id: SystemOneProviderId): string {
184
+ const provider: SystemOneProvider = SYSTEM_ONE_PROVIDERS[id];
185
+ const configKey = urlKeyOf(provider) ?? "";
186
+ return `${configKey} is not configured. SystemOne provider "${id}" needs the URL of a ${provider.label} server. ${SET_URL(configKey)}`;
187
+ }
188
+
189
+ export function systemOneUrlUnreadableMessage(id: SystemOneProviderId): string {
190
+ return `Could not read ${urlKeyOf(SYSTEM_ONE_PROVIDERS[id]) ?? ""} from swarm config`;
191
+ }
192
+
193
+ export function systemOneUrlInvalidMessage(id: SystemOneProviderId): string {
194
+ const provider: SystemOneProvider = SYSTEM_ONE_PROVIDERS[id];
195
+ const configKey = urlKeyOf(provider) ?? "";
196
+ // The value is not echoed: a URL can carry a credential.
197
+ return `${configKey} is not a usable ${provider.label} server URL. It must be an https URL (http only for localhost) with no credentials, query, or fragment. ${SET_URL(configKey)}`;
198
+ }
199
+
200
+ // ─── Credential and endpoint ────────────────────────────────
201
+
202
+ /** A bearer token is one visible token; anything else would break or split the header. */
203
+ function hasWhitespaceOrControl(value: string): boolean {
204
+ for (const char of value) {
205
+ const code = char.charCodeAt(0);
206
+ if (code <= 0x20 || code === 0x7f) return true;
207
+ }
208
+ return false;
209
+ }
210
+
211
+ const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "[::1]"]);
212
+
213
+ /**
214
+ * The full request URL for a configured server, or null when the value is not
215
+ * usable. The credential is sent to this URL, so it must be https (http only for
216
+ * loopback, for local development) and carry no userinfo, query, or fragment.
217
+ */
218
+ export function systemOneEndpointFromBase(base: string, path: string): string | null {
219
+ let url: URL;
220
+ try {
221
+ url = new URL(base.trim());
222
+ } catch {
223
+ return null;
224
+ }
225
+ const secure = url.protocol === "https:";
226
+ const loopback = url.protocol === "http:" && LOOPBACK_HOSTS.has(url.hostname);
227
+ if (!secure && !loopback) return null;
228
+ if (url.username || url.password || url.search || url.hash) return null;
229
+ return `${url.origin}${url.pathname.replace(/\/+$/, "")}${path}`;
230
+ }
231
+
232
+ export type SystemOneTarget =
233
+ | { ok: true; apiKey: string; endpoint: string }
234
+ | { ok: false; error: string };
235
+
236
+ export interface SystemOneTargetOverrides {
237
+ /** Replaces the credential lookup. */
238
+ apiKey?: (id: SystemOneProviderId) => Promise<string | null | undefined>;
239
+ /** Replaces the lookup of a configured server's base URL. */
240
+ serverUrl?: (id: SystemOneProviderId) => Promise<string | null | undefined>;
241
+ }
242
+
243
+ /**
244
+ * Resolve where a provider is reached and the credential it needs, or the message
245
+ * to show when it cannot be used. When both a server URL and a key are missing,
246
+ * both are named in one message. The overrides replace the lookups (tests inject
247
+ * fakes); the deployment check and the blank / malformed / URL checks still apply.
248
+ */
249
+ export async function resolveSystemOneTarget(
250
+ id: SystemOneProviderId,
251
+ ctx: SystemOneProviderContext,
252
+ overrides: SystemOneTargetOverrides = {},
253
+ ): Promise<SystemOneTarget> {
254
+ const provider: SystemOneProvider = SYSTEM_ONE_PROVIDERS[id];
255
+ const deploymentProblem = provider.deploymentProblem?.(ctx.env);
256
+ if (deploymentProblem) return { ok: false, error: deploymentProblem };
257
+
258
+ const errors: string[] = [];
259
+
260
+ let endpoint: string | undefined;
261
+ if (typeof provider.endpoint === "string") {
262
+ endpoint = provider.endpoint;
263
+ } else {
264
+ const { configKey, path } = provider.endpoint;
265
+ let base: string | null | undefined;
266
+ let readable = true;
267
+ try {
268
+ base = await (overrides.serverUrl
269
+ ? overrides.serverUrl(id)
270
+ : readGlobalConfig(ctx.db, configKey));
271
+ } catch {
272
+ readable = false;
273
+ errors.push(systemOneUrlUnreadableMessage(id));
274
+ }
275
+ if (readable) {
276
+ if (!base || base.trim() === "") {
277
+ errors.push(systemOneUrlMissingMessage(id));
278
+ } else {
279
+ const resolved = systemOneEndpointFromBase(base, path);
280
+ if (resolved) endpoint = resolved;
281
+ else errors.push(systemOneUrlInvalidMessage(id));
282
+ }
283
+ }
284
+ }
285
+
286
+ let apiKey: string | null | undefined;
287
+ let keyProblem: string | undefined;
288
+ try {
289
+ apiKey = await (overrides.apiKey ? overrides.apiKey(id) : provider.readKey(ctx));
290
+ } catch {
291
+ keyProblem = systemOneKeyUnreadableMessage(id);
292
+ }
293
+ if (!keyProblem) {
294
+ if (!apiKey || apiKey.trim() === "") keyProblem = systemOneKeyMissingMessage(id);
295
+ else if (hasWhitespaceOrControl(apiKey)) keyProblem = systemOneKeyMalformedMessage(id);
296
+ }
297
+ if (keyProblem) errors.push(keyProblem);
298
+
299
+ if (errors.length > 0 || !endpoint || !apiKey) return { ok: false, error: errors.join(" ") };
300
+ return { ok: true, apiKey, endpoint };
301
+ }
@@ -7,16 +7,28 @@ export interface WorkflowLlmConfig {
7
7
  model: string;
8
8
  }
9
9
 
10
- /** Resolve an OpenAI-compatible credential and endpoint for workflow LLM nodes. */
10
+ /**
11
+ * Resolve an OpenAI-compatible credential and endpoint for workflow LLM nodes.
12
+ *
13
+ * `requireKind` is for callers that must talk to one specific provider: the
14
+ * resolver picks by precedence (OpenRouter, then Anthropic, then OpenAI), so a
15
+ * caller that needs the OpenRouter key would otherwise be handed an OpenAI key
16
+ * whenever the OpenRouter one is absent.
17
+ */
11
18
  export async function resolveWorkflowLlmConfig(
12
19
  requestedModel?: string,
13
20
  env: NodeJS.ProcessEnv = process.env,
21
+ options: { requireKind?: "openrouter" | "openai" } = {},
14
22
  ): Promise<WorkflowLlmConfig> {
15
23
  const credential = await resolveCredential({ env });
16
24
  if (!credential) {
17
25
  throw new Error("No workflow LLM credential found. Set OPENROUTER_API_KEY or OPENAI_API_KEY.");
18
26
  }
19
27
 
28
+ if (options.requireKind && credential.kind !== options.requireKind) {
29
+ throw new Error(`No ${options.requireKind} credential found for this workflow LLM call.`);
30
+ }
31
+
20
32
  if (credential.kind !== "openrouter" && credential.kind !== "openai") {
21
33
  throw new Error(
22
34
  `Workflow LLM nodes do not support the resolved ${credential.kind} credential yet. Set OPENROUTER_API_KEY or OPENAI_API_KEY.`,
@@ -0,0 +1,86 @@
1
+ import type { WorkflowDefinition } from "../types";
2
+ import type { ExecutorReadinessNode } from "./executors/base";
3
+ import type { ExecutorRegistry } from "./executors/registry";
4
+
5
+ export interface WorkflowReadinessProblem {
6
+ /** Executor type that reported the problem. */
7
+ type: string;
8
+ nodeIds: string[];
9
+ message: string;
10
+ }
11
+
12
+ /**
13
+ * Ask every executor type a definition uses whether it can run right now, e.g.
14
+ * whether the credential a `system-one-decision` node needs is configured. Types are checked in
15
+ * first-use order, one call per type. Unregistered types are skipped (definition
16
+ * validation reports them). A check that throws is reported as a problem rather
17
+ * than propagated, so a broken check can never crash a save or start a run blind.
18
+ */
19
+ export async function findWorkflowReadinessProblems(
20
+ definition: Pick<WorkflowDefinition, "nodes">,
21
+ registry: ExecutorRegistry,
22
+ ): Promise<WorkflowReadinessProblem[]> {
23
+ const nodesByType = new Map<string, ExecutorReadinessNode[]>();
24
+ for (const node of definition.nodes) {
25
+ if (!registry.has(node.type)) continue;
26
+ const group = nodesByType.get(node.type) ?? [];
27
+ group.push({ id: node.id, config: node.config });
28
+ nodesByType.set(node.type, group);
29
+ }
30
+
31
+ const problems: WorkflowReadinessProblem[] = [];
32
+ for (const [type, nodes] of nodesByType) {
33
+ try {
34
+ for (const problem of await registry.get(type).checkReadiness(nodes)) {
35
+ problems.push({ type, ...problem });
36
+ }
37
+ } catch {
38
+ problems.push({
39
+ type,
40
+ nodeIds: nodes.map((node) => node.id),
41
+ message: `Could not check whether "${type}" nodes are ready to run`,
42
+ });
43
+ }
44
+ }
45
+ return problems;
46
+ }
47
+
48
+ function describeNodes(problem: WorkflowReadinessProblem): string {
49
+ const noun = problem.nodeIds.length === 1 ? "node" : "nodes";
50
+ return `${problem.type} ${noun} ${problem.nodeIds.map((id) => `"${id}"`).join(", ")}`;
51
+ }
52
+
53
+ /**
54
+ * Message an author gets when a save succeeded but a node cannot run yet.
55
+ * The executor's own message comes first so it reads the same as at run start.
56
+ */
57
+ export function formatReadinessWarning(problem: WorkflowReadinessProblem): string {
58
+ return `${problem.message} Affects ${describeNodes(problem)}. Runs of this workflow fail before any node executes until this is fixed.`;
59
+ }
60
+
61
+ /** The problems as one sentence list; callers prefix what was stopped. */
62
+ export function formatReadinessProblems(problems: readonly WorkflowReadinessProblem[]): string {
63
+ return problems
64
+ .map((problem) => `${problem.message} Needed by ${describeNodes(problem)}.`)
65
+ .join(" ");
66
+ }
67
+
68
+ /** Error recorded on a run that was stopped before any node executed. */
69
+ export function formatReadinessRunError(problems: readonly WorkflowReadinessProblem[]): string {
70
+ return `Run not started, no node executed: ${formatReadinessProblems(problems)}`;
71
+ }
72
+
73
+ /** Warnings for a definition that was just saved. Empty when every executor is ready. */
74
+ export async function workflowSaveWarnings(
75
+ definition: Pick<WorkflowDefinition, "nodes">,
76
+ registry: ExecutorRegistry,
77
+ ): Promise<string[]> {
78
+ return (await findWorkflowReadinessProblems(definition, registry)).map(formatReadinessWarning);
79
+ }
80
+
81
+ /** Append save-time warnings to a tool message so the author sees them in the summary. */
82
+ export function withSaveWarnings(text: string, warnings: readonly string[]): string {
83
+ return warnings.length === 0
84
+ ? text
85
+ : `${text} ${warnings.map((warning) => `Warning: ${warning}`).join(" ")}`;
86
+ }
@@ -13,6 +13,7 @@ import {
13
13
  updateWorkflowRun,
14
14
  } from "../be/db";
15
15
  import type { WorkflowRunStep } from "../types";
16
+ import { shapeApprovalResolution } from "./approval-resolution";
16
17
  import { loadCompletedStepRouting } from "./completed-step-routing";
17
18
  import { FAILED_TASK_OUTPUT_PREFIX } from "./constants";
18
19
  import { findReadyNodes, isWorkflowRunActive, walkGraph } from "./engine";
@@ -279,18 +280,17 @@ async function recoverApprovalWaitingRuns(registry: ExecutorRegistry): Promise<n
279
280
  return null;
280
281
  }
281
282
 
282
- const nextPort =
283
- approvalStatus === "timeout"
284
- ? "timeout"
285
- : approvalStatus === "rejected"
286
- ? "rejected"
287
- : "approved";
288
-
289
- const stepOutput = {
290
- requestId: stuck.approvalId,
291
- status: approvalStatus,
292
- responses: approval?.responses ?? null,
293
- };
283
+ const { output: stepOutput, nextPort } = shapeApprovalResolution(
284
+ registry,
285
+ workflow.definition,
286
+ stuck.nodeId,
287
+ (await getWorkflowRunStep(stuck.stepId))?.output,
288
+ {
289
+ requestId: stuck.approvalId,
290
+ status: approvalStatus,
291
+ responses: (approval?.responses ?? null) as Record<string, unknown> | null,
292
+ },
293
+ );
294
294
 
295
295
  // Use port-based routing to determine correct successors. Claimed: the
296
296
  // live approval.resolved bus event may have routed this step between
@@ -22,6 +22,7 @@ import {
22
22
  type ApprovalSlackClient,
23
23
  postApprovalCancellationUpdates,
24
24
  } from "./approval-notifications";
25
+ import { shapeApprovalResolution } from "./approval-resolution";
25
26
  import { loadCompletedStepRouting } from "./completed-step-routing";
26
27
  import { FAILED_TASK_OUTPUT_PREFIX } from "./constants";
27
28
  import { getNextTargets } from "./definition";
@@ -32,6 +33,7 @@ import type { ExecutorRegistry } from "./executors/registry";
32
33
  import { computeNextPort } from "./executors/wait";
33
34
  import { resolveForeachParent } from "./foreach-join";
34
35
  import { getSecretInputKeys } from "./input";
36
+ import { findWorkflowReadinessProblems, formatReadinessProblems } from "./readiness";
35
37
  import {
36
38
  checkpointPortStepAndResolveSuccessors,
37
39
  completeTaskStepAndResolveSuccessors,
@@ -325,6 +327,18 @@ export async function retryFailedRun(runId: string, registry: ExecutorRegistry):
325
327
  if (!failedStep) throw new Error("No failed step found");
326
328
 
327
329
  const completedNodeIds = new Set(await getCompletedStepNodeIds(runId));
330
+
331
+ // A retry re-executes the failed node and everything after it. Refuse before the
332
+ // run is reset when a node still to run cannot run (say, a system-one-decision node with no API
333
+ // key), so the retry does not repeat side effects only to fail again downstream.
334
+ const notReady = await findWorkflowReadinessProblems(
335
+ { nodes: workflow.definition.nodes.filter((node) => !completedNodeIds.has(node.id)) },
336
+ registry,
337
+ );
338
+ if (notReady.length > 0) {
339
+ throw new Error(`Retry not started, run left failed: ${formatReadinessProblems(notReady)}`);
340
+ }
341
+
328
342
  const { activeEdges } = await loadCompletedStepRouting(
329
343
  workflow.definition,
330
344
  runId,
@@ -519,15 +533,15 @@ async function resumeFromApprovalResolution(
519
533
  return;
520
534
  }
521
535
 
522
- // Determine output port based on approval status
523
- const nextPort =
524
- event.status === "timeout" ? "timeout" : event.status === "rejected" ? "rejected" : "approved";
525
-
526
- const stepOutput = {
527
- requestId: event.requestId,
528
- status: event.status,
529
- responses: event.responses,
530
- };
536
+ // Output and port for the approval status. A step parked by an executor other
537
+ // than human-in-the-loop shapes its own (see shapeApprovalResolution).
538
+ const { output: stepOutput, nextPort } = shapeApprovalResolution(
539
+ registry,
540
+ workflow.definition,
541
+ step.nodeId,
542
+ step.output,
543
+ { requestId: event.requestId, status: event.status, responses: event.responses },
544
+ );
531
545
 
532
546
  // Use port-based routing to determine the correct successors.
533
547
  // findReadyNodes without activeEdges would return ALL structural successors
@@ -29,12 +29,11 @@ You are Lead. Produce one concise daily digest of blockers, stale claims, open P
29
29
 
30
30
  Run these read-only global scripts first. Use their outputs as evidence, then apply judgment:
31
31
 
32
- 1. `script-run` global script `Heartbeat Audit` with args `{ "heartbeatMarkdown": "<current /workspace/HEARTBEAT.md text>" }`
33
- 2. `script-run` global script `schedule-health` with args `{ "days": 7, "publishPage": true }`
34
- 3. `script-run` global script `task-failure-audit` with args `{ "days": 7, "groupBy": "reason", "publishPage": true }`
35
- 4. Read `GET /status`. If the `embeddings` milestone is `unverified`, append this line to the digest: "Memory search is off: set OPENAI_API_KEY on the API server to enable it."
32
+ 1. `script-run` global script `schedule-health` with args `{ "days": 7, "publishPage": true }`
33
+ 2. `script-run` global script `task-failure-audit` with args `{ "days": 7, "groupBy": "reason", "publishPage": true }`
34
+ 3. Read `GET /status`. If the `embeddings` milestone is `unverified`, append this line to the digest: "Memory search is off: set OPENAI_API_KEY on the API server to enable it."
36
35
 
37
- Do not script-ify judgment or notification copy. Use the script outputs to identify stale blocker claims, schedule risks, provider failure clusters, and digest-run health.
36
+ Do not script-ify judgment or notification copy. Use the script outputs to identify schedule risks and provider failure clusters.
38
37
 
39
38
  ## Phase 1: Gather Blockers
40
39
 
@@ -64,21 +64,22 @@ ffmpeg -y -i in.mp4 -filter:v "setpts=PTS/1.5,fps=30" -an \
64
64
 
65
65
  Speed-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`.
66
66
 
67
- Add `--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. Upload the 1.5x file through the same agent-fs QA path and signed-URL steps as screenshots below, and link it in the PR body.
67
+ Add `--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. Upload the 1.5x file through the same agent-fs QA path as screenshots below, and link it in the PR body.
68
68
 
69
69
  ## Step 3: share the screenshot through agent-fs
70
70
 
71
- Screenshots on the worker disk disappear with the task. Upload them to agent-fs under the qa path convention and share the signed URL. `--file` is binary-safe. `--content` is text-only and mangles PNGs.
71
+ Screenshots on the worker disk disappear with the task. Upload them to agent-fs under the qa path convention. Use `share-create` when a person needs to open the file, and `signed-url` for raw embeds such as an image in GitHub PR markdown. `--file` is binary-safe. `--content` is text-only and mangles PNGs.
72
72
 
73
73
  ```bash
74
74
  agent-fs write thoughts/<agent-id>/qa/<topic>-screenshots/<name>.png \
75
75
  --file /tmp/<name>.png -m "<what it shows>"
76
76
  agent-fs stat thoughts/<agent-id>/qa/<topic>-screenshots/<name>.png --json # confirm size > 0
77
- agent-fs signed-url thoughts/<agent-id>/qa/<topic>-screenshots/<name>.png --json # 24h default, --expires-in up to 7d
77
+ agent-fs share-create thoughts/<agent-id>/qa/<topic>-screenshots/<name>.png --json
78
+ agent-fs signed-url thoughts/<agent-id>/qa/<topic>-screenshots/<name>.png --json # raw URL for embeds; 24h default, --expires-in up to 7d
78
79
  ```
79
80
 
80
- - Embed the URL as `![caption](<url>)` in the PR body, review comment, Linear comment, or Slack message.
81
- - In `store-progress`, list the upload in the `attachments` field with `kind: "agent-fs"` and the path, and paste the signed URL in the progress text.
81
+ - Embed the signed URL as `![caption](<url>)` in the PR body, review comment, Linear comment, or Slack message.
82
+ - 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.
82
83
  - If `agent-fs auth whoami` fails, report the local path, say the upload was skipped, and continue.
83
84
 
84
85
  The `artifacts` skill holds the full agent-fs recipe and the naming conventions.
@@ -59,21 +59,22 @@ ffmpeg -y -i in.mp4 -filter:v "setpts=PTS/1.5,fps=30" -an \
59
59
 
60
60
  Speed-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`.
61
61
 
62
- Add `--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. Upload the 1.5x file through the same agent-fs QA path and signed-URL steps as screenshots below, and link it in the PR body.
62
+ Add `--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. Upload the 1.5x file through the same agent-fs QA path as screenshots below, and link it in the PR body.
63
63
 
64
64
  ## Step 3: share the screenshot through agent-fs
65
65
 
66
- Screenshots on the worker disk disappear with the task. Upload them to agent-fs under the qa path convention and share the signed URL. `--file` is binary-safe. `--content` is text-only and mangles PNGs.
66
+ Screenshots on the worker disk disappear with the task. Upload them to agent-fs under the qa path convention. Use `share-create` when a person needs to open the file, and `signed-url` for raw embeds such as an image in GitHub PR markdown. `--file` is binary-safe. `--content` is text-only and mangles PNGs.
67
67
 
68
68
  ```bash
69
69
  agent-fs write thoughts/<agent-id>/qa/<topic>-screenshots/<name>.png \
70
70
  --file /tmp/<name>.png -m "<what it shows>"
71
71
  agent-fs stat thoughts/<agent-id>/qa/<topic>-screenshots/<name>.png --json # confirm size > 0
72
- agent-fs signed-url thoughts/<agent-id>/qa/<topic>-screenshots/<name>.png --json # 24h default, --expires-in up to 7d
72
+ agent-fs share-create thoughts/<agent-id>/qa/<topic>-screenshots/<name>.png --json
73
+ agent-fs signed-url thoughts/<agent-id>/qa/<topic>-screenshots/<name>.png --json # raw URL for embeds; 24h default, --expires-in up to 7d
73
74
  ```
74
75
 
75
- - Embed the URL as `![caption](<url>)` in the PR body, review comment, Linear comment, or Slack message.
76
- - In `store-progress`, list the upload in the `attachments` field with `kind: "agent-fs"` and the path, and paste the signed URL in the progress text.
76
+ - Embed the signed URL as `![caption](<url>)` in the PR body, review comment, Linear comment, or Slack message.
77
+ - 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.
77
78
  - If `agent-fs auth whoami` fails, report the local path, say the upload was skipped, and continue.
78
79
 
79
80
  The `artifacts` skill holds the full agent-fs recipe and the naming conventions.
@@ -4,7 +4,7 @@
4
4
  "displayName": "Heartbeat Runbook",
5
5
  "slug": "heartbeat-runbook",
6
6
  "title": "Heartbeat Runbook",
7
- "description": "Heartbeat, HEARTBEAT.md, standing orders, heartbeat-checklist task, boot-triage task: how the lead keeps its heartbeat runbook short and handles a checklist or boot-triage task. Covers the tracked-item cap, lift triggers, the `Heartbeat Audit` and `boot-triage` scripts, and reboot-interrupted work. Lead only.",
7
+ "description": "Heartbeat, HEARTBEAT.md, standing orders, heartbeat-checklist task, boot-triage task: how the lead keeps its heartbeat runbook short and handles a checklist or boot-triage task. Covers the tracked-item cap, lift triggers, the `schedule-health`, `task-failure-audit` and `boot-triage` scripts, and reboot-interrupted work. Lead only.",
8
8
  "version": "1.0.0",
9
9
  "category": "skills",
10
10
  "placeholders": [],
@@ -29,8 +29,8 @@ Two kinds of sections:
29
29
 
30
30
  1. Read the latest runbook. The snapshot in the task may be stale.
31
31
  2. Prune first. Check every tracked item against its lift trigger. Remove resolved, stale, and past-date items.
32
- 3. Run the seeded audit: `script-run` with name `Heartbeat Audit` and `args: { heartbeatMarkdown: <the runbook text> }`. It reports resolved stale PRs, pool-target risk schedules, schedule and provider failure clusters, and whether the daily blocker digest ran today.
33
- 4. Read the system status and the audit result for stalled tasks, idle workers next to open work, and anomalies.
32
+ 3. Run the seeded scripts: `script-run` with name `schedule-health`, then `task-failure-audit`, both with `args: { days: 1, publishPage: false }`. They report failing schedules and failure clusters. Check any other standing order in the runbook yourself.
33
+ 4. Read the system status and the script results for stalled tasks, idle workers next to open work, and anomalies.
34
34
  5. Reboot-interrupted failures: a failure reason "worker session not found" or "worker session heartbeat is stale" means a server restart cut the task off. For each one: `get-task-details` on the task, confirm a retry task tagged `reboot-retry` exists and moves, recreate the task when no retry exists and the work is still needed. These are never "expected cleanup".
35
35
  6. Act with your tools: create tasks, cancel stuck ones, post to Slack under the `slack-interaction` skill rules.
36
36
  7. Update the runbook after pruning.
@@ -43,7 +43,7 @@ The swarm ships named scripts at global scope. Each one replaces a multi-step to
43
43
  | `complete-task` | `{ taskId, output }` | finish a task from inside a script |
44
44
  | `report-progress` | `{ taskId, note }` | a progress note from inside a script |
45
45
  | `swarm-overview` | `{}` | agents and task counts |
46
- | `Heartbeat Audit`, `boot-triage` | see `heartbeat-runbook` | the lead's heartbeat data gathering |
46
+ | `schedule-health`, `task-failure-audit`, `boot-triage` | see `heartbeat-runbook` | the lead's heartbeat data gathering |
47
47
 
48
48
  ## Authoring contract
49
49
 
@@ -38,7 +38,7 @@ The swarm ships named scripts at global scope. Each one replaces a multi-step to
38
38
  | `complete-task` | `{ taskId, output }` | finish a task from inside a script |
39
39
  | `report-progress` | `{ taskId, note }` | a progress note from inside a script |
40
40
  | `swarm-overview` | `{}` | agents and task counts |
41
- | `Heartbeat Audit`, `boot-triage` | see `heartbeat-runbook` | the lead's heartbeat data gathering |
41
+ | `schedule-health`, `task-failure-audit`, `boot-triage` | see `heartbeat-runbook` | the lead's heartbeat data gathering |
42
42
 
43
43
  ## Authoring contract
44
44