@humwick/server 0.130.0 → 0.130.2

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 (486) hide show
  1. package/README.md +5 -0
  2. package/docs/guides/tools-administration.md +6 -1
  3. package/docs/guides/tools-coordination.md +1 -0
  4. package/docs/guides/tools-tasks.md +8 -0
  5. package/docs/guides/workflows.md +58 -0
  6. package/docs/openapi.json +414 -1
  7. package/package.json +8 -17
  8. package/public/assets/{MessageBubble-BWpoBjl4.js → MessageBubble-BWLqMGKG.js} +2 -2
  9. package/public/assets/{PlanGraphPanel-C0auck3n.js → PlanGraphPanel-3TlIQpjk.js} +2 -2
  10. package/public/assets/{TraceView-DBnOx7AQ.js → TraceView-Wimm0Czv.js} +2 -2
  11. package/public/assets/{VaultDecisionCard-0kp2oNBC.js → VaultDecisionCard-Bau_vNax.js} +2 -2
  12. package/public/assets/{activity-yLztXEBt.js → activity-CrSurn-r.js} +2 -2
  13. package/public/assets/{agent-lineage-BbJDR1dW.js → agent-lineage-CknAJKcb.js} +2 -2
  14. package/public/assets/agent-liveness-CaNwK0Zf.js +2 -0
  15. package/public/assets/{agent-name-CTZBrSD9.js → agent-name-DKbiIhBo.js} +2 -2
  16. package/public/assets/agent-profiles-DwF6EQ4R.js +2 -0
  17. package/public/assets/{agent-project-badge-9TqzUUYE.js → agent-project-badge-Diz4Sjjb.js} +2 -2
  18. package/public/assets/{agent-tools-D4n40pn8.js → agent-tools-CRhpRQza.js} +2 -2
  19. package/public/assets/{agent-type-icon-D6FUarOf.js → agent-type-icon-LrUEU3lB.js} +2 -2
  20. package/public/assets/{agents-BYDslkJc.js → agents-B-v66C-4.js} +2 -2
  21. package/public/assets/{analytics-B-XSbEMC.js → analytics-BqCb-LKi.js} +2 -2
  22. package/public/assets/{artifact-viewer-DdWIfV6M.js → artifact-viewer-kf4pGuAR.js} +2 -2
  23. package/public/assets/{artifacts-oBOqks-2.js → artifacts-D32zyw4Y.js} +2 -2
  24. package/public/assets/{artifacts-DPN1q4pA.js → artifacts-DJevXcvg.js} +2 -2
  25. package/public/assets/{automation-4KcRFXEu.js → automation-BpRajx_5.js} +2 -2
  26. package/public/assets/{backlog-Cw9xPTCF.js → backlog-D_UjTdzX.js} +2 -2
  27. package/public/assets/{busy-health-CTu32MZt.js → busy-health-N3VEh4ao.js} +2 -2
  28. package/public/assets/{channels-BiXzg1G9.js → channels-9jopungq.js} +2 -2
  29. package/public/assets/{chat-Ctk2qwvA.js → chat-CSXrTB3C.js} +2 -2
  30. package/public/assets/{code-preview-Bm1wWqAp.js → code-preview-p1NnmqK0.js} +2 -2
  31. package/public/assets/{connectors-C36ax4Vw.js → connectors-CZdqlDNL.js} +2 -2
  32. package/public/assets/{cosmos-Jj9LK2Wf.js → cosmos-BgdsE4dG.js} +2 -2
  33. package/public/assets/{crate-agent-H6S7-B9T.js → crate-agent-30aXTKva.js} +2 -2
  34. package/public/assets/{dependency-graph-Cd1E23wa.js → dependency-graph-B_jvw2GE.js} +2 -2
  35. package/public/assets/{deploys-Bwfq146q.js → deploys-DoN6bAb5.js} +2 -2
  36. package/public/assets/{display-CfO7rTuF.js → display-BrKWFGJz.js} +2 -2
  37. package/public/assets/{files-DhhZI2Ih.js → files-D6XEu8Ng.js} +2 -2
  38. package/public/assets/fleet-C7jQWbw2.js +2 -0
  39. package/public/assets/{fleet-metrics-BkHP-P3R.js → fleet-metrics-BiDs3MLx.js} +2 -2
  40. package/public/assets/{formatted-body-Cm1RrzmN.js → formatted-body-CQBwc1fb.js} +3 -3
  41. package/public/assets/{formatted-body-impl-DQiOuOD1.js → formatted-body-impl-DyG0WsQS.js} +2 -2
  42. package/public/assets/{host-providers-dialog-CHqwXTov.js → host-providers-dialog-DKaW31dN.js} +2 -2
  43. package/public/assets/index-CRHep3M9.js +15 -0
  44. package/public/assets/index-MYAIwYTX.css +2 -0
  45. package/public/assets/{insights-fH1EL-qZ.js → insights-CzrtkpOE.js} +2 -2
  46. package/public/assets/{integrations-C5Oix6ng.js → integrations-aVsUmigt.js} +2 -2
  47. package/public/assets/{issue-ref-chip-ODyondIa.js → issue-ref-chip-BnSlM_95.js} +2 -2
  48. package/public/assets/{log-viewer-Cvfy6PW7.js → log-viewer-qe7Ee47Z.js} +2 -2
  49. package/public/assets/{maintenance-D8CDH5wZ.js → maintenance-NfjqFZAk.js} +2 -2
  50. package/public/assets/{managed-agents-egllbpij.js → managed-agents-BiH0HgaE.js} +2 -2
  51. package/public/assets/{markdown-preview-CYSFgVgK.js → markdown-preview-BixaT3Xr.js} +3 -3
  52. package/public/assets/{markdown-preview-impl-BteVIjX4.js → markdown-preview-impl-DNGlWIS3.js} +2 -2
  53. package/public/assets/{memory-C8WmXsyr.js → memory-DNsCl6UX.js} +2 -2
  54. package/public/assets/{messages-BzQdqlO6.js → messages-DopU1L1X.js} +2 -2
  55. package/public/assets/{orchestrators-B1HxzkNw.js → orchestrators-Cq7ckLQT.js} +2 -2
  56. package/public/assets/{overview-DCUnI4Z7.js → overview-C1DCTzzV.js} +2 -2
  57. package/public/assets/{pairs-ftCd2XcD.js → pairs-DE_m6rrE.js} +2 -2
  58. package/public/assets/{profile-icon-B2zH8DMH.js → profile-icon-B3rfh77F.js} +2 -2
  59. package/public/assets/project-profile-instructions-DQWZE0kA.js +2 -0
  60. package/public/assets/projects-Br2Bd2h1.js +2 -0
  61. package/public/assets/{prompt-settings-Bftq9czX.js → prompt-settings-BKvFsvGD.js} +2 -2
  62. package/public/assets/provider-display-DsuWMvVL.js +2 -0
  63. package/public/assets/{registry-DWCU5c8q.js → registry-Cw38_6gW.js} +2 -2
  64. package/public/assets/run-card-CpKqRbhg.js +2 -0
  65. package/public/assets/scroll-text-BKXS1ahr.js +2 -0
  66. package/public/assets/{secrets-C7GIUATz.js → secrets-DxI-vzSq.js} +2 -2
  67. package/public/assets/{service-access-settings-C3aujqTG.js → service-access-settings-D9s_VJbd.js} +2 -2
  68. package/public/assets/{settings-C6sZ9eBa.js → settings-CKZTVGIx.js} +2 -2
  69. package/public/assets/{shipping-Bu8p1gEw.js → shipping-B9YtnwtG.js} +2 -2
  70. package/public/assets/{stats-V9Z-GxUd.js → stats-DyUBKyWI.js} +2 -2
  71. package/public/assets/{status-dot-CVv0iGHL.js → status-dot-DYqlEUhJ.js} +2 -2
  72. package/public/assets/{store-BvtFdlGX.js → store-DcClXLH0.js} +3 -3
  73. package/public/assets/{tasks-CXU75HRF.js → tasks-Bh7Chr4-.js} +2 -2
  74. package/public/assets/{teams-BPf976zb.js → teams-bowI7qVY.js} +2 -2
  75. package/public/assets/{terminal-viewer-DE9BOe-8.js → terminal-viewer-BgVLHdsU.js} +3 -3
  76. package/public/assets/{terminal-viewer-impl-D04pjmGh.js → terminal-viewer-impl-Dz22UPy6.js} +2 -2
  77. package/public/assets/token-estimate-Co-Jdphh.js +2 -0
  78. package/public/assets/{use-agent-terminal-B1yR456R.js → use-agent-terminal-DSQQ-iY7.js} +2 -2
  79. package/public/assets/{use-decisions-D-S4tpDf.js → use-decisions-samMQhQf.js} +2 -2
  80. package/public/assets/use-project-list-59LheVP7.js +2 -0
  81. package/public/assets/{use-selectors-CnrAk2YP.js → use-selectors-BZa25xEB.js} +2 -2
  82. package/public/assets/{user-avatar-nBFIcR4M.js → user-avatar-DHxlvUvV.js} +2 -2
  83. package/public/assets/versioned-prompt-editor-BmED5DnJ.js +8 -0
  84. package/public/assets/{work-queue-TCjJxeT-.js → work-queue-CSvpuNLz.js} +2 -2
  85. package/public/assets/{workflow-marker-Cf3Hf2fN.js → workflow-marker-DAZcj5qt.js} +2 -2
  86. package/public/assets/{workflows-CXQX20Qj.js → workflows-DdmxRSyg.js} +2 -2
  87. package/public/assets/{workspaces-qSYzKYMA.js → workspaces-BxeKfVuF.js} +2 -2
  88. package/public/cosmos.html +4 -4
  89. package/public/index.html +31 -29
  90. package/runner/plugins/claude/.claude-plugin/plugin.json +1 -1
  91. package/runner/plugins/claude/hooks/gate-run-guard.provisioned.mjs +31 -1
  92. package/runner/src/adapters/claude-effort.ts +2 -4
  93. package/runner/src/adapters/codex-launch-config.ts +4 -5
  94. package/runner/src/adapters/codex.ts +1 -1
  95. package/runner/src/adapters/opencode-provider.ts +7 -22
  96. package/runner/src/adapters/opencode.ts +4 -4
  97. package/runner/src/agent-runner/credentials.ts +42 -12
  98. package/runner/src/agent-runner/inbox.ts +27 -0
  99. package/runner/src/agent-runner/lifecycle-milestones.ts +0 -2
  100. package/runner/src/bin/claude.ts +4 -0
  101. package/runner/src/bin/codex.ts +4 -0
  102. package/runner/src/bin/opencode.ts +4 -0
  103. package/runner/src/index.ts +5 -2
  104. package/runner/src/provider-setup.ts +34 -0
  105. package/runner/src/providers.ts +2 -1
  106. package/runner/src/runner-core.ts +18 -23
  107. package/runner/src/spawn-input.ts +2 -1
  108. package/runner/src/test-run-dedup.ts +59 -56
  109. package/scripts/cd-config.ts +24 -3
  110. package/scripts/gate-run-lease.ts +10 -7
  111. package/scripts/gate.sh +5 -1
  112. package/src/branch-land-notice.ts +93 -0
  113. package/src/branch-landed.ts +7 -76
  114. package/src/bus.ts +19 -20
  115. package/src/cd-deploy-notify.ts +30 -2
  116. package/src/cd-host-lag-notify.ts +7 -6
  117. package/src/ci/incidents.ts +2 -2
  118. package/src/ci/monitor.ts +5 -8
  119. package/src/ci/root-missing.ts +2 -7
  120. package/src/ci/transition-notify.ts +7 -39
  121. package/src/cli/cd-pauses.ts +19 -0
  122. package/src/cli/orchestrator.ts +1 -1
  123. package/src/cli/state.ts +2 -19
  124. package/src/cli/task-edit.ts +22 -0
  125. package/src/cli/task.ts +3 -1
  126. package/src/cli/workflow.ts +8 -2
  127. package/src/commands-db.ts +10 -6
  128. package/src/commands-write.ts +8 -0
  129. package/src/config-store-write.ts +13 -1
  130. package/src/config-store.ts +25 -14
  131. package/src/db/activity-async.ts +6 -1
  132. package/src/db/activity-write-ops.ts +13 -1
  133. package/src/db/activity.ts +1 -14
  134. package/src/db/agent-lifecycle-write-ops.ts +1 -0
  135. package/src/db/agent-meta.ts +2 -2
  136. package/src/db/agent-ownership-write-ops.ts +4 -0
  137. package/src/db/agent-search.ts +1 -15
  138. package/src/db/agents-async.ts +58 -7
  139. package/src/db/agents-write-ops.ts +28 -5
  140. package/src/db/agents.ts +81 -54
  141. package/src/db/artifacts-write-ops.ts +8 -3
  142. package/src/db/bus-outbox-write-ops.ts +1 -0
  143. package/src/db/channels-write-ops.ts +4 -0
  144. package/src/db/ci-incidents-async.ts +5 -5
  145. package/src/db/ci-incidents-write-ops.ts +2 -2
  146. package/src/db/claude-api-budget-ops.ts +5 -5
  147. package/src/db/claude-app-oauth.ts +4 -4
  148. package/src/db/codex-reset-consumes-write-ops.ts +3 -0
  149. package/src/db/command-row.ts +10 -2
  150. package/src/db/commands-list-query.ts +2 -0
  151. package/src/db/commands-spawn-read.ts +1 -1
  152. package/src/db/commands-write-ops.ts +21 -0
  153. package/src/db/config-store-write-ops.ts +9 -2
  154. package/src/db/continuation-archives-async.ts +5 -0
  155. package/src/db/continuation-archives-write-ops.ts +9 -0
  156. package/src/db/continuations.ts +2 -2
  157. package/src/db/db-writer-worker.ts +8 -1
  158. package/src/db/delivery-write-ops.ts +5 -0
  159. package/src/db/deploy-events-async.ts +1 -0
  160. package/src/db/deploy-events-read-ops.ts +2 -1
  161. package/src/db/deploy-events.ts +35 -4
  162. package/src/db/hosts-registration.ts +10 -2
  163. package/src/db/hosts-write-ops.ts +14 -0
  164. package/src/db/hosts.ts +9 -6
  165. package/src/db/inbox-write-ops.ts +4 -0
  166. package/src/db/inbox.ts +0 -14
  167. package/src/db/index.ts +2 -0
  168. package/src/db/insights-write-ops.ts +1 -0
  169. package/src/db/integrations-async.ts +8 -0
  170. package/src/db/integrations-ops.ts +15 -0
  171. package/src/db/integrations.ts +15 -21
  172. package/src/db/issue-lifecycle.ts +3 -4
  173. package/src/db/issue-refs-async.ts +20 -0
  174. package/src/db/issue-refs-write-ops.ts +29 -0
  175. package/src/db/issue-refs.ts +7 -8
  176. package/src/db/long-running-work.ts +2 -2
  177. package/src/db/macos-privacy.ts +1 -1
  178. package/src/db/main-connection-writes.ts +23 -2
  179. package/src/db/maintenance-job-reconcile.ts +61 -0
  180. package/src/db/maintenance-job-runs-async.ts +14 -0
  181. package/src/db/maintenance-job-write-ops.ts +9 -0
  182. package/src/db/mappers.ts +2 -14
  183. package/src/db/merge-lease.ts +1 -15
  184. package/src/db/message-reads-write-ops.ts +3 -0
  185. package/src/db/message-write-ops.ts +4 -0
  186. package/src/db/migrations.ts +7 -15
  187. package/src/db/orchestrators-async.ts +7 -3
  188. package/src/db/orchestrators-write-ops.ts +7 -1
  189. package/src/db/orchestrators.ts +21 -77
  190. package/src/db/pairs.ts +1 -13
  191. package/src/db/project-profile-instructions.ts +50 -0
  192. package/src/db/prompt-versions-ops.ts +1 -0
  193. package/src/db/prompt-versions.ts +9 -2
  194. package/src/db/provider-catalog-write-ops.ts +4 -4
  195. package/src/db/provider-config-history.ts +8 -3
  196. package/src/db/provider-flap-messages-write-ops.ts +1 -0
  197. package/src/db/provider-model-seed.ts +34 -0
  198. package/src/db/provider-quota-lease-write-ops.ts +1 -0
  199. package/src/db/provider-quota-write-ops.ts +2 -0
  200. package/src/db/provisioning-write-ops.ts +9 -8
  201. package/src/db/read-op-marshaling-fixtures.ts +5 -0
  202. package/src/db/read-ops.ts +4 -0
  203. package/src/db/routing-policy.ts +3 -4
  204. package/src/db/schema.ts +9 -3
  205. package/src/db/secrets-broker-write-ops.ts +5 -5
  206. package/src/db/secrets-vault-write.ts +7 -7
  207. package/src/db/services-write-ops.ts +3 -0
  208. package/src/db/spawn-delegation-state.ts +11 -4
  209. package/src/db/spawn-tree-cleanup.ts +3 -0
  210. package/src/db/stats-async.ts +4 -3
  211. package/src/db/task-entity.ts +23 -2
  212. package/src/db/task-list.ts +3 -1
  213. package/src/db/tasks.ts +1 -17
  214. package/src/db/team-spawn-delegation.ts +1 -1
  215. package/src/db/teams-write-ops.ts +4 -4
  216. package/src/db/timer-cutover-write-ops.ts +7 -0
  217. package/src/db/timer-ingress-write-ops.ts +9 -0
  218. package/src/db/timer-ingress.ts +7 -1
  219. package/src/db/timer-operator-write-ops.ts +14 -0
  220. package/src/db/timer-transfer-write-ops.ts +4 -0
  221. package/src/db/timer-write-ops.ts +14 -0
  222. package/src/db/token-db-write-ops.ts +21 -0
  223. package/src/db/token-standing.ts +15 -11
  224. package/src/db/user-settings-write-ops.ts +1 -0
  225. package/src/db/vault-backup.ts +2 -2
  226. package/src/db/vault-expiry.ts +2 -2
  227. package/src/db/vault-health.ts +3 -3
  228. package/src/db/vault-token-ops.ts +8 -8
  229. package/src/db/web-tool-call-ops.ts +3 -3
  230. package/src/db/workflow-contact.ts +1 -1
  231. package/src/db/workflow-task-read-ops.ts +30 -0
  232. package/src/db/write-ops.ts +15 -1
  233. package/src/db/writer-fault-seam.ts +5 -2
  234. package/src/db/writer-gateway.ts +21 -12
  235. package/src/dispatch-availability.ts +10 -6
  236. package/src/ephemeral/{landing-ingress-proof-worker.ts → landing-ingress-proof.test-fixture.ts} +4 -2
  237. package/src/host/disk-alert.ts +121 -0
  238. package/src/lifecycle-manager.ts +3 -2
  239. package/src/maintenance/cd-watcher.ts +3 -3
  240. package/src/maintenance/critical-job-watchdog.ts +16 -17
  241. package/src/maintenance/index.ts +1 -0
  242. package/src/maintenance/jobs.ts +3 -2
  243. package/src/maintenance/scheduler.ts +84 -128
  244. package/src/maintenance/stalled-delivery-pager.ts +6 -5
  245. package/src/maintenance/workspaces.ts +1 -1
  246. package/src/managed-policy.ts +31 -27
  247. package/src/mcp/index.ts +5 -2
  248. package/src/mcp/output-descriptions.ts +8 -8
  249. package/src/mcp/presentation.ts +4 -4
  250. package/src/mcp/project-profile-instructions.ts +32 -0
  251. package/src/mcp/tool-groups.ts +1 -1
  252. package/src/mcp/tools-spawn.ts +4 -3
  253. package/src/mcp-agent-action.ts +1 -2
  254. package/src/mcp-host-tools.ts +1 -1
  255. package/src/mcp-issue-ref-tools.ts +12 -11
  256. package/src/mcp-maintenance-tools.ts +2 -2
  257. package/src/mcp-profile-trust-tools.ts +4 -4
  258. package/src/mcp-provider-catalog.ts +2 -2
  259. package/src/mcp-task-tools.ts +11 -5
  260. package/src/mcp-team-template-tools.ts +3 -3
  261. package/src/mcp-workflow-tools.ts +10 -3
  262. package/src/notification-types.ts +2 -1
  263. package/src/operator-alarm.ts +26 -11
  264. package/src/package-pack.ts +42 -39
  265. package/src/project-instructions.ts +4 -0
  266. package/src/project-profile-instruction-text.ts +6 -0
  267. package/src/project-profile-instructions.ts +20 -0
  268. package/src/provider-catalog-store.ts +3 -49
  269. package/src/provider-config-store.ts +12 -11
  270. package/src/provider-model-seed.ts +1 -18
  271. package/src/provider-quota-config-store.ts +5 -4
  272. package/src/provider-secrets.test-fixture.ts +3 -3
  273. package/src/recipe-runner.ts +2 -2
  274. package/src/repo-config-source.ts +10 -4
  275. package/src/routes/agent-profiles.ts +6 -6
  276. package/src/routes/agents-spawn.ts +1 -1
  277. package/src/routes/agents.ts +6 -6
  278. package/src/routes/commands-listing.ts +4 -0
  279. package/src/routes/commands.ts +2 -2
  280. package/src/routes/continuation-archives.ts +2 -2
  281. package/src/routes/host-resource-control.ts +4 -1
  282. package/src/routes/index.ts +10 -1
  283. package/src/routes/integrations.ts +24 -50
  284. package/src/routes/issue-refs.ts +7 -7
  285. package/src/routes/orchestrator-bootstrap.ts +3 -3
  286. package/src/routes/orchestrator-host-registration.ts +2 -2
  287. package/src/routes/orchestrator-host-utilization.ts +3 -1
  288. package/src/routes/orchestrator-proxy.ts +2 -2
  289. package/src/routes/orchestrator.ts +4 -4
  290. package/src/routes/project-profile-instructions.ts +27 -0
  291. package/src/routes/project-trust.ts +2 -2
  292. package/src/routes/provider-config.ts +9 -8
  293. package/src/routes/provider-quota-config.ts +2 -2
  294. package/src/routes/settings.ts +5 -5
  295. package/src/routes/stats.ts +6 -6
  296. package/src/routes/task-http.ts +15 -1
  297. package/src/routes/task-issues.ts +11 -26
  298. package/src/routes/tasks.ts +22 -53
  299. package/src/routes/team-templates.ts +5 -5
  300. package/src/routes/teams-config.ts +2 -2
  301. package/src/routes/tokens.ts +12 -5
  302. package/src/routes/workflows.ts +20 -5
  303. package/src/routing-policy-normalize.ts +1 -2
  304. package/src/routing-step-defaults.ts +0 -2
  305. package/src/runner-token-mint-limit.ts +63 -0
  306. package/src/runtime-grant.ts +4 -2
  307. package/src/runtime-tokens.ts +50 -9
  308. package/src/security.ts +13 -20
  309. package/src/server-background.ts +1 -1
  310. package/src/server.ts +5 -2
  311. package/src/services/host-utilization.ts +23 -0
  312. package/src/services/idle-epics.ts +5 -6
  313. package/src/services/issue-lifecycle.ts +57 -61
  314. package/src/services/issue-refs.ts +9 -13
  315. package/src/services/kernel-timers-cutover-chaos-worker.ts +12 -7
  316. package/src/services/kernel-timers-ingress.ts +1 -1
  317. package/src/services/kernel-timers-request-key-migration-chaos-worker.ts +4 -1
  318. package/src/services/landed-ancestry.ts +19 -3
  319. package/src/services/memory-areas.ts +15 -51
  320. package/src/services/plan-ci-notice.ts +57 -0
  321. package/src/services/plan-ci-recovery.ts +2 -55
  322. package/src/services/plan-wake-digest.ts +1 -0
  323. package/src/services/profile-trust.ts +4 -3
  324. package/src/services/project-config-read.ts +70 -0
  325. package/src/services/project-profile-instructions.ts +54 -0
  326. package/src/services/provisioning-harness/budget-baseline.json +4 -4
  327. package/src/services/restart-spawn.ts +61 -49
  328. package/src/services/self-care-state.ts +2 -1
  329. package/src/services/self-effort.ts +5 -5
  330. package/src/services/send-message.ts +5 -1
  331. package/src/services/spawn-agent.ts +13 -2
  332. package/src/services/spawn-chain-inspector.ts +6 -13
  333. package/src/services/spawn-policy.ts +68 -0
  334. package/src/services/spawn-tree-cleanup.ts +2 -2
  335. package/src/services/task-dispatch.ts +3 -0
  336. package/src/services/task-state-reconcile.ts +6 -14
  337. package/src/services/team-templates.ts +9 -8
  338. package/src/services/transient-agent-reaper.ts +3 -1
  339. package/src/services/workflow-auto-start.ts +25 -13
  340. package/src/services/workflow-ci-main.ts +1 -1
  341. package/src/services/workflow-deferred-cleanup.ts +4 -2
  342. package/src/services/workflow-effect-driver.ts +2 -0
  343. package/src/services/workflow-effects.ts +18 -11
  344. package/src/services/workflow-land-notice.ts +10 -0
  345. package/src/services/workflow-pause-text.ts +43 -0
  346. package/src/services/workflow-reconcile.ts +13 -11
  347. package/src/services/workflow-run-view.ts +2 -0
  348. package/src/services/workflow-runs.ts +29 -13
  349. package/src/services/workflow-supervisor-state.ts +5 -0
  350. package/src/services/workflow-supervisor.ts +7 -3
  351. package/src/settings/registry.ts +1 -2
  352. package/src/spawn-command.ts +4 -4
  353. package/src/spawn-models.ts +10 -12
  354. package/src/spawn-placement.ts +1 -1
  355. package/src/spawn-targets.ts +1 -1
  356. package/src/startup-context-injection.ts +1 -0
  357. package/src/state-adoption/cd-cutover.ts +53 -7
  358. package/src/state-adoption/fleet-cutover.ts +35 -0
  359. package/src/state-adoption/fleet-plan.ts +36 -0
  360. package/src/state-adoption/index.ts +2 -2
  361. package/src/state-adoption/linux-plan.ts +2 -2
  362. package/src/state-adoption/linux.ts +2 -2
  363. package/src/state-adoption/macos-plan.ts +2 -2
  364. package/src/state-adoption/macos.ts +1 -1
  365. package/src/steward.ts +7 -6
  366. package/src/task-dispatch-live.ts +1 -1
  367. package/src/task-dispatcher.ts +1 -1
  368. package/src/teams-config-store.ts +4 -3
  369. package/src/token-db-write.ts +10 -0
  370. package/src/workspace-command-settle.ts +2 -1
  371. package/src/workspace-steward-wake.ts +2 -2
  372. package/node_modules/@humwick/kernel/LICENSE +0 -167
  373. package/node_modules/@humwick/kernel/async-entity-persistence.ts +0 -344
  374. package/node_modules/@humwick/kernel/async-event-log.ts +0 -107
  375. package/node_modules/@humwick/kernel/automations.pilot.ts +0 -147
  376. package/node_modules/@humwick/kernel/config.ts +0 -111
  377. package/node_modules/@humwick/kernel/contract.ts +0 -572
  378. package/node_modules/@humwick/kernel/control-authz.ts +0 -281
  379. package/node_modules/@humwick/kernel/control.ts +0 -645
  380. package/node_modules/@humwick/kernel/effects.ts +0 -493
  381. package/node_modules/@humwick/kernel/event-log.ts +0 -319
  382. package/node_modules/@humwick/kernel/http-transport.ts +0 -384
  383. package/node_modules/@humwick/kernel/identity-grammar.ts +0 -20
  384. package/node_modules/@humwick/kernel/identity.ts +0 -248
  385. package/node_modules/@humwick/kernel/index.ts +0 -28
  386. package/node_modules/@humwick/kernel/kernel-persistence.ts +0 -181
  387. package/node_modules/@humwick/kernel/landing-incident.ts +0 -306
  388. package/node_modules/@humwick/kernel/landing.ts +0 -953
  389. package/node_modules/@humwick/kernel/manifest-validation.ts +0 -460
  390. package/node_modules/@humwick/kernel/mcp-transport.ts +0 -109
  391. package/node_modules/@humwick/kernel/message-authority-chaos-worker.ts +0 -200
  392. package/node_modules/@humwick/kernel/message-authority.ts +0 -281
  393. package/node_modules/@humwick/kernel/module-host.ts +0 -722
  394. package/node_modules/@humwick/kernel/openapi-init.ts +0 -14
  395. package/node_modules/@humwick/kernel/package.json +0 -47
  396. package/node_modules/@humwick/kernel/persistence-schema.ts +0 -248
  397. package/node_modules/@humwick/kernel/persistence.ts +0 -510
  398. package/node_modules/@humwick/kernel/registry.ts +0 -60
  399. package/node_modules/@humwick/kernel/sqlite-kernel-persistence.ts +0 -499
  400. package/node_modules/@humwick/kernel/sqlite-module-host.ts +0 -87
  401. package/node_modules/@humwick/kernel/timers-chaos-worker.ts +0 -186
  402. package/node_modules/@humwick/kernel/timers.pilot.ts +0 -1411
  403. package/node_modules/@humwick/kernel/workflow-activation.ts +0 -25
  404. package/node_modules/@humwick/kernel/workflow-backstop.ts +0 -40
  405. package/node_modules/@humwick/kernel/workflow-cancel-tree.ts +0 -20
  406. package/node_modules/@humwick/kernel/workflow-chaos-worker.ts +0 -213
  407. package/node_modules/@humwick/kernel/workflow-composition.ts +0 -231
  408. package/node_modules/@humwick/kernel/workflow-container.ts +0 -72
  409. package/node_modules/@humwick/kernel/workflow-control.test-fixture.ts +0 -42
  410. package/node_modules/@humwick/kernel/workflow-control.ts +0 -178
  411. package/node_modules/@humwick/kernel/workflow-definition.ts +0 -507
  412. package/node_modules/@humwick/kernel/workflow-dispatch.ts +0 -60
  413. package/node_modules/@humwick/kernel/workflow-foreach.ts +0 -167
  414. package/node_modules/@humwick/kernel/workflow-journal-codec.ts +0 -339
  415. package/node_modules/@humwick/kernel/workflow-journal.ts +0 -922
  416. package/node_modules/@humwick/kernel/workflow-lane.ts +0 -48
  417. package/node_modules/@humwick/kernel/workflow-lease-owner.ts +0 -60
  418. package/node_modules/@humwick/kernel/workflow-recovery.test-fixture.ts +0 -19
  419. package/node_modules/@humwick/kernel/workflow-runtime.ts +0 -938
  420. package/node_modules/@humwick/kernel/workflow-script.ts +0 -11
  421. package/node_modules/@humwick/kernel/workflow-state.ts +0 -299
  422. package/node_modules/@humwick/kernel/workflow-step-commit.ts +0 -195
  423. package/node_modules/@humwick/kernel/workflow-transitions.ts +0 -317
  424. package/node_modules/@humwick/kernel/workflow-values.ts +0 -153
  425. package/node_modules/@humwick/kernel/workflow.pilot.ts +0 -376
  426. package/node_modules/@humwick/offload/LICENSE +0 -167
  427. package/node_modules/@humwick/offload/README.md +0 -57
  428. package/node_modules/@humwick/offload/dist/async-runner.d.ts +0 -18
  429. package/node_modules/@humwick/offload/dist/async-runner.d.ts.map +0 -1
  430. package/node_modules/@humwick/offload/dist/async-runner.js +0 -158
  431. package/node_modules/@humwick/offload/dist/async-runner.js.map +0 -1
  432. package/node_modules/@humwick/offload/dist/envelope.d.ts +0 -25
  433. package/node_modules/@humwick/offload/dist/envelope.d.ts.map +0 -1
  434. package/node_modules/@humwick/offload/dist/envelope.js +0 -80
  435. package/node_modules/@humwick/offload/dist/envelope.js.map +0 -1
  436. package/node_modules/@humwick/offload/dist/fixtures/worker-task.d.ts +0 -10
  437. package/node_modules/@humwick/offload/dist/fixtures/worker-task.d.ts.map +0 -1
  438. package/node_modules/@humwick/offload/dist/fixtures/worker-task.js +0 -59
  439. package/node_modules/@humwick/offload/dist/fixtures/worker-task.js.map +0 -1
  440. package/node_modules/@humwick/offload/dist/index.d.ts +0 -5
  441. package/node_modules/@humwick/offload/dist/index.d.ts.map +0 -1
  442. package/node_modules/@humwick/offload/dist/index.js +0 -5
  443. package/node_modules/@humwick/offload/dist/index.js.map +0 -1
  444. package/node_modules/@humwick/offload/dist/subprocess-runner.d.ts +0 -62
  445. package/node_modules/@humwick/offload/dist/subprocess-runner.d.ts.map +0 -1
  446. package/node_modules/@humwick/offload/dist/subprocess-runner.js +0 -295
  447. package/node_modules/@humwick/offload/dist/subprocess-runner.js.map +0 -1
  448. package/node_modules/@humwick/offload/dist/types.d.ts +0 -111
  449. package/node_modules/@humwick/offload/dist/types.d.ts.map +0 -1
  450. package/node_modules/@humwick/offload/dist/types.js +0 -176
  451. package/node_modules/@humwick/offload/dist/types.js.map +0 -1
  452. package/node_modules/@humwick/offload/dist/worker-pool.d.ts +0 -23
  453. package/node_modules/@humwick/offload/dist/worker-pool.d.ts.map +0 -1
  454. package/node_modules/@humwick/offload/dist/worker-pool.js +0 -435
  455. package/node_modules/@humwick/offload/dist/worker-pool.js.map +0 -1
  456. package/node_modules/@humwick/offload/dist/worker-runtime.d.ts +0 -2
  457. package/node_modules/@humwick/offload/dist/worker-runtime.d.ts.map +0 -1
  458. package/node_modules/@humwick/offload/dist/worker-runtime.js +0 -27
  459. package/node_modules/@humwick/offload/dist/worker-runtime.js.map +0 -1
  460. package/node_modules/@humwick/offload/dist/worker.d.ts +0 -5
  461. package/node_modules/@humwick/offload/dist/worker.d.ts.map +0 -1
  462. package/node_modules/@humwick/offload/dist/worker.js +0 -1
  463. package/node_modules/@humwick/offload/dist/worker.js.map +0 -1
  464. package/node_modules/@humwick/offload/package.json +0 -46
  465. package/node_modules/@humwick/offload/src/async-runner.ts +0 -170
  466. package/node_modules/@humwick/offload/src/envelope.ts +0 -92
  467. package/node_modules/@humwick/offload/src/fixtures/worker-task.ts +0 -49
  468. package/node_modules/@humwick/offload/src/index.ts +0 -4
  469. package/node_modules/@humwick/offload/src/subprocess-runner.ts +0 -334
  470. package/node_modules/@humwick/offload/src/types.ts +0 -235
  471. package/node_modules/@humwick/offload/src/worker-pool.ts +0 -463
  472. package/node_modules/@humwick/offload/src/worker-runtime.ts +0 -32
  473. package/node_modules/@humwick/offload/src/worker.ts +0 -5
  474. package/public/assets/agent-liveness-DyqxfB-t.js +0 -2
  475. package/public/assets/agent-profiles-BpI_jTxq.js +0 -2
  476. package/public/assets/fleet-BL4UXS_k.js +0 -2
  477. package/public/assets/index-B7ZerHLY.css +0 -2
  478. package/public/assets/index-BK0WV6hF.js +0 -15
  479. package/public/assets/projects-B_lmcqfO.js +0 -2
  480. package/public/assets/provider-display-DbLWpP3j.js +0 -2
  481. package/public/assets/run-card-CO_bguQf.js +0 -2
  482. package/public/assets/use-project-list-B-T2Bnnn.js +0 -2
  483. package/public/assets/versioned-prompt-editor-Diu8BIaM.js +0 -8
  484. package/runner/src/gate-run-lease-probe.ts +0 -38
  485. package/src/db/claude-catalog-reconcile.ts +0 -27
  486. package/src/routing-model-aliases.ts +0 -21
@@ -1,14 +1,17 @@
1
1
  // #3091 — workflows by default: a builder spawn for a Task that lands code starts the project's build-review
2
2
  // workflow instead, and its run spawns the builder. One core behind relay_spawn_agent, relay_task_dispatch and
3
3
  // POST /api/agents/spawn; `workflow: false` keeps the manual path. review, research and ops Tasks never start one.
4
+ // #3736 — a project spawn-policy rule matching the Task names the workflow instead, whatever the Task's type.
4
5
  import { WORKFLOW_AUTO_START_NAME, workflowAutoStarts, type WorkflowRunAgent, type WorkflowRunView } from "@humwick/sdk";
5
- import { getTaskDetailAsync, ValidationError } from "../db";
6
+ import { SPAWN_POLICY_FILE } from "@humwick/sdk/spawn-policy";
7
+ import { getAgent, getTaskDetailAsync, resolveProjectForCwd, ValidationError } from "../db";
6
8
  import { getAgentProfile } from "../config-store";
7
9
  import { McpAuthError, McpNotFoundError } from "../mcp-errors";
8
10
  import { ServiceAuthError } from "./errors";
9
11
  import { getWorkflowRun, launchWorkflowRun, prepareWorkflowRun, type PreparedWorkflowRun } from "./workflow-runs";
10
12
  import type { WorkflowPins } from "./workflow-pins";
11
13
  import type { AuthContext } from "./auth-context";
14
+ import { spawnerKind, spawnPolicyTask, spawnPolicyWorkflowFor } from "./spawn-policy";
12
15
 
13
16
  /** A builder spawn as its transport received it: the Task, the opt-out, and every other field the caller set (wire names). */
14
17
  export interface BuilderSpawn {
@@ -58,18 +61,27 @@ function refused(fields: Record<string, unknown>): string[] {
58
61
  export async function workflowForBuilderSpawn(spawn: BuilderSpawn, ctx: AuthContext): Promise<BuilderSpawnOutcome> {
59
62
  if (spawn.workflow === false || spawn.taskId === undefined) return { started: false };
60
63
  const task = await getTaskDetailAsync(spawn.taskId);
61
- const type = task?.kind === "deliverable" ? task.dispatch?.type : undefined;
62
- if (!task || !workflowAutoStarts(type)) return { started: false };
63
- const subject = `T${task.id} is a ${type} Task, so this spawn starts workflow ${WORKFLOW_AUTO_START_NAME}, whose run spawns the builder`;
64
- const optOut = "pass workflow:false to spawn by hand";
64
+ if (!task || task.kind !== "deliverable") return { started: false };
65
+ const type = task.dispatch?.type;
66
+ const { cwd, orchestratorId } = spawn.fields;
67
+ const caller = ctx.callerAgentId ? getAgent(ctx.callerAgentId) ?? undefined : undefined;
68
+ const checkout = typeof cwd === "string" && cwd ? { cwd, ...(typeof orchestratorId === "string" && orchestratorId ? { orchestratorId } : {}) } : undefined;
69
+ const policy = await spawnPolicyWorkflowFor(
70
+ { projectId: task.projectId ?? (checkout ? resolveProjectForCwd(checkout.cwd)?.id : undefined), ...(checkout ? { checkout } : {}) },
71
+ { spawner: spawnerKind(ctx, {}, caller), task: spawnPolicyTask(task)! },
72
+ );
73
+ if (!policy && !workflowAutoStarts(type)) return { started: false };
74
+ const workflow = policy?.workflow ?? WORKFLOW_AUTO_START_NAME;
75
+ const why = policy ? `T${task.id} matches ${SPAWN_POLICY_FILE} rule "${policy.rule}"` : `T${task.id} is a ${type} Task`;
76
+ const subject = `${why}, so this spawn starts workflow ${workflow}, whose run spawns the builder`;
77
+ const optOut = policy ? "the project's spawn policy runs this Task only through that workflow" : "pass workflow:false to spawn by hand";
65
78
  if (!taskRepos(task).length) return { started: false, reason: `T${task.id} names no repo, so no workflow can run on it` };
66
79
  const conflicts = refused(spawn.fields);
67
- if (conflicts.length) throw new ValidationError(`${subject}; the run cannot honor ${conflicts.map((key) => `${key}=${JSON.stringify(spawn.fields[key])}`).join(", ")}. Drop ${conflicts.length > 1 ? "them" : "it"}, or ${optOut}`);
80
+ if (conflicts.length) throw new ValidationError(`${subject}; the run cannot honor ${conflicts.map((key) => `${key}=${JSON.stringify(spawn.fields[key])}`).join(", ")}. Drop ${conflicts.length > 1 ? "them" : "it"}${policy ? "" : `, or ${optOut}`}`);
68
81
  try {
69
82
  // Located as the spawn itself would be: its cwd and host, else the Task's repo root.
70
- const { cwd, orchestratorId } = spawn.fields;
71
83
  const prepared = await prepareWorkflowRun({
72
- workflow: WORKFLOW_AUTO_START_NAME, taskId: task.id, ...(typeof cwd === "string" && cwd ? { cwd } : {}), ...(typeof orchestratorId === "string" && orchestratorId ? { orchestratorId } : {}),
84
+ workflow, taskId: task.id, ...(typeof cwd === "string" && cwd ? { cwd } : {}), ...(typeof orchestratorId === "string" && orchestratorId ? { orchestratorId } : {}),
73
85
  }, ctx);
74
86
  const inputSteps = Object.entries(prepared.read.workflow.definition.steps).filter(([, step]) => step.task === "input");
75
87
  const role = spawn.fields.role;
@@ -80,7 +92,7 @@ export async function workflowForBuilderSpawn(spawn: BuilderSpawn, ctx: AuthCont
80
92
  const steer = BRIEF_FIELDS.map((key) => spawn.fields[key]).filter((value): value is string => typeof value === "string" && Boolean(value.trim())).join("\n\n");
81
93
  // The call answers for the builder's spawn: one the run cannot dispatch fails it, as a spawn by hand would (#3160).
82
94
  const started = await launchWorkflowRun(prepared, { ...(pins ? { pins } : {}), ...(steer ? { steer } : {}), refuseUndispatched: true }, ctx) as WorkflowRunView & { existing?: true; warnings?: string[] };
83
- return { started: true, receipt: await receiptFor(prepared, started, set(spawn.fields).filter((key) => UNUSED_FIELDS.includes(key)), ctx) };
95
+ return { started: true, receipt: await receiptFor(prepared, started, set(spawn.fields).filter((key) => UNUSED_FIELDS.includes(key)), { why, policyRule: policy?.rule }, ctx) };
84
96
  } catch (error) {
85
97
  if (error instanceof ServiceAuthError) throw new McpAuthError(`${subject}: ${error.message}; ${optOut}`);
86
98
  if (error instanceof ValidationError || error instanceof McpAuthError || error instanceof McpNotFoundError) error.message = `${subject}: ${error.message}; ${optOut}`;
@@ -99,7 +111,7 @@ function builderPin(fields: Record<string, unknown>): Record<string, string> {
99
111
  }
100
112
 
101
113
  /** The spawn's receipt: the run, and its builder as a spawn receipt names one (the newest agent of the build step). */
102
- async function receiptFor(prepared: PreparedWorkflowRun, started: WorkflowRunView & { existing?: true; warnings?: string[] }, ignored: string[], ctx: AuthContext): Promise<Record<string, unknown>> {
114
+ async function receiptFor(prepared: PreparedWorkflowRun, started: WorkflowRunView & { existing?: true; warnings?: string[] }, ignored: string[], cause: { why: string; policyRule?: string }, ctx: AuthContext): Promise<Record<string, unknown>> {
103
115
  const { task, read } = prepared;
104
116
  const run = await getWorkflowRun(started.runId, ctx, { verbose: true }) as WorkflowRunView & { agents: WorkflowRunAgent[] };
105
117
  const step = run.steps.find((entry) => read.workflow.definition.steps[entry.name]?.task === "input")!;
@@ -107,8 +119,8 @@ async function receiptFor(prepared: PreparedWorkflowRun, started: WorkflowRunVie
107
119
  const profile = step.pin?.profile ?? read.workflow.roles.get(step.role)?.extends;
108
120
  const label = `workflow ${run.workflow} (${run.source ?? "project"}) run ${run.runId}`;
109
121
  const note = started.existing
110
- ? `T${task.id} already runs ${label}, which owns its builder: no builder was spawned. relay_workflow_get shows its agents; pass workflow:false to spawn beside it.`
111
- : `T${task.id} is a ${task.dispatch?.type} Task, so Relay started ${label}; its run ${agent ? "spawned" : "is spawning"} the builder (role ${step.role} on profile ${profile}). You hear back on a step spawn failure, an escalation or the result. Pass workflow:false to spawn by hand.`;
122
+ ? `T${task.id} already runs ${label}, which owns its builder: no builder was spawned. relay_workflow_get shows its agents${cause.policyRule ? "" : "; pass workflow:false to spawn beside it"}.`
123
+ : `${cause.why}, so Relay started ${label}; its run ${agent ? "spawned" : "is spawning"} the builder (role ${step.role} on profile ${profile}). You hear back on a step spawn failure, an escalation or the result.${cause.policyRule ? "" : " Pass workflow:false to spawn by hand."}`;
112
124
  const warnings = [...(started.warnings ?? []), ...(ignored.length ? [`the run sets these for its own agents, so ${ignored.join(", ")} went unused`] : [])];
113
125
  return {
114
126
  ok: true,
@@ -116,7 +128,7 @@ async function receiptFor(prepared: PreparedWorkflowRun, started: WorkflowRunVie
116
128
  taskId: task.id,
117
129
  workflowRun: {
118
130
  runId: run.runId, workflow: run.workflow, ...(run.source ? { source: run.source } : {}), sha: run.sha, status: run.status, step: run.step,
119
- taskType: task.dispatch?.type, ...(started.existing ? { existing: true } : {}),
131
+ taskType: task.dispatch?.type, ...(cause.policyRule ? { spawnPolicyRule: cause.policyRule } : {}), ...(started.existing ? { existing: true } : {}),
120
132
  builder: { step: step.name, role: step.role, ...(profile ? { profile } : {}), ...(step.pin ? { pin: step.pin } : {}) },
121
133
  },
122
134
  ...(agent ? { spawnRequestId: agent.spawnRequestId, agentId: agent.agentId ?? null, registered: Boolean(agent.agentId), ...(agent.provider ? { provider: agent.provider } : {}) } : {}),
@@ -1,4 +1,4 @@
1
- import { correlatedCiIncidents } from "./plan-ci-recovery";
1
+ import { correlatedCiIncidents } from "./plan-ci-notice";
2
2
  // #3045 H5 — the `ci-main` step after the land: CI on main for the exact landed commit. The CI monitor's
3
3
  // existing tick observes every commit a run waits on (ci_sha_observations); this module only reads that
4
4
  // evidence. Red alerts the coordinator once while the run keeps waiting; green on every watch ends the run
@@ -1,3 +1,4 @@
1
+ import { workflowLandVerification } from "./workflow-land-notice";
1
2
  import type { WorkflowEffect, WorkflowRunRecord } from "@humwick/kernel/workflow.pilot";
2
3
  import { notifySystemMessage } from "../notify";
3
4
  import { workflowCoordinatorTarget } from "./workflow-coordinator";
@@ -29,10 +30,11 @@ async function resumeHosts(sup: Supervisor, orchestratorId?: string): Promise<vo
29
30
  }
30
31
 
31
32
  export async function notifyDeferredWorkflowCleanup(run: WorkflowRunRecord, effect: WorkflowEffect): Promise<void> {
33
+ const proof = run.landed?.sha ? await workflowLandVerification(run, run.landed.sha) : undefined;
32
34
  notifySystemMessage(await workflowCoordinatorTarget(run), {
33
35
  subject: `Workflow ${run.id}: cleanup deferred`,
34
- body: `Workflow ${run.id} landed as ${run.landed?.sha}. Shutdowns on offline hosts are recorded durably and will run when those hosts re-register. The run can finish with its CI result.`,
35
- payload: { kind: "workflow.cleanup-deferred", runId: run.id, effectId: effect.id, deferred: effect.result?.deferred },
36
+ body: `Workflow ${run.id}: ${proof?.verified ? `landed as ${run.landed?.sha}. verified: true.` : proof?.line ?? "No land receipt."} Shutdowns on offline hosts are recorded durably and will run when those hosts re-register. The run can finish with its CI result.`,
37
+ payload: { ...(proof ? { verified: proof.verified } : {}), kind: "workflow.cleanup-deferred", runId: run.id, effectId: effect.id, deferred: effect.result?.deferred },
36
38
  idempotencyKey: `workflow:${run.id}:cleanup-deferred`,
37
39
  replyExpected: false,
38
40
  maxAgeSeconds: null,
@@ -156,6 +156,8 @@ export async function drainEffects(sup: Supervisor): Promise<number> {
156
156
  continue;
157
157
  }
158
158
  const run = await invoke<WorkflowRunRecord>(sup, "get", { runId: effect.runId });
159
+ // #3757 — a paused run spawns, messages, arms and reaps nothing until its unpause; only its pause notices go out.
160
+ if (run.pause && effect.kind !== "pause-notice") continue;
159
161
  if (await settleEffect(sup, run, effect)) settled += 1;
160
162
  } catch (error) {
161
163
  console.warn(`workflow effect ${effect.id} could not be settled: ${errMessage(error)}`);
@@ -1,4 +1,4 @@
1
- import { planCiNotice } from "./plan-ci-recovery";
1
+ import { planCiNotice } from "./plan-ci-notice";
2
2
  import { performWorkflowDecision, performWithdrawWorkflowDecision } from "./workflow-decisions";
3
3
  import { getCommandAsync } from "../commands-write";
4
4
  // #3045 — the src half of a workflow run: the kernel writes effect intents and waits; this module
@@ -23,9 +23,10 @@ import { spawnTaskDispatch } from "./task-dispatch";
23
23
  import type { DispatchPin } from "../task-dispatcher";
24
24
  import { createTaskEntityAsync } from "./task-entity";
25
25
  import { workflowNotice } from "./workflow-notice";
26
+ import { workflowPauseNotice } from "./workflow-pause-text";
26
27
  import { planQuietingRunNotice } from "./plan-wake-policy";
27
28
  import { workflowOverdueLead } from "./workflow-overdue-notice";
28
- import { workflowLandNotice } from "./workflow-land-notice";
29
+ import { workflowLandNotice, workflowLandVerification } from "./workflow-land-notice";
29
30
  import { ciRedLead, ciResultLine } from "./workflow-ci-main";
30
31
  import { stepTaskDispatch } from "./workflow-pins";
31
32
  import { workflowDispatchResult } from "./workflow-dispatch-result";
@@ -112,7 +113,7 @@ async function dispatchRole(run: WorkflowRunRecord, effect: WorkflowEffect, agen
112
113
  const spawnRequestId = spawnRequestIdFor(effect);
113
114
  await call("prepareDispatch", { id: effect.id, taskId, spawnRequestId });
114
115
  const spawn = await spawnTaskDispatch({
115
- taskId, role, roleSha: run.sha, ...(run.callerAgentId ? { workflowCaller: { agentId: run.callerAgentId, maxLiveAgents: run.maxLiveAgents ?? 2, runId: run.rootRunId ?? run.id } } : {}), ...(run.snapshot?.[run.workflow]?.roles[role] ? { roleSnapshot: run.snapshot[run.workflow]!.roles[role] as unknown as import("../project-roles").ProjectRole } : {}), workflowRunId: run.id, pin: stepDispatchPin(pin, step), pinSources: pin?.from, createReview: false, ...(pin?.orchestratorId ? { orchestratorId: pin.orchestratorId } : {}),
116
+ taskId, role, roleSha: run.sha, ...(run.callerAgentId ? { workflowCaller: { agentId: run.callerAgentId, maxLiveAgents: run.maxLiveAgents ?? 2, runId: run.rootRunId ?? run.id } } : {}), ...(run.snapshot?.[run.workflow]?.roles[role] ? { roleSnapshot: run.snapshot[run.workflow]!.roles[role] as unknown as import("../project-roles").ProjectRole } : {}), workflowRunId: run.id, workflowNames: [...new Set([run.workflow, ...Object.keys(run.snapshot ?? {})])], pin: stepDispatchPin(pin, step), pinSources: pin?.from, createReview: false, ...(pin?.orchestratorId ? { orchestratorId: pin.orchestratorId } : {}),
116
117
  taskRepoCwd: run.cwd, roleHost: run.orchestratorId, ...(pin?.profile ? { roleProfile: pin.profile } : {}),
117
118
  spawnRequestId, waitForRegistrationMs: 0, verbose: false, ...(steer ? { steer } : {}), ...avoid,
118
119
  }, WORKFLOW_CTX);
@@ -377,6 +378,10 @@ export async function performWorkflowEffect(run: WorkflowRunRecord, effect: Work
377
378
  return { messageId: message.id };
378
379
  }
379
380
  case "landed": return { superseded: "the CI verdict owns the coordinator notice" };
381
+ case "pause-notice": {
382
+ const message = notifySystemMessage(await workflowCoordinatorTarget(run), { ...workflowPauseNotice(runLabel(run), run, effect), idempotencyKey: key, replyExpected: false });
383
+ return { messageId: message.id };
384
+ }
380
385
  case "reap":
381
386
  return reapAgents(run, effect, reapTargets(run, effect, agents));
382
387
  case "dispatch": {
@@ -451,12 +456,13 @@ export async function performWorkflowEffect(run: WorkflowRunRecord, effect: Work
451
456
  }
452
457
  if (effect.payload.reason === "ci-red" || effect.payload.reason === "ci-late") {
453
458
  const sha = str(effect.payload.landedSha) ?? run.landed?.sha ?? "";
459
+ const proof = await workflowLandVerification(run, sha);
454
460
  const message = notifySystemMessage(await workflowCoordinatorTarget(run), {
455
- subject: `${runLabel(run)} landed, CI on main ${effect.payload.reason === "ci-red" ? "red" : "late"}`,
456
- body: workflowNotice(run, [`T${run.taskId} landed as ${sha}.`, ...(effect.payload.reason === "ci-red" ? ciRedLead(runLabel(run), effect.payload) : [ciResultLine(effect.payload.ci as WorkflowCiVerdict)!])]),
461
+ subject: `${runLabel(run)} ${proof.verified ? "landed" : "land verification FAILED"}, CI on main ${effect.payload.reason === "ci-red" ? "red" : "late"}`,
462
+ body: workflowNotice(run, [proof.verified ? `T${run.taskId} landed as ${sha}. ${proof.line}` : proof.line, ...(effect.payload.reason === "ci-red" ? ciRedLead(runLabel(run), effect.payload) : [ciResultLine(effect.payload.ci as WorkflowCiVerdict)!])]),
457
463
  idempotencyKey: `workflow:landed:${run.id}:${sha}:${effect.waitKey ?? effect.id}`,
458
464
  replyExpected: false,
459
- payload: { kind: "workflow.landed", runId: run.id, taskId: run.taskId, landedSha: sha, reason: effect.payload.reason, ci: effect.payload.ci },
465
+ payload: { verified: proof.verified, kind: "workflow.landed", runId: run.id, taskId: run.taskId, landedSha: sha, reason: effect.payload.reason, ci: effect.payload.ci },
460
466
  });
461
467
  return { messageId: message.id };
462
468
  }
@@ -494,18 +500,19 @@ export async function performWorkflowEffect(run: WorkflowRunRecord, effect: Work
494
500
  const receipt = await planCiNotice(run, effect.payload, true);
495
501
  if (receipt) return receipt;
496
502
  }
503
+ const proof = sha ? await workflowLandVerification(run, sha) : undefined;
497
504
  const ciLine = ciResultLine(ci);
498
505
  const quietFor = await planQuietingRunNotice(run.taskId, "result", ci);
499
- if (quietFor) return { quiet: "plan-dispatched", planId: quietFor };
506
+ if (quietFor && proof?.verified !== false) return { quiet: "plan-dispatched", planId: quietFor };
500
507
  const nothingToLand = effect.payload.nothingToLand === true || child?.output?.nothingToLand === true;
501
508
  const rounds = typeof effect.payload.rounds === "number" ? effect.payload.rounds : child?.output?.rounds ?? child?.round;
502
- const outcome = !sha ? "finished its script steps" : nothingToLand ? `had nothing to land: its reviewed head is already on main as ${sha}` : `landed as ${sha}`;
509
+ const outcome = !sha ? "finished its script steps" : !proof?.verified ? `has an unverified land receipt for ${sha}` : nothingToLand ? `had nothing to land: its reviewed head is already on main as ${sha}` : `landed as ${sha}`;
503
510
  const message = notifySystemMessage(await workflowCoordinatorTarget(run), {
504
- subject: `${runLabel(run)} done${nothingToLand ? ", nothing to land" : ""}${ci?.status === "fail" ? ", CI on main red" : ""}`,
505
- body: workflowNotice(run, [`${runLabel(run)} is done: T${run.taskId} ${outcome}${rounds === undefined ? "" : ` after ${rounds} round(s)`}.`, ciLine ?? (child || !run.rootRunId ? "CI on main: not configured." : "CI on main: unknown.")], children.items, !!children.cursor),
511
+ subject: `${runLabel(run)} ${proof?.verified === false ? "land verification FAILED" : "done"}${nothingToLand ? ", nothing to land" : ""}${ci?.status === "fail" ? ", CI on main red" : ""}`,
512
+ body: workflowNotice(run, [`${runLabel(run)} is done: T${run.taskId} ${outcome}${rounds === undefined ? "" : ` after ${rounds} round(s)`}.${proof ? ` ${proof.line}` : ""}`, ciLine ?? (child || !run.rootRunId ? "CI on main: not configured." : "CI on main: unknown.")], children.items, !!children.cursor),
506
513
  idempotencyKey: key,
507
514
  replyExpected: false,
508
- payload: { kind: "workflow.result", runId: run.id, taskId: run.taskId, landedSha: sha, ...(rounds === undefined ? {} : { rounds }), ...(nothingToLand ? { nothingToLand } : {}), ...(ci ? { ci } : {}) },
515
+ payload: { ...(proof ? { verified: proof.verified } : {}), kind: "workflow.result", runId: run.id, taskId: run.taskId, landedSha: sha, ...(rounds === undefined ? {} : { rounds }), ...(nothingToLand ? { nothingToLand } : {}), ...(ci ? { ci } : {}) },
509
516
  });
510
517
  return { messageId: message.id };
511
518
  }
@@ -1,3 +1,5 @@
1
+ import { isRecord } from "@humwick/sdk";
2
+ import { verifyPublishedLand } from "./landed-ancestry";
1
3
  import type { WorkflowRunRecord } from "@humwick/kernel/workflow.pilot";
2
4
  import { landLaneSnapshot } from "../db/commands-land-wait";
3
5
  import { formatRelativeTime } from "../mcp/presentation";
@@ -16,3 +18,11 @@ export async function workflowLandNotice(run: WorkflowRunRecord): Promise<string
16
18
  const progress = workspace.landProgressAt == null ? "unknown" : formatRelativeTime(workspace.landProgressAt).relative;
17
19
  return `Land workspace ${workspace.id}: landState ${state} (${age}); last reported progress: ${progress}; lane position ${position} among waiting lands; merging now: ${active.length ? active.join(", ") : lane ? "none" : "unknown"}; gate host: ${gateHost}.`;
18
20
  }
21
+
22
+ export async function workflowLandVerification(run: WorkflowRunRecord, sha: string): Promise<{ verified: boolean; line: string }> {
23
+ const workspace = await deliveredWorkspace(run);
24
+ const hostProof = workspace?.metadata.mergeResult;
25
+ const verdict = isRecord(hostProof) && hostProof.landVerifiedSha === sha ? "confirmed" : await verifyPublishedLand(workspace?.repoRoot ?? run.cwd, sha, workspace?.baseRef);
26
+ return verdict === "confirmed" ? { verified: true, line: "verified: true" }
27
+ : { verified: false, line: `Land NOT verified: ${sha} is ${verdict === "not-ancestor" ? "absent from" : "unresolvable on"} fetched origin. Coordinator action required.` };
28
+ }
@@ -0,0 +1,43 @@
1
+ // #3757 — what a paused run tells its coordinator: a notice on pause and unpause, and one reminder line per paused run
2
+ // of its team in each new context generation (the self-care section of the self-resume continuation).
3
+ import { isRecord, parseJson } from "@humwick/sdk";
4
+ import type { WorkflowEffect, WorkflowRunRecord } from "@humwick/kernel/workflow.pilot";
5
+ import { coordinatedTeamIds } from "../db/coordinator-seat";
6
+ import { taskOwnerRoleTeamId } from "../db/task-owner";
7
+ import { pausedWorkflowRuns } from "../db/workflow-task-read-ops";
8
+ import { workflowText } from "./workflow-notice";
9
+
10
+ const UNPAUSE = "relay_workflow_resume {runId, pause: false}";
11
+ type Pause = NonNullable<WorkflowRunRecord["pause"]>;
12
+
13
+ function pauseOf(value: unknown): Pause | undefined {
14
+ return isRecord(value) && typeof value.reason === "string" && typeof value.by === "string" && typeof value.at === "number"
15
+ ? { reason: value.reason, by: value.by, at: value.at } : undefined;
16
+ }
17
+
18
+ function pausedBy(pause: Pause): string {
19
+ return `paused since ${new Date(pause.at).toISOString()} by ${pause.by}: ${workflowText(pause.reason, 240)}`;
20
+ }
21
+
22
+ /** The coordinator notice a pause or an unpause sends. */
23
+ export function workflowPauseNotice(label: string, run: WorkflowRunRecord, effect: WorkflowEffect): { subject: string; body: string; payload: Record<string, unknown> } {
24
+ const pause = pauseOf(effect.payload) ?? { reason: "", by: "unknown", at: effect.createdAt };
25
+ const kind = effect.payload.action === "unpaused" ? "workflow.unpaused" : "workflow.paused";
26
+ const body = kind === "workflow.paused"
27
+ ? `${label} is ${pausedBy(pause)}. A working step agent finishes; the run does not go past its next step, delivery, review or land, and its timers stand still. Continue it: ${UNPAUSE}.`
28
+ : `${label} was unpaused by ${String(effect.payload.unpausedBy)} after ${Math.round(Number(effect.payload.pausedMs) / 60_000)}m. It was ${pausedBy(pause)}. It goes on from step ${run.step}; its deadlines moved by the paused time.`;
29
+ return { subject: `${label} ${kind === "workflow.paused" ? "paused" : "unpaused"}`, body, payload: { kind, runId: run.id, taskId: run.taskId, ...pause, ...(kind === "workflow.unpaused" ? { unpausedBy: effect.payload.unpausedBy } : {}) } };
30
+ }
31
+
32
+ /** The paused runs of the teams this agent coordinates, and the ones it started; "" when none are paused. */
33
+ export function renderPausedRunsSection(agentId: string): string {
34
+ const teams = new Set(coordinatedTeamIds(agentId));
35
+ const lines = pausedWorkflowRuns().flatMap((row) => {
36
+ const teamId = taskOwnerRoleTeamId(row.owner) ?? row.teamId;
37
+ if (row.coordinator !== agentId && row.callerAgentId !== agentId && !(teamId && teams.has(teamId))) return [];
38
+ const pause = pauseOf(parseJson<unknown>(row.pause, undefined));
39
+ if (!pause) return [];
40
+ return [`- ${row.runId} on T${row.taskId}${row.taskTitle ? ` (${workflowText(row.taskTitle, 80)})` : ""}: ${pausedBy(pause)}`];
41
+ });
42
+ return lines.length ? [`Paused workflow runs of your team: they keep their agents and wait. Continue one with ${UNPAUSE}.`, ...lines].join("\n") : "";
43
+ }
@@ -8,7 +8,7 @@ import { workflowDecisionSignal } from "./workflow-decisions";
8
8
  import { errMessage, isRecord, stringValue } from "@humwick/sdk";
9
9
 
10
10
  import { waitTimeoutMs } from "@humwick/kernel/workflow-definition";
11
- import { workflowEffectId, type WorkflowAgent, type WorkflowEffect, type WorkflowRunRecord, type WorkflowSignal } from "@humwick/kernel/workflow.pilot";
11
+ import { workflowEffectId, workflowRunClock, type WorkflowAgent, type WorkflowEffect, type WorkflowRunRecord, type WorkflowSignal } from "@humwick/kernel/workflow.pilot";
12
12
  import { listActivityEventsAsync } from "../db";
13
13
 
14
14
  import { getGateway } from "../db/gateway";
@@ -21,7 +21,7 @@ import { workflowCleanupDeferred, notifyDeferredWorkflowCleanup } from "./workfl
21
21
 
22
22
  import { deliveredWorkspace, landedSignal, landStopSignal } from "./workflow-land";
23
23
 
24
- import { invoke, SUPERVISOR, testOptions, type Supervisor } from "./workflow-supervisor-state";
24
+ import { invoke, SUPERVISOR, supervisorNow, testOptions, type Supervisor } from "./workflow-supervisor-state";
25
25
  import { workflowWaitAgent } from "./workflow-agent-obligation";
26
26
  import { landingProgress } from "../workspace-phase";
27
27
  type Json = Record<string, unknown>;
@@ -49,7 +49,7 @@ async function alertStalled(sup: Supervisor, run: WorkflowRunRecord, wait: Await
49
49
  /** A wait past its deadline alerts the coordinator once and keeps waiting, like a stopped land (#3045 H1). */
50
50
  async function alertTimeout(sup: Supervisor, run: WorkflowRunRecord, wait: Awaiting, taskId: number | undefined): Promise<boolean> {
51
51
  const timeoutMs = wait.deadline !== undefined ? wait.deadline - wait.since : waitTimeoutMs(run.definition, run.step);
52
- const now = (testOptions.now ?? Date.now)();
52
+ const now = workflowRunClock(run, supervisorNow());
53
53
  if (wait.timedOutAt !== undefined || now < wait.since + timeoutMs) return false;
54
54
  if (wait.kind === "landed") {
55
55
  const workspace = await deliveredWorkspace(run);
@@ -65,13 +65,15 @@ export async function reconcileRun(sup: Supervisor, run: WorkflowRunRecord, land
65
65
  const wait = run.awaiting;
66
66
  if (!wait || (await invoke<{ present: boolean }>(sup, "hasSignal", { key: wait.key })).present) return false;
67
67
  if (wait.kind === "for-each") return false;
68
+ // #3757 — a paused run's deadlines stand still: they are read at its pause.
69
+ const now = workflowRunClock(run, supervisorNow());
68
70
  let payload: Json | undefined;
69
71
  let kind: WorkflowSignal["kind"] = wait.kind;
70
72
  const taskId = wait.taskId ?? await effectTaskId(sup, wait.effectId);
71
73
  if (wait.kind === "decision") {
72
74
  payload = workflowDecisionSignal(wait.effectId!);
73
75
  if (!payload) {
74
- if (wait.deadline === undefined || (testOptions.now ?? Date.now)() < wait.deadline) return false;
76
+ if (wait.deadline === undefined || now < wait.deadline) return false;
75
77
  kind = "effect-failed";
76
78
  payload = { effect: "decision", error: "DECISION_DEADLINE: operator decision reached its explicit deadline" };
77
79
  }
@@ -80,7 +82,7 @@ export async function reconcileRun(sup: Supervisor, run: WorkflowRunRecord, land
80
82
  if (!effect?.result?.childRunId) return false;
81
83
  const child = await invoke<WorkflowRunRecord>(sup, "get", { runId: effect.result.childRunId });
82
84
  if (child.status === "running") {
83
- if (wait.deadline !== undefined && (testOptions.now ?? Date.now)() >= wait.deadline) return (await invoke<{ created: boolean }>(sup, "parkChild", { runId: run.id, childRunId: child.id, reason: `CHILD_TIMEOUT: ${child.id} exceeded ${wait.deadline}` })).created;
85
+ if (wait.deadline !== undefined && now >= wait.deadline) return (await invoke<{ created: boolean }>(sup, "parkChild", { runId: run.id, childRunId: child.id, reason: `CHILD_TIMEOUT: ${child.id} exceeded ${wait.deadline}` })).created;
84
86
  return false;
85
87
  }
86
88
  if (child.status === "escalated") {
@@ -112,19 +114,19 @@ export async function reconcileRun(sup: Supervisor, run: WorkflowRunRecord, land
112
114
  payload = taskId === undefined ? undefined : await deliverySignal(run, taskId, wait.afterEventId, wait.since);
113
115
  if (!payload) {
114
116
  const { effect } = await invoke<{ effect?: WorkflowEffect }>(sup, "getEffect", { id: wait.effectId ?? workflowEffectId(run.id, run.waitSeq, "dispatch") });
115
- payload = await spawnFailureSignal(effect, (testOptions.now ?? Date.now)());
117
+ payload = await spawnFailureSignal(effect, now);
116
118
  if (payload) kind = "effect-failed";
117
119
  }
118
120
  if (!payload) {
119
- payload = await exitSignal(sup, run);
121
+ payload = await exitSignal(sup, run, now);
120
122
  if (payload) kind = payload.effect === "dispatch" ? "effect-failed" : "agent-exited";
121
123
  }
122
124
  } else if (wait.kind === "ci") {
123
125
  // A CI wait ends on its verdict, never on a timeout alert: past the deadline it reports CI red or "no CI".
124
- const outcome = await ciMainOutcome(run, wait, (testOptions.now ?? Date.now)(), (await deliveredWorkspace(run))?.baseRef);
126
+ const outcome = await ciMainOutcome(run, wait, now, (await deliveredWorkspace(run))?.baseRef);
125
127
  if (outcome.red && (await invoke<{ created: boolean }>(sup, "alert", { runId: run.id, waitKey: wait.key, ...outcome.red })).created) return true;
126
128
  if (!outcome.verdict) return false;
127
- if (outcome.verdict.status === "none" && wait.deadline !== undefined && (testOptions.now ?? Date.now)() >= wait.deadline
129
+ if (outcome.verdict.status === "none" && wait.deadline !== undefined && now >= wait.deadline
128
130
  && (await invoke<{ created: boolean }>(sup, "alert", { runId: run.id, waitKey: wait.key, key: "ci-late", reason: "ci-late", detail: { ci: outcome.verdict, landedSha: wait.sha } })).created) return true;
129
131
  payload = outcome.verdict;
130
132
  } else {
@@ -153,10 +155,10 @@ export async function reconcileRun(sup: Supervisor, run: WorkflowRunRecord, land
153
155
  }
154
156
 
155
157
  /** #3166 — the wait's step agent exited without a delivery, has stayed gone past the grace, and is not live again. */
156
- async function exitSignal(sup: Supervisor, run: WorkflowRunRecord): Promise<Json | undefined> {
158
+ async function exitSignal(sup: Supervisor, run: WorkflowRunRecord, now: number): Promise<Json | undefined> {
157
159
  const { agent } = await invoke<{ agent?: WorkflowAgent }>(sup, "exitedWaitAgent", { runId: run.id });
158
160
  if (agent?.spawnFailedAt && !agent.readyAt) return { effect: "dispatch", error: `spawn ${agent.spawnRequestId}: ${agent.spawnError ?? "spawn failed"}` };
159
- if (!agent?.exitedAt || (testOptions.now ?? Date.now)() - agent.exitedAt < AGENT_EXIT_GRACE_MS) return undefined;
161
+ if (!agent?.exitedAt || now - agent.exitedAt < AGENT_EXIT_GRACE_MS) return undefined;
160
162
  return agentExitSignal(agent);
161
163
  }
162
164
 
@@ -40,6 +40,7 @@ export function runView(run: WorkflowRunRecord): WorkflowRunView {
40
40
  ...(run.awaiting ? { awaiting: awaitingView(run.awaiting) } : {}),
41
41
  ...(run.overdueAt !== undefined ? { overdueAt: run.overdueAt } : {}),
42
42
  ...(run.overdueNotifiedAt !== undefined ? { overdueNotifiedAt: run.overdueNotifiedAt } : {}),
43
+ ...(run.pause ? { paused: run.pause } : {}),
43
44
  ...(run.reason ? { reason: run.reason } : {}),
44
45
  ...(run.resumes?.length ? { resumes: run.resumes } : {}),
45
46
  ...(run.output ? { output: run.output } : {}),
@@ -57,6 +58,7 @@ export function leanRunView(run: WorkflowRunRecord, agents: WorkflowRunAgent[]):
57
58
  ...(run.overdueAt !== undefined ? { overdueAt: run.overdueAt } : {}),
58
59
  ...(run.overdueNotifiedAt !== undefined ? { overdueNotifiedAt: run.overdueNotifiedAt } : {}),
59
60
  ...(run.awaiting ? { awaiting: awaitingView(run.awaiting) } : {}),
61
+ ...(run.pause ? { paused: run.pause } : {}),
60
62
  ...(run.reason ? { reason: workflowText(run.reason) } : {}),
61
63
  ...(run.output ? { output: { ...run.output, ...(run.output.ci ? { ci: { status: run.output.ci.status, ...(run.output.ci.sha ? { sha: run.output.ci.sha } : {}) } } : {}) } } : {}),
62
64
  agents: agents.map((agent) => ({
@@ -43,6 +43,7 @@ import { leanRunView, runView } from "./workflow-run-view";
43
43
  import type { AuthContext } from "./auth-context";
44
44
 
45
45
  import { invoke, live, runAgents, requestPump, serialized, ensureSupervisor, type Supervisor } from "./workflow-supervisor";
46
+ import { supervisorNow } from "./workflow-supervisor-state";
46
47
  export { workflowAgentObligation } from "./workflow-agent-obligation";
47
48
  import { reconcileRun } from "./workflow-reconcile";
48
49
  import { holdWorkflowVerification } from "./workflow-verification";
@@ -198,12 +199,7 @@ export async function readWorkflowRunBudget(runId: string, ctx: AuthContext): Pr
198
199
 
199
200
  /** #3352 — a coordinator amends a live run's cap; new spawns see it at their next admission. */
200
201
  export async function amendWorkflowRunBudget(input: { runId: string; tokenCap: number }, ctx: AuthContext): Promise<Json> {
201
- const sup = await ensureSupervisor();
202
- const run = await readRun(sup, input.runId);
203
- const task = await getTaskDetailAsync(run.taskId);
204
- if (!task || !(ctx.callerAgentId === run.callerAgentId || canControlTaskReviewHold(ctx, task))) {
205
- throw new ServiceAuthError(`amending the budget of workflow run ${run.id} is coordinator-only — ${COORDINATOR_AUTHORITY_HINT}`);
206
- }
202
+ const { sup, run } = await controlledRun(input.runId, ctx, "amending the budget of");
207
203
  const amended = await serialized(sup, () => invoke<WorkflowRunRecord>(sup, "amendTokenBudget", { runId: run.id, tokenCap: input.tokenCap })).catch((error: unknown) => {
208
204
  if (error instanceof OpError && error.code === "conflict") throw new ValidationError(error.message);
209
205
  throw error;
@@ -270,13 +266,7 @@ export async function resumeWorkflowRun(input: { runId: string; activationId?: s
270
266
  /** relay_workflow_cancel (#3166): end a running or escalated run cancelled. A running run ends at once, its step
271
267
  * agents reaped per their `reap`; the Task, its review hold, any delivery and an armed land stay as they are. */
272
268
  export async function cancelWorkflowRun(input: { runId: string; reason?: string }, ctx: AuthContext): Promise<Json> {
273
- const sup = await ensureSupervisor();
274
- const run = await readRun(sup, input.runId);
275
- const task = await getTaskDetailAsync(run.taskId);
276
- if (!task || !(ctx.callerAgentId === run.callerAgentId || canControlTaskReviewHold(ctx, task))) {
277
- throw new ServiceAuthError(`cancelling workflow run ${run.id} is coordinator-only — ${COORDINATOR_AUTHORITY_HINT}`);
278
- }
279
- const by = ctx.callerAgentId ?? ctx.actor.id;
269
+ const { sup, run, by } = await controlledRun(input.runId, ctx, "cancelling");
280
270
  const reason = `cancelled by ${by}${input.reason ? `: ${input.reason}` : ""}`;
281
271
  // Between passes, so the cancel lands before the next routing.
282
272
  const { cancelled } = await serialized(sup, () => invoke<{ cancelled: boolean }>(sup, "cancel", { runId: run.id, reason, by })).catch((error: unknown) => {
@@ -287,6 +277,32 @@ export async function cancelWorkflowRun(input: { runId: string; reason?: string
287
277
  return { ...runView(await readRun(sup, run.id)), cancelled };
288
278
  }
289
279
 
280
+ /** The run a coordinator-only control acts on: its caller, the input Task's coordinators and admins may. */
281
+ async function controlledRun(runId: string, ctx: AuthContext, verb: string): Promise<{ sup: Supervisor; run: WorkflowRunRecord; by: string }> {
282
+ const sup = await ensureSupervisor();
283
+ const run = await readRun(sup, runId);
284
+ const task = await getTaskDetailAsync(run.taskId);
285
+ if (!task || !(ctx.callerAgentId === run.callerAgentId || canControlTaskReviewHold(ctx, task))) {
286
+ throw new ServiceAuthError(`${verb} workflow run ${run.id} is coordinator-only — ${COORDINATOR_AUTHORITY_HINT}`);
287
+ }
288
+ return { sup, run, by: ctx.callerAgentId ?? ctx.actor.id };
289
+ }
290
+
291
+ /** #3757 — pause a running run at its next seam (reason required) or unpause it; its agents keep working meanwhile. */
292
+ export async function pauseWorkflowRun(input: { runId: string; pause: boolean; reason?: string }, ctx: AuthContext): Promise<Json> {
293
+ const reason = input.reason?.trim();
294
+ if (input.pause && !reason) throw new ValidationError("pausing a workflow run needs a reason (MCP: note): say why it waits and for what");
295
+ const { sup, run, by } = await controlledRun(input.runId, ctx, input.pause ? "pausing" : "unpausing");
296
+ // Between passes, so no pass routes or performs with a stale view of the pause.
297
+ await serialized(sup, () => invoke(sup, input.pause ? "pause" : "unpause", { runId: run.id, by, now: supervisorNow(), ...(input.pause ? { reason } : {}) }))
298
+ .catch((error: unknown) => {
299
+ if (error instanceof OpError && (error.code === "conflict" || error.code === "invalid_input")) throw new ValidationError(error.message);
300
+ throw error;
301
+ });
302
+ await requestPump(sup);
303
+ return runView(await readRun(sup, run.id));
304
+ }
305
+
290
306
  /** relay_workflow_get: the run's visible record, after every pass already scheduled. */
291
307
  export async function getWorkflowRun(runId: string, ctx: AuthContext, options: WorkflowRunReadOptions = {}): Promise<Json> {
292
308
  const sup = await ensureSupervisor();
@@ -45,6 +45,11 @@ let stopped = Promise.resolve();
45
45
  let changeTimer: Timer | undefined;
46
46
  export let testOptions: SupervisorTestOptions = {};
47
47
 
48
+ /** The supervisor's clock: deadlines are read and a pause is stamped against it. */
49
+ export function supervisorNow(): number {
50
+ return (testOptions.now ?? Date.now)();
51
+ }
52
+
48
53
  /** The supervisor bound to the current database handle (a test that swaps the handle rebuilds it). */
49
54
  export function live(): Supervisor | undefined {
50
55
  return current && current.generation === getDbGeneration() ? current : undefined;
@@ -25,9 +25,13 @@ function startSupervisor(): Supervisor {
25
25
  persistence: createGatewayKernelPersistence(),
26
26
  leaseTtlMs: testOptions.leaseTtlMs ?? LEASE_TTL_MS,
27
27
  opCommitFaults: testOptions.opCommitFaults,
28
- // A wait is satisfied by its signal row: one keyed read instead of the default event-log scan.
29
- workflowWaitEvaluator: async (waitFor) => waitFor.kind === "event" && waitFor.correlationId !== undefined
30
- && ((await host.invoke(workflow, "hasSignal", SUPERVISOR, { key: waitFor.correlationId })) as { present: boolean }).present,
28
+ // A wait is satisfied by its signal row: one keyed read instead of the default event-log scan. A paused run's
29
+ // signal waits for its unpause (#3757).
30
+ workflowWaitEvaluator: async (waitFor) => {
31
+ if (waitFor.kind !== "event" || waitFor.correlationId === undefined) return false;
32
+ const wait = await host.invoke(workflow, "hasSignal", SUPERVISOR, { key: waitFor.correlationId }) as { present: boolean; paused?: boolean };
33
+ return wait.present && !wait.paused;
34
+ },
31
35
  });
32
36
  host.register(workflow);
33
37
  const sup: Supervisor = {
@@ -32,7 +32,6 @@ import {
32
32
  SPAWN_PROVIDERS,
33
33
  TEAM_IDLE_THRESHOLD_MINUTES_MAX,
34
34
  TEAM_IDLE_THRESHOLD_MINUTES_MIN,
35
- VALID_EFFORTS,
36
35
  type JSONSchema,
37
36
  type SettingDescriptor,
38
37
  type SettingScope,
@@ -166,7 +165,7 @@ const STEWARD_SCHEMA: JSONSchema = {
166
165
  enabled: { type: "boolean" },
167
166
  provider: { type: "string", enum: [...SPAWN_PROVIDERS] },
168
167
  model: { type: "string", maxLength: 120 },
169
- effort: { type: "string", enum: [...VALID_EFFORTS] },
168
+ effort: { type: "string", maxLength: 120 },
170
169
  permissionMode: { type: "string", enum: [...APPROVAL_MODES] },
171
170
  keepaliveSeconds: { type: "integer", minimum: 0, maximum: 2_592_000 },
172
171
  },
@@ -112,11 +112,12 @@ export function resolveSpawnModelParams(
112
112
  // #2599 — the full record is server-computed once from the effective-catalog selection:
113
113
  // qualified runtime name + both windows + the explicit threshold. Same positive-int gates
114
114
  // as the legacy window (never 0/NaN/negative), via the shared builder.
115
- const record = runnerSupportsModelRecord(opts.runnerProtocol)
115
+ const record = runnerSupportsModelRecord(opts.runnerProtocol ?? MODEL_RECORD_PROTOCOL_VERSION)
116
116
  ? resolvedModelRecordFromSelection({
117
117
  alias: selection.modelAlias,
118
118
  providerModel: selection.providerModel,
119
119
  effort: selection.effort,
120
+ efforts: selection.model?.efforts,
120
121
  qualifiedModel: qualifyServerModelName(provider, selection.providerModel, effectiveProviderCatalogList()),
121
122
  limits: selection.model?.limits,
122
123
  ...(selection.model?.contextThreshold ? { contextThreshold: selection.model.contextThreshold } : {}),
@@ -135,7 +136,6 @@ export function resolveSpawnModelParams(
135
136
  ...(record ? { modelRecord: record } : {}),
136
137
  };
137
138
  } catch (error) {
138
- if (opts.modelSource === "runtime") return { ...(model ? { model } : {}), ...(effort ? { effort } : {}) };
139
139
  throw new ValidationError(errMessage(error));
140
140
  }
141
141
  }
@@ -157,11 +157,12 @@ export async function resolveSpawnModelParamsAsync(
157
157
  );
158
158
  const windowTokens = selection.model?.limits?.contextWindowTokens?.value;
159
159
  // #2599 — same full-record twin as the sync path (see its comment).
160
- const record = runnerSupportsModelRecord(opts.runnerProtocol)
160
+ const record = runnerSupportsModelRecord(opts.runnerProtocol ?? MODEL_RECORD_PROTOCOL_VERSION)
161
161
  ? resolvedModelRecordFromSelection({
162
162
  alias: selection.modelAlias,
163
163
  providerModel: selection.providerModel,
164
164
  effort: selection.effort,
165
+ efforts: selection.model?.efforts,
165
166
  qualifiedModel: qualifyServerModelName(provider, selection.providerModel, liveCatalog),
166
167
  limits: selection.model?.limits,
167
168
  ...(selection.model?.contextThreshold ? { contextThreshold: selection.model.contextThreshold } : {}),
@@ -176,7 +177,6 @@ export async function resolveSpawnModelParamsAsync(
176
177
  ...(record ? { modelRecord: record } : {}),
177
178
  };
178
179
  } catch (error) {
179
- if (opts.modelSource === "runtime") return { ...(model ? { model } : {}), ...(effort ? { effort } : {}) };
180
180
  throw new ValidationError(errMessage(error));
181
181
  }
182
182
  }
@@ -2,21 +2,19 @@ import { isOpenAiApiModel } from "@humwick/sdk";
2
2
  import type { ProviderCatalogEntry } from "@humwick/sdk/provider-catalog";
3
3
  import type { Orchestrator, SpawnProvider } from "./types";
4
4
 
5
- /** A missing report preserves compatibility with older orchestrators; a reported catalog is authoritative. */
6
- export function hostSupportsModel(host: Orchestrator, provider: SpawnProvider, model?: string, effort?: string): boolean {
7
- const catalog = host.providerCatalog?.find((entry) => entry.provider === provider);
8
- if (!model || !catalog) return !isOpenAiApiModel(provider, model);
9
- const status = catalog.models.find((entry) => entry.alias === model || entry.providerModel === model);
10
- return status !== undefined && !status.disabled
11
- && (!effort || status.efforts.includes(effort));
5
+ /** Hosts report local capabilities; model definitions and efforts belong to the hub catalog. */
6
+ export function hostSupportsModel(host: Orchestrator, provider: SpawnProvider, model?: string): boolean {
7
+ if (!isOpenAiApiModel(provider, model)) return true;
8
+ const ready = host.providerStatus?.find((entry) => entry.name === provider)?.features?.openaiApi;
9
+ if (ready !== undefined) return ready;
10
+ // Older hosts encoded credential readiness by including the paid model in their catalog.
11
+ return host.providerCatalog?.find((entry) => entry.provider === provider)?.models
12
+ .some((entry) => isOpenAiApiModel(provider, entry.alias) && !entry.disabled) ?? false;
12
13
  }
13
14
 
14
- /** Keep Relay's model definitions and settings, intersected with the host's reported capabilities. */
15
+ /** Project the hub catalog onto the host's genuinely local capabilities. */
15
16
  export function hostRunnableCatalog(entry: ProviderCatalogEntry, host: Orchestrator): ProviderCatalogEntry {
16
17
  const models = entry.models.filter((model) => !model.disabled && !model.unavailable
17
- && hostSupportsModel(host, entry.provider, model.alias)).map((model) => {
18
- const efforts = model.efforts.filter((effort) => hostSupportsModel(host, entry.provider, model.alias, effort));
19
- return { ...model, efforts, defaultEffort: model.defaultEffort && efforts.includes(model.defaultEffort) ? model.defaultEffort : undefined };
20
- });
18
+ && hostSupportsModel(host, entry.provider, model.alias));
21
19
  return { ...entry, models };
22
20
  }
@@ -321,7 +321,7 @@ export function hostSpawnAdmission(
321
321
  if (view?.policy.spawnEnabled === false) add("host", "draining", `host ${item.hostname} (${item.id}) is draining: new spawns disabled`);
322
322
  if (!pinned && view && (!view.enabled || view.state === "unavailable" || view.state === "retired" || view.band === "blocked")) add("host", "unavailable", "host unavailable or blocked");
323
323
  if (!pinned && view && view.aggregates.agentSlotsMax !== 0 && view.aggregates.agentSlotsUsed >= view.aggregates.agentSlotsMax) add("capacity", "slots", "host spawn slots full");
324
- if (!pinned && !hostSupportsModel(item, provider, model, effort)) add("provider", "model", `provider '${provider}' does not support requested model or effort`);
324
+ if (!pinned && !hostSupportsModel(item, provider, model)) add("provider", "model", `provider '${provider}' does not support requested model or effort`);
325
325
  if (capacity) add("capacity", "agent_cap", `host ${item.hostname} (${item.id}) is ${capacity}`);
326
326
  return assessSpawnAdmission(item, provider, checks, pinnedAuth);
327
327
  }
@@ -290,7 +290,7 @@ function providerMatrixFor(
290
290
  const providers = [...new Set([...o.providers, ...(o.providerStatus ?? []).filter((s) => s.degraded || s.cli?.transient || s.runner?.transient || s.auth?.transient).map((s) => s.name)])];
291
291
  return providers.filter((provider) => !disabledProviders.has(provider) && !hostDisabledFor?.has(provider)).map((provider) => {
292
292
  const globalEntry = global.find((e) => e.provider === provider);
293
- // Relay owns model settings; the heartbeat limits what this host can run (#3202).
293
+ // The hub owns models and efforts; hosts contribute only local capabilities.
294
294
  const cat = globalEntry ? hostRunnableCatalog(globalEntry, o) : undefined;
295
295
  const quotaAdvisory = quotaAdvisories.get(provider) ?? defaultQuotaAdvisory(provider);
296
296
  // #932 — advertise the SAME default a bare spawn would actually resolve to. resolveDefaultModel