@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
@@ -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
@@ -132,6 +132,7 @@ export function composeStartupContextForAgent(agent: AgentCard): ComposedSpawnIn
132
132
  if (!inputs) return null;
133
133
  const landGatesHost = landGatesHostForAgent(agent);
134
134
  return composeSpawnInstructionsWithEvents({
135
+ projectId: memorySeamForAgent(agent).identity.projectId,
135
136
  ...(inputs.cwd ? { cwd: inputs.cwd } : {}),
136
137
  // #2171 — the validation block belongs to the machine this agent runs on. This seam is
137
138
  // synchronous (it settles a lifecycle command), so for a remote host it serves what that host's
@@ -2,15 +2,17 @@
2
2
  import { isRecord, withFileLock } from "@humwick/sdk";
3
3
  import { readFileSync } from "node:fs";
4
4
  import { join } from "node:path";
5
- import { currentPendingDeployEvent, finishDeployEvent, forceFailDeployEvent, getDeployEvent, recordDeployEventExecutor, tryBeginDeployEvent } from "../db";
5
+ import { currentPendingDeployEvent, finishDeployEvent, forceFailDeployEvent, getDeployEvent, getMaintenanceJobPause, recordDeployEventExecutor, recordDeployEventHosts, renewDeployEventLease, tryBeginDeployEvent } from "../db";
6
6
  import { localDeployExecutor, serializeDeployExecutor } from "../deploy-executor";
7
7
  import { atomicWrite, exists } from "./files";
8
8
  import { loadJournal, type AdoptionManifest } from "./index";
9
9
  import { parseLinuxPlan } from "./linux-plan";
10
10
  import { parseMacosPlan } from "./macos-plan";
11
+ import { parseFleetPlan, type FleetPlan } from "./fleet-plan";
11
12
 
12
13
  type Direction = "adopt" | "rollback";
13
- export interface CutoverManifest { version: 1; migrationId: string; target: string; platform: "linux" | "macos"; handoff: unknown }
14
+ export interface CutoverManifest { version: 1; migrationId: string; target: string; platform: "linux" | "macos"; handoff: unknown; fleet?: FleetPlan }
15
+ export interface CutoverProgress { eventId: string; has(step: string): boolean; complete(step: string): void; steps(): string[] }
14
16
  export interface CdPause { id: string; holder: string }
15
17
  /** The existing maintenance pause API for the cd-watcher job. `pause` returns the active pause, ours or not. */
16
18
  export interface CdPauseControl { pause(holder: string, reason: string): Promise<CdPause>; current(): Promise<CdPause | null>; resume(): Promise<void> }
@@ -21,6 +23,8 @@ export interface CutoverContext {
21
23
  /** Runs a short deploy-ledger burst against the canonical DB. */
22
24
  ledger: <T>(fn: () => T) => T;
23
25
  interrupt?: (step: string) => void;
26
+ /** Complete an interrupted local hub handoff before using the API it supervises. */
27
+ recoverHandoff?: (rollback: boolean) => Promise<void>;
24
28
  }
25
29
  interface CutoverJournal { version: 1; manifest: CutoverManifest; events: Record<Direction, string>; pauseId?: string; completed: string[] }
26
30
 
@@ -28,7 +32,12 @@ export function parseCutoverManifest(value: unknown): { manifest: CutoverManifes
28
32
  if (!isRecord(value) || value.version !== 1 || typeof value.migrationId !== "string" || !/^[A-Za-z0-9._-]{1,64}$/.test(value.migrationId)
29
33
  || typeof value.target !== "string" || !/^[0-9a-f]{40}$/.test(value.target) || (value.platform !== "linux" && value.platform !== "macos")) throw new Error("Invalid CD cutover manifest");
30
34
  const adoption = (value.platform === "linux" ? parseLinuxPlan(value.handoff) : parseMacosPlan(value.handoff)).adoption;
31
- return { manifest: { version: 1, migrationId: value.migrationId, target: value.target, platform: value.platform, handoff: value.handoff }, adoption };
35
+ const fleet = value.fleet === undefined ? undefined : parseFleetPlan(value.fleet);
36
+ if (fleet) {
37
+ const hub = fleet.hosts.find(host => host.id === fleet.hub)!;
38
+ if (hub.platform !== value.platform || JSON.stringify(hub.handoff) !== JSON.stringify(value.handoff)) throw new Error("Hub handoff must match the control host manifest");
39
+ }
40
+ return { manifest: { version: 1, migrationId: value.migrationId, target: value.target, platform: value.platform, handoff: value.handoff, ...(fleet ? { fleet } : {}) }, adoption };
32
41
  }
33
42
 
34
43
  async function withJournal<T>(input: unknown, create: boolean, fn: (journal: CutoverJournal, save: () => void, adoption: AdoptionManifest) => Promise<T>): Promise<T> {
@@ -55,7 +64,7 @@ const directionOf = (journal: CutoverJournal): Direction => journal.completed.so
55
64
  * Every step reconciles on retry: the pause is re-ensured, the journaled lease row is re-attached
56
65
  * and re-stamped with this process as executor, and the supervisor handoff resumes from its journal.
57
66
  */
58
- export async function runCutover(input: unknown, context: CutoverContext & { rollback?: boolean; handoff: (rollback: boolean) => Promise<unknown> }) {
67
+ export async function runCutover(input: unknown, context: CutoverContext & { rollback?: boolean; handoff: (rollback: boolean, progress: CutoverProgress) => Promise<unknown> }) {
59
68
  return withJournal(input, !context.rollback, async (journal, save, adoption) => {
60
69
  const direction: Direction = context.rollback ? "rollback" : "adopt";
61
70
  const { manifest } = journal;
@@ -69,6 +78,20 @@ export async function runCutover(input: unknown, context: CutoverContext & { rol
69
78
  if (journal.completed.includes(`${direction}:complete`)) return result();
70
79
  acknowledge("prepared");
71
80
 
81
+ const hubPrefix = manifest.fleet && `host:${manifest.fleet.hub}:handoff`;
82
+ const interruptedDirection = (["rollback", "adopt"] as const).find(prior => hubPrefix
83
+ && journal.completed.includes(`${prior}:${hubPrefix}-started`) && !journal.completed.includes(`${prior}:${hubPrefix}`));
84
+ if (interruptedDirection && context.recoverHandoff) {
85
+ context.ledger(() => {
86
+ const lease = getDeployEvent(journal.events[interruptedDirection]);
87
+ const pause = getMaintenanceJobPause("cd-watcher");
88
+ if (lease?.status !== "pending" || lease.initiatedBy !== holder || !pause || pause.id !== journal.pauseId || pause.holder !== holder) throw new Error("Interrupted hub handoff no longer owns its pause and deploy lease");
89
+ recordDeployEventExecutor({ id: lease.id, executor: serializeDeployExecutor(localDeployExecutor(context.executorHost)) });
90
+ renewDeployEventLease({ id: lease.id });
91
+ });
92
+ await context.recoverHandoff(direction === "rollback");
93
+ }
94
+
72
95
  // A pause does not cancel an in-flight deploy; the lease below proves the previous executor settled.
73
96
  const pause = await context.pauses.pause(holder, `Humwick cutover ${manifest.migrationId} (${direction}) to ${manifest.target}; CD stays paused until explicit resume`);
74
97
  if (pause.holder !== holder) throw new Error(`cd-watcher is paused by ${pause.holder}; refusing the cutover`);
@@ -81,7 +104,7 @@ export async function runCutover(input: unknown, context: CutoverContext & { rol
81
104
  if (direction === "rollback" && getDeployEvent(journal.events.adopt)?.status === "pending") {
82
105
  forceFailDeployEvent({ id: journal.events.adopt, reason: `superseded by rollback of ${manifest.migrationId}` });
83
106
  }
84
- const event = getDeployEvent(id) ?? tryBeginDeployEvent({ id, commitSha: manifest.target, hosts: [{ id: context.executorHost, status: "pending" }], initiatedBy: holder });
107
+ const event = getDeployEvent(id) ?? tryBeginDeployEvent({ id, commitSha: manifest.target, hosts: (manifest.fleet?.hosts ?? [{ id: context.executorHost }]).map(host => ({ id: host.id, status: "pending" })), initiatedBy: holder });
85
108
  if (!event) {
86
109
  const held = currentPendingDeployEvent();
87
110
  throw new Error(held ? `deploy lease ${held.id} is held by ${held.initiatedBy}; retry once it settles` : "an unreleased restart epoch blocks the deploy lease; retry once it settles");
@@ -94,14 +117,37 @@ export async function runCutover(input: unknown, context: CutoverContext & { rol
94
117
  acknowledge("lease");
95
118
 
96
119
  if (status === "pending") {
97
- if (direction === "adopt" || handoffBegun(adoption, manifest.platform)) await context.handoff(direction === "rollback");
120
+ const progress: CutoverProgress = {
121
+ eventId: id,
122
+ has: (step) => journal.completed.includes(`${direction}:${step}`),
123
+ steps: () => [...journal.completed],
124
+ complete(step) {
125
+ acknowledge(step);
126
+ if (manifest.fleet && !context.ledger(() => recordDeployEventHosts(id, fleetStatuses()))) throw new Error(`Lost deploy lease ${id}`);
127
+ },
128
+ };
129
+ const heartbeat = setInterval(() => {
130
+ try { context.ledger(() => renewDeployEventLease({ id })); }
131
+ catch (error) { console.error(`Cutover lease heartbeat failed: ${error}`); }
132
+ }, 60_000);
133
+ heartbeat.unref();
134
+ try {
135
+ if (direction === "adopt" || manifest.fleet || handoffBegun(adoption, manifest.platform)) await context.handoff(direction === "rollback", progress);
136
+ } finally { clearInterval(heartbeat); }
98
137
  acknowledge("handoff");
99
138
  context.ledger(() => {
100
- if (getDeployEvent(id)?.status === "pending") finishDeployEvent({ id, status: terminal, hosts: [{ id: context.executorHost, status: terminal }] });
139
+ const event = getDeployEvent(id);
140
+ if (event?.status === "pending") finishDeployEvent({ id, status: terminal, hosts: manifest.fleet ? fleetStatuses() : [{ id: context.executorHost, status: terminal }] });
141
+ else if (event?.status !== terminal) throw new Error(`Lost deploy lease ${id}; CD remains paused`);
101
142
  });
102
143
  }
103
144
  acknowledge("complete");
104
145
  return result();
146
+ function fleetStatuses() {
147
+ return manifest.fleet!.hosts.map(host => ({ id: host.id, status: direction === "rollback" && !journal.completed.includes(`adopt:host:${host.id}:deploy-started`) ? "skipped"
148
+ : journal.completed.includes(`${direction}:host:${host.id}:verified`) ? terminal : "pending",
149
+ reason: journal.completed.filter(step => step.startsWith(`${direction}:host:${host.id}:`)).map(step => step.split(":").at(-1)).join(", ") }));
150
+ }
105
151
  });
106
152
  }
107
153
 
@@ -0,0 +1,35 @@
1
+ // #3470: the fleet is one P4d cutover, with one pause/lease and durable progress per host.
2
+ import { parseCutoverManifest, runCutover, type CutoverContext, type CutoverProgress } from "./cd-cutover";
3
+ import { orderedFleetHosts, type FleetHost } from "./fleet-plan";
4
+
5
+ export interface FleetCutoverContext extends CutoverContext {
6
+ rollback?: boolean;
7
+ deploy(host: FleetHost, sha: string, version: string | undefined, eventId: string): Promise<void>;
8
+ handoff(host: FleetHost, rollback: boolean, progress: CutoverProgress): Promise<void>;
9
+ attest(host: FleetHost, sha: string): Promise<void>;
10
+ }
11
+ export async function runFleetCutover(input: unknown, context: FleetCutoverContext) {
12
+ const { manifest } = parseCutoverManifest(input);
13
+ if (!manifest.fleet || manifest.fleet.hub !== context.executorHost) throw new Error("Fleet cutover must run on its pinned hub");
14
+ return runCutover(manifest, { ...context, handoff: async (rollback, progress) => {
15
+ const ordered = orderedFleetHosts(manifest.fleet!);
16
+ for (const host of rollback ? ordered.reverse() : ordered) {
17
+ const prefix = `host:${host.id}:`;
18
+ const touched = (phase: string) => progress.steps().includes(`adopt:${prefix}${phase}-started`);
19
+ if (rollback && !touched("deploy") && !touched("handoff")) continue;
20
+ const phase = async (name: string, action: () => Promise<void>) => {
21
+ if (progress.has(`${prefix}${name}`)) return;
22
+ progress.complete(`${prefix}${name}-started`);
23
+ await action();
24
+ progress.complete(`${prefix}${name}`);
25
+ };
26
+ const sha = rollback ? host.previous.sha : manifest.target;
27
+ // Restore supervisor names before the old executor resolves its legacy units on rollback.
28
+ if (rollback && touched("handoff")) await phase("handoff", () => context.handoff(host, true, progress));
29
+ if (!rollback || touched("deploy")) await phase("deploy", () => context.deploy(host, sha, rollback ? host.previous.version : undefined, progress.eventId));
30
+ if (!rollback) await phase("handoff", () => context.handoff(host, false, progress));
31
+ await context.attest(host, sha);
32
+ progress.complete(`${prefix}verified`);
33
+ }
34
+ } });
35
+ }
@@ -0,0 +1,36 @@
1
+ // #3470 P7b: host identities and rollback artifacts are pinned before acquiring the deployment lease.
2
+ import { isRecord } from "@humwick/sdk";
3
+ import { absolute } from "./files";
4
+ import { parseLinuxPlan } from "./linux-plan";
5
+ import { parseMacosPlan } from "./macos-plan";
6
+
7
+ export interface FleetHost {
8
+ id: string;
9
+ hostname: string;
10
+ platform: "linux" | "macos";
11
+ handoff: unknown;
12
+ cli: string;
13
+ manifestPath: string;
14
+ cwd: string;
15
+ previous: { sha: string; version: string };
16
+ }
17
+ export interface FleetPlan { canary: string; hub: string; hosts: FleetHost[] }
18
+ export function parseFleetPlan(value: unknown): FleetPlan {
19
+ if (!isRecord(value) || typeof value.canary !== "string" || typeof value.hub !== "string" || value.canary === value.hub
20
+ || !Array.isArray(value.hosts) || value.hosts.length < 2) throw new Error("Invalid fleet cutover plan");
21
+ const hosts = value.hosts.map((host): FleetHost => {
22
+ if (!isRecord(host) || typeof host.id !== "string" || !/^[A-Za-z0-9._-]+$/.test(host.id) || typeof host.hostname !== "string" || !host.hostname || !["linux", "macos"].includes(String(host.platform))
23
+ || !isRecord(host.previous) || typeof host.previous.sha !== "string" || !/^[0-9a-f]{40}$/.test(host.previous.sha)
24
+ || typeof host.previous.version !== "string" || !/^\d+\.\d+\.\d+$/.test(host.previous.version)) throw new Error("Invalid fleet host or rollback artifact");
25
+ (host.platform === "linux" ? parseLinuxPlan : parseMacosPlan)(host.handoff, host.hostname);
26
+ return { id: host.id, hostname: host.hostname, platform: host.platform as FleetHost["platform"], handoff: host.handoff,
27
+ cli: absolute(host.cli), manifestPath: absolute(host.manifestPath), cwd: absolute(host.cwd),
28
+ previous: { sha: host.previous.sha, version: host.previous.version } };
29
+ });
30
+ if (new Set(hosts.map(host => host.id)).size !== hosts.length || !hosts.some(host => host.id === value.hub)
31
+ || !hosts.some(host => host.id === value.canary && host.platform === "linux")) throw new Error("Fleet needs a unique hub and Linux canary");
32
+ return { canary: value.canary, hub: value.hub, hosts };
33
+ }
34
+ export function orderedFleetHosts(plan: FleetPlan): FleetHost[] {
35
+ return [plan.hosts.find(host => host.id === plan.canary)!, ...plan.hosts.filter(host => host.id !== plan.canary && host.id !== plan.hub), plan.hosts.find(host => host.id === plan.hub)!];
36
+ }
@@ -18,8 +18,8 @@ export interface AdoptionJournal {
18
18
  db?: Identity; env: EnvSnapshot[]; completed: string[];
19
19
  }
20
20
 
21
- export function parseAdoptionManifest(value: unknown): AdoptionManifest {
22
- if (!isRecord(value) || value.version !== 1 || value.host !== hostname()
21
+ export function parseAdoptionManifest(value: unknown, expectedHost = hostname()): AdoptionManifest {
22
+ if (!isRecord(value) || value.version !== 1 || value.host !== expectedHost
23
23
  || !Array.isArray(value.roots) || !value.roots.length || !Array.isArray(value.envFiles)) throw new Error("Invalid adoption manifest or wrong host");
24
24
  const roots = value.roots.map(pair => {
25
25
  if (!isRecord(pair)) throw new Error("Invalid directory pair");
@@ -13,10 +13,10 @@ export interface LinuxPlan {
13
13
  adoption: AdoptionManifest; scope: SystemdUnitScope; unitDirectory: string;
14
14
  services: { legacy: string; canonical: string }[]; files: LinuxArtifact[]; preserve: { unit: string; mainPid: number }[];
15
15
  }
16
- export function parseLinuxPlan(value: unknown): LinuxPlan {
16
+ export function parseLinuxPlan(value: unknown, expectedHost?: string): LinuxPlan {
17
17
  if (!isRecord(value) || !["user", "system"].includes(value.scope as string) || !Array.isArray(value.services) || !value.services.length
18
18
  || !Array.isArray(value.files) || !Array.isArray(value.preserve)) throw new Error("Invalid Linux handoff manifest");
19
- const adoption = parseAdoptionManifest(value.adoption);
19
+ const adoption = parseAdoptionManifest(value.adoption, expectedHost);
20
20
  const unitDirectory = absolute(value.unitDirectory);
21
21
  const services = value.services.map(item => {
22
22
  if (!isRecord(item) || typeof item.legacy !== "string" || !/^agent-relay(?:-orchestrator)?(?:-dev)?\.service$/.test(item.legacy)
@@ -26,7 +26,7 @@ export async function adoptLinux(input: unknown, options: LinuxHandoffOptions =
26
26
  const inspect = async (unit: string): Promise<UnitState> => {
27
27
  const output = await manager("show", unit, "--property=ActiveState,UnitFileState,MainPID,InvocationID");
28
28
  const props = Object.fromEntries(output.trim().split("\n").map(line => { const i = line.indexOf("="); return [line.slice(0, i), line.slice(i + 1)]; }));
29
- if (!["active", "inactive", "failed"].includes(props.ActiveState!) || !["enabled", "disabled", "static", "", "not-found"].includes(props.UnitFileState!)) throw new Error(`Unsupported service state: ${unit}`);
29
+ if (!["active", "inactive", "failed"].includes(props.ActiveState!) || !["enabled", "disabled", "static", "transient", "", "not-found"].includes(props.UnitFileState!)) throw new Error(`Unsupported service state: ${unit}`);
30
30
  return { active: props.ActiveState === "active", enabled: props.UnitFileState === "enabled", mainPid: Number(props.MainPID || 0), invocationId: props.InvocationID! };
31
31
  };
32
32
  return withFileLock(`${journalPath}.lock`, async () => {
@@ -34,7 +34,7 @@ export async function adoptLinux(input: unknown, options: LinuxHandoffOptions =
34
34
  const saved = prior?.linux as LinuxJournal | undefined;
35
35
  if (saved && (JSON.stringify(saved.plan) !== JSON.stringify(plan) || saved.artifacts?.length !== plan.files.length || saved.services?.length !== plan.services.length
36
36
  || saved.preserved?.length !== plan.preserve.length || !Array.isArray(saved.completed))) throw new Error("Linux journal does not match manifest");
37
- if (options.rollback && !saved) throw new Error("No Linux handoff to roll back");
37
+ if (options.rollback && !saved) return { journal: journalPath, direction: "rollback", preserved: [], unchanged: true };
38
38
  const artifacts = checkArtifacts(plan, saved?.artifacts);
39
39
  const services = saved?.services ?? await Promise.all(plan.services.map(async service => {
40
40
  const canonical = await inspect(service.canonical);
@@ -15,10 +15,10 @@ export function plistLabel(content: string, expected: string): void {
15
15
  const labels = [...content.matchAll(/<key>\s*Label\s*<\/key>\s*<string>([^<]*)<\/string>/g)];
16
16
  if (labels.length !== 1 || labels[0]![1] !== expected) throw new Error(`Plist Label must match ${expected}`);
17
17
  }
18
- export function parseMacosPlan(value: unknown): MacosPlan {
18
+ export function parseMacosPlan(value: unknown, expectedHost?: string): MacosPlan {
19
19
  if (!isRecord(value) || typeof value.domain !== "string" || !/^gui\/\d+$/.test(value.domain)
20
20
  || !Array.isArray(value.services) || !value.services.length || !Array.isArray(value.files) || !Array.isArray(value.preserve)) throw new Error("Invalid macOS handoff manifest");
21
- const adoption = parseAdoptionManifest(value.adoption);
21
+ const adoption = parseAdoptionManifest(value.adoption, expectedHost);
22
22
  const plistDirectory = absolute(value.plistDirectory);
23
23
  const services = value.services.map(item => {
24
24
  if (!isRecord(item) || typeof item.legacy !== "string"
@@ -47,7 +47,7 @@ export async function adoptMacos(input: unknown, options: MacosHandoffOptions =
47
47
  if (saved && (JSON.stringify(saved.plan) !== JSON.stringify(plan) || saved.artifacts?.length !== plan.files.length || saved.services?.length !== plan.services.length
48
48
  || !saved.services.every(state => typeof state.loaded === "boolean" && typeof state.disabled === "boolean" && typeof state.canonicalDisabled === "boolean")
49
49
  || !Array.isArray(saved.completed))) throw new Error("macOS journal does not match manifest");
50
- if (options.rollback && !saved) throw new Error("No macOS handoff to roll back");
50
+ if (options.rollback && !saved) return { journal: journalPath, direction: "rollback", preserved: [], unchanged: true };
51
51
  const artifacts = checkMacosArtifacts(plan, saved?.artifacts);
52
52
  const flags = await disabled();
53
53
  const services = saved?.services ?? await Promise.all(plan.services.map(async service => {
package/src/steward.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { basename } from "node:path";
2
2
  import { stewardFallbackTarget } from "./config";
3
- import { deleteConfig, getManagedAgentState, getSpawnPolicy, getStewardConfig, setConfig } from "./config-store";
3
+ import { getManagedAgentState, getSpawnPolicy, getStewardConfig } from "./config-store";
4
+ import { deleteConfigAsync, setConfigAsync } from "./config-store-write";
4
5
  import { listOrchestrators } from "./db";
5
6
  import { getLifecycleManager } from "./lifecycle-manager";
6
7
  import { renderTemplate } from "./prompt-resolver";
@@ -137,7 +138,7 @@ function stewardPolicyDiffers(existing: SpawnPolicy, desired: SpawnPolicy): bool
137
138
  * - it still has a live/transitioning managed process. Deleting the policy under a running process
138
139
  * would orphan it: `onConfigChanged` finds no policy for a deleted key, so nothing would stop it.
139
140
  */
140
- function collapseLegacyStewardPolicies(canonicalName: string, repoRoot: string, owner: Orchestrator): boolean | undefined {
141
+ async function collapseLegacyStewardPolicies(canonicalName: string, repoRoot: string, owner: Orchestrator): Promise<boolean | undefined> {
141
142
  let inheritedEnabled: boolean | undefined;
142
143
  for (const legacyName of legacyStewardPolicyNames(repoRoot, owner.id)) {
143
144
  if (legacyName === canonicalName) continue;
@@ -151,7 +152,7 @@ function collapseLegacyStewardPolicies(canonicalName: string, repoRoot: string,
151
152
  continue;
152
153
  }
153
154
  if (legacy.enabled === false) inheritedEnabled = false;
154
- deleteConfig("spawn-policy", legacyName, "steward-legacy-collapse");
155
+ await deleteConfigAsync("spawn-policy", legacyName, "steward-legacy-collapse");
155
156
  getLifecycleManager().onConfigChanged("spawn-policy", legacyName);
156
157
  console.warn(`[steward] collapsed legacy steward policy '${legacyName}' into '${canonicalName}' (#1955)`);
157
158
  }
@@ -165,7 +166,7 @@ function collapseLegacyStewardPolicies(canonicalName: string, repoRoot: string,
165
166
  * owns the repo. Idempotent — refreshes the policy only when config-derived fields
166
167
  * change, so it can be called on every sweep without churn.
167
168
  */
168
- export function ensureRepoSteward(repoRoot: string, owningOrchestrator?: Orchestrator): string | null {
169
+ export async function ensureRepoStewardAsync(repoRoot: string, owningOrchestrator?: Orchestrator): Promise<string | null> {
169
170
  const config = getStewardConfig();
170
171
  if (!config.enabled) return null;
171
172
  const owner = owningOrchestrator ?? listOrchestrators().find((orch) =>
@@ -177,7 +178,7 @@ export function ensureRepoSteward(repoRoot: string, owningOrchestrator?: Orchest
177
178
  const name = repoStewardPolicyName(repoRoot, owner.id);
178
179
  // #1955 mechanism 1 — merge a legacy suffix-less twin for this (repo, host) BEFORE reading the
179
180
  // canonical row, so the pair converges on one policy instead of coexisting as two.
180
- const inheritedEnabled = collapseLegacyStewardPolicies(name, repoRoot, owner);
181
+ const inheritedEnabled = await collapseLegacyStewardPolicies(name, repoRoot, owner);
181
182
  const baseDesired = desiredStewardPolicy(repoRoot, owner, config);
182
183
  const existing = getSpawnPolicy(name);
183
184
  // #1761 (9) — respect an operator's per-policy stop. desiredStewardPolicy hardcodes `enabled:true`;
@@ -193,7 +194,7 @@ export function ensureRepoSteward(repoRoot: string, owningOrchestrator?: Orchest
193
194
  : { ...baseDesired, ...(inheritedEnabled === undefined ? {} : { enabled: inheritedEnabled }) };
194
195
  if (existing && !stewardPolicyDiffers(existing.value, desired)) return name;
195
196
 
196
- setConfig("spawn-policy", name, desired, "steward-autoprovision");
197
+ await setConfigAsync("spawn-policy", name, desired, "steward-autoprovision");
197
198
  getLifecycleManager().onConfigChanged("spawn-policy", name);
198
199
  return name;
199
200
  }
@@ -16,7 +16,7 @@ export type DispatchSnapshot = DispatchContext & Required<Pick<DispatchContext,
16
16
  export async function captureDispatchContext(ctx: Omit<DispatchContext, "policy" | "bands" | "catalog"> = {}): Promise<DispatchSnapshot> {
17
17
  const now = ctx.now ?? Date.now();
18
18
  const [snapshot, orchestrators, hostViews] = await Promise.all([providerQuotaRoutingSnapshotAsync(now), listOrchestratorsAsync(), listHostViewsAsync()]);
19
- const { targetAdmission: assess, targetUnavailableReason } = liveTargetAdmission(orchestrators, hostViews, ctx.orchestratorId, ctx.spawnCap);
19
+ const { targetAdmission: assess, targetUnavailableReason } = liveTargetAdmission(orchestrators, hostViews, snapshot.catalog, ctx.orchestratorId, ctx.spawnCap);
20
20
  const probeUncertainty = new Set<string>();
21
21
  const targetAdmission = (target: Parameters<typeof assess>[0]) => {
22
22
  const result = assess(target);
@@ -310,7 +310,7 @@ export function dispatchTask(shape: TaskDispatch | undefined, pin: DispatchPin =
310
310
  if (eligibility(target.provider)) throw routingResolutionError(preferences, catalog, eligibility);
311
311
  if (ctx.mustResolveAvailable && bandRank(bandFor(target.provider, bands)) > bandRank("normal")) warnings.push(`[CRITICAL] mustResolveAvailable: degraded to ${target.provider} (${bandFor(target.provider, bands)} band). Contention likely.`);
312
312
  }
313
- if (!modelPinned && ctx.targetAvailable && !ctx.targetAvailable(target)) {
313
+ if (ctx.targetAvailable && !ctx.targetAvailable(target)) {
314
314
  throw new Error(ctx.targetUnavailableReason?.(target) ?? `No eligible online host runs ${target.provider}/${target.model ?? "default"}${target.effort ? ` at effort ${target.effort}` : ""}.`);
315
315
  }
316
316
  if (factors.preferences) factors.preferences = { ...factors.preferences, excluded: preferenceExclusions(preferences, factors.ranking, (p) => eligibility(p) ?? (!advertised.has(p) ? "not-advertised" : undefined)) };
@@ -1,4 +1,5 @@
1
- import { getConfig, setConfig } from "./config-store";
1
+ import { getConfig } from "./config-store";
2
+ import { setConfigAsync } from "./config-store-write";
2
3
  import { emitConfigChanged } from "./sse";
3
4
  import {
4
5
  DEFAULT_TEAMS_CONFIG,
@@ -14,9 +15,9 @@ export function getTeamsConfig(): TeamsConfig {
14
15
  return entry ? normalizeTeamsConfig(entry.value) : { ...DEFAULT_TEAMS_CONFIG };
15
16
  }
16
17
 
17
- export function setTeamsConfig(value: unknown, opts: { updatedBy?: string } = {}): TeamsConfig {
18
+ export async function setTeamsConfigAsync(value: unknown, opts: { updatedBy?: string } = {}): Promise<TeamsConfig> {
18
19
  const normalized = normalizeTeamsConfig(value);
19
- const entry = setConfig(TEAMS_CONFIG_NAMESPACE, TEAMS_CONFIG_KEY, normalized, opts.updatedBy);
20
+ const entry = await setConfigAsync(TEAMS_CONFIG_NAMESPACE, TEAMS_CONFIG_KEY, normalized, opts.updatedBy);
20
21
  emitConfigChanged(entry.namespace, entry.key, entry.version);
21
22
  return normalized;
22
23
  }
@@ -13,6 +13,7 @@ import { getGateway } from "./db/gateway.ts";
13
13
  import type { CreateTokenInput, HandoffTokenInput, PruneExpiredTokenRecordsInput, TokenHandoff } from "./token-db.ts";
14
14
  import type { TokenRevokeReason } from "./security.ts";
15
15
  import type { TokenRecord } from "./types.ts";
16
+ import type { LaunchFact } from "./db/launch-facts.ts";
16
17
 
17
18
  /** Mint a component token off-loop. The `jti` is generated here so it is BOTH the token's id and the
18
19
  * gateway idempotency key that satisfies the non-idempotent-op retry-safety gate (H5 §3.7.1). */
@@ -24,6 +25,15 @@ export function createTokenAsync(input: CreateTokenInput): Promise<{ token: stri
24
25
  }>;
25
26
  }
26
27
 
28
+ /** Mint a runner token and, for a fresh launch, record its launch fact, in one writer op (#1870 S7). */
29
+ export function createRunnerTokenAsync(input: CreateTokenInput, launch?: { agentId: string; fact: LaunchFact }): Promise<{ token: string; record: TokenRecord }> {
30
+ const jti = randomUUID();
31
+ return getGateway().write("tokens.createRunner", { input, jti, ...(launch ? { launch } : {}) }, { idempotencyKey: jti }) as unknown as Promise<{
32
+ token: string;
33
+ record: TokenRecord;
34
+ }>;
35
+ }
36
+
27
37
  /** Hand a token off to its successor off-loop (#1245): fold the eligibility read, then mint and link the
28
38
  * successor or replay the pending one — atomic in one op (ADR §3.4). Resolves `null` when ineligible. */
29
39
  export function handoffTokenAsync(input: HandoffTokenInput): Promise<TokenHandoff | null> {
@@ -280,7 +280,7 @@ async function settleWorkspaceMergeCommandResult(command: Command, markTransitio
280
280
  // genuine-but-unverifiable land on its next pass instead of it being stranded as falsely-landed.
281
281
  const landTargetRef = landedWorkspace?.baseRef ?? "main";
282
282
  const landAncestry = mergeResult.merged === true && !prLanded && landedCommitCount !== 0 && landedWorkspace
283
- ? await verifyWorkspaceLandedAncestry(landedWorkspace.repoRoot, authoritativeLandedSha, landTargetRef, cleanString(mergeResult.landVerifiedSha, "result.landVerifiedSha", { max: 64 }))
283
+ ? await verifyWorkspaceLandedAncestry(landedWorkspace.repoRoot, authoritativeLandedSha, landTargetRef.startsWith("origin/") ? landTargetRef : `origin/${landTargetRef}`, cleanString(mergeResult.landVerifiedSha, "result.landVerifiedSha", { max: 64 }))
284
284
  : "confirmed";
285
285
  const ancestryUnconfirmed = landAncestry !== "confirmed";
286
286
  const normalizedStatus = ancestryUnconfirmed
@@ -381,6 +381,7 @@ async function settleWorkspaceMergeCommandResult(command: Command, markTransitio
381
381
  taskIds: resolveLandedTaskIds({ workspace: landedWorkspace,
382
382
  sourceHeadSha: typeof command.params.expectedHeadSha === "string" ? command.params.expectedHeadSha : undefined }),
383
383
  mergedSha: authoritativeLandedSha,
384
+ ...(landAncestry === "confirmed" && !prLanded ? { landVerifiedSha: authoritativeLandedSha } : { landVerifiedSha: stringValue(mergeResult.landVerifiedSha) }),
384
385
  // Git/PR titles have no 200-character limit. Bound display text without rejecting the land (#3244).
385
386
  subject: stringValue(prLanded ? prLanded.subject : mergeResult.subject)?.slice(0, 200),
386
387
  prLand: Boolean(prLanded),
@@ -4,7 +4,7 @@ import { resolveStewardIdentity } from "./steward-identity";
4
4
  import { getLifecycleManager } from "./lifecycle-manager";
5
5
  import { getManagedAgentState } from "./config-store";
6
6
  import { stewardHandoffIdentity } from "./steward-provenance";
7
- import { ensureRepoSteward } from "./steward";
7
+ import { ensureRepoStewardAsync } from "./steward";
8
8
  import { recordStewardWakeFailure } from "./workspace-steward-failure";
9
9
  import { workspaceStewardEligible } from "./steward-queue";
10
10
  import { workspaceOwnerOrchestrator } from "./workspace-host";
@@ -71,7 +71,7 @@ export async function wakeRepoSteward(ws: WorkspaceRecord, reason: string, opts:
71
71
  return { ok: false, cooldown: false, error: ownerResult.error };
72
72
  }
73
73
  try {
74
- const policyName = ensureRepoSteward(ws.repoRoot, ownerResult.owner);
74
+ const policyName = await ensureRepoStewardAsync(ws.repoRoot, ownerResult.owner);
75
75
  if (!policyName) throw new Error("cannot spawn steward: steward policy could not be provisioned");
76
76
  const { ownerAgentId, coordinatorAgentId } = resolveStewardIdentity(ws);
77
77
  const coordinatorLine = coordinatorAgentId