@desplega.ai/agent-swarm 1.158.0 → 1.160.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 (378) hide show
  1. package/README.md +1 -1
  2. package/dist/{acp-adapter-xaq4n6pc.js → acp-adapter-2mjwcsfh.js} +124 -27
  3. package/dist/{actions-p9hz5yat.js → actions-e43rxskr.js} +35 -30
  4. package/dist/{anthropic-messages-51pz26qe.js → anthropic-messages-5qx80j9k.js} +106 -16
  5. package/dist/{app-g26wdcz6.js → app-mc7xfagn.js} +12 -10
  6. package/dist/{assistant-wydfqkb3.js → assistant-sedkzv3b.js} +42 -31
  7. package/dist/{azure-openai-responses-8gksqtmz.js → azure-openai-responses-nfb21z9a.js} +13 -14
  8. package/dist/{boot-reembed-se8zmdcp.js → boot-reembed-cqz9swn5.js} +13 -12
  9. package/dist/{boot-reembed-g5ckg9yb.js → boot-reembed-m3656j2n.js} +14 -13
  10. package/dist/{boot-scrub-logs-wfxqan61.js → boot-scrub-logs-xbxf5d7w.js} +11 -9
  11. package/dist/boot-seeds-gsetbs5y.js +28 -0
  12. package/dist/{claude-adapter-tv2x4f36.js → claude-adapter-dc6289mc.js} +28 -23
  13. package/dist/{claude-managed-adapter-41b2s7fd.js → claude-managed-adapter-naaw51me.js} +4 -3
  14. package/dist/{claude-managed-setup-60g8aazx.js → claude-managed-setup-eacvchx0.js} +12 -3
  15. package/dist/claude-sdk-session-bqp2xvym.js +29722 -0
  16. package/dist/{cli-987hg0aa.js → cli-0jwe9034.js} +2 -2
  17. package/dist/{cli-vj6h9kc2.js → cli-0m7wct0d.js} +59 -1
  18. package/dist/{cli-c07r9a2m.js → cli-0q3avq3s.js} +12 -11
  19. package/dist/{cli-19jenss3.js → cli-0wgxmwf6.js} +31 -10
  20. package/dist/cli-0x01kwrd.js +491 -0
  21. package/dist/cli-0xqde53b.js +176 -0
  22. package/dist/cli-10nhj6xq.js +8355 -0
  23. package/dist/{cli-tt9k8r35.js → cli-12tszpk9.js} +479 -910
  24. package/dist/{cli-5b6b62tz.js → cli-13hw4z91.js} +1 -1
  25. package/dist/{cli-cqt19wg6.js → cli-1a2j50m0.js} +11 -2
  26. package/dist/{cli-xberxa1r.js → cli-25fcq4b4.js} +2 -2
  27. package/dist/{cli-npssxgaz.js → cli-2pdaysab.js} +52 -2
  28. package/dist/cli-3qhyh31j.js +166 -0
  29. package/dist/cli-447dt1h3.js +3828 -0
  30. package/dist/cli-4derpgyk.js +115 -0
  31. package/dist/{cli-00w2c66p.js → cli-523679eh.js} +13 -2
  32. package/dist/{cli-y95wp3tg.js → cli-58e8yr0d.js} +1 -1
  33. package/dist/{cli-24k806sc.js → cli-5nbmpy8w.js} +3 -3
  34. package/dist/{cli-53sz8gkz.js → cli-6cs6wzxt.js} +1 -1
  35. package/dist/cli-741yb0xx.js +1027 -0
  36. package/dist/cli-74h9hscv.js +69 -0
  37. package/dist/cli-7cxw8b60.js +14910 -0
  38. package/dist/{cli-xhaeg2nc.js → cli-7e398j1h.js} +1 -1
  39. package/dist/cli-7kyk9jr3.js +22 -0
  40. package/dist/cli-89s4ngw3.js +183 -0
  41. package/dist/{cli-9b13ggd1.js → cli-89v6kyq7.js} +1 -0
  42. package/dist/{cli-ftfvggzx.js → cli-9hsgwz4w.js} +1045 -581
  43. package/dist/{cli-rqz8qbw7.js → cli-a8dvhzrt.js} +59 -20
  44. package/dist/{cli-j57qkrpq.js → cli-azhgzetg.js} +395 -56
  45. package/dist/cli-br0qdrnp.js +2150 -0
  46. package/dist/{cli-rj19hc34.js → cli-btfv8y88.js} +1 -1
  47. package/dist/{cli-8qmme6vd.js → cli-c8a36vcc.js} +2 -2
  48. package/dist/cli-cpvsaqsw.js +333 -0
  49. package/dist/cli-d5czme6m.js +17805 -0
  50. package/dist/{cli-h38x0qhg.js → cli-et8m9xsv.js} +4 -3
  51. package/dist/{cli-x24pshtq.js → cli-evvtk35y.js} +6 -6
  52. package/dist/{cli-7s3syk22.js → cli-f8gy6r2r.js} +5 -5
  53. package/dist/{cli-zt1227k1.js → cli-fkqhbg8g.js} +5 -3
  54. package/dist/{cli-9httdpyq.js → cli-g2whhdrf.js} +1 -1
  55. package/dist/cli-gt4hxb8p.js +99 -0
  56. package/dist/{cli-s8eet254.js → cli-jajnsm8q.js} +37184 -53838
  57. package/dist/{cli-7gtvvpbh.js → cli-jayjpjmb.js} +21 -12
  58. package/dist/{cli-68w09sjw.js → cli-jyk0ynw2.js} +27 -89
  59. package/dist/{cli-aehm1rbq.js → cli-jz4w45qt.js} +1152 -61
  60. package/dist/cli-kmcpfwn2.js +333856 -0
  61. package/dist/{cli-4198qc9v.js → cli-ktmyrtpn.js} +19 -3
  62. package/dist/{cli-j55n64b6.js → cli-mfe5btp9.js} +505 -189
  63. package/dist/{cli-q67y4wzm.js → cli-phsaj4xq.js} +24 -2
  64. package/dist/{cli-1hkrch3k.js → cli-pm1rst0h.js} +46 -7
  65. package/dist/{cli-pw9rj5t9.js → cli-pqetb83f.js} +1 -176
  66. package/dist/{cli-ggm92wcn.js → cli-ptr8h7kp.js} +20 -1
  67. package/dist/{cli-s8s499xy.js → cli-qbksc292.js} +3 -2
  68. package/dist/{cli-47m6dkd3.js → cli-qydf22qq.js} +597 -346
  69. package/dist/{cli-3gmcwfyx.js → cli-r1mxd1xh.js} +13 -6
  70. package/dist/cli-rb9m6rpe.js +4980 -0
  71. package/dist/cli-rykfmgwg.js +102 -0
  72. package/dist/{cli-5d10vd2m.js → cli-shkxm1vn.js} +6 -5
  73. package/dist/{cli-eetrvv91.js → cli-t3p77qk1.js} +1 -1
  74. package/dist/cli-tfky2838.js +756 -0
  75. package/dist/{cli-g8djp6qe.js → cli-tmqt7dpj.js} +40 -0
  76. package/dist/{cli-fgqxwrpv.js → cli-tpq83z89.js} +1631 -127
  77. package/dist/cli-trc24wk2.js +143 -0
  78. package/dist/{cli-aky8tctm.js → cli-tzx2ke1v.js} +3 -3
  79. package/dist/cli-v7fn5gms.js +1425 -0
  80. package/dist/{cli-wscdvcqz.js → cli-vctdqs5h.js} +1 -1
  81. package/dist/{cli-cw9zck8j.js → cli-xjcywfsf.js} +8 -4
  82. package/dist/{cli-k0g1sjzm.js → cli-ybgv4kw2.js} +18 -11
  83. package/dist/{cli-rjehvrts.js → cli-ydmcgve1.js} +8 -8
  84. package/dist/{cli-vhjwh6cz.js → cli-ygwm91nt.js} +1 -1
  85. package/dist/{cli-mcnhgnn1.js → cli-ykk5jaax.js} +184 -26
  86. package/dist/{cli-de1396j2.js → cli-ywybs5as.js} +7 -1
  87. package/dist/cli-zpep9q5e.js +491 -0
  88. package/dist/cli.js +41 -32
  89. package/dist/cloudflare-workers-ai-system-one-k6fbxw9d.js +46 -0
  90. package/dist/{codex-adapter-cnm8wk11.js → codex-adapter-nmxcatww.js} +26 -17
  91. package/dist/{codex-hook-9hptmdhj.js → codex-hook-20me6byw.js} +2 -2
  92. package/dist/{codex-login-x3jk4etn.js → codex-login-w6fz4xbz.js} +50 -5
  93. package/dist/{codex-session-runner-gfg547zj.js → codex-session-runner-90aa80ev.js} +24 -17
  94. package/dist/{commands-e3bn0p75.js → commands-8wcq1kn7.js} +11 -9
  95. package/dist/{db-03v9kzr2.js → db-j40kc4se.js} +25 -11
  96. package/dist/{devin-adapter-8vgkwh29.js → devin-adapter-dpsv6hd9.js} +1 -1
  97. package/dist/{dispatcher-nrmg8yht.js → dispatcher-4zqy768d.js} +34 -29
  98. package/dist/{dsh-adapter-k03nfqa6.js → dsh-adapter-mpnzgztj.js} +3 -3
  99. package/dist/{e2b-93x1k0j5.js → e2b-ez07znam.js} +4 -3
  100. package/dist/execute-6e7qeyk4.js +1017 -0
  101. package/dist/{google-generative-ai-bxmv50nc.js → google-generative-ai-k6n5nbpp.js} +6 -5
  102. package/dist/{google-vertex-809af514.js → google-vertex-44xxtcvp.js} +6 -5
  103. package/dist/{handlers-aqcedbyb.js → handlers-kjdce1w5.js} +37 -32
  104. package/dist/{hook-ybvmrmhq.js → hook-czx13n12.js} +19 -15
  105. package/dist/{hook-7vkskkre.js → hook-r2t4bqfg.js} +18 -14
  106. package/dist/{http-2eqa8t24.js → http-3bbb19be.js} +900 -145
  107. package/dist/{index-f6cryxwe.js → index-4a5njnm1.js} +2 -59
  108. package/dist/{index-x71bvq0p.js → index-668me3f0.js} +35 -30
  109. package/dist/{index-c6zsv1bd.js → index-7aj368tw.js} +13 -12
  110. package/dist/{index-1yks3qcr.js → index-fg0dsp6c.js} +34 -29
  111. package/dist/{index-chgdkq36.js → index-kkv84jq9.js} +34 -29
  112. package/dist/{index-a3vn96n8.js → index-zg7vp46x.js} +198 -54
  113. package/dist/{keepalive-vtkf5n99.js → keepalive-yra8dy57.js} +13 -11
  114. package/dist/{lead-h3sp357x.js → lead-va0pad2c.js} +54 -49
  115. package/dist/llama-cpp-classify-tgj35k8d.js +335 -0
  116. package/dist/{maintenance-jzeqtk3q.js → maintenance-hqewtmyg.js} +14 -13
  117. package/dist/{mistral-conversations-zx0kn554.js → mistral-conversations-pc3ntnxt.js} +14 -17
  118. package/dist/{oauth-refresh-sweep-qp3dzmdp.js → oauth-refresh-sweep-7rdt1m9n.js} +12 -10
  119. package/dist/{onboard-tmsp0qbz.js → onboard-j6ms6d0x.js} +6 -5
  120. package/dist/{openai-codex-responses-4vjqzvjp.js → openai-codex-responses-bbancrr0.js} +21 -8
  121. package/dist/{openai-completions-9hjrqbw8.js → openai-completions-jpaj5g2b.js} +9 -12
  122. package/dist/{openai-responses-7b1pn0xt.js → openai-responses-200k1wx4.js} +23 -18
  123. package/dist/{opencode-adapter-x6x2da1z.js → opencode-adapter-89rabkw2.js} +8 -7
  124. package/dist/{openrouter-images-9f9ek4rw.js → openrouter-images-8yx3487g.js} +5 -7
  125. package/dist/{otel-impl-9mz73q65.js → otel-impl-a792rjz0.js} +1 -1
  126. package/dist/{pi-messages-txpjj39q.js → pi-messages-vxdt8v7j.js} +2 -1
  127. package/dist/{pi-mono-adapter-m6dvv0dz.js → pi-mono-adapter-g7qzvr3v.js} +295 -70
  128. package/dist/pricing-refresh-wx0tp7cb.js +39 -0
  129. package/dist/quickjs-r1pq2107.wasm +0 -0
  130. package/dist/{rbac-roles-c9wqz83x.js → rbac-roles-8tzawacv.js} +12 -10
  131. package/dist/{render-v2-gryrh9h8.js → render-v2-mm7zswb8.js} +12 -10
  132. package/dist/runtime-zyssgse0.js +24 -0
  133. package/dist/{setup-q9720bt6.js → setup-8y70ys6t.js} +6 -5
  134. package/dist/typesafe-system-one-2ef30pec.js +27 -0
  135. package/dist/{virtual-modules-3htpgn8x.js → virtual-modules-6nmk7h3n.js} +22 -13
  136. package/dist/{work-objects-b76x7deb.js → work-objects-fg6xkb0n.js} +37 -32
  137. package/dist/{worker-6vrxbv41.js → worker-wmwhxsph.js} +54 -49
  138. package/openapi.json +1314 -91
  139. package/package.json +12 -11
  140. package/src/be/db/agents.ts +20 -0
  141. package/src/be/db/api-keys.ts +70 -0
  142. package/src/be/db/tasks/human-free.ts +442 -0
  143. package/src/be/db/tasks/read.ts +56 -1
  144. package/src/be/db/tasks/write.ts +107 -11
  145. package/src/be/db.ts +274 -188
  146. package/src/be/harness-model-support.ts +182 -0
  147. package/src/be/human-free-drain.ts +67 -0
  148. package/src/be/memory/providers/sqlite-store.ts +51 -15
  149. package/src/be/migrations/182_task_human_free_flag.sql +63 -0
  150. package/src/be/migrations/183_human_free_reclassify_queue.sql +25 -0
  151. package/src/be/migrations/184_model_catalog.sql +120 -0
  152. package/src/be/migrations/185_api_key_auth_failures.sql +6 -0
  153. package/src/be/migrations/186_api_key_seat_mismatch.sql +8 -0
  154. package/src/be/model-catalog-store.ts +644 -0
  155. package/src/be/model-tier-keys.ts +46 -0
  156. package/src/be/model-tier-resolution.ts +534 -0
  157. package/src/be/model-validation.ts +188 -0
  158. package/src/be/models-catalog.ts +61 -7
  159. package/src/be/modelsdev-cache.json +82 -0
  160. package/src/be/modelsdev-cache.ts +2 -0
  161. package/src/be/pricing-refresh.ts +154 -12
  162. package/src/be/scripts/typecheck.ts +15 -0
  163. package/src/be/seed/runner.ts +18 -12
  164. package/src/be/seed/types.ts +1 -0
  165. package/src/be/seed-scripts/catalog/task-failure-audit.ts +61 -9
  166. package/src/be/swarm-config-guard.ts +8 -0
  167. package/src/cli.tsx +10 -0
  168. package/src/commands/claude-managed-setup.ts +3 -1
  169. package/src/commands/codex-login.ts +67 -0
  170. package/src/commands/credential-outcome-report.ts +191 -0
  171. package/src/commands/rate-limit-outcome.ts +34 -4
  172. package/src/commands/runner.ts +357 -186
  173. package/src/commands/seat-mismatch-report.ts +46 -0
  174. package/src/extensions/apply-task-create.ts +27 -1
  175. package/src/extensions/contract-types.generated.ts +1 -1
  176. package/src/heartbeat/heartbeat.ts +79 -17
  177. package/src/heartbeat/templates.ts +2 -2
  178. package/src/http/agents.ts +62 -1
  179. package/src/http/api-keys.ts +140 -2
  180. package/src/http/codex-oauth-keep-warm.ts +3 -5
  181. package/src/http/index.ts +8 -0
  182. package/src/http/memory.ts +12 -5
  183. package/src/http/models-catalog.ts +357 -8
  184. package/src/http/poll.ts +54 -3
  185. package/src/http/schedules.ts +51 -6
  186. package/src/http/script-runs.ts +12 -0
  187. package/src/http/session-data.ts +2 -5
  188. package/src/http/stats.ts +6 -8
  189. package/src/http/status.ts +15 -1
  190. package/src/http/tasks.ts +90 -7
  191. package/src/http/usage-cache.ts +56 -15
  192. package/src/http/workflows.ts +46 -8
  193. package/src/mcp-client/http-client.ts +5 -0
  194. package/src/memory/recall-query.ts +74 -0
  195. package/src/prompts/base-prompt.ts +15 -4
  196. package/src/prompts/session-templates.ts +23 -1
  197. package/src/providers/acp-adapter.ts +1 -0
  198. package/src/providers/claude-adapter.ts +14 -5
  199. package/src/providers/claude-managed-adapter.ts +1 -0
  200. package/src/providers/claude-managed-models.ts +47 -161
  201. package/src/providers/claude-sdk-session.ts +3 -1
  202. package/src/providers/codex-adapter.ts +14 -14
  203. package/src/providers/codex-models.ts +65 -198
  204. package/src/providers/devin-adapter.ts +1 -0
  205. package/src/providers/dsh-adapter.ts +2 -1
  206. package/src/providers/harness-version.ts +83 -7
  207. package/src/providers/opencode-adapter.ts +53 -1
  208. package/src/providers/pi-binary-root/src/extensions/codemode/worker.ts +11 -0
  209. package/src/providers/pi-codemode-runtime.ts +17 -0
  210. package/src/providers/pi-mono-adapter.ts +361 -83
  211. package/src/providers/pi-mono-extension.ts +16 -1
  212. package/src/providers/reasoning-effort.ts +44 -140
  213. package/src/providers/types.ts +8 -0
  214. package/src/rbac/legacy-policy.ts +43 -0
  215. package/src/rbac/permissions.ts +15 -0
  216. package/src/scripts-runtime/sdk-allowlist.ts +4 -0
  217. package/src/scripts-runtime/types/stdlib.d.ts +15 -0
  218. package/src/scripts-runtime/types/swarm-extension.d.ts +3 -0
  219. package/src/scripts-runtime/types/swarm-sdk.d.ts +15 -0
  220. package/src/server.ts +6 -0
  221. package/src/slack/assistant.ts +13 -0
  222. package/src/slack/message-text.ts +56 -2
  223. package/src/slack/render-v2.ts +87 -37
  224. package/src/slack/session-status.ts +541 -0
  225. package/src/slack/thread-buffer.ts +10 -2
  226. package/src/tasks/sibling-awareness.ts +3 -2
  227. package/src/telemetry.ts +5 -4
  228. package/src/templates.d.ts +6 -0
  229. package/src/tests/agents-list-model-display.test.ts +4 -6
  230. package/src/tests/api-key-auth-failure-route.test.ts +142 -0
  231. package/src/tests/api-key-tracking.test.ts +368 -1
  232. package/src/tests/base-prompt.test.ts +43 -2
  233. package/src/tests/claude-adapter.test.ts +68 -0
  234. package/src/tests/claude-managed-adapter.test.ts +12 -18
  235. package/src/tests/codex-adapter.test.ts +2 -1
  236. package/src/tests/codex-auth-failure.test.ts +52 -0
  237. package/src/tests/codex-gpt6-release.test.ts +66 -5
  238. package/src/tests/codex-login.test.ts +116 -0
  239. package/src/tests/context-window.test.ts +7 -5
  240. package/src/tests/credential-exhaustion-policy.test.ts +118 -3
  241. package/src/tests/credential-outcome-report.test.ts +171 -0
  242. package/src/tests/credential-status-api.test.ts +103 -2
  243. package/src/tests/db-extraction-contract.test.ts +1 -0
  244. package/src/tests/db-task-read-contract.test.ts +10 -1
  245. package/src/tests/db-task-write-contract.test.ts +535 -2
  246. package/src/tests/docs-index-footprint.test.js +3 -3
  247. package/src/tests/dokploy-deploy.test.ts +315 -0
  248. package/src/tests/dsh-adapter.test.ts +1 -1
  249. package/src/tests/explicit-model-validation.test.ts +583 -0
  250. package/src/tests/extensions-pre-task.test.ts +73 -0
  251. package/src/tests/fetch-resolved-env.test.ts +46 -0
  252. package/src/tests/hang-watchdog-worker.ts +47 -0
  253. package/src/tests/hang-watchdog.test.ts +45 -0
  254. package/src/tests/hang-watchdog.ts +30 -0
  255. package/src/tests/harness-model-guard.test.ts +123 -0
  256. package/src/tests/harness-model-support.test.ts +374 -0
  257. package/src/tests/harness-version.test.ts +70 -3
  258. package/src/tests/heartbeat-checklist-config.test.ts +71 -0
  259. package/src/tests/heartbeat-checklist.test.ts +4 -2
  260. package/src/tests/heartbeat-orphan-resume.test.ts +48 -0
  261. package/src/tests/http-task-write-ownership.test.ts +172 -0
  262. package/src/tests/human-free-flag.test.ts +279 -0
  263. package/src/tests/human-free-oracle.ts +57 -0
  264. package/src/tests/human-free-reclassify.test.ts +669 -0
  265. package/src/tests/memory-fts-populate.test.ts +139 -0
  266. package/src/tests/memory-http-recall-gating.test.ts +125 -1
  267. package/src/tests/migration-046-budgets.test.ts +3 -3
  268. package/src/tests/model-catalog-migration.test.ts +134 -0
  269. package/src/tests/model-catalog-table.test.ts +249 -0
  270. package/src/tests/model-catalog-write-access.test.ts +387 -0
  271. package/src/tests/model-rate-limit-windows.test.ts +19 -0
  272. package/src/tests/model-tier-resolution.test.ts +611 -0
  273. package/src/tests/models-catalog.test.ts +1 -1
  274. package/src/tests/multi-runtime-enabled-stats.test.ts +65 -1
  275. package/src/tests/opencode-adapter.test.ts +157 -2
  276. package/src/tests/package-publish.test.ts +137 -36
  277. package/src/tests/pi-codemode.test.ts +246 -0
  278. package/src/tests/pi-mono-adapter.test.ts +3 -0
  279. package/src/tests/pi-native-mcp.test.ts +193 -0
  280. package/src/tests/pi-tool-exposure.test.ts +374 -0
  281. package/src/tests/preload.ts +3 -0
  282. package/src/tests/pricing-routes.test.ts +2 -2
  283. package/src/tests/prompt-template-session.test.ts +5 -3
  284. package/src/tests/rate-limit-event.test.ts +65 -0
  285. package/src/tests/rate-limit-outcome.test.ts +90 -0
  286. package/src/tests/rbac-engine.test.ts +77 -3
  287. package/src/tests/reasoning-effort.test.ts +109 -0
  288. package/src/tests/refresh-modelsdev-carryforward.test.ts +5 -5
  289. package/src/tests/runner-key-completion-outcome.test.ts +112 -0
  290. package/src/tests/runner-key-outcome-late-reset.test.ts +224 -0
  291. package/src/tests/runner-registration-retry.test.ts +149 -0
  292. package/src/tests/runtime-model-catalog.test.ts +86 -0
  293. package/src/tests/script-connections.test.ts +9 -0
  294. package/src/tests/seat-mismatch-report.test.ts +85 -0
  295. package/src/tests/seed-scripts.test.ts +138 -0
  296. package/src/tests/seed.test.ts +25 -0
  297. package/src/tests/session-costs.test.ts +14 -11
  298. package/src/tests/slack-message-text.test.ts +101 -0
  299. package/src/tests/slack-render-v2.test.ts +96 -0
  300. package/src/tests/slack-session-status.test.ts +626 -0
  301. package/src/tests/slack-thread-buffer-context.test.ts +93 -0
  302. package/src/tests/status.test.ts +29 -0
  303. package/src/tests/steering-promotion.test.ts +51 -0
  304. package/src/tests/store-progress-ownership.test.ts +136 -0
  305. package/src/tests/subscription-plans.test.ts +28 -0
  306. package/src/tests/task-cascade-fail.test.ts +148 -2
  307. package/src/tests/task-citations.test.ts +3 -2
  308. package/src/tests/tool-annotations.test.ts +1 -1
  309. package/src/tests/usage-cache.test.ts +166 -0
  310. package/src/tests/workflow-agent-task.test.ts +30 -0
  311. package/src/tests/workflow-executors.test.ts +5 -2
  312. package/src/tests/workflow-system-one-decision-review.test.ts +1021 -0
  313. package/src/tests/workflow-system-one-decision-save-warnings.test.ts +365 -0
  314. package/src/tests/workflow-system-one-decision.test.ts +2063 -0
  315. package/src/tests/workflow-triggers-v2.test.ts +320 -0
  316. package/src/tools/model-catalog/index.ts +137 -0
  317. package/src/tools/schedules/create-schedule.ts +17 -2
  318. package/src/tools/schedules/patch-schedule.ts +31 -1
  319. package/src/tools/schedules/update-schedule.ts +31 -1
  320. package/src/tools/send-task.ts +18 -1
  321. package/src/tools/store-progress.ts +49 -4
  322. package/src/tools/task-action.ts +14 -1
  323. package/src/tools/tool-config.ts +3 -1
  324. package/src/tools/workflows/create-workflow.ts +17 -4
  325. package/src/tools/workflows/patch-workflow-node.ts +17 -7
  326. package/src/tools/workflows/patch-workflow.ts +9 -2
  327. package/src/tools/workflows/update-workflow.ts +17 -4
  328. package/src/types.ts +41 -1
  329. package/src/utils/codex-auth-failure.ts +34 -0
  330. package/src/utils/context-window.ts +47 -53
  331. package/src/utils/credentials.ts +72 -30
  332. package/src/utils/error-tracker.ts +80 -0
  333. package/src/utils/harness-cli-version.ts +115 -0
  334. package/src/utils/harness-model-error.ts +33 -0
  335. package/src/utils/model-rate-limit-windows.ts +12 -0
  336. package/src/utils/runtime-model-catalog.ts +152 -0
  337. package/src/utils/spawn-model-guard.ts +43 -0
  338. package/src/utils/subscription-plans.ts +22 -0
  339. package/src/workflows/approval-resolution.ts +39 -0
  340. package/src/workflows/definition.ts +7 -0
  341. package/src/workflows/engine.ts +69 -1
  342. package/src/workflows/executors/agent-task.ts +16 -6
  343. package/src/workflows/executors/base.ts +53 -0
  344. package/src/workflows/executors/human-in-the-loop.ts +8 -8
  345. package/src/workflows/executors/index.ts +1 -0
  346. package/src/workflows/executors/registry.ts +2 -0
  347. package/src/workflows/executors/system-one-decision.ts +1232 -0
  348. package/src/workflows/executors/system-one-providers.ts +301 -0
  349. package/src/workflows/executors/workflow-llm.ts +13 -1
  350. package/src/workflows/model-validation.ts +45 -0
  351. package/src/workflows/patch-definition.ts +5 -0
  352. package/src/workflows/readiness.ts +86 -0
  353. package/src/workflows/recovery.ts +12 -12
  354. package/src/workflows/resume.ts +23 -9
  355. package/src/workflows/triggers.ts +85 -0
  356. package/templates/schedules/daily-blocker-digest/content.md +4 -5
  357. package/templates/skills/agent-browser/SKILL.md +6 -5
  358. package/templates/skills/agent-browser/content.md +6 -5
  359. package/templates/skills/heartbeat-runbook/config.json +1 -1
  360. package/templates/skills/heartbeat-runbook/content.md +2 -2
  361. package/templates/skills/swarm-scripts/SKILL.md +1 -1
  362. package/templates/skills/swarm-scripts/content.md +1 -1
  363. package/templates/skills/workflow-iterate/content.md +82 -1
  364. package/templates/workflows/claude-code-changelog-watch/content.md +2 -0
  365. package/templates/workflows/pr-review-status-sweep/content.md +2 -0
  366. package/dist/boot-seeds-s3q01xj3.js +0 -26
  367. package/dist/claude-sdk-session-t442qz6f.js +0 -39108
  368. package/dist/cli-0xjkj1j7.js +0 -61
  369. package/dist/cli-20cj37vt.js +0 -335561
  370. package/dist/cli-2djtr51b.js +0 -48
  371. package/dist/cli-65hjmev5.js +0 -1141
  372. package/dist/cli-aw1jwp1r.js +0 -14075
  373. package/dist/cli-h967nb6r.js +0 -6603
  374. package/dist/cli-vapyk8sw.js +0 -20
  375. package/dist/cli-ws6rqycm.js +0 -247
  376. package/dist/pricing-refresh-v1cbr9te.js +0 -27
  377. package/src/providers/modelsdev-reasoning.json +0 -6453
  378. package/dist/{index-f24115ge.js → index-2wsdsnmm.js} +3 -3
@@ -522,3 +522,49 @@ describe("memory rater config reload", () => {
522
522
  expect(process.env.MEMORY_RATERS).toBe("");
523
523
  });
524
524
  });
525
+
526
+ describe("deleting a reloadable config row", () => {
527
+ // One key per test: the runner remembers each overridden key's boot value
528
+ // for the life of the process.
529
+ const keys = ["PI_CODEMODE", "PI_TOOL_DEFERRAL", "SWARM_ORG_NAME"];
530
+ const saved = new Map(keys.map((key) => [key, process.env[key]]));
531
+ afterEach(() => {
532
+ for (const [key, value] of saved) {
533
+ if (value === undefined) delete process.env[key];
534
+ else process.env[key] = value;
535
+ }
536
+ });
537
+
538
+ async function reload(agentId: string, configs: Array<{ key: string; value: string }>) {
539
+ mockResponsesByAgentId.set(agentId, { status: 200, body: { configs } });
540
+ const result = await fetchResolvedEnv(testUrl, "key", agentId);
541
+ return applyResolvedEnvToProcessEnv(result.env);
542
+ }
543
+
544
+ test("unset at boot: deleting the row unsets the var", async () => {
545
+ delete process.env.PI_CODEMODE;
546
+ await reload("row-delete-unset", [{ key: "PI_CODEMODE", value: "true" }]);
547
+ expect(process.env.PI_CODEMODE).toBe("true");
548
+
549
+ const changed = await reload("row-delete-unset", []);
550
+ expect(changed).toContain("PI_CODEMODE");
551
+ expect(process.env.PI_CODEMODE).toBeUndefined();
552
+ });
553
+
554
+ test("set at boot: deleting the overriding row restores the boot value", async () => {
555
+ process.env.PI_TOOL_DEFERRAL = "false";
556
+ await reload("row-delete-boot", [{ key: "PI_TOOL_DEFERRAL", value: "true" }]);
557
+ expect(process.env.PI_TOOL_DEFERRAL).toBe("true");
558
+
559
+ const changed = await reload("row-delete-boot", []);
560
+ expect(changed).toContain("PI_TOOL_DEFERRAL");
561
+ expect(process.env.PI_TOOL_DEFERRAL).toBe("false");
562
+ });
563
+
564
+ test("set at boot and never overridden: untouched", async () => {
565
+ process.env.SWARM_ORG_NAME = "container-org";
566
+ const changed = await reload("row-never-set", []);
567
+ expect(changed).not.toContain("SWARM_ORG_NAME");
568
+ expect(process.env.SWARM_ORG_NAME).toBe("container-org");
569
+ });
570
+ });
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Runs on its own thread, so it keeps ticking while the test file's main thread
3
+ * is stuck in a synchronous loop. See `hang-watchdog.ts` for why it exists.
4
+ */
5
+ import { writeSync } from "node:fs";
6
+ import { workerData } from "node:worker_threads";
7
+
8
+ const { heartbeat, file, limitMs } = workerData as {
9
+ heartbeat: Int32Array;
10
+ file: string;
11
+ limitMs: number;
12
+ };
13
+
14
+ // Synchronous write: the process is SIGKILLed right after, which drops anything buffered.
15
+ const log = (message: string) => writeSync(2, `[hang-watchdog] ${message}\n`);
16
+
17
+ const POLL_MS = 1000;
18
+ const WARN_MS = 10_000;
19
+ let last = Atomics.load(heartbeat, 0);
20
+ let lastChange = Date.now();
21
+ let warned = false;
22
+
23
+ while (true) {
24
+ // Sleep on a slot nobody notifies: a timed wait that does not need this thread's event loop.
25
+ Atomics.wait(heartbeat, 1, 0, POLL_MS);
26
+ const current = Atomics.load(heartbeat, 0);
27
+ const now = Date.now();
28
+ if (current !== last) {
29
+ last = current;
30
+ lastChange = now;
31
+ warned = false;
32
+ continue;
33
+ }
34
+ const stalledMs = now - lastChange;
35
+ if (!warned && stalledMs >= WARN_MS) {
36
+ warned = true;
37
+ log(`${file}: main thread has not yielded for ${Math.round(stalledMs / 1000)}s`);
38
+ }
39
+ if (stalledMs >= limitMs) {
40
+ log(
41
+ `${file}: main thread blocked for ${Math.round(stalledMs / 1000)}s ` +
42
+ `(limit ${Math.round(limitMs / 1000)}s), killing test worker pid ${process.pid}. ` +
43
+ "A synchronous busy loop cannot be interrupted by --timeout.",
44
+ );
45
+ process.kill(process.pid, "SIGKILL");
46
+ }
47
+ }
@@ -0,0 +1,45 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { CHILD_PROCESS_TEST_BUDGET_MS, runChild } from "./test-proc";
3
+
4
+ const watchdogModule = JSON.stringify(`${import.meta.dir}/hang-watchdog`);
5
+
6
+ /** Runs `body` in a fresh `bun` process with the watchdog armed at `limitMs` (no test preload). */
7
+ function runWithWatchdog(body: string, limitMs: number) {
8
+ return runChild(
9
+ [
10
+ process.execPath,
11
+ "-e",
12
+ `import { startHangWatchdog } from ${watchdogModule}; startHangWatchdog("fixture"); ${body}`,
13
+ ],
14
+ { env: { ...process.env, TEST_HANG_WATCHDOG_MS: String(limitMs) } },
15
+ );
16
+ }
17
+
18
+ describe("startHangWatchdog", () => {
19
+ test(
20
+ "kills a process whose main thread is stuck in a synchronous loop, and only that one",
21
+ async () => {
22
+ const [spinning, yielding, disabled] = await Promise.all([
23
+ runWithWatchdog("while (true) {}", 3000),
24
+ // Yields every tick, so the heartbeat keeps moving well past the limit.
25
+ runWithWatchdog('await Bun.sleep(4500); console.log("finished");', 3000),
26
+ // 0 turns it off: a 4s synchronous spin is left alone.
27
+ runWithWatchdog(
28
+ 'const end = Date.now() + 4000; while (Date.now() < end) {} console.log("finished");',
29
+ 0,
30
+ ),
31
+ ]);
32
+
33
+ expect(spinning.signalCode).toBe("SIGKILL");
34
+ expect(spinning.stderr).toContain("[hang-watchdog] fixture: main thread blocked for");
35
+ expect(spinning.durationMs).toBeLessThan(10_000);
36
+
37
+ expect(yielding.exitCode).toBe(0);
38
+ expect(yielding.stdout).toContain("finished");
39
+
40
+ expect(disabled.exitCode).toBe(0);
41
+ expect(disabled.stdout).toContain("finished");
42
+ },
43
+ CHILD_PROCESS_TEST_BUDGET_MS,
44
+ );
45
+ });
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Fail a wedged test worker in seconds instead of at the CI job limit.
3
+ *
4
+ * `bun test --timeout` is a timer on the test file's own event loop. A synchronous
5
+ * busy loop (the intermittent `Run Tests (2/2)` hang, which spun one test worker at
6
+ * ~80% CPU with no syscalls for 20 minutes) starves that loop, so the timeout never
7
+ * fires and the job only ends at the workflow limit.
8
+ *
9
+ * The main thread bumps a shared counter every second. A second thread watches it and
10
+ * kills the process when the counter stops moving, which makes `bun test` report the
11
+ * file as failed. Tests that yield to the event loop at all never trip it.
12
+ *
13
+ * `TEST_HANG_WATCHDOG_MS=0` turns it off; any other number sets the limit (default 60s).
14
+ */
15
+ import { Worker } from "node:worker_threads";
16
+
17
+ const DEFAULT_LIMIT_MS = 60_000;
18
+
19
+ export function startHangWatchdog(file: string = Bun.main): void {
20
+ const raw = process.env.TEST_HANG_WATCHDOG_MS;
21
+ const limitMs = raw === undefined ? DEFAULT_LIMIT_MS : Number(raw);
22
+ if (!Number.isFinite(limitMs) || limitMs <= 0) return;
23
+
24
+ const heartbeat = new Int32Array(new SharedArrayBuffer(8));
25
+ const worker = new Worker(new URL("./hang-watchdog-worker.ts", import.meta.url), {
26
+ workerData: { heartbeat, file, limitMs },
27
+ });
28
+ worker.unref();
29
+ setInterval(() => Atomics.add(heartbeat, 0, 1), 1000).unref();
30
+ }
@@ -0,0 +1,123 @@
1
+ // Harness model guard: a task model from another harness family never reaches a CLI.
2
+ // Covers the heartbeat pool auto-assign filter and the worker-side spawn guard.
3
+ // Create-time and claim-time checks live in explicit-model-validation.test.ts and
4
+ // model-tier-resolution.test.ts.
5
+
6
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
7
+ import { unlink } from "node:fs/promises";
8
+ import {
9
+ closeDb,
10
+ createAgent,
11
+ createTaskExtended,
12
+ getDbClient,
13
+ getTaskById,
14
+ initDb,
15
+ } from "../be/db";
16
+ import { codeLevelTriage } from "../heartbeat/heartbeat";
17
+ import { guardSpawnModel } from "../utils/spawn-model-guard";
18
+ import "../tools/templates";
19
+
20
+ const TEST_DB_PATH = "./test-harness-model-guard.sqlite";
21
+
22
+ async function removeDbFiles(path: string): Promise<void> {
23
+ for (const suffix of ["", "-wal", "-shm"]) {
24
+ await unlink(path + suffix).catch(() => undefined);
25
+ }
26
+ }
27
+
28
+ beforeAll(async () => {
29
+ await removeDbFiles(TEST_DB_PATH);
30
+ closeDb();
31
+ initDb(TEST_DB_PATH);
32
+ });
33
+
34
+ afterAll(async () => {
35
+ closeDb();
36
+ await removeDbFiles(TEST_DB_PATH);
37
+ });
38
+
39
+ beforeEach(async () => {
40
+ await getDbClient().run("DELETE FROM agent_tasks");
41
+ await getDbClient().run("DELETE FROM agents");
42
+ });
43
+
44
+ describe("autoAssignPoolTasks harness filter", () => {
45
+ test("a claude-opus-5-5 pool task goes to the idle Claude worker, not the idle Codex worker", async () => {
46
+ // The Codex worker sorts first, so a missing filter would pick it.
47
+ const codex = await createAgent({
48
+ name: "aaa-idle-codex",
49
+ isLead: false,
50
+ status: "idle",
51
+ harnessProvider: "codex",
52
+ });
53
+ const claude = await createAgent({
54
+ name: "zzz-idle-claude",
55
+ isLead: false,
56
+ status: "idle",
57
+ harnessProvider: "claude",
58
+ });
59
+ const task = await createTaskExtended("pool work", { model: "claude-opus-5-5" });
60
+
61
+ const findings = await codeLevelTriage();
62
+
63
+ expect(findings.autoAssigned).toEqual([{ taskId: task.id, agentId: claude.id }]);
64
+ expect((await getTaskById(task.id))?.agentId).not.toBe(codex.id);
65
+ });
66
+
67
+ test("with only an idle Codex worker the task stays queued", async () => {
68
+ await createAgent({
69
+ name: "only-codex",
70
+ isLead: false,
71
+ status: "idle",
72
+ harnessProvider: "codex",
73
+ });
74
+ const task = await createTaskExtended("pool work", { model: "claude-opus-5-5" });
75
+
76
+ const findings = await codeLevelTriage();
77
+
78
+ expect(findings.autoAssigned).toHaveLength(0);
79
+ expect((await getTaskById(task.id))?.status).toBe("unassigned");
80
+ });
81
+ });
82
+
83
+ describe("guardSpawnModel (worker spawn)", () => {
84
+ test("a MODEL_OVERRIDE from another harness family falls back to the adapter default with a warning", () => {
85
+ const decision = guardSpawnModel({
86
+ taskModel: "",
87
+ configModel: "gpt-6-luna",
88
+ harness: "claude",
89
+ role: "worker",
90
+ });
91
+ expect(decision).toEqual({
92
+ kind: "ok",
93
+ model: "",
94
+ warning:
95
+ "[worker] MODEL_OVERRIDE gpt-6-luna does not run on the claude harness; using the adapter default",
96
+ });
97
+ });
98
+
99
+ test("a compatible MODEL_OVERRIDE, a shortname, and an unpinned harness pass through", () => {
100
+ expect(
101
+ guardSpawnModel({ taskModel: "", configModel: "gpt-6-luna", harness: "codex", role: "w" }),
102
+ ).toEqual({ kind: "ok", model: "gpt-6-luna" });
103
+ expect(
104
+ guardSpawnModel({ taskModel: "", configModel: "opus", harness: "codex", role: "w" }),
105
+ ).toEqual({ kind: "ok", model: "opus" });
106
+ expect(
107
+ guardSpawnModel({ taskModel: "claude-opus-5-5", configModel: "", harness: "pi", role: "w" }),
108
+ ).toEqual({ kind: "ok", model: "claude-opus-5-5" });
109
+ });
110
+
111
+ test("a task model from another harness family is a mismatch, so the runner never spawns", () => {
112
+ const decision = guardSpawnModel({
113
+ taskModel: "claude-opus-5-5",
114
+ configModel: "gpt-6-luna",
115
+ harness: "codex",
116
+ role: "worker",
117
+ });
118
+ expect(decision.kind).toBe("mismatch");
119
+ expect(decision.kind === "mismatch" && decision.reason).toBe(
120
+ '[model-harness-mismatch] Model "claude-opus-5-5" does not run on the codex harness of this worker. The task pinned a model from another harness family. Re-create the task with modelTier or with a codex model.',
121
+ );
122
+ });
123
+ });
@@ -0,0 +1,374 @@
1
+ // Model-catalog phase 4: harness CLI model support, CLI-unsupported fallback
2
+ // and fail-fast at claim, and the worker-side outcome reporter.
3
+
4
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
5
+ import { unlink } from "node:fs/promises";
6
+ import { closeDb, createAgent, createTaskExtended, getDbClient, initDb } from "../be/db";
7
+ import {
8
+ getHarnessModelSupport,
9
+ recordHarnessModelSupport,
10
+ setAgentHarnessCliVersion,
11
+ } from "../be/harness-model-support";
12
+ import { type ModelCatalogEntry, replaceModelCatalog } from "../be/model-catalog-store";
13
+ import { invalidateTierResolutionCatalog } from "../be/model-tier-resolution";
14
+ import { handlePoll } from "../http/poll";
15
+ import {
16
+ parseCliVersion,
17
+ reportHarnessModelOutcome,
18
+ resetHarnessCliVersionForTests,
19
+ } from "../utils/harness-cli-version";
20
+ import { isUnknownModelError } from "../utils/harness-model-error";
21
+
22
+ const TEST_DB_PATH = "./test-harness-model-support.sqlite";
23
+ const DAY = 24 * 60 * 60 * 1000;
24
+ const CLI = "2.1.283";
25
+
26
+ async function removeDbFiles(path: string): Promise<void> {
27
+ for (const suffix of ["", "-wal", "-shm"]) {
28
+ try {
29
+ await unlink(path + suffix);
30
+ } catch (error) {
31
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
32
+ }
33
+ }
34
+ }
35
+
36
+ function entry(modelId: string, daysOld: number): ModelCatalogEntry {
37
+ return {
38
+ provider: "anthropic",
39
+ modelId,
40
+ releaseDate: new Date(Date.now() - daysOld * DAY).toISOString().slice(0, 10),
41
+ pricing: { input: 5, output: 25 },
42
+ checkedAt: Date.now(),
43
+ };
44
+ }
45
+
46
+ type Trigger = { type: string; taskId: string; task: Record<string, unknown> };
47
+
48
+ async function callPoll(agentId: string): Promise<Trigger | null> {
49
+ let bodyStr = "";
50
+ const req = {
51
+ method: "GET",
52
+ url: "/api/poll",
53
+ headers: { "x-agent-id": agentId },
54
+ } as unknown as Parameters<typeof handlePoll>[0];
55
+ const res = {
56
+ setHeader() {},
57
+ writeHead() {},
58
+ end(body?: string) {
59
+ bodyStr = body ?? "";
60
+ },
61
+ } as unknown as Parameters<typeof handlePoll>[1];
62
+ await handlePoll(req, res, ["api", "poll"], new URLSearchParams(), agentId);
63
+ return (JSON.parse(bodyStr) as { trigger: Trigger | null }).trigger;
64
+ }
65
+
66
+ async function worker(name: string) {
67
+ const agent = await createAgent({
68
+ name,
69
+ isLead: false,
70
+ status: "idle",
71
+ maxTasks: 1,
72
+ harnessProvider: "claude",
73
+ });
74
+ await setAgentHarnessCliVersion(agent.id, CLI);
75
+ return agent;
76
+ }
77
+
78
+ beforeAll(async () => {
79
+ await removeDbFiles(TEST_DB_PATH);
80
+ initDb(TEST_DB_PATH);
81
+ });
82
+
83
+ afterAll(async () => {
84
+ closeDb();
85
+ await removeDbFiles(TEST_DB_PATH);
86
+ });
87
+
88
+ beforeEach(async () => {
89
+ const client = getDbClient();
90
+ await client.run("DELETE FROM agent_tasks");
91
+ await client.run("DELETE FROM agents");
92
+ await client.run("DELETE FROM harness_model_support");
93
+ await client.run("DELETE FROM model_alias_resolutions");
94
+ await replaceModelCatalog([
95
+ entry("claude-opus-5", 200),
96
+ entry("claude-opus-5-5", 30),
97
+ entry("claude-sonnet-5-5", 10),
98
+ ]);
99
+ invalidateTierResolutionCatalog();
100
+ delete process.env.MODEL_TIER_CLAUDE_SMART;
101
+ });
102
+
103
+ describe("claim-time CLI support", () => {
104
+ test("alias resolving to an unsupported model falls back within the family", async () => {
105
+ process.env.MODEL_TIER_CLAUDE_SMART = "latest:anthropic/opus@stable";
106
+ await recordHarnessModelSupport({
107
+ harness: "claude",
108
+ cliVersion: CLI,
109
+ modelId: "claude-opus-5-5",
110
+ status: "unsupported",
111
+ error: "There's an issue with the selected model (claude-opus-5-5).",
112
+ });
113
+ const w = await worker("w-fallback");
114
+ const task = await createTaskExtended("smart work", { agentId: w.id, modelTier: "smart" });
115
+
116
+ const trigger = await callPoll(w.id);
117
+ expect(trigger?.task.resolvedModel).toBe("claude-opus-5");
118
+ expect(trigger?.task.modelSource).toBe("fallback:cli-unsupported");
119
+ const row = await getDbClient().get<{ resolvedModel: string; modelSource: string }>(
120
+ "SELECT resolvedModel, modelSource FROM agent_tasks WHERE id = ?",
121
+ [task.id],
122
+ );
123
+ expect(row).toEqual({
124
+ resolvedModel: "claude-opus-5",
125
+ modelSource: "fallback:cli-unsupported",
126
+ });
127
+ delete process.env.MODEL_TIER_CLAUDE_SMART;
128
+ });
129
+
130
+ test("the fallback never picks an unpriced sibling, which would run outside cost accounting", async () => {
131
+ process.env.MODEL_TIER_CLAUDE_SMART = "latest:anthropic/opus@stable";
132
+ // Newer than the priced fallback (claude-opus-5) but with no price yet.
133
+ await replaceModelCatalog([
134
+ entry("claude-opus-5", 200),
135
+ entry("claude-opus-5-5", 30),
136
+ { ...entry("claude-opus-5-4", 60), pricing: null },
137
+ ]);
138
+ invalidateTierResolutionCatalog();
139
+ await recordHarnessModelSupport({
140
+ harness: "claude",
141
+ cliVersion: CLI,
142
+ modelId: "claude-opus-5-5",
143
+ status: "unsupported",
144
+ });
145
+ const w = await worker("w-unpriced-fallback");
146
+ await createTaskExtended("smart work", { agentId: w.id, modelTier: "smart" });
147
+
148
+ const trigger = await callPoll(w.id);
149
+ expect(trigger?.task.resolvedModel).toBe("claude-opus-5");
150
+ expect(trigger?.task.modelSource).toBe("fallback:cli-unsupported");
151
+ delete process.env.MODEL_TIER_CLAUDE_SMART;
152
+ });
153
+
154
+ test("explicit unsupported model fails fast: trigger carries modelUnsupported", async () => {
155
+ await recordHarnessModelSupport({
156
+ harness: "claude",
157
+ cliVersion: CLI,
158
+ modelId: "claude-opus-5-5",
159
+ status: "unsupported",
160
+ });
161
+ const w = await worker("w-explicit");
162
+ await createTaskExtended("pinned work", { agentId: w.id, model: "claude-opus-5-5" });
163
+ const trigger = await callPoll(w.id);
164
+ expect(String(trigger?.task.modelUnsupported)).toContain("claude-opus-5-5");
165
+ expect(trigger?.task.resolvedModel).toBeUndefined();
166
+ });
167
+
168
+ test("an explicit latest: alias on an unsupported model falls back, like a tier alias", async () => {
169
+ await recordHarnessModelSupport({
170
+ harness: "claude",
171
+ cliVersion: CLI,
172
+ modelId: "claude-opus-5-5",
173
+ status: "unsupported",
174
+ });
175
+ const w = await worker("w-explicit-alias");
176
+ const task = await createTaskExtended("aliased work", {
177
+ agentId: w.id,
178
+ model: "latest:anthropic/opus",
179
+ });
180
+ const trigger = await callPoll(w.id);
181
+ expect(trigger?.task.modelUnsupported).toBeUndefined();
182
+ expect(trigger?.task.resolvedModel).toBe("claude-opus-5");
183
+ expect(trigger?.task.modelSource).toBe("fallback:cli-unsupported");
184
+ // The alias the task named is still on the record.
185
+ expect(trigger?.task.modelAlias).toBe("latest:anthropic/opus");
186
+ const row = await getDbClient().get<{ modelAlias: string }>(
187
+ "SELECT modelAlias FROM agent_tasks WHERE id = ?",
188
+ [task.id],
189
+ );
190
+ expect(row?.modelAlias).toBe("latest:anthropic/opus");
191
+ });
192
+
193
+ test("an alias with no usable sibling keeps its resolution instead of failing the task", async () => {
194
+ for (const modelId of ["claude-opus-5-5", "claude-opus-5"]) {
195
+ await recordHarnessModelSupport({
196
+ harness: "claude",
197
+ cliVersion: CLI,
198
+ modelId,
199
+ status: "unsupported",
200
+ });
201
+ }
202
+ const w = await worker("w-no-sibling");
203
+ await createTaskExtended("aliased work", { agentId: w.id, model: "latest:anthropic/opus" });
204
+ const trigger = await callPoll(w.id);
205
+ expect(trigger?.task.modelUnsupported).toBeUndefined();
206
+ expect(trigger?.task.resolvedModel).toBe("claude-opus-5-5");
207
+ expect(trigger?.task.modelSource).toBe("model");
208
+ });
209
+
210
+ test("a tier default (CLI shortname) the CLI rejected falls back within its family", async () => {
211
+ await recordHarnessModelSupport({
212
+ harness: "claude",
213
+ cliVersion: CLI,
214
+ modelId: "opus",
215
+ status: "unsupported",
216
+ });
217
+ const w = await worker("w-tier-default");
218
+ await createTaskExtended("smart work", { agentId: w.id, modelTier: "smart" });
219
+ const trigger = await callPoll(w.id);
220
+ expect(trigger?.task.modelUnsupported).toBeUndefined();
221
+ expect(trigger?.task.resolvedModel).toBe("claude-opus-5-5");
222
+ expect(trigger?.task.modelSource).toBe("fallback:cli-unsupported");
223
+ });
224
+
225
+ test("a tier default shortname stays when only a catalog id was rejected: the CLI resolves it", async () => {
226
+ await recordHarnessModelSupport({
227
+ harness: "claude",
228
+ cliVersion: CLI,
229
+ modelId: "claude-opus-5-5",
230
+ status: "unsupported",
231
+ });
232
+ const w = await worker("w-shortname-ok");
233
+ await createTaskExtended("smart work", { agentId: w.id, modelTier: "smart" });
234
+ const trigger = await callPoll(w.id);
235
+ expect(trigger?.task.resolvedModel).toBe("opus");
236
+ expect(trigger?.task.modelSource).toBe("tier-default");
237
+ });
238
+
239
+ test("a concrete id from a tier config value falls back; only a pinned task model fails fast", async () => {
240
+ process.env.MODEL_TIER_CLAUDE_SMART = "claude-opus-5-5";
241
+ await recordHarnessModelSupport({
242
+ harness: "claude",
243
+ cliVersion: CLI,
244
+ modelId: "claude-opus-5-5",
245
+ status: "unsupported",
246
+ });
247
+ const w = await worker("w-tier-config");
248
+ await createTaskExtended("smart work", { agentId: w.id, modelTier: "smart" });
249
+ const trigger = await callPoll(w.id);
250
+ expect(trigger?.task.resolvedModel).toBe("claude-opus-5");
251
+ expect(trigger?.task.modelSource).toBe("fallback:cli-unsupported");
252
+ delete process.env.MODEL_TIER_CLAUDE_SMART;
253
+ });
254
+
255
+ test("unknown support (no row) is allowed; other CLI versions are unaffected", async () => {
256
+ await recordHarnessModelSupport({
257
+ harness: "claude",
258
+ cliVersion: "2.0.0",
259
+ modelId: "claude-opus-5-5",
260
+ status: "unsupported",
261
+ });
262
+ const w = await worker("w-unknown");
263
+ await createTaskExtended("pinned work", { agentId: w.id, model: "claude-opus-5-5" });
264
+ const trigger = await callPoll(w.id);
265
+ expect(trigger?.task.resolvedModel).toBe("claude-opus-5-5");
266
+ expect(trigger?.task.modelUnsupported).toBeUndefined();
267
+ });
268
+
269
+ test("an ok row is never downgraded to unknown", async () => {
270
+ const base = { harness: "claude", cliVersion: CLI, modelId: "claude-opus-5-5" };
271
+ await recordHarnessModelSupport({ ...base, status: "ok" });
272
+ await recordHarnessModelSupport({ ...base, status: "unknown" });
273
+ expect(await getHarnessModelSupport("claude", CLI, "claude-opus-5-5")).toBe("ok");
274
+ });
275
+ });
276
+
277
+ describe("worker outcome reporting", () => {
278
+ test("parses CLI versions and recognizes unknown-model errors", () => {
279
+ expect(parseCliVersion("2.1.283 (Claude Code)")).toBe("2.1.283");
280
+ expect(parseCliVersion("codex-cli 0.157.1")).toBe("0.157.1");
281
+ expect(isUnknownModelError("There's an issue with the selected model (claude-x).")).toBe(true);
282
+ expect(isUnknownModelError("The model `gpt-9` does not exist or you do not have access")).toBe(
283
+ true,
284
+ );
285
+ expect(isUnknownModelError("rate limit reached")).toBe(false);
286
+ });
287
+
288
+ test("with a model id, only a rejection that names that model counts", () => {
289
+ const rejection = "There's an issue with the selected model (claude-x). It may not exist.";
290
+ expect(isUnknownModelError(rejection, "claude-x")).toBe(true);
291
+ expect(isUnknownModelError(rejection, "CLAUDE-X")).toBe(true);
292
+ expect(isUnknownModelError("The model `gpt-9` does not exist", "gpt-9")).toBe(true);
293
+ expect(isUnknownModelError("unknown model 'gpt-9'\nstack trace", "gpt-9")).toBe(true);
294
+ // Rejection text for another model, or phrase-only text from an unrelated failure.
295
+ expect(isUnknownModelError(rejection, "claude-y")).toBe(false);
296
+ expect(isUnknownModelError("tool call failed: invalid model output", "claude-x")).toBe(false);
297
+ expect(isUnknownModelError("unknown model type in schema\nran claude-x fine", "claude-x")).toBe(
298
+ false,
299
+ );
300
+ });
301
+
302
+ test("an unrelated failure containing a rejection phrase does not report unsupported", async () => {
303
+ resetHarnessCliVersionForTests({ claude: CLI });
304
+ const bodies: unknown[] = [];
305
+ const fetchImpl = (async (_url: string, init: RequestInit) => {
306
+ bodies.push(JSON.parse(String(init.body)));
307
+ return new Response("{}", { status: 200 });
308
+ }) as unknown as typeof fetch;
309
+
310
+ await reportHarnessModelOutcome({
311
+ apiUrl: "http://x",
312
+ agentId: "a",
313
+ harness: "claude",
314
+ fetchImpl,
315
+ model: "m1",
316
+ exitCode: 1,
317
+ failureReason: "Tool call failed: invalid model output from the summarizer",
318
+ });
319
+
320
+ expect(bodies).toEqual([]);
321
+ resetHarnessCliVersionForTests();
322
+ });
323
+
324
+ test("sends unsupported on a model rejection, ok once on success, nothing on other failures", async () => {
325
+ resetHarnessCliVersionForTests({ claude: CLI });
326
+ const bodies: unknown[] = [];
327
+ const fetchImpl = (async (_url: string, init: RequestInit) => {
328
+ bodies.push(JSON.parse(String(init.body)));
329
+ return new Response("{}", { status: 200 });
330
+ }) as unknown as typeof fetch;
331
+ const base = { apiUrl: "http://x", agentId: "a", harness: "claude", fetchImpl };
332
+
333
+ await reportHarnessModelOutcome({ ...base, model: "m1", exitCode: 1, failureReason: "boom" });
334
+ await reportHarnessModelOutcome({
335
+ ...base,
336
+ model: "m1",
337
+ exitCode: 1,
338
+ failureReason: "There's an issue with the selected model (m1).",
339
+ });
340
+ await reportHarnessModelOutcome({ ...base, model: "m2", exitCode: 0 });
341
+ await reportHarnessModelOutcome({ ...base, model: "m2", exitCode: 0 });
342
+
343
+ expect(bodies).toMatchObject([
344
+ { modelId: "m1", status: "unsupported", cliVersion: CLI },
345
+ { modelId: "m2", status: "ok" },
346
+ ]);
347
+ resetHarnessCliVersionForTests();
348
+ });
349
+
350
+ test("scrubs secrets from the failure text before it leaves the worker", async () => {
351
+ resetHarnessCliVersionForTests({ claude: CLI });
352
+ const token = "ghp_abcdefghijklmnopqrstuvwxyz0123456789";
353
+ const bodies: Array<{ error?: string }> = [];
354
+ const fetchImpl = (async (_url: string, init: RequestInit) => {
355
+ bodies.push(JSON.parse(String(init.body)));
356
+ return new Response("{}", { status: 200 });
357
+ }) as unknown as typeof fetch;
358
+
359
+ await reportHarnessModelOutcome({
360
+ apiUrl: "http://x",
361
+ agentId: "a",
362
+ harness: "claude",
363
+ fetchImpl,
364
+ model: "m1",
365
+ exitCode: 1,
366
+ failureReason: `There's an issue with the selected model (m1). token=${token}`,
367
+ });
368
+
369
+ expect(bodies).toHaveLength(1);
370
+ expect(bodies[0]?.error).toContain("selected model");
371
+ expect(bodies[0]?.error).not.toContain(token);
372
+ resetHarnessCliVersionForTests();
373
+ });
374
+ });