@humwick/server 0.130.1 → 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 (437) hide show
  1. package/README.md +5 -0
  2. package/docs/guides/tools-administration.md +4 -0
  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-B_rFEbnL.js → fleet-C7jQWbw2.js} +2 -2
  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 +8 -0
  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 +3 -3
  128. package/src/config-store-write.ts +13 -1
  129. package/src/config-store.ts +25 -14
  130. package/src/db/activity-async.ts +6 -1
  131. package/src/db/activity-write-ops.ts +12 -1
  132. package/src/db/activity.ts +1 -14
  133. package/src/db/agent-search.ts +1 -15
  134. package/src/db/agents-async.ts +52 -5
  135. package/src/db/agents-write-ops.ts +12 -3
  136. package/src/db/agents.ts +81 -54
  137. package/src/db/ci-incidents-async.ts +5 -5
  138. package/src/db/ci-incidents-write-ops.ts +2 -2
  139. package/src/db/command-row.ts +10 -2
  140. package/src/db/commands-list-query.ts +2 -0
  141. package/src/db/commands-spawn-read.ts +1 -1
  142. package/src/db/config-store-write-ops.ts +7 -2
  143. package/src/db/continuation-archives-async.ts +5 -0
  144. package/src/db/continuation-archives-write-ops.ts +7 -0
  145. package/src/db/continuations.ts +2 -2
  146. package/src/db/db-writer-worker.ts +8 -1
  147. package/src/db/deploy-events-async.ts +1 -0
  148. package/src/db/deploy-events-read-ops.ts +2 -1
  149. package/src/db/deploy-events.ts +35 -4
  150. package/src/db/hosts-registration.ts +10 -2
  151. package/src/db/hosts.ts +9 -6
  152. package/src/db/inbox.ts +0 -14
  153. package/src/db/index.ts +2 -0
  154. package/src/db/integrations-async.ts +8 -0
  155. package/src/db/integrations-ops.ts +15 -0
  156. package/src/db/integrations.ts +15 -21
  157. package/src/db/issue-lifecycle.ts +3 -4
  158. package/src/db/issue-refs-async.ts +20 -0
  159. package/src/db/issue-refs-write-ops.ts +29 -0
  160. package/src/db/issue-refs.ts +7 -8
  161. package/src/db/long-running-work.ts +2 -2
  162. package/src/db/main-connection-writes.ts +23 -2
  163. package/src/db/maintenance-job-reconcile.ts +61 -0
  164. package/src/db/maintenance-job-runs-async.ts +14 -0
  165. package/src/db/maintenance-job-write-ops.ts +9 -0
  166. package/src/db/mappers.ts +2 -14
  167. package/src/db/merge-lease.ts +1 -15
  168. package/src/db/migrations.ts +7 -15
  169. package/src/db/orchestrators.ts +4 -72
  170. package/src/db/pairs.ts +1 -13
  171. package/src/db/project-profile-instructions.ts +50 -0
  172. package/src/db/prompt-versions.ts +9 -2
  173. package/src/db/provider-config-history.ts +8 -3
  174. package/src/db/provider-model-seed.ts +34 -0
  175. package/src/db/read-op-marshaling-fixtures.ts +5 -0
  176. package/src/db/read-ops.ts +4 -0
  177. package/src/db/routing-policy.ts +2 -3
  178. package/src/db/schema.ts +9 -3
  179. package/src/db/spawn-delegation-state.ts +11 -4
  180. package/src/db/stats-async.ts +4 -3
  181. package/src/db/task-entity.ts +23 -2
  182. package/src/db/task-list.ts +3 -1
  183. package/src/db/tasks.ts +1 -17
  184. package/src/db/timer-ingress.ts +7 -1
  185. package/src/db/token-db-write-ops.ts +17 -0
  186. package/src/db/token-standing.ts +15 -11
  187. package/src/db/workflow-contact.ts +1 -1
  188. package/src/db/workflow-task-read-ops.ts +30 -0
  189. package/src/db/write-ops.ts +6 -0
  190. package/src/db/writer-gateway.ts +17 -11
  191. package/src/dispatch-availability.ts +10 -6
  192. package/src/ephemeral/{landing-ingress-proof-worker.ts → landing-ingress-proof.test-fixture.ts} +4 -2
  193. package/src/host/disk-alert.ts +121 -0
  194. package/src/lifecycle-manager.ts +3 -2
  195. package/src/maintenance/cd-watcher.ts +3 -3
  196. package/src/maintenance/critical-job-watchdog.ts +16 -17
  197. package/src/maintenance/index.ts +1 -0
  198. package/src/maintenance/jobs.ts +3 -2
  199. package/src/maintenance/scheduler.ts +84 -128
  200. package/src/maintenance/stalled-delivery-pager.ts +6 -5
  201. package/src/maintenance/workspaces.ts +1 -1
  202. package/src/managed-policy.ts +31 -27
  203. package/src/mcp/index.ts +5 -2
  204. package/src/mcp/output-descriptions.ts +8 -8
  205. package/src/mcp/presentation.ts +4 -4
  206. package/src/mcp/project-profile-instructions.ts +32 -0
  207. package/src/mcp/tool-groups.ts +1 -1
  208. package/src/mcp/tools-spawn.ts +4 -3
  209. package/src/mcp-agent-action.ts +1 -2
  210. package/src/mcp-issue-ref-tools.ts +12 -11
  211. package/src/mcp-maintenance-tools.ts +2 -2
  212. package/src/mcp-profile-trust-tools.ts +4 -4
  213. package/src/mcp-provider-catalog.ts +2 -2
  214. package/src/mcp-task-tools.ts +11 -5
  215. package/src/mcp-team-template-tools.ts +3 -3
  216. package/src/mcp-workflow-tools.ts +10 -3
  217. package/src/notification-types.ts +2 -1
  218. package/src/operator-alarm.ts +26 -11
  219. package/src/project-instructions.ts +4 -0
  220. package/src/project-profile-instruction-text.ts +6 -0
  221. package/src/project-profile-instructions.ts +20 -0
  222. package/src/provider-catalog-store.ts +3 -49
  223. package/src/provider-config-store.ts +12 -11
  224. package/src/provider-model-seed.ts +1 -18
  225. package/src/provider-quota-config-store.ts +5 -4
  226. package/src/provider-secrets.test-fixture.ts +3 -3
  227. package/src/recipe-runner.ts +2 -2
  228. package/src/repo-config-source.ts +10 -4
  229. package/src/routes/agent-profiles.ts +6 -6
  230. package/src/routes/agents-spawn.ts +1 -1
  231. package/src/routes/agents.ts +6 -6
  232. package/src/routes/commands-listing.ts +4 -0
  233. package/src/routes/commands.ts +2 -2
  234. package/src/routes/continuation-archives.ts +2 -2
  235. package/src/routes/index.ts +10 -1
  236. package/src/routes/integrations.ts +24 -50
  237. package/src/routes/issue-refs.ts +7 -7
  238. package/src/routes/orchestrator-bootstrap.ts +1 -1
  239. package/src/routes/orchestrator-host-registration.ts +2 -2
  240. package/src/routes/orchestrator-host-utilization.ts +3 -1
  241. package/src/routes/orchestrator-proxy.ts +2 -2
  242. package/src/routes/orchestrator.ts +2 -2
  243. package/src/routes/project-profile-instructions.ts +27 -0
  244. package/src/routes/project-trust.ts +2 -2
  245. package/src/routes/provider-config.ts +6 -6
  246. package/src/routes/provider-quota-config.ts +2 -2
  247. package/src/routes/settings.ts +5 -5
  248. package/src/routes/stats.ts +6 -6
  249. package/src/routes/task-http.ts +15 -1
  250. package/src/routes/task-issues.ts +11 -26
  251. package/src/routes/tasks.ts +22 -53
  252. package/src/routes/team-templates.ts +5 -5
  253. package/src/routes/teams-config.ts +2 -2
  254. package/src/routes/tokens.ts +12 -5
  255. package/src/routes/workflows.ts +20 -5
  256. package/src/routing-policy-normalize.ts +1 -2
  257. package/src/routing-step-defaults.ts +0 -2
  258. package/src/runner-token-mint-limit.ts +63 -0
  259. package/src/runtime-grant.ts +4 -2
  260. package/src/runtime-tokens.ts +39 -7
  261. package/src/security.ts +13 -20
  262. package/src/server-background.ts +1 -1
  263. package/src/server.ts +5 -2
  264. package/src/services/host-utilization.ts +23 -0
  265. package/src/services/idle-epics.ts +5 -6
  266. package/src/services/issue-lifecycle.ts +57 -61
  267. package/src/services/issue-refs.ts +9 -13
  268. package/src/services/kernel-timers-cutover-chaos-worker.ts +10 -7
  269. package/src/services/kernel-timers-ingress.ts +1 -1
  270. package/src/services/landed-ancestry.ts +19 -3
  271. package/src/services/memory-areas.ts +15 -51
  272. package/src/services/plan-ci-notice.ts +57 -0
  273. package/src/services/plan-ci-recovery.ts +2 -55
  274. package/src/services/plan-wake-digest.ts +1 -0
  275. package/src/services/profile-trust.ts +4 -3
  276. package/src/services/project-config-read.ts +70 -0
  277. package/src/services/project-profile-instructions.ts +54 -0
  278. package/src/services/provisioning-harness/budget-baseline.json +4 -4
  279. package/src/services/restart-spawn.ts +61 -49
  280. package/src/services/self-care-state.ts +2 -1
  281. package/src/services/self-effort.ts +5 -5
  282. package/src/services/send-message.ts +5 -1
  283. package/src/services/spawn-agent.ts +13 -2
  284. package/src/services/spawn-chain-inspector.ts +6 -13
  285. package/src/services/spawn-policy.ts +68 -0
  286. package/src/services/spawn-tree-cleanup.ts +2 -2
  287. package/src/services/task-dispatch.ts +3 -0
  288. package/src/services/task-state-reconcile.ts +6 -14
  289. package/src/services/team-templates.ts +9 -8
  290. package/src/services/transient-agent-reaper.ts +3 -1
  291. package/src/services/workflow-auto-start.ts +25 -13
  292. package/src/services/workflow-ci-main.ts +1 -1
  293. package/src/services/workflow-deferred-cleanup.ts +4 -2
  294. package/src/services/workflow-effect-driver.ts +2 -0
  295. package/src/services/workflow-effects.ts +18 -11
  296. package/src/services/workflow-land-notice.ts +10 -0
  297. package/src/services/workflow-pause-text.ts +43 -0
  298. package/src/services/workflow-reconcile.ts +13 -11
  299. package/src/services/workflow-run-view.ts +2 -0
  300. package/src/services/workflow-runs.ts +29 -13
  301. package/src/services/workflow-supervisor-state.ts +5 -0
  302. package/src/services/workflow-supervisor.ts +7 -3
  303. package/src/settings/registry.ts +1 -2
  304. package/src/spawn-command.ts +4 -4
  305. package/src/spawn-models.ts +10 -12
  306. package/src/spawn-placement.ts +1 -1
  307. package/src/spawn-targets.ts +1 -1
  308. package/src/startup-context-injection.ts +1 -0
  309. package/src/state-adoption/cd-cutover.ts +53 -7
  310. package/src/state-adoption/fleet-cutover.ts +35 -0
  311. package/src/state-adoption/fleet-plan.ts +36 -0
  312. package/src/state-adoption/index.ts +2 -2
  313. package/src/state-adoption/linux-plan.ts +2 -2
  314. package/src/state-adoption/linux.ts +2 -2
  315. package/src/state-adoption/macos-plan.ts +2 -2
  316. package/src/state-adoption/macos.ts +1 -1
  317. package/src/steward.ts +7 -6
  318. package/src/task-dispatch-live.ts +1 -1
  319. package/src/task-dispatcher.ts +1 -1
  320. package/src/teams-config-store.ts +4 -3
  321. package/src/token-db-write.ts +10 -0
  322. package/src/workspace-command-settle.ts +2 -1
  323. package/src/workspace-steward-wake.ts +2 -2
  324. package/node_modules/@humwick/kernel/LICENSE +0 -167
  325. package/node_modules/@humwick/kernel/async-entity-persistence.ts +0 -344
  326. package/node_modules/@humwick/kernel/async-event-log.ts +0 -107
  327. package/node_modules/@humwick/kernel/automations.pilot.ts +0 -147
  328. package/node_modules/@humwick/kernel/config.ts +0 -111
  329. package/node_modules/@humwick/kernel/contract.ts +0 -572
  330. package/node_modules/@humwick/kernel/control-authz.ts +0 -281
  331. package/node_modules/@humwick/kernel/control.ts +0 -645
  332. package/node_modules/@humwick/kernel/effects.ts +0 -493
  333. package/node_modules/@humwick/kernel/event-log.ts +0 -319
  334. package/node_modules/@humwick/kernel/http-transport.ts +0 -384
  335. package/node_modules/@humwick/kernel/identity-grammar.ts +0 -20
  336. package/node_modules/@humwick/kernel/identity.ts +0 -248
  337. package/node_modules/@humwick/kernel/index.ts +0 -28
  338. package/node_modules/@humwick/kernel/kernel-persistence.ts +0 -181
  339. package/node_modules/@humwick/kernel/landing-incident.ts +0 -306
  340. package/node_modules/@humwick/kernel/landing.ts +0 -953
  341. package/node_modules/@humwick/kernel/manifest-validation.ts +0 -460
  342. package/node_modules/@humwick/kernel/mcp-transport.ts +0 -109
  343. package/node_modules/@humwick/kernel/message-authority-chaos-worker.ts +0 -200
  344. package/node_modules/@humwick/kernel/message-authority.ts +0 -281
  345. package/node_modules/@humwick/kernel/module-host.ts +0 -722
  346. package/node_modules/@humwick/kernel/openapi-init.ts +0 -14
  347. package/node_modules/@humwick/kernel/package.json +0 -39
  348. package/node_modules/@humwick/kernel/persistence-schema.ts +0 -248
  349. package/node_modules/@humwick/kernel/persistence.ts +0 -510
  350. package/node_modules/@humwick/kernel/registry.ts +0 -60
  351. package/node_modules/@humwick/kernel/sqlite-kernel-persistence.ts +0 -499
  352. package/node_modules/@humwick/kernel/sqlite-module-host.ts +0 -87
  353. package/node_modules/@humwick/kernel/timers-chaos-worker.ts +0 -186
  354. package/node_modules/@humwick/kernel/timers.pilot.ts +0 -1411
  355. package/node_modules/@humwick/kernel/workflow-activation.ts +0 -25
  356. package/node_modules/@humwick/kernel/workflow-backstop.ts +0 -40
  357. package/node_modules/@humwick/kernel/workflow-cancel-tree.ts +0 -20
  358. package/node_modules/@humwick/kernel/workflow-chaos-worker.ts +0 -213
  359. package/node_modules/@humwick/kernel/workflow-composition.ts +0 -231
  360. package/node_modules/@humwick/kernel/workflow-container.ts +0 -72
  361. package/node_modules/@humwick/kernel/workflow-control.test-fixture.ts +0 -42
  362. package/node_modules/@humwick/kernel/workflow-control.ts +0 -178
  363. package/node_modules/@humwick/kernel/workflow-definition.ts +0 -507
  364. package/node_modules/@humwick/kernel/workflow-dispatch.ts +0 -60
  365. package/node_modules/@humwick/kernel/workflow-foreach.ts +0 -167
  366. package/node_modules/@humwick/kernel/workflow-journal-codec.ts +0 -339
  367. package/node_modules/@humwick/kernel/workflow-journal.ts +0 -922
  368. package/node_modules/@humwick/kernel/workflow-lane.ts +0 -48
  369. package/node_modules/@humwick/kernel/workflow-lease-owner.ts +0 -60
  370. package/node_modules/@humwick/kernel/workflow-recovery.test-fixture.ts +0 -19
  371. package/node_modules/@humwick/kernel/workflow-runtime.ts +0 -938
  372. package/node_modules/@humwick/kernel/workflow-script.ts +0 -11
  373. package/node_modules/@humwick/kernel/workflow-state.ts +0 -299
  374. package/node_modules/@humwick/kernel/workflow-step-commit.ts +0 -195
  375. package/node_modules/@humwick/kernel/workflow-transitions.ts +0 -317
  376. package/node_modules/@humwick/kernel/workflow-values.ts +0 -153
  377. package/node_modules/@humwick/kernel/workflow.pilot.ts +0 -376
  378. package/node_modules/@humwick/offload/LICENSE +0 -167
  379. package/node_modules/@humwick/offload/README.md +0 -57
  380. package/node_modules/@humwick/offload/dist/async-runner.d.ts +0 -18
  381. package/node_modules/@humwick/offload/dist/async-runner.d.ts.map +0 -1
  382. package/node_modules/@humwick/offload/dist/async-runner.js +0 -158
  383. package/node_modules/@humwick/offload/dist/async-runner.js.map +0 -1
  384. package/node_modules/@humwick/offload/dist/envelope.d.ts +0 -25
  385. package/node_modules/@humwick/offload/dist/envelope.d.ts.map +0 -1
  386. package/node_modules/@humwick/offload/dist/envelope.js +0 -80
  387. package/node_modules/@humwick/offload/dist/envelope.js.map +0 -1
  388. package/node_modules/@humwick/offload/dist/fixtures/worker-task.d.ts +0 -10
  389. package/node_modules/@humwick/offload/dist/fixtures/worker-task.d.ts.map +0 -1
  390. package/node_modules/@humwick/offload/dist/fixtures/worker-task.js +0 -59
  391. package/node_modules/@humwick/offload/dist/fixtures/worker-task.js.map +0 -1
  392. package/node_modules/@humwick/offload/dist/index.d.ts +0 -5
  393. package/node_modules/@humwick/offload/dist/index.d.ts.map +0 -1
  394. package/node_modules/@humwick/offload/dist/index.js +0 -5
  395. package/node_modules/@humwick/offload/dist/index.js.map +0 -1
  396. package/node_modules/@humwick/offload/dist/subprocess-runner.d.ts +0 -62
  397. package/node_modules/@humwick/offload/dist/subprocess-runner.d.ts.map +0 -1
  398. package/node_modules/@humwick/offload/dist/subprocess-runner.js +0 -295
  399. package/node_modules/@humwick/offload/dist/subprocess-runner.js.map +0 -1
  400. package/node_modules/@humwick/offload/dist/types.d.ts +0 -111
  401. package/node_modules/@humwick/offload/dist/types.d.ts.map +0 -1
  402. package/node_modules/@humwick/offload/dist/types.js +0 -176
  403. package/node_modules/@humwick/offload/dist/types.js.map +0 -1
  404. package/node_modules/@humwick/offload/dist/worker-pool.d.ts +0 -23
  405. package/node_modules/@humwick/offload/dist/worker-pool.d.ts.map +0 -1
  406. package/node_modules/@humwick/offload/dist/worker-pool.js +0 -435
  407. package/node_modules/@humwick/offload/dist/worker-pool.js.map +0 -1
  408. package/node_modules/@humwick/offload/dist/worker-runtime.d.ts +0 -2
  409. package/node_modules/@humwick/offload/dist/worker-runtime.d.ts.map +0 -1
  410. package/node_modules/@humwick/offload/dist/worker-runtime.js +0 -27
  411. package/node_modules/@humwick/offload/dist/worker-runtime.js.map +0 -1
  412. package/node_modules/@humwick/offload/dist/worker.d.ts +0 -5
  413. package/node_modules/@humwick/offload/dist/worker.d.ts.map +0 -1
  414. package/node_modules/@humwick/offload/dist/worker.js +0 -1
  415. package/node_modules/@humwick/offload/dist/worker.js.map +0 -1
  416. package/node_modules/@humwick/offload/package.json +0 -42
  417. package/node_modules/@humwick/offload/src/async-runner.ts +0 -170
  418. package/node_modules/@humwick/offload/src/envelope.ts +0 -92
  419. package/node_modules/@humwick/offload/src/fixtures/worker-task.ts +0 -49
  420. package/node_modules/@humwick/offload/src/index.ts +0 -4
  421. package/node_modules/@humwick/offload/src/subprocess-runner.ts +0 -334
  422. package/node_modules/@humwick/offload/src/types.ts +0 -235
  423. package/node_modules/@humwick/offload/src/worker-pool.ts +0 -463
  424. package/node_modules/@humwick/offload/src/worker-runtime.ts +0 -32
  425. package/node_modules/@humwick/offload/src/worker.ts +0 -5
  426. package/public/assets/agent-liveness-DyqxfB-t.js +0 -2
  427. package/public/assets/agent-profiles-BpI_jTxq.js +0 -2
  428. package/public/assets/index-B7ZerHLY.css +0 -2
  429. package/public/assets/index-BU3uWo2t.js +0 -15
  430. package/public/assets/projects-B_lmcqfO.js +0 -2
  431. package/public/assets/provider-display-DbLWpP3j.js +0 -2
  432. package/public/assets/run-card-CO_bguQf.js +0 -2
  433. package/public/assets/use-project-list-B-T2Bnnn.js +0 -2
  434. package/public/assets/versioned-prompt-editor-Diu8BIaM.js +0 -8
  435. package/runner/src/gate-run-lease-probe.ts +0 -38
  436. package/src/db/claude-catalog-reconcile.ts +0 -27
  437. package/src/routing-model-aliases.ts +0 -21
@@ -4,11 +4,11 @@ import { getGateway } from "../db/gateway";
4
4
  import { getAgentAsync } from "../db";
5
5
  import { shutdownAgent } from "./shutdown-agent";
6
6
  import { currentStop } from "./workflow-effects";
7
- import { invoke, testOptions, type Supervisor } from "./workflow-supervisor-state";
7
+ import { invoke, supervisorNow, type Supervisor } from "./workflow-supervisor-state";
8
8
 
9
9
  /** Every retry resumes the same outbox effect; only host witnesses release reservations. */
10
10
  export async function performSpawnTreeCleanup(sup: Supervisor, effect: WorkflowEffect): Promise<boolean> {
11
- const now = (testOptions.now ?? Date.now)();
11
+ const now = supervisorNow();
12
12
  if (typeof effect.result?.nextAttemptAt === "number" && effect.result.nextAttemptAt > now) return false;
13
13
  const agentId = String(effect.payload.agentId);
14
14
  await getGateway().write("spawnTree.prepareCleanup", { agentId });
@@ -105,6 +105,7 @@ export interface TaskDispatchSpawnInput {
105
105
  /** #3045 — the workflow run dispatching this step; #3091 the base profile its step pins, the run's checkout standing in
106
106
  * for the Task's repo root, and the host its role is read on first. Service-only. */
107
107
  workflowRunId?: string;
108
+ workflowNames?: string[];
108
109
  workflowCaller?: SpawnAgentInput["workflowCaller"];
109
110
  roleProfile?: string;
110
111
  taskRepoCwd?: string;
@@ -139,6 +140,7 @@ export async function spawnTaskDispatch(input: TaskDispatchSpawnInput, ctx: Auth
139
140
  role,
140
141
  roleSha, roleSnapshot,
141
142
  workflowRunId,
143
+ workflowNames: input.workflowNames,
142
144
  workflowCaller: input.workflowCaller,
143
145
  roleProfile,
144
146
  taskRepoCwd,
@@ -173,6 +175,7 @@ export async function spawnTaskDispatch(input: TaskDispatchSpawnInput, ctx: Auth
173
175
  ...(decision ? { decision } : {}),
174
176
  ...(reviewTaskId !== undefined ? { reviewTaskId } : {}),
175
177
  ...(spawn.warnings ? { warnings: spawn.warnings } : {}),
178
+ ...(spawn.spawnPolicy ? { spawnPolicy: spawn.spawnPolicy } : {}),
176
179
  ...(verbose ? { command: summarizeSpawnCommandForCaller(spawn.command) } : {}),
177
180
  };
178
181
  }
@@ -4,8 +4,9 @@
4
4
  // live in src/db/task-state-reconcile.ts; every change is applied by its conditional writer, which
5
5
  // re-checks the rule in the transaction, then audited here with one `task.reconciled` relay event.
6
6
  import { errMessage } from "@humwick/sdk";
7
- import { isSqliteBusyError, withFastFailConnection, writerLockAvailable } from "../db/connection";
8
- import { getIssueRef, updateIssueRef } from "../db/issue-refs";
7
+ import { writerLockAvailable } from "../db/connection";
8
+ import { getIssueRef } from "../db/issue-refs";
9
+ import { updateIssueRefAsync } from "../db/issue-refs-async";
9
10
  import { applyTaskStateReconcileAsync, listTaskStateReconcileCandidatesAsync } from "../db/task-state-reconcile-async";
10
11
  import { TASK_RECONCILE_ACTOR, type TaskReconcileRule, type TaskStateReconcileApplied } from "../db/task-state-reconcile";
11
12
  import { emitRelayEvent } from "../events";
@@ -104,19 +105,10 @@ async function refreshIssueRefs(refIds: string[], opts: TaskStateReconcileOption
104
105
  try {
105
106
  const live = await (opts.adapterResolver ?? resolveIssueTrackerAdapter)(ref).getStatus(ref);
106
107
  const status = live.status === "open" || live.status === "closed" ? live.status : ref.status;
107
- // The tracker read can take long: re-check the lock right before the write, and bound the write itself.
108
+ // The tracker read can take long: re-check the lock right before the write. The write itself waits on
109
+ // the writer thread, never on the loop (#1870 S7).
108
110
  if (steps.contended()) break;
109
- const written = await steps.timed(() => {
110
- try {
111
- withFastFailConnection(STEP_BUSY_TIMEOUT_MS, () => updateIssueRef(ref.id, { status, title: live.title, closeReason: live.closeReason, statusSyncedAt: Date.now() }));
112
- return true;
113
- } catch (error) {
114
- if (!isSqliteBusyError(error)) throw error;
115
- result.moreWork = true;
116
- return false;
117
- }
118
- });
119
- if (!written) break;
111
+ await updateIssueRefAsync(ref.id, { status, title: live.title, closeReason: live.closeReason, statusSyncedAt: Date.now() });
120
112
  closedAny ||= status === "closed";
121
113
  } catch (error) {
122
114
  refreshFailedAt.set(id, now);
@@ -1,6 +1,7 @@
1
1
  import { isRecord, SPAWN_PROVIDERS, VALID_WORKSPACE_MODES, APPROVAL_MODES, VALID_AGENT_LIFECYCLES } from "@humwick/sdk";
2
2
  import { getTeam, ValidationError } from "../db";
3
- import { deleteConfig, getConfig, listConfig, setConfig } from "../config-store";
3
+ import { getConfig, listConfig } from "../config-store";
4
+ import { deleteConfigAsync, setConfigAsync } from "../config-store-write";
4
5
  import { TEAM_TEMPLATE_NAMESPACE } from "../team-template-config";
5
6
  import { cleanString, optionalEnum } from "../validation";
6
7
  import type { AuthContext } from "./auth-context";
@@ -47,11 +48,11 @@ function withTimestamps(template: TeamTemplate, existing?: TeamTemplate): TeamTe
47
48
  };
48
49
  }
49
50
 
50
- export function saveTeamTemplate(input: Record<string, unknown>, updatedBy?: string): TeamTemplate {
51
+ export async function saveTeamTemplateAsync(input: Record<string, unknown>, updatedBy?: string): Promise<TeamTemplate> {
51
52
  if (input.teamId !== undefined && input.composition !== undefined) {
52
53
  throw new ValidationError("teamId capture and manual composition are mutually exclusive");
53
54
  }
54
- if (input.teamId !== undefined) return captureTeamTemplateFromTeam(cleanString(input.teamId, "teamId", { required: true, max: 120 })!, input, updatedBy);
55
+ if (input.teamId !== undefined) return captureTeamTemplateFromTeamAsync(cleanString(input.teamId, "teamId", { required: true, max: 120 })!, input, updatedBy);
55
56
  const slug = templateSlug(input);
56
57
  const existing = getConfig<TeamTemplate>(TEAM_TEMPLATE_NAMESPACE, slug)?.value;
57
58
  const template = withTimestamps({
@@ -64,7 +65,7 @@ export function saveTeamTemplate(input: Record<string, unknown>, updatedBy?: str
64
65
  ...(cleanString(input.definitionOfDone, "definitionOfDone", { max: 4000 }) ? { definitionOfDone: cleanString(input.definitionOfDone, "definitionOfDone", { max: 4000 }) } : {}),
65
66
  ...(input.planSkeleton !== undefined ? { planSkeleton: input.planSkeleton as TeamTemplate["planSkeleton"] } : {}),
66
67
  }, existing);
67
- return setConfig(TEAM_TEMPLATE_NAMESPACE, template.slug, template, updatedBy).value as TeamTemplate;
68
+ return (await setConfigAsync(TEAM_TEMPLATE_NAMESPACE, template.slug, template, updatedBy)).value as TeamTemplate;
68
69
  }
69
70
 
70
71
  export function listTeamTemplates(): TeamTemplate[] {
@@ -75,8 +76,8 @@ export function getTeamTemplate(slug: string): TeamTemplate | null {
75
76
  return getConfig<TeamTemplate>(TEAM_TEMPLATE_NAMESPACE, slug)?.value ?? null;
76
77
  }
77
78
 
78
- export function deleteTeamTemplate(slug: string, updatedBy?: string): boolean {
79
- return deleteConfig(TEAM_TEMPLATE_NAMESPACE, slug, updatedBy);
79
+ export function deleteTeamTemplateAsync(slug: string, updatedBy?: string): Promise<boolean> {
80
+ return deleteConfigAsync(TEAM_TEMPLATE_NAMESPACE, slug, updatedBy);
80
81
  }
81
82
 
82
83
  function parseFrozenComposition(raw: string): Array<Record<string, unknown>> {
@@ -107,7 +108,7 @@ function compositionFromFrozenSnapshot(raw: string): TeamTemplateCompositionEntr
107
108
  return composition;
108
109
  }
109
110
 
110
- export function captureTeamTemplateFromTeam(teamId: string, input: Record<string, unknown> = {}, updatedBy?: string): TeamTemplate {
111
+ export async function captureTeamTemplateFromTeamAsync(teamId: string, input: Record<string, unknown> = {}, updatedBy?: string): Promise<TeamTemplate> {
111
112
  const team = getTeam(teamId);
112
113
  if (!team) throw new ValidationError(`team ${teamId} not found`);
113
114
  if (!team.composition) throw new ValidationError("team has no frozen composition snapshot; dissolve it before capture");
@@ -123,7 +124,7 @@ export function captureTeamTemplateFromTeam(teamId: string, input: Record<string
123
124
  ...(team.brief ? { brief: team.brief } : {}),
124
125
  ...(team.definitionOfDone ? { definitionOfDone: team.definitionOfDone } : {}),
125
126
  }, existing);
126
- return setConfig(TEAM_TEMPLATE_NAMESPACE, template.slug, template, updatedBy).value as TeamTemplate;
127
+ return (await setConfigAsync(TEAM_TEMPLATE_NAMESPACE, template.slug, template, updatedBy)).value as TeamTemplate;
127
128
  }
128
129
 
129
130
  function cleanWait(value: unknown): number {
@@ -15,6 +15,7 @@ import { activeAgentKeep } from "./agent-keep";
15
15
  import { automaticDisposalBlocker } from "./automatic-disposal";
16
16
  import { agentGenerationKey } from "../db/agent-shutdown-in-flight";
17
17
  import { workflowAgentObligation } from "./workflow-agent-obligation";
18
+ import { workflowRunPaused } from "../db/workflow-task-read-ops";
18
19
  import type { AgentCard, Command, WorkspaceRecord } from "../types";
19
20
  import { emitCommandEvent } from "../command-events";
20
21
 
@@ -356,7 +357,8 @@ export async function reapTransientAgents(context: { signal?: AbortSignal } = {}
356
357
  if (completedProviderTurns(agent) === 0) { transientTracker.delete(agent.id); waiting.push(agent.id); continue; }
357
358
  // #3234 — an empty branch is not completion while the workflow still awaits this agent's delivery.
358
359
  const runId = workflowRunIdOf(agent);
359
- if (runId && await workflowAgentObligation(runId, agent.id)) { transientTracker.delete(agent.id); waiting.push(agent.id); continue; }
360
+ // #3757 — a paused run keeps all its step agents.
361
+ if (runId && (workflowRunPaused(runId) || await workflowAgentObligation(runId, agent.id))) { transientTracker.delete(agent.id); waiting.push(agent.id); continue; }
360
362
  const entry = transientTracker.get(agent.id) ?? { firstIdleAt: now };
361
363
  transientTracker.set(agent.id, entry);
362
364
  const idleForMs = now - entry.firstIdleAt;
@@ -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();