elanous 0.2.16 → 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 (354) 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-b46d45b705e8c9ae.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/9247-269e4464cc4b491c.js +1 -0
  19. package/apps/pwa/out/_next/static/chunks/app/approvals/page-d92f7ee3994e84c7.js +1 -0
  20. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-3cb52e5ea8fc2fa5.js → page-4733c967b56a4277.js} +1 -1
  21. package/apps/pwa/out/_next/static/chunks/app/board/page-d22fd849cc22f104.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/app/botlab/{page-c1e20d7d338713f1.js → page-bb742f995c6769e7.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/app/bots/{page-482c5dbde699947c.js → page-02b073a2ac793b83.js} +1 -1
  24. package/apps/pwa/out/_next/static/chunks/app/ceo/page-60c3da708f9a0ccc.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/app/chat/page-8a9b3594bc23289b.js +1 -0
  26. package/apps/pwa/out/_next/static/chunks/app/consult/page-a0eb02638b1e6f31.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/app/control/{page-a0e2a6d1bfb52fc5.js → page-308111abadf35670.js} +1 -1
  28. package/apps/pwa/out/_next/static/chunks/app/decisions/page-6eaf7bac37830c21.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/app/design-check/{page-c53598cfce6156f2.js → page-5d97b2901e0d1ea6.js} +1 -1
  30. package/apps/pwa/out/_next/static/chunks/app/editor/{page-13a63953f53c6a23.js → page-c39554c321576982.js} +1 -1
  31. package/apps/pwa/out/_next/static/chunks/app/exec/{page-5d3c8429d04f3831.js → page-8c3d8e63ae2e1c98.js} +1 -1
  32. package/apps/pwa/out/_next/static/chunks/app/field/{page-3193bdc0bbab047a.js → page-9b72a07d424701c6.js} +1 -1
  33. package/apps/pwa/out/_next/static/chunks/app/inside/page-bd5d586429167fea.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/app/intake/{page-6107ca8ddd5e3bdd.js → page-a572f58cd3ca5864.js} +1 -1
  35. package/apps/pwa/out/_next/static/chunks/app/layout-59b7834d497c2255.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/app/live/page-be5ea965a07fbc41.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/app/loops/page-c130b63862437814.js +1 -0
  38. package/apps/pwa/out/_next/static/chunks/app/market/page-fa244bd146a00ae8.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-44d56d6391965a6a.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/app/morning/{page-4167348049166852.js → page-83a700d4c754d802.js} +1 -1
  41. package/apps/pwa/out/_next/static/chunks/app/{not-found-de52e8fdf9095f7a.js → not-found-7b3170f374f77b9d.js} +1 -1
  42. package/apps/pwa/out/_next/static/chunks/app/observatory/page-5f23d0c66a3b4793.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-ca3952cd78a83ca2.js → page-a3c58f5c72a5f3f9.js} +1 -1
  44. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-a2c4625596f8d69c.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-0e9099531d239f11.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/app/outputs/page-e8e1ecaea7cd71d2.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/app/page-a1b7ffed710a93e0.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/app/reflection/{page-1ebfe049c640e171.js → page-1467047104141297.js} +1 -1
  49. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-2da6f9b8a38c6978.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/sessions/page-102310930791197a.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-f331e38fefc00393.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/settings/page-6ed0b73bae8351d8.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-abe8e3b74bea1ffc.js → page-44d5f767491692bf.js} +1 -1
  54. package/apps/pwa/out/_next/static/chunks/app/setup/page-43336055451f6e37.js +1 -0
  55. package/apps/pwa/out/_next/static/chunks/app/share/{page-d28573d6792b4275.js → page-3cc47dca0922ab9a.js} +1 -1
  56. package/apps/pwa/out/_next/static/chunks/app/showroom/page-a77ee355a2304ff0.js +1 -0
  57. package/apps/pwa/out/_next/static/chunks/app/tasks/{page-d751de5f8062ff5e.js → page-b17cd46cf0e9b8cf.js} +1 -1
  58. package/apps/pwa/out/_next/static/chunks/app/term/{page-fe1759f94003b32b.js → page-61553851aa606146.js} +1 -1
  59. package/apps/pwa/out/_next/static/chunks/app/today/{page-d64c984e855c8c68.js → page-a1f585e154e61c59.js} +1 -1
  60. package/apps/pwa/out/_next/static/chunks/app/trace/page-7bf54c9bb9274af8.js +1 -0
  61. package/apps/pwa/out/_next/static/chunks/app/vault/{page-9afdee66b820c76e.js → page-5150ef17b5383a57.js} +1 -1
  62. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-b9d8ce70792c1672.js +1 -0
  63. package/apps/pwa/out/_next/static/chunks/app/workflows/page-a5a3de231f2cc7d5.js +1 -0
  64. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-3544f154c87b43ee.js → page-134da468538baf48.js} +1 -1
  65. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-31ac4afa2dcd915c.js +1 -0
  66. package/apps/pwa/out/_next/static/chunks/{webpack-539cde99554be20f.js → webpack-c2782b330447ffb6.js} +1 -1
  67. package/apps/pwa/out/_next/static/css/7fa358fec7da7c47.css +1 -0
  68. package/apps/pwa/out/approvals/index.html +1 -1
  69. package/apps/pwa/out/approvals/index.txt +14 -14
  70. package/apps/pwa/out/autopilot/index.html +1 -1
  71. package/apps/pwa/out/autopilot/index.txt +14 -14
  72. package/apps/pwa/out/board/index.html +1 -1
  73. package/apps/pwa/out/board/index.txt +14 -14
  74. package/apps/pwa/out/botlab/index.html +1 -1
  75. package/apps/pwa/out/botlab/index.txt +14 -14
  76. package/apps/pwa/out/bots/index.html +1 -1
  77. package/apps/pwa/out/bots/index.txt +14 -14
  78. package/apps/pwa/out/ceo/index.html +1 -1
  79. package/apps/pwa/out/ceo/index.txt +14 -14
  80. package/apps/pwa/out/chat/index.html +1 -1
  81. package/apps/pwa/out/chat/index.txt +14 -14
  82. package/apps/pwa/out/consult/index.html +1 -1
  83. package/apps/pwa/out/consult/index.txt +14 -14
  84. package/apps/pwa/out/control/index.html +1 -1
  85. package/apps/pwa/out/control/index.txt +14 -14
  86. package/apps/pwa/out/decisions/index.html +1 -0
  87. package/apps/pwa/out/decisions/index.txt +30 -0
  88. package/apps/pwa/out/design-check/index.html +1 -1
  89. package/apps/pwa/out/design-check/index.txt +14 -14
  90. package/apps/pwa/out/editor/index.html +1 -1
  91. package/apps/pwa/out/editor/index.txt +14 -14
  92. package/apps/pwa/out/exec/index.html +1 -1
  93. package/apps/pwa/out/exec/index.txt +14 -14
  94. package/apps/pwa/out/field/index.html +1 -1
  95. package/apps/pwa/out/field/index.txt +14 -14
  96. package/apps/pwa/out/healthz +1 -1
  97. package/apps/pwa/out/index.html +1 -1
  98. package/apps/pwa/out/index.txt +14 -14
  99. package/apps/pwa/out/inside/index.html +1 -1
  100. package/apps/pwa/out/inside/index.txt +14 -14
  101. package/apps/pwa/out/intake/index.html +1 -1
  102. package/apps/pwa/out/intake/index.txt +14 -14
  103. package/apps/pwa/out/live/index.html +1 -1
  104. package/apps/pwa/out/live/index.txt +14 -14
  105. package/apps/pwa/out/loops/index.html +1 -1
  106. package/apps/pwa/out/loops/index.txt +14 -14
  107. package/apps/pwa/out/market/index.html +1 -1
  108. package/apps/pwa/out/market/index.txt +14 -14
  109. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  110. package/apps/pwa/out/missions/placeholder/index.txt +14 -14
  111. package/apps/pwa/out/morning/index.html +1 -1
  112. package/apps/pwa/out/morning/index.txt +14 -14
  113. package/apps/pwa/out/observatory/index.html +1 -1
  114. package/apps/pwa/out/observatory/index.txt +14 -14
  115. package/apps/pwa/out/ops/checklist/index.html +1 -1
  116. package/apps/pwa/out/ops/checklist/index.txt +14 -14
  117. package/apps/pwa/out/ops/release/index.html +1 -1
  118. package/apps/pwa/out/ops/release/index.txt +14 -14
  119. package/apps/pwa/out/ops/seats/index.html +1 -1
  120. package/apps/pwa/out/ops/seats/index.txt +14 -14
  121. package/apps/pwa/out/outputs/index.html +1 -1
  122. package/apps/pwa/out/outputs/index.txt +14 -14
  123. package/apps/pwa/out/reflection/index.html +1 -1
  124. package/apps/pwa/out/reflection/index.txt +15 -15
  125. package/apps/pwa/out/scheduler/index.html +1 -1
  126. package/apps/pwa/out/scheduler/index.txt +14 -14
  127. package/apps/pwa/out/sessions/index.html +1 -1
  128. package/apps/pwa/out/sessions/index.txt +14 -14
  129. package/apps/pwa/out/settings/devices/index.html +1 -1
  130. package/apps/pwa/out/settings/devices/index.txt +14 -14
  131. package/apps/pwa/out/settings/index.html +1 -1
  132. package/apps/pwa/out/settings/index.txt +14 -14
  133. package/apps/pwa/out/setup/done/index.html +1 -1
  134. package/apps/pwa/out/setup/done/index.txt +14 -14
  135. package/apps/pwa/out/setup/index.html +1 -1
  136. package/apps/pwa/out/setup/index.txt +14 -14
  137. package/apps/pwa/out/share/index.html +1 -1
  138. package/apps/pwa/out/share/index.txt +14 -14
  139. package/apps/pwa/out/showroom/index.html +1 -1
  140. package/apps/pwa/out/showroom/index.txt +14 -14
  141. package/apps/pwa/out/tasks/index.html +1 -1
  142. package/apps/pwa/out/tasks/index.txt +14 -14
  143. package/apps/pwa/out/term/index.html +1 -1
  144. package/apps/pwa/out/term/index.txt +14 -14
  145. package/apps/pwa/out/today/index.html +1 -1
  146. package/apps/pwa/out/today/index.txt +14 -14
  147. package/apps/pwa/out/trace/index.html +1 -1
  148. package/apps/pwa/out/trace/index.txt +14 -14
  149. package/apps/pwa/out/vault/index.html +1 -1
  150. package/apps/pwa/out/vault/index.txt +14 -14
  151. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  152. package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
  153. package/apps/pwa/out/workflows/index.html +1 -1
  154. package/apps/pwa/out/workflows/index.txt +14 -14
  155. package/apps/pwa/out/workspace/index.html +1 -1
  156. package/apps/pwa/out/workspace/index.txt +14 -14
  157. package/apps/pwa/out/worktrees/index.html +1 -1
  158. package/apps/pwa/out/worktrees/index.txt +14 -14
  159. package/graphs/intake/intake-daily.yaml +9 -2
  160. package/graphs/intake/news-sources.yaml +4 -0
  161. package/graphs/intake/recipes.yaml +6 -1
  162. package/package.json +2 -1
  163. package/samples/workflows/templates/approval-gated-deploy.yaml +30 -0
  164. package/samples/workflows/templates/chat-driven-research.yaml +35 -0
  165. package/samples/workflows/templates/discord-listen-and-respond.yaml +29 -0
  166. package/samples/workflows/templates/http-poll-then-alert.yaml +30 -0
  167. package/samples/workflows/templates/manual-quickstart.yaml +21 -0
  168. package/samples/workflows/templates/scheduled-daily-digest.yaml +30 -0
  169. package/samples/workflows/templates/webhook-deploy-hook.yaml +27 -0
  170. package/scripts/__fixtures__/intake-news.xml +7 -0
  171. package/scripts/ci-public-export-import-gate.ts +7 -2
  172. package/scripts/ci-typecheck-changed.ts +21 -6
  173. package/scripts/discord-seat-probe.ts +3 -2
  174. package/scripts/docs/generate-cli-anatomy.ts +162 -0
  175. package/scripts/goals/authoring-metrics.ts +73 -0
  176. package/scripts/intake-news.ts +169 -0
  177. package/scripts/landing-heal/must-fix-match.ts +7 -7
  178. package/scripts/landing-heal/run-mission.ts +3 -2
  179. package/scripts/public-export-test-run.ts +37 -2
  180. package/scripts/release-loop/checklist-gate-node.ts +13 -11
  181. package/scripts/release-loop/cut-branch.ts +2 -1
  182. package/scripts/release-loop/cutoff.ts +4 -2
  183. package/scripts/release-loop/docs-land-node.ts +77 -11
  184. package/scripts/release-loop/docs-node.ts +1 -1
  185. package/scripts/release-loop/gate-node.ts +99 -14
  186. package/scripts/release-loop/node-verdict.ts +2 -1
  187. package/scripts/release-loop/npm-publish-node.ts +11 -7
  188. package/scripts/release-loop/ops-upgrade-node.ts +8 -1
  189. package/scripts/release-loop/preflight.ts +246 -0
  190. package/scripts/release-loop/publish-node.ts +10 -3
  191. package/scripts/release-loop/release-docs.ts +134 -0
  192. package/scripts/release-loop/release-readiness.ts +11 -6
  193. package/scripts/release-loop/release-version.ts +48 -0
  194. package/scripts/release-loop/resume-release.ts +115 -0
  195. package/scripts/release-loop/unattended-release.ts +43 -7
  196. package/scripts/release-loop/version-node.ts +149 -41
  197. package/scripts/release-story/draft.ts +42 -14
  198. package/scripts/rhythm/daily.ts +25 -4
  199. package/scripts/skills/public-check.ts +62 -12
  200. package/scripts/test-deterministic.ts +51 -33
  201. package/scripts/test-diet/effectiveness.ts +62 -1
  202. package/scripts/test-diet/lib.ts +1 -1
  203. package/scripts/test-diet/node.ts +8 -1
  204. package/src/agent-mission/driver.ts +53 -10
  205. package/src/agent-mission/review-loop.ts +4 -3
  206. package/src/agent-substrate/pr-comment-meta.ts +20 -0
  207. package/src/autopilot/build/llm-conflict-merge.ts +28 -8
  208. package/src/autopilot/track-agent.ts +5 -3
  209. package/src/briefing/brief-items.ts +86 -17
  210. package/src/cli/autopilot-cli.ts +86 -1
  211. package/src/cli/brief-cli.ts +36 -2
  212. package/src/cli/decisions-cli.ts +4 -1
  213. package/src/cli/freeze-cli.ts +3 -2
  214. package/src/cli/intake-cli.ts +74 -2
  215. package/src/cli/log-event-names.ts +1 -0
  216. package/src/cli/notify-cli.ts +75 -0
  217. package/src/cli/pr-cli.ts +12 -1
  218. package/src/cli/provider-cli.ts +24 -0
  219. package/src/cli/release-cli.ts +101 -17
  220. package/src/cli/self-update-canary.ts +46 -0
  221. package/src/cli/self-update.ts +200 -1
  222. package/src/cli/tasks-cli.ts +309 -3
  223. package/src/context-bus/condense.ts +19 -3
  224. package/src/context-bus/context-day.ts +77 -24
  225. package/src/context-bus/context-first.ts +33 -0
  226. package/src/context-card/index.ts +45 -0
  227. package/src/context-card/task.ts +76 -0
  228. package/src/dashboard/slash-runtime/dashboard-handlers.ts +2 -0
  229. package/src/decisions/decision-ledger.ts +10 -1
  230. package/src/discord-self-message.ts +20 -2
  231. package/src/discord-slash-wire.ts +1 -0
  232. package/src/domains/outbound-alert.ts +200 -27
  233. package/src/domains/outbound-log-probe.ts +16 -1
  234. package/src/domains/release-tool.ts +2 -2
  235. package/src/execution-loop/gates.ts +11 -2
  236. package/src/flow-loop/split.ts +8 -4
  237. package/src/git-fs/worktree.ts +23 -3
  238. package/src/harness/harness-cli-command.ts +151 -11
  239. package/src/harness/harness-pod-dispatch.ts +11 -0
  240. package/src/harness/harness-queue.ts +466 -32
  241. package/src/harness/helper-scan.ts +10 -3
  242. package/src/harness/nested-elanous-policy.ts +23 -4
  243. package/src/hq/fence-wrapper.ts +123 -0
  244. package/src/hq/hq.ts +8 -0
  245. package/src/hq/seats.ts +369 -0
  246. package/src/index.ts +190 -23
  247. package/src/intake-plane/adopt-gate.ts +49 -0
  248. package/src/intake-plane/check.ts +3 -0
  249. package/src/intake-plane/collect-github.ts +57 -25
  250. package/src/intake-plane/digest.ts +39 -4
  251. package/src/intake-plane/discord-seat-work.ts +8 -2
  252. package/src/intake-plane/requirement-funnel.ts +316 -0
  253. package/src/knowledge/know-find.ts +10 -1
  254. package/src/loops/budget.ts +36 -0
  255. package/src/loops/orchestrator/finish-rate.ts +73 -1
  256. package/src/loops/orchestrator/grid-place.ts +11 -0
  257. package/src/loops/orchestrator/tick.ts +295 -20
  258. package/src/maturity/feature-maturity.ts +3 -2
  259. package/src/nexus/api/decisions-route.ts +11 -1
  260. package/src/nexus/api/grid.ts +61 -0
  261. package/src/nexus/api/http-server.ts +19 -1
  262. package/src/nexus/api/loop-edges.ts +69 -13
  263. package/src/nexus/api/ops-seats-sources.ts +2 -2
  264. package/src/nexus/api/task-cards-api.ts +19 -0
  265. package/src/nexus/api/workflow-templates.ts +3 -0
  266. package/src/oauth/codex.ts +120 -2
  267. package/src/onboarding/first-chat-gate.ts +29 -0
  268. package/src/release-loop/checklist.ts +104 -6
  269. package/src/release-loop/feature-store.ts +10 -9
  270. package/src/release-loop/landing-freeze.ts +24 -14
  271. package/src/release-loop/placement.ts +13 -3
  272. package/src/release-loop/rubric.ts +80 -0
  273. package/src/seat-dispatch/seat-ask.ts +79 -16
  274. package/src/seat-loop/seat-loop.ts +107 -37
  275. package/src/self-dev/ask-launch-flow.ts +50 -10
  276. package/src/self-dev/ask-path-candidates.ts +68 -0
  277. package/src/self-dev/dev-pipeline.ts +4 -0
  278. package/src/self-dev/draft-triage-rules.ts +1 -1
  279. package/src/self-dev/orchestrate-cli.ts +22 -0
  280. package/src/self-dev/round-pr-supersede.ts +244 -0
  281. package/src/self-dev/run-store.ts +5 -1
  282. package/src/self-dev/run-supervisor.ts +21 -1
  283. package/src/self-dev/supervisor-verdict-edges.ts +46 -0
  284. package/src/self-implement/follow-up-goals.ts +2 -2
  285. package/src/self-implement/frozen-merges.ts +25 -20
  286. package/src/self-implement/gate-remote.ts +427 -0
  287. package/src/self-implement/goal-author-clarification.ts +81 -0
  288. package/src/self-implement/goal-author-cli.ts +33 -5
  289. package/src/self-implement/goal-author.ts +93 -12
  290. package/src/self-implement/goal-clarification-escalation.ts +37 -1
  291. package/src/self-implement/goal-document-write.ts +28 -0
  292. package/src/self-implement/heal-intake.ts +4 -0
  293. package/src/self-implement/host-regate.ts +3 -1
  294. package/src/self-implement/l8-merge-queue.ts +29 -5
  295. package/src/self-implement/orchestrator.ts +78 -13
  296. package/src/self-implement/prior-draft-findings.ts +4 -3
  297. package/src/skills/skill-feedback.ts +76 -0
  298. package/src/task-agent/actions.ts +151 -0
  299. package/src/task-agent/judge.ts +122 -0
  300. package/src/task-agent/mission.ts +423 -0
  301. package/src/task-agent/shadow.ts +90 -0
  302. package/src/task-agent/task-hand.ts +235 -0
  303. package/src/task-cards/card-store.ts +4 -1
  304. package/src/task-orchestrator/surfaces/pod-failure-reason.ts +43 -4
  305. package/src/task-orchestrator/surfaces/pod-lease.ts +5 -2
  306. package/src/task-orchestrator/surfaces/pod-pool.ts +81 -13
  307. package/src/task-orchestrator/surfaces/self-implement-pod.ts +225 -35
  308. package/src/telegram-commands.ts +1 -1
  309. package/src/telegram-loops-command.ts +22 -15
  310. package/src/telegram.ts +5 -20
  311. package/src/user-config.ts +86 -4
  312. package/src/version/packed-revision.json +1 -1
  313. package/apps/pwa/out/_next/static/chunks/2699-63b73fa4d3bcf6ff.js +0 -1
  314. package/apps/pwa/out/_next/static/chunks/4554-3565eea1f1495ba7.js +0 -1
  315. package/apps/pwa/out/_next/static/chunks/4614-21403aac730d4586.js +0 -1
  316. package/apps/pwa/out/_next/static/chunks/5135-44008fe8b1f2aa41.js +0 -1
  317. package/apps/pwa/out/_next/static/chunks/5233-bf28bd6bd7a32099.js +0 -1
  318. package/apps/pwa/out/_next/static/chunks/5977-c952712c87a1f757.js +0 -1
  319. package/apps/pwa/out/_next/static/chunks/6877-7c6208705c284124.js +0 -1
  320. package/apps/pwa/out/_next/static/chunks/7395-164e238f471b5d7b.js +0 -1
  321. package/apps/pwa/out/_next/static/chunks/7787-02a1c0b04e25e811.js +0 -1
  322. package/apps/pwa/out/_next/static/chunks/8827-e2f5b09a8d37e83b.js +0 -1
  323. package/apps/pwa/out/_next/static/chunks/8949-ecb4128a0858b911.js +0 -1
  324. package/apps/pwa/out/_next/static/chunks/899-46946fe8363e1262.js +0 -1
  325. package/apps/pwa/out/_next/static/chunks/9247-29d889b809288e65.js +0 -1
  326. package/apps/pwa/out/_next/static/chunks/app/approvals/page-79c3ed84baaedd03.js +0 -1
  327. package/apps/pwa/out/_next/static/chunks/app/board/page-a424ef80885d1743.js +0 -1
  328. package/apps/pwa/out/_next/static/chunks/app/ceo/page-1e962b31d1f11081.js +0 -1
  329. package/apps/pwa/out/_next/static/chunks/app/chat/page-8c2d9526164f5d09.js +0 -1
  330. package/apps/pwa/out/_next/static/chunks/app/consult/page-ffff6d819fbc6e0f.js +0 -1
  331. package/apps/pwa/out/_next/static/chunks/app/inside/page-76270efaee638f17.js +0 -1
  332. package/apps/pwa/out/_next/static/chunks/app/layout-dce686e582c23937.js +0 -1
  333. package/apps/pwa/out/_next/static/chunks/app/live/page-d3d53b146416e422.js +0 -1
  334. package/apps/pwa/out/_next/static/chunks/app/loops/page-1a61c7ca1ceb3ae7.js +0 -1
  335. package/apps/pwa/out/_next/static/chunks/app/market/page-51f811fa119308b6.js +0 -1
  336. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-dacff46049e1557e.js +0 -1
  337. package/apps/pwa/out/_next/static/chunks/app/observatory/page-36a7d994f4ca0299.js +0 -1
  338. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-5b042736aa113756.js +0 -1
  339. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-acb749f1e432bcf3.js +0 -1
  340. package/apps/pwa/out/_next/static/chunks/app/outputs/page-cd78862707999911.js +0 -1
  341. package/apps/pwa/out/_next/static/chunks/app/page-54b9ef8afe6278fd.js +0 -1
  342. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-57924393e3e4e539.js +0 -1
  343. package/apps/pwa/out/_next/static/chunks/app/sessions/page-9e0ce6c8129fa641.js +0 -1
  344. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-31fa1ce4c4aff9ae.js +0 -1
  345. package/apps/pwa/out/_next/static/chunks/app/settings/page-0f5cd57ca928f42b.js +0 -1
  346. package/apps/pwa/out/_next/static/chunks/app/setup/page-4364ef9f4e7c7f0f.js +0 -1
  347. package/apps/pwa/out/_next/static/chunks/app/showroom/page-59f03fda76c03d22.js +0 -1
  348. package/apps/pwa/out/_next/static/chunks/app/trace/page-86914b0dee8ab7f8.js +0 -1
  349. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-d4aef67abf264874.js +0 -1
  350. package/apps/pwa/out/_next/static/chunks/app/workflows/page-1ea64acadd704ace.js +0 -1
  351. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-30c2073bfc21c004.js +0 -1
  352. package/apps/pwa/out/_next/static/css/001d5fbaee388a25.css +0 -1
  353. package/apps/pwa/out/_next/static/rv9cierV0jSOvDBm1g0XB/_buildManifest.js +0 -1
  354. /package/apps/pwa/out/_next/static/{rv9cierV0jSOvDBm1g0XB → P6YTVgzuZz5oTZhY4Din_}/_ssgManifest.js +0 -0
@@ -6,26 +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;
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;
25
30
  };
26
31
  export type QueuePool = { running: number; pending: number; reserved: number; limit: number };
27
32
  export type QueueTick = { outcome: 'launched' | 'waiting' | 'skipped'; item?: QueueItem; reason: string };
28
- 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[] };
29
37
  type ProcessProbe = {
30
38
  run?: typeof spawnSync; platform?: NodeJS.Platform;
31
39
  cwd?: (pid: number) => string;
@@ -53,6 +61,10 @@ export interface HarnessQueueDeps {
53
61
  cap?: (seat: QueueSeat) => number;
54
62
  pool?: () => QueuePool;
55
63
  launch?: (item: QueueItem, args: string[], root: string) => Promise<number>;
64
+ /** Test seam: runs after a direct CLI call enqueued its row and before that call's own tick (race tests). */
65
+ afterEnqueue?: (item: QueueItem) => void | Promise<void>;
66
+ /** Script the default launcher runs under the queue child wrapper (default bin/elanous.mjs). */
67
+ launchCommand?: string;
56
68
  alive?: (pid: number) => boolean;
57
69
  processes?: () => readonly QueueProcess[];
58
70
  receipt?: (root: string, launchId: string) => 'started' | 'finished' | 'not-started' | null;
@@ -63,8 +75,13 @@ export interface HarnessQueueDeps {
63
75
  next: readonly Pick<ChecklistItem, 'id' | 'title' | 'owner' | 'status' | 'evidence'>[] };
64
76
  idleLog?: (category: string, event: string, data: Record<string, unknown>) => void;
65
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;
66
80
  /** FINISH-RATE metrics source; a test process without it skips the real measurement. */
67
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>;
68
85
  }
69
86
 
70
87
  const FINISH_CACHE_MS = 10 * 60 * 1_000;
@@ -100,6 +117,11 @@ async function cachedFinishMetrics(root: string, now: Date, deps: HarnessQueueDe
100
117
  mkdirSync(join(root, 'harness'), { recursive: true });
101
118
  writeFileSync(path, JSON.stringify({ at: now.toISOString(), metrics }));
102
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
+ }
103
125
  return metrics;
104
126
  }
105
127
 
@@ -143,6 +165,12 @@ function read(path: string): QueueItem[] {
143
165
  && (row.pid === undefined || Number.isSafeInteger(row.pid))
144
166
  && (row.launchId === undefined || typeof row.launchId === 'string')
145
167
  && (row.idempotencyKey === undefined || typeof row.idempotencyKey === 'string')
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')
171
+ && (row.launchCwd === undefined || typeof row.launchCwd === 'string' && row.launchCwd.startsWith('/'))
172
+ && (row.cellId === undefined || typeof row.cellId === 'string')
173
+ && (row.priority === undefined || typeof row.priority === 'number' && Number.isFinite(row.priority))
146
174
  && (row.waitingReason === undefined || typeof row.waitingReason === 'string'))) {
147
175
  throw new Error('harness queue: invalid queue file (no launch)');
148
176
  }
@@ -207,7 +235,67 @@ function seat(value: string): QueueSeat {
207
235
  return value as QueueSeat;
208
236
  }
209
237
 
210
- export async function addHarnessQueue(input: { seat: string; say?: string; ask?: string; hold?: boolean; heavy?: boolean; idempotencyKey?: string }, deps: HarnessQueueDeps = {}): Promise<QueueItem> {
238
+ /** Cell id named in a goal text (`칸: ONEDOOR-2` · `칸 ONEDOOR-2`) — ids carry at least one hyphen. */
239
+ export function queueCellId(text: string): string | undefined {
240
+ return /칸[:\s]\s*([A-Z][A-Z0-9]*(?:-[A-Z0-9]+)+)/u.exec(text)?.[1];
241
+ }
242
+
243
+ /** A say row names its cell in the sentence; an ask row names it inside the goal document (unreadable → none). */
244
+ function queueRowCellId(kind: QueueItem['kind'], input: string): string | undefined {
245
+ if (kind === 'say') return queueCellId(input);
246
+ try { return queueCellId(readFileSync(input, 'utf8')); } catch { return undefined; }
247
+ }
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
+
292
+ export class HarnessQueueDuplicateError extends Error {
293
+ constructor(existing: QueueItem, key: string) {
294
+ super(`같은 ${key} 이 이미 대기열에 있거나 도는 중이다 — ${existing.id} (${existing.status})`);
295
+ }
296
+ }
297
+
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> {
211
299
  const assigned = seat(input.seat);
212
300
  if ((input.say === undefined) === (input.ask === undefined)) throw new Error('harness queue add: --say 또는 --ask 중 하나만 필요');
213
301
  const kind = input.ask === undefined ? 'say' : 'ask';
@@ -228,11 +316,30 @@ export async function addHarnessQueue(input: { seat: string; say?: string; ask?:
228
316
  return existing;
229
317
  }
230
318
  }
319
+ // Recorded at enqueue: a later edit or deletion of an ask document cannot reopen its cell.
320
+ const cellId = queueRowCellId(kind, value);
321
+ if (input.refuseDuplicate) {
322
+ const cell = cellId;
323
+ const existing = rows.find((row) => row.status !== 'finished'
324
+ && !['retryable', 'succeeded'].includes(queueRowOutcome(row, root))
325
+ && ((row.kind === kind && row.input === value) || (cell !== undefined && (row.cellId ?? queueRowCellId(row.kind, row.input)) === cell)));
326
+ if (existing) {
327
+ const key = existing.kind === kind && existing.input === value ? '골' : `칸 ${cell}`;
328
+ observe('skipped', { id: existing.id, seat: assigned, reason: 'duplicate', key }, deps);
329
+ throw new HarnessQueueDuplicateError(existing, key);
330
+ }
331
+ }
332
+ const priority = defaultQueuePriority(cellId, root, deps);
231
333
  const item: QueueItem = { id: `hq-${randomUUID()}`, seat: assigned, kind, input: value,
232
334
  hold: input.hold === true, heavy: input.heavy === true, at: new Date().toISOString(), status: 'queued',
335
+ ...(input.launchArgs === undefined ? {} : { launchArgs: input.launchArgs }),
336
+ ...(input.poolHint === undefined ? {} : { poolHint: input.poolHint, ...(input.poolHintSource ? { poolHintSource: input.poolHintSource } : {}) }),
337
+ ...(input.launchCwd === undefined ? {} : { launchCwd: input.launchCwd }),
338
+ ...(cellId === undefined ? {} : { cellId }),
339
+ ...(priority === undefined ? {} : { priority }),
233
340
  ...(input.idempotencyKey === undefined ? {} : { idempotencyKey: input.idempotencyKey }) };
234
341
  save(path, [...rows, item]);
235
- observe('enqueued', { id: item.id, seat: item.seat, kind }, deps);
342
+ observe('enqueued', { id: item.id, seat: item.seat, kind, ...(priority === undefined ? {} : { priority }) }, deps);
236
343
  return item;
237
344
  });
238
345
  }
@@ -295,6 +402,46 @@ function queueRowOutcome(row: QueueItem, root: string): 'pending' | 'succeeded'
295
402
  return state === 'started' ? 'pending' : 'unknown';
296
403
  }
297
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
+
298
445
  export async function removeHarnessQueue(id: string, deps: HarnessQueueDeps = {}): Promise<boolean> {
299
446
  const root = deps.root ?? effectiveInstanceRoot();
300
447
  return locked(root, async (path) => {
@@ -317,18 +464,28 @@ export async function removeHarnessQueue(id: string, deps: HarnessQueueDeps = {}
317
464
  });
318
465
  }
319
466
 
320
- export function queueLaunchArgs(item: QueueItem): string[] {
321
- 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',
322
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];
323
476
  }
324
477
 
325
- export function readHarnessQueuePool(): QueuePool {
478
+ function harnessQueuePoolMembers(): ReturnType<typeof parsePodPool> {
326
479
  const config = getUserConfig();
327
480
  const spec = resolvePodPoolSpec(undefined, process.env, () => config.harness?.podPool ?? config.pod?.pool);
328
481
  const current = spec ? null : leaseKubectl(['config', 'current-context']);
329
482
  const context = spec ?? (current?.status === 0 ? current.stdout.trim() : '');
330
483
  if (!context) throw new Error('pod lease status: no pool/context');
331
- const members = parsePodPool(context);
484
+ return parsePodPool(context);
485
+ }
486
+
487
+ export function readHarnessQueuePool(): QueuePool {
488
+ const members = harnessQueuePoolMembers();
332
489
  const observed = measurePoolLease(members);
333
490
  if (observed.members.some((member) => member.running === null || member.pending === null)) throw new Error('pod lease status: incomplete running/pending measurement');
334
491
  const pendingJobs = observed.members.flatMap((member) => member.pendingJobs ?? []);
@@ -341,6 +498,41 @@ export function readHarnessQueuePool(): QueuePool {
341
498
  };
342
499
  }
343
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
+
344
536
  function alive(pid: number): boolean {
345
537
  try { process.kill(pid, 0); return true; } catch { return false; }
346
538
  }
@@ -378,14 +570,25 @@ export function readHarnessQueueProcesses(probe: ProcessProbe = {}): readonly Qu
378
570
  const ambiguousPids = new Set<number>();
379
571
  const candidatePids = new Set(candidates.map(({ pid }) => pid));
380
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>>();
381
575
  for (const record of listSelfDevRuns(probe.runsDir ?? selfDevRunsDir())) {
382
576
  const pid = record.pid;
383
- if (!pid || !candidatePids.has(pid) || !record.pidStart || !queueSeatNames.some((name) => name === record.seat)) continue;
577
+ if (!pid || !candidatePids.has(pid) || !record.pidStart) continue;
384
578
  if (!births.has(pid)) {
385
579
  try { births.set(pid, (probe.birthId ?? ((id: number) => processBirthId(id, platform)))(pid)); }
386
580
  catch { births.set(pid, undefined); }
387
581
  }
388
- 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;
389
592
  const prior = ledgerSeats.get(pid);
390
593
  if (prior && prior !== record.seat) { ledgerSeats.delete(pid); ambiguousPids.add(pid); }
391
594
  else ledgerSeats.set(pid, record.seat!);
@@ -404,6 +607,8 @@ export function readHarnessQueueProcesses(probe: ProcessProbe = {}): readonly Qu
404
607
  let launchId: string | undefined;
405
608
  let runId: string | undefined;
406
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];
407
612
  let envSeat: string | undefined;
408
613
  if (platform === 'linux') {
409
614
  try {
@@ -445,10 +650,16 @@ export function readHarnessQueueProcesses(probe: ProcessProbe = {}): readonly Qu
445
650
  const top = eligible.filter((entry) => entry.depth === depth);
446
651
  const seats = new Set(top.map((entry) => entry.seat));
447
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) ?? [])]))];
448
658
  try { debug.log('harness.queue', 'attributed', { pid: root.pid, seat: assigned?.seat ?? null, source: assigned?.source ?? 'all' }); }
449
659
  catch { /* Observation does not block inventory. */ }
450
660
  rows.push({ pid: root.pid, ...(assigned ? { seat: assigned.seat } : {}), ...(launchId ? { launchId } : {}),
451
- ...(runId ? { runId } : {}) });
661
+ ...(runId ? { runId } : {}), ...(progressAt !== undefined ? { progressAt } : {}), ...(stopReason ? { stopReason } : {}),
662
+ ...(runIds.length ? { ledgerRunIds: runIds } : {}) });
452
663
  }
453
664
  return rows;
454
665
  }
@@ -468,15 +679,36 @@ function receipt(root: string, launchId: string): 'started' | 'finished' | 'not-
468
679
  }
469
680
  }
470
681
 
471
- async function launch(item: QueueItem, args: string[], root: string): Promise<number> {
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
+
699
+ function defaultLaunch(command?: string) {
700
+ return (item: QueueItem, args: string[], root: string) => launch(item, args, root, command);
701
+ }
702
+
703
+ async function launch(item: QueueItem, args: string[], root: string, command?: string): Promise<number> {
472
704
  const repo = resolve(import.meta.dir, '../..');
473
705
  const logPath = join(root, 'harness', `${item.id}.log`);
474
706
  const fd = openSync(logPath, 'a', 0o600);
475
707
  try {
476
708
  const child = spawn(process.execPath, [join(repo, 'src', 'harness', 'harness-queue-child.ts'),
477
- harnessQueueReceiptPath(root, item.launchId!), join(repo, 'bin', 'elanous.mjs'),
709
+ harnessQueueReceiptPath(root, item.launchId!), command ?? join(repo, 'bin', 'elanous.mjs'),
478
710
  ...(resolve(root) === prodInstanceRoot() ? [] : [`--test=${root}`]), ...args], {
479
- cwd: repo, detached: true, stdio: ['ignore', fd, fd],
711
+ cwd: item.launchCwd ?? repo, detached: true, stdio: ['ignore', fd, fd],
480
712
  env: { ...process.env, ELANOUS_STATE_DIR: root, ELANOUS_HARNESS_SEAT: item.seat,
481
713
  ELANOUS_HARNESS_QUEUE_LAUNCH: item.launchId! },
482
714
  });
@@ -536,18 +768,23 @@ function warnLegacySeatCap(path: string): void {
536
768
  } catch { /* An unreadable config cannot affect dispatch or observation. */ }
537
769
  }
538
770
 
539
- function groupQueueRuns(processes: readonly QueueProcess[]): { seat?: QueueSeat; launchIds: Set<string> }[] {
540
- 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[] }[] = [];
541
777
  for (const row of processes) {
542
778
  const keys = [row.runId && `run:${row.runId}`, `root:${row.rootPid ?? row.pid}`,
543
779
  row.launchId && `launch:${row.launchId}`, `pid:${row.pid}`].filter((key): key is string => Boolean(key));
544
780
  const matching = groups.filter((group) => keys.some((key) => group.keys.has(key)));
545
781
  const group = matching[0] ?? { seats: new Set<QueueSeat>(), unknown: false,
546
- launchIds: new Set<string>(), keys: new Set<string>() };
782
+ launchIds: new Set<string>(), keys: new Set<string>(), rows: [] };
547
783
  for (const other of matching.slice(1)) {
548
784
  for (const key of other.keys) group.keys.add(key);
549
785
  for (const seat of other.seats) group.seats.add(seat);
550
786
  for (const id of other.launchIds) group.launchIds.add(id);
787
+ group.rows.push(...other.rows);
551
788
  group.unknown ||= other.unknown;
552
789
  groups.splice(groups.indexOf(other), 1);
553
790
  }
@@ -555,12 +792,40 @@ function groupQueueRuns(processes: readonly QueueProcess[]): { seat?: QueueSeat;
555
792
  if (row.seat) group.seats.add(row.seat);
556
793
  else group.unknown = true;
557
794
  if (row.launchId) group.launchIds.add(row.launchId);
795
+ group.rows.push(row);
558
796
  if (!matching.length) groups.push(group);
559
797
  }
560
- return groups.map((group) => ({
561
- ...(group.unknown || group.seats.size !== 1 ? {} : { seat: [...group.seats][0] }),
562
- launchIds: group.launchIds,
563
- }));
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
+ };
564
829
  }
565
830
 
566
831
  const IDLE_REQUEST_INTERVAL_MS = 15 * 60_000;
@@ -675,7 +940,16 @@ export function requestIdleSeats(root: string, now: Date, items: readonly QueueI
675
940
  }
676
941
  }
677
942
 
678
- export async function tickHarnessQueue(deps: HarnessQueueDeps = {}): Promise<QueueTick> {
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
+
952
+ export async function tickHarnessQueue(deps: HarnessQueueDeps = {}, requestedId?: string): Promise<QueueTick> {
679
953
  const root = deps.root ?? effectiveInstanceRoot();
680
954
  return locked(root, async (path) => {
681
955
  try {
@@ -692,17 +966,47 @@ export async function tickHarnessQueue(deps: HarnessQueueDeps = {}): Promise<Que
692
966
  catch { /* Shadow observation cannot affect queue dispatch. */ }
693
967
  }
694
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
+ }
695
985
  try { await (deps.idleRequest ?? requestIdleSeats)(root, (deps.now ?? (() => new Date()))(), items, deps); }
696
986
  catch (error) {
697
987
  try { debug.log('loop.orchestrator', 'idle-request-failed', { reason: String(error) }); }
698
988
  catch { /* Idle observation cannot affect queue dispatch. */ }
699
989
  }
700
990
  const heads = new Map<QueueSeat, QueueItem>();
701
- 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
+ }
996
+ if (requestedId && ![...heads.values()].some((row) => row.id === requestedId)) {
997
+ const row = items.find((item) => item.id === requestedId);
998
+ if (!row || row.status !== 'queued') return { outcome: 'skipped', item: row, reason: '이미 처리됨 또는 항목 없음' };
999
+ const reason = '앞선 대기열 항목 차례';
1000
+ if (row.waitingReason !== reason) save(path, items.map((item) => item.id === requestedId ? { ...item, waitingReason: reason } : item));
1001
+ observe('waiting', { id: row.id, seat: row.seat, reason }, deps);
1002
+ return { outcome: 'waiting', item: { ...row, waitingReason: reason }, reason };
1003
+ }
702
1004
  if (!heads.size) { observe('skipped', { reason: 'empty' }, deps); return { outcome: 'skipped', reason: 'empty' }; }
703
1005
  const lastSeat = lastLaunchedSeat(path);
704
1006
  const first = lastSeat ? (queueSeatNames.indexOf(lastSeat) + 1) % queueSeatNames.length
705
1007
  : queueSeatNames.indexOf(items.find((row) => row.status === 'queued')!.seat);
1008
+ // A requested tick (direct CLI call) keeps the seat round-robin: it launches its row only when that row is the first
1009
+ // head with room; an earlier seat's head with room means the requested row waits for its turn.
706
1010
  const candidates = [...queueSeatNames.slice(first), ...queueSeatNames.slice(0, first)]
707
1011
  .flatMap((name) => heads.get(name) ? [heads.get(name)!] : []);
708
1012
  const config = deps.configPath === undefined ? getUserConfig() : getUserConfig(deps.configPath);
@@ -729,7 +1033,9 @@ export async function tickHarnessQueue(deps: HarnessQueueDeps = {}): Promise<Que
729
1033
  ...(details.injectedCap === undefined ? {} : { injectedCap: details.injectedCap }), capKeys: details.winners,
730
1034
  attributed, unattributed }, deps);
731
1035
  }
732
- return { outcome: 'waiting', item: { ...candidates[0]!, waitingReason: blocked.get(candidates[0]!.seat)!.reason }, reason };
1036
+ const subject = (requestedId ? candidates.find((item) => item.id === requestedId) : undefined) ?? candidates[0]!;
1037
+ const subjectReason = blocked.get(subject.seat)!.reason;
1038
+ return { outcome: 'waiting', item: { ...subject, waitingReason: subjectReason }, reason: requestedId ? subjectReason : reason };
733
1039
  };
734
1040
  for (const item of candidates) {
735
1041
  const injectedCap = deps.cap?.(item.seat);
@@ -751,6 +1057,109 @@ export async function tickHarnessQueue(deps: HarnessQueueDeps = {}): Promise<Que
751
1057
  if (run.seat) attributed[run.seat]++;
752
1058
  else unattributed++;
753
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
+ };
754
1163
  current = items.flatMap((row) => {
755
1164
  if ((row.status !== 'launched' && row.status !== 'launching') || !row.launchId) return [row];
756
1165
  const state = (deps.receipt ?? receipt)(root, row.launchId);
@@ -762,17 +1171,15 @@ export async function tickHarnessQueue(deps: HarnessQueueDeps = {}): Promise<Que
762
1171
  return [];
763
1172
  });
764
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);
765
1176
  let pool: QueuePool | undefined;
766
1177
  let poolReason: string | undefined;
767
1178
  let finishHeld: string | null | undefined;
768
1179
  for (const item of candidates) {
769
1180
  const injectedCap = deps.cap?.(item.seat);
770
1181
  const details = seatCapDetails({ seat: item.seat, caps, gate, injectedCap });
771
- const charged = runs.filter((run) => !run.seat || run.seat === item.seat);
772
- const launchIds = new Set(charged.flatMap((run) => [...run.launchIds]));
773
- const active = charged.length + current.filter((row) => row.seat === item.seat &&
774
- (row.status === 'launching' || row.status === 'launched')
775
- && (!row.launchId || !launchIds.has(row.launchId))).length;
1182
+ const active = seatActive(item.seat);
776
1183
  if (active >= details.cap) { blocked.set(item.seat, { reason: seatCapReason(item.seat, active, details), active }); continue; }
777
1184
  if (!pool && !poolReason) {
778
1185
  try { pool = (deps.pool ?? readHarnessQueuePool)(); }
@@ -799,11 +1206,38 @@ export async function tickHarnessQueue(deps: HarnessQueueDeps = {}): Promise<Que
799
1206
  + current.filter((row) => row.status === 'launching' || row.status === 'launched').length, pool!.limit, deps);
800
1207
  }
801
1208
  if (finishHeld) { blocked.set(item.seat, { reason: finishHeld, active }); continue; }
802
- const launching: QueueItem = { ...item, status: 'launching', launchId: `hq-${randomUUID()}` };
1209
+ if (requestedId && item.id !== requestedId) {
1210
+ const requested = current.find((row) => row.id === requestedId)!;
1211
+ // The requested row's own block is the truthful reason — even when its seat comes later in the rotation and was not
1212
+ // evaluated yet; «another seat's turn» only when the requested seat itself has room.
1213
+ const own = blocked.get(requested.seat)?.reason ?? (() => {
1214
+ const ownCap = deps.cap?.(requested.seat);
1215
+ const details = seatCapDetails({ seat: requested.seat, caps, gate, injectedCap: ownCap });
1216
+ const active = seatActive(requested.seat);
1217
+ if (active >= details.cap) return seatCapReason(requested.seat, active, details);
1218
+ const budget = decideSpawn({ seat: requested.seat, running: active, caps, gate, injectedCap: ownCap });
1219
+ return budget.allow ? undefined : `${budget.reason}: ${seatCapReason(requested.seat, active, details)}`;
1220
+ })();
1221
+ const reason = own ?? `다른 자리 차례 — ${item.seat}`;
1222
+ if (requested.waitingReason !== reason) save(path, current.map((row) => row.id === requestedId ? { ...row, waitingReason: reason } : row));
1223
+ observe('waiting', { id: requested.id, seat: requested.seat, reason }, deps);
1224
+ return { outcome: 'waiting', item: { ...requested, waitingReason: reason }, reason };
1225
+ }
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()}` };
803
1237
  delete launching.waitingReason;
804
1238
  save(path, current.map((row) => row.id === item.id ? launching : row));
805
1239
  try {
806
- const pid = await (deps.launch ?? launch)(launching, queueLaunchArgs(launching), root);
1240
+ const pid = await (deps.launch ?? defaultLaunch(deps.launchCommand))(launching, queueLaunchArgs(launching), root);
807
1241
  if (!Number.isSafeInteger(pid) || pid < 1) throw new Error('launcher returned no pid');
808
1242
  const launched: QueueItem = { ...launching, pid, status: 'launched' };
809
1243
  save(path, current.map((row) => row.id === item.id ? launched : row));