@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
@@ -9,10 +9,19 @@
9
9
  //
10
10
  // Pattern mirrors Phase B's ProjectClassifier facade shape.
11
11
  import { WorkflowInstanceStore, WorkflowInstanceError, } from "./workflow-instance-store.js";
12
- import { WorkflowProjector, WorkflowProjectorError, } from "./workflow-projector.js";
12
+ import { resolveExceptionRoute } from "./workflow-exception-router.js";
13
+ import { classifyGateTrip, workflowExceptionTags } from "./workflow-exception.js";
14
+ import { newQitemId } from "./queue-repository.js";
15
+ import { WorkflowProjector, WorkflowProjectorError, compileGate, nodeRuntimeOf, reconcileExplicitOwnerHarness, resolveDefaultOwner, } from "./workflow-projector.js";
16
+ import { loadHostRegistry } from "./hosts/hosts-registry-reader.js";
13
17
  import { WorkflowSpecCache, WorkflowSpecError } from "./workflow-spec-cache.js";
14
18
  import { WorkflowStepTrailLog } from "./workflow-step-trail-log.js";
15
19
  import { WorkflowValidator, } from "./workflow-validator.js";
20
+ import { disarmWorkflowKeepalive, ensureWorkflowKeepaliveArmed } from "./workflow-keepalive-arming.js";
21
+ import { evaluateStepDeadline } from "./workflow-deadline.js";
22
+ import { rigDeclaresRole, rigMemberExists, roleResolutionContext, tryResolveRoleByCapability, } from "./workflow-role-context.js";
23
+ import { isHumanSeatSession } from "./human-route-enforcer.js";
24
+ import { parseSessionName } from "./session-name.js";
16
25
  export class WorkflowRuntime {
17
26
  specCache;
18
27
  instanceStore;
@@ -23,20 +32,65 @@ export class WorkflowRuntime {
23
32
  eventBus;
24
33
  queueRepo;
25
34
  now;
35
+ watchdogJobsRepo;
36
+ exceptionDial;
26
37
  constructor(deps) {
27
38
  this.db = deps.db;
28
39
  this.eventBus = deps.eventBus;
29
40
  this.queueRepo = deps.queueRepo;
30
41
  this.now = deps.now ?? (() => new Date());
42
+ this.watchdogJobsRepo = deps.watchdogJobsRepo;
43
+ this.exceptionDial = deps.exceptionDial;
31
44
  this.specCache = new WorkflowSpecCache(this.db, this.now);
32
45
  this.instanceStore = new WorkflowInstanceStore(this.db, this.now);
33
46
  this.trailLog = new WorkflowStepTrailLog(this.db);
34
47
  this.validator = new WorkflowValidator();
35
- this.projector = new WorkflowProjector(this.db, this.eventBus, this.queueRepo, this.instanceStore, this.trailLog, this.specCache, this.now);
48
+ this.projector = new WorkflowProjector(this.db, this.eventBus, this.queueRepo, this.instanceStore, this.trailLog, this.specCache, this.now, this.watchdogJobsRepo, deps.exceptionDial);
36
49
  }
50
+ /**
51
+ * OPR.0.4.6.WF5 FR-2: resolve the maturity dial for a CACHED spec —
52
+ * the class-(b) detection paths (sweep/keepalive) call this through
53
+ * the startup-injected closure. null = spec not cached (the caller's
54
+ * never-lost fallback applies). Uses the SAME preferred_targets[0]
55
+ * string-pick as step-owner resolution (arch Seam-A uniformity).
56
+ */
57
+ resolveExceptionRouteFor(workflowName, workflowVersion, exceptionClass,
58
+ /** OPR.0.4.6.FAC1 (arch Q3): the instance's bound rig — dial
59
+ * position 3 (orchestrator-role) then resolves capability-aware
60
+ * on that rig when the role declares no preferred_targets.
61
+ * Absent/null = the shipped fleet-blind string-pick only. */
62
+ boundRig) {
63
+ const specRow = this.specCache.getByNameVersion(workflowName, workflowVersion);
64
+ if (!specRow)
65
+ return null;
66
+ const spec = specRow.spec;
67
+ const roleCtx = roleResolutionContext(this.db, boundRig ?? null);
68
+ return resolveExceptionRoute({
69
+ exceptionClass,
70
+ spec,
71
+ hostDialDefault: this.exceptionDial?.hostDefault() ?? null,
72
+ resolveRoleTarget: (role) => spec.roles?.[role]?.preferred_targets?.[0] ??
73
+ tryResolveRoleByCapability(roleCtx, role),
74
+ humanFallbackSeat: this.exceptionDial?.humanFallbackSeat ?? "human@host",
75
+ });
76
+ }
77
+ /**
78
+ * OPR.0.4.6.WF2 FR-3: the production host-registry probe for the
79
+ * validator — built on the daemon hosts-registry reader (read-only
80
+ * twin of the CLI registry). An unreadable/missing registry reports
81
+ * every id as unregistered with an empty id list (fail-loud at the
82
+ * validator's host_not_registered issue, never a silent pass).
83
+ */
84
+ hostRegistryLookup = (hostId) => {
85
+ const loaded = loadHostRegistry();
86
+ if (!loaded.ok)
87
+ return { registered: false, registeredIds: [] };
88
+ const ids = loaded.registry.hosts.map((h) => h.id);
89
+ return { registered: ids.includes(hostId), registeredIds: ids };
90
+ };
37
91
  validate(specPath, seatLivenessCheck) {
38
92
  const specRow = this.specCache.readThrough(specPath);
39
- return this.validator.validate(specRow.spec, seatLivenessCheck);
93
+ return this.validator.validate(specRow.spec, seatLivenessCheck, this.hostRegistryLookup);
40
94
  }
41
95
  /**
42
96
  * Create a workflow instance + first-step qitem. The entry qitem is
@@ -53,7 +107,7 @@ export class WorkflowRuntime {
53
107
  // the bare name straight to readThrough -> spec_file_missing.
54
108
  const resolvedSpecPath = this.specCache.resolveSourcePathByName(input.specPath) ?? input.specPath;
55
109
  const specRow = this.specCache.readThrough(resolvedSpecPath);
56
- const validation = this.validator.validate(specRow.spec);
110
+ const validation = this.validator.validate(specRow.spec, undefined, this.hostRegistryLookup);
57
111
  if (!validation.ok) {
58
112
  throw new WorkflowProjectorError("spec_invalid", `cannot instantiate: spec ${specRow.name}@${specRow.version} has ${validation.issues.filter((i) => i.severity === "error").length} validation error(s); run validate to inspect`, { specPath: input.specPath, issues: validation.issues });
59
113
  }
@@ -61,8 +115,245 @@ export class WorkflowRuntime {
61
115
  if (!entryStep) {
62
116
  throw new WorkflowProjectorError("spec_no_steps", `cannot instantiate: spec ${specRow.name}@${specRow.version} has no steps[]`, { specPath: input.specPath });
63
117
  }
64
- const entryRole = specRow.spec.roles?.[entryStep.actor_role];
65
- const entryOwner = input.entryOwnerSession ?? entryRole?.preferred_targets?.[0] ?? null;
118
+ // OPR.0.4.6.WF2 FR-3: THE v1 execution boundary (the slice-11
119
+ // pattern). A REMOTE host pin is legal LANGUAGE (it validated
120
+ // above) but fails loud HERE — the queue is local-only until MH-3
121
+ // (cross-host queue routing); minting a qitem into a queue that
122
+ // cannot route it, or silently running the step locally, are both
123
+ // forbidden. Checked for EVERY step at instantiate (the earliest
124
+ // knowable moment — a mid-run surprise would strand the instance).
125
+ for (const step of specRow.spec.steps) {
126
+ if (step.host && step.host !== "local") {
127
+ throw new WorkflowProjectorError("host_pin_remote_unsupported", `cannot instantiate: step "${step.id}" pins host "${step.host}". Remote-step execution requires MH-3 (cross-host queue routing), which has not shipped — the queue is local-only today. Workaround: run that step's seat on this host (host: local, or drop the pin), or wait for MH-3.`, { specPath: input.specPath, stepId: step.id, host: step.host, boundary: "MH-3" });
128
+ }
129
+ }
130
+ // OPR.0.4.6.FAC1 (AC-1) + arch ruling 2026-07-07 (target-rig
131
+ // zero-regression, "Option A refined by PROVENANCE"): resolve the
132
+ // instance's rig binding by SPLITTING on the provenance of the rig
133
+ // name, because the two sources carry different intent:
134
+ //
135
+ // - Operator `input.targetRig` (explicit `--rig X`) is AUTHORITATIVE:
136
+ // an explicit instantiation demand. Unknown X → `bound_rig_unknown`
137
+ // HARD-FAIL, loud, before any mutation (unchanged built behavior).
138
+ // - Spec-default `spec.target.rig` is ADVISORY: the spec author's
139
+ // default HINT, authored under the pre-FAC-1 regime where the field
140
+ // was IGNORED at runtime (display-only). Unknown → DEGRADE to
141
+ // UNBOUND + a LOUD advisory (not silent, not a hard-fail). This
142
+ // preserves AC-1 zero-regression for shipped/example specs (e.g.
143
+ // `conveyor` declares `target.rig: conveyor` AND routes every step
144
+ // via preferred_targets — degrading to unbound routes exactly as
145
+ // pre-FAC-1). A spec that genuinely needs a bound rig still fails
146
+ // loudly, per-step, at the right granularity (entry →
147
+ // `entry_owner_unresolved` at instantiate; later role-only step →
148
+ // `next_owner_unresolved` at projection). Nothing degrades to
149
+ // silence — it degrades to per-step honest failure with a heads-up.
150
+ // - Neither set → unbound (byte-identical today's behavior).
151
+ //
152
+ // name→id re-resolves fresh at each later resolution site, so a rig
153
+ // vanishing mid-run fails loud there (WF-5 catches it).
154
+ const registeredRigNames = () => this.db.prepare(`SELECT DISTINCT name FROM rigs ORDER BY name`).all().map((r) => r.name);
155
+ const rigIsRegistered = (name) => this.db.prepare(`SELECT id FROM rigs WHERE name = ? LIMIT 1`).get(name) !== undefined;
156
+ const advisories = [];
157
+ let boundRig;
158
+ if (input.targetRig != null) {
159
+ // AUTHORITATIVE path: honor the operator's explicit demand or fail loud.
160
+ if (!rigIsRegistered(input.targetRig)) {
161
+ const registered = registeredRigNames();
162
+ throw new WorkflowProjectorError("bound_rig_unknown", `cannot instantiate: target rig "${input.targetRig}" is not a registered rig on this daemon. Registered rigs: ${registered.length > 0 ? registered.join(", ") : "(none)"}. Check \`rig ps\`, create/import the rig first, or instantiate with a different --rig.`, { specPath: input.specPath, targetRig: input.targetRig, registeredRigs: registered });
163
+ }
164
+ boundRig = input.targetRig;
165
+ }
166
+ else if (specRow.spec.target?.rig != null) {
167
+ // ADVISORY path: the spec author's DEFAULT hint. Unknown → degrade
168
+ // to unbound with a loud advisory (never a hard-fail on a default).
169
+ const specDefaultRig = specRow.spec.target.rig;
170
+ if (!rigIsRegistered(specDefaultRig)) {
171
+ const registered = registeredRigNames();
172
+ boundRig = null;
173
+ advisories.push(`workflow spec default target.rig "${specDefaultRig}" is not a registered rig on this daemon — instantiating UNBOUND. ` +
174
+ `Steps route via their declared preferred_targets; any role-only step (no preferred_targets) will fail per-role at the right time ` +
175
+ `(entry at instantiate, later steps at projection). Pass --rig <name> to bind explicitly. ` +
176
+ `Registered rigs: ${registered.length > 0 ? registered.join(", ") : "(none)"}.`);
177
+ }
178
+ else {
179
+ boundRig = specDefaultRig;
180
+ }
181
+ }
182
+ else {
183
+ boundRig = null;
184
+ }
185
+ // OPR.0.4.6.WF2 FR-2: static harness-pin reconciliation for EVERY
186
+ // pinned step at instantiate (the earliest knowable moment against
187
+ // current inventory); projection re-checks at each route (runtimes
188
+ // can change mid-flight).
189
+ //
190
+ // OPR.0.4.6.FAC1 (ARCH Q2 = GUARD B1, binding): this eager loop
191
+ // does NO live role resolution and RECORDS NOTHING. It keeps the
192
+ // shipped harness/preferred-target reconciliation for steps whose
193
+ // role DECLARES targets (spec-only facts — sound to check now); a
194
+ // BOUND instance's role-only step (zero declared targets) is
195
+ // deliberately SKIPPED here — a factory rig warms up, and that
196
+ // step's liveness is its own projection-time concern where
197
+ // loud-with-candidates + WF-5 own the failure. The structural
198
+ // zero-role-coverage check below is the only instantiate-time
199
+ // hard-fail for role-only steps. Unbound specs keep today's
200
+ // behavior byte-identically (a pinned no-target step still fails
201
+ // "(none declared)" at instantiate — nothing could ever resolve it).
202
+ const runtimeOf = (session) => nodeRuntimeOf(this.db, session);
203
+ const declaredTargetsOf = (roleName) => (specRow.spec.roles?.[roleName]?.preferred_targets ?? []).length;
204
+ for (const step of specRow.spec.steps) {
205
+ if (step.harness) {
206
+ // rev1-r2 blocker fix: gated steps are NOT excluded — a pinned
207
+ // gated step reconciles through its gate compile (human gates
208
+ // resolve the step owner pin-aware; handler gates match the pin
209
+ // against the handler role's targets). Both throw
210
+ // harness_pin_unsatisfied when no candidate matches.
211
+ if (step.gate) {
212
+ const gateIsHuman = isHumanSeatSession(step.gate.target);
213
+ const gateRoleTargets = gateIsHuman
214
+ ? declaredTargetsOf(step.actor_role)
215
+ : declaredTargetsOf(step.gate.target);
216
+ if (boundRig !== null && gateRoleTargets === 0)
217
+ continue; // role-only on a bound rig: projection resolves
218
+ compileGate(specRow.spec, step, runtimeOf);
219
+ }
220
+ else if (!(step === entryStep && input.entryOwnerSession)) {
221
+ if (boundRig !== null && declaredTargetsOf(step.actor_role) === 0)
222
+ continue;
223
+ resolveDefaultOwner(specRow.spec, step, runtimeOf);
224
+ }
225
+ }
226
+ }
227
+ // OPR.0.4.6.FAC1 (ARCH Q2): the STRUCTURAL role-coverage check for
228
+ // a BOUND instance — hard-fail ONLY when a step's role (or a
229
+ // handler-gate's target role) with zero declared preferred_targets
230
+ // is declared by ZERO seats on the bound rig, at ANY lifecycle
231
+ // state. Existence, not liveness: catches typos and missing role
232
+ // attributes at instantiate WITHOUT eager live resolution. The
233
+ // entry step is included — it resolves live below anyway, but a
234
+ // structural miss reads better as this named error.
235
+ if (boundRig !== null) {
236
+ for (const step of specRow.spec.steps) {
237
+ const rolesToCover = [];
238
+ if (declaredTargetsOf(step.actor_role) === 0)
239
+ rolesToCover.push(step.actor_role);
240
+ if (step.gate && !isHumanSeatSession(step.gate.target) && declaredTargetsOf(step.gate.target) === 0) {
241
+ rolesToCover.push(step.gate.target);
242
+ }
243
+ for (const roleName of rolesToCover) {
244
+ if (!rigDeclaresRole(this.db, boundRig, roleName)) {
245
+ throw new WorkflowProjectorError("bound_rig_role_uncovered", `cannot instantiate: step "${step.id}" needs role "${roleName}" but NO seat on rig "${boundRig}" declares that role (at any lifecycle state). Add a member with role ${roleName} to rig ${boundRig} (rig add), declare role: ${roleName} on an existing member, or add preferred_targets to the role in the spec. (A declared-but-not-yet-running seat is fine — liveness is checked when the step projects.)`, { specPath: input.specPath, stepId: step.id, role: roleName, boundRig });
246
+ }
247
+ }
248
+ }
249
+ }
250
+ // OPR.0.4.6.FAC3 (FR-5): the member-exists instantiate ADVISORY —
251
+ // catch a mis-routed destination (a typo'd/stale member on a rig
252
+ // this daemon DOES know) loudly at the earliest knowable moment,
253
+ // never silently orphaned. ADVISORY-NEVER-DENY: instantiate always
254
+ // proceeds, and the queue transport gate stays rig-exists-only —
255
+ // hardening it to member-exists would gate EVERY queue write and
256
+ // break legitimate non-managed destinations (adopted seats, human
257
+ // seats, MH-3-forwarded items). A read-only pre-txn pass over
258
+ // spec-declared targets; sync SQL only.
259
+ //
260
+ // Scope = roles REFERENCED BY STEPS (actor_role + a handler gate's
261
+ // target role — the same reference set the structural coverage
262
+ // check above walks): the advisory must name a declaring step, and
263
+ // an unreferenced role's targets never route. Skip order per the
264
+ // queue-gate archetype: human-seat classifier BEFORE parse (the
265
+ // identical predicate the transport uses) → non-canonical
266
+ // (raw/adopted destinations are legitimate; the inventory cannot
267
+ // vouch for them) → unregistered rig (the transport already rejects
268
+ // those loudly at queue-write — no double advisory) → member probe
269
+ // (existence at ANY lifecycle state/kind; liveness is projection's
270
+ // business). ONE aggregated advisory per unique unknown target,
271
+ // naming every declaring step/role pair.
272
+ {
273
+ const unknownTargets = new Map();
274
+ const seenPairs = new Set();
275
+ const probeRoleTargets = (roleName, stepId) => {
276
+ for (const target of specRow.spec.roles?.[roleName]?.preferred_targets ?? []) {
277
+ if (isHumanSeatSession(target))
278
+ continue;
279
+ const parsed = parseSessionName(target);
280
+ if (parsed.kind !== "canonical")
281
+ continue;
282
+ if (!rigIsRegistered(parsed.rig))
283
+ continue;
284
+ let memberExists;
285
+ try {
286
+ memberExists = rigMemberExists(this.db, parsed.rig, target);
287
+ }
288
+ catch {
289
+ // ADVISORY-NEVER-THROW (VM-caught, run-1): the probe rides the
290
+ // full inventory projection, which can error on a
291
+ // partial-schema DB (e.g. a test fixture without the snapshots
292
+ // table). A probe error means the inventory cannot vouch
293
+ // EITHER way — skip silently; an advisory path must never be
294
+ // able to fail the instantiate.
295
+ continue;
296
+ }
297
+ if (memberExists)
298
+ continue;
299
+ const pairKey = JSON.stringify([target, stepId, roleName]);
300
+ if (seenPairs.has(pairKey))
301
+ continue;
302
+ seenPairs.add(pairKey);
303
+ const entry = unknownTargets.get(target) ?? { rig: parsed.rig, declaredBy: [] };
304
+ entry.declaredBy.push({ stepId, role: roleName });
305
+ unknownTargets.set(target, entry);
306
+ }
307
+ };
308
+ for (const step of specRow.spec.steps) {
309
+ probeRoleTargets(step.actor_role, step.id);
310
+ if (step.gate && !isHumanSeatSession(step.gate.target)) {
311
+ probeRoleTargets(step.gate.target, step.id);
312
+ }
313
+ }
314
+ for (const [target, { rig, declaredBy }] of unknownTargets) {
315
+ const declares = declaredBy
316
+ .map((d) => `step "${d.stepId}" (role "${d.role}")`)
317
+ .join(", ");
318
+ advisories.push(`preferred target "${target}" names rig "${rig}" (registered) but NO member of that rig has this coordinate — declared by ${declares}. ` +
319
+ `Work routed there will not be claimed; it will surface as a stuck exception. ` +
320
+ `Check the member name against \`rig ps\`, or add the member to rig "${rig}".`);
321
+ }
322
+ }
323
+ // OPR.0.4.6.WF2 FR-5: a gated ENTRY step compiles to the gate item
324
+ // (human-routed or handler-routed) and the instance parks waiting
325
+ // from birth — same socket as a mid-flow gate.
326
+ // OPR.0.4.6.FAC1 (ARCH Q2/call-site row 3): the ENTRY step gets
327
+ // FULL live resolution at instantiate and is RECORDED — the entry
328
+ // packet is actually created now (the first routing decision), so
329
+ // this is resolve-once, not eager pre-resolution. A tier-3 failure
330
+ // here re-throws under the entry error code, candidates preserved.
331
+ const entryRoleCtx = roleResolutionContext(this.db, boundRig);
332
+ let entryGate;
333
+ let entryOwner;
334
+ try {
335
+ entryGate = entryStep.gate
336
+ ? compileGate(specRow.spec, entryStep, runtimeOf, entryRoleCtx)
337
+ : null;
338
+ if (entryGate) {
339
+ entryOwner = entryGate.destinationSession;
340
+ }
341
+ else if (input.entryOwnerSession) {
342
+ reconcileExplicitOwnerHarness(entryStep, input.entryOwnerSession, runtimeOf);
343
+ entryOwner = input.entryOwnerSession;
344
+ }
345
+ else {
346
+ entryOwner = resolveDefaultOwner(specRow.spec, entryStep, runtimeOf, entryRoleCtx);
347
+ }
348
+ }
349
+ catch (err) {
350
+ if (err instanceof WorkflowProjectorError && err.code === "next_owner_unresolved") {
351
+ // entry_owner_unresolved SPEAKS CANDIDATES: same structured
352
+ // details, the entry site's error-code contract preserved.
353
+ throw new WorkflowProjectorError("entry_owner_unresolved", `cannot instantiate: ${err.message}`, { specPath: input.specPath, entryStepId: entryStep.id, entryRole: entryStep.actor_role, ...(err.details ?? {}) });
354
+ }
355
+ throw err;
356
+ }
66
357
  if (!entryOwner) {
67
358
  throw new WorkflowProjectorError("entry_owner_unresolved", `cannot instantiate: entry step "${entryStep.id}" (role "${entryStep.actor_role}") has no preferred_targets and no entryOwnerSession was supplied`, { specPath: input.specPath, entryStepId: entryStep.id, entryRole: entryStep.actor_role });
68
359
  }
@@ -82,10 +373,31 @@ export class WorkflowRuntime {
82
373
  // projector resolves the correct step on the first project()
83
374
  // call without any trail-based inference.
84
375
  currentStepId: entryStep.id,
376
+ // OPR.0.4.6.FAC1: the resolved rig binding persists with the
377
+ // instance row (same txn as the entry packet).
378
+ boundRig,
85
379
  });
86
380
  instanceId = instance.instanceId;
87
- // Create entry qitem in the same txn.
381
+ // Create entry qitem in the same txn (gate-aware: a gated entry
382
+ // rides the shipped human-route / handler-route write path).
383
+ // OPR.0.4.6.WF5 FR-1 class (c) (guard code-review fold — the entry
384
+ // twin of the projector's mid-flow stamp): a HUMAN-gated ENTRY
385
+ // carries the class-(c) exception identity on the WF-2 item
386
+ // itself, occurrence = the preallocated packet id. Handler-role
387
+ // entries stay negative.
388
+ const entryGateQitemId = entryGate ? newQitemId() : undefined;
389
+ const entryGateException = entryGate && entryGateQitemId
390
+ ? classifyGateTrip({
391
+ workflowName: specRow.name,
392
+ instanceId: instance.instanceId,
393
+ gatedStepId: entryStep.id,
394
+ gateKind: entryGate.kind,
395
+ gatePacketId: entryGateQitemId,
396
+ parkOn: entryGate.parkOn,
397
+ })
398
+ : null;
88
399
  const created = this.queueRepo.createWithinTransaction({
400
+ qitemId: entryGateQitemId,
89
401
  sourceSession: input.createdBySession,
90
402
  destinationSession: entryOwner,
91
403
  body: workflowInstantiateBody({
@@ -93,16 +405,55 @@ export class WorkflowRuntime {
93
405
  instanceId: instance.instanceId,
94
406
  entryStep,
95
407
  rootObjective: input.rootObjective,
408
+ gate: entryGate,
96
409
  }),
97
410
  priority: "routine",
98
- tier: "mode2",
99
- tags: ["workflow", "entry", `workflow:${specRow.name}`, `instance:${instance.instanceId}`],
411
+ tier: entryGate?.tier ?? "mode2",
412
+ tags: [
413
+ "workflow",
414
+ entryGate ? "gate" : "entry",
415
+ `workflow:${specRow.name}`,
416
+ `instance:${instance.instanceId}`,
417
+ ...(entryGateException ? workflowExceptionTags(entryGateException.identity).filter((t) => !t.startsWith("workflow:") && !t.startsWith("instance:")) : []),
418
+ ],
419
+ summary: entryGate?.summary ?? undefined,
420
+ evidenceRef: entryGate?.evidenceRef ?? undefined,
100
421
  });
101
422
  entryQitemId = created.qitemId;
102
423
  entryQitemDestinationSession = created.destinationSession;
103
424
  entryQitemNudge = created.nudge;
104
425
  persistedEvents.push(created.persistedEvent);
105
- this.instanceStore.updateFrontier(instance.instanceId, [created.qitemId], "active");
426
+ // OPR.0.4.6.WF2 FR-5 (guard blocker 1): a HUMAN-gated ENTRY parks
427
+ // in the same txn — the leg-1 blocked_on human-seat shape the
428
+ // shipped resolve verb acts on (same as the projector's mid-flow
429
+ // gate park).
430
+ if (entryGate?.parkOn) {
431
+ const parked = this.queueRepo.updateWithinTransaction({
432
+ qitemId: created.qitemId,
433
+ actorSession: input.createdBySession,
434
+ state: "blocked",
435
+ closureReason: "blocked_on",
436
+ closureTarget: entryGate.parkOn,
437
+ blockedOn: entryGate.parkOn,
438
+ transitionNote: `workflow gate: parked on ${entryGate.parkOn} pending sign-off`,
439
+ });
440
+ persistedEvents.push(parked.persistedEvent);
441
+ }
442
+ this.instanceStore.updateFrontier(instance.instanceId, [created.qitemId], entryGate ? "waiting" : "active", {
443
+ // FR-5: guarded even here — the instance was created in this
444
+ // txn at version 0; uniformity keeps every advance guarded.
445
+ expectedVersion: instance.version,
446
+ });
447
+ // FR-3: arm the per-instance keepalive INSIDE the same txn that
448
+ // creates the entry packet (covers commit-then-crash-before-nudge
449
+ // from the very first step).
450
+ if (this.watchdogJobsRepo) {
451
+ ensureWorkflowKeepaliveArmed(this.watchdogJobsRepo, {
452
+ instanceId: instance.instanceId,
453
+ targetSession: entryOwner,
454
+ registeredBySession: input.createdBySession,
455
+ });
456
+ }
106
457
  persistedEvents.push(this.eventBus.persistWithinTransaction({
107
458
  type: "workflow.instantiated",
108
459
  instanceId: instance.instanceId,
@@ -123,11 +474,328 @@ export class WorkflowRuntime {
123
474
  spec: specRow,
124
475
  entryQitemId: entryQitemId,
125
476
  entryOwnerSession: entryOwner,
477
+ advisories,
126
478
  };
127
479
  }
128
480
  async project(input) {
129
481
  return this.projector.project(input);
130
482
  }
483
+ /**
484
+ * OPR.0.4.6.WF3 FR-4 — `route`: re-target the CURRENT FRONTIER step
485
+ * of a live instance to a new owner. THE ADJUDICATED MECHANISM
486
+ * (arch, formal, on the advance-authority ground): CLOSE + RECREATE
487
+ * + FRONTIER REBIND in ONE scribe transaction. Revocation is
488
+ * STRUCTURAL: the old packet leaves the frontier inside this txn, so
489
+ * a zombie old owner's stale `project` hits the shipped
490
+ * `packet_not_on_frontier` 409 — zero new validation machinery on
491
+ * the hot advance path (the weighed-and-rejected alternative).
492
+ *
493
+ * The observable contract (PRD FR-4 (1)-(8)):
494
+ * (1) owner = target after route (5) frontier non-dangling
495
+ * (2) current_step_id UNCHANGED (6) additive event detail
496
+ * (3) actor+reason+old→new durable (7) pin + version guard held
497
+ * (4) NO forged completion closure (8) zombie structurally 409'd
498
+ * Route is NOT an advance: hop_count does not bump (max_hops counts
499
+ * steps, not re-targets); BR-3 — `project` stays the sole advance.
500
+ */
501
+ /**
502
+ * OPR.0.4.6.WF5 FR-4 — RESUME from where it stopped (redrive
503
+ * semantics, the one engine extension). One scribe transaction:
504
+ * failed → active REBOUND to the failed step, a FRESH frontier packet
505
+ * to the step's RE-RESOLVED owner, the trail preserved and extended
506
+ * (completed steps never re-run), the livelock rail re-baselined
507
+ * (hops-since-resume — exactly one more bounded window), the redrive
508
+ * count recorded, open exception items for THIS occurrence closed
509
+ * (resolve+resume closes the occurrence; a later re-failure is a NEW
510
+ * occurrence), keepalive re-armed, additive workflow.resumed event.
511
+ *
512
+ * THE ARCH PIN (plan Rev-2, binding): the owner is RE-RESOLVED
513
+ * through the SAME resolution path projection uses
514
+ * (resolveDefaultOwner — preferred_targets + harness reconciliation),
515
+ * NEVER copied from the closed packet's recorded destination: a dead
516
+ * seat is a common CAUSE of the exception, and resume is the one
517
+ * sanctioned re-resolution point (FAC-1 R1) the binding layer later
518
+ * upgrades uniformly.
519
+ */
520
+ async resume(input) {
521
+ const persistedEvents = [];
522
+ let result;
523
+ let nudgeTo = null;
524
+ const txn = this.db.transaction(() => {
525
+ const instance = this.instanceStore.getByIdOrThrow(input.instanceId);
526
+ if (instance.status !== "failed") {
527
+ throw new WorkflowProjectorError("instance_not_failed", `instance ${instance.instanceId} is ${instance.status}; resume re-drives FAILED instances only (a waiting instance resumes via the shipped project path; an active instance needs no resume)`, { instanceId: instance.instanceId, status: instance.status, expectedStatus: "failed" });
528
+ }
529
+ const specRow = this.specCache.getByNameVersion(instance.workflowName, instance.workflowVersion);
530
+ if (!specRow) {
531
+ throw new WorkflowProjectorError("spec_not_cached", `workflow spec ${instance.workflowName}@${instance.workflowVersion} is not in the spec cache; re-run validate to refresh it before resuming`, { workflowName: instance.workflowName, workflowVersion: instance.workflowVersion });
532
+ }
533
+ const spec = specRow.spec;
534
+ const decision = (instance.lastContinuationDecision ?? {});
535
+ const failedStepId = decision.currentStep;
536
+ const failedPacketId = decision.closedPacket;
537
+ if (!failedStepId) {
538
+ throw new WorkflowProjectorError("resume_step_unrecoverable", `instance ${instance.instanceId} carries no recorded failed step (pre-R2 row without lastContinuationDecision.currentStep); cannot rebind — instantiate a fresh run`, { instanceId: instance.instanceId });
539
+ }
540
+ const step = spec.steps.find((st) => st.id === failedStepId);
541
+ if (!step) {
542
+ throw new WorkflowProjectorError("resume_step_missing_from_spec", `failed step "${failedStepId}" no longer exists in ${instance.workflowName}@${instance.workflowVersion}; fix the spec (the cached version is authoritative for in-flight instances) or instantiate a fresh run`, { instanceId: instance.instanceId, stepId: failedStepId });
543
+ }
544
+ // THE ARCH PIN: re-resolve, never copy.
545
+ // OPR.0.4.6.FAC1 (call-site row 5): resume is the ONE sanctioned
546
+ // re-resolution point and now runs the full tier stack — a bound
547
+ // instance's role-only failed step re-resolves capability-aware
548
+ // against CURRENT inventory (a dead seat is a common CAUSE of the
549
+ // exception; the redrive picks the seat that is eligible NOW).
550
+ const owner = resolveDefaultOwner(spec, step, (session) => nodeRuntimeOf(this.db, session), roleResolutionContext(this.db, instance.boundRig));
551
+ if (!owner) {
552
+ throw new WorkflowProjectorError("next_owner_unresolved", `cannot resolve an owner for failed step "${step.id}" (role "${step.actor_role}"); add preferred_targets to the role before resuming`, { instanceId: instance.instanceId, stepId: step.id, role: step.actor_role });
553
+ }
554
+ // Fresh frontier packet — the redrive delivery. The --decision
555
+ // text lands durably in the packet body (the resumer’s
556
+ // instruction reaches the step owner).
557
+ const created = this.queueRepo.createWithinTransaction({
558
+ sourceSession: input.actorSession,
559
+ destinationSession: owner,
560
+ body: `WORKFLOW RESUME (redrive)\n` +
561
+ `workflow: ${instance.workflowName} v${instance.workflowVersion}\n` +
562
+ `instance: ${instance.instanceId}\n` +
563
+ `step: ${step.id} (role ${step.actor_role}) — re-driven from the recorded failure; completed steps are NOT re-run\n` +
564
+ `resumed by: ${input.actorSession} (redrive #${(instance.resumeCount ?? 0) + 1})\n` +
565
+ (input.decision ? `decision: ${input.decision}\n` : "") +
566
+ `history: rig workflow trace ${instance.instanceId}`,
567
+ priority: "routine",
568
+ tier: "mode2",
569
+ tags: [
570
+ "workflow",
571
+ "resume",
572
+ `workflow:${instance.workflowName}`,
573
+ `instance:${instance.instanceId}`,
574
+ ],
575
+ chainOfRecord: failedPacketId ? [failedPacketId] : undefined,
576
+ });
577
+ persistedEvents.push(created.persistedEvent);
578
+ nudgeTo = { qitemId: created.qitemId, session: created.destinationSession, nudge: created.nudge };
579
+ // Resolve+resume CLOSES the occurrence: open exception items for
580
+ // THIS episode close honestly with resume provenance. A later
581
+ // re-failure mints a NEW packet id = a NEW occurrence (never
582
+ // hidden behind this resolved past).
583
+ let exceptionItemsClosed = 0;
584
+ if (failedPacketId) {
585
+ const openItems = this.db
586
+ .prepare(`SELECT qitem_id FROM queue_items
587
+ WHERE state IN ('pending','in-progress','blocked')
588
+ AND tags LIKE ? AND tags LIKE ?`)
589
+ .all(`%"occurrence:${failedPacketId}"%`, `%"workflow-exception"%`);
590
+ for (const row of openItems) {
591
+ const closedItem = this.queueRepo.updateWithinTransaction({
592
+ qitemId: row.qitem_id,
593
+ actorSession: input.actorSession,
594
+ state: "done",
595
+ closureReason: "no-follow-on",
596
+ transitionNote: `workflow resume: occurrence resolved by ${input.actorSession} redriving step ${step.id}${input.decision ? ` — ${input.decision}` : ""}`,
597
+ });
598
+ persistedEvents.push(closedItem.persistedEvent);
599
+ exceptionItemsClosed += 1;
600
+ }
601
+ }
602
+ // Frontier rebind + status + THE LIVELOCK RAIL: hops_baseline =
603
+ // hopCount at resume (one fresh bounded window under the same
604
+ // max_hops), resume_count recorded, version guard held
605
+ // (concurrent resumes: exactly one commits).
606
+ this.instanceStore.updateFrontier(instance.instanceId, [created.qitemId], "active", {
607
+ currentStepId: step.id,
608
+ expectedVersion: instance.version,
609
+ resumeStamp: {
610
+ resumeCount: (instance.resumeCount ?? 0) + 1,
611
+ hopsBaseline: instance.hopCount,
612
+ },
613
+ });
614
+ // Keepalive re-arm for the redriven owner (in-txn, WF-1 FR-3).
615
+ if (this.watchdogJobsRepo) {
616
+ ensureWorkflowKeepaliveArmed(this.watchdogJobsRepo, {
617
+ instanceId: instance.instanceId,
618
+ targetSession: owner,
619
+ registeredBySession: input.actorSession,
620
+ });
621
+ }
622
+ persistedEvents.push(this.eventBus.persistWithinTransaction({
623
+ type: "workflow.resumed",
624
+ instanceId: instance.instanceId,
625
+ workflowName: instance.workflowName,
626
+ stepId: step.id,
627
+ resumedBy: input.actorSession,
628
+ decision: input.decision ?? null,
629
+ resumeCount: (instance.resumeCount ?? 0) + 1,
630
+ }));
631
+ result = {
632
+ instanceId: instance.instanceId,
633
+ stepId: step.id,
634
+ newPacketId: created.qitemId,
635
+ ownerSession: owner,
636
+ resumeCount: (instance.resumeCount ?? 0) + 1,
637
+ exceptionItemsClosed,
638
+ };
639
+ });
640
+ txn();
641
+ for (const e of persistedEvents) {
642
+ this.eventBus.notifySubscribers(e);
643
+ }
644
+ // Closure-assignment cast (the shipped post-commit idiom): TS cannot
645
+ // track the txn-closure write, so narrow via the cast.
646
+ const resumeNudge = nudgeTo;
647
+ if (resumeNudge) {
648
+ await this.queueRepo.maybeNudge(resumeNudge.qitemId, resumeNudge.session, resumeNudge.nudge);
649
+ }
650
+ return result;
651
+ }
652
+ async route(input) {
653
+ const persistedEvents = [];
654
+ let result;
655
+ let nudgeTo = null;
656
+ const txn = this.db.transaction(() => {
657
+ const instance = this.instanceStore.getByIdOrThrow(input.instanceId);
658
+ if (instance.status !== "active" && instance.status !== "waiting") {
659
+ throw new WorkflowProjectorError("instance_not_active", `instance ${instance.instanceId} is ${instance.status}; only a live (active|waiting) instance can be re-routed`, { instanceId: instance.instanceId, status: instance.status });
660
+ }
661
+ const oldPacketId = instance.currentFrontier[0];
662
+ if (!oldPacketId) {
663
+ throw new WorkflowProjectorError("packet_not_found", `instance ${instance.instanceId} has an empty frontier; nothing to re-route`, { instanceId: instance.instanceId });
664
+ }
665
+ const oldPacket = this.queueRepo.getById(oldPacketId);
666
+ if (!oldPacket) {
667
+ throw new WorkflowProjectorError("packet_not_found", `frontier packet ${oldPacketId} not found`, { instanceId: instance.instanceId, packetId: oldPacketId });
668
+ }
669
+ const fromSession = oldPacket.destinationSession;
670
+ // (7) harness pin: the SAME reconciliation the projector applies
671
+ // to explicit --next-owner overrides — an explicit route target
672
+ // never silently defeats a declared pin.
673
+ const specRow = this.specCache.getByNameVersion(instance.workflowName, instance.workflowVersion);
674
+ const step = instance.currentStepId && specRow
675
+ ? specRow.spec.steps.find((s) => s.id === instance.currentStepId) ?? null
676
+ : null;
677
+ if (step) {
678
+ reconcileExplicitOwnerHarness(step, input.toSession, (session) => nodeRuntimeOf(this.db, session));
679
+ }
680
+ // (3)+(4) close the old packet HONESTLY: handed_off_to with full
681
+ // provenance in the transition — never a forged completion.
682
+ const closed = this.queueRepo.updateWithinTransaction({
683
+ qitemId: oldPacketId,
684
+ actorSession: input.actorSession,
685
+ viaWorkflowVerb: true,
686
+ state: "handed-off",
687
+ closureReason: "handed_off_to",
688
+ closureTarget: input.toSession,
689
+ handedOffTo: input.toSession,
690
+ transitionNote: `workflow route: ${input.actorSession} re-routed step ${instance.currentStepId ?? "?"} from ${fromSession} to ${input.toSession}${input.reason ? ` — ${input.reason}` : ""}`,
691
+ });
692
+ persistedEvents.push(closed.persistedEvent);
693
+ // Recreate the SAME step for the new owner (step identity is the
694
+ // work's continuity — the qitem id is a storage artifact of the
695
+ // append-only design). chainOfRecord threads the lineage.
696
+ // FULL-FIDELITY FIELD CARRY (rev1-r2 BLOCKING fold): the
697
+ // successor IS the same work item, so it keeps the source
698
+ // packet's priority/tier/summary/evidenceRef/targetRepo — a
699
+ // human-gated packet (blocked on a human seat) MUST keep
700
+ // summary + evidence_ref or the shipped human-park validator
701
+ // rejects the repark below (human_route_fields_required) and
702
+ // the waiting-on-human class — the one route most exists for —
703
+ // becomes un-routable.
704
+ const created = this.queueRepo.createWithinTransaction({
705
+ sourceSession: input.actorSession,
706
+ destinationSession: input.toSession,
707
+ body: oldPacket.body,
708
+ priority: oldPacket.priority ?? "routine",
709
+ tier: oldPacket.tier ?? "mode2",
710
+ // OPR.0.4.6.WF5 (rev1-r2 B1 fold): the successor IS the same work
711
+ // item — tags carry VERBATIM (+ re-route) so a routed class-(c)
712
+ // gate item keeps its exception identity on the live frontier
713
+ // packet (workflow-exception/step:/exception:human_gate_trip/
714
+ // occurrence:<ORIGINAL gate packet id> — the occurrence is the
715
+ // EPISODE, which route does not end; chainOfRecord links the
716
+ // packet lineage). The WF-3 full-fidelity-carry lesson, extended
717
+ // to the tag dimension.
718
+ tags: Array.from(new Set([
719
+ ...(oldPacket.tags ?? []),
720
+ "workflow",
721
+ "re-route",
722
+ `workflow:${instance.workflowName}`,
723
+ `instance:${instance.instanceId}`,
724
+ ])),
725
+ chainOfRecord: [oldPacketId],
726
+ summary: oldPacket.summary ?? undefined,
727
+ evidenceRef: oldPacket.evidenceRef ?? undefined,
728
+ targetRepo: oldPacket.targetRepo ?? undefined,
729
+ });
730
+ persistedEvents.push(created.persistedEvent);
731
+ nudgeTo = { qitemId: created.qitemId, session: created.destinationSession, nudge: created.nudge };
732
+ // A parked (waiting) frontier packet keeps its park on the
733
+ // successor — route changes the owner, never the recorded state.
734
+ // summary/evidenceRef passed explicitly too (belt + suspenders
735
+ // with the create-side carry): validateHumanPark evaluates the
736
+ // EFFECTIVE values, so a human park re-parks with its fields
737
+ // intact instead of throwing human_route_fields_required.
738
+ if (oldPacket.state === "blocked" && oldPacket.blockedOn) {
739
+ const reparked = this.queueRepo.updateWithinTransaction({
740
+ qitemId: created.qitemId,
741
+ actorSession: input.actorSession,
742
+ state: "blocked",
743
+ closureReason: "blocked_on",
744
+ closureTarget: oldPacket.blockedOn,
745
+ blockedOn: oldPacket.blockedOn,
746
+ summary: oldPacket.summary ?? undefined,
747
+ evidenceRef: oldPacket.evidenceRef ?? undefined,
748
+ transitionNote: `workflow route: park preserved (${oldPacket.blockedOn})`,
749
+ });
750
+ persistedEvents.push(reparked.persistedEvent);
751
+ }
752
+ // (2)+(5)+(7) frontier REBIND: same step, new packet, version
753
+ // guard held; NO hop bump (not an advance).
754
+ this.instanceStore.updateFrontier(instance.instanceId, [created.qitemId], instance.status, {
755
+ currentStepId: "preserve",
756
+ expectedVersion: instance.version,
757
+ });
758
+ // Keepalive re-target IN-TXN (arch n3: covers route's lost-nudge
759
+ // window — the armed job re-nudges the new owner).
760
+ if (this.watchdogJobsRepo) {
761
+ disarmWorkflowKeepalive(this.watchdogJobsRepo, instance.instanceId, `workflow route: re-targeted to ${input.toSession}`);
762
+ ensureWorkflowKeepaliveArmed(this.watchdogJobsRepo, {
763
+ instanceId: instance.instanceId,
764
+ targetSession: input.toSession,
765
+ registeredBySession: input.actorSession,
766
+ });
767
+ }
768
+ // (6) the shipped event shape {rigName, cause} extended ADDITIVELY.
769
+ persistedEvents.push(this.eventBus.persistWithinTransaction({
770
+ type: "workflow.routing_table_changed",
771
+ // OPR.0.4.6.FAC1 (display-only): the instance's actual bound
772
+ // rig wins over the spec's default label.
773
+ rigName: instance.boundRig ?? specRow?.targetRig ?? "",
774
+ cause: "workflow_route",
775
+ instanceId: instance.instanceId,
776
+ stepId: instance.currentStepId,
777
+ from: fromSession,
778
+ to: input.toSession,
779
+ }));
780
+ result = {
781
+ instanceId: instance.instanceId,
782
+ stepId: instance.currentStepId,
783
+ closedPacketId: oldPacketId,
784
+ newPacketId: created.qitemId,
785
+ fromSession,
786
+ toSession: input.toSession,
787
+ instanceStatus: instance.status,
788
+ };
789
+ });
790
+ txn();
791
+ for (const e of persistedEvents)
792
+ this.eventBus.notifySubscribers(e);
793
+ if (nudgeTo) {
794
+ const n = nudgeTo;
795
+ await this.queueRepo.maybeNudge(n.qitemId, n.session, n.nudge, input.actorSession);
796
+ }
797
+ return result;
798
+ }
131
799
  /**
132
800
  * Continue: idempotent inspector for the current frontier of an
133
801
  * instance. v1 is read-only — returns the current state. POC's
@@ -137,7 +805,39 @@ export class WorkflowRuntime {
137
805
  continue(instanceId) {
138
806
  const instance = this.instanceStore.getByIdOrThrow(instanceId);
139
807
  const trail = this.trailLog.listForInstance(instanceId);
140
- return { instance, trail };
808
+ return { instance: this.withDeadline(instance), trail };
809
+ }
810
+ /**
811
+ * OPR.0.4.6.WF1 FR-2 COMPLETION FIXBACK (build-vs-ratified-AC debt,
812
+ * qitem-20260706211220-279039f5): the ratified FR-2 AC requires the
813
+ * stuck classification to be "queryable via list/show/trace … with
814
+ * the evidence (step, owner, deadline, age)". The merged WF-1 build
815
+ * surfaced it only through the boot sweep + keepalive nudges; this
816
+ * closes the queryability clause by deriving the SAME evaluator
817
+ * verdict (one threshold home — workflow-deadline.ts) at read time.
818
+ *
819
+ * DERIVED, NEVER STORED: recomputed per read from (instance,
820
+ * frontier packets, now) — a normal re-projection self-clears it,
821
+ * exactly like every other evaluator consumer. Exposes the FULL
822
+ * classification tuple (state + evidence{step, owner, anchor,
823
+ * anchorAt, overdueBySeconds, ageSeconds}) so BOTH consumers (WF-3's
824
+ * status rollup and WF-5's FR-3 ▲ source) read one shape — no
825
+ * boolean flattening, no second path.
826
+ */
827
+ deadlineFor(instance) {
828
+ const packets = instance.currentFrontier
829
+ .map((id) => this.queueRepo.getById(id))
830
+ .filter((p) => p != null);
831
+ return evaluateStepDeadline(instance, packets, this.now());
832
+ }
833
+ /** The additive read enrichment consumed by list/show/trace routes. */
834
+ withDeadline(instance) {
835
+ return { ...instance, deadline: this.deadlineFor(instance) };
836
+ }
837
+ /** List instances (optionally filtered) with the deadline verdict attached. */
838
+ listInstancesWithDeadline(status) {
839
+ const rows = status ? this.instanceStore.listByStatus(status) : this.instanceStore.listAll();
840
+ return rows.map((row) => this.withDeadline(row));
141
841
  }
142
842
  }
143
843
  function workflowInstantiateBody(input) {
@@ -149,6 +849,13 @@ function workflowInstantiateBody(input) {
149
849
  "",
150
850
  `Root objective: ${input.rootObjective}`,
151
851
  ];
852
+ if (input.gate) {
853
+ 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.`);
854
+ if (input.gate.summary)
855
+ lines.push(`Ask: ${input.gate.summary}`);
856
+ if (input.gate.evidenceRef)
857
+ lines.push(`Evidence: ${input.gate.evidenceRef}`);
858
+ }
152
859
  if (input.entryStep.objective) {
153
860
  lines.push("", `Step objective: ${input.entryStep.objective}`);
154
861
  }