@openrig/cli 0.4.4 → 0.4.6

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 (431) hide show
  1. package/daemon/assets/plugins/openrig-core/skills/mission-slice-sop/SKILL.md +6 -8
  2. package/daemon/assets/plugins/openrig-core/skills/openrig-cmux/SKILL.md +86 -0
  3. package/daemon/assets/plugins/openrig-core/skills/openrig-herdr/SKILL.md +158 -0
  4. package/daemon/assets/plugins/openrig-core/skills/openrig-user/SKILL.md +744 -96
  5. package/daemon/dist/adapters/codex-runtime-adapter.d.ts.map +1 -1
  6. package/daemon/dist/adapters/codex-runtime-adapter.js +12 -8
  7. package/daemon/dist/adapters/codex-runtime-adapter.js.map +1 -1
  8. package/daemon/dist/adapters/pi-resume.d.ts +32 -0
  9. package/daemon/dist/adapters/pi-resume.d.ts.map +1 -0
  10. package/daemon/dist/adapters/pi-resume.js +125 -0
  11. package/daemon/dist/adapters/pi-resume.js.map +1 -0
  12. package/daemon/dist/adapters/pi-runner-protocol.d.ts +88 -0
  13. package/daemon/dist/adapters/pi-runner-protocol.d.ts.map +1 -0
  14. package/daemon/dist/adapters/pi-runner-protocol.js +153 -0
  15. package/daemon/dist/adapters/pi-runner-protocol.js.map +1 -0
  16. package/daemon/dist/adapters/pi-runner.d.ts +99 -0
  17. package/daemon/dist/adapters/pi-runner.d.ts.map +1 -0
  18. package/daemon/dist/adapters/pi-runner.js +519 -0
  19. package/daemon/dist/adapters/pi-runner.js.map +1 -0
  20. package/daemon/dist/adapters/pi-runtime-adapter.d.ts +61 -0
  21. package/daemon/dist/adapters/pi-runtime-adapter.d.ts.map +1 -0
  22. package/daemon/dist/adapters/pi-runtime-adapter.js +356 -0
  23. package/daemon/dist/adapters/pi-runtime-adapter.js.map +1 -0
  24. package/daemon/dist/adapters/tmux.d.ts +22 -1
  25. package/daemon/dist/adapters/tmux.d.ts.map +1 -1
  26. package/daemon/dist/adapters/tmux.js +40 -1
  27. package/daemon/dist/adapters/tmux.js.map +1 -1
  28. package/daemon/dist/build-info.js +3 -3
  29. package/daemon/dist/builtins/workflow-specs/branched-remediation.yaml +31 -0
  30. package/daemon/dist/builtins/workflow-specs/factory-rsi.yaml +152 -0
  31. package/daemon/dist/builtins/workflow-specs/gated-release.yaml +26 -0
  32. package/daemon/dist/builtins/workflow-specs/linear-build.yaml +21 -0
  33. package/daemon/dist/db/migrations/049_workflow_instance_version.d.ts +20 -0
  34. package/daemon/dist/db/migrations/049_workflow_instance_version.d.ts.map +1 -0
  35. package/daemon/dist/db/migrations/049_workflow_instance_version.js +24 -0
  36. package/daemon/dist/db/migrations/049_workflow_instance_version.js.map +1 -0
  37. package/daemon/dist/db/migrations/050_workflow_spec_json.d.ts +22 -0
  38. package/daemon/dist/db/migrations/050_workflow_spec_json.d.ts.map +1 -0
  39. package/daemon/dist/db/migrations/050_workflow_spec_json.js +26 -0
  40. package/daemon/dist/db/migrations/050_workflow_spec_json.js.map +1 -0
  41. package/daemon/dist/db/migrations/051_workflow_resume.d.ts +22 -0
  42. package/daemon/dist/db/migrations/051_workflow_resume.d.ts.map +1 -0
  43. package/daemon/dist/db/migrations/051_workflow_resume.js +27 -0
  44. package/daemon/dist/db/migrations/051_workflow_resume.js.map +1 -0
  45. package/daemon/dist/db/migrations/052_workflow_instance_bound_rig.d.ts +26 -0
  46. package/daemon/dist/db/migrations/052_workflow_instance_bound_rig.d.ts.map +1 -0
  47. package/daemon/dist/db/migrations/052_workflow_instance_bound_rig.js +30 -0
  48. package/daemon/dist/db/migrations/052_workflow_instance_bound_rig.js.map +1 -0
  49. package/daemon/dist/db/migrations/053_sessions_node_id_index.d.ts +48 -0
  50. package/daemon/dist/db/migrations/053_sessions_node_id_index.d.ts.map +1 -0
  51. package/daemon/dist/db/migrations/053_sessions_node_id_index.js +53 -0
  52. package/daemon/dist/db/migrations/053_sessions_node_id_index.js.map +1 -0
  53. package/daemon/dist/db/migrations/054_queue_transitions_archive.d.ts +34 -0
  54. package/daemon/dist/db/migrations/054_queue_transitions_archive.d.ts.map +1 -0
  55. package/daemon/dist/db/migrations/054_queue_transitions_archive.js +49 -0
  56. package/daemon/dist/db/migrations/054_queue_transitions_archive.js.map +1 -0
  57. package/daemon/dist/domain/claim-service.d.ts +11 -0
  58. package/daemon/dist/domain/claim-service.d.ts.map +1 -1
  59. package/daemon/dist/domain/claim-service.js +3 -1
  60. package/daemon/dist/domain/claim-service.js.map +1 -1
  61. package/daemon/dist/domain/discovery-types.d.ts +1 -1
  62. package/daemon/dist/domain/discovery-types.d.ts.map +1 -1
  63. package/daemon/dist/domain/hosts/hosts-registry-reader.d.ts +1 -0
  64. package/daemon/dist/domain/hosts/hosts-registry-reader.d.ts.map +1 -1
  65. package/daemon/dist/domain/hosts/hosts-registry-reader.js +19 -0
  66. package/daemon/dist/domain/hosts/hosts-registry-reader.js.map +1 -1
  67. package/daemon/dist/domain/hosts/hosts-registry-writer.d.ts +11 -0
  68. package/daemon/dist/domain/hosts/hosts-registry-writer.d.ts.map +1 -0
  69. package/daemon/dist/domain/hosts/hosts-registry-writer.js +54 -0
  70. package/daemon/dist/domain/hosts/hosts-registry-writer.js.map +1 -0
  71. package/daemon/dist/domain/hosts/read-through.d.ts +28 -0
  72. package/daemon/dist/domain/hosts/read-through.d.ts.map +1 -0
  73. package/daemon/dist/domain/hosts/read-through.js +139 -0
  74. package/daemon/dist/domain/hosts/read-through.js.map +1 -0
  75. package/daemon/dist/domain/hosts/remote-daemon-http.d.ts +21 -0
  76. package/daemon/dist/domain/hosts/remote-daemon-http.d.ts.map +1 -1
  77. package/daemon/dist/domain/hosts/remote-daemon-http.js +49 -0
  78. package/daemon/dist/domain/hosts/remote-daemon-http.js.map +1 -1
  79. package/daemon/dist/domain/node-inventory.d.ts +18 -0
  80. package/daemon/dist/domain/node-inventory.d.ts.map +1 -1
  81. package/daemon/dist/domain/node-inventory.js +206 -95
  82. package/daemon/dist/domain/node-inventory.js.map +1 -1
  83. package/daemon/dist/domain/node-launcher.d.ts +11 -0
  84. package/daemon/dist/domain/node-launcher.d.ts.map +1 -1
  85. package/daemon/dist/domain/node-launcher.js +10 -0
  86. package/daemon/dist/domain/node-launcher.js.map +1 -1
  87. package/daemon/dist/domain/policies/workflow-keepalive.d.ts +7 -0
  88. package/daemon/dist/domain/policies/workflow-keepalive.d.ts.map +1 -1
  89. package/daemon/dist/domain/policies/workflow-keepalive.js +93 -13
  90. package/daemon/dist/domain/policies/workflow-keepalive.js.map +1 -1
  91. package/daemon/dist/domain/ps-projection.d.ts.map +1 -1
  92. package/daemon/dist/domain/ps-projection.js +37 -26
  93. package/daemon/dist/domain/ps-projection.js.map +1 -1
  94. package/daemon/dist/domain/queue-repository.d.ts +105 -0
  95. package/daemon/dist/domain/queue-repository.d.ts.map +1 -1
  96. package/daemon/dist/domain/queue-repository.js +193 -4
  97. package/daemon/dist/domain/queue-repository.js.map +1 -1
  98. package/daemon/dist/domain/queue-retention.d.ts +130 -0
  99. package/daemon/dist/domain/queue-retention.d.ts.map +1 -0
  100. package/daemon/dist/domain/queue-retention.js +224 -0
  101. package/daemon/dist/domain/queue-retention.js.map +1 -0
  102. package/daemon/dist/domain/restore-orchestrator.d.ts +5 -0
  103. package/daemon/dist/domain/restore-orchestrator.d.ts.map +1 -1
  104. package/daemon/dist/domain/restore-orchestrator.js +29 -2
  105. package/daemon/dist/domain/restore-orchestrator.js.map +1 -1
  106. package/daemon/dist/domain/resume-token-capture.d.ts +13 -0
  107. package/daemon/dist/domain/resume-token-capture.d.ts.map +1 -1
  108. package/daemon/dist/domain/resume-token-capture.js +14 -1
  109. package/daemon/dist/domain/resume-token-capture.js.map +1 -1
  110. package/daemon/dist/domain/resume-token-validation.d.ts +1 -1
  111. package/daemon/dist/domain/resume-token-validation.d.ts.map +1 -1
  112. package/daemon/dist/domain/resume-token-validation.js +58 -10
  113. package/daemon/dist/domain/resume-token-validation.js.map +1 -1
  114. package/daemon/dist/domain/review/compose.d.ts +50 -1
  115. package/daemon/dist/domain/review/compose.d.ts.map +1 -1
  116. package/daemon/dist/domain/review/compose.js +92 -3
  117. package/daemon/dist/domain/review/compose.js.map +1 -1
  118. package/daemon/dist/domain/review/fleet-compose.d.ts +52 -0
  119. package/daemon/dist/domain/review/fleet-compose.d.ts.map +1 -0
  120. package/daemon/dist/domain/review/fleet-compose.js +294 -0
  121. package/daemon/dist/domain/review/fleet-compose.js.map +1 -0
  122. package/daemon/dist/domain/review/gather.d.ts +16 -1
  123. package/daemon/dist/domain/review/gather.d.ts.map +1 -1
  124. package/daemon/dist/domain/review/gather.js +177 -41
  125. package/daemon/dist/domain/review/gather.js.map +1 -1
  126. package/daemon/dist/domain/review/types.d.ts +104 -1
  127. package/daemon/dist/domain/review/types.d.ts.map +1 -1
  128. package/daemon/dist/domain/rig-expansion-service.d.ts.map +1 -1
  129. package/daemon/dist/domain/rig-expansion-service.js +3 -0
  130. package/daemon/dist/domain/rig-expansion-service.js.map +1 -1
  131. package/daemon/dist/domain/rig-repository.d.ts +7 -6
  132. package/daemon/dist/domain/rig-repository.d.ts.map +1 -1
  133. package/daemon/dist/domain/rig-repository.js +33 -11
  134. package/daemon/dist/domain/rig-repository.js.map +1 -1
  135. package/daemon/dist/domain/rigspec-codec.d.ts.map +1 -1
  136. package/daemon/dist/domain/rigspec-codec.js +3 -0
  137. package/daemon/dist/domain/rigspec-codec.js.map +1 -1
  138. package/daemon/dist/domain/rigspec-exporter.d.ts.map +1 -1
  139. package/daemon/dist/domain/rigspec-exporter.js +4 -0
  140. package/daemon/dist/domain/rigspec-exporter.js.map +1 -1
  141. package/daemon/dist/domain/rigspec-instantiator.d.ts.map +1 -1
  142. package/daemon/dist/domain/rigspec-instantiator.js +11 -0
  143. package/daemon/dist/domain/rigspec-instantiator.js.map +1 -1
  144. package/daemon/dist/domain/rigspec-preflight.d.ts +6 -0
  145. package/daemon/dist/domain/rigspec-preflight.d.ts.map +1 -1
  146. package/daemon/dist/domain/rigspec-preflight.js +26 -1
  147. package/daemon/dist/domain/rigspec-preflight.js.map +1 -1
  148. package/daemon/dist/domain/rigspec-schema.d.ts.map +1 -1
  149. package/daemon/dist/domain/rigspec-schema.js +20 -1
  150. package/daemon/dist/domain/rigspec-schema.js.map +1 -1
  151. package/daemon/dist/domain/runtime-verifier.d.ts +7 -1
  152. package/daemon/dist/domain/runtime-verifier.d.ts.map +1 -1
  153. package/daemon/dist/domain/runtime-verifier.js +39 -1
  154. package/daemon/dist/domain/runtime-verifier.js.map +1 -1
  155. package/daemon/dist/domain/seat-handover-service.d.ts +9 -0
  156. package/daemon/dist/domain/seat-handover-service.d.ts.map +1 -1
  157. package/daemon/dist/domain/seat-handover-service.js +2 -0
  158. package/daemon/dist/domain/seat-handover-service.js.map +1 -1
  159. package/daemon/dist/domain/seat-identity-store.d.ts +7 -0
  160. package/daemon/dist/domain/seat-identity-store.d.ts.map +1 -1
  161. package/daemon/dist/domain/seat-identity-store.js +27 -0
  162. package/daemon/dist/domain/seat-identity-store.js.map +1 -1
  163. package/daemon/dist/domain/seat-status-service.d.ts.map +1 -1
  164. package/daemon/dist/domain/seat-status-service.js +6 -4
  165. package/daemon/dist/domain/seat-status-service.js.map +1 -1
  166. package/daemon/dist/domain/session-name.d.ts +22 -0
  167. package/daemon/dist/domain/session-name.d.ts.map +1 -1
  168. package/daemon/dist/domain/session-name.js +29 -0
  169. package/daemon/dist/domain/session-name.js.map +1 -1
  170. package/daemon/dist/domain/slices/slice-detail-projector.d.ts.map +1 -1
  171. package/daemon/dist/domain/slices/slice-detail-projector.js +4 -5
  172. package/daemon/dist/domain/slices/slice-detail-projector.js.map +1 -1
  173. package/daemon/dist/domain/spec-library-workflow-scanner.d.ts +18 -2
  174. package/daemon/dist/domain/spec-library-workflow-scanner.d.ts.map +1 -1
  175. package/daemon/dist/domain/spec-library-workflow-scanner.js +23 -1
  176. package/daemon/dist/domain/spec-library-workflow-scanner.js.map +1 -1
  177. package/daemon/dist/domain/startup-proof.d.ts +10 -6
  178. package/daemon/dist/domain/startup-proof.d.ts.map +1 -1
  179. package/daemon/dist/domain/startup-proof.js +35 -7
  180. package/daemon/dist/domain/startup-proof.js.map +1 -1
  181. package/daemon/dist/domain/successor-session-launcher.d.ts +16 -0
  182. package/daemon/dist/domain/successor-session-launcher.d.ts.map +1 -1
  183. package/daemon/dist/domain/successor-session-launcher.js +13 -0
  184. package/daemon/dist/domain/successor-session-launcher.js.map +1 -1
  185. package/daemon/dist/domain/terminal/cmux-provider-adapter.d.ts +27 -0
  186. package/daemon/dist/domain/terminal/cmux-provider-adapter.d.ts.map +1 -0
  187. package/daemon/dist/domain/terminal/cmux-provider-adapter.js +67 -0
  188. package/daemon/dist/domain/terminal/cmux-provider-adapter.js.map +1 -0
  189. package/daemon/dist/domain/terminal/herdr-adapter.d.ts +77 -0
  190. package/daemon/dist/domain/terminal/herdr-adapter.d.ts.map +1 -0
  191. package/daemon/dist/domain/terminal/herdr-adapter.js +266 -0
  192. package/daemon/dist/domain/terminal/herdr-adapter.js.map +1 -0
  193. package/daemon/dist/domain/terminal/herdr-transport.d.ts +50 -0
  194. package/daemon/dist/domain/terminal/herdr-transport.d.ts.map +1 -0
  195. package/daemon/dist/domain/terminal/herdr-transport.js +145 -0
  196. package/daemon/dist/domain/terminal/herdr-transport.js.map +1 -0
  197. package/daemon/dist/domain/terminal/terminal-provider.d.ts +88 -0
  198. package/daemon/dist/domain/terminal/terminal-provider.d.ts.map +1 -0
  199. package/daemon/dist/domain/terminal/terminal-provider.js +17 -0
  200. package/daemon/dist/domain/terminal/terminal-provider.js.map +1 -0
  201. package/daemon/dist/domain/terminal/terminal-service.d.ts +60 -0
  202. package/daemon/dist/domain/terminal/terminal-service.d.ts.map +1 -0
  203. package/daemon/dist/domain/terminal/terminal-service.js +174 -0
  204. package/daemon/dist/domain/terminal/terminal-service.js.map +1 -0
  205. package/daemon/dist/domain/terminal/terminal-views-store.d.ts +77 -0
  206. package/daemon/dist/domain/terminal/terminal-views-store.d.ts.map +1 -0
  207. package/daemon/dist/domain/terminal/terminal-views-store.js +146 -0
  208. package/daemon/dist/domain/terminal/terminal-views-store.js.map +1 -0
  209. package/daemon/dist/domain/terminal/view-composer.d.ts +42 -0
  210. package/daemon/dist/domain/terminal/view-composer.d.ts.map +1 -0
  211. package/daemon/dist/domain/terminal/view-composer.js +137 -0
  212. package/daemon/dist/domain/terminal/view-composer.js.map +1 -0
  213. package/daemon/dist/domain/tmux-option-defaults.d.ts +89 -0
  214. package/daemon/dist/domain/tmux-option-defaults.d.ts.map +1 -0
  215. package/daemon/dist/domain/tmux-option-defaults.js +132 -0
  216. package/daemon/dist/domain/tmux-option-defaults.js.map +1 -0
  217. package/daemon/dist/domain/types.d.ts +34 -1
  218. package/daemon/dist/domain/types.d.ts.map +1 -1
  219. package/daemon/dist/domain/types.js.map +1 -1
  220. package/daemon/dist/domain/user-settings/settings-store.d.ts +1 -1
  221. package/daemon/dist/domain/user-settings/settings-store.d.ts.map +1 -1
  222. package/daemon/dist/domain/user-settings/settings-store.js +92 -0
  223. package/daemon/dist/domain/user-settings/settings-store.js.map +1 -1
  224. package/daemon/dist/domain/workflow/slice-workflow-projection.d.ts.map +1 -1
  225. package/daemon/dist/domain/workflow/slice-workflow-projection.js +6 -2
  226. package/daemon/dist/domain/workflow/slice-workflow-projection.js.map +1 -1
  227. package/daemon/dist/domain/workflow-boot-sweep.d.ts +28 -0
  228. package/daemon/dist/domain/workflow-boot-sweep.d.ts.map +1 -0
  229. package/daemon/dist/domain/workflow-boot-sweep.js +102 -0
  230. package/daemon/dist/domain/workflow-boot-sweep.js.map +1 -0
  231. package/daemon/dist/domain/workflow-deadline.d.ts +74 -0
  232. package/daemon/dist/domain/workflow-deadline.d.ts.map +1 -0
  233. package/daemon/dist/domain/workflow-deadline.js +165 -0
  234. package/daemon/dist/domain/workflow-deadline.js.map +1 -0
  235. package/daemon/dist/domain/workflow-exception-escalation.d.ts +33 -0
  236. package/daemon/dist/domain/workflow-exception-escalation.d.ts.map +1 -0
  237. package/daemon/dist/domain/workflow-exception-escalation.js +104 -0
  238. package/daemon/dist/domain/workflow-exception-escalation.js.map +1 -0
  239. package/daemon/dist/domain/workflow-exception-router.d.ts +33 -0
  240. package/daemon/dist/domain/workflow-exception-router.d.ts.map +1 -0
  241. package/daemon/dist/domain/workflow-exception-router.js +96 -0
  242. package/daemon/dist/domain/workflow-exception-router.js.map +1 -0
  243. package/daemon/dist/domain/workflow-exception.d.ts +90 -0
  244. package/daemon/dist/domain/workflow-exception.d.ts.map +1 -0
  245. package/daemon/dist/domain/workflow-exception.js +122 -0
  246. package/daemon/dist/domain/workflow-exception.js.map +1 -0
  247. package/daemon/dist/domain/workflow-frontier-guard.d.ts +24 -0
  248. package/daemon/dist/domain/workflow-frontier-guard.d.ts.map +1 -0
  249. package/daemon/dist/domain/workflow-frontier-guard.js +30 -0
  250. package/daemon/dist/domain/workflow-frontier-guard.js.map +1 -0
  251. package/daemon/dist/domain/workflow-instance-store.d.ts +28 -0
  252. package/daemon/dist/domain/workflow-instance-store.d.ts.map +1 -1
  253. package/daemon/dist/domain/workflow-instance-store.js +66 -10
  254. package/daemon/dist/domain/workflow-instance-store.js.map +1 -1
  255. package/daemon/dist/domain/workflow-keepalive-arming.d.ts +35 -0
  256. package/daemon/dist/domain/workflow-keepalive-arming.d.ts.map +1 -0
  257. package/daemon/dist/domain/workflow-keepalive-arming.js +87 -0
  258. package/daemon/dist/domain/workflow-keepalive-arming.js.map +1 -0
  259. package/daemon/dist/domain/workflow-projector.d.ts +117 -2
  260. package/daemon/dist/domain/workflow-projector.d.ts.map +1 -1
  261. package/daemon/dist/domain/workflow-projector.js +672 -41
  262. package/daemon/dist/domain/workflow-projector.js.map +1 -1
  263. package/daemon/dist/domain/workflow-role-context.d.ts +57 -0
  264. package/daemon/dist/domain/workflow-role-context.d.ts.map +1 -0
  265. package/daemon/dist/domain/workflow-role-context.js +120 -0
  266. package/daemon/dist/domain/workflow-role-context.js.map +1 -0
  267. package/daemon/dist/domain/workflow-role-resolver.d.ts +65 -0
  268. package/daemon/dist/domain/workflow-role-resolver.d.ts.map +1 -0
  269. package/daemon/dist/domain/workflow-role-resolver.js +108 -0
  270. package/daemon/dist/domain/workflow-role-resolver.js.map +1 -0
  271. package/daemon/dist/domain/workflow-runtime.d.ts +157 -1
  272. package/daemon/dist/domain/workflow-runtime.d.ts.map +1 -1
  273. package/daemon/dist/domain/workflow-runtime.js +718 -11
  274. package/daemon/dist/domain/workflow-runtime.js.map +1 -1
  275. package/daemon/dist/domain/workflow-spec-cache.d.ts +26 -1
  276. package/daemon/dist/domain/workflow-spec-cache.d.ts.map +1 -1
  277. package/daemon/dist/domain/workflow-spec-cache.js +327 -13
  278. package/daemon/dist/domain/workflow-spec-cache.js.map +1 -1
  279. package/daemon/dist/domain/workflow-types.d.ts +143 -8
  280. package/daemon/dist/domain/workflow-types.d.ts.map +1 -1
  281. package/daemon/dist/domain/workflow-types.js +14 -1
  282. package/daemon/dist/domain/workflow-types.js.map +1 -1
  283. package/daemon/dist/domain/workflow-validator.d.ts +11 -1
  284. package/daemon/dist/domain/workflow-validator.d.ts.map +1 -1
  285. package/daemon/dist/domain/workflow-validator.js +246 -1
  286. package/daemon/dist/domain/workflow-validator.js.map +1 -1
  287. package/daemon/dist/index.d.ts +17 -0
  288. package/daemon/dist/index.d.ts.map +1 -1
  289. package/daemon/dist/index.js +44 -0
  290. package/daemon/dist/index.js.map +1 -1
  291. package/daemon/dist/routes/activity.js +22 -0
  292. package/daemon/dist/routes/activity.js.map +1 -1
  293. package/daemon/dist/routes/hosts.d.ts +5 -0
  294. package/daemon/dist/routes/hosts.d.ts.map +1 -0
  295. package/daemon/dist/routes/hosts.js +346 -0
  296. package/daemon/dist/routes/hosts.js.map +1 -0
  297. package/daemon/dist/routes/queue.d.ts +2 -7
  298. package/daemon/dist/routes/queue.d.ts.map +1 -1
  299. package/daemon/dist/routes/queue.js +264 -9
  300. package/daemon/dist/routes/queue.js.map +1 -1
  301. package/daemon/dist/routes/review.d.ts.map +1 -1
  302. package/daemon/dist/routes/review.js +26 -0
  303. package/daemon/dist/routes/review.js.map +1 -1
  304. package/daemon/dist/routes/seat.js +13 -0
  305. package/daemon/dist/routes/seat.js.map +1 -1
  306. package/daemon/dist/routes/terminal.d.ts +11 -0
  307. package/daemon/dist/routes/terminal.d.ts.map +1 -0
  308. package/daemon/dist/routes/terminal.js +104 -0
  309. package/daemon/dist/routes/terminal.js.map +1 -0
  310. package/daemon/dist/routes/workflow.d.ts.map +1 -1
  311. package/daemon/dist/routes/workflow.js +102 -10
  312. package/daemon/dist/routes/workflow.js.map +1 -1
  313. package/daemon/dist/server.d.ts +5 -0
  314. package/daemon/dist/server.d.ts.map +1 -1
  315. package/daemon/dist/server.js +22 -0
  316. package/daemon/dist/server.js.map +1 -1
  317. package/daemon/dist/startup.d.ts.map +1 -1
  318. package/daemon/dist/startup.js +221 -12
  319. package/daemon/dist/startup.js.map +1 -1
  320. package/daemon/specs/agents/factory-rsi/dogfood/agent.yaml +29 -0
  321. package/daemon/specs/agents/factory-rsi/dogfood/guidance/role.md +37 -0
  322. package/daemon/specs/agents/factory-rsi/release-manager/agent.yaml +29 -0
  323. package/daemon/specs/agents/factory-rsi/release-manager/guidance/role.md +40 -0
  324. package/daemon/specs/agents/shared/skills/core/mission-slice-sop/SKILL.md +6 -8
  325. package/daemon/specs/agents/shared/skills/core/openrig-cmux/SKILL.md +86 -0
  326. package/daemon/specs/agents/shared/skills/core/openrig-herdr/SKILL.md +158 -0
  327. package/daemon/specs/agents/shared/skills/core/openrig-user/SKILL.md +6 -6
  328. package/daemon/specs/rigs/launch/factory-rsi/CULTURE.md +47 -0
  329. package/daemon/specs/rigs/launch/factory-rsi/rig.yaml +115 -0
  330. package/dist/build-info.js +3 -3
  331. package/dist/commands/broadcast.d.ts +6 -1
  332. package/dist/commands/broadcast.d.ts.map +1 -1
  333. package/dist/commands/broadcast.js +101 -15
  334. package/dist/commands/broadcast.js.map +1 -1
  335. package/dist/commands/capture.d.ts.map +1 -1
  336. package/dist/commands/capture.js +99 -8
  337. package/dist/commands/capture.js.map +1 -1
  338. package/dist/commands/config.d.ts.map +1 -1
  339. package/dist/commands/config.js +4 -0
  340. package/dist/commands/config.js.map +1 -1
  341. package/dist/commands/down.d.ts.map +1 -1
  342. package/dist/commands/down.js +5 -0
  343. package/dist/commands/down.js.map +1 -1
  344. package/dist/commands/host.d.ts +14 -0
  345. package/dist/commands/host.d.ts.map +1 -1
  346. package/dist/commands/host.js +338 -14
  347. package/dist/commands/host.js.map +1 -1
  348. package/dist/commands/launch.d.ts.map +1 -1
  349. package/dist/commands/launch.js +5 -0
  350. package/dist/commands/launch.js.map +1 -1
  351. package/dist/commands/ps.d.ts.map +1 -1
  352. package/dist/commands/ps.js +10 -4
  353. package/dist/commands/ps.js.map +1 -1
  354. package/dist/commands/queue.d.ts +36 -0
  355. package/dist/commands/queue.d.ts.map +1 -1
  356. package/dist/commands/queue.js +73 -7
  357. package/dist/commands/queue.js.map +1 -1
  358. package/dist/commands/restore-packet.d.ts.map +1 -1
  359. package/dist/commands/restore-packet.js +13 -2
  360. package/dist/commands/restore-packet.js.map +1 -1
  361. package/dist/commands/send.d.ts.map +1 -1
  362. package/dist/commands/send.js +114 -9
  363. package/dist/commands/send.js.map +1 -1
  364. package/dist/commands/terminal.d.ts +6 -0
  365. package/dist/commands/terminal.d.ts.map +1 -0
  366. package/dist/commands/terminal.js +116 -0
  367. package/dist/commands/terminal.js.map +1 -0
  368. package/dist/commands/transcript.d.ts +6 -1
  369. package/dist/commands/transcript.d.ts.map +1 -1
  370. package/dist/commands/transcript.js +113 -30
  371. package/dist/commands/transcript.js.map +1 -1
  372. package/dist/commands/up.d.ts.map +1 -1
  373. package/dist/commands/up.js +9 -0
  374. package/dist/commands/up.js.map +1 -1
  375. package/dist/commands/whoami.d.ts.map +1 -1
  376. package/dist/commands/whoami.js +11 -0
  377. package/dist/commands/whoami.js.map +1 -1
  378. package/dist/commands/workflow-errors.d.ts +23 -0
  379. package/dist/commands/workflow-errors.d.ts.map +1 -0
  380. package/dist/commands/workflow-errors.js +100 -0
  381. package/dist/commands/workflow-errors.js.map +1 -0
  382. package/dist/commands/workflow-follow.d.ts +71 -0
  383. package/dist/commands/workflow-follow.d.ts.map +1 -0
  384. package/dist/commands/workflow-follow.js +259 -0
  385. package/dist/commands/workflow-follow.js.map +1 -0
  386. package/dist/commands/workflow-render.d.ts +114 -0
  387. package/dist/commands/workflow-render.d.ts.map +1 -0
  388. package/dist/commands/workflow-render.js +223 -0
  389. package/dist/commands/workflow-render.js.map +1 -0
  390. package/dist/commands/workflow.d.ts +1 -0
  391. package/dist/commands/workflow.d.ts.map +1 -1
  392. package/dist/commands/workflow.js +278 -14
  393. package/dist/commands/workflow.js.map +1 -1
  394. package/dist/config-store.d.ts +15 -1
  395. package/dist/config-store.d.ts.map +1 -1
  396. package/dist/config-store.js +103 -0
  397. package/dist/config-store.js.map +1 -1
  398. package/dist/cross-host-cli-helpers.d.ts +13 -0
  399. package/dist/cross-host-cli-helpers.d.ts.map +1 -1
  400. package/dist/cross-host-cli-helpers.js +27 -0
  401. package/dist/cross-host-cli-helpers.js.map +1 -1
  402. package/dist/cross-host-target.d.ts +49 -0
  403. package/dist/cross-host-target.d.ts.map +1 -0
  404. package/dist/cross-host-target.js +35 -0
  405. package/dist/cross-host-target.js.map +1 -0
  406. package/dist/host-registry.d.ts +8 -0
  407. package/dist/host-registry.d.ts.map +1 -1
  408. package/dist/host-registry.js +32 -0
  409. package/dist/host-registry.js.map +1 -1
  410. package/dist/host-selection.d.ts +13 -0
  411. package/dist/host-selection.d.ts.map +1 -0
  412. package/dist/host-selection.js +54 -0
  413. package/dist/host-selection.js.map +1 -0
  414. package/dist/index.d.ts +2 -0
  415. package/dist/index.d.ts.map +1 -1
  416. package/dist/index.js +2 -0
  417. package/dist/index.js.map +1 -1
  418. package/dist/remote-host-ops.d.ts +1 -0
  419. package/dist/remote-host-ops.d.ts.map +1 -1
  420. package/dist/remote-host-ops.js +11 -4
  421. package/dist/remote-host-ops.js.map +1 -1
  422. package/dist/session-name.d.ts +23 -0
  423. package/dist/session-name.d.ts.map +1 -0
  424. package/dist/session-name.js +61 -0
  425. package/dist/session-name.js.map +1 -0
  426. package/package.json +1 -1
  427. package/ui/dist/assets/index-CFqxa0zW.css +32 -0
  428. package/ui/dist/assets/index-zrFqS7gL.js +623 -0
  429. package/ui/dist/index.html +2 -2
  430. package/ui/dist/assets/index-D69ZhNIr.js +0 -598
  431. package/ui/dist/assets/index-DJYun-8d.css +0 -32
@@ -19,6 +19,30 @@
19
19
  //
20
20
  // All in one db.transaction. If any step throws, all rollback. After
21
21
  // commit, fan out events to subscribers + nudge next owner.
22
+ import { exceedsMaxHops, MAX_HOPS_BASELINE_V1 } from "./workflow-deadline.js";
23
+ import { isHumanSeatSession } from "./human-route-enforcer.js";
24
+ import { selectRoleSeat } from "./workflow-role-resolver.js";
25
+ import { roleResolutionContext, tryResolveRoleByCapability, } from "./workflow-role-context.js";
26
+ import { classifyFailedInstance, classifyGateTrip, workflowExceptionTags } from "./workflow-exception.js";
27
+ import { newQitemId } from "./queue-repository.js";
28
+ import { resolveExceptionRoute } from "./workflow-exception-router.js";
29
+ import { disarmWorkflowKeepalive, ensureWorkflowKeepaliveArmed, } from "./workflow-keepalive-arming.js";
30
+ /**
31
+ * OPR.0.4.6.WF2 FR-2: canonical-session → node runtime column lookup
32
+ * (the harness-pin reconciliation source). Latest session row wins
33
+ * (the node-inventory join discipline). null = not a managed node.
34
+ * Shared by the projector (projection-time pins) and the runtime
35
+ * facade (instantiate-time static pin checks).
36
+ */
37
+ export function nodeRuntimeOf(db, session) {
38
+ const row = db
39
+ .prepare(`SELECT n.runtime FROM nodes n
40
+ JOIN sessions s ON s.node_id = n.id
41
+ WHERE s.session_name = ?
42
+ ORDER BY s.id DESC LIMIT 1`)
43
+ .get(session);
44
+ return row?.runtime ?? null;
45
+ }
22
46
  export class WorkflowProjectorError extends Error {
23
47
  code;
24
48
  details;
@@ -37,7 +61,17 @@ export class WorkflowProjector {
37
61
  trailLog;
38
62
  specCache;
39
63
  now;
40
- constructor(db, eventBus, queueRepo, instanceStore, trailLog, specCache, now = () => new Date()) {
64
+ watchdogJobsRepo;
65
+ exceptionDial;
66
+ constructor(db, eventBus, queueRepo, instanceStore, trailLog, specCache, now = () => new Date(),
67
+ /** OPR.0.4.6.WF1 FR-3: optional — arms/disarms the per-instance keepalive in-txn. */
68
+ watchdogJobsRepo,
69
+ /** OPR.0.4.6.WF5 FR-2: the maturity-dial inputs, injected at startup
70
+ * (the validateRig precedent — the projector never reads config
71
+ * itself). hostDefault is read LIVE per exception (dial flips apply
72
+ * to future items only). Absent = engine defaults (orchestrator-
73
+ * first chain with the human@host never-lost fallback). */
74
+ exceptionDial) {
41
75
  this.db = db;
42
76
  this.eventBus = eventBus;
43
77
  this.queueRepo = queueRepo;
@@ -45,6 +79,11 @@ export class WorkflowProjector {
45
79
  this.trailLog = trailLog;
46
80
  this.specCache = specCache;
47
81
  this.now = now;
82
+ this.watchdogJobsRepo = watchdogJobsRepo;
83
+ this.exceptionDial = exceptionDial;
84
+ }
85
+ nodeRuntimeOf(session) {
86
+ return nodeRuntimeOf(this.db, session);
48
87
  }
49
88
  /**
50
89
  * Close the current packet AND create/project the next-step packet
@@ -94,25 +133,144 @@ export class WorkflowProjector {
94
133
  });
95
134
  }
96
135
  // Determine next step (one-hop default; multi-hop is graduation).
97
- const nextStep = resolveNextStep(spec, currentStep);
136
+ // OPR.0.4.6.WF2 FR-1: the recorded exit participates — a MAPPED exit
137
+ // resolves to its branch target (deterministic: same (spec, step,
138
+ // exit) → same target); unmapped exits fall through to the exact
139
+ // structural semantics shipped today.
140
+ const branchTargetId = currentStep.next_hop?.on?.[input.exit];
141
+ const branchRouted = branchTargetId !== undefined;
142
+ const nextStep = resolveNextStep(spec, currentStep, input.exit);
143
+ if (branchRouted && !nextStep) {
144
+ // Validator guarantees branch targets exist at validate/instantiate;
145
+ // this guards a stale cached spec drifting under a live instance.
146
+ throw new WorkflowProjectorError("branch_target_missing", `step "${currentStep.id}" maps exit "${input.exit}" to step "${branchTargetId}" but the spec ${spec.id}@${spec.version} has no such step. Re-validate the spec file.`, { instanceId: instance.instanceId, stepId: currentStep.id, exit: input.exit, branchTargetId });
147
+ }
148
+ // A route happens on a structural handoff OR any branch-mapped exit
149
+ // (the ONE WF-2 engine extension: language-driven routing through
150
+ // the same transactional scribe).
151
+ const willRoute = branchRouted || input.exit === "handoff";
152
+ // FR-5 (G3): the waiting-replay ABSORPTION check. `waiting`
153
+ // deliberately KEEPS the closed packet on the frontier (the
154
+ // keepalive wakes the parked owner off it), so a replayed waiting
155
+ // project passes the frontier guard — before this slice it
156
+ // double-wrote the trail. Absorption closes the hole under the
157
+ // guard-ratified FULL CLOSURE-INTENT IDENTITY (G-WF1-1): the replay
158
+ // is absorbed (ZERO writes, the stored first outcome returned) ONLY
159
+ // when EVERY field that can change observable queue/trail/decision
160
+ // state matches the stored decision exactly. ANY mismatch is a NEW
161
+ // legitimate decision (re-park-with-new-reason) recorded honestly
162
+ // through the normal write path below — never absorbed, never
163
+ // rejected. Terminal-exit replays stay the distinct frontier 409
164
+ // above (FR-1c).
165
+ if (input.exit === "waiting" && instance.status === "waiting") {
166
+ const absorbed = matchesStoredWaitingDecision(instance, input, currentStep.id, this.trailLog);
167
+ if (absorbed) {
168
+ return {
169
+ instance,
170
+ closurePriorPacketId: input.currentPacketId,
171
+ closureReason: "waiting",
172
+ nextQitemId: null,
173
+ nextOwnerSession: null,
174
+ nextStepId: null,
175
+ emittedEventTypes: [],
176
+ absorbedReplay: true,
177
+ };
178
+ }
179
+ }
180
+ // FR-6 (G4): enforce loop_guards.max_hops at projection time — this
181
+ // makes migration 034's "enforced at projection time" comment TRUE.
182
+ // A handoff that would exceed the guard is converted into an HONEST
183
+ // structured failure: the packet still closes (BR-3 shapes), the
184
+ // instance fails with the guard named in trail + event evidence,
185
+ // and no next qitem is minted — never an unbounded hop loop, never
186
+ // a silent stop, never a parked potato. The owner's requested exit
187
+ // was validated against allowed_exits ABOVE; the conversion below
188
+ // is engine-authored (guard enforcement), not an owner exit, so it
189
+ // deliberately bypasses allowed_exits.
190
+ // The comparison runs against an EFFECTIVE BASELINE (arch N1):
191
+ // v1 pins baseline = MAX_HOPS_BASELINE_V1 (0); WF-5 FR-4's resume
192
+ // later amends the baseline so each redrive gets one bounded window.
193
+ // OPR.0.4.6.WF2: the guard fires on ANY route (structural handoff OR
194
+ // a branch-mapped exit) — branch edges create the canonical
195
+ // remediation cycles, and an advance is an advance. Guard precedence
196
+ // is BEFORE branch resolution takes effect: a tripped guard is a
197
+ // terminal honest failure, never a routed branch.
198
+ const maxHops = spec.loop_guards?.max_hops;
199
+ // OPR.0.4.6.WF5 FR-4 (the livelock rail): the baseline is the
200
+ // instance's recorded hops_baseline — 0 for never-resumed instances
201
+ // (byte-identical to the v1 constant), hopCount-at-resume after a
202
+ // redrive, so each sanctioned drive gets exactly one bounded window.
203
+ const maxHopsTripped = willRoute &&
204
+ exceedsMaxHops(instance.hopCount, instance.hopsBaseline ?? MAX_HOPS_BASELINE_V1, maxHops);
205
+ const effectiveExit = maxHopsTripped ? "failed" : input.exit;
206
+ const effectiveResultNote = maxHopsTripped
207
+ ? `max_hops_exceeded: hop ${instance.hopCount + 1} would exceed loop_guards.max_hops=${maxHops}`
208
+ : input.resultNote;
209
+ // The route actually happens only when the guard did not trip.
210
+ const routes = willRoute && !maxHopsTripped;
211
+ let effectiveClosureEvidence = maxHopsTripped
212
+ ? {
213
+ ...(input.closureEvidence ?? {}),
214
+ max_hops_guard: {
215
+ code: "max_hops_exceeded",
216
+ maxHops,
217
+ hopCount: instance.hopCount,
218
+ attemptedHop: instance.hopCount + 1,
219
+ },
220
+ }
221
+ : input.closureEvidence;
222
+ // OPR.0.4.6.WF2 FR-1 (arch PIN 1): the taken branch is recorded
223
+ // ADDITIVELY in the trail row's structured evidence JSON (and in
224
+ // last_continuation_decision below) — NEVER in closure_reason (a
225
+ // closed Phase-A enum + kept hot-potato contract).
226
+ if (routes && branchRouted && nextStep) {
227
+ effectiveClosureEvidence = {
228
+ ...(effectiveClosureEvidence ?? {}),
229
+ branch_taken: { exit: input.exit, target: nextStep.id },
230
+ };
231
+ }
98
232
  const evaluatedAt = this.now().toISOString();
233
+ // OPR.0.4.6.FAC1: the bound-rig resolution context — LAZY (building
234
+ // it reads nothing; the snapshot materializes only if tier-3 role
235
+ // resolution actually runs, which is strictly AFTER the frontier +
236
+ // absorption guards above returned/threw for replays — guard B1's
237
+ // zero-inventory-reads-on-replays pin holds by construction).
238
+ // undefined for unbound instances (tier 3 does not exist for them).
239
+ const roleCtx = roleResolutionContext(this.db, instance.boundRig);
99
240
  let nextQitemId = null;
100
241
  let nextOwnerSession = null;
101
242
  let nextStepId = null;
102
243
  let nextQitemCreatePostCommit = null;
244
+ // OPR.0.4.6.WF5 FR-2 class (a): the exception item's post-commit nudge
245
+ // (delivery is best-effort; the durable item is the guarantee).
246
+ let exceptionItemPostCommit = null;
103
247
  const persistedEvents = [];
104
248
  const txn = this.db.transaction(() => {
105
249
  // 1. Resolve next owner BEFORE closing prior so closure_target is set.
250
+ // OPR.0.4.6.WF2: owner resolution runs for ANY route (structural
251
+ // handoff or branch-mapped exit). A gated target step compiles to
252
+ // the gate destination instead of the step's role owner (FR-5).
106
253
  let resolvedNextOwner = null;
107
- if (input.exit === "handoff") {
254
+ let gateCompile = null;
255
+ if (routes) {
108
256
  if (!nextStep) {
109
257
  throw new WorkflowProjectorError("no_next_step", `workflow instance ${instance.instanceId} reached terminal step "${currentStep.id}" but exit was handoff; use exit=done for terminal steps or extend the spec with a next step`, { instanceId: instance.instanceId, currentStepId: currentStep.id });
110
258
  }
111
- const owner = input.nextOwnerSession ?? resolveDefaultOwner(spec, nextStep);
112
- if (!owner) {
113
- throw new WorkflowProjectorError("next_owner_unresolved", `cannot resolve next owner for step "${nextStep.id}" (role "${nextStep.actor_role}"); supply nextOwnerSession explicitly or add preferred_targets to the role in the spec`, { instanceId: instance.instanceId, nextStepId: nextStep.id, nextRole: nextStep.actor_role });
259
+ if (nextStep.gate) {
260
+ gateCompile = compileGate(spec, nextStep, (s) => this.nodeRuntimeOf(s), roleCtx);
261
+ resolvedNextOwner = gateCompile.destinationSession;
262
+ }
263
+ else if (input.nextOwnerSession) {
264
+ reconcileExplicitOwnerHarness(nextStep, input.nextOwnerSession, (s) => this.nodeRuntimeOf(s));
265
+ resolvedNextOwner = input.nextOwnerSession;
266
+ }
267
+ else {
268
+ const owner = resolveDefaultOwner(spec, nextStep, (s) => this.nodeRuntimeOf(s), roleCtx);
269
+ if (!owner) {
270
+ throw new WorkflowProjectorError("next_owner_unresolved", `cannot resolve next owner for step "${nextStep.id}" (role "${nextStep.actor_role}"); supply nextOwnerSession explicitly or add preferred_targets to the role in the spec`, { instanceId: instance.instanceId, nextStepId: nextStep.id, nextRole: nextStep.actor_role });
271
+ }
272
+ resolvedNextOwner = owner;
114
273
  }
115
- resolvedNextOwner = owner;
116
274
  }
117
275
  // 2. R1 fix (guard blocker 1): close the current packet via Phase A's
118
276
  // QueueRepository.updateWithinTransaction. This validates closure
@@ -120,10 +278,11 @@ export class WorkflowProjector {
120
278
  // metadata (closure_reason, closure_target, handed_off_to / blocked_on),
121
279
  // appends queue_transitions, and emits queue.updated — all inside this
122
280
  // outer transaction. The Phase A closure authority is preserved.
123
- const closure = workflowExitToQueueClosure(input, resolvedNextOwner);
281
+ const closure = workflowExitToQueueClosure({ ...input, exit: effectiveExit, resultNote: effectiveResultNote }, resolvedNextOwner);
124
282
  const queueUpdate = this.queueRepo.updateWithinTransaction({
125
283
  qitemId: input.currentPacketId,
126
284
  actorSession: input.actorSession,
285
+ viaWorkflowVerb: true,
127
286
  state: closure.state,
128
287
  closureReason: closure.closureReason,
129
288
  closureTarget: closure.closureTarget ?? undefined,
@@ -132,12 +291,55 @@ export class WorkflowProjector {
132
291
  transitionNote: closure.transitionNote,
133
292
  });
134
293
  persistedEvents.push(queueUpdate.persistedEvent);
135
- // 3. If exit is handoff: create next-step qitem in same txn.
294
+ // 3. If this close routes (structural handoff OR branch-mapped
295
+ // exit): create the next-step qitem in the same txn. A gated
296
+ // target compiles to the gate item (FR-5) — the SHIPPED write
297
+ // path (tier/summary/evidence_ref) — instead of an ordinary
298
+ // step packet; the instance then PARKS (status ladder below).
136
299
  let createdNext = null;
137
- if (input.exit === "handoff" && nextStep && resolvedNextOwner) {
300
+ if (routes && nextStep && resolvedNextOwner) {
138
301
  nextOwnerSession = resolvedNextOwner;
139
302
  nextStepId = nextStep.id;
303
+ // OPR.0.4.6.FAC1 (arch-endorsed): the ADDITIVE owner_resolution
304
+ // trail evidence — which tier resolved this routing decision
305
+ // (the branch_taken precedent; structured evidence JSON, NEVER
306
+ // closure_reason). Makes the determinism proofs readable and
307
+ // gives trace a rendering cell.
308
+ effectiveClosureEvidence = {
309
+ ...(effectiveClosureEvidence ?? {}),
310
+ owner_resolution: {
311
+ mode: gateCompile
312
+ ? "gate"
313
+ : input.nextOwnerSession
314
+ ? "explicit"
315
+ : (spec.roles?.[nextStep.actor_role]?.preferred_targets ?? []).length > 0
316
+ ? "preferred_targets"
317
+ : "role",
318
+ role: nextStep.actor_role,
319
+ ...(instance.boundRig ? { boundRig: instance.boundRig } : {}),
320
+ seat: resolvedNextOwner,
321
+ },
322
+ };
323
+ // OPR.0.4.6.WF5 FR-1 class (c) (guard code-review fold): a HUMAN
324
+ // gate reach IS the exception, and the WF-2 compiled item IS the
325
+ // attention item (no second item ever) — so the class-(c)
326
+ // exception identity rides THIS packet's tags. The packet id is
327
+ // PREALLOCATED so occurrence:<gatePacketId> exists at create
328
+ // (identity queryable from birth — arch cell-2). Handler-role
329
+ // gates stay negative (deterministic handoffs, not exceptions).
330
+ const gateQitemId = gateCompile ? newQitemId() : undefined;
331
+ const gateException = gateCompile && gateQitemId
332
+ ? classifyGateTrip({
333
+ workflowName: instance.workflowName,
334
+ instanceId: instance.instanceId,
335
+ gatedStepId: nextStep.id,
336
+ gateKind: gateCompile.kind,
337
+ gatePacketId: gateQitemId,
338
+ parkOn: gateCompile.parkOn,
339
+ })
340
+ : null;
140
341
  createdNext = this.queueRepo.createWithinTransaction({
342
+ qitemId: gateQitemId,
141
343
  sourceSession: input.actorSession,
142
344
  destinationSession: resolvedNextOwner,
143
345
  body: workflowHandoffBody({
@@ -146,11 +348,20 @@ export class WorkflowProjector {
146
348
  currentStep,
147
349
  nextStep,
148
350
  actorSession: input.actorSession,
149
- resultNote: input.resultNote,
351
+ resultNote: effectiveResultNote,
352
+ gate: gateCompile,
150
353
  }),
151
354
  priority: "routine",
152
- tier: "mode2",
153
- tags: ["workflow", "handoff", `workflow:${spec.id}`, `instance:${instance.instanceId}`],
355
+ tier: gateCompile?.tier ?? "mode2",
356
+ tags: [
357
+ "workflow",
358
+ gateCompile ? "gate" : "handoff",
359
+ `workflow:${spec.id}`,
360
+ `instance:${instance.instanceId}`,
361
+ ...(gateException ? workflowExceptionTags(gateException.identity).filter((t) => !t.startsWith("workflow:") && !t.startsWith("instance:")) : []),
362
+ ],
363
+ summary: gateCompile?.summary ?? undefined,
364
+ evidenceRef: gateCompile?.evidenceRef ?? undefined,
154
365
  chainOfRecord: [input.currentPacketId],
155
366
  });
156
367
  nextQitemId = createdNext.qitemId;
@@ -158,6 +369,34 @@ export class WorkflowProjector {
158
369
  destinationSession: createdNext.destinationSession,
159
370
  nudge: createdNext.nudge,
160
371
  };
372
+ // OPR.0.4.6.WF2 FR-5 (guard blocker 1): a HUMAN gate packet PARKS
373
+ // in the same txn — state=blocked, blocked_on=<human seat> — the
374
+ // exact leg-1 shape the shipped `rig queue resolve` verb acts on
375
+ // (validateHumanPark enforces summary + evidence_ref here, carried
376
+ // from create). Resolve unparks it for the step owner to resume.
377
+ if (gateCompile?.parkOn) {
378
+ const parked = this.queueRepo.updateWithinTransaction({
379
+ qitemId: createdNext.qitemId,
380
+ actorSession: input.actorSession,
381
+ state: "blocked",
382
+ closureReason: "blocked_on",
383
+ closureTarget: gateCompile.parkOn,
384
+ blockedOn: gateCompile.parkOn,
385
+ transitionNote: `workflow gate: parked on ${gateCompile.parkOn} pending sign-off`,
386
+ });
387
+ persistedEvents.push(parked.persistedEvent);
388
+ }
389
+ }
390
+ // OPR.0.4.6.WF1 FR-1 proof seam (ACK Rev-2, reviewer-approved
391
+ // shape): an env-gated hold INSIDE the transaction, between the
392
+ // queue close / next-qitem create and the trail append, giving
393
+ // the fr1-midtxn-process-kill VM proof a deterministic window to
394
+ // SIGKILL the daemon mid-transaction. Default off/absent = zero
395
+ // effect (test-only; never set in production). Synchronous by
396
+ // design — better-sqlite3 transactions are sync.
397
+ const holdMs = Number(process.env.OPENRIG_TEST_WF_TXN_HOLD_MS ?? 0);
398
+ if (holdMs > 0) {
399
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, holdMs);
161
400
  }
162
401
  // 4. Append step trail entry (prior + next ids).
163
402
  this.trailLog.record({
@@ -165,8 +404,8 @@ export class WorkflowProjector {
165
404
  stepId: currentStep.id,
166
405
  stepRole: currentStep.actor_role,
167
406
  closedAt: evaluatedAt,
168
- closureReason: input.exit,
169
- closureEvidence: input.closureEvidence ?? null,
407
+ closureReason: effectiveExit,
408
+ closureEvidence: effectiveClosureEvidence ?? null,
170
409
  actorSession: input.actorSession,
171
410
  nextQitemId,
172
411
  priorQitemId: input.currentPacketId,
@@ -179,24 +418,37 @@ export class WorkflowProjector {
179
418
  // owner to wake.
180
419
  const remainingFrontier = instance.currentFrontier.filter((id) => id !== input.currentPacketId);
181
420
  let nextFrontier;
182
- if (input.exit === "waiting") {
183
- // Keep the closed packet on frontier; the watchdog uses it to wake the owner.
184
- nextFrontier = [...remainingFrontier, input.currentPacketId];
185
- }
186
- else if (nextQitemId) {
421
+ if (nextQitemId) {
422
+ // OPR.0.4.6.WF2: a ROUTED close (structural handoff OR a
423
+ // branch-mapped exit — including a mapped `waiting`) moves the
424
+ // frontier to the new packet. Checked FIRST so a mapped waiting
425
+ // routes instead of re-parking the closed packet.
187
426
  nextFrontier = [...remainingFrontier, nextQitemId];
188
427
  }
428
+ else if (effectiveExit === "waiting") {
429
+ // Unrouted waiting: keep the closed packet on frontier; the
430
+ // watchdog uses it to wake the owner.
431
+ nextFrontier = [...remainingFrontier, input.currentPacketId];
432
+ }
189
433
  else {
190
434
  nextFrontier = remainingFrontier;
191
435
  }
192
436
  // R1 fix (guard blocker 2a): exit=failed sets status=failed, NOT
193
437
  // completed. Emits workflow.failed instead of workflow.completed.
438
+ // OPR.0.4.6.WF2: a ROUTED close keeps the instance ACTIVE bound to
439
+ // the route target (the branch execution contract) — UNLESS the
440
+ // target step is GATED, in which case the instance parks honestly
441
+ // (`waiting`) on the gate item until resolve/close continues it.
442
+ // Non-routed exits keep today's ladder byte-identically.
194
443
  let nextStatus;
195
444
  let completedAt = null;
196
- if (input.exit === "waiting") {
445
+ if (routes) {
446
+ nextStatus = gateCompile ? "waiting" : "active";
447
+ }
448
+ else if (effectiveExit === "waiting") {
197
449
  nextStatus = "waiting";
198
450
  }
199
- else if (input.exit === "failed") {
451
+ else if (effectiveExit === "failed") {
200
452
  nextStatus = "failed";
201
453
  completedAt = evaluatedAt;
202
454
  }
@@ -208,14 +460,14 @@ export class WorkflowProjector {
208
460
  nextStatus = "active";
209
461
  }
210
462
  // R2 fix: set durable current_step_id transition.
211
- // handoff next step
212
- // waiting → preserve (resume on same packet still on same step)
213
- // done / failed → clear (terminal)
463
+ // routed (handoff or branch) route target step
464
+ // waiting (unrouted) → preserve (resume on same packet/step)
465
+ // done / failed (unrouted) → clear (terminal)
214
466
  let currentStepIdUpdate;
215
- if (input.exit === "handoff" && nextStep) {
467
+ if (routes && nextStep) {
216
468
  currentStepIdUpdate = nextStep.id;
217
469
  }
218
- else if (input.exit === "waiting") {
470
+ else if (effectiveExit === "waiting") {
219
471
  currentStepIdUpdate = "preserve";
220
472
  }
221
473
  else {
@@ -223,25 +475,57 @@ export class WorkflowProjector {
223
475
  currentStepIdUpdate = "clear";
224
476
  }
225
477
  this.instanceStore.updateFrontier(instance.instanceId, nextFrontier, nextStatus, {
226
- bumpHopCount: input.exit === "handoff",
478
+ // A branch route IS an advance (arch PIN 2): it bumps the hop
479
+ // count and the FR-5 version guard under the identical
480
+ // discipline as the linear path.
481
+ bumpHopCount: routes,
227
482
  lastContinuationDecision: {
228
- exit: input.exit,
483
+ exit: effectiveExit,
229
484
  actorSession: input.actorSession,
230
485
  closedPacket: input.currentPacketId,
231
486
  nextPacket: nextQitemId,
232
- resultNote: input.resultNote ?? null,
487
+ resultNote: effectiveResultNote ?? null,
233
488
  blockedOn: input.blockedOn ?? null,
234
489
  currentStep: currentStep.id,
490
+ // OPR.0.4.6.WF2 FR-1 (arch PIN 1): ADDITIVE branch-taken
491
+ // record — null on every non-branch close.
492
+ branchTaken: routes && branchRouted && nextStep ? nextStep.id : null,
235
493
  },
236
494
  completedAt,
237
495
  currentStepId: currentStepIdUpdate,
496
+ // FR-5: the optimistic-concurrency guard — this write commits
497
+ // only if no other writer advanced the instance since our read;
498
+ // a stale writer throws instance_version_conflict and the whole
499
+ // scribe transaction rolls back.
500
+ expectedVersion: instance.version,
238
501
  });
502
+ // 5b. OPR.0.4.6.WF1 FR-3: keepalive arm/disarm INSIDE the scribe
503
+ // txn. A handoff ensures the per-instance job is armed (idempotent
504
+ // — also heals pre-WF-1 instances on their first post-upgrade
505
+ // hop); a terminal status disarms it (no orphaned watchdog
506
+ // noise). Waiting keeps the job armed — the keepalive wakes the
507
+ // parked owner. Plain INSERT/UPDATE, verified txn-composable.
508
+ if (this.watchdogJobsRepo) {
509
+ if (routes && resolvedNextOwner) {
510
+ // Any route arms the keepalive for the new owner — including a
511
+ // gate park (waiting keeps the job armed; it wakes the parked
512
+ // gate target exactly like any parked owner).
513
+ ensureWorkflowKeepaliveArmed(this.watchdogJobsRepo, {
514
+ instanceId: instance.instanceId,
515
+ targetSession: resolvedNextOwner,
516
+ registeredBySession: input.actorSession,
517
+ });
518
+ }
519
+ else if (nextStatus === "completed" || nextStatus === "failed") {
520
+ disarmWorkflowKeepalive(this.watchdogJobsRepo, instance.instanceId, `workflow_${nextStatus}`);
521
+ }
522
+ }
239
523
  // 6. Persist workflow events within the same txn.
240
524
  persistedEvents.push(this.eventBus.persistWithinTransaction({
241
525
  type: "workflow.step_closed",
242
526
  instanceId: instance.instanceId,
243
527
  stepId: currentStep.id,
244
- closureReason: input.exit,
528
+ closureReason: effectiveExit,
245
529
  actorSession: input.actorSession,
246
530
  priorQitemId: input.currentPacketId,
247
531
  }));
@@ -267,8 +551,77 @@ export class WorkflowProjector {
267
551
  type: "workflow.failed",
268
552
  instanceId: instance.instanceId,
269
553
  workflowName: instance.workflowName,
270
- reason: input.resultNote ?? "workflow_step_failed",
554
+ reason: effectiveResultNote ?? "workflow_step_failed",
271
555
  }));
556
+ // OPR.0.4.6.WF5 FR-2 class (a): the exception attention item is
557
+ // BORN IN THIS TRANSACTION — if this commit lands, the item
558
+ // exists; if anything throws, instance failure AND item roll
559
+ // back together. There is NO window where the instance is
560
+ // failed and no item exists (the load-bearing never-lost AC,
561
+ // target-independent at every dial position — hold-onto 1).
562
+ const exception = classifyFailedInstance({
563
+ instance: { ...instance, status: "failed" },
564
+ failedStepId: currentStep.id,
565
+ failedPacketId: input.currentPacketId,
566
+ failureReason: effectiveResultNote ?? null,
567
+ });
568
+ if (exception) {
569
+ const route = resolveExceptionRoute({
570
+ exceptionClass: exception.identity.exceptionClass,
571
+ spec,
572
+ hostDialDefault: this.exceptionDial?.hostDefault() ?? null,
573
+ // FAC-1 (arch Q3 — routing uniformity, bounded): declared
574
+ // preferred_targets stay the override; a bound instance's
575
+ // orchestrator-role dial position falls through to a
576
+ // NON-THROWING capability pick on the bound rig (null →
577
+ // the router's human@host never-lost fallback — exception
578
+ // routing never fails a close). Fresh decision per episode;
579
+ // not a replay concern.
580
+ resolveRoleTarget: (role) => spec.roles?.[role]?.preferred_targets?.[0] ??
581
+ tryResolveRoleByCapability(roleCtx, role),
582
+ humanFallbackSeat: this.exceptionDial?.humanFallbackSeat ?? "human@host",
583
+ });
584
+ const evidenceRef = `rig workflow trace ${instance.instanceId}`;
585
+ const itemBody = `WORKFLOW EXCEPTION (${exception.identity.exceptionClass})\n` +
586
+ `workflow: ${instance.workflowName} v${instance.workflowVersion}\n` +
587
+ `instance: ${instance.instanceId}\n` +
588
+ `step: ${currentStep.id} (role ${currentStep.actor_role})\n` +
589
+ `reason: ${exception.reason}\n` +
590
+ `evidence: ${evidenceRef}\n` +
591
+ `resolve: diagnose via the trace above, then \`rig workflow resume ${instance.instanceId} [--decision <text>]\` re-drives from this step (completed steps never re-run).`;
592
+ const createExceptionItem = (destination, tier) => this.queueRepo.createWithinTransaction({
593
+ sourceSession: input.actorSession,
594
+ destinationSession: destination,
595
+ body: itemBody,
596
+ priority: "urgent",
597
+ tier,
598
+ tags: workflowExceptionTags(exception.identity),
599
+ summary: exception.reason,
600
+ evidenceRef,
601
+ chainOfRecord: [input.currentPacketId],
602
+ });
603
+ let createdException;
604
+ try {
605
+ createdException = createExceptionItem(route.destinationSession, route.tier);
606
+ }
607
+ catch {
608
+ // THE NEVER-LOST WRITE-GATE FALLBACK: a routed destination the
609
+ // queue's destination gate rejects (e.g. a spec-declared
610
+ // target that is not a live session) must not lose the
611
+ // exception OR fail the close — re-route human@host (always
612
+ // validates: the destination gate special-cases human seats)
613
+ // with the human-routed tier. Any failure of THIS create is a
614
+ // real storage error and propagates: the whole close rolls
615
+ // back rather than committing an item-less failure.
616
+ createdException = createExceptionItem(this.exceptionDial?.humanFallbackSeat ?? "human@host", "human-gate");
617
+ }
618
+ persistedEvents.push(createdException.persistedEvent);
619
+ exceptionItemPostCommit = {
620
+ qitemId: createdException.qitemId,
621
+ destinationSession: createdException.destinationSession,
622
+ nudge: createdException.nudge,
623
+ };
624
+ }
272
625
  }
273
626
  });
274
627
  txn();
@@ -276,6 +629,13 @@ export class WorkflowProjector {
276
629
  for (const e of persistedEvents) {
277
630
  this.eventBus.notifySubscribers(e);
278
631
  }
632
+ // OPR.0.4.6.WF5 FR-2: best-effort nudge for the exception item (the
633
+ // shipped non-fatal delivery pattern — the durable item is the
634
+ // guarantee, the nudge is a courtesy).
635
+ const excPost = exceptionItemPostCommit;
636
+ if (excPost) {
637
+ await this.queueRepo.maybeNudge(excPost.qitemId, excPost.destinationSession, excPost.nudge);
638
+ }
279
639
  const postCommit = nextQitemCreatePostCommit;
280
640
  if (nextQitemId && postCommit) {
281
641
  await this.queueRepo.maybeNudge(nextQitemId, postCommit.destinationSession, postCommit.nudge);
@@ -284,7 +644,7 @@ export class WorkflowProjector {
284
644
  return {
285
645
  instance: updatedInstance,
286
646
  closurePriorPacketId: input.currentPacketId,
287
- closureReason: input.exit,
647
+ closureReason: effectiveExit,
288
648
  nextQitemId,
289
649
  nextOwnerSession,
290
650
  nextStepId,
@@ -292,6 +652,61 @@ export class WorkflowProjector {
292
652
  };
293
653
  }
294
654
  }
655
+ /**
656
+ * OPR.0.4.6.WF1 FR-5 (G-WF1-1, guard-ratified): the FULL
657
+ * CLOSURE-INTENT IDENTITY predicate for waiting-replay absorption.
658
+ * Every field that can change observable queue/trail/decision state
659
+ * must match the stored decision exactly:
660
+ * exit=waiting · closedPacket · currentStep · actorSession ·
661
+ * resultNote (null ≡ absent) · blockedOn (normalized to the
662
+ * EFFECTIVE blocker — the shipped `external-gate` default applied
663
+ * before comparison, workflowExitToQueueClosure:waiting) ·
664
+ * closureEvidence (deep-equal against the stored TRAIL row's
665
+ * evidence, since that is where evidence lands).
666
+ * Plus instance.status === waiting (checked by the caller).
667
+ */
668
+ function matchesStoredWaitingDecision(instance, input, currentStepId, trailLog) {
669
+ const stored = instance.lastContinuationDecision;
670
+ if (!stored)
671
+ return false;
672
+ if (stored.exit !== "waiting")
673
+ return false;
674
+ if (stored.closedPacket !== input.currentPacketId)
675
+ return false;
676
+ if (stored.currentStep !== currentStepId)
677
+ return false;
678
+ if (stored.actorSession !== input.actorSession)
679
+ return false;
680
+ const storedNote = stored.resultNote ?? null;
681
+ const inputNote = input.resultNote ?? null;
682
+ if (storedNote !== inputNote)
683
+ return false;
684
+ const storedBlocker = (stored.blockedOn ?? null) ?? "external-gate";
685
+ const inputBlocker = input.blockedOn ?? "external-gate";
686
+ if (storedBlocker !== inputBlocker)
687
+ return false;
688
+ // closureEvidence lands in the trail row, not the decision — compare
689
+ // against the most recent waiting close of this exact packet.
690
+ const trail = trailLog.listForInstance(instance.instanceId);
691
+ const storedRow = trail.find((t) => t.priorQitemId === input.currentPacketId && t.closureReason === "waiting");
692
+ if (!storedRow)
693
+ return false;
694
+ return isDeepEqual(storedRow.closureEvidence ?? null, input.closureEvidence ?? null);
695
+ }
696
+ function isDeepEqual(a, b) {
697
+ if (a === b)
698
+ return true;
699
+ if (a === null || b === null || typeof a !== "object" || typeof b !== "object") {
700
+ return false;
701
+ }
702
+ if (Array.isArray(a) !== Array.isArray(b))
703
+ return false;
704
+ const aKeys = Object.keys(a);
705
+ const bKeys = Object.keys(b);
706
+ if (aKeys.length !== bKeys.length)
707
+ return false;
708
+ return aKeys.every((k) => isDeepEqual(a[k], b[k]));
709
+ }
295
710
  /**
296
711
  * R2 fix (guard blocker 1): resolve the current step from durable
297
712
  * instance.currentStepId, NOT from trail order. The previous trail-based
@@ -320,10 +735,25 @@ function resolveCurrentStep(spec, instance) {
320
735
  null);
321
736
  }
322
737
  /**
323
- * Resolve the next step. Prefer the spec-authored `next_hop.suggested_roles`
324
- * edge, then fall back to declaration order for legacy/simple specs.
738
+ * Resolve the next step. OPR.0.4.6.WF2 FR-1: when a `recordedExit` is
739
+ * supplied AND the step's `next_hop.on` maps it, the MAPPED target wins
740
+ * (the deterministic branch — same (spec, step, exit) → same target,
741
+ * every time). Otherwise the structural default: the spec-authored
742
+ * `next_hop.suggested_roles` edge, then declaration order.
743
+ *
744
+ * EXPORTED (OPR.0.4.6.WF1 FR-7): this is THE single routing seam —
745
+ * the validator's reachability/cycle analysis runs over these exact
746
+ * semantics (never a parallel re-implementation). The validator calls
747
+ * it WITHOUT an exit for the structural default path and unions the
748
+ * branch edges explicitly — one routing truth, no semantic fork.
325
749
  */
326
- function resolveNextStep(spec, currentStep) {
750
+ export function resolveNextStep(spec, currentStep, recordedExit) {
751
+ const mappedTargetId = recordedExit
752
+ ? currentStep.next_hop?.on?.[recordedExit]
753
+ : undefined;
754
+ if (mappedTargetId !== undefined) {
755
+ return spec.steps.find((s) => s.id === mappedTargetId) ?? null;
756
+ }
327
757
  if (currentStep.next_hop?.mode === "forbid")
328
758
  return null;
329
759
  for (const role of currentStep.next_hop?.suggested_roles ?? []) {
@@ -338,15 +768,113 @@ function resolveNextStep(spec, currentStep) {
338
768
  return null;
339
769
  return spec.steps[idx + 1] ?? null;
340
770
  }
771
+ /**
772
+ * OPR.0.4.6.FAC1 — resolve a ROLE to a live capable seat on the bound
773
+ * rig, or throw LOUD-WITH-CANDIDATES (BR-5). The structured details
774
+ * carry every evaluated candidate with its named disqualifier
775
+ * (`not_live(lifecycleState=x)` / `runtime_mismatch(has≠needs)` /
776
+ * `role_not_declared` / `adopted_seat_not_role_resolvable_v1`) so the
777
+ * operator OR the WF-5 exception orchestrator can act; the
778
+ * zero-candidate case gets its own named message. Thrown BEFORE any
779
+ * side effect (the :199-221 validation-before-mutation discipline);
780
+ * never a spawn, never auto-add_member, never a dead-seat route.
781
+ */
782
+ function resolveRoleOnBoundRig(roleName, harness, roleCtx, stepId) {
783
+ const candidates = roleCtx.candidatesForRig();
784
+ if (candidates === null) {
785
+ throw new WorkflowProjectorError("bound_rig_not_found", `cannot resolve role "${roleName}" for step "${stepId}": the instance's bound rig "${roleCtx.boundRig}" no longer resolves to a registered rig (torn down mid-run?). Recreate/import the rig under the same name, or instantiate a fresh run bound elsewhere.`, { stepId, role: roleName, boundRig: roleCtx.boundRig });
786
+ }
787
+ const result = selectRoleSeat({ role: roleName, harness, candidates });
788
+ if (result.seat)
789
+ return result.seat;
790
+ const declaring = result.disqualified.filter((c) => c.facts.role === roleName);
791
+ if (declaring.length === 0) {
792
+ throw new WorkflowProjectorError("next_owner_unresolved", `cannot resolve role "${roleName}" for step "${stepId}" (no seats on rig "${roleCtx.boundRig}" declare role "${roleName}"). Add a member with role ${roleName} to rig ${roleCtx.boundRig} via rig add, declare role on an existing member, or add preferred_targets to the role in the spec.`, {
793
+ stepId,
794
+ role: roleName,
795
+ boundRig: roleCtx.boundRig,
796
+ candidates: result.disqualified,
797
+ });
798
+ }
799
+ throw new WorkflowProjectorError("next_owner_unresolved", `cannot resolve a live capable seat for role "${roleName}" for step "${stepId}" on rig "${roleCtx.boundRig}". Candidates: ${declaring
800
+ .map((c) => `${c.coordinate ?? c.logicalId} → ${c.disqualifier}`)
801
+ .join(", ")}. Start/repair a role seat (see rig ps), add a member with role ${roleName} via rig add, or add preferred_targets to the role.`, {
802
+ stepId,
803
+ role: roleName,
804
+ boundRig: roleCtx.boundRig,
805
+ candidates: result.disqualified,
806
+ });
807
+ }
341
808
  /**
342
809
  * Resolve a default owner for a step. v1 picks the first declared
343
- * preferred_target. Returns null if none declared (caller must supply
344
- * nextOwnerSession explicitly).
810
+ * preferred_target. OPR.0.4.6.WF2 FR-2: a step with a `harness:` pin
811
+ * picks the first preferred_target whose node runtime matches the pin
812
+ * instead; no match is a STRUCTURED routing failure naming the pin and
813
+ * each candidate's actual runtime — never a silent mis-route to the
814
+ * wrong harness. `runtimeOf` resolves a canonical session name to its
815
+ * node's runtime column (null = not a managed node / unknown).
816
+ *
817
+ * OPR.0.4.6.FAC1: gains the optional bound-rig context — see the tier
818
+ * comment in the body. Tier order (P2-6, guard-BLOCKING on deviation):
819
+ * 1. explicit input.nextOwnerSession (caller-reconciled)
820
+ * 2. gate compile (the caller branches there first)
821
+ * 3. declared preferred_targets — TODAY'S path byte-for-byte
822
+ * 4. NEW: role capability on the bound rig (no targets + bound only)
823
+ * 5. null → the shipped next_owner_unresolved at the caller.
824
+ *
825
+ * Returns null only for the unpinned no-targets UNBOUND case (caller
826
+ * must supply nextOwnerSession explicitly — unchanged v1 behavior).
345
827
  */
346
- function resolveDefaultOwner(spec, step) {
828
+ export function resolveDefaultOwner(spec, step, runtimeOf,
829
+ /** OPR.0.4.6.FAC1: the bound-rig resolution context. Present only for
830
+ * BOUND instances at LIVE resolution sites (projection next-step,
831
+ * gate compile, entry, resume) — never at the eager instantiate loop
832
+ * (guard B1: no eager live resolution of future steps). */
833
+ roleCtx) {
347
834
  const role = spec.roles?.[step.actor_role];
348
835
  const targets = role?.preferred_targets ?? [];
349
- return targets[0] ?? null;
836
+ // OPR.0.4.6.FAC1 TIER 3 (the ONLY new tier; order is sacred — P2-6):
837
+ // declared preferred_targets above stay TODAY'S code path
838
+ // byte-for-byte and are NEVER liveness/inventory-filtered. Capability
839
+ // resolution activates exclusively when the role declares ZERO
840
+ // targets AND the instance is bound to a rig. Unbound no-target
841
+ // behavior below stays byte-identical (null / "(none declared)").
842
+ if (targets.length === 0 && roleCtx) {
843
+ return resolveRoleOnBoundRig(step.actor_role, step.harness, roleCtx, step.id);
844
+ }
845
+ if (!step.harness)
846
+ return targets[0] ?? null;
847
+ if (!runtimeOf) {
848
+ // Defense-in-depth: a pinned step must never resolve without
849
+ // runtime awareness (that would be exactly the silent mis-route
850
+ // FR-2 forbids). Production call sites always supply runtimeOf.
851
+ throw new WorkflowProjectorError("harness_pin_unverifiable", `step "${step.id}" pins harness "${step.harness}" but no runtime lookup is available to reconcile it — refusing to resolve an owner blind.`, { stepId: step.id, harness: step.harness });
852
+ }
853
+ const candidates = targets.map((t) => ({ session: t, runtime: runtimeOf(t) }));
854
+ const match = candidates.find((c) => c.runtime === step.harness);
855
+ if (match)
856
+ return match.session;
857
+ throw new WorkflowProjectorError("harness_pin_unsatisfied", `step "${step.id}" pins harness "${step.harness}" but no preferred_target of role "${step.actor_role}" runs it. Candidates: ${candidates.length === 0
858
+ ? "(none declared)"
859
+ : candidates.map((c) => `${c.session} → ${c.runtime ?? "unknown"}`).join(", ")}. Add a ${step.harness} seat to the role's preferred_targets, or change/remove the pin.`, {
860
+ stepId: step.id,
861
+ harness: step.harness,
862
+ candidates: candidates.map((c) => ({ session: c.session, runtime: c.runtime })),
863
+ });
864
+ }
865
+ /**
866
+ * OPR.0.4.6.WF2 FR-2: reconcile an EXPLICIT owner override against a
867
+ * step's harness pin. An operator-supplied owner whose runtime does not
868
+ * match the pin is rejected loud — an explicit override must never
869
+ * silently defeat a declared pin.
870
+ */
871
+ export function reconcileExplicitOwnerHarness(step, explicitOwner, runtimeOf) {
872
+ if (!step.harness)
873
+ return;
874
+ const actual = runtimeOf(explicitOwner);
875
+ if (actual !== step.harness) {
876
+ throw new WorkflowProjectorError("harness_pin_unsatisfied", `step "${step.id}" pins harness "${step.harness}" but the explicitly supplied owner ${explicitOwner} runs ${actual ?? "unknown (not a managed node)"}. Supply a ${step.harness} seat or change/remove the pin.`, { stepId: step.id, harness: step.harness, explicitOwner, actualRuntime: actual });
877
+ }
350
878
  }
351
879
  /**
352
880
  * R1 fix (guard blocker 1): translate a workflow exit into the Phase A
@@ -413,14 +941,117 @@ function workflowExitToQueueClosure(input, resolvedNextOwner) {
413
941
  }
414
942
  }
415
943
  }
944
+ export function compileGate(spec, gatedStep, runtimeOf,
945
+ /** OPR.0.4.6.FAC1: bound-rig context — threads capability resolution
946
+ * into BOTH gate paths (guard B3: the human-gate OWNER path and the
947
+ * handler-role DESTINATION branch are different code paths and each
948
+ * must carry it). Absent = shipped behavior byte-identical. */
949
+ roleCtx) {
950
+ const gate = gatedStep.gate;
951
+ if (!gate) {
952
+ throw new WorkflowProjectorError("gate_missing", `compileGate called for step "${gatedStep.id}" which declares no gate`, { stepId: gatedStep.id });
953
+ }
954
+ if (isHumanSeatSession(gate.target)) {
955
+ // Defense-in-depth: the validator requires these at validate time;
956
+ // the shipped human-park write path (validateHumanPark at the
957
+ // blocked_on transition) enforces them again at park.
958
+ if (!gate.summary || !gate.evidence_ref) {
959
+ throw new WorkflowProjectorError("gate_human_fields_missing", `step "${gatedStep.id}" gates on human seat ${gate.target} but is missing ${!gate.summary ? "summary" : "evidence_ref"} — a human-parked item requires both (the shipped human-route contract).`, { stepId: gatedStep.id, target: gate.target });
960
+ }
961
+ // The packet belongs to the gated step's ROLE OWNER (the seat that
962
+ // does the work once the human signs off) and PARKS blocked_on the
963
+ // human seat — the exact leg-1 shape `rig queue resolve` unparks.
964
+ // FAC-1 (guard B3 row 2a): the owner resolves through the full tier
965
+ // stack — a bound instance's role-only gated step gets a rig-local
966
+ // owner by capability.
967
+ const owner = resolveDefaultOwner(spec, gatedStep, runtimeOf, roleCtx);
968
+ if (!owner) {
969
+ throw new WorkflowProjectorError("gate_owner_unresolved", `step "${gatedStep.id}" gates on human seat ${gate.target} but its role "${gatedStep.actor_role}" declares no preferred_targets — the parked packet has no owner to resume after resolve. Add preferred_targets to the role.`, { stepId: gatedStep.id, target: gate.target, role: gatedStep.actor_role });
970
+ }
971
+ return {
972
+ destinationSession: owner,
973
+ tier: undefined,
974
+ summary: gate.summary,
975
+ evidenceRef: gate.evidence_ref,
976
+ kind: "human",
977
+ parkOn: gate.target,
978
+ };
979
+ }
980
+ const role = spec.roles?.[gate.target];
981
+ if (!role) {
982
+ throw new WorkflowProjectorError("gate_target_unresolved", `step "${gatedStep.id}" gates on "${gate.target}" which is neither a human seat session nor a role declared in workflow.roles. Declare the role or use a human seat session (human@kernel form).`, { stepId: gatedStep.id, target: gate.target });
983
+ }
984
+ const handlerTargets = role.preferred_targets ?? [];
985
+ if (handlerTargets.length === 0) {
986
+ // OPR.0.4.6.FAC1 (guard B3 row 2b — the branch that would silently
987
+ // stay old-style if unthreaded): declared preferred_targets remain
988
+ // the override tier byte-identically; when the handler role has NO
989
+ // targets AND the instance is bound, the handler seat resolves by
990
+ // capability on the bound rig (the step's harness pin binds the
991
+ // ACTUAL routed destination — the WF-2 rev1-r2 contract); otherwise
992
+ // the shipped loud failure stands.
993
+ if (roleCtx) {
994
+ const seat = resolveRoleOnBoundRig(gate.target, gatedStep.harness, roleCtx, gatedStep.id);
995
+ return {
996
+ destinationSession: seat,
997
+ tier: undefined,
998
+ summary: gate.summary ?? null,
999
+ evidenceRef: gate.evidence_ref ?? null,
1000
+ kind: "handler-role",
1001
+ parkOn: null,
1002
+ };
1003
+ }
1004
+ throw new WorkflowProjectorError("gate_handler_unresolved", `step "${gatedStep.id}" gates on handler role "${gate.target}" but that role declares no preferred_targets — the gate item has no seat to route to. Add preferred_targets to the role.`, { stepId: gatedStep.id, target: gate.target });
1005
+ }
1006
+ // OPR.0.4.6.WF2 (rev1-r2 blocker): the step's harness pin binds the
1007
+ // packet's ACTUAL routed destination — for a handler gate, the handler
1008
+ // seat. FR-2's contract ("never a silent mis-route to the wrong
1009
+ // harness") admits no gated bypass: a pinned gated step routes to the
1010
+ // first handler preferred_target whose node runtime matches, or fails
1011
+ // structured naming the pin and every candidate's runtime.
1012
+ let seat;
1013
+ if (gatedStep.harness) {
1014
+ const candidates = handlerTargets.map((t) => ({ session: t, runtime: runtimeOf(t) }));
1015
+ const match = candidates.find((c) => c.runtime === gatedStep.harness);
1016
+ if (!match) {
1017
+ throw new WorkflowProjectorError("harness_pin_unsatisfied", `step "${gatedStep.id}" pins harness "${gatedStep.harness}" and gates on handler role "${gate.target}", but no preferred_target of that role runs it. Candidates: ${candidates.map((c) => `${c.session} → ${c.runtime ?? "unknown"}`).join(", ")}. Add a ${gatedStep.harness} seat to the handler role's preferred_targets, or change/remove the pin.`, {
1018
+ stepId: gatedStep.id,
1019
+ harness: gatedStep.harness,
1020
+ gateTarget: gate.target,
1021
+ candidates: candidates.map((c) => ({ session: c.session, runtime: c.runtime })),
1022
+ });
1023
+ }
1024
+ seat = match.session;
1025
+ }
1026
+ else {
1027
+ seat = handlerTargets[0];
1028
+ }
1029
+ return {
1030
+ destinationSession: seat,
1031
+ tier: undefined,
1032
+ summary: gate.summary ?? null,
1033
+ evidenceRef: gate.evidence_ref ?? null,
1034
+ kind: "handler-role",
1035
+ parkOn: null,
1036
+ };
1037
+ }
416
1038
  function workflowHandoffBody(input) {
417
1039
  const lines = [
418
- `### Workflow handoff: ${input.spec.id}@${input.spec.version} step ${input.nextStep.id}`,
1040
+ input.gate
1041
+ ? `### Workflow gate: ${input.spec.id}@${input.spec.version} step ${input.nextStep.id}`
1042
+ : `### Workflow handoff: ${input.spec.id}@${input.spec.version} step ${input.nextStep.id}`,
419
1043
  "",
420
1044
  `Workflow instance: ${input.instance.instanceId}`,
421
1045
  `Prior step: ${input.currentStep.id} (${input.currentStep.actor_role}) closed by ${input.actorSession}`,
422
1046
  `This step: ${input.nextStep.id} (${input.nextStep.actor_role})`,
423
1047
  ];
1048
+ if (input.gate) {
1049
+ lines.push(`Gate: ${input.gate.kind === "human" ? "human sign-off" : "handler-role check"} — the workflow is PARKED (waiting) until this item is resolved/closed; the flow then continues from this step.`);
1050
+ if (input.gate.summary)
1051
+ lines.push(`Ask: ${input.gate.summary}`);
1052
+ if (input.gate.evidenceRef)
1053
+ lines.push(`Evidence: ${input.gate.evidenceRef}`);
1054
+ }
424
1055
  if (input.nextStep.objective) {
425
1056
  lines.push(`Objective: ${input.nextStep.objective}`);
426
1057
  }