elanous 0.2.17 → 0.2.18

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 (309) 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/P6YTVgzuZz5oTZhY4Din_/_buildManifest.js +1 -0
  4. package/apps/pwa/out/_next/static/chunks/{1307-aac7332576c4d6fc.js → 1307-30dff22c9e1f3073.js} +1 -1
  5. package/apps/pwa/out/_next/static/chunks/195-fb1d7690bf30bab4.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/1986-33e13d648a9df765.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/2537-6c289376caa88ca1.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/2699-325cc923ba7b8631.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/3857-4f936cdb30322aa6.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/4554-5761612531d0146b.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/4565-3d7e3fdeb43686a8.js +1 -0
  12. package/apps/pwa/out/_next/static/chunks/5977-a11f37254196b659.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/{6158.9c0449a9a8d0064b.js → 6158.af2e93b7adff709b.js} +1 -1
  14. package/apps/pwa/out/_next/static/chunks/6877-74399f69c7da1433.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/7797-df5aa0381fdc078f.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/8401-8615a660c84cdb03.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/8695-846034ce44852c70.js +1 -0
  18. package/apps/pwa/out/_next/static/chunks/app/approvals/{page-3143b07a817d1da9.js → page-d92f7ee3994e84c7.js} +1 -1
  19. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-17ba086ef82d339b.js → page-4733c967b56a4277.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/board/{page-7eb661ff23af7da2.js → page-d22fd849cc22f104.js} +1 -1
  21. package/apps/pwa/out/_next/static/chunks/app/botlab/page-bb742f995c6769e7.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/app/bots/{page-6e5c3a867557298b.js → page-02b073a2ac793b83.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/app/ceo/page-60c3da708f9a0ccc.js +1 -0
  24. package/apps/pwa/out/_next/static/chunks/app/chat/page-8a9b3594bc23289b.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/app/consult/{page-5e77cd1802567c89.js → page-a0eb02638b1e6f31.js} +1 -1
  26. package/apps/pwa/out/_next/static/chunks/app/control/page-308111abadf35670.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/app/decisions/page-6eaf7bac37830c21.js +1 -0
  28. package/apps/pwa/out/_next/static/chunks/app/design-check/{page-4187666132fc98c4.js → page-5d97b2901e0d1ea6.js} +1 -1
  29. package/apps/pwa/out/_next/static/chunks/app/editor/{page-09328d499162bae4.js → page-c39554c321576982.js} +1 -1
  30. package/apps/pwa/out/_next/static/chunks/app/exec/{page-29ff3a8f84f03b6a.js → page-8c3d8e63ae2e1c98.js} +1 -1
  31. package/apps/pwa/out/_next/static/chunks/app/field/{page-80ec7401bc83f511.js → page-9b72a07d424701c6.js} +1 -1
  32. package/apps/pwa/out/_next/static/chunks/app/inside/page-bd5d586429167fea.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/app/intake/{page-fc7d86467291ae38.js → page-a572f58cd3ca5864.js} +1 -1
  34. package/apps/pwa/out/_next/static/chunks/app/layout-59b7834d497c2255.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/app/live/page-be5ea965a07fbc41.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/app/loops/{page-0e2d6985fb42cd2f.js → page-c130b63862437814.js} +1 -1
  37. package/apps/pwa/out/_next/static/chunks/app/market/{page-13e4108999f58d13.js → page-fa244bd146a00ae8.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-e8296ee284aa1293.js → page-44d56d6391965a6a.js} +1 -1
  39. package/apps/pwa/out/_next/static/chunks/app/morning/{page-043a9111e74f6b24.js → page-83a700d4c754d802.js} +1 -1
  40. package/apps/pwa/out/_next/static/chunks/app/{not-found-23a4601ceb7f452e.js → not-found-7b3170f374f77b9d.js} +1 -1
  41. package/apps/pwa/out/_next/static/chunks/app/observatory/page-5f23d0c66a3b4793.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-10d958605a77b3bd.js → page-a3c58f5c72a5f3f9.js} +1 -1
  43. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-a2c4625596f8d69c.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-0e9099531d239f11.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/app/outputs/{page-7a1d85e121065cc8.js → page-e8e1ecaea7cd71d2.js} +1 -1
  46. package/apps/pwa/out/_next/static/chunks/app/page-a1b7ffed710a93e0.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/app/reflection/{page-12916918988ac97e.js → page-1467047104141297.js} +1 -1
  48. package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-d8a3347a9d0c3f4a.js → page-2da6f9b8a38c6978.js} +1 -1
  49. package/apps/pwa/out/_next/static/chunks/app/sessions/page-102310930791197a.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-f331e38fefc00393.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/settings/page-6ed0b73bae8351d8.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-1d824510df025d18.js → page-44d5f767491692bf.js} +1 -1
  53. package/apps/pwa/out/_next/static/chunks/app/setup/page-43336055451f6e37.js +1 -0
  54. package/apps/pwa/out/_next/static/chunks/app/share/{page-7f49fe645d5132f7.js → page-3cc47dca0922ab9a.js} +1 -1
  55. package/apps/pwa/out/_next/static/chunks/app/showroom/page-a77ee355a2304ff0.js +1 -0
  56. package/apps/pwa/out/_next/static/chunks/app/tasks/{page-27d71f131acd9320.js → page-b17cd46cf0e9b8cf.js} +1 -1
  57. package/apps/pwa/out/_next/static/chunks/app/term/page-61553851aa606146.js +1 -0
  58. package/apps/pwa/out/_next/static/chunks/app/today/page-a1f585e154e61c59.js +1 -0
  59. package/apps/pwa/out/_next/static/chunks/app/trace/{page-424659bacfb8cdd7.js → page-7bf54c9bb9274af8.js} +1 -1
  60. package/apps/pwa/out/_next/static/chunks/app/vault/page-5150ef17b5383a57.js +1 -0
  61. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-b9d8ce70792c1672.js +1 -0
  62. package/apps/pwa/out/_next/static/chunks/app/workflows/page-a5a3de231f2cc7d5.js +1 -0
  63. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-5d4fef9ccd7d406b.js → page-134da468538baf48.js} +1 -1
  64. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-31ac4afa2dcd915c.js +1 -0
  65. package/apps/pwa/out/_next/static/chunks/{webpack-539cde99554be20f.js → webpack-c2782b330447ffb6.js} +1 -1
  66. package/apps/pwa/out/_next/static/css/7fa358fec7da7c47.css +1 -0
  67. package/apps/pwa/out/approvals/index.html +1 -1
  68. package/apps/pwa/out/approvals/index.txt +14 -14
  69. package/apps/pwa/out/autopilot/index.html +1 -1
  70. package/apps/pwa/out/autopilot/index.txt +14 -14
  71. package/apps/pwa/out/board/index.html +1 -1
  72. package/apps/pwa/out/board/index.txt +14 -14
  73. package/apps/pwa/out/botlab/index.html +1 -1
  74. package/apps/pwa/out/botlab/index.txt +14 -14
  75. package/apps/pwa/out/bots/index.html +1 -1
  76. package/apps/pwa/out/bots/index.txt +14 -14
  77. package/apps/pwa/out/ceo/index.html +1 -1
  78. package/apps/pwa/out/ceo/index.txt +14 -14
  79. package/apps/pwa/out/chat/index.html +1 -1
  80. package/apps/pwa/out/chat/index.txt +14 -14
  81. package/apps/pwa/out/consult/index.html +1 -1
  82. package/apps/pwa/out/consult/index.txt +14 -14
  83. package/apps/pwa/out/control/index.html +1 -1
  84. package/apps/pwa/out/control/index.txt +14 -14
  85. package/apps/pwa/out/decisions/index.html +1 -0
  86. package/apps/pwa/out/decisions/index.txt +30 -0
  87. package/apps/pwa/out/design-check/index.html +1 -1
  88. package/apps/pwa/out/design-check/index.txt +14 -14
  89. package/apps/pwa/out/editor/index.html +1 -1
  90. package/apps/pwa/out/editor/index.txt +14 -14
  91. package/apps/pwa/out/exec/index.html +1 -1
  92. package/apps/pwa/out/exec/index.txt +14 -14
  93. package/apps/pwa/out/field/index.html +1 -1
  94. package/apps/pwa/out/field/index.txt +14 -14
  95. package/apps/pwa/out/healthz +1 -1
  96. package/apps/pwa/out/index.html +1 -1
  97. package/apps/pwa/out/index.txt +14 -14
  98. package/apps/pwa/out/inside/index.html +1 -1
  99. package/apps/pwa/out/inside/index.txt +14 -14
  100. package/apps/pwa/out/intake/index.html +1 -1
  101. package/apps/pwa/out/intake/index.txt +14 -14
  102. package/apps/pwa/out/live/index.html +1 -1
  103. package/apps/pwa/out/live/index.txt +14 -14
  104. package/apps/pwa/out/loops/index.html +1 -1
  105. package/apps/pwa/out/loops/index.txt +14 -14
  106. package/apps/pwa/out/market/index.html +1 -1
  107. package/apps/pwa/out/market/index.txt +14 -14
  108. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  109. package/apps/pwa/out/missions/placeholder/index.txt +14 -14
  110. package/apps/pwa/out/morning/index.html +1 -1
  111. package/apps/pwa/out/morning/index.txt +14 -14
  112. package/apps/pwa/out/observatory/index.html +1 -1
  113. package/apps/pwa/out/observatory/index.txt +14 -14
  114. package/apps/pwa/out/ops/checklist/index.html +1 -1
  115. package/apps/pwa/out/ops/checklist/index.txt +14 -14
  116. package/apps/pwa/out/ops/release/index.html +1 -1
  117. package/apps/pwa/out/ops/release/index.txt +14 -14
  118. package/apps/pwa/out/ops/seats/index.html +1 -1
  119. package/apps/pwa/out/ops/seats/index.txt +14 -14
  120. package/apps/pwa/out/outputs/index.html +1 -1
  121. package/apps/pwa/out/outputs/index.txt +14 -14
  122. package/apps/pwa/out/reflection/index.html +1 -1
  123. package/apps/pwa/out/reflection/index.txt +15 -15
  124. package/apps/pwa/out/scheduler/index.html +1 -1
  125. package/apps/pwa/out/scheduler/index.txt +14 -14
  126. package/apps/pwa/out/sessions/index.html +1 -1
  127. package/apps/pwa/out/sessions/index.txt +14 -14
  128. package/apps/pwa/out/settings/devices/index.html +1 -1
  129. package/apps/pwa/out/settings/devices/index.txt +14 -14
  130. package/apps/pwa/out/settings/index.html +1 -1
  131. package/apps/pwa/out/settings/index.txt +14 -14
  132. package/apps/pwa/out/setup/done/index.html +1 -1
  133. package/apps/pwa/out/setup/done/index.txt +14 -14
  134. package/apps/pwa/out/setup/index.html +1 -1
  135. package/apps/pwa/out/setup/index.txt +14 -14
  136. package/apps/pwa/out/share/index.html +1 -1
  137. package/apps/pwa/out/share/index.txt +14 -14
  138. package/apps/pwa/out/showroom/index.html +1 -1
  139. package/apps/pwa/out/showroom/index.txt +14 -14
  140. package/apps/pwa/out/tasks/index.html +1 -1
  141. package/apps/pwa/out/tasks/index.txt +14 -14
  142. package/apps/pwa/out/term/index.html +1 -1
  143. package/apps/pwa/out/term/index.txt +14 -14
  144. package/apps/pwa/out/today/index.html +1 -1
  145. package/apps/pwa/out/today/index.txt +14 -14
  146. package/apps/pwa/out/trace/index.html +1 -1
  147. package/apps/pwa/out/trace/index.txt +14 -14
  148. package/apps/pwa/out/vault/index.html +1 -1
  149. package/apps/pwa/out/vault/index.txt +14 -14
  150. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  151. package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
  152. package/apps/pwa/out/workflows/index.html +1 -1
  153. package/apps/pwa/out/workflows/index.txt +14 -14
  154. package/apps/pwa/out/workspace/index.html +1 -1
  155. package/apps/pwa/out/workspace/index.txt +14 -14
  156. package/apps/pwa/out/worktrees/index.html +1 -1
  157. package/apps/pwa/out/worktrees/index.txt +14 -14
  158. package/package.json +2 -1
  159. package/samples/workflows/templates/approval-gated-deploy.yaml +30 -0
  160. package/samples/workflows/templates/chat-driven-research.yaml +35 -0
  161. package/samples/workflows/templates/discord-listen-and-respond.yaml +29 -0
  162. package/samples/workflows/templates/http-poll-then-alert.yaml +30 -0
  163. package/samples/workflows/templates/manual-quickstart.yaml +21 -0
  164. package/samples/workflows/templates/scheduled-daily-digest.yaml +30 -0
  165. package/samples/workflows/templates/webhook-deploy-hook.yaml +27 -0
  166. package/scripts/ci-public-export-import-gate.ts +7 -2
  167. package/scripts/ci-typecheck-changed.ts +21 -6
  168. package/scripts/discord-seat-probe.ts +3 -2
  169. package/scripts/docs/generate-cli-anatomy.ts +162 -0
  170. package/scripts/goals/authoring-metrics.ts +73 -0
  171. package/scripts/landing-heal/must-fix-match.ts +7 -7
  172. package/scripts/landing-heal/run-mission.ts +3 -2
  173. package/scripts/public-export-test-run.ts +37 -2
  174. package/scripts/release-loop/checklist-gate-node.ts +13 -11
  175. package/scripts/release-loop/cut-branch.ts +2 -1
  176. package/scripts/release-loop/cutoff.ts +4 -2
  177. package/scripts/release-loop/docs-land-node.ts +8 -1
  178. package/scripts/release-loop/docs-node.ts +1 -1
  179. package/scripts/release-loop/gate-node.ts +99 -14
  180. package/scripts/release-loop/node-verdict.ts +2 -1
  181. package/scripts/release-loop/npm-publish-node.ts +11 -7
  182. package/scripts/release-loop/ops-upgrade-node.ts +8 -1
  183. package/scripts/release-loop/preflight.ts +246 -0
  184. package/scripts/release-loop/publish-node.ts +8 -1
  185. package/scripts/release-loop/release-readiness.ts +11 -6
  186. package/scripts/release-loop/release-version.ts +48 -0
  187. package/scripts/release-loop/resume-release.ts +115 -0
  188. package/scripts/release-loop/unattended-release.ts +43 -7
  189. package/scripts/release-loop/version-node.ts +145 -40
  190. package/scripts/rhythm/daily.ts +25 -4
  191. package/scripts/skills/public-check.ts +62 -12
  192. package/scripts/test-deterministic.ts +51 -33
  193. package/scripts/test-diet/effectiveness.ts +62 -1
  194. package/scripts/test-diet/lib.ts +1 -1
  195. package/scripts/test-diet/node.ts +8 -1
  196. package/src/agent-mission/review-loop.ts +4 -3
  197. package/src/agent-substrate/pr-comment-meta.ts +20 -0
  198. package/src/autopilot/build/llm-conflict-merge.ts +28 -8
  199. package/src/briefing/brief-items.ts +86 -17
  200. package/src/cli/autopilot-cli.ts +30 -11
  201. package/src/cli/brief-cli.ts +36 -2
  202. package/src/cli/freeze-cli.ts +3 -2
  203. package/src/cli/intake-cli.ts +73 -1
  204. package/src/cli/notify-cli.ts +75 -0
  205. package/src/cli/provider-cli.ts +12 -2
  206. package/src/cli/release-cli.ts +101 -17
  207. package/src/cli/tasks-cli.ts +309 -3
  208. package/src/context-bus/condense.ts +19 -3
  209. package/src/context-bus/context-day.ts +77 -24
  210. package/src/context-bus/context-first.ts +33 -0
  211. package/src/discord-self-message.ts +20 -2
  212. package/src/discord-slash-wire.ts +1 -0
  213. package/src/domains/outbound-alert.ts +200 -27
  214. package/src/domains/outbound-log-probe.ts +16 -1
  215. package/src/domains/release-tool.ts +2 -2
  216. package/src/execution-loop/gates.ts +11 -2
  217. package/src/flow-loop/split.ts +8 -4
  218. package/src/git-fs/worktree.ts +23 -3
  219. package/src/harness/harness-cli-command.ts +10 -2
  220. package/src/harness/harness-queue.ts +388 -33
  221. package/src/harness/helper-scan.ts +10 -3
  222. package/src/hq/seats.ts +369 -0
  223. package/src/index.ts +133 -15
  224. package/src/intake-plane/adopt-gate.ts +49 -0
  225. package/src/intake-plane/check.ts +3 -0
  226. package/src/intake-plane/collect-github.ts +57 -25
  227. package/src/intake-plane/discord-seat-work.ts +8 -2
  228. package/src/intake-plane/requirement-funnel.ts +316 -0
  229. package/src/knowledge/know-find.ts +10 -1
  230. package/src/loops/budget.ts +36 -0
  231. package/src/loops/orchestrator/finish-rate.ts +73 -1
  232. package/src/loops/orchestrator/grid-place.ts +11 -0
  233. package/src/loops/orchestrator/tick.ts +295 -20
  234. package/src/maturity/feature-maturity.ts +3 -2
  235. package/src/nexus/api/decisions-route.ts +11 -1
  236. package/src/nexus/api/loop-edges.ts +69 -13
  237. package/src/nexus/api/ops-seats-sources.ts +2 -2
  238. package/src/nexus/api/workflow-templates.ts +3 -0
  239. package/src/oauth/codex.ts +38 -0
  240. package/src/onboarding/first-chat-gate.ts +29 -0
  241. package/src/release-loop/checklist.ts +104 -6
  242. package/src/release-loop/feature-store.ts +10 -9
  243. package/src/release-loop/landing-freeze.ts +6 -4
  244. package/src/release-loop/placement.ts +13 -3
  245. package/src/release-loop/rubric.ts +80 -0
  246. package/src/seat-dispatch/seat-ask.ts +79 -16
  247. package/src/seat-loop/seat-loop.ts +12 -6
  248. package/src/self-dev/ask-launch-flow.ts +24 -3
  249. package/src/self-dev/ask-path-candidates.ts +68 -0
  250. package/src/self-dev/dev-pipeline.ts +4 -0
  251. package/src/self-dev/draft-triage-rules.ts +1 -1
  252. package/src/self-dev/orchestrate-cli.ts +22 -0
  253. package/src/self-dev/round-pr-supersede.ts +244 -0
  254. package/src/self-dev/run-supervisor.ts +14 -0
  255. package/src/self-implement/follow-up-goals.ts +2 -2
  256. package/src/self-implement/gate-remote.ts +427 -0
  257. package/src/self-implement/goal-author.ts +8 -3
  258. package/src/self-implement/l8-merge-queue.ts +29 -5
  259. package/src/self-implement/orchestrator.ts +59 -12
  260. package/src/self-implement/prior-draft-findings.ts +4 -3
  261. package/src/skills/skill-feedback.ts +76 -0
  262. package/src/task-agent/actions.ts +39 -6
  263. package/src/task-agent/judge.ts +31 -5
  264. package/src/task-agent/mission.ts +423 -0
  265. package/src/task-agent/shadow.ts +90 -0
  266. package/src/task-agent/task-hand.ts +235 -0
  267. package/src/task-orchestrator/surfaces/pod-failure-reason.ts +10 -0
  268. package/src/task-orchestrator/surfaces/self-implement-pod.ts +86 -18
  269. package/src/telegram-loops-command.ts +1 -1
  270. package/src/telegram.ts +5 -20
  271. package/src/user-config.ts +63 -3
  272. package/src/version/packed-revision.json +1 -1
  273. package/apps/pwa/out/_next/static/chunks/2699-63b73fa4d3bcf6ff.js +0 -1
  274. package/apps/pwa/out/_next/static/chunks/4554-3565eea1f1495ba7.js +0 -1
  275. package/apps/pwa/out/_next/static/chunks/4614-21403aac730d4586.js +0 -1
  276. package/apps/pwa/out/_next/static/chunks/5135-44008fe8b1f2aa41.js +0 -1
  277. package/apps/pwa/out/_next/static/chunks/5233-bf28bd6bd7a32099.js +0 -1
  278. package/apps/pwa/out/_next/static/chunks/5977-c952712c87a1f757.js +0 -1
  279. package/apps/pwa/out/_next/static/chunks/6877-fe94e76a5841ee9b.js +0 -1
  280. package/apps/pwa/out/_next/static/chunks/7395-164e238f471b5d7b.js +0 -1
  281. package/apps/pwa/out/_next/static/chunks/7787-02a1c0b04e25e811.js +0 -1
  282. package/apps/pwa/out/_next/static/chunks/8401-4ce1c42c8d89903f.js +0 -1
  283. package/apps/pwa/out/_next/static/chunks/8949-ecb4128a0858b911.js +0 -1
  284. package/apps/pwa/out/_next/static/chunks/899-f41be85e8a680651.js +0 -1
  285. package/apps/pwa/out/_next/static/chunks/app/botlab/page-6b8444914da74ca6.js +0 -1
  286. package/apps/pwa/out/_next/static/chunks/app/ceo/page-7110f732f45bf944.js +0 -1
  287. package/apps/pwa/out/_next/static/chunks/app/chat/page-9a41d65355fc25b9.js +0 -1
  288. package/apps/pwa/out/_next/static/chunks/app/control/page-41afec0b255ed2bb.js +0 -1
  289. package/apps/pwa/out/_next/static/chunks/app/inside/page-ab93b240f687d69a.js +0 -1
  290. package/apps/pwa/out/_next/static/chunks/app/layout-de43a186e5ba5683.js +0 -1
  291. package/apps/pwa/out/_next/static/chunks/app/live/page-8b08cafad27759c9.js +0 -1
  292. package/apps/pwa/out/_next/static/chunks/app/observatory/page-1ed1324b1dc36754.js +0 -1
  293. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-166745e51ac0c7b0.js +0 -1
  294. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-a04260b8ccc968d4.js +0 -1
  295. package/apps/pwa/out/_next/static/chunks/app/page-1f50e3d4e13435f4.js +0 -1
  296. package/apps/pwa/out/_next/static/chunks/app/sessions/page-a9117d2cd3093d36.js +0 -1
  297. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-5a64cb5208c6a8fd.js +0 -1
  298. package/apps/pwa/out/_next/static/chunks/app/settings/page-1407ec83ea64c1f9.js +0 -1
  299. package/apps/pwa/out/_next/static/chunks/app/setup/page-6e878ebbe1737d00.js +0 -1
  300. package/apps/pwa/out/_next/static/chunks/app/showroom/page-81a4e7f8dbfe74cb.js +0 -1
  301. package/apps/pwa/out/_next/static/chunks/app/term/page-3ab59d19913b200e.js +0 -1
  302. package/apps/pwa/out/_next/static/chunks/app/today/page-bf85b2f5bf077925.js +0 -1
  303. package/apps/pwa/out/_next/static/chunks/app/vault/page-ab32a8f522a60371.js +0 -1
  304. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-fc2df261f8a2c7df.js +0 -1
  305. package/apps/pwa/out/_next/static/chunks/app/workflows/page-09461366af82ef13.js +0 -1
  306. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-ea90a8ac73ce2749.js +0 -1
  307. package/apps/pwa/out/_next/static/css/001d5fbaee388a25.css +0 -1
  308. package/apps/pwa/out/_next/static/yRA5ur6DcPl0YHSRheH2O/_buildManifest.js +0 -1
  309. /package/apps/pwa/out/_next/static/{yRA5ur6DcPl0YHSRheH2O → P6YTVgzuZz5oTZhY4Din_}/_ssgManifest.js +0 -0
@@ -6,27 +6,34 @@ import { Database } from 'bun:sqlite';
6
6
  import { debug } from '../debug/log.js';
7
7
  import { effectiveInstanceRoot, prodInstanceRoot, releaseLedgerRoot } from '../instance/resolve.js';
8
8
  import { listChecklist, type ChecklistItem } from '../release-loop/checklist.js';
9
+ import { readLandingFreeze } from '../release-loop/landing-freeze.js';
9
10
  import { listSchedules } from '../release-loop/release-schedule.js';
11
+ import { parseRubric, readRubricItems, rubricScore } from '../release-loop/rubric.js';
10
12
  import { withFileLockSync } from '../storage/file-lock.js';
11
13
  import { trafficTick, TRAFFIC_SEATS, type TrafficCell } from '../loops/orchestrator/traffic.js';
12
- import { decideSpawn, seatCapDetails, seatCapReason } from '../loops/budget.js';
14
+ import { decideSpawn, SEAT_CAP_STALE_RUN_MINUTES, SEAT_CAP_UNKNOWN_SEAT_CAP, seatCapDetails, seatCapExclusion, seatCapReason } from '../loops/budget.js';
13
15
  import { listSelfDevRuns, processBirthId, selfDevRunsDir } from '../self-dev/run-store.js';
14
16
  import { getUserConfig, userConfigPath, type OrchestratorLoopConfig } from '../user-config.js';
15
17
  import { hostLeaseCounts, leaseHasPendingPod } from '../pod-lease/host-lease.js';
16
18
  import { leaseKubectl, measurePoolLease } from '../task-orchestrator/surfaces/pod-lease.js';
17
19
  import { parsePodPool, podPoolHostLease, resolvePodPoolSpec } from '../task-orchestrator/surfaces/pod-pool.js';
18
20
  import { writeHarnessQueueReceipt } from './harness-queue-child.js';
19
- import { finishAdvice, measureFinish, type FinishMetrics } from '../loops/orchestrator/finish-rate.js';
21
+ import { finishAdvice, measureFinish, recordFinishHistory, type FinishMetrics } from '../loops/orchestrator/finish-rate.js';
20
22
 
21
23
  export type QueueSeat = 'OP' | 'TC' | 'MK' | 'UX';
22
24
  export type QueueItem = {
23
25
  id: string; seat: QueueSeat; kind: 'say' | 'ask'; input: string; hold: boolean; heavy: boolean;
24
26
  at: string; status: 'queued' | 'launching' | 'launched' | 'finished'; pid?: number; launchId?: string; idempotencyKey?: string; waitingReason?: string;
25
- launchArgs?: string[]; launchCwd?: string; cellId?: string;
27
+ launchArgs?: string[]; poolHint?: string; /** 힌트를 넣은 쪽 — 'grid' 면 GRID 가 소유(그것만 GRID 가 바꾸거나 지운다). */ poolHintSource?: 'grid'; launchCwd?: string; cellId?: string;
28
+ /** TASK-QUEUE: larger launches first within a seat; a row without it ranks below every prioritized row (FIFO among equals). */
29
+ priority?: number;
26
30
  };
27
31
  export type QueuePool = { running: number; pending: number; reserved: number; limit: number };
28
32
  export type QueueTick = { outcome: 'launched' | 'waiting' | 'skipped'; item?: QueueItem; reason: string };
29
- export type QueueProcess = { pid: number; seat?: QueueSeat; launchId?: string; runId?: string; rootPid?: number };
33
+ /** progressAt = newest run-ledger updatedAt among the tree's members; stopReason = the ledger's supervisor stop (SEAT-CAP-STALE).
34
+ * ledgerRunIds = run ids of the members' ledger records — the `elanous.run` label of their Pod Jobs (SEAT-CAP-STALE2). */
35
+ export type QueueProcess = { pid: number; seat?: QueueSeat; launchId?: string; runId?: string; rootPid?: number;
36
+ progressAt?: number; stopReason?: string; ledgerRunIds?: string[] };
30
37
  type ProcessProbe = {
31
38
  run?: typeof spawnSync; platform?: NodeJS.Platform;
32
39
  cwd?: (pid: number) => string;
@@ -68,8 +75,13 @@ export interface HarnessQueueDeps {
68
75
  next: readonly Pick<ChecklistItem, 'id' | 'title' | 'owner' | 'status' | 'evidence'>[] };
69
76
  idleLog?: (category: string, event: string, data: Record<string, unknown>) => void;
70
77
  log?: (event: 'enqueued' | 'launched' | 'waiting' | 'skipped', data: Record<string, unknown>) => void;
78
+ /** TASK-QUEUE seam: default priority for a cell at enqueue (default: rubric score of that cell in an unreleased schedule). */
79
+ cellPriority?: (cellId: string, root: string, now: Date) => number | undefined;
71
80
  /** FINISH-RATE metrics source; a test process without it skips the real measurement. */
72
81
  finishMetrics?: (now: Date) => FinishMetrics | Promise<FinishMetrics>;
82
+ /** SEAT-CAP-STALE2: run ids with an unfinished Pod Job (running or pending); throws when unreadable. A test process without
83
+ * it sees no Pod work (never a real kubectl). */
84
+ podRuns?: () => ReadonlySet<string>;
73
85
  }
74
86
 
75
87
  const FINISH_CACHE_MS = 10 * 60 * 1_000;
@@ -105,6 +117,11 @@ async function cachedFinishMetrics(root: string, now: Date, deps: HarnessQueueDe
105
117
  mkdirSync(join(root, 'harness'), { recursive: true });
106
118
  writeFileSync(path, JSON.stringify({ at: now.toISOString(), metrics }));
107
119
  } catch (error) { debug.log('loop.orchestrator', 'finish-cache-write-failed', { reason: String(error) }); }
120
+ // 새로 잰 뒤에만 시간별 이력 한 줄(캐시 적중 틱은 안 쓴다) — 실패해도 발사 판정은 그대로다.
121
+ try { recordFinishHistory(root, metrics, now); }
122
+ catch (error) {
123
+ try { debug.log('loop.orchestrator', 'finish-history-write-failed', { reason: String(error) }); } catch { /* observation is fail-soft */ }
124
+ }
108
125
  return metrics;
109
126
  }
110
127
 
@@ -149,8 +166,11 @@ function read(path: string): QueueItem[] {
149
166
  && (row.launchId === undefined || typeof row.launchId === 'string')
150
167
  && (row.idempotencyKey === undefined || typeof row.idempotencyKey === 'string')
151
168
  && (row.launchArgs === undefined || Array.isArray(row.launchArgs) && row.launchArgs.every((arg: unknown) => typeof arg === 'string'))
169
+ && (row.poolHint === undefined || typeof row.poolHint === 'string')
170
+ && (row.poolHintSource === undefined || row.poolHintSource === 'grid')
152
171
  && (row.launchCwd === undefined || typeof row.launchCwd === 'string' && row.launchCwd.startsWith('/'))
153
172
  && (row.cellId === undefined || typeof row.cellId === 'string')
173
+ && (row.priority === undefined || typeof row.priority === 'number' && Number.isFinite(row.priority))
154
174
  && (row.waitingReason === undefined || typeof row.waitingReason === 'string'))) {
155
175
  throw new Error('harness queue: invalid queue file (no launch)');
156
176
  }
@@ -226,13 +246,56 @@ function queueRowCellId(kind: QueueItem['kind'], input: string): string | undefi
226
246
  try { return queueCellId(readFileSync(input, 'utf8')); } catch { return undefined; }
227
247
  }
228
248
 
249
+ /** A CEO directive mark on the cell (crown U+1F451 · 대표 지시) lifts it above every rubric score (max rubric ≈ 25.5). */
250
+ export const QUEUE_CEO_PRIORITY_BONUS = 100;
251
+ const CEO_MARK = /\u{1F451}|대표\s*지시/u;
252
+
253
+ /** Rubric score of a cell, searched in unreleased schedules by cut order; no ledger, no cell, or no rubric → undefined. */
254
+ export function queueCellRubricPriority(cellId: string, root: string, now: Date): number | undefined {
255
+ const ledger = root === effectiveInstanceRoot() ? releaseLedgerRoot() : root;
256
+ if (!existsSync(join(ledger, 'release', 'features.sqlite'))) return undefined;
257
+ const versions = listSchedules(ledger).filter((row) => Date.parse(row.cutAt) > now.getTime())
258
+ .sort((a, b) => Date.parse(a.cutAt) - Date.parse(b.cutAt)).map((row) => row.version);
259
+ for (const version of versions) {
260
+ const cell = readRubricItems(version, ledger).find((item) => item.id === cellId);
261
+ if (!cell) continue;
262
+ const text = `${cell.title}\n${cell.evidence ?? ''}`;
263
+ const rubric = parseRubric(text);
264
+ const ceo = CEO_MARK.test(text);
265
+ if (!rubric && !ceo) return undefined;
266
+ return (rubric ? rubricScore(rubric) : 0) + (ceo ? QUEUE_CEO_PRIORITY_BONUS : 0);
267
+ }
268
+ return undefined;
269
+ }
270
+
271
+ /** A failed rubric lookup leaves the row without priority (FIFO) — it never refuses the enqueue. */
272
+ function defaultQueuePriority(cellId: string | undefined, root: string, deps: HarnessQueueDeps): number | undefined {
273
+ if (cellId === undefined) return undefined;
274
+ try {
275
+ const value = (deps.cellPriority ?? queueCellRubricPriority)(cellId, root, (deps.now ?? (() => new Date()))());
276
+ return value !== undefined && Number.isFinite(value) ? value : undefined;
277
+ } catch (error) {
278
+ try { debug.log('harness.queue', 'priority-lookup-failed', { cellId, reason: String(error) }); }
279
+ catch { /* Observation cannot affect enqueue. */ }
280
+ return undefined;
281
+ }
282
+ }
283
+
284
+ /** Seat head order: priority descending (none = lowest), then arrival ascending, then file order. */
285
+ function queueHeadBefore(a: QueueItem, b: QueueItem): boolean {
286
+ const pa = a.priority ?? -Infinity, pb = b.priority ?? -Infinity;
287
+ if (pa !== pb) return pa > pb;
288
+ const ta = Date.parse(a.at), tb = Date.parse(b.at);
289
+ return Number.isFinite(ta) && Number.isFinite(tb) && ta < tb;
290
+ }
291
+
229
292
  export class HarnessQueueDuplicateError extends Error {
230
293
  constructor(existing: QueueItem, key: string) {
231
294
  super(`같은 ${key} 이 이미 대기열에 있거나 도는 중이다 — ${existing.id} (${existing.status})`);
232
295
  }
233
296
  }
234
297
 
235
- export async function addHarnessQueue(input: { seat: string; say?: string; ask?: string; hold?: boolean; heavy?: boolean; idempotencyKey?: string; launchArgs?: string[]; launchCwd?: string; refuseDuplicate?: boolean }, deps: HarnessQueueDeps = {}): Promise<QueueItem> {
298
+ export async function addHarnessQueue(input: { seat: string; say?: string; ask?: string; hold?: boolean; heavy?: boolean; idempotencyKey?: string; launchArgs?: string[]; poolHint?: string; poolHintSource?: 'grid'; launchCwd?: string; refuseDuplicate?: boolean }, deps: HarnessQueueDeps = {}): Promise<QueueItem> {
236
299
  const assigned = seat(input.seat);
237
300
  if ((input.say === undefined) === (input.ask === undefined)) throw new Error('harness queue add: --say 또는 --ask 중 하나만 필요');
238
301
  const kind = input.ask === undefined ? 'say' : 'ask';
@@ -266,14 +329,17 @@ export async function addHarnessQueue(input: { seat: string; say?: string; ask?:
266
329
  throw new HarnessQueueDuplicateError(existing, key);
267
330
  }
268
331
  }
332
+ const priority = defaultQueuePriority(cellId, root, deps);
269
333
  const item: QueueItem = { id: `hq-${randomUUID()}`, seat: assigned, kind, input: value,
270
334
  hold: input.hold === true, heavy: input.heavy === true, at: new Date().toISOString(), status: 'queued',
271
335
  ...(input.launchArgs === undefined ? {} : { launchArgs: input.launchArgs }),
336
+ ...(input.poolHint === undefined ? {} : { poolHint: input.poolHint, ...(input.poolHintSource ? { poolHintSource: input.poolHintSource } : {}) }),
272
337
  ...(input.launchCwd === undefined ? {} : { launchCwd: input.launchCwd }),
273
338
  ...(cellId === undefined ? {} : { cellId }),
339
+ ...(priority === undefined ? {} : { priority }),
274
340
  ...(input.idempotencyKey === undefined ? {} : { idempotencyKey: input.idempotencyKey }) };
275
341
  save(path, [...rows, item]);
276
- observe('enqueued', { id: item.id, seat: item.seat, kind }, deps);
342
+ observe('enqueued', { id: item.id, seat: item.seat, kind, ...(priority === undefined ? {} : { priority }) }, deps);
277
343
  return item;
278
344
  });
279
345
  }
@@ -336,6 +402,46 @@ function queueRowOutcome(row: QueueItem, root: string): 'pending' | 'succeeded'
336
402
  return state === 'started' ? 'pending' : 'unknown';
337
403
  }
338
404
 
405
+ /** 아직 발사 전(queued)인 항목의 풀 힌트만 바꾼다 — 발사 중·발사 뒤 항목은 손대지 않는다. 바꿨으면 true. */
406
+ export async function setQueuedPoolHint(id: string, poolHint: string | undefined, deps: HarnessQueueDeps = {}, stillValid?: (item: QueueItem) => boolean): Promise<boolean> {
407
+ const root = deps.root ?? effectiveInstanceRoot();
408
+ return locked(root, async (path) => {
409
+ const rows = read(path);
410
+ const item = rows.find((row) => row.id === id);
411
+ if (!item || item.status !== 'queued' || item.poolHint === poolHint) return false;
412
+ // GRID 가 소유하지 않은 힌트(사람·다른 경로가 넣은 것)는 바꾸지도 지우지도 않는다. 힌트가 없던 항목에 새로 넣는 것은 허용.
413
+ if (item.poolHint !== undefined && item.poolHintSource !== 'grid') return false;
414
+ // 바꾸기 직전, 같은 잠금 안에서 호출자의 전제(예: 의뢰가 아직 queued)를 다시 확인한다.
415
+ if (stillValid && !stillValid(item)) return false;
416
+ if (poolHint === undefined) { delete item.poolHint; delete item.poolHintSource; } else { item.poolHint = poolHint; item.poolHintSource = 'grid'; }
417
+ save(path, rows);
418
+ return true;
419
+ });
420
+ }
421
+
422
+ /** TASK-QUEUE: re-rank a waiting row; launched or launching rows are refused (their slot is already taken).
423
+ * `by` is required — the caller names who asked; the audit never invents an actor. */
424
+ export async function setHarnessQueuePriority(id: string, priority: number, by: string, deps: HarnessQueueDeps = {}): Promise<QueueItem> {
425
+ if (!by.trim()) throw new Error('harness queue prio: 요청 주체(by)가 비었다');
426
+ if (!Number.isFinite(priority)) throw new Error(`harness queue prio: 우선순위는 유한한 수여야 한다 — ${priority}`);
427
+ const root = deps.root ?? effectiveInstanceRoot();
428
+ return locked(root, async (path) => {
429
+ const rows = read(path);
430
+ const item = rows.find((row) => row.id === id);
431
+ if (!item) throw new Error(`harness queue prio: 항목 없음 — ${id}`);
432
+ if (item.status !== 'queued') throw new Error(`harness queue prio: 대기(queued) 행만 바꾼다 — ${id} (${item.status})`);
433
+ const updated: QueueItem = { ...item, priority };
434
+ save(path, rows.map((row) => row.id === id ? updated : row));
435
+ // The audit is part of the change: a failed audit write restores the previous file and fails the call.
436
+ try { debug.log('harness.queue', 'prio', { id, from: item.priority ?? null, to: priority, by }); }
437
+ catch (error) {
438
+ save(path, rows);
439
+ throw new Error(`harness queue prio: 감사 기록 실패로 되돌림 — ${String(error)}`);
440
+ }
441
+ return updated;
442
+ });
443
+ }
444
+
339
445
  export async function removeHarnessQueue(id: string, deps: HarnessQueueDeps = {}): Promise<boolean> {
340
446
  const root = deps.root ?? effectiveInstanceRoot();
341
447
  return locked(root, async (path) => {
@@ -358,19 +464,28 @@ export async function removeHarnessQueue(id: string, deps: HarnessQueueDeps = {}
358
464
  });
359
465
  }
360
466
 
361
- export function queueLaunchArgs(item: QueueItem): string[] {
362
- if (item.launchArgs) return item.launchArgs;
363
- return ['harness', item.kind, item.input, '--substrate', 'pod',
467
+ export function queueLaunchArgs(item: Pick<QueueItem, 'kind' | 'input' | 'hold' | 'heavy' | 'launchArgs' | 'poolHint'>): string[] {
468
+ const args = item.launchArgs ?? ['harness', item.kind, item.input, '--substrate', 'pod',
364
469
  ...(item.hold ? ['--no-auto-merge'] : []), ...(item.heavy ? ['--pod-memory', 'high'] : [])];
470
+ if (!item.poolHint || args[0] !== 'harness' || !['say', 'ask'].includes(args[1] ?? '')) return args;
471
+ const substrateIndex = args.indexOf('--substrate');
472
+ if (substrateIndex < 0 || args[substrateIndex + 1] !== 'pod') return args;
473
+ const poolIndex = args.indexOf('--pod-pool');
474
+ if (poolIndex >= 0) return args;
475
+ return [...args, '--pod-pool', item.poolHint];
365
476
  }
366
477
 
367
- export function readHarnessQueuePool(): QueuePool {
478
+ function harnessQueuePoolMembers(): ReturnType<typeof parsePodPool> {
368
479
  const config = getUserConfig();
369
480
  const spec = resolvePodPoolSpec(undefined, process.env, () => config.harness?.podPool ?? config.pod?.pool);
370
481
  const current = spec ? null : leaseKubectl(['config', 'current-context']);
371
482
  const context = spec ?? (current?.status === 0 ? current.stdout.trim() : '');
372
483
  if (!context) throw new Error('pod lease status: no pool/context');
373
- const members = parsePodPool(context);
484
+ return parsePodPool(context);
485
+ }
486
+
487
+ export function readHarnessQueuePool(): QueuePool {
488
+ const members = harnessQueuePoolMembers();
374
489
  const observed = measurePoolLease(members);
375
490
  if (observed.members.some((member) => member.running === null || member.pending === null)) throw new Error('pod lease status: incomplete running/pending measurement');
376
491
  const pendingJobs = observed.members.flatMap((member) => member.pendingJobs ?? []);
@@ -383,6 +498,41 @@ export function readHarnessQueuePool(): QueuePool {
383
498
  };
384
499
  }
385
500
 
501
+ /** Same sanitizing as `k8sLabelValue` (self-implement-pod.ts), which writes the `elanous.run` label value. */
502
+ export function queueRunLabelValue(runId: string): string {
503
+ return runId.replace(/[^A-Za-z0-9._-]+/g, '-').slice(0, 63).replace(/^[^A-Za-z0-9]+|[^A-Za-z0-9]+$/g, '');
504
+ }
505
+
506
+ /** SEAT-CAP-STALE2: `elanous.run` / `elanous.child-run` labels of Jobs without a Complete/Failed condition (running or pending). */
507
+ export function queuePodRunsFromJobs(stdout: string): Set<string> {
508
+ const parsed = JSON.parse(stdout) as { items?: unknown };
509
+ if (!Array.isArray(parsed.items)) throw new Error('pod jobs: invalid response');
510
+ const runs = new Set<string>();
511
+ for (const job of parsed.items as { metadata?: { labels?: Record<string, unknown> };
512
+ status?: { conditions?: { type?: unknown; status?: unknown }[] } }[]) {
513
+ const finished = (job.status?.conditions ?? []).some((c) => (c.type === 'Complete' || c.type === 'Failed') && c.status === 'True');
514
+ if (finished) continue;
515
+ for (const key of ['elanous.run', 'elanous.child-run']) {
516
+ const value = job.metadata?.labels?.[key];
517
+ if (typeof value === 'string' && value) runs.add(value);
518
+ }
519
+ }
520
+ return runs;
521
+ }
522
+
523
+ /** One read-only Job list per pool member; any unreadable member makes the whole answer unavailable (throws). */
524
+ export function readHarnessQueuePodRuns(kubectl: typeof leaseKubectl = leaseKubectl,
525
+ members: readonly { context: string }[] = harnessQueuePoolMembers()): ReadonlySet<string> {
526
+ const runs = new Set<string>();
527
+ for (const member of members) {
528
+ const result = kubectl(['--context', member.context, '--request-timeout=10s', '-n', 'elanous-test', 'get', 'jobs',
529
+ '-l', 'elanous.substrate=pod', '-o', 'json']);
530
+ if (result.status !== 0) throw new Error(`pod jobs ${member.context}: ${result.stderr.trim().split('\n').pop() || `rc=${result.status}`}`);
531
+ for (const run of queuePodRunsFromJobs(result.stdout)) runs.add(run);
532
+ }
533
+ return runs;
534
+ }
535
+
386
536
  function alive(pid: number): boolean {
387
537
  try { process.kill(pid, 0); return true; } catch { return false; }
388
538
  }
@@ -420,14 +570,25 @@ export function readHarnessQueueProcesses(probe: ProcessProbe = {}): readonly Qu
420
570
  const ambiguousPids = new Set<number>();
421
571
  const candidatePids = new Set(candidates.map(({ pid }) => pid));
422
572
  const births = new Map<number, string | undefined>();
573
+ const ledgerProgress = new Map<number, { at: number; stopReason?: string }>();
574
+ const ledgerRunIds = new Map<number, Set<string>>();
423
575
  for (const record of listSelfDevRuns(probe.runsDir ?? selfDevRunsDir())) {
424
576
  const pid = record.pid;
425
- if (!pid || !candidatePids.has(pid) || !record.pidStart || !queueSeatNames.some((name) => name === record.seat)) continue;
577
+ if (!pid || !candidatePids.has(pid) || !record.pidStart) continue;
426
578
  if (!births.has(pid)) {
427
579
  try { births.set(pid, (probe.birthId ?? ((id: number) => processBirthId(id, platform)))(pid)); }
428
580
  catch { births.set(pid, undefined); }
429
581
  }
430
- if (births.get(pid) !== record.pidStart || ambiguousPids.has(pid)) continue;
582
+ if (births.get(pid) !== record.pidStart) continue;
583
+ if (record.runId) ledgerRunIds.set(pid, (ledgerRunIds.get(pid) ?? new Set<string>()).add(record.runId));
584
+ // Newest updatedAt is the process's latest progress; it is stopped only when every matched record stopped.
585
+ if (Number.isFinite(record.updatedAt)) {
586
+ const prior = ledgerProgress.get(pid);
587
+ const stopped = (prior ? prior.stopReason !== undefined : true) && Boolean(record.supervisorStopReason);
588
+ const stopReason = stopped ? (prior?.stopReason ?? record.supervisorStopReason) : undefined;
589
+ ledgerProgress.set(pid, { at: Math.max(prior?.at ?? -Infinity, record.updatedAt), ...(stopReason ? { stopReason } : {}) });
590
+ }
591
+ if (!queueSeatNames.some((name) => name === record.seat) || ambiguousPids.has(pid)) continue;
431
592
  const prior = ledgerSeats.get(pid);
432
593
  if (prior && prior !== record.seat) { ledgerSeats.delete(pid); ambiguousPids.add(pid); }
433
594
  else ledgerSeats.set(pid, record.seat!);
@@ -446,6 +607,8 @@ export function readHarnessQueueProcesses(probe: ProcessProbe = {}): readonly Qu
446
607
  let launchId: string | undefined;
447
608
  let runId: string | undefined;
448
609
  for (const { process: { pid, command }, depth } of members) {
610
+ // The queue wrapper's argv carries its receipt path, so its launch id is its identity on every platform.
611
+ launchId ??= /\/harness\/(hq-[0-9a-f-]{36})\.receipt\.json(?:\s|$)/.exec(command)?.[1];
449
612
  let envSeat: string | undefined;
450
613
  if (platform === 'linux') {
451
614
  try {
@@ -487,10 +650,16 @@ export function readHarnessQueueProcesses(probe: ProcessProbe = {}): readonly Qu
487
650
  const top = eligible.filter((entry) => entry.depth === depth);
488
651
  const seats = new Set(top.map((entry) => entry.seat));
489
652
  const assigned = seats.size === 1 ? top[0] : undefined;
653
+ const progress = members.flatMap(({ process: { pid } }) => ledgerProgress.has(pid) ? [ledgerProgress.get(pid)!] : []);
654
+ const progressAt = progress.length ? Math.max(...progress.map((entry) => entry.at)) : undefined;
655
+ // Stopped only when every ledger-bearing member's supervisor stopped; one live supervisor keeps the run in progress.
656
+ const stopReason = progress.length && progress.every((entry) => entry.stopReason) ? progress[0]!.stopReason : undefined;
657
+ const runIds = [...new Set(members.flatMap(({ process: { pid } }) => [...(ledgerRunIds.get(pid) ?? [])]))];
490
658
  try { debug.log('harness.queue', 'attributed', { pid: root.pid, seat: assigned?.seat ?? null, source: assigned?.source ?? 'all' }); }
491
659
  catch { /* Observation does not block inventory. */ }
492
660
  rows.push({ pid: root.pid, ...(assigned ? { seat: assigned.seat } : {}), ...(launchId ? { launchId } : {}),
493
- ...(runId ? { runId } : {}) });
661
+ ...(runId ? { runId } : {}), ...(progressAt !== undefined ? { progressAt } : {}), ...(stopReason ? { stopReason } : {}),
662
+ ...(runIds.length ? { ledgerRunIds: runIds } : {}) });
494
663
  }
495
664
  return rows;
496
665
  }
@@ -510,6 +679,23 @@ function receipt(root: string, launchId: string): 'started' | 'finished' | 'not-
510
679
  }
511
680
  }
512
681
 
682
+ /** Launch time embedded in a UUIDv7 launch id (48-bit ms prefix); undefined for older v4 ids. */
683
+ export function queueLaunchTime(launchId: string): number | undefined {
684
+ const match = /^hq-([0-9a-f]{8})-([0-9a-f]{4})-7[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$/.exec(launchId);
685
+ if (!match) return undefined;
686
+ const at = parseInt(`${match[1]}${match[2]}`, 16);
687
+ return Number.isSafeInteger(at) && at > 0 ? at : undefined;
688
+ }
689
+
690
+ /** When the wrapper wrote its receipt (≈ spawn time); undefined when absent or unreadable. */
691
+ function receiptTime(root: string, launchId: string): number | undefined {
692
+ try {
693
+ const record = JSON.parse(readFileSync(harnessQueueReceiptPath(root, launchId), 'utf8')) as { at?: unknown };
694
+ const at = typeof record.at === 'string' ? Date.parse(record.at) : NaN;
695
+ return Number.isFinite(at) ? at : undefined;
696
+ } catch { return undefined; }
697
+ }
698
+
513
699
  function defaultLaunch(command?: string) {
514
700
  return (item: QueueItem, args: string[], root: string) => launch(item, args, root, command);
515
701
  }
@@ -582,18 +768,23 @@ function warnLegacySeatCap(path: string): void {
582
768
  } catch { /* An unreadable config cannot affect dispatch or observation. */ }
583
769
  }
584
770
 
585
- function groupQueueRuns(processes: readonly QueueProcess[]): { seat?: QueueSeat; launchIds: Set<string> }[] {
586
- const groups: { seats: Set<QueueSeat>; unknown: boolean; launchIds: Set<string>; keys: Set<string> }[] = [];
771
+ type QueueRun = { seat?: QueueSeat; launchIds: Set<string>; pids: Set<number>; runIds: Set<string>; progressAt?: number; stopReason?: string;
772
+ /** Every run id of the group (env and ledger) — matched against Pod Job labels only, never used for grouping. */
773
+ podRunIds: Set<string> };
774
+
775
+ function groupQueueRuns(processes: readonly QueueProcess[]): QueueRun[] {
776
+ const groups: { seats: Set<QueueSeat>; unknown: boolean; launchIds: Set<string>; keys: Set<string>; rows: QueueProcess[] }[] = [];
587
777
  for (const row of processes) {
588
778
  const keys = [row.runId && `run:${row.runId}`, `root:${row.rootPid ?? row.pid}`,
589
779
  row.launchId && `launch:${row.launchId}`, `pid:${row.pid}`].filter((key): key is string => Boolean(key));
590
780
  const matching = groups.filter((group) => keys.some((key) => group.keys.has(key)));
591
781
  const group = matching[0] ?? { seats: new Set<QueueSeat>(), unknown: false,
592
- launchIds: new Set<string>(), keys: new Set<string>() };
782
+ launchIds: new Set<string>(), keys: new Set<string>(), rows: [] };
593
783
  for (const other of matching.slice(1)) {
594
784
  for (const key of other.keys) group.keys.add(key);
595
785
  for (const seat of other.seats) group.seats.add(seat);
596
786
  for (const id of other.launchIds) group.launchIds.add(id);
787
+ group.rows.push(...other.rows);
597
788
  group.unknown ||= other.unknown;
598
789
  groups.splice(groups.indexOf(other), 1);
599
790
  }
@@ -601,12 +792,40 @@ function groupQueueRuns(processes: readonly QueueProcess[]): { seat?: QueueSeat;
601
792
  if (row.seat) group.seats.add(row.seat);
602
793
  else group.unknown = true;
603
794
  if (row.launchId) group.launchIds.add(row.launchId);
795
+ group.rows.push(row);
604
796
  if (!matching.length) groups.push(group);
605
797
  }
606
- return groups.map((group) => ({
607
- ...(group.unknown || group.seats.size !== 1 ? {} : { seat: [...group.seats][0] }),
608
- launchIds: group.launchIds,
609
- }));
798
+ return groups.map((group) => {
799
+ const progress = group.rows.filter((row) => row.progressAt !== undefined);
800
+ const progressAt = progress.length ? Math.max(...progress.map((row) => row.progressAt!)) : undefined;
801
+ const stopReason = progress.length && progress.every((row) => row.stopReason) ? progress[0]!.stopReason : undefined;
802
+ return {
803
+ ...(group.unknown || group.seats.size !== 1 ? {} : { seat: [...group.seats][0] }),
804
+ launchIds: group.launchIds,
805
+ pids: new Set(group.rows.flatMap((row) => [row.pid, ...(row.rootPid ? [row.rootPid] : [])])),
806
+ runIds: new Set(group.rows.flatMap((row) => row.runId ? [row.runId] : [])),
807
+ podRunIds: new Set(group.rows.flatMap((row) => [...(row.runId ? [row.runId] : []), ...(row.ledgerRunIds ?? [])])),
808
+ ...(progressAt !== undefined ? { progressAt } : {}), ...(stopReason ? { stopReason } : {}),
809
+ };
810
+ });
811
+ }
812
+
813
+ /** SEAT-CAP-STALE limits: config harness.queue > env > default. */
814
+ export function harnessQueueCountingLimits(queue: { staleRunMinutes?: number; unknownSeatCap?: number } | undefined,
815
+ env: NodeJS.ProcessEnv = process.env): { staleRunMinutes: number; unknownSeatCap: number } {
816
+ const fromEnv = (name: string, min: number): number | undefined => {
817
+ const raw = env[name];
818
+ if (raw === undefined || !/^\d+$/.test(raw.trim())) return undefined;
819
+ const value = Number(raw.trim());
820
+ return Number.isSafeInteger(value) && value >= min ? value : undefined;
821
+ };
822
+ const valid = (value: unknown, min: number): value is number => typeof value === 'number' && Number.isSafeInteger(value) && value >= min;
823
+ return {
824
+ staleRunMinutes: valid(queue?.staleRunMinutes, 1) ? queue!.staleRunMinutes!
825
+ : fromEnv('ELANOUS_HARNESS_QUEUE_STALE_RUN_MINUTES', 1) ?? SEAT_CAP_STALE_RUN_MINUTES,
826
+ unknownSeatCap: valid(queue?.unknownSeatCap, 0) ? queue!.unknownSeatCap!
827
+ : fromEnv('ELANOUS_HARNESS_QUEUE_UNKNOWN_SEAT_CAP', 0) ?? SEAT_CAP_UNKNOWN_SEAT_CAP,
828
+ };
610
829
  }
611
830
 
612
831
  const IDLE_REQUEST_INTERVAL_MS = 15 * 60_000;
@@ -721,6 +940,15 @@ export function requestIdleSeats(root: string, now: Date, items: readonly QueueI
721
940
  }
722
941
  }
723
942
 
943
+ /** ORCH2 ③ — 발사를 막는 동결이면 사유(waitingReason)와 관측 칸을, 아니면 null. 동결 파일을 못 읽으면 닫힌 쪽(막는다). */
944
+ function launchFreezeHold(root: string, now: Date): { reason: string; freezeReason: string; until: string | null } | null {
945
+ let freeze: ReturnType<typeof readLandingFreeze>;
946
+ try { freeze = readLandingFreeze(root, now); }
947
+ catch { return { reason: '동결 상태 읽기 실패', freezeReason: '동결 상태 읽기 실패', until: null }; }
948
+ if (freeze?.holdLaunches !== true) return null;
949
+ return { reason: `동결 — ${freeze.reason}${freeze.until ? ` · ~${freeze.until}` : ''}`, freezeReason: freeze.reason, until: freeze.until };
950
+ }
951
+
724
952
  export async function tickHarnessQueue(deps: HarnessQueueDeps = {}, requestedId?: string): Promise<QueueTick> {
725
953
  const root = deps.root ?? effectiveInstanceRoot();
726
954
  return locked(root, async (path) => {
@@ -738,13 +966,33 @@ export async function tickHarnessQueue(deps: HarnessQueueDeps = {}, requestedId?
738
966
  catch { /* Shadow observation cannot affect queue dispatch. */ }
739
967
  }
740
968
  const items = read(path);
969
+ // ORCH2 ③ 동결 창의 «발사» 절반 — `freeze on --hold-launches` 면 이 틱은 발사 0. 동결 파일을 못 읽으면 착지 쪽과 같이 닫힌 쪽(발사 0).
970
+ const hold = launchFreezeHold(root, (deps.now ?? (() => new Date()))());
971
+ if (hold) {
972
+ const reason = hold.reason;
973
+ const queued = items.filter((item) => item.status === 'queued');
974
+ const updated = items.map((item) => item.status === 'queued' && item.waitingReason !== reason ? { ...item, waitingReason: reason } : item);
975
+ if (updated.some((item, index) => item !== items[index])) save(path, updated);
976
+ for (const item of queued) observe('waiting', { id: item.id, seat: item.seat, reason }, deps);
977
+ if (queued.length) {
978
+ try { debug.log('loop.orchestrator', 'freeze-held-launch', { reason: hold.freezeReason, until: hold.until, queued: queued.length, phase: 'tick-start' }); }
979
+ catch { /* Observation cannot affect dispatch. */ }
980
+ }
981
+ const subject = (requestedId ? queued.find((item) => item.id === requestedId) : undefined) ?? (!requestedId ? queued[0] : undefined);
982
+ return subject ? { outcome: 'waiting', item: { ...subject, waitingReason: reason }, reason }
983
+ : { outcome: 'skipped', reason: requestedId ? '이미 처리됨 또는 항목 없음' : 'empty' };
984
+ }
741
985
  try { await (deps.idleRequest ?? requestIdleSeats)(root, (deps.now ?? (() => new Date()))(), items, deps); }
742
986
  catch (error) {
743
987
  try { debug.log('loop.orchestrator', 'idle-request-failed', { reason: String(error) }); }
744
988
  catch { /* Idle observation cannot affect queue dispatch. */ }
745
989
  }
746
990
  const heads = new Map<QueueSeat, QueueItem>();
747
- for (const row of items) if (row.status === 'queued' && !heads.has(row.seat)) heads.set(row.seat, row);
991
+ for (const row of items) {
992
+ if (row.status !== 'queued') continue;
993
+ const head = heads.get(row.seat);
994
+ if (!head || queueHeadBefore(row, head)) heads.set(row.seat, row);
995
+ }
748
996
  if (requestedId && ![...heads.values()].some((row) => row.id === requestedId)) {
749
997
  const row = items.find((item) => item.id === requestedId);
750
998
  if (!row || row.status !== 'queued') return { outcome: 'skipped', item: row, reason: '이미 처리됨 또는 항목 없음' };
@@ -809,6 +1057,109 @@ export async function tickHarnessQueue(deps: HarnessQueueDeps = {}, requestedId?
809
1057
  if (run.seat) attributed[run.seat]++;
810
1058
  else unattributed++;
811
1059
  }
1060
+ const limits = harnessQueueCountingLimits(config.harness?.queue);
1061
+ const nowMs = (deps.now ?? (() => new Date()))().getTime();
1062
+ const excludedLogged = new Set<string>();
1063
+ const logExcluded = (key: string, data: Record<string, unknown>): void => {
1064
+ if (excludedLogged.has(key)) return;
1065
+ excludedLogged.add(key);
1066
+ try { debug.log('harness.queue', 'seat-cap-excluded', data); }
1067
+ catch { /* Observation cannot affect dispatch. */ }
1068
+ };
1069
+ const runLabel = (run: QueueRun): Record<string, unknown> => ({ runId: [...run.runIds][0] ?? [...run.podRunIds][0] ?? null,
1070
+ launchId: [...run.launchIds][0] ?? null, pid: [...run.pids][0] });
1071
+ const runKey = (run: QueueRun): string => `run:${[...run.pids].join(',')}`;
1072
+ // SEAT-CAP-STALE2: a run quiet in its host ledger may be working in a Pod (POD-OBS writes elsewhere). Its unfinished Job
1073
+ // keeps it progressing. One Job read per tick, only once some run would go stale; an unreadable read keeps every such run.
1074
+ let podRuns: { ok: true; runs: ReadonlySet<string> } | { ok: false } | undefined;
1075
+ const podState = (): typeof podRuns & {} => {
1076
+ if (podRuns) return podRuns;
1077
+ const testProcess = process.env.NODE_ENV === 'test' || Boolean(process.env.ELANOUS_TEST_HOME);
1078
+ try { podRuns = { ok: true, runs: (deps.podRuns ?? (testProcess ? () => new Set<string>() : () => readHarnessQueuePodRuns()))() }; }
1079
+ catch (error) {
1080
+ podRuns = { ok: false };
1081
+ try { debug.log('harness.queue', 'progress-source-unavailable', { source: 'pod-jobs', error: String(error).slice(0, 240) }, { level: 'warn' }); }
1082
+ catch { /* Observation cannot affect dispatch. */ }
1083
+ }
1084
+ return podRuns;
1085
+ };
1086
+ const progressLogged = new Set<string>();
1087
+ const runVerdict = (run: QueueRun, seat: QueueSeat, extra: Record<string, unknown> = {}): ReturnType<typeof seatCapExclusion> => {
1088
+ const verdict = seatCapExclusion({ kind: 'run', seat, progressAt: run.progressAt, stopReason: run.stopReason }, nowMs, limits);
1089
+ if (verdict?.reason !== 'stale') return verdict;
1090
+ const pod = podState();
1091
+ const podRunId = pod.ok ? [...run.podRunIds].find((id) => pod.runs.has(queueRunLabelValue(id))) : undefined;
1092
+ if (pod.ok && !podRunId) return verdict;
1093
+ if (!progressLogged.has(runKey(run))) {
1094
+ progressLogged.add(runKey(run));
1095
+ try { debug.log('harness.queue', 'seat-cap-progress', { ...runLabel(run), ...extra, seat, source: pod.ok ? 'pod-job' : 'pod-unavailable',
1096
+ ...(podRunId ? { podRunId } : {}), ledgerIdleMin: verdict.idleMin }); }
1097
+ catch { /* Observation cannot affect dispatch. */ }
1098
+ }
1099
+ return null;
1100
+ };
1101
+ // Identity is the launch id only: a recycled wrapper pid must never adopt an unrelated run.
1102
+ const matches = (run: QueueRun, row: QueueItem): boolean => Boolean(row.launchId && run.launchIds.has(row.launchId));
1103
+ let unknownLogged = false;
1104
+ // A seat-less run that is a queue row's launch takes that row's seat; only the rest form the unknown-seat bucket.
1105
+ const logUnknownBucket = (): void => {
1106
+ if (unknownLogged) return;
1107
+ unknownLogged = true;
1108
+ const launchedRows = current.filter((row) => row.status === 'launching' || row.status === 'launched');
1109
+ const bucket = runs.filter((run) => !run.seat && !launchedRows.some((row) => matches(run, row)));
1110
+ for (const run of bucket) {
1111
+ const verdict = seatCapExclusion({ kind: 'run', progressAt: run.progressAt }, nowMs, limits);
1112
+ logExcluded(runKey(run), { ...runLabel(run), seat: null, reason: 'unknown-seat', idleMin: verdict?.idleMin ?? null });
1113
+ }
1114
+ if (bucket.length > limits.unknownSeatCap) {
1115
+ try { debug.log('harness.queue', 'unknown-seat-over-cap', { count: bucket.length, cap: limits.unknownSeatCap }, { level: 'warn' }); }
1116
+ catch { /* Observation cannot affect dispatch. */ }
1117
+ }
1118
+ };
1119
+ // Seat-less runs live in their own bucket; a seat is charged only by its own live, progressing runs and rows.
1120
+ const countSeat = (seat: QueueSeat): number => {
1121
+ logUnknownBucket();
1122
+ const own = runs.filter((run) => run.seat === seat);
1123
+ let active = 0;
1124
+ for (const run of own) {
1125
+ const verdict = runVerdict(run, seat);
1126
+ if (!verdict) { active++; continue; }
1127
+ logExcluded(runKey(run), { ...runLabel(run), seat, reason: verdict.reason, idleMin: verdict.idleMin });
1128
+ }
1129
+ for (const row of current) {
1130
+ if (row.seat !== seat || (row.status !== 'launching' && row.status !== 'launched')) continue;
1131
+ // A row whose own-seat run is already in the inventory is that run (matched by launch id).
1132
+ if (own.some((run) => matches(run, row))) continue;
1133
+ // A seat-less run that is this row's launch takes the row's seat; its progress still decides whether it holds it.
1134
+ const unattributedRun = runs.find((run) => !run.seat && matches(run, row));
1135
+ if (unattributedRun) {
1136
+ const verdict = runVerdict(unattributedRun, seat, { launchId: row.launchId ?? null });
1137
+ if (!verdict) { active++; continue; }
1138
+ logExcluded(runKey(unattributedRun), { ...runLabel(unattributedRun), launchId: row.launchId ?? null, seat, reason: verdict.reason, idleMin: verdict.idleMin });
1139
+ continue;
1140
+ }
1141
+ // A legacy row without a launch id has no receipt; wrapper liveness and the launch grace still decide.
1142
+ const state = row.launchId ? (deps.receipt ?? receipt)(root, row.launchId) : null;
1143
+ // A row with a launch id is live only through the inventory (its wrapper argv carries the id; matched above), so a
1144
+ // recycled pid can never keep it; the launch grace covers the moment before the wrapper appears. Only a legacy row
1145
+ // without a launch id falls back to its recorded pid's liveness.
1146
+ const live = !row.launchId && row.pid !== undefined && (deps.alive ?? alive)(row.pid);
1147
+ // The grace runs from the receipt time (written at spawn), else the launch id's own time (UUIDv7), else enqueue time
1148
+ // (legacy v4 ids only).
1149
+ const launchedAt = (row.launchId ? receiptTime(root, row.launchId) ?? queueLaunchTime(row.launchId) : undefined)
1150
+ ?? Date.parse(row.at);
1151
+ const verdict = seatCapExclusion({ kind: 'row', status: row.status, receipt: state, live,
1152
+ ...(Number.isFinite(launchedAt) ? { launchedAt } : {}) }, nowMs, limits);
1153
+ if (!verdict) { active++; continue; }
1154
+ logExcluded(`row:${row.id}`, { runId: null, launchId: row.launchId ?? null, queueId: row.id, seat, reason: verdict.reason, idleMin: verdict.idleMin });
1155
+ }
1156
+ return active;
1157
+ };
1158
+ const seatCounts = new Map<QueueSeat, number>();
1159
+ const seatActive = (seat: QueueSeat): number => {
1160
+ if (!seatCounts.has(seat)) seatCounts.set(seat, countSeat(seat));
1161
+ return seatCounts.get(seat)!;
1162
+ };
812
1163
  current = items.flatMap((row) => {
813
1164
  if ((row.status !== 'launched' && row.status !== 'launching') || !row.launchId) return [row];
814
1165
  const state = (deps.receipt ?? receipt)(root, row.launchId);
@@ -820,17 +1171,15 @@ export async function tickHarnessQueue(deps: HarnessQueueDeps = {}, requestedId?
820
1171
  return [];
821
1172
  });
822
1173
  if (current.length !== items.length || current.some((row, index) => row !== items[index])) save(path, current);
1174
+ // Every seat is counted once per tick so each excluded run or row is observed even when its seat has no queued head.
1175
+ for (const seat of queueSeatNames) seatActive(seat);
823
1176
  let pool: QueuePool | undefined;
824
1177
  let poolReason: string | undefined;
825
1178
  let finishHeld: string | null | undefined;
826
1179
  for (const item of candidates) {
827
1180
  const injectedCap = deps.cap?.(item.seat);
828
1181
  const details = seatCapDetails({ seat: item.seat, caps, gate, injectedCap });
829
- const charged = runs.filter((run) => !run.seat || run.seat === item.seat);
830
- const launchIds = new Set(charged.flatMap((run) => [...run.launchIds]));
831
- const active = charged.length + current.filter((row) => row.seat === item.seat &&
832
- (row.status === 'launching' || row.status === 'launched')
833
- && (!row.launchId || !launchIds.has(row.launchId))).length;
1182
+ const active = seatActive(item.seat);
834
1183
  if (active >= details.cap) { blocked.set(item.seat, { reason: seatCapReason(item.seat, active, details), active }); continue; }
835
1184
  if (!pool && !poolReason) {
836
1185
  try { pool = (deps.pool ?? readHarnessQueuePool)(); }
@@ -864,11 +1213,7 @@ export async function tickHarnessQueue(deps: HarnessQueueDeps = {}, requestedId?
864
1213
  const own = blocked.get(requested.seat)?.reason ?? (() => {
865
1214
  const ownCap = deps.cap?.(requested.seat);
866
1215
  const details = seatCapDetails({ seat: requested.seat, caps, gate, injectedCap: ownCap });
867
- const charged = runs.filter((run) => !run.seat || run.seat === requested.seat);
868
- const launchIds = new Set(charged.flatMap((run) => [...run.launchIds]));
869
- const active = charged.length + current.filter((row) => row.seat === requested.seat &&
870
- (row.status === 'launching' || row.status === 'launched')
871
- && (!row.launchId || !launchIds.has(row.launchId))).length;
1216
+ const active = seatActive(requested.seat);
872
1217
  if (active >= details.cap) return seatCapReason(requested.seat, active, details);
873
1218
  const budget = decideSpawn({ seat: requested.seat, running: active, caps, gate, injectedCap: ownCap });
874
1219
  return budget.allow ? undefined : `${budget.reason}: ${seatCapReason(requested.seat, active, details)}`;
@@ -878,7 +1223,17 @@ export async function tickHarnessQueue(deps: HarnessQueueDeps = {}, requestedId?
878
1223
  observe('waiting', { id: requested.id, seat: requested.seat, reason }, deps);
879
1224
  return { outcome: 'waiting', item: { ...requested, waitingReason: reason }, reason };
880
1225
  }
881
- const launching: QueueItem = { ...item, status: 'launching', launchId: `hq-${randomUUID()}` };
1226
+ // 발사 직전 다시 본다 — 틱 앞의 확인과 여기 사이에 await(idleRequest·pool·finish 측정)가 있어 그새 켜진 동결을 놓치지 않게.
1227
+ const lateHold = launchFreezeHold(root, (deps.now ?? (() => new Date()))());
1228
+ if (lateHold) {
1229
+ save(path, current.map((row) => row.id === item.id ? { ...row, waitingReason: lateHold.reason } : row));
1230
+ observe('waiting', { id: item.id, seat: item.seat, reason: lateHold.reason }, deps);
1231
+ try { debug.log('loop.orchestrator', 'freeze-held-launch', { reason: lateHold.freezeReason, until: lateHold.until, queued: 1, phase: 'pre-launch' }); }
1232
+ catch { /* Observation cannot affect dispatch. */ }
1233
+ return { outcome: 'waiting', item: { ...item, waitingReason: lateHold.reason }, reason: lateHold.reason };
1234
+ }
1235
+ // UUIDv7 carries the launch time, so the dead-row grace can run from launch without a new queue field.
1236
+ const launching: QueueItem = { ...item, status: 'launching', launchId: `hq-${Bun.randomUUIDv7()}` };
882
1237
  delete launching.waitingReason;
883
1238
  save(path, current.map((row) => row.id === item.id ? launching : row));
884
1239
  try {