elanous 0.2.17 → 0.2.19

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 (491) hide show
  1. package/apps/pwa/out/404/index.html +1 -1
  2. package/apps/pwa/out/404.html +1 -1
  3. package/apps/pwa/out/_next/static/J5H7BvqgSI8DUjtRRcA_S/_buildManifest.js +1 -0
  4. package/apps/pwa/out/_next/static/chunks/1105-c473ed46272fba79.js +1 -0
  5. package/apps/pwa/out/_next/static/chunks/1157-cba0e8553d35a980.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/{726.83532d69452ee24b.js → 1470.98a6ab61ccb4131c.js} +1 -1
  7. package/apps/pwa/out/_next/static/chunks/171-23a877ed11720de9.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/216-5acd01cbde4acda7.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/2330-c2f07246c8788af1.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/2336-e830c777eb6bce04.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/{9247-269e4464cc4b491c.js → 2375-e061bb24cd2b5cde.js} +1 -1
  12. package/apps/pwa/out/_next/static/chunks/2415.ed6428ff334c7ce2.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/{211-9c9090c59e2c5907.js → 2977-e38442e86289f1d3.js} +1 -1
  14. package/apps/pwa/out/_next/static/chunks/{f6b7c9ba.64a895b8b2fedbdc.js → 2ea96958.e8c7e112de50b067.js} +1 -1
  15. package/apps/pwa/out/_next/static/chunks/3094.67339318c7f72a3a.js +2 -0
  16. package/apps/pwa/out/_next/static/chunks/338-ceb956612dcfaa86.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/3395-835e4ceaa6335ba4.js +1 -0
  18. package/apps/pwa/out/_next/static/chunks/3403.4bc97b4066945a94.js +5 -0
  19. package/apps/pwa/out/_next/static/chunks/3441-be82e3595aef3a20.js +1 -0
  20. package/apps/pwa/out/_next/static/chunks/3689-89905039bf3e7257.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/371-4ad6af55dad02ced.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/3822-6f268bc26c6a84ac.js +1 -0
  23. package/apps/pwa/out/_next/static/chunks/4114-3e63117452378807.js +1 -0
  24. package/apps/pwa/out/_next/static/chunks/{1268.0840ff91e083fcbd.js → 4323.c5dfb6f503304775.js} +1 -1
  25. package/apps/pwa/out/_next/static/chunks/4457-d7df81c90f9d0268.js +1 -0
  26. package/apps/pwa/out/_next/static/chunks/4551-77dadae76f6cbace.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/{6110.1297d4093fdfbb0d.js → 4847.56f5bc454fbcc827.js} +2 -2
  28. package/apps/pwa/out/_next/static/chunks/4978.6fd93b974441e87f.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/5012-f746f6549b869f91.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/5609-012bb3557211f07e.js +1 -0
  31. package/apps/pwa/out/_next/static/chunks/5791-bb82743fa6299562.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/{7327-2d69fb321ba73389.js → 6062-73e68343a9814f6a.js} +1 -1
  33. package/apps/pwa/out/_next/static/chunks/62-5bfed1e23790b745.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/6320-9bc8561fde416e07.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/649-ca96d1c5d09e02b8.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/6725-68ab7cc0b1faf00d.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/6726-85a7db59b288c53f.js +1 -0
  38. package/apps/pwa/out/_next/static/chunks/6735-33267c4630d0d963.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/6998-a654fe5c58271600.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/7633-141682015873beb6.js +1 -0
  41. package/apps/pwa/out/_next/static/chunks/{935.4ddaa99fc1d1126c.js → 7647.2fcd30d894d5ee30.js} +1 -1
  42. package/apps/pwa/out/_next/static/chunks/7673-2eda1507d38dadb8.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/7990-e12efba616f46e03.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/{f6231284-45726550aba08fca.js → 8046cadf-211ca3c0e00f2b72.js} +1 -1
  45. package/apps/pwa/out/_next/static/chunks/8067-143791231c99bd02.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/8142-744998d65d7bb17e.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/8430-a10cfba8ef282d4e.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/8870-9614a213b990b10b.js +25 -0
  49. package/apps/pwa/out/_next/static/chunks/920-f7dceb78f1c585ef.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/9454-12c2f30dea62291c.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/976-4a9b4ebdca7c739a.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/9989-15cdf568bafa9fc8.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/ae787e4c-e5b1ef00b2dbceca.js +1 -0
  54. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-859cea1570b6227a.js +1 -0
  55. package/apps/pwa/out/_next/static/chunks/app/approvals/page-deef5ee191830f14.js +1 -0
  56. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-b193da485f9a14b2.js +1 -0
  57. package/apps/pwa/out/_next/static/chunks/app/board/page-b6e1a79df0e214f4.js +1 -0
  58. package/apps/pwa/out/_next/static/chunks/app/botlab/page-e84df4d907f703c7.js +1 -0
  59. package/apps/pwa/out/_next/static/chunks/app/bots/page-a5fb9a5db89ae4ea.js +1 -0
  60. package/apps/pwa/out/_next/static/chunks/app/ceo/page-1499f481d7fc52d6.js +1 -0
  61. package/apps/pwa/out/_next/static/chunks/app/chat/page-c7cf753aeb648790.js +1 -0
  62. package/apps/pwa/out/_next/static/chunks/app/consult/page-4118469e514e10fd.js +1 -0
  63. package/apps/pwa/out/_next/static/chunks/app/control/page-e968f7630654332a.js +1 -0
  64. package/apps/pwa/out/_next/static/chunks/app/decisions/page-505915052f947022.js +1 -0
  65. package/apps/pwa/out/_next/static/chunks/app/design-check/page-c2f962fb4a3b06c6.js +1 -0
  66. package/apps/pwa/out/_next/static/chunks/app/editor/page-7b5ed0bd8a589ec4.js +1 -0
  67. package/apps/pwa/out/_next/static/chunks/app/exec/page-cf8193250dd42dd4.js +1 -0
  68. package/apps/pwa/out/_next/static/chunks/app/field/page-cf3f5dfa35dea8e9.js +1 -0
  69. package/apps/pwa/out/_next/static/chunks/app/healthz/route-859cea1570b6227a.js +1 -0
  70. package/apps/pwa/out/_next/static/chunks/app/inside/page-5b29d9b06ceadade.js +1 -0
  71. package/apps/pwa/out/_next/static/chunks/app/intake/page-a15e7fcc283e9ca7.js +1 -0
  72. package/apps/pwa/out/_next/static/chunks/app/layout-d2915e7b70ff3d0e.js +1 -0
  73. package/apps/pwa/out/_next/static/chunks/app/live/page-b70cd005d9169e25.js +1 -0
  74. package/apps/pwa/out/_next/static/chunks/app/loops/page-2595f0eba29f57e1.js +1 -0
  75. package/apps/pwa/out/_next/static/chunks/app/market/page-c776322454156e36.js +1 -0
  76. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-77bbd17f4435aa10.js +1 -0
  77. package/apps/pwa/out/_next/static/chunks/app/morning/page-049689e611ecc936.js +1 -0
  78. package/apps/pwa/out/_next/static/chunks/app/not-found-85954a4fb09c44eb.js +1 -0
  79. package/apps/pwa/out/_next/static/chunks/app/observatory/page-e773e552e1068ee6.js +1 -0
  80. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-5633c4bfa7684b4a.js +1 -0
  81. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-d829b92bd01b4994.js +1 -0
  82. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-061c317fc771becf.js +1 -0
  83. package/apps/pwa/out/_next/static/chunks/app/outputs/page-3a45ece8865ed4b6.js +1 -0
  84. package/apps/pwa/out/_next/static/chunks/app/page-0be990618c0d56bd.js +1 -0
  85. package/apps/pwa/out/_next/static/chunks/app/reflection/page-bfea4865dc244b73.js +1 -0
  86. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-0d6ba8e337741484.js +1 -0
  87. package/apps/pwa/out/_next/static/chunks/app/sessions/page-76a88b55d816482e.js +1 -0
  88. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-bba1542df59f61ce.js +1 -0
  89. package/apps/pwa/out/_next/static/chunks/app/settings/page-4185c95689bb372e.js +1 -0
  90. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-b699e1a8d5d34b8b.js +1 -0
  91. package/apps/pwa/out/_next/static/chunks/app/setup/layout-859cea1570b6227a.js +1 -0
  92. package/apps/pwa/out/_next/static/chunks/app/setup/page-c225e338c31bf274.js +1 -0
  93. package/apps/pwa/out/_next/static/chunks/app/share/page-449353f70507ca1f.js +1 -0
  94. package/apps/pwa/out/_next/static/chunks/app/showroom/page-90b889714102476d.js +1 -0
  95. package/apps/pwa/out/_next/static/chunks/app/tasks/page-f1ee7fbeb7f96ddc.js +1 -0
  96. package/apps/pwa/out/_next/static/chunks/app/term/page-d59a95e1434dfaa0.js +1 -0
  97. package/apps/pwa/out/_next/static/chunks/app/today/page-71491a23414d1a47.js +1 -0
  98. package/apps/pwa/out/_next/static/chunks/app/trace/page-de7112bdb65fa4fe.js +1 -0
  99. package/apps/pwa/out/_next/static/chunks/app/vault/page-feb4d0d88079660d.js +1 -0
  100. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-f17d887b40dd6b1d.js +1 -0
  101. package/apps/pwa/out/_next/static/chunks/app/workflows/page-8da77be2192bcd5d.js +1 -0
  102. package/apps/pwa/out/_next/static/chunks/app/workspace/page-72c3b9a5e9582b29.js +1 -0
  103. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-c3afe4c8d0c48c39.js +1 -0
  104. package/apps/pwa/out/_next/static/chunks/framework-ea806bb57f2b8923.js +1 -0
  105. package/apps/pwa/out/_next/static/chunks/main-8d2302399c2c8f87.js +1 -0
  106. package/apps/pwa/out/_next/static/chunks/main-app-b9308f2d8a931531.js +1 -0
  107. package/apps/pwa/out/_next/static/chunks/pages/_app-b10928c60f0b8488.js +1 -0
  108. package/apps/pwa/out/_next/static/chunks/pages/_error-689039227d443fab.js +1 -0
  109. package/apps/pwa/out/_next/static/chunks/webpack-6f35f9d892fa630b.js +1 -0
  110. package/apps/pwa/out/_next/static/css/{e99e3cff94ced78a.css → 599369d853c61df7.css} +1 -1
  111. package/apps/pwa/out/_next/static/css/85f53c6f35d11382.css +1 -0
  112. package/apps/pwa/out/_next/static/css/d3427996da11e480.css +1 -0
  113. package/apps/pwa/out/approvals/index.html +1 -1
  114. package/apps/pwa/out/approvals/index.txt +23 -23
  115. package/apps/pwa/out/autopilot/index.html +1 -1
  116. package/apps/pwa/out/autopilot/index.txt +23 -23
  117. package/apps/pwa/out/board/index.html +1 -1
  118. package/apps/pwa/out/board/index.txt +23 -23
  119. package/apps/pwa/out/botlab/index.html +1 -1
  120. package/apps/pwa/out/botlab/index.txt +22 -22
  121. package/apps/pwa/out/bots/index.html +1 -1
  122. package/apps/pwa/out/bots/index.txt +22 -22
  123. package/apps/pwa/out/ceo/index.html +1 -1
  124. package/apps/pwa/out/ceo/index.txt +23 -23
  125. package/apps/pwa/out/chat/index.html +1 -1
  126. package/apps/pwa/out/chat/index.txt +22 -22
  127. package/apps/pwa/out/consult/index.html +1 -1
  128. package/apps/pwa/out/consult/index.txt +23 -23
  129. package/apps/pwa/out/control/index.html +1 -1
  130. package/apps/pwa/out/control/index.txt +23 -23
  131. package/apps/pwa/out/decisions/index.html +1 -0
  132. package/apps/pwa/out/decisions/index.txt +30 -0
  133. package/apps/pwa/out/design-check/index.html +1 -1
  134. package/apps/pwa/out/design-check/index.txt +23 -23
  135. package/apps/pwa/out/editor/index.html +1 -1
  136. package/apps/pwa/out/editor/index.txt +25 -24
  137. package/apps/pwa/out/exec/index.html +1 -1
  138. package/apps/pwa/out/exec/index.txt +23 -23
  139. package/apps/pwa/out/field/index.html +1 -1
  140. package/apps/pwa/out/field/index.txt +23 -23
  141. package/apps/pwa/out/healthz +1 -1
  142. package/apps/pwa/out/index.html +1 -1
  143. package/apps/pwa/out/index.txt +22 -22
  144. package/apps/pwa/out/inside/index.html +1 -1
  145. package/apps/pwa/out/inside/index.txt +24 -23
  146. package/apps/pwa/out/intake/index.html +1 -1
  147. package/apps/pwa/out/intake/index.txt +23 -23
  148. package/apps/pwa/out/live/index.html +1 -1
  149. package/apps/pwa/out/live/index.txt +23 -23
  150. package/apps/pwa/out/loops/index.html +1 -1
  151. package/apps/pwa/out/loops/index.txt +28 -27
  152. package/apps/pwa/out/market/index.html +1 -1
  153. package/apps/pwa/out/market/index.txt +22 -22
  154. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  155. package/apps/pwa/out/missions/placeholder/index.txt +22 -22
  156. package/apps/pwa/out/morning/index.html +1 -1
  157. package/apps/pwa/out/morning/index.txt +23 -23
  158. package/apps/pwa/out/observatory/index.html +1 -1
  159. package/apps/pwa/out/observatory/index.txt +23 -23
  160. package/apps/pwa/out/ops/checklist/index.html +1 -1
  161. package/apps/pwa/out/ops/checklist/index.txt +22 -22
  162. package/apps/pwa/out/ops/release/index.html +1 -1
  163. package/apps/pwa/out/ops/release/index.txt +22 -22
  164. package/apps/pwa/out/ops/seats/index.html +1 -1
  165. package/apps/pwa/out/ops/seats/index.txt +22 -22
  166. package/apps/pwa/out/outputs/index.html +1 -1
  167. package/apps/pwa/out/outputs/index.txt +22 -22
  168. package/apps/pwa/out/reflection/index.html +1 -1
  169. package/apps/pwa/out/reflection/index.txt +23 -23
  170. package/apps/pwa/out/scheduler/index.html +1 -1
  171. package/apps/pwa/out/scheduler/index.txt +23 -23
  172. package/apps/pwa/out/sessions/index.html +1 -1
  173. package/apps/pwa/out/sessions/index.txt +22 -22
  174. package/apps/pwa/out/settings/devices/index.html +1 -1
  175. package/apps/pwa/out/settings/devices/index.txt +23 -23
  176. package/apps/pwa/out/settings/index.html +1 -1
  177. package/apps/pwa/out/settings/index.txt +23 -23
  178. package/apps/pwa/out/setup/done/index.html +1 -1
  179. package/apps/pwa/out/setup/done/index.txt +23 -23
  180. package/apps/pwa/out/setup/index.html +1 -1
  181. package/apps/pwa/out/setup/index.txt +23 -23
  182. package/apps/pwa/out/share/index.html +1 -1
  183. package/apps/pwa/out/share/index.txt +23 -23
  184. package/apps/pwa/out/showroom/index.html +1 -1
  185. package/apps/pwa/out/showroom/index.txt +22 -22
  186. package/apps/pwa/out/tasks/index.html +1 -1
  187. package/apps/pwa/out/tasks/index.txt +23 -23
  188. package/apps/pwa/out/term/index.html +1 -1
  189. package/apps/pwa/out/term/index.txt +23 -23
  190. package/apps/pwa/out/today/index.html +1 -1
  191. package/apps/pwa/out/today/index.txt +23 -23
  192. package/apps/pwa/out/trace/index.html +1 -1
  193. package/apps/pwa/out/trace/index.txt +23 -23
  194. package/apps/pwa/out/vault/index.html +1 -1
  195. package/apps/pwa/out/vault/index.txt +23 -23
  196. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  197. package/apps/pwa/out/workflows/chat-ui/index.txt +23 -23
  198. package/apps/pwa/out/workflows/index.html +1 -1
  199. package/apps/pwa/out/workflows/index.txt +23 -23
  200. package/apps/pwa/out/workspace/index.html +1 -1
  201. package/apps/pwa/out/workspace/index.txt +23 -23
  202. package/apps/pwa/out/worktrees/index.html +1 -1
  203. package/apps/pwa/out/worktrees/index.txt +23 -23
  204. package/graphs/ops/guardian.yaml +26 -0
  205. package/graphs/ops/recipes.yaml +3 -0
  206. package/package.json +2 -1
  207. package/samples/workflows/templates/approval-gated-deploy.yaml +30 -0
  208. package/samples/workflows/templates/chat-driven-research.yaml +35 -0
  209. package/samples/workflows/templates/discord-listen-and-respond.yaml +29 -0
  210. package/samples/workflows/templates/http-poll-then-alert.yaml +30 -0
  211. package/samples/workflows/templates/manual-quickstart.yaml +21 -0
  212. package/samples/workflows/templates/scheduled-daily-digest.yaml +30 -0
  213. package/samples/workflows/templates/webhook-deploy-hook.yaml +27 -0
  214. package/scripts/ci-public-export-import-gate.ts +7 -2
  215. package/scripts/ci-typecheck-changed.ts +21 -6
  216. package/scripts/coord-channel-watch.sh +2 -2
  217. package/scripts/discord-seat-probe.ts +6 -3
  218. package/scripts/docs/generate-cli-anatomy.ts +162 -0
  219. package/scripts/goals/authoring-metrics.ts +73 -0
  220. package/scripts/landing-heal/must-fix-match.ts +7 -7
  221. package/scripts/landing-heal/run-mission.ts +3 -2
  222. package/scripts/lib/pch-regress-map.ts +23 -0
  223. package/scripts/morning-report.ts +18 -4
  224. package/scripts/pch-regress.ts +88 -0
  225. package/scripts/public-export-test-run.ts +37 -2
  226. package/scripts/release-loop/checklist-gate-node.ts +13 -11
  227. package/scripts/release-loop/cut-branch.ts +2 -1
  228. package/scripts/release-loop/cutoff.ts +4 -2
  229. package/scripts/release-loop/docs-land-node.ts +8 -1
  230. package/scripts/release-loop/docs-node.ts +1 -1
  231. package/scripts/release-loop/gate-diff.ts +18 -0
  232. package/scripts/release-loop/gate-node.ts +436 -59
  233. package/scripts/release-loop/gate-partial.ts +106 -0
  234. package/scripts/release-loop/gate-timing.ts +120 -0
  235. package/scripts/release-loop/node-verdict.ts +2 -1
  236. package/scripts/release-loop/npm-publish-node.ts +11 -7
  237. package/scripts/release-loop/ops-upgrade-node.ts +8 -1
  238. package/scripts/release-loop/preflight.ts +246 -0
  239. package/scripts/release-loop/publish-node.ts +8 -1
  240. package/scripts/release-loop/release-readiness.ts +11 -6
  241. package/scripts/release-loop/release-version.ts +48 -0
  242. package/scripts/release-loop/resume-release.ts +174 -0
  243. package/scripts/release-loop/shard-plan.ts +65 -0
  244. package/scripts/release-loop/unattended-release.ts +45 -7
  245. package/scripts/release-loop/version-node.ts +145 -40
  246. package/scripts/rhythm/daily.ts +25 -4
  247. package/scripts/skills/public-check.ts +62 -12
  248. package/scripts/test-deterministic.ts +51 -33
  249. package/scripts/test-diet/effectiveness.ts +62 -1
  250. package/scripts/test-diet/lib.ts +1 -1
  251. package/scripts/test-diet/node.ts +8 -1
  252. package/scripts/video/scene-table.ts +159 -0
  253. package/src/acp/core-turn-bridge.ts +39 -2
  254. package/src/agent-mission/review-loop.ts +4 -3
  255. package/src/agent-substrate/pr-comment-meta.ts +20 -0
  256. package/src/autopilot/build/llm-conflict-merge.ts +28 -8
  257. package/src/away/presence.ts +40 -0
  258. package/src/away/release-watch.ts +255 -0
  259. package/src/briefing/brief-items.ts +86 -17
  260. package/src/chat/index.ts +2 -1
  261. package/src/chat/short-question.ts +3 -40
  262. package/src/cli/autopilot-cli.ts +30 -11
  263. package/src/cli/away-cli.ts +68 -0
  264. package/src/cli/brief-cli.ts +36 -2
  265. package/src/cli/freeze-cli.ts +3 -2
  266. package/src/cli/guardian-cli.ts +49 -0
  267. package/src/cli/intake-cli.ts +85 -7
  268. package/src/cli/log-event-names.ts +1 -0
  269. package/src/cli/notify-cli.ts +75 -0
  270. package/src/cli/pod-cli.ts +5 -1
  271. package/src/cli/provider-cli.ts +12 -2
  272. package/src/cli/pty-drive-cli.ts +14 -3
  273. package/src/cli/release-cli.ts +128 -19
  274. package/src/cli/setup-cli.ts +31 -1
  275. package/src/cli/tasks-cli.ts +403 -4
  276. package/src/context-bus/condense.ts +19 -3
  277. package/src/context-bus/context-day.ts +77 -24
  278. package/src/context-bus/context-first.ts +33 -0
  279. package/src/context-bus/context-now-public.ts +1 -1
  280. package/src/context-bus/context-now-surfaces.ts +16 -6
  281. package/src/context-bus/context-now.ts +97 -3
  282. package/src/core-turn/short-question.ts +100 -0
  283. package/src/dashboard/dashboard-status-lines.ts +24 -4
  284. package/src/dashboard/index.ts +50 -5
  285. package/src/dashboard/input/slash-executor.ts +6 -2
  286. package/src/dashboard/slash-runtime/dashboard-handlers.ts +63 -36
  287. package/src/dashboard/slash-runtime/decide-slash.ts +60 -0
  288. package/src/dashboard/slash-runtime/help-from-registry.ts +21 -8
  289. package/src/dashboard/slash-runtime/help-groups.ts +44 -0
  290. package/src/dashboard/slash-runtime/model-choices.ts +125 -0
  291. package/src/discord-self-message.ts +20 -2
  292. package/src/discord-slash-wire.ts +1 -0
  293. package/src/domains/guardian-tick.ts +138 -0
  294. package/src/domains/outbound-alert.ts +244 -40
  295. package/src/domains/outbound-log-probe.ts +16 -1
  296. package/src/domains/release-tool.ts +2 -2
  297. package/src/domains/schedule-registry.ts +23 -0
  298. package/src/domains/telegram-kind-route.ts +39 -0
  299. package/src/domains/watch-default-scheduler.ts +95 -0
  300. package/src/esc-abort-gate.ts +44 -0
  301. package/src/execution-loop/gates.ts +11 -2
  302. package/src/flow-loop/split.ts +8 -4
  303. package/src/git-fs/worktree.ts +23 -3
  304. package/src/graph-runner/runner.ts +25 -5
  305. package/src/harness/harness-cli-command.ts +12 -2
  306. package/src/harness/harness-queue.ts +388 -33
  307. package/src/harness/harness-salvage-cli.ts +78 -0
  308. package/src/harness/harness-worktree-add.ts +68 -0
  309. package/src/harness/helper-scan.ts +10 -3
  310. package/src/harness/ledger-write-guard.ts +6 -16
  311. package/src/hq/hq.ts +6 -1
  312. package/src/hq/seats.ts +369 -0
  313. package/src/index.ts +185 -18
  314. package/src/instance/test-write-guard-spy-first.fixture.ts +13 -0
  315. package/src/instance/test-write-guard.ts +200 -0
  316. package/src/intake-plane/adopt-gate.ts +49 -0
  317. package/src/intake-plane/check.ts +3 -0
  318. package/src/intake-plane/collect-github.ts +57 -25
  319. package/src/intake-plane/digest.ts +39 -15
  320. package/src/intake-plane/discord-seat-work.ts +8 -2
  321. package/src/intake-plane/harness-target.ts +4 -3
  322. package/src/intake-plane/intake-sources.ts +1 -0
  323. package/src/intake-plane/items.ts +1 -0
  324. package/src/intake-plane/requirement-funnel.ts +316 -0
  325. package/src/knowledge/know-find.ts +10 -1
  326. package/src/loops/budget.ts +36 -0
  327. package/src/loops/orchestrator/degradation.ts +57 -0
  328. package/src/loops/orchestrator/finish-rate.ts +106 -4
  329. package/src/loops/orchestrator/grid-place.ts +11 -0
  330. package/src/loops/orchestrator/tick.ts +450 -54
  331. package/src/maturity/feature-maturity.ts +8 -5
  332. package/src/maturity/tui-slash-maturity.ts +1 -0
  333. package/src/mss/logging/log-store.ts +3 -0
  334. package/src/nexus/api/decisions-route.ts +11 -1
  335. package/src/nexus/api/http-server.ts +16 -5
  336. package/src/nexus/api/loop-edges.ts +352 -23
  337. package/src/nexus/api/ops-api.ts +96 -28
  338. package/src/nexus/api/ops-seats-sources.ts +2 -2
  339. package/src/nexus/api/workflow-templates.ts +3 -0
  340. package/src/nexus/index.ts +7 -0
  341. package/src/oauth/codex.ts +38 -0
  342. package/src/onboarding/first-chat-gate.ts +29 -0
  343. package/src/release-loop/checklist.ts +106 -6
  344. package/src/release-loop/feature-store.ts +64 -16
  345. package/src/release-loop/landing-freeze.ts +6 -4
  346. package/src/release-loop/light-rc.ts +173 -0
  347. package/src/release-loop/placement.ts +64 -20
  348. package/src/release-loop/rubric.ts +136 -0
  349. package/src/seat-dispatch/seat-ask.ts +79 -16
  350. package/src/seat-loop/seat-loop.ts +12 -6
  351. package/src/self-dev/ask-launch-flow.ts +24 -3
  352. package/src/self-dev/ask-path-candidates.ts +68 -0
  353. package/src/self-dev/dev-cli.ts +36 -0
  354. package/src/self-dev/dev-pipeline.ts +4 -0
  355. package/src/self-dev/draft-triage-rules.ts +1 -1
  356. package/src/self-dev/orchestrate-cli.ts +22 -0
  357. package/src/self-dev/orchestrate.ts +1 -0
  358. package/src/self-dev/pr-preview.ts +179 -0
  359. package/src/self-dev/round-pr-supersede.ts +244 -0
  360. package/src/self-dev/run-supervisor.ts +14 -0
  361. package/src/self-dev/shard-run.ts +23 -7
  362. package/src/self-implement/follow-up-goals.ts +2 -2
  363. package/src/self-implement/gate-cli.ts +29 -8
  364. package/src/self-implement/gate-remote.ts +427 -0
  365. package/src/self-implement/goal-author-clarification.ts +88 -13
  366. package/src/self-implement/goal-author.ts +8 -3
  367. package/src/self-implement/goal-clarification-escalation.ts +2 -0
  368. package/src/self-implement/l8-merge-queue.ts +29 -5
  369. package/src/self-implement/orchestrator.ts +61 -13
  370. package/src/self-implement/prior-draft-findings.ts +4 -3
  371. package/src/self-implement/salvage-branches.ts +254 -0
  372. package/src/skills/skill-feedback.ts +76 -0
  373. package/src/task-agent/actions.ts +101 -7
  374. package/src/task-agent/board.ts +132 -0
  375. package/src/task-agent/card-evidence.ts +203 -0
  376. package/src/task-agent/completion-evidence.ts +126 -0
  377. package/src/task-agent/judge.ts +49 -5
  378. package/src/task-agent/live-moves.ts +176 -0
  379. package/src/task-agent/mission.ts +449 -0
  380. package/src/task-agent/shadow.ts +258 -0
  381. package/src/task-agent/task-hand.ts +337 -0
  382. package/src/task-orchestrator/surfaces/pod-command-job.ts +64 -4
  383. package/src/task-orchestrator/surfaces/pod-failure-reason.ts +10 -0
  384. package/src/task-orchestrator/surfaces/pod-lease.ts +142 -13
  385. package/src/task-orchestrator/surfaces/pod-pool.ts +13 -5
  386. package/src/task-orchestrator/surfaces/self-implement-pod.ts +88 -20
  387. package/src/telegram-away-command.ts +38 -0
  388. package/src/telegram-commands.ts +11 -0
  389. package/src/telegram-loops-command.ts +1 -1
  390. package/src/telegram-report-log-probe.ts +21 -0
  391. package/src/telegram-report.ts +27 -3
  392. package/src/telegram.ts +5 -20
  393. package/src/tui-client/dashboard-session.ts +1 -0
  394. package/src/user-config.ts +196 -4
  395. package/src/version/packed-revision.json +1 -1
  396. package/apps/pwa/out/_next/static/chunks/1307-aac7332576c4d6fc.js +0 -1
  397. package/apps/pwa/out/_next/static/chunks/1826-5d279da64bbbc2de.js +0 -1
  398. package/apps/pwa/out/_next/static/chunks/1844-9ec4b1d12f6e80eb.js +0 -1
  399. package/apps/pwa/out/_next/static/chunks/1866-e7a6999f25548b30.js +0 -1
  400. package/apps/pwa/out/_next/static/chunks/1948.15f356b15a625597.js +0 -5
  401. package/apps/pwa/out/_next/static/chunks/2196-4700653d12a44b40.js +0 -1
  402. package/apps/pwa/out/_next/static/chunks/224-798346143007ff65.js +0 -1
  403. package/apps/pwa/out/_next/static/chunks/2699-63b73fa4d3bcf6ff.js +0 -1
  404. package/apps/pwa/out/_next/static/chunks/2957-96ffab3d521fb08f.js +0 -1
  405. package/apps/pwa/out/_next/static/chunks/311.e685738f92e6e2ec.js +0 -1
  406. package/apps/pwa/out/_next/static/chunks/3194-807b212fb8eae065.js +0 -1
  407. package/apps/pwa/out/_next/static/chunks/3324-d1d2979821dfa845.js +0 -1
  408. package/apps/pwa/out/_next/static/chunks/3558-bbb5ab553a0471d3.js +0 -1
  409. package/apps/pwa/out/_next/static/chunks/3916-3d02dd1fc257dd3c.js +0 -1
  410. package/apps/pwa/out/_next/static/chunks/4144-a0041c8dc3be57ac.js +0 -1
  411. package/apps/pwa/out/_next/static/chunks/4554-3565eea1f1495ba7.js +0 -1
  412. package/apps/pwa/out/_next/static/chunks/4614-21403aac730d4586.js +0 -1
  413. package/apps/pwa/out/_next/static/chunks/4795-f372490b178583fc.js +0 -1
  414. package/apps/pwa/out/_next/static/chunks/5135-44008fe8b1f2aa41.js +0 -1
  415. package/apps/pwa/out/_next/static/chunks/5233-bf28bd6bd7a32099.js +0 -1
  416. package/apps/pwa/out/_next/static/chunks/5360-be1f1d8f9fcc9755.js +0 -1
  417. package/apps/pwa/out/_next/static/chunks/5977-c952712c87a1f757.js +0 -1
  418. package/apps/pwa/out/_next/static/chunks/5994-ae92c0de046ab9ba.js +0 -1
  419. package/apps/pwa/out/_next/static/chunks/6158.9c0449a9a8d0064b.js +0 -2
  420. package/apps/pwa/out/_next/static/chunks/6877-fe94e76a5841ee9b.js +0 -1
  421. package/apps/pwa/out/_next/static/chunks/7290-f09c928a06c52e2d.js +0 -1
  422. package/apps/pwa/out/_next/static/chunks/7395-164e238f471b5d7b.js +0 -1
  423. package/apps/pwa/out/_next/static/chunks/7787-02a1c0b04e25e811.js +0 -1
  424. package/apps/pwa/out/_next/static/chunks/8060-cc008cc6fedfa818.js +0 -1
  425. package/apps/pwa/out/_next/static/chunks/8085-d065d0393c1760f5.js +0 -1
  426. package/apps/pwa/out/_next/static/chunks/8401-4ce1c42c8d89903f.js +0 -1
  427. package/apps/pwa/out/_next/static/chunks/8682-7061dd4fa7bd62ed.js +0 -1
  428. package/apps/pwa/out/_next/static/chunks/8868-10a3a4440fb0ccd8.js +0 -25
  429. package/apps/pwa/out/_next/static/chunks/8949-ecb4128a0858b911.js +0 -1
  430. package/apps/pwa/out/_next/static/chunks/899-f41be85e8a680651.js +0 -1
  431. package/apps/pwa/out/_next/static/chunks/9070.067c106528ec1ae6.js +0 -1
  432. package/apps/pwa/out/_next/static/chunks/9103-60fbef33e66f0a46.js +0 -1
  433. package/apps/pwa/out/_next/static/chunks/a9f2b54d-7865aba0f4d2ee49.js +0 -1
  434. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-c1a96669158471ec.js +0 -1
  435. package/apps/pwa/out/_next/static/chunks/app/approvals/page-3143b07a817d1da9.js +0 -1
  436. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-17ba086ef82d339b.js +0 -1
  437. package/apps/pwa/out/_next/static/chunks/app/board/page-7eb661ff23af7da2.js +0 -1
  438. package/apps/pwa/out/_next/static/chunks/app/botlab/page-6b8444914da74ca6.js +0 -1
  439. package/apps/pwa/out/_next/static/chunks/app/bots/page-6e5c3a867557298b.js +0 -1
  440. package/apps/pwa/out/_next/static/chunks/app/ceo/page-7110f732f45bf944.js +0 -1
  441. package/apps/pwa/out/_next/static/chunks/app/chat/page-9a41d65355fc25b9.js +0 -1
  442. package/apps/pwa/out/_next/static/chunks/app/consult/page-5e77cd1802567c89.js +0 -1
  443. package/apps/pwa/out/_next/static/chunks/app/control/page-41afec0b255ed2bb.js +0 -1
  444. package/apps/pwa/out/_next/static/chunks/app/design-check/page-4187666132fc98c4.js +0 -1
  445. package/apps/pwa/out/_next/static/chunks/app/editor/page-09328d499162bae4.js +0 -1
  446. package/apps/pwa/out/_next/static/chunks/app/exec/page-29ff3a8f84f03b6a.js +0 -1
  447. package/apps/pwa/out/_next/static/chunks/app/field/page-80ec7401bc83f511.js +0 -1
  448. package/apps/pwa/out/_next/static/chunks/app/healthz/route-c1a96669158471ec.js +0 -1
  449. package/apps/pwa/out/_next/static/chunks/app/inside/page-ab93b240f687d69a.js +0 -1
  450. package/apps/pwa/out/_next/static/chunks/app/intake/page-fc7d86467291ae38.js +0 -1
  451. package/apps/pwa/out/_next/static/chunks/app/layout-de43a186e5ba5683.js +0 -1
  452. package/apps/pwa/out/_next/static/chunks/app/live/page-8b08cafad27759c9.js +0 -1
  453. package/apps/pwa/out/_next/static/chunks/app/loops/page-0e2d6985fb42cd2f.js +0 -1
  454. package/apps/pwa/out/_next/static/chunks/app/market/page-13e4108999f58d13.js +0 -1
  455. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-e8296ee284aa1293.js +0 -1
  456. package/apps/pwa/out/_next/static/chunks/app/morning/page-043a9111e74f6b24.js +0 -1
  457. package/apps/pwa/out/_next/static/chunks/app/not-found-23a4601ceb7f452e.js +0 -1
  458. package/apps/pwa/out/_next/static/chunks/app/observatory/page-1ed1324b1dc36754.js +0 -1
  459. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-10d958605a77b3bd.js +0 -1
  460. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-166745e51ac0c7b0.js +0 -1
  461. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-a04260b8ccc968d4.js +0 -1
  462. package/apps/pwa/out/_next/static/chunks/app/outputs/page-7a1d85e121065cc8.js +0 -1
  463. package/apps/pwa/out/_next/static/chunks/app/page-1f50e3d4e13435f4.js +0 -1
  464. package/apps/pwa/out/_next/static/chunks/app/reflection/page-12916918988ac97e.js +0 -1
  465. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-d8a3347a9d0c3f4a.js +0 -1
  466. package/apps/pwa/out/_next/static/chunks/app/sessions/page-a9117d2cd3093d36.js +0 -1
  467. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-5a64cb5208c6a8fd.js +0 -1
  468. package/apps/pwa/out/_next/static/chunks/app/settings/page-1407ec83ea64c1f9.js +0 -1
  469. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-1d824510df025d18.js +0 -1
  470. package/apps/pwa/out/_next/static/chunks/app/setup/layout-c1a96669158471ec.js +0 -1
  471. package/apps/pwa/out/_next/static/chunks/app/setup/page-6e878ebbe1737d00.js +0 -1
  472. package/apps/pwa/out/_next/static/chunks/app/share/page-7f49fe645d5132f7.js +0 -1
  473. package/apps/pwa/out/_next/static/chunks/app/showroom/page-81a4e7f8dbfe74cb.js +0 -1
  474. package/apps/pwa/out/_next/static/chunks/app/tasks/page-27d71f131acd9320.js +0 -1
  475. package/apps/pwa/out/_next/static/chunks/app/term/page-3ab59d19913b200e.js +0 -1
  476. package/apps/pwa/out/_next/static/chunks/app/today/page-bf85b2f5bf077925.js +0 -1
  477. package/apps/pwa/out/_next/static/chunks/app/trace/page-424659bacfb8cdd7.js +0 -1
  478. package/apps/pwa/out/_next/static/chunks/app/vault/page-ab32a8f522a60371.js +0 -1
  479. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-fc2df261f8a2c7df.js +0 -1
  480. package/apps/pwa/out/_next/static/chunks/app/workflows/page-09461366af82ef13.js +0 -1
  481. package/apps/pwa/out/_next/static/chunks/app/workspace/page-5d4fef9ccd7d406b.js +0 -1
  482. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-ea90a8ac73ce2749.js +0 -1
  483. package/apps/pwa/out/_next/static/chunks/framework-5c169757551134ed.js +0 -1
  484. package/apps/pwa/out/_next/static/chunks/main-59fe95fed40c7a01.js +0 -1
  485. package/apps/pwa/out/_next/static/chunks/main-app-4ca74dbbee7057c6.js +0 -1
  486. package/apps/pwa/out/_next/static/chunks/pages/_app-13a0a886df4d97a1.js +0 -1
  487. package/apps/pwa/out/_next/static/chunks/pages/_error-6af8591cd3a3f038.js +0 -1
  488. package/apps/pwa/out/_next/static/chunks/webpack-539cde99554be20f.js +0 -1
  489. package/apps/pwa/out/_next/static/css/001d5fbaee388a25.css +0 -1
  490. package/apps/pwa/out/_next/static/yRA5ur6DcPl0YHSRheH2O/_buildManifest.js +0 -1
  491. /package/apps/pwa/out/_next/static/{yRA5ur6DcPl0YHSRheH2O → J5H7BvqgSI8DUjtRRcA_S}/_ssgManifest.js +0 -0
@@ -1,34 +1,48 @@
1
1
  import { withFileLockSync } from '../../storage/file-lock.js';
2
- import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
2
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
3
3
  import { dirname, join } from 'node:path';
4
4
  import { CardStore, taskCardsDir, type TaskCard } from '../../task-cards/card-store.js';
5
5
  import { Database } from 'bun:sqlite';
6
6
  import { type ChecklistItem } from '../../release-loop/checklist.js';
7
7
  import { effectiveInstanceRoot } from '../../instance/resolve.js';
8
- import { getUserConfig } from '../../user-config.js';
8
+ import { getUserConfig, type OrchestratorLoopConfig } from '../../user-config.js';
9
+ import { rubricPriority } from '../../release-loop/rubric.js';
9
10
  import { debug } from '../../debug/log.js';
10
11
  import { listSelfDevRuns, runSummaryLine, selfDevRunsDir } from '../../self-dev/run-store.js';
11
- import { planAction, runSeatLoopOnce, type SeatItem } from '../../seat-loop/seat-loop.js';
12
- import { harnessQueueOutcome } from '../../harness/harness-queue.js';
12
+ import { planAction, runSeatLoopOnce, type SeatDeps, type SeatItem } from '../../seat-loop/seat-loop.js';
13
+ import { addHarnessQueue, harnessQueueOutcome, setQueuedPoolHint } from '../../harness/harness-queue.js';
14
+ import { placeOnGrid, type GridHost } from './grid-place.js';
15
+ import { checkPodPool, parsePodPool, resolvePodPoolSpec } from '../../task-orchestrator/surfaces/pod-pool.js';
16
+ import { leaseKubectl } from '../../task-orchestrator/surfaces/pod-lease.js';
17
+ import { readTaskAgentState, TASK_SEATS, type HandTaskOptions, type TaskCard as TaskAgentCard, type HandTaskResult, type TaskLauncher, type TaskSeat } from '../../task-agent/task-hand.js';
13
18
 
14
19
  export type Window = '08' | '12' | '18';
15
20
  export type Node = 'intake' | 'split' | 'place' | 'delegate' | 'launch' | 'reconcile' | 'report';
16
21
  export type Mode = 'off' | 'shadow' | 'live';
17
22
  const SEAT_LOOP_IDS: Readonly<Record<string, string>> = { OP: 'op-seat', MK: 'cmo-seat', TC: 'tc-seat', UX: 'ux-seat' };
18
- export interface Cell { id: string; title: string; seat?: string; host?: string; version?: string; cardId: string; origin: 'candidate' | 'flow1a'; priority?: 'P0' | 'P1' | 'P2'; predecessors?: string[] }
23
+ export interface Cell { id: string; title: string; kind?: string; seat?: string; host?: string; gridHost?: boolean; version?: string; cardId: string; origin: 'candidate' | 'flow1a'; priority?: 'P0' | 'P1' | 'P2' | 'P3' | 'P4'; predecessors?: string[] }
19
24
  export interface TickState {
20
25
  runId: string; window: Window; mode: Mode; day: string;
21
26
  cards: TaskCard[]; skippedCards: Array<{ id: string; reason: string }>; cells: Cell[]; placed: number; unplaced?: number; delegated: number; wouldDelegate: number; rebalanced?: number; rebalanceBlocked?: Array<{ id: string; reason: string }>;
22
27
  reconciled: { reached: number; progressing: number; blocked: number; unknown: number };
23
28
  runSummaries?: string[];
29
+ degradation?: { level: 1 | 2; reason: string };
30
+ /** The highest degradation actually applied in this run — the report keeps it even if the signal recovers before report. */
31
+ degradationPeak?: { level: 1 | 2; reason: string };
32
+ deferredCells?: Cell[];
33
+ /** place ran at L2 (skipped) in this run — its unplaced cells are carried even if the board is back by delegate. */
34
+ placeDegraded?: boolean;
35
+ delegatedKeys?: string[];
24
36
  launched?: Array<{ seat: string; status: string; key?: string; queueId?: string; runId?: string }>;
37
+ /** ORCH-TA-HAND — cells handed to TASK-AGENT this run (card id · mode · launched). */
38
+ handed?: Array<{ key: string; cell: string; cardId: string; mode: 'shadow' | 'live'; launched: boolean }>;
25
39
  queueOutcomes?: Array<{ queueId: string; key?: string; outcome: string }>;
26
40
  steps: Array<{ node: Node; reason: string; count: number; targetLoopId?: string }>;
27
41
  nodes: Partial<Record<Node, 'ok' | 'skipped'>>;
28
42
  missing: string[];
29
43
  }
30
44
  /** FLOW1a contract: with `{ shadow: true }` the splitter only proposes cells and writes nothing (cards, ledgers, LLM side effects excepted). */
31
- export type SplitAdapter = (card: TaskCard, opts?: { shadow?: boolean }) => Promise<Array<{ id: string; title: string; seat?: string; host?: string }>> | Array<{ id: string; title: string; seat?: string; host?: string }>;
45
+ export type SplitAdapter = (card: TaskCard, opts?: { shadow?: boolean }) => Promise<Array<{ id: string; title: string; kind?: string; seat?: string; host?: string }>> | Array<{ id: string; title: string; kind?: string; seat?: string; host?: string }>;
32
46
  /** RELPLAN1 `placeCell(input: PlacementCell)` — owner·priority·predecessors are required; the result carries no seat. */
33
47
  export type PlacementInput = { id: string; title: string; owner: string; priority: 'P0' | 'P1' | 'P2'; predecessors: string[] };
34
48
  export type PlaceAdapter = (cell: PlacementInput, deps?: { dryRun?: boolean; now?: Date }) => Promise<{ version?: string } | null> | { version?: string } | null;
@@ -36,12 +50,51 @@ export type RebalanceAdapter = (version: string) => Promise<{ decisions: unknown
36
50
  export interface TickDeps {
37
51
  root?: string; now?: Date; mode?: Mode; runId?: string; window?: Window;
38
52
  cards?: () => TaskCard[]; split?: SplitAdapter; placeCell?: PlaceAdapter;
39
- seatTurn?: (seat: string, root: string) => Promise<{ status: string; queueId?: string; runId?: string; item?: { source: string; id: string } }>;
53
+ degradation?: () => { llm: boolean; board: boolean };
54
+ gridHosts?: readonly GridHost[];
55
+ seatTurn?: (seat: string, root: string, host?: string, options?: Pick<SeatDeps, 'enqueue'>) => Promise<{ status: string; queueId?: string; runId?: string; item?: { source: string; id: string } }>;
40
56
  queueOutcome?: (queueId: string, root: string) => 'pending' | 'unknown' | 'retryable' | 'succeeded';
57
+ /** 발사 전 대기열 항목의 풀 힌트 갱신(주입 안 하면 harness-queue `setQueuedPoolHint`). */
58
+ setPoolHint?: (queueId: string, poolHint: string | undefined, root: string) => Promise<boolean>;
41
59
  loadAdapter?: (path: string, name: string) => Promise<unknown>;
42
60
  checklist?: (version: string) => Pick<ChecklistItem, 'id' | 'status'>[];
43
61
  observe?: (event: string, data: { loopId: string; runId: string; window: Window; node: Node; reason: string; count: number; targetLoopId?: string }) => void;
44
62
  print?: (line: string) => void;
63
+ /** ORCH-TA-HAND seams — absent = config `loops.orchestrator.handToTaskAgent(Cell)` · real `handTask` · `defaultTaskLauncher`. */
64
+ handToTaskAgent?: 'off' | 'shadow' | 'live';
65
+ handToTaskAgentCell?: string;
66
+ handTask?: (opts: HandTaskOptions) => Promise<HandTaskResult>;
67
+ taskLauncher?: TaskLauncher;
68
+ }
69
+
70
+ /** ORCH-TA-HAND ledger — one row per handed `orch:<card>:<cell>` key so a cell is handed once across ticks and processes. */
71
+ function handedLedger(root: string): string { return join(root, 'loop', 'orchestrator', 'handed.jsonl'); }
72
+ type HandedRow = { key: string; cell: string; cardId: string; mode: 'shadow' | 'live'; launched: boolean; runId?: string };
73
+ /** Completed hands (`status: 'handed'`) by ledger key — a re-entered run restores its `handed[]` from these. */
74
+ function handedRows(path: string): Map<string, HandedRow> {
75
+ const rows = new Map<string, HandedRow>();
76
+ if (!existsSync(path)) return rows;
77
+ for (const line of readFileSync(path, 'utf8').split('\n')) {
78
+ if (!line.trim()) continue;
79
+ let row: Partial<HandedRow> & { status?: unknown };
80
+ try { row = JSON.parse(line) as typeof row; } catch { throw new Error('invalid orchestrator handed ledger'); }
81
+ if (row.status === 'handed' && typeof row.key === 'string' && typeof row.cardId === 'string' && typeof row.cell === 'string'
82
+ && (row.mode === 'shadow' || row.mode === 'live')) rows.set(row.key, { key: row.key, cell: row.cell, cardId: row.cardId, mode: row.mode, launched: row.launched === true, ...(typeof row.runId === 'string' ? { runId: row.runId } : {}) });
83
+ }
84
+ return rows;
85
+ }
86
+ function handedKeys(path: string): Set<string> {
87
+ if (!existsSync(path)) return new Set();
88
+ const keys = new Set<string>();
89
+ for (const line of readFileSync(path, 'utf8').split('\n')) {
90
+ if (!line.trim()) continue;
91
+ let row: { key?: unknown; status?: unknown };
92
+ try { row = JSON.parse(line) as typeof row; } catch { throw new Error('invalid orchestrator handed ledger'); }
93
+ if (typeof row.key !== 'string') continue;
94
+ // A released claim (a shadow hand that failed before its card) frees the key again — the last row decides.
95
+ if (row.status === 'released') keys.delete(row.key); else keys.add(row.key);
96
+ }
97
+ return keys;
45
98
  }
46
99
 
47
100
  export function kstWindow(now: Date): Window {
@@ -88,7 +141,9 @@ export function reportLine(state: TickState): string {
88
141
  const r = state.reconciled;
89
142
  const line = `orchestrator ${state.window} cards=${state.cards.length} cells=${state.cells.length} placed=${state.missing.includes('relplan1-absent') ? 'missing' : state.placed} delegated=${state.mode === 'shadow' ? `would ${state.wouldDelegate}` : state.delegated} reconciled=${r.reached}/${r.progressing}/${r.blocked}/${r.unknown} mode=${state.mode}`;
90
143
  const details = [...(state.rebalanceBlocked ?? []).map(row => `⛔ 이월 거부 ${row.id}: ${row.reason}`), ...(state.runSummaries ?? [])];
91
- return details.length ? `${line}\n${details.join('\n')}` : line;
144
+ const body = details.length ? `${line}\n${details.join('\n')}` : line;
145
+ const applied = state.degradationPeak ?? state.degradation;
146
+ return applied ? `강등 L${applied.level}: ${applied.reason}\n${body}` : body;
92
147
  }
93
148
  function readCardSnapshot(root: string): { cards: TaskCard[]; skippedCards: TickState['skippedCards'] } {
94
149
  const dir = taskCardsDir(root);
@@ -122,10 +177,138 @@ function readChecklist(root: string, version: string): Pick<ChecklistItem, 'id'
122
177
  try { return db.query('SELECT feature_id AS id, status FROM assignments WHERE version = ?').all(version) as Pick<ChecklistItem, 'id' | 'status'>[]; }
123
178
  finally { db.close(); }
124
179
  }
180
+ /** 명시 인벤토리: 없으면 undefined, «있는데 못 읽으면» null — null 은 풀 명부로 대체하지 않는다. */
181
+ function readGridHosts(): readonly GridHost[] | null | undefined {
182
+ const supplied = process.env.ELANOUS_GRID_HOSTS;
183
+ if (supplied === undefined) return undefined;
184
+ try {
185
+ const inventory: unknown = JSON.parse(supplied);
186
+ if (!Array.isArray(inventory) || !inventory.every(row => row && typeof row === 'object' &&
187
+ typeof row.name === 'string' && row.name.trim() !== '' && Array.isArray(row.capabilities) &&
188
+ row.capabilities.every((capability: unknown) => typeof capability === 'string') &&
189
+ typeof row.available === 'boolean')) return null;
190
+ return inventory.map(row => ({ name: row.name as string, capabilities: new Set<string>(row.capabilities), available: row.available as boolean }));
191
+ } catch { return null; }
192
+ }
193
+ /** 주입 > 명시 인벤토리 > 풀 명부. 명시 인벤토리를 못 읽으면 제안하지 않는다(기존 경로 그대로). */
194
+ function resolveGridHosts(deps: TickDeps, observe: (event: string, reason: string, count: number) => void): readonly GridHost[] | undefined {
195
+ if (deps.gridHosts) return deps.gridHosts;
196
+ const supplied = readGridHosts();
197
+ if (supplied === null) { observe('node-missing', 'grid-inventory-unreadable', 0); return undefined; }
198
+ return supplied ?? gridHostsFromPodPool();
199
+ }
200
+ function preferredPoolSpec(host: string): string | undefined {
201
+ const config = getUserConfig();
202
+ const spec = resolvePodPoolSpec(undefined, process.env, () => config.harness?.podPool ?? config.pod?.pool);
203
+ if (!spec) return undefined;
204
+ try {
205
+ const members = parsePodPool(spec);
206
+ const member = members.find(candidate => (candidate.sshHost ?? candidate.context) === host);
207
+ if (!member) return undefined;
208
+ const parts = spec.split(',').map(part => part.trim());
209
+ const preferred = parts.find(part => part === member.context ||
210
+ ['@', ':', '#'].some(separator => part.startsWith(`${member.context}${separator}`)));
211
+ return preferred ? [preferred, ...parts.filter(part => part !== preferred)].join(',') : undefined;
212
+ } catch { return undefined; }
213
+ }
214
+ function gridHostsFromPodPool(): readonly GridHost[] | undefined {
215
+ try {
216
+ const config = getUserConfig();
217
+ const spec = resolvePodPoolSpec(undefined, process.env, () => config.harness?.podPool ?? config.pod?.pool);
218
+ if (!spec) return undefined;
219
+ const members = parsePodPool(spec);
220
+ const checked = checkPodPool(members, leaseKubectl);
221
+ const ready = new Set(checked.ready.map(member => member.context));
222
+ return members.map(member => ({
223
+ name: member.sshHost ?? member.context,
224
+ capabilities: new Set(['pod']),
225
+ available: ready.has(member.context),
226
+ }));
227
+ } catch { return undefined; }
228
+ }
125
229
  function journal(root: string): string { return join(root, 'seat-requests', 'requests.jsonl'); }
126
- function readRequests(root: string): Array<{ key: string; status: string }> {
230
+ function readRequests(root: string): Array<{ key: string; status: string; seat?: string; kind?: string; host?: string; gridHost?: boolean }> {
127
231
  const path = journal(root);
128
- return existsSync(path) ? readFileSync(path, 'utf8').split('\n').filter(Boolean).map(line => JSON.parse(line) as { key: string; status: string }) : [];
232
+ return existsSync(path) ? readFileSync(path, 'utf8').split('\n').filter(Boolean).map(line => JSON.parse(line) as { key: string; status: string; seat?: string; kind?: string; host?: string; gridHost?: boolean }) : [];
233
+ }
234
+ function refreshGridHosts(cells: Cell[], hosts: readonly GridHost[] | undefined, mode: Mode,
235
+ root: string, observe: (event: string, reason: string, count: number, targetLoopId?: string) => void): void {
236
+ if (hosts === undefined) return;
237
+ for (const cell of cells) {
238
+ if (mode !== 'live' || cell.origin !== 'flow1a' || !cell.seat || !SEAT_LOOP_IDS[cell.seat]) {
239
+ observe('exchange', `would-host:${cell.id}:${placeOnGrid(cell, hosts).host ?? 'null'}`, 1);
240
+ continue;
241
+ }
242
+ const path = journal(root);
243
+ const key = `orch:${cell.cardId}:${cell.id}`;
244
+ // 의뢰가 이미 대기열을 떠났으면(발사·완료) 제안만 관측하고 칸의 호스트는 그 행의 확정 호스트로 되돌린다.
245
+ const keepLaunched = (row: { host?: string; gridHost?: boolean }): void => {
246
+ if (row.host) cell.host = row.host; else delete cell.host;
247
+ if (row.gridHost) cell.gridHost = true; else delete cell.gridHost;
248
+ };
249
+ let changedFrom: string | undefined;
250
+ mkdirSync(dirname(path), { recursive: true });
251
+ // 상태 확인 · 칸 변경 · 행 갱신은 모두 같은 잠금 안의 한 결정이다 — 다른 틱이 그 사이 행을 만들거나 바꿔도 덮지 않는다.
252
+ withFileLockSync(`${path}.lock`, () => {
253
+ const current = readRequests(root).filter(row => row.key === key).at(-1);
254
+ // 호스트 결정도 잠금 안 — 상태 확인과 같은 순간의 명부로 고른다.
255
+ const choice = placeOnGrid(cell, hosts);
256
+ observe('exchange', `would-host:${cell.id}:${choice.host ?? 'null'}`, 1);
257
+ if (current && current.status !== 'queued') { keepLaunched(current); return; }
258
+ if (choice.host) { cell.host = choice.host; cell.gridHost = true; }
259
+ else if (cell.gridHost || current?.gridHost) { delete cell.host; delete cell.gridHost; }
260
+ if (!current || (current.host === cell.host && current.gridHost === cell.gridHost && (current.kind === cell.kind || !cell.kind))) return;
261
+ if (current.host !== cell.host) changedFrom = current.host;
262
+ const updated = { ...current };
263
+ delete updated.host;
264
+ delete updated.gridHost;
265
+ if (cell.host) updated.host = cell.host;
266
+ if (cell.gridHost) updated.gridHost = true;
267
+ if (cell.kind) updated.kind = cell.kind;
268
+ appendFileSync(path, `${JSON.stringify(updated)}\n`);
269
+ });
270
+ if (changedFrom !== undefined) observe('exchange', `rehost:${cell.id}:${changedFrom}→${cell.host ?? 'none'}`, 1, SEAT_LOOP_IDS[cell.seat]);
271
+ }
272
+ }
273
+ /**
274
+ * 재배치된 의뢰가 앞 틱에서 이미 대기열에 들어갔다면(아직 발사 전) 그 항목의 풀 힌트도 의뢰 행의 새 호스트로 맞춘다.
275
+ * 의뢰 키 ↔ 대기열 id 는 live 틱 상태들의 `launched` 에서 찾는다(날짜를 넘겨 대기 중인 항목도). 의뢰 행이 아직 queued 일 때만 옮긴다.
276
+ */
277
+ async function syncQueuedPoolHints(root: string, current: NonNullable<TickState['launched']>, deps: TickDeps,
278
+ observe: (event: string, reason: string, count: number) => void): Promise<void> {
279
+ const dir = join(root, 'loop', 'orchestrator');
280
+ const turns = [...current];
281
+ if (existsSync(dir)) for (const name of readdirSync(dir).filter(file => /^[a-zA-Z0-9._-]+\.json$/.test(file))) {
282
+ try {
283
+ const other = JSON.parse(readFileSync(join(dir, name), 'utf8')) as TickState;
284
+ if (other.mode === 'live') turns.push(...(other.launched ?? []));
285
+ } catch { /* 깨진 상태 파일은 건너뛴다 */ }
286
+ }
287
+ const latest = new Map(readRequests(root).map(row => [row.key, row]));
288
+ const seen = new Set<string>();
289
+ for (const turn of turns) {
290
+ if (!turn.queueId || !turn.key || seen.has(turn.queueId)) continue;
291
+ seen.add(turn.queueId);
292
+ const row = latest.get(turn.key);
293
+ if (!row || row.kind !== 'pod' || row.status !== 'queued') continue;
294
+ // GRID 가 고른 행이면 그 호스트 쪽 힌트로, GRID 가 호스트를 거둔 행(둘 다 막힘)이면 GRID 가 넣었던 힌트만 지운다.
295
+ // GRID 와 무관한 기존 pod 의뢰의 힌트(ORCH1·명시 풀)는 건드리지 않는다.
296
+ const gridChosen = !!(row.gridHost && row.host);
297
+ if (!gridChosen && row.host) continue;
298
+ const hint = gridChosen ? preferredPoolSpec(row.host!) : undefined;
299
+ if (gridChosen && !hint) continue;
300
+ const key = turn.key;
301
+ // 갱신 직전 대기열 잠금 안에서: 의뢰가 아직 queued 이고 행의 호스트가 계산 때와 같으며,
302
+ // 지우는 경우엔 지금 대기열 힌트가 GRID 가 만든 꼴일 때만.
303
+ const stillQueued = (item: { poolHint?: string; poolHintSource?: 'grid' }): boolean => {
304
+ const now = readRequests(root).filter(r => r.key === key).at(-1);
305
+ if (now?.status !== 'queued' || now.host !== row.host || now.gridHost !== row.gridHost) return false;
306
+ // 지우는 쪽은 GRID 가 소유한 힌트만(소유 기록 확인) — 같은 문자열이라도 사람이 넣은 힌트는 남긴다.
307
+ return gridChosen || item.poolHintSource === 'grid';
308
+ };
309
+ if (await (deps.setPoolHint ?? ((id: string, value: string | undefined, stateRoot: string) => setQueuedPoolHint(id, value, { root: stateRoot }, stillQueued)))(turn.queueId, hint, root))
310
+ observe('exchange', `rehost-queue:${turn.key}:${hint ?? 'none'}`, 1);
311
+ }
129
312
  }
130
313
  function stateFile(root: string, runId: string): string {
131
314
  if (!/^[a-zA-Z0-9._-]+$/.test(runId) || runId === '.' || runId === '..') throw new Error('Invalid orchestrator runId');
@@ -149,6 +332,22 @@ function restorePrevious(root: string, day: string, mode: Mode): Pick<TickState,
149
332
  }
150
333
  return { cards: [...cards.values()], cells: [...cells.values()], launched: [...launched.values()] };
151
334
  }
335
+ function previousDelegations(root: string, day: string, mode: Mode, runId: string): { deferred: Cell[]; keys: string[] } {
336
+ const dir = join(root, 'loop', 'orchestrator');
337
+ if (!existsSync(dir)) return { deferred: [], keys: [] };
338
+ const states = readdirSync(dir).filter(name => /^[a-zA-Z0-9._-]+\.json$/.test(name))
339
+ .flatMap(name => {
340
+ try {
341
+ const path = join(dir, name);
342
+ const state = JSON.parse(readFileSync(path, 'utf8')) as TickState;
343
+ return state.runId !== runId && state.day <= day && state.mode === mode && state.window === '08' &&
344
+ state.nodes?.delegate === 'ok' && Array.isArray(state.deferredCells)
345
+ ? [{ name, state, mtime: statSync(path).mtimeMs }] : [];
346
+ } catch { return []; }
347
+ });
348
+ states.sort((a, b) => a.state.day.localeCompare(b.state.day) || a.mtime - b.mtime || a.name.localeCompare(b.name));
349
+ return { deferred: states.at(-1)?.state.deferredCells ?? [], keys: states.flatMap(({ state }) => Array.isArray(state.delegatedKeys) ? state.delegatedKeys : []) };
350
+ }
152
351
  function initial(runId: string, window: Window, mode: Mode, day: string): TickState {
153
352
  return { runId, window, mode, day, cards: [], skippedCards: [], cells: [], placed: 0, delegated: 0, wouldDelegate: 0,
154
353
  reconciled: { reached: 0, progressing: 0, blocked: 0, unknown: 0 }, steps: [], nodes: {}, missing: [] };
@@ -177,11 +376,20 @@ export async function runOrchestratorNode(node: Node, deps: TickDeps = {}): Prom
177
376
  const root = deps.root ?? effectiveInstanceRoot();
178
377
  const runId = deps.runId ?? graphRunId() ?? `${kstDay(now)}-${window}`;
179
378
  const path = stateFile(root, runId);
180
- const configured = deps.mode ?? getUserConfig().loops?.orchestrator?.mode;
379
+ const orchestratorConfig = getUserConfig().loops?.orchestrator;
380
+ const configured = deps.mode ?? orchestratorConfig?.mode;
181
381
  const mode: Mode = configured === 'off' || configured === 'live' ? configured : 'shadow';
182
382
  const state: TickState = existsSync(path) ? JSON.parse(readFileSync(path, 'utf8')) as TickState : initial(runId, window, mode, kstDay(now));
183
383
  state.steps ??= [];
184
384
  if (state.window !== window || state.mode !== mode) throw new Error('orchestrator run window/mode changed');
385
+ if (deps.degradation) {
386
+ const signal = deps.degradation();
387
+ state.degradation = !signal.board ? { level: 2, reason: signal.llm ? 'board 불가' : 'LLM·board 불가' }
388
+ : !signal.llm ? { level: 1, reason: 'LLM 불가' } : undefined;
389
+ // Only nodes that act on the degradation (split · place · delegate · launch) count toward the reported peak.
390
+ const applies = (['split', 'place', 'delegate', 'launch'] as Node[]).includes(node) && activeNode(node, window);
391
+ if (applies && state.degradation && (!state.degradationPeak || state.degradation.level >= state.degradationPeak.level)) state.degradationPeak = state.degradation;
392
+ }
185
393
  const predecessor: Partial<Record<Node, Node>> = { split: 'intake', place: 'split', delegate: 'place', launch: 'delegate', reconcile: 'launch', report: 'reconcile' };
186
394
  const preceding = predecessor[node];
187
395
  if (preceding && !state.nodes[preceding]) throw new Error(`orchestrator ${node}: missing ${preceding} result`);
@@ -208,14 +416,17 @@ export async function runOrchestratorNode(node: Node, deps: TickDeps = {}): Prom
208
416
  observe('exchange', 'wish-cards', state.cards.length);
209
417
  } else if (node === 'split') {
210
418
  // Shadow also asks the splitter (it only proposes cells); only live records `orch:split` on the card.
211
- const split = deps.split ?? await adapter<SplitAdapter>('../../flow-loop/split.js', 'splitCard');
212
- if (!split) { state.missing.push('flow1a-absent'); observe('node-missing', 'flow1a-absent', state.cards.length); }
419
+ const ruleOnly = (state.degradation?.level ?? 0) >= 1;
420
+ const split = ruleOnly ? undefined : deps.split ?? await adapter<SplitAdapter>('../../flow-loop/split.js', 'splitCard');
421
+ if (!split && !ruleOnly) { state.missing.push('flow1a-absent'); observe('node-missing', 'flow1a-absent', state.cards.length); }
213
422
  state.cells = [];
214
423
  for (const card of state.cards) {
215
- const cells = split ? await split(card, { shadow: mode === 'shadow' }) : [{ id: card.id, title: card.title }];
424
+ const cells = ruleOnly ? [{ id: card.id, title: card.title }] : split ? await split(card, { shadow: mode === 'shadow' }) : [{ id: card.id, title: card.title }];
216
425
  for (const cell of cells) {
217
426
  if (!cell.id?.trim() || !cell.title?.trim()) throw new Error('invalid split cell');
218
- state.cells.push({ cardId: card.id, id: cell.id, title: cell.title, origin: split ? 'flow1a' : 'candidate', ...(cell.seat ? { seat: cell.seat } : {}), ...(cell.host !== undefined ? { host: cell.host } : {}) });
427
+ state.cells.push({ cardId: card.id, id: cell.id, title: cell.title, origin: split ? 'flow1a' : 'candidate',
428
+ ...(ruleOnly ? { priority: rubricPriority(card.sections.map(section => section.content).join('\n')) } : {}),
429
+ ...(cell.kind !== undefined ? { kind: cell.kind } : {}), ...(cell.seat ? { seat: cell.seat } : {}), ...(cell.host !== undefined ? { host: cell.host } : {}) });
219
430
  }
220
431
  observe('exchange', `split:${card.id}`, cells.length);
221
432
  if (split && cells.length && mode === 'live') {
@@ -226,63 +437,213 @@ export async function runOrchestratorNode(node: Node, deps: TickDeps = {}): Prom
226
437
  }
227
438
  }
228
439
  } else if (node === 'place') {
229
- const place = deps.placeCell ?? await adapter<PlaceAdapter>('../../release-loop/placement.js', 'placeCell');
230
- const available = typeof place === 'function';
231
- if (!available) { state.missing.push('relplan1-absent'); observe('node-missing', 'relplan1-absent', state.cells.length); }
232
- if (mode === 'shadow' && deps.placeCell) observe('exchange', 'relplan1-shadow-not-invoked', state.cells.length);
233
- // 담당 없는 칸은 배치를 부르든 안 부르든 센다 — 배치 호출 여부와 집계를 묶지 않는다.
234
- for (const cell of state.cells) {
235
- if (cell.origin === 'flow1a' && (!cell.seat || !SEAT_LOOP_IDS[cell.seat])) { state.unplaced = (state.unplaced ?? 0) + 1; observe('exchange', 'unplaced-no-owner', 1); }
440
+ // Carry unfinished assignments into the same place/delegate path before applying this tick's cap.
441
+ const carry = previousDelegations(root, state.day, mode, runId).deferred;
442
+ if (carry.length) {
443
+ // Carried cells keep their place ahead of this tick's new cells (deduplicated) — a cell deferred by the cap is not
444
+ // pushed back again by a newly split cell.
445
+ const carried = new Set(carry.map(cell => `orch:${cell.cardId}:${cell.id}`));
446
+ state.cells = [...carry, ...state.cells.filter(cell => !carried.has(`orch:${cell.cardId}:${cell.id}`))];
236
447
  }
237
- if (available && (mode === 'live' || !deps.placeCell)) for (const cell of state.cells) {
238
- if (cell.origin !== 'flow1a' || !cell.seat || !SEAT_LOOP_IDS[cell.seat]) continue;
239
- // RELPLAN's dryRun calculates the same release without writing a checklist in shadow.
240
- try {
241
- const placed = await place({ id: cell.id, title: cell.title, owner: cell.seat, priority: cell.priority ?? 'P2', predecessors: cell.predecessors ?? [] }, { dryRun: mode === 'shadow', now });
242
- if (validVersion(placed?.version)) {
243
- cell.version = placed.version;
244
- state.placed++;
245
- observe('exchange', mode === 'shadow' ? `would-place:${cell.id}:${cell.version}` : `placed:${cell.id}:${cell.version}`, 1);
246
- } else { state.unplaced = (state.unplaced ?? 0) + 1; observe('exchange', 'unplaced-no-release', 1); }
247
- } catch (error) {
248
- state.unplaced = (state.unplaced ?? 0) + 1;
249
- observe('exchange', `unplaced:${cell.id}:${String(error).slice(0, 160)}`, 1);
448
+ if (state.degradation?.level === 2) {
449
+ state.placeDegraded = true;
450
+ observe('degraded-skip', 'place', 0);
451
+ } else {
452
+ const gridHosts = resolveGridHosts(deps, observe);
453
+ refreshGridHosts(state.cells, gridHosts, mode, root, observe);
454
+ if (mode === 'live') await syncQueuedPoolHints(root, state.launched ?? [], deps, observe);
455
+ const place = deps.placeCell ?? await adapter<PlaceAdapter>('../../release-loop/placement.js', 'placeCell');
456
+ const available = typeof place === 'function';
457
+ if (!available) { state.missing.push('relplan1-absent'); observe('node-missing', 'relplan1-absent', state.cells.length); }
458
+ if (mode === 'shadow' && deps.placeCell) observe('exchange', 'relplan1-shadow-not-invoked', state.cells.length);
459
+ // 담당 없는 칸은 배치를 부르든 안 부르든 센다 — 배치 호출 여부와 집계를 묶지 않는다.
460
+ for (const cell of state.cells) {
461
+ if (cell.origin === 'flow1a' && (!cell.seat || !SEAT_LOOP_IDS[cell.seat])) { state.unplaced = (state.unplaced ?? 0) + 1; observe('exchange', 'unplaced-no-owner', 1); }
462
+ }
463
+ if (available && (mode === 'live' || !deps.placeCell)) for (const cell of state.cells) {
464
+ if (cell.origin !== 'flow1a' || !cell.seat || !SEAT_LOOP_IDS[cell.seat]) continue;
465
+ // RELPLAN's dryRun calculates the same release without writing a checklist in shadow.
466
+ try {
467
+ const priority = cell.priority === 'P3' || cell.priority === 'P4' ? 'P2' : cell.priority ?? 'P2';
468
+ const placed = await place({ id: cell.id, title: cell.title, owner: cell.seat, priority, predecessors: cell.predecessors ?? [] }, { dryRun: mode === 'shadow', now });
469
+ if (validVersion(placed?.version)) {
470
+ cell.version = placed.version;
471
+ state.placed++;
472
+ observe('exchange', mode === 'shadow' ? `would-place:${cell.id}:${cell.version}` : `placed:${cell.id}:${cell.version}`, 1);
473
+ } else { state.unplaced = (state.unplaced ?? 0) + 1; observe('exchange', 'unplaced-no-release', 1); }
474
+ } catch (error) {
475
+ state.unplaced = (state.unplaced ?? 0) + 1;
476
+ observe('exchange', `unplaced:${cell.id}:${String(error).slice(0, 160)}`, 1);
477
+ }
250
478
  }
251
479
  }
252
480
  } else if (node === 'delegate') {
481
+ if (state.degradation?.level === 2) observe('degraded-skip', 'delegate', 0);
482
+ const cap = orchestratorConfig?.maxDelegatePerTick;
483
+ const previous = previousDelegations(root, state.day, mode, runId);
484
+ if (cap !== undefined || state.degradation?.level === 2 || state.placeDegraded || previous.deferred.length) {
485
+ state.deferredCells = [];
486
+ state.delegatedKeys = [];
487
+ }
253
488
  const path = journal(root);
254
- const keys = new Set(readRequests(root).map(row => row.key));
489
+ // 모드와 칸 필터는 따로 푼다 — 주입으로 모드만 줘도 설정의 칸 필터는 그대로 먹는다.
490
+ const handConfig: Partial<OrchestratorLoopConfig> = deps.handToTaskAgent !== undefined && deps.handToTaskAgentCell !== undefined ? {} : getUserConfig().loops?.orchestrator ?? {};
491
+ // live 로 넘긴 칸은 지금 설정(off·칸 필터)과 무관하게 자리 경로로 다시 안 간다 — 발사 주체가 둘이 되지 않게.
492
+ // ⭐ 넘김 claim 과 자리 저널 기록은 «같은 잠금»(자리 저널 잠금) 아래에서 서로를 다시 읽는다 — 동시 틱이 낡은 값을 보지 않게.
493
+ const ledger = handedLedger(root);
494
+ const liveHanded = handedKeys(ledger);
495
+ // Earlier runs' (would-)assignments only count while a cap is configured — they are what the cap carried over.
496
+ // With no cap the tick behaves as before: shadow would-delegates every cell again, live dedups on the seat journal.
497
+ const keys = new Set([...readRequests(root).map(row => row.key), ...(cap !== undefined ? previous.keys : [])]);
498
+ // 넘김 뒤 상태 저장 전에 죽은 같은 run 이 다시 돌면 원장에서 handed[] 를 되살린다.
499
+ const ledgerHanded = handedRows(ledger);
500
+ const restoreHanded = (handKey: string, key: string, rows: Map<string, HandedRow> = handedRows(ledger)): void => {
501
+ const row = rows.get(handKey);
502
+ // 이 run 이 넘긴 것만 되살린다 — 다른 run 의 넘김을 «이 run 에서 넘겼다»로 적지 않게.
503
+ if (!row || row.runId !== runId || (state.handed ?? []).some(entry => entry.key === key && entry.mode === row.mode)) return;
504
+ (state.handed ??= []).push({ key, cell: row.cell, cardId: row.cardId, mode: row.mode, launched: row.launched });
505
+ };
506
+ // A retried delegate (died after assigning, before saving state) rebuilds this run's cap usage from what it already
507
+ // wrote: seat rows tagged with this run and live hands claimed by this run.
508
+ if (cap !== undefined && state.delegatedKeys) {
509
+ const used = new Set(state.delegatedKeys);
510
+ if (existsSync(path)) for (const line of readFileSync(path, 'utf8').split('\n')) {
511
+ if (!line) continue;
512
+ let row: { key?: unknown; source?: unknown; orchRunId?: unknown };
513
+ try { row = JSON.parse(line) as typeof row; } catch { throw new Error('invalid seat request journal'); }
514
+ if (row.source === 'orchestrator' && row.orchRunId === runId && typeof row.key === 'string') used.add(row.key);
515
+ }
516
+ for (const row of handedRows(ledger).values()) if (row.runId === runId && row.mode === 'live') used.add(row.key);
517
+ if (existsSync(ledger)) for (const line of readFileSync(ledger, 'utf8').split('\n')) {
518
+ if (!line) continue;
519
+ try {
520
+ const claim = JSON.parse(line) as { key?: unknown; runId?: unknown; mode?: unknown; status?: unknown };
521
+ if (claim.runId === runId && claim.mode === 'live' && claim.status === 'claimed' && typeof claim.key === 'string') used.add(claim.key);
522
+ } catch { /* handedKeys already rejects a broken ledger */ }
523
+ }
524
+ state.delegatedKeys = [...used];
525
+ }
526
+ const journalHas = (key: string): boolean => {
527
+ if (!existsSync(path)) return false;
528
+ return readFileSync(path, 'utf8').split('\n').some(line => {
529
+ if (!line) return false;
530
+ try { return (JSON.parse(line) as { key?: string }).key === key; } catch { throw new Error('invalid seat request journal'); }
531
+ });
532
+ };
255
533
  for (const cell of state.cells) {
256
534
  if (cell.origin !== 'flow1a' || !cell.seat) continue;
257
535
  const targetLoopId = SEAT_LOOP_IDS[cell.seat];
258
536
  if (!targetLoopId) continue;
259
537
  const key = `orch:${cell.cardId}:${cell.id}`;
538
+ // 저널 키로 건너뛰기 «전에» 원장에서 복구한다 — shadow 넘김 ⊕ 자리 기록 뒤 상태 저장 전에 죽은 run 도 handed[] 를 되찾게.
539
+ restoreHanded(key, key, ledgerHanded);
540
+ restoreHanded(`shadow:${key}`, key, ledgerHanded);
260
541
  if (keys.has(key)) continue;
542
+ if (liveHanded.has(key)) { keys.add(key); observe('exchange', `handed-to-task-agent:${key}`, 1, 'task-agent'); continue; }
543
+ // COORD-HA ②: a live tick skips unplaced cells for assignment; L2 assigns nothing new (live hands included) and the
544
+ // per-tick cap defers the rest to the next tick — both before the TASK-AGENT hand so a hand never bypasses them.
545
+ const unplacedLive = mode === 'live' && !validVersion(cell.version);
546
+ // L2 carries every cell — placed or not — to the next tick: split already marked the card, so a cell skipped here
547
+ // would never be split again.
548
+ if (state.degradation?.level === 2 || (unplacedLive && state.placeDegraded)) {
549
+ state.deferredCells?.push(cell);
550
+ continue;
551
+ }
552
+ // The cap limits assignments (a live hand or a seat row); a shadow card is not an assignment and is still written.
553
+ const capped = !unplacedLive && cap !== undefined && state.delegatedKeys!.length >= cap;
554
+ // ORCH-TA-HAND: hand the picked cell to TASK-AGENT. shadow = card only (the seat path below is unchanged);
555
+ // live (tick live ⊕ hand live) = launch through TASK-AGENT and skip the seat journal so the cell launches once.
556
+ const handMode = deps.handToTaskAgent ?? handConfig.handToTaskAgent ?? 'off';
557
+ const handCell = deps.handToTaskAgentCell ?? handConfig.handToTaskAgentCell;
558
+ if (handMode !== 'off' && (!handCell || handCell === cell.id) && (TASK_SEATS as readonly string[]).includes(cell.seat)) {
559
+ // live 는 tick 도 live 이고 배치(유효한 판)가 된 칸만 — 나머지는 카드만(shadow · 배치 전 칸도 표본이 된다).
560
+ const handLive = handMode === 'live' && mode === 'live' && validVersion(cell.version) && !capped;
561
+ // shadow 표본은 따로 센다 — 자리 경로로 아직 안 간 칸(shadow 틱·배치 전)이 나중에 live 로 바뀌면 그때 뜬다.
562
+ // ⚠️ 이미 자리 저널에 들어간 칸은 live 로 바꿔도 다시 안 넘긴다(keys.has 에서 끝) — 한 칸의 발사 주체는 하나다(의도).
563
+ const handKey = handLive ? key : `shadow:${key}`;
564
+ mkdirSync(dirname(ledger), { recursive: true });
565
+ mkdirSync(dirname(path), { recursive: true });
566
+ // Claim under the seat-journal lock — the same lock the seat append takes, so a live claim and a seat row never both land.
567
+ const claimed = withFileLockSync(`${path}.lock`, () => {
568
+ if (handedKeys(ledger).has(handKey)) return false;
569
+ if (handLive && journalHas(key)) return false;
570
+ appendFileSync(ledger, `${JSON.stringify({ key: handKey, cell: cell.id, seat: cell.seat, mode: handLive ? 'live' : 'shadow', status: 'claimed', runId, at: now.toISOString() })}\n`);
571
+ return true;
572
+ });
573
+ if (claimed) {
574
+ try {
575
+ const result = await (deps.handTask ?? (async (opts: HandTaskOptions) => (await import('../../task-agent/task-hand.js')).handTask(opts)))({
576
+ text: cell.title, seat: cell.seat as TaskSeat, checklistId: cell.id, live: handLive, now: () => now,
577
+ statePath: join(root, 'task-agent-actions.json'),
578
+ ...(handLive ? { launcher: deps.taskLauncher ?? (async (args: string[], _cwd?: string, context?: Parameters<TaskLauncher>[2]) => (await import('../../cli/tasks-cli.js')).defaultTaskLauncher(args, undefined, context)) } : {}),
579
+ });
580
+ withFileLockSync(`${path}.lock`, () => appendFileSync(ledger, `${JSON.stringify({ key: handKey, cell: cell.id, cardId: result.card.id, mode: result.mode, launched: result.launched, status: 'handed', runId, at: now.toISOString() })}\n`));
581
+ (state.handed ??= []).push({ key, cell: cell.id, cardId: result.card.id, mode: result.mode, launched: result.launched });
582
+ observe('hand-to-task-agent', `${key}:${result.card.id}:${result.mode}${result.launched ? ':launched' : ''}`, 1, 'task-agent');
583
+ // A live hand is an assignment: it counts toward maxDelegatePerTick like a seat row (COORD-HA ②).
584
+ if (handLive) { keys.add(key); state.delegatedKeys?.push(key); continue; }
585
+ } catch (error) {
586
+ observe('hand-to-task-agent-failed', `${key}:${String(error).slice(0, 160)}`, 1, 'task-agent');
587
+ // A failed live hand stays claimed (no relaunch loop) and does not fall back to the seat path — one launcher per cell.
588
+ if (handLive) { keys.add(key); continue; }
589
+ // A failed shadow hand launched nothing — release the claim so a later tick can write the card,
590
+ // but only when no card for this cell was written (a failure after the card must not make a second one).
591
+ let cardWritten = true;
592
+ try {
593
+ const storePath = join(root, 'task-agent-actions.json');
594
+ if (!existsSync(storePath)) cardWritten = false;
595
+ else {
596
+ // 상태를 믿을 수 있을 때만 «카드 0장»으로 읽는다 — tasks 가 없거나 꼴이 틀리면 claim 을 남긴다.
597
+ const tasks = readTaskAgentState<{ tasks?: unknown }>(storePath).tasks;
598
+ if (tasks && typeof tasks === 'object' && !Array.isArray(tasks)
599
+ && Object.values(tasks).every(card => card && typeof card === 'object' && !Array.isArray(card))) {
600
+ cardWritten = Object.values(tasks as Record<string, TaskAgentCard>).some(card => card.checklistId === cell.id && card.text === cell.title);
601
+ }
602
+ }
603
+ } catch { cardWritten = true; /* unreadable store → keep the claim (never risk a duplicate) */ }
604
+ if (!cardWritten) withFileLockSync(`${path}.lock`, () => appendFileSync(ledger, `${JSON.stringify({ key: handKey, cell: cell.id, status: 'released', runId, reason: String(error).slice(0, 160), at: now.toISOString() })}\n`));
605
+ }
606
+ } else {
607
+ restoreHanded(handKey, key);
608
+ if (handLive) { keys.add(key); continue; }
609
+ }
610
+ }
611
+ if (capped) {
612
+ state.deferredCells!.push(cell);
613
+ observe('cap-deferred', key, 1, targetLoopId);
614
+ continue;
615
+ }
616
+ if (unplacedLive) continue;
261
617
  if (!validVersion(cell.version)) {
262
- if (mode === 'shadow') { keys.add(key); state.wouldDelegate++; observe('would-delegate', `${key} (unplaced)`, 1, targetLoopId); }
618
+ if (mode === 'shadow') { keys.add(key); state.wouldDelegate++; state.delegatedKeys?.push(key); observe('would-delegate', `${key} (unplaced)`, 1, targetLoopId); }
263
619
  continue;
264
620
  }
265
- if (mode === 'shadow') { keys.add(key); state.wouldDelegate++; observe('would-delegate', key, 1, targetLoopId); continue; }
621
+ if (mode === 'shadow') { keys.add(key); state.wouldDelegate++; state.delegatedKeys?.push(key); observe('would-delegate', key, 1, targetLoopId); continue; }
266
622
  keys.add(key);
267
623
  mkdirSync(dirname(path), { recursive: true });
268
624
  // TC review must-fix ①: the seat-requests journal contract needs receiptId, and appends are serialized under the
269
625
  // same lock as the traffic node — an invalid row stops request intake for every seat.
270
- const appended = withFileLockSync(`${path}.lock`, () => {
271
- const existing = existsSync(path) ? readFileSync(path, 'utf8') : '';
272
- if (existing.split('\n').some(line => {
273
- if (!line) return false;
274
- try { return (JSON.parse(line) as { key?: string }).key === key; } catch { throw new Error('invalid seat request journal'); }
275
- })) return false;
276
- appendFileSync(path, `${JSON.stringify({ key, receiptId: key, seat: cell.seat, ...(cell.host !== undefined ? { host: cell.host } : {}), loopId: targetLoopId, text: cell.title, status: 'queued', queuedAt: now.toISOString(), source: 'orchestrator', cell: cell.id, version: cell.version })}\n`);
626
+ const appended = withFileLockSync(`${path}.lock`, (): boolean | 'handed' => {
627
+ if (journalHas(key)) return false;
628
+ // 잠금 안에서 다시 본다 — 동시 틱이 이 칸을 방금 live 로 넘겼으면 자리 경로로 보내지 않는다.
629
+ if (handedKeys(ledger).has(key)) return 'handed';
630
+ appendFileSync(path, `${JSON.stringify({ key, receiptId: key, seat: cell.seat, ...(cell.kind !== undefined ? { kind: cell.kind } : {}), ...(cell.host !== undefined ? { host: cell.host } : {}), ...(cell.gridHost ? { gridHost: true } : {}), loopId: targetLoopId, text: cell.title, status: 'queued', queuedAt: now.toISOString(), source: 'orchestrator', cell: cell.id, version: cell.version, orchRunId: runId })}\n`);
277
631
  return true;
278
632
  });
633
+ if (appended === 'handed') { observe('exchange', `seat-suppressed-handed:${key}`, 1, 'task-agent'); continue; }
279
634
  if (!appended) continue;
280
635
  state.delegated++;
636
+ state.delegatedKeys?.push(key);
281
637
  observe('exchange', 'queued', 1, targetLoopId);
282
638
  }
283
639
  } else if (node === 'launch') {
284
- const eligible = state.cells.filter(cell => cell.origin === 'flow1a' && cell.seat && SEAT_LOOP_IDS[cell.seat]
285
- && validVersion(cell.version) && (mode === 'shadow' || readRequests(root).some(row => row.key === `orch:${cell.cardId}:${cell.id}` && row.status === 'queued')));
640
+ if (state.degradation?.level === 2) observe('degraded-skip', 'launch', 0);
641
+ // shadow 는 저널을 읽지 않는다 — 깨진 live 저널이 관측 전용 틱을 넘어뜨리지 않게.
642
+ const latestRequests = new Map(mode === 'live' && state.degradation?.level !== 2 ? readRequests(root).map(row => [row.key, row]) : []);
643
+ const eligible = state.cells.filter(cell => state.degradation?.level !== 2 && cell.origin === 'flow1a' && cell.seat && SEAT_LOOP_IDS[cell.seat]
644
+ && validVersion(cell.version) && (mode === 'shadow' || latestRequests.get(`orch:${cell.cardId}:${cell.id}`)?.status === 'queued')
645
+ && (orchestratorConfig?.maxDelegatePerTick === undefined || state.delegatedKeys?.includes(`orch:${cell.cardId}:${cell.id}`)
646
+ || (mode === 'live' && latestRequests.has(`orch:${cell.cardId}:${cell.id}`))));
286
647
  const seats = [...new Set(eligible.map(cell => cell.seat!))];
287
648
  state.launched = [];
288
649
  for (const seat of seats) {
@@ -293,9 +654,23 @@ export async function runOrchestratorNode(node: Node, deps: TickDeps = {}): Prom
293
654
  state.launched.push({ seat, key: item.id, status: action.kind === 'harness' ? 'would-launch' : `would-${action.kind}` });
294
655
  observe('exchange', `would-${action.kind}:${seat}`, 1, SEAT_LOOP_IDS[seat]);
295
656
  } else {
296
- const turn = await (deps.seatTurn ?? ((assigned: string, stateRoot: string) => runSeatLoopOnce(assigned, {
297
- root: stateRoot, config: { ...getUserConfig().loops?.seat, mode: 'live-safe', seats: [assigned] },
298
- })))(seat, root);
657
+ const selected = eligible.find(cell => cell.seat === seat && latestRequests.get(`orch:${cell.cardId}:${cell.id}`)?.status === 'queued');
658
+ const host = selected && latestRequests.get(`orch:${selected.cardId}:${selected.id}`)?.host;
659
+ const pools = new Map<string, string>([...latestRequests].flatMap(([key, row]) => {
660
+ if (row.status !== 'queued' || row.seat !== seat || row.kind !== 'pod' || !row.gridHost || !row.host || !key.startsWith('orch:')) return [];
661
+ const pool = preferredPoolSpec(row.host);
662
+ // 풀 힌트는 «설정된 풀»의 순서 바꾸기다 — 고른 호스트가 풀에 없으면 힌트를 지어내지 않고 그 사실을 남긴다.
663
+ if (!pool) observe('exchange', `grid-host-unmapped:${key}:${row.host}`, 1, SEAT_LOOP_IDS[seat]);
664
+ return pool ? [[key, pool] as const] : [];
665
+ }));
666
+ const enqueue: SeatDeps['enqueue'] | undefined = pools.size
667
+ ? (queuedSeat, text, queuedRoot, idempotencyKey, item) => addHarnessQueue({ seat: queuedSeat, say: text, idempotencyKey,
668
+ ...(item.source === 'request' && pools.has(item.id) ? { poolHint: pools.get(item.id), poolHintSource: 'grid' as const } : {}) }, { root: queuedRoot })
669
+ : undefined;
670
+ const options = enqueue ? { enqueue } : undefined;
671
+ const turn = await (deps.seatTurn ?? ((assigned: string, stateRoot: string, _preferred?: string, overrides?: Pick<SeatDeps, 'enqueue'>) =>
672
+ runSeatLoopOnce(assigned, { root: stateRoot,
673
+ config: { ...getUserConfig().loops?.seat, mode: 'live-safe', seats: [assigned] }, ...overrides })))(seat, root, host, options);
299
674
  // 자리 턴이 «이 틱이 넣은 의뢰»를 처리했을 때만 칸에 잇는다 — 대기열의 다른 의뢰는 이 카드의 발사가 아니다.
300
675
  const targets = new Set(eligible.filter(row => row.seat === seat).map(row => `orch:${row.cardId}:${row.id}`));
301
676
  const handled = 'item' in turn && turn.item?.source === 'request' ? turn.item.id : undefined;
@@ -318,6 +693,10 @@ export async function runOrchestratorNode(node: Node, deps: TickDeps = {}): Prom
318
693
  state.cells = merge(previous.cells, state.cells, cell => `${cell.cardId}:${cell.id}`);
319
694
  state.launched = merge(previous.launched ?? [], state.launched ?? [], turn => turn.queueId ?? `${turn.seat}:${turn.key ?? ''}`);
320
695
  }
696
+ if (mode === 'live') {
697
+ refreshGridHosts(state.cells, resolveGridHosts(deps, observe), mode, root, observe);
698
+ await syncQueuedPoolHints(root, state.launched ?? [], deps, observe);
699
+ }
321
700
  }
322
701
  state.queueOutcomes = (state.launched ?? []).filter(turn => turn.queueId).map(turn => {
323
702
  const outcome = (deps.queueOutcome ?? ((id: string, stateRoot: string) => harnessQueueOutcome(id, { root: stateRoot })))(turn.queueId!, root);
@@ -362,19 +741,36 @@ export async function runOrchestratorNode(node: Node, deps: TickDeps = {}): Prom
362
741
  return state;
363
742
  }
364
743
 
744
+ /** COORD-HA — the production supply of `TickDeps.degradation` (config `loops.orchestrator.degradationSignal`). Shadow (the
745
+ * default) probes and logs what the tick would have done; only live hands the signal to the tick. */
746
+ export async function degradationDeps(node: Node, deps: { config?: () => Partial<OrchestratorLoopConfig> | undefined; root?: string;
747
+ probe?: (root: string) => { llm: boolean; board: boolean; why: string[] } } = {}): Promise<Pick<TickDeps, 'degradation'>> {
748
+ const mode = (deps.config ?? (() => getUserConfig().loops?.orchestrator))()?.degradationSignal ?? 'shadow';
749
+ if (mode === 'off') return {};
750
+ const root = deps.root ?? effectiveInstanceRoot();
751
+ const signal = (deps.probe ?? (await import('./degradation.js')).probeDegradation)(root);
752
+ const level = !signal.board ? 2 : !signal.llm ? 1 : 0;
753
+ debug.log('loop.orchestrator', 'degradation-signal', { node, mode, llm: signal.llm, board: signal.board, level, why: signal.why });
754
+ return mode === 'live' ? { degradation: () => ({ llm: signal.llm, board: signal.board }) } : {};
755
+ }
756
+
365
757
  if (import.meta.main) {
366
758
  const node = process.argv[2];
367
759
  const index = process.argv.indexOf('--window');
368
- const window = index < 0 ? undefined : process.argv[index + 1];
760
+ // ORCH-LIVE-1008: `graph run` recipes pass no flags, so a manual/rehearsal tick picks its window through the
761
+ // environment (`ELANOUS_ORCH_WINDOW=08 elanous graph run graphs/orchestrator/orchestrator.yaml`). The flag wins.
762
+ const window = index < 0 ? process.env.ELANOUS_ORCH_WINDOW : process.argv[index + 1];
763
+ // A given flag (even without a value) or a set variable must name a real window — never fall back to «now».
764
+ const windowGiven = index >= 0 || process.env.ELANOUS_ORCH_WINDOW !== undefined;
369
765
  if (!['intake', 'split', 'place', 'delegate', 'launch', 'reconcile', 'report'].includes(node ?? '') ||
370
- (window !== undefined && !['08', '12', '18'].includes(window))) {
371
- console.error('orchestrator: expected <intake|split|place|delegate|launch|reconcile|report> [--window 08|12|18]');
766
+ (windowGiven && !['08', '12', '18'].includes(window ?? ''))) {
767
+ console.error('orchestrator: expected <intake|split|place|delegate|launch|reconcile|report> [--window 08|12|18] (or ELANOUS_ORCH_WINDOW=08|12|18)');
372
768
  process.exitCode = 2;
373
769
  } else {
374
770
  void (async () => {
375
771
  try { await (await import('../../domains/standalone-log-sink.js')).registerStandaloneLogSink('orchestrator'); }
376
772
  catch { /* local execution can still use the per-run file */ }
377
- await runOrchestratorNode(node as Node, { window: window as Window | undefined });
773
+ await runOrchestratorNode(node as Node, { window: window as Window | undefined, ...await degradationDeps(node as Node) });
378
774
  })().catch(error => { console.error(`orchestrator ${node}: ${String(error)}`); process.exitCode = 1; });
379
775
  }
380
776
  }