elanous 0.2.14 → 0.2.16

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 (350) 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/chunks/{1307-6e7e0377c1db0bdc.js → 1307-b46d45b705e8c9ae.js} +1 -1
  4. package/apps/pwa/out/_next/static/chunks/5977-c952712c87a1f757.js +1 -0
  5. package/apps/pwa/out/_next/static/chunks/899-46946fe8363e1262.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/{9103-7a3ae7f60af3b99f.js → 9103-60fbef33e66f0a46.js} +1 -1
  7. package/apps/pwa/out/_next/static/chunks/app/approvals/page-79c3ed84baaedd03.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-7b2b4f378354869a.js → page-3cb52e5ea8fc2fa5.js} +1 -1
  9. package/apps/pwa/out/_next/static/chunks/app/board/page-a424ef80885d1743.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/app/botlab/page-c1e20d7d338713f1.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/app/bots/{page-1512d91c7491ed62.js → page-482c5dbde699947c.js} +1 -1
  12. package/apps/pwa/out/_next/static/chunks/app/ceo/page-1e962b31d1f11081.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/app/chat/{page-a627db94ec6904a0.js → page-8c2d9526164f5d09.js} +1 -1
  14. package/apps/pwa/out/_next/static/chunks/app/consult/page-ffff6d819fbc6e0f.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/app/control/{page-e6b84ab07d3567d5.js → page-a0e2a6d1bfb52fc5.js} +1 -1
  16. package/apps/pwa/out/_next/static/chunks/app/design-check/page-c53598cfce6156f2.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/app/editor/{page-fed69f9759d248d0.js → page-13a63953f53c6a23.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/exec/{page-15374909d8f1aebd.js → page-5d3c8429d04f3831.js} +1 -1
  19. package/apps/pwa/out/_next/static/chunks/app/field/{page-012a742f81218b01.js → page-3193bdc0bbab047a.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/inside/page-76270efaee638f17.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/app/intake/{page-99effd9d893a43dc.js → page-6107ca8ddd5e3bdd.js} +1 -1
  22. package/apps/pwa/out/_next/static/chunks/app/{layout-938de57ae0315e46.js → layout-dce686e582c23937.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/app/live/page-d3d53b146416e422.js +1 -0
  24. package/apps/pwa/out/_next/static/chunks/app/loops/{page-160089123a40dd91.js → page-1a61c7ca1ceb3ae7.js} +1 -1
  25. package/apps/pwa/out/_next/static/chunks/app/market/{page-629e1ede482ec6eb.js → page-51f811fa119308b6.js} +1 -1
  26. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-dacff46049e1557e.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/app/morning/{page-27e99b4e54a849d4.js → page-4167348049166852.js} +1 -1
  28. package/apps/pwa/out/_next/static/chunks/app/not-found-de52e8fdf9095f7a.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/app/observatory/page-36a7d994f4ca0299.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-32cde713be47cddb.js → page-ca3952cd78a83ca2.js} +1 -1
  31. package/apps/pwa/out/_next/static/chunks/app/ops/release/{page-e7e10cd3386222e5.js → page-5b042736aa113756.js} +1 -1
  32. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-acb749f1e432bcf3.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/app/outputs/page-cd78862707999911.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/app/page-54b9ef8afe6278fd.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/app/reflection/{page-e64caff084f7ce21.js → page-1ebfe049c640e171.js} +1 -1
  36. package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-db07eaffba0df7e3.js → page-57924393e3e4e539.js} +1 -1
  37. package/apps/pwa/out/_next/static/chunks/app/sessions/page-9e0ce6c8129fa641.js +1 -0
  38. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-31fa1ce4c4aff9ae.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/app/settings/{page-86f91034bd5cabe5.js → page-0f5cd57ca928f42b.js} +1 -1
  40. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-abe8e3b74bea1ffc.js +1 -0
  41. package/apps/pwa/out/_next/static/chunks/app/setup/page-4364ef9f4e7c7f0f.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/app/share/{page-ce4d8a1215ce53a3.js → page-d28573d6792b4275.js} +1 -1
  43. package/apps/pwa/out/_next/static/chunks/app/showroom/{page-58043db78ba519ae.js → page-59f03fda76c03d22.js} +1 -1
  44. package/apps/pwa/out/_next/static/chunks/app/tasks/{page-357750dfb91bf960.js → page-d751de5f8062ff5e.js} +1 -1
  45. package/apps/pwa/out/_next/static/chunks/app/term/{page-274703bc41b71333.js → page-fe1759f94003b32b.js} +1 -1
  46. package/apps/pwa/out/_next/static/chunks/app/today/page-d64c984e855c8c68.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/app/trace/page-86914b0dee8ab7f8.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/app/vault/{page-3b7e821dc492bdfd.js → page-9afdee66b820c76e.js} +1 -1
  49. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-d4aef67abf264874.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/workflows/{page-f8da5dc6f3752276.js → page-1ea64acadd704ace.js} +1 -1
  51. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-2f810b18d89ecba6.js → page-3544f154c87b43ee.js} +1 -1
  52. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-30c2073bfc21c004.js +1 -0
  53. package/apps/pwa/out/_next/static/css/001d5fbaee388a25.css +1 -0
  54. package/apps/pwa/out/_next/static/css/{04dabcbcc4bfa6db.css → 085673d826b98b62.css} +1 -1
  55. package/apps/pwa/out/_next/static/rv9cierV0jSOvDBm1g0XB/_buildManifest.js +1 -0
  56. package/apps/pwa/out/approvals/index.html +1 -1
  57. package/apps/pwa/out/approvals/index.txt +15 -15
  58. package/apps/pwa/out/autopilot/index.html +1 -1
  59. package/apps/pwa/out/autopilot/index.txt +15 -15
  60. package/apps/pwa/out/board/index.html +1 -1
  61. package/apps/pwa/out/board/index.txt +15 -15
  62. package/apps/pwa/out/botlab/index.html +1 -1
  63. package/apps/pwa/out/botlab/index.txt +15 -15
  64. package/apps/pwa/out/bots/index.html +1 -1
  65. package/apps/pwa/out/bots/index.txt +15 -15
  66. package/apps/pwa/out/ceo/index.html +1 -0
  67. package/apps/pwa/out/ceo/index.txt +33 -0
  68. package/apps/pwa/out/chat/index.html +1 -1
  69. package/apps/pwa/out/chat/index.txt +15 -15
  70. package/apps/pwa/out/consult/index.html +1 -1
  71. package/apps/pwa/out/consult/index.txt +15 -15
  72. package/apps/pwa/out/control/index.html +1 -1
  73. package/apps/pwa/out/control/index.txt +15 -15
  74. package/apps/pwa/out/design-check/index.html +1 -1
  75. package/apps/pwa/out/design-check/index.txt +15 -15
  76. package/apps/pwa/out/editor/index.html +1 -1
  77. package/apps/pwa/out/editor/index.txt +15 -15
  78. package/apps/pwa/out/exec/index.html +1 -1
  79. package/apps/pwa/out/exec/index.txt +15 -15
  80. package/apps/pwa/out/field/index.html +1 -1
  81. package/apps/pwa/out/field/index.txt +15 -15
  82. package/apps/pwa/out/healthz +1 -1
  83. package/apps/pwa/out/index.html +1 -1
  84. package/apps/pwa/out/index.txt +15 -15
  85. package/apps/pwa/out/inside/index.html +1 -1
  86. package/apps/pwa/out/inside/index.txt +15 -15
  87. package/apps/pwa/out/intake/index.html +1 -1
  88. package/apps/pwa/out/intake/index.txt +15 -15
  89. package/apps/pwa/out/live/index.html +1 -1
  90. package/apps/pwa/out/live/index.txt +15 -15
  91. package/apps/pwa/out/loops/index.html +1 -1
  92. package/apps/pwa/out/loops/index.txt +15 -15
  93. package/apps/pwa/out/market/index.html +1 -1
  94. package/apps/pwa/out/market/index.txt +15 -15
  95. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  96. package/apps/pwa/out/missions/placeholder/index.txt +15 -15
  97. package/apps/pwa/out/morning/index.html +1 -1
  98. package/apps/pwa/out/morning/index.txt +15 -15
  99. package/apps/pwa/out/observatory/index.html +1 -1
  100. package/apps/pwa/out/observatory/index.txt +15 -15
  101. package/apps/pwa/out/ops/checklist/index.html +1 -1
  102. package/apps/pwa/out/ops/checklist/index.txt +15 -15
  103. package/apps/pwa/out/ops/release/index.html +1 -1
  104. package/apps/pwa/out/ops/release/index.txt +15 -15
  105. package/apps/pwa/out/ops/seats/index.html +1 -1
  106. package/apps/pwa/out/ops/seats/index.txt +15 -15
  107. package/apps/pwa/out/outputs/index.html +1 -1
  108. package/apps/pwa/out/outputs/index.txt +15 -15
  109. package/apps/pwa/out/reflection/index.html +1 -1
  110. package/apps/pwa/out/reflection/index.txt +16 -16
  111. package/apps/pwa/out/scheduler/index.html +1 -1
  112. package/apps/pwa/out/scheduler/index.txt +15 -15
  113. package/apps/pwa/out/sessions/index.html +1 -1
  114. package/apps/pwa/out/sessions/index.txt +15 -15
  115. package/apps/pwa/out/settings/devices/index.html +1 -1
  116. package/apps/pwa/out/settings/devices/index.txt +15 -15
  117. package/apps/pwa/out/settings/index.html +1 -1
  118. package/apps/pwa/out/settings/index.txt +15 -15
  119. package/apps/pwa/out/setup/done/index.html +1 -1
  120. package/apps/pwa/out/setup/done/index.txt +15 -15
  121. package/apps/pwa/out/setup/index.html +1 -1
  122. package/apps/pwa/out/setup/index.txt +15 -15
  123. package/apps/pwa/out/share/index.html +1 -1
  124. package/apps/pwa/out/share/index.txt +15 -15
  125. package/apps/pwa/out/showroom/index.html +1 -1
  126. package/apps/pwa/out/showroom/index.txt +15 -15
  127. package/apps/pwa/out/tasks/index.html +1 -1
  128. package/apps/pwa/out/tasks/index.txt +15 -15
  129. package/apps/pwa/out/term/index.html +1 -1
  130. package/apps/pwa/out/term/index.txt +15 -15
  131. package/apps/pwa/out/today/index.html +1 -1
  132. package/apps/pwa/out/today/index.txt +15 -15
  133. package/apps/pwa/out/trace/index.html +1 -1
  134. package/apps/pwa/out/trace/index.txt +15 -15
  135. package/apps/pwa/out/vault/index.html +1 -1
  136. package/apps/pwa/out/vault/index.txt +15 -15
  137. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  138. package/apps/pwa/out/workflows/chat-ui/index.txt +15 -15
  139. package/apps/pwa/out/workflows/index.html +1 -1
  140. package/apps/pwa/out/workflows/index.txt +15 -15
  141. package/apps/pwa/out/workspace/index.html +1 -1
  142. package/apps/pwa/out/workspace/index.txt +15 -15
  143. package/apps/pwa/out/worktrees/index.html +1 -1
  144. package/apps/pwa/out/worktrees/index.txt +15 -15
  145. package/graphs/doc-gen/doc-gen.yaml +24 -0
  146. package/graphs/doc-gen/recipes.yaml +3 -0
  147. package/graphs/heal/heal-loop.yaml +1 -1
  148. package/graphs/implement-loop.yaml +1 -1
  149. package/graphs/orchestrator/orchestrator.yaml +7 -0
  150. package/graphs/orchestrator/recipes.yaml +3 -0
  151. package/graphs/release/recipes.yaml +3 -0
  152. package/graphs/release/release-loop.yaml +4 -2
  153. package/graphs/steward/recipes.yaml +2 -1
  154. package/package.json +1 -1
  155. package/scripts/brand/check.ts +7 -5
  156. package/scripts/ci-public-export-import-gate.ts +154 -0
  157. package/scripts/ci-test-interference-gate.ts +5 -1
  158. package/scripts/ci-typecheck-changed.ts +82 -31
  159. package/scripts/docs/doc-rot.ts +164 -0
  160. package/scripts/docs/generate-graph-anatomy.ts +437 -0
  161. package/scripts/docs-cli-check.ts +4 -4
  162. package/scripts/expose-rubric-suggest.ts +18 -12
  163. package/scripts/goals/goal-census.ts +343 -0
  164. package/scripts/lecture-note/node.ts +31 -11
  165. package/scripts/lessons/handbook-page.ts +30 -3
  166. package/scripts/marketing/generate-ax-brief.ts +140 -0
  167. package/scripts/marketing/generate-feature-map.ts +118 -0
  168. package/scripts/measure/author-section-contribution.ts +84 -0
  169. package/scripts/release-loop/checklist-gate-node.ts +53 -14
  170. package/scripts/release-loop/cut-branch.ts +43 -13
  171. package/scripts/release-loop/gate-node.ts +82 -14
  172. package/scripts/release-loop/vault-note-node.ts +257 -8
  173. package/scripts/release-story/draft.ts +57 -9
  174. package/scripts/rhythm/daily.ts +263 -76
  175. package/scripts/rhythm/monthly.ts +97 -0
  176. package/scripts/rhythm/weekly.ts +30 -9
  177. package/scripts/smart-merge/resync-shadow.ts +147 -0
  178. package/src/acp/client.ts +17 -2
  179. package/src/agent-env/profile.ts +161 -0
  180. package/src/agent-mission/driver.ts +51 -1
  181. package/src/agent-mission/supervisor-metrics-cli.ts +40 -0
  182. package/src/agent-mission/supervisor-metrics.ts +218 -0
  183. package/src/agent-substrate/pr-reviewer.ts +2 -0
  184. package/src/ask-user-question/pending-questions.ts +5 -0
  185. package/src/auth/github-app-token.ts +112 -13
  186. package/src/autopilot/build/llm-conflict-merge.ts +32 -6
  187. package/src/autopilot/track-agent-shadow.ts +2 -1
  188. package/src/briefing/brief-items.ts +276 -0
  189. package/src/budget/usage-outcomes.ts +204 -0
  190. package/src/claims/claims-render.ts +14 -5
  191. package/src/cli/agent-env-cli.ts +21 -0
  192. package/src/cli/brief-cli.ts +73 -0
  193. package/src/cli/decisions-cli.ts +9 -0
  194. package/src/cli/doctor-cli.ts +23 -1
  195. package/src/cli/doctor-node-modules.ts +147 -0
  196. package/src/cli/intake-cli.ts +46 -17
  197. package/src/cli/know-cli.ts +20 -0
  198. package/src/cli/logs-cli.ts +43 -1
  199. package/src/cli/pod-cli.ts +2 -0
  200. package/src/cli/pr-cli.ts +80 -12
  201. package/src/cli/release-cli.ts +66 -29
  202. package/src/cli/seat-requests-cli.ts +52 -0
  203. package/src/cli/self-update.ts +200 -1
  204. package/src/cli/usage-cli.ts +26 -0
  205. package/src/context-bus/context-now-public.ts +16 -6
  206. package/src/context-bus/context-now.ts +3 -1
  207. package/src/context-bus/coord-events.ts +6 -4
  208. package/src/decisions/decision-cards.ts +33 -7
  209. package/src/decisions/proact-meter.ts +286 -0
  210. package/src/decisions/proact1-lite.ts +86 -0
  211. package/src/decisions/web-push-decision-cards.ts +1 -1
  212. package/src/discord-self-message.ts +9 -0
  213. package/src/domains/alert-audit.ts +271 -0
  214. package/src/domains/core-tools.ts +4 -3
  215. package/src/domains/ops-facts-tool.ts +31 -1
  216. package/src/flow-loop/split.ts +24 -0
  217. package/src/harness/harness-cli-command.ts +147 -17
  218. package/src/harness/harness-incidents.ts +2 -0
  219. package/src/harness/harness-pod-dispatch.ts +67 -8
  220. package/src/harness/harness-queue.ts +380 -50
  221. package/src/harness/harness-seams.ts +13 -3
  222. package/src/harness/helper-repair.ts +218 -0
  223. package/src/harness/helper-scan.ts +27 -4
  224. package/src/harness/launch-stamp.ts +236 -0
  225. package/src/harness/ledger-write-guard-child.fixture.ts +12 -0
  226. package/src/harness/ledger-write-guard.ts +35 -0
  227. package/src/harness/nested-elanous-policy.ts +114 -0
  228. package/src/harness/pod-memory-policy.ts +15 -0
  229. package/src/harness/repository-name.ts +55 -0
  230. package/src/hq/fence-audit.ts +90 -0
  231. package/src/hq/hq.ts +70 -6
  232. package/src/hq/lease.ts +9 -7
  233. package/src/hq/move-plan.ts +126 -0
  234. package/src/hq/promote.ts +31 -0
  235. package/src/hq/transfer.ts +185 -0
  236. package/src/index.ts +189 -13
  237. package/src/intake-plane/check.ts +63 -0
  238. package/src/intake-plane/digest.ts +65 -21
  239. package/src/intake-plane/front-classifier.ts +6 -0
  240. package/src/intake-plane/items.ts +3 -2
  241. package/src/intake-plane/lens.ts +177 -0
  242. package/src/intake-plane/route.ts +145 -1
  243. package/src/knowledge/know-find.ts +105 -0
  244. package/src/llm.ts +75 -19
  245. package/src/loops/budget.ts +36 -5
  246. package/src/loops/checker.ts +2 -2
  247. package/src/loops/orchestrator/author-depth.ts +65 -9
  248. package/src/loops/orchestrator/author-input-check.ts +8 -3
  249. package/src/loops/orchestrator/author-ledger.ts +3 -1
  250. package/src/loops/orchestrator/finish-rate.ts +14 -3
  251. package/src/loops/orchestrator/seat-share.ts +56 -0
  252. package/src/loops/orchestrator/tick.ts +107 -29
  253. package/src/loops/orchestrator/traffic.ts +117 -11
  254. package/src/loops/registry.ts +142 -61
  255. package/src/maturity/bot-slash-catalog.ts +11 -2
  256. package/src/maturity/feature-maturity.ts +4 -3
  257. package/src/maturity/tui-slash-availability.ts +40 -0
  258. package/src/mss/logging/log-store.ts +35 -4
  259. package/src/nexus/api/http-server.ts +28 -14
  260. package/src/nexus/api/loop-edges.ts +173 -0
  261. package/src/nexus/api/schedules-read.ts +13 -0
  262. package/src/nexus/api/seat-requests.ts +79 -56
  263. package/src/oauth/overload-failover-launch.ts +75 -0
  264. package/src/oauth/overload-failover.ts +137 -0
  265. package/src/prompt-enhance/enhance.ts +6 -1
  266. package/src/release-loop/auto-start.ts +6 -3
  267. package/src/release-loop/checklist.ts +19 -5
  268. package/src/release-loop/feature-store.ts +59 -31
  269. package/src/release-loop/merged-pr-checklist.ts +125 -0
  270. package/src/release-loop/placement.ts +98 -29
  271. package/src/seat-dispatch/seat-ask.ts +33 -6
  272. package/src/seat-dispatch/seat-request-eligibility.ts +19 -0
  273. package/src/seat-dispatch/seat-request-ledger.ts +133 -0
  274. package/src/seat-loop/seat-loop.ts +215 -12
  275. package/src/self-dev/ask-launch-flow.ts +22 -1
  276. package/src/self-dev/authored-goal-trace.ts +64 -0
  277. package/src/self-dev/dev-cli.ts +19 -1
  278. package/src/self-dev/dev-pipeline.ts +14 -9
  279. package/src/self-dev/draft-sweep.ts +90 -10
  280. package/src/self-dev/draft-triage-rules.ts +25 -8
  281. package/src/self-dev/entrance-registry.ts +10 -6
  282. package/src/self-dev/old-door.ts +73 -0
  283. package/src/self-dev/orchestrate.ts +15 -1
  284. package/src/self-dev/review-provider-fallback.ts +6 -1
  285. package/src/self-dev/run-supervisor.ts +49 -2
  286. package/src/self-implement/app-api-budget.ts +166 -0
  287. package/src/self-implement/budget-gate.ts +7 -5
  288. package/src/self-implement/follow-up-goals.ts +289 -0
  289. package/src/self-implement/frozen-merges.ts +3 -2
  290. package/src/self-implement/goal-author.ts +50 -6
  291. package/src/self-implement/goal-clarification-escalation.ts +5 -0
  292. package/src/self-implement/heal-intake.ts +120 -2
  293. package/src/self-implement/heal-triage.ts +44 -0
  294. package/src/self-implement/host-regate.ts +9 -2
  295. package/src/self-implement/l8-merge-queue.ts +132 -0
  296. package/src/self-implement/orchestrator.ts +325 -47
  297. package/src/self-implement/pipeline-shape.ts +1 -1
  298. package/src/self-implement/revert-guard.ts +42 -7
  299. package/src/self-implement/run-ledger.ts +6 -0
  300. package/src/self-implement/run-status-mapping.ts +2 -0
  301. package/src/self-implement/seams.ts +45 -16
  302. package/src/self-implement/self-implement-cli.ts +1 -0
  303. package/src/self-implement/speed-claims.ts +338 -0
  304. package/src/session-runtime/index.ts +2 -0
  305. package/src/session-runtime/retry-api.ts +87 -0
  306. package/src/session-runtime/retry-policy.ts +107 -6
  307. package/src/steward/triage.ts +74 -3
  308. package/src/steward/working-backwards.ts +20 -2
  309. package/src/task-cards/card-cli.ts +25 -0
  310. package/src/task-cards/card-store.ts +5 -3
  311. package/src/task-orchestrator/surfaces/pod-artifact-return.ts +63 -48
  312. package/src/task-orchestrator/surfaces/pod-command-job.ts +84 -9
  313. package/src/task-orchestrator/surfaces/pod-failure-reason.ts +48 -0
  314. package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +4 -3
  315. package/src/task-orchestrator/surfaces/pod-ledger-prod-sink.ts +75 -0
  316. package/src/task-orchestrator/surfaces/pod-pool.ts +70 -9
  317. package/src/task-orchestrator/surfaces/pod-source-receive.ts +37 -1
  318. package/src/task-orchestrator/surfaces/self-implement-pod.ts +166 -46
  319. package/src/task-orchestrator/surfaces/self-implement.ts +7 -1
  320. package/src/telegram-commands.ts +5 -0
  321. package/src/telegram.ts +72 -4
  322. package/src/user-config.ts +64 -7
  323. package/src/version/packed-revision.json +1 -1
  324. package/src/workflow-runtime/nodes/bash.ts +9 -1
  325. package/apps/pwa/out/_next/static/chunks/4562-071d8bac44a01069.js +0 -1
  326. package/apps/pwa/out/_next/static/chunks/5977-2df489622a52a33a.js +0 -1
  327. package/apps/pwa/out/_next/static/chunks/app/approvals/page-74383f890f075511.js +0 -1
  328. package/apps/pwa/out/_next/static/chunks/app/board/page-c496bb16da85385f.js +0 -1
  329. package/apps/pwa/out/_next/static/chunks/app/botlab/page-1abfe3aec3be6bdc.js +0 -1
  330. package/apps/pwa/out/_next/static/chunks/app/consult/page-fb260b9283dbf646.js +0 -1
  331. package/apps/pwa/out/_next/static/chunks/app/design-check/page-61ec1d2e92bdd50a.js +0 -1
  332. package/apps/pwa/out/_next/static/chunks/app/inside/page-fd7e200d4d83fbd7.js +0 -1
  333. package/apps/pwa/out/_next/static/chunks/app/live/page-d47266766766d227.js +0 -1
  334. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-2ce5471926761fdd.js +0 -1
  335. package/apps/pwa/out/_next/static/chunks/app/not-found-e995d30f6800b6bf.js +0 -1
  336. package/apps/pwa/out/_next/static/chunks/app/observatory/page-39f89c53038a51d4.js +0 -1
  337. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-5a753bb4ec0cc511.js +0 -1
  338. package/apps/pwa/out/_next/static/chunks/app/outputs/page-04250f0e2c0d03df.js +0 -1
  339. package/apps/pwa/out/_next/static/chunks/app/page-341480ed8e3c1ea8.js +0 -1
  340. package/apps/pwa/out/_next/static/chunks/app/sessions/page-8ac8d71c2adf9c8a.js +0 -1
  341. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-e1e47d8d940aba3d.js +0 -1
  342. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-94dc383d3d54db1a.js +0 -1
  343. package/apps/pwa/out/_next/static/chunks/app/setup/page-b3759cc14da80908.js +0 -1
  344. package/apps/pwa/out/_next/static/chunks/app/today/page-194dce4a83143fce.js +0 -1
  345. package/apps/pwa/out/_next/static/chunks/app/trace/page-35bdeae2e2c17147.js +0 -1
  346. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-16bef3da05a900c3.js +0 -1
  347. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-134c1bc762aee62c.js +0 -1
  348. package/apps/pwa/out/_next/static/css/2e33713c444d5b39.css +0 -1
  349. package/apps/pwa/out/_next/static/x6mjI6vqOkVVu0jFuDkz1/_buildManifest.js +0 -1
  350. /package/apps/pwa/out/_next/static/{x6mjI6vqOkVVu0jFuDkz1 → rv9cierV0jSOvDBm1g0XB}/_ssgManifest.js +0 -0
@@ -1,26 +1,31 @@
1
1
  import { spawn, spawnSync } from 'node:child_process';
2
2
  import { createHash, randomUUID } from 'node:crypto';
3
- import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readlinkSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
3
+ import { appendFileSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, readlinkSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
4
4
  import { join, resolve } from 'node:path';
5
5
  import { Database } from 'bun:sqlite';
6
6
  import { debug } from '../debug/log.js';
7
- import { effectiveInstanceRoot, prodInstanceRoot } from '../instance/resolve.js';
8
- import { decideSpawn } from '../loops/budget.js';
7
+ import { effectiveInstanceRoot, prodInstanceRoot, releaseLedgerRoot } from '../instance/resolve.js';
8
+ import { listChecklist, type ChecklistItem } from '../release-loop/checklist.js';
9
+ import { listSchedules } from '../release-loop/release-schedule.js';
10
+ import { withFileLockSync } from '../storage/file-lock.js';
11
+ import { trafficTick, TRAFFIC_SEATS, type TrafficCell } from '../loops/orchestrator/traffic.js';
12
+ import { decideSpawn, seatCapDetails, seatCapReason } from '../loops/budget.js';
9
13
  import { listSelfDevRuns, processBirthId, selfDevRunsDir } from '../self-dev/run-store.js';
10
- import { getUserConfig } from '../user-config.js';
14
+ import { getUserConfig, userConfigPath, type OrchestratorLoopConfig } from '../user-config.js';
11
15
  import { hostLeaseCounts, leaseHasPendingPod } from '../pod-lease/host-lease.js';
12
16
  import { leaseKubectl, measurePoolLease } from '../task-orchestrator/surfaces/pod-lease.js';
13
17
  import { parsePodPool, podPoolHostLease, resolvePodPoolSpec } from '../task-orchestrator/surfaces/pod-pool.js';
14
18
  import { writeHarnessQueueReceipt } from './harness-queue-child.js';
19
+ import { finishAdvice, measureFinish, type FinishMetrics } from '../loops/orchestrator/finish-rate.js';
15
20
 
16
21
  export type QueueSeat = 'OP' | 'TC' | 'MK' | 'UX';
17
22
  export type QueueItem = {
18
23
  id: string; seat: QueueSeat; kind: 'say' | 'ask'; input: string; hold: boolean; heavy: boolean;
19
- at: string; status: 'queued' | 'launching' | 'launched' | 'finished'; pid?: number; launchId?: string; idempotencyKey?: string;
24
+ at: string; status: 'queued' | 'launching' | 'launched' | 'finished'; pid?: number; launchId?: string; idempotencyKey?: string; waitingReason?: string;
20
25
  };
21
26
  export type QueuePool = { running: number; pending: number; reserved: number; limit: number };
22
27
  export type QueueTick = { outcome: 'launched' | 'waiting' | 'skipped'; item?: QueueItem; reason: string };
23
- export type QueueProcess = { pid: number; seat?: QueueSeat; launchId?: string };
28
+ export type QueueProcess = { pid: number; seat?: QueueSeat; launchId?: string; runId?: string; rootPid?: number };
24
29
  type ProcessProbe = {
25
30
  run?: typeof spawnSync; platform?: NodeJS.Platform;
26
31
  cwd?: (pid: number) => string;
@@ -51,7 +56,71 @@ export interface HarnessQueueDeps {
51
56
  alive?: (pid: number) => boolean;
52
57
  processes?: () => readonly QueueProcess[];
53
58
  receipt?: (root: string, launchId: string) => 'started' | 'finished' | 'not-started' | null;
59
+ now?: () => Date;
60
+ authorShadow?: (root: string, now: Date) => void | Promise<void>;
61
+ idleRequest?: (root: string, now: Date, items: readonly QueueItem[], deps: HarnessQueueDeps) => void | Promise<void>;
62
+ idleCells?: (now: Date) => { current: readonly Pick<ChecklistItem, 'id' | 'title' | 'owner' | 'status' | 'evidence'>[];
63
+ next: readonly Pick<ChecklistItem, 'id' | 'title' | 'owner' | 'status' | 'evidence'>[] };
64
+ idleLog?: (category: string, event: string, data: Record<string, unknown>) => void;
54
65
  log?: (event: 'enqueued' | 'launched' | 'waiting' | 'skipped', data: Record<string, unknown>) => void;
66
+ /** FINISH-RATE metrics source; a test process without it skips the real measurement. */
67
+ finishMetrics?: (now: Date) => FinishMetrics | Promise<FinishMetrics>;
68
+ }
69
+
70
+ const FINISH_CACHE_MS = 10 * 60 * 1_000;
71
+ const FINISH_KEYS = ['launched', 'landed', 'landingRate', 'staleDrafts', 'conflictRatio', 'unknownMergeable'] as const;
72
+
73
+ /** A cache row missing any metric or its reasons map is re-measured, never fed to finishAdvice. */
74
+ function validFinishMetrics(value: unknown): value is FinishMetrics {
75
+ if (!value || typeof value !== 'object') return false;
76
+ const row = value as Record<string, unknown>;
77
+ return FINISH_KEYS.every((key) => row[key] === null || (typeof row[key] === 'number' && Number.isFinite(row[key])))
78
+ && !!row.reasons && typeof row.reasons === 'object' && !Array.isArray(row.reasons);
79
+ }
80
+
81
+ /** Reads finish metrics at most once per 10 minutes per root; unreadable or missing → null (never a hold). */
82
+ async function cachedFinishMetrics(root: string, now: Date, deps: HarnessQueueDeps): Promise<FinishMetrics | null> {
83
+ // Checked before the cache: a cached real measurement must not reach an uninjected test tick either.
84
+ if (!deps.finishMetrics && (process.env.NODE_ENV === 'test' || process.env.ELANOUS_TEST_HOME)) {
85
+ debug.log('loop.orchestrator', 'finish-skipped-test', {});
86
+ return null;
87
+ }
88
+ const path = join(root, 'harness', 'finish-metrics.json');
89
+ try {
90
+ if (existsSync(path)) {
91
+ const cached = JSON.parse(readFileSync(path, 'utf8')) as { at?: unknown; metrics?: unknown };
92
+ const at = typeof cached.at === 'string' ? Date.parse(cached.at) : NaN;
93
+ if (Number.isFinite(at) && now.getTime() - at >= 0 && now.getTime() - at < FINISH_CACHE_MS
94
+ && validFinishMetrics(cached.metrics)) return cached.metrics;
95
+ }
96
+ } catch { /* A broken cache is re-measured. */ }
97
+ const measure = deps.finishMetrics ?? (() => measureFinish());
98
+ const metrics = await measure(now);
99
+ try {
100
+ mkdirSync(join(root, 'harness'), { recursive: true });
101
+ writeFileSync(path, JSON.stringify({ at: now.toISOString(), metrics }));
102
+ } catch (error) { debug.log('loop.orchestrator', 'finish-cache-write-failed', { reason: String(error) }); }
103
+ return metrics;
104
+ }
105
+
106
+ /** FINISH-RATE: a backlogged board holds this launch when the pool already fills the launch share. */
107
+ async function finishGateReason(root: string, now: Date, mode: 'off' | 'shadow' | 'on', running: number, totalSlots: number,
108
+ deps: HarnessQueueDeps): Promise<string | null> {
109
+ if (mode === 'off') return null;
110
+ let metrics: FinishMetrics | null;
111
+ try { metrics = await cachedFinishMetrics(root, now, deps); }
112
+ catch (error) {
113
+ debug.log('loop.orchestrator', 'finish-measure-failed', { reason: String(error) });
114
+ return null;
115
+ }
116
+ if (!metrics) return null;
117
+ const advice = finishAdvice(metrics, totalSlots);
118
+ if (advice.state !== 'backlogged' || running < advice.launchSlots) return null;
119
+ debug.log('loop.orchestrator', 'finish-rebalanced', {
120
+ finishSlots: advice.finishSlots, launchSlots: advice.launchSlots, running, reasons: advice.reasons, mode,
121
+ });
122
+ if (mode === 'shadow') return null;
123
+ return `마무리 우선 — finish=${advice.finishSlots}/${totalSlots} · 사유 ${advice.reasons.join(', ')}`;
55
124
  }
56
125
 
57
126
  export function harnessQueuePath(root = effectiveInstanceRoot()): string {
@@ -73,7 +142,8 @@ function read(path: string): QueueItem[] {
73
142
  && ['queued', 'launching', 'launched', 'finished'].includes(row.status)
74
143
  && (row.pid === undefined || Number.isSafeInteger(row.pid))
75
144
  && (row.launchId === undefined || typeof row.launchId === 'string')
76
- && (row.idempotencyKey === undefined || typeof row.idempotencyKey === 'string'))) {
145
+ && (row.idempotencyKey === undefined || typeof row.idempotencyKey === 'string')
146
+ && (row.waitingReason === undefined || typeof row.waitingReason === 'string'))) {
77
147
  throw new Error('harness queue: invalid queue file (no launch)');
78
148
  }
79
149
  return value as QueueItem[];
@@ -90,6 +160,29 @@ function save(path: string, items: QueueItem[]): void {
90
160
  }
91
161
  }
92
162
 
163
+ function lastLaunchedSeat(path: string): QueueSeat | undefined {
164
+ const marker = `${path}.round-robin.json`;
165
+ if (!existsSync(marker)) return undefined;
166
+ const value: unknown = JSON.parse(readFileSync(marker, 'utf8'));
167
+ if (!value || typeof value !== 'object' || Array.isArray(value)
168
+ || !queueSeatNames.some((name) => name === (value as { lastSeat?: unknown }).lastSeat)) {
169
+ throw new Error('harness queue: invalid round-robin state (no launch)');
170
+ }
171
+ return (value as { lastSeat: QueueSeat }).lastSeat;
172
+ }
173
+
174
+ function saveLastLaunchedSeat(path: string, lastSeat: QueueSeat): void {
175
+ const marker = `${path}.round-robin.json`;
176
+ const temporary = `${marker}.${randomUUID()}.tmp`;
177
+ try {
178
+ writeFileSync(temporary, JSON.stringify({ lastSeat }), { flag: 'wx', mode: 0o600 });
179
+ renameSync(temporary, marker);
180
+ } catch (error) {
181
+ try { unlinkSync(temporary); } catch { /* write did not finish */ }
182
+ throw error;
183
+ }
184
+ }
185
+
93
186
  // SQLite OS locks release on process death, including death while awaiting a launch.
94
187
  async function locked<T>(root: string, action: (path: string) => Promise<T>): Promise<T> {
95
188
  const path = harnessQueuePath(root);
@@ -309,6 +402,7 @@ export function readHarnessQueueProcesses(probe: ProcessProbe = {}): readonly Qu
309
402
  const attributed: { seat: QueueSeat; depth: number; source: 'env' | 'flag' | 'ledger' | 'cwd'; priority: number }[] = [];
310
403
  const worktrees: { seat: QueueSeat; depth: number }[] = [];
311
404
  let launchId: string | undefined;
405
+ let runId: string | undefined;
312
406
  for (const { process: { pid, command }, depth } of members) {
313
407
  let envSeat: string | undefined;
314
408
  if (platform === 'linux') {
@@ -316,6 +410,7 @@ export function readHarnessQueueProcesses(probe: ProcessProbe = {}): readonly Qu
316
410
  const env = (probe.environ ?? ((id: number) => readFileSync(`/proc/${id}/environ`, 'utf8')))(pid).split('\0');
317
411
  envSeat = env.find((entry) => entry.startsWith('ELANOUS_HARNESS_SEAT='))?.slice('ELANOUS_HARNESS_SEAT='.length);
318
412
  launchId ??= env.find((entry) => entry.startsWith('ELANOUS_HARNESS_QUEUE_LAUNCH='))?.slice('ELANOUS_HARNESS_QUEUE_LAUNCH='.length);
413
+ runId ??= env.find((entry) => entry.startsWith('ELANOUS_RUN_ID='))?.slice('ELANOUS_RUN_ID='.length);
319
414
  } catch { /* Unknown attribution is charged to all seats. */ }
320
415
  }
321
416
  const flag = /--seat[=\s]+(OP|TC|MK|UX)(?:\s|$)/.exec(command)?.[1] as QueueSeat | undefined;
@@ -352,7 +447,8 @@ export function readHarnessQueueProcesses(probe: ProcessProbe = {}): readonly Qu
352
447
  const assigned = seats.size === 1 ? top[0] : undefined;
353
448
  try { debug.log('harness.queue', 'attributed', { pid: root.pid, seat: assigned?.seat ?? null, source: assigned?.source ?? 'all' }); }
354
449
  catch { /* Observation does not block inventory. */ }
355
- rows.push({ pid: root.pid, ...(assigned ? { seat: assigned.seat } : {}), ...(launchId ? { launchId } : {}) });
450
+ rows.push({ pid: root.pid, ...(assigned ? { seat: assigned.seat } : {}), ...(launchId ? { launchId } : {}),
451
+ ...(runId ? { runId } : {}) });
356
452
  }
357
453
  return rows;
358
454
  }
@@ -421,34 +517,241 @@ export async function reconcileHarnessQueue(id: string, deps: HarnessQueueDeps =
421
517
  });
422
518
  }
423
519
 
520
+ const warnedLegacySeatCapPaths = new Set<string>();
521
+
522
+ function warnLegacySeatCap(path: string): void {
523
+ if (warnedLegacySeatCapPaths.has(path)) return;
524
+ try {
525
+ const raw: unknown = JSON.parse(readFileSync(path, 'utf8'));
526
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return;
527
+ const harness = (raw as Record<string, unknown>).harness;
528
+ if (!harness || typeof harness !== 'object' || Array.isArray(harness)) return;
529
+ const queue = (harness as Record<string, unknown>).queue;
530
+ if (!queue || typeof queue !== 'object' || Array.isArray(queue)
531
+ || !Object.hasOwn(queue, 'seatCap')) return;
532
+ warnedLegacySeatCapPaths.add(path);
533
+ debug.log('harness.queue', 'legacy-seat-cap-ignored', {
534
+ key: 'harness.queue.seatCap', replacement: 'loops.orchestrator.seatCaps', path,
535
+ }, { level: 'warn' });
536
+ } catch { /* An unreadable config cannot affect dispatch or observation. */ }
537
+ }
538
+
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> }[] = [];
541
+ for (const row of processes) {
542
+ const keys = [row.runId && `run:${row.runId}`, `root:${row.rootPid ?? row.pid}`,
543
+ row.launchId && `launch:${row.launchId}`, `pid:${row.pid}`].filter((key): key is string => Boolean(key));
544
+ const matching = groups.filter((group) => keys.some((key) => group.keys.has(key)));
545
+ const group = matching[0] ?? { seats: new Set<QueueSeat>(), unknown: false,
546
+ launchIds: new Set<string>(), keys: new Set<string>() };
547
+ for (const other of matching.slice(1)) {
548
+ for (const key of other.keys) group.keys.add(key);
549
+ for (const seat of other.seats) group.seats.add(seat);
550
+ for (const id of other.launchIds) group.launchIds.add(id);
551
+ group.unknown ||= other.unknown;
552
+ groups.splice(groups.indexOf(other), 1);
553
+ }
554
+ for (const key of keys) group.keys.add(key);
555
+ if (row.seat) group.seats.add(row.seat);
556
+ else group.unknown = true;
557
+ if (row.launchId) group.launchIds.add(row.launchId);
558
+ if (!matching.length) groups.push(group);
559
+ }
560
+ return groups.map((group) => ({
561
+ ...(group.unknown || group.seats.size !== 1 ? {} : { seat: [...group.seats][0] }),
562
+ launchIds: group.launchIds,
563
+ }));
564
+ }
565
+
566
+ const IDLE_REQUEST_INTERVAL_MS = 15 * 60_000;
567
+ const IDLE_REQUEST_MINUTES = 30;
568
+ type IdleRequestState = { checkedAt: string; idleSince: Partial<Record<QueueSeat, string>>; notified?: string[] };
569
+
570
+ function idleRequestCells(now: Date, root: string): ReturnType<NonNullable<HarnessQueueDeps['idleCells']>> {
571
+ const ledger = root === effectiveInstanceRoot() ? releaseLedgerRoot() : root;
572
+ if (!existsSync(join(ledger, 'release', 'features.sqlite'))) throw new Error('release checklist ledger unavailable');
573
+ const upcoming = listSchedules(ledger).filter(row => Date.parse(row.cutAt) > now.getTime())
574
+ .sort((a, b) => Date.parse(a.cutAt) - Date.parse(b.cutAt));
575
+ if (!upcoming[0]) throw new Error('current release schedule unavailable');
576
+ return { current: listChecklist(upcoming[0].version, ledger).items,
577
+ next: upcoming[1] ? listChecklist(upcoming[1].version, ledger).items : [] };
578
+ }
579
+
580
+ function hasRunEvidence(cell: Pick<ChecklistItem, 'evidence'>): boolean {
581
+ return /(?:\brun[-_][a-z0-9-]+\b|\b(?:런|run)(?:\s*(?:근거|id|ID))?\s*[::#]\s*\S+)/i.test(cell.evidence ?? '');
582
+ }
583
+
584
+ /** The queue lock serializes the observation marker; request journal lock also covers external traffic writers. */
585
+ export function requestIdleSeats(root: string, now: Date, items: readonly QueueItem[], deps: HarnessQueueDeps = {}): void {
586
+ const config: OrchestratorLoopConfig | undefined = (deps.configPath ? getUserConfig(deps.configPath) : getUserConfig()).loops?.orchestrator;
587
+ const mode = config?.idleRequest ?? 'shadow';
588
+ if (mode === 'off') return;
589
+ if (!Number.isFinite(now.getTime())) throw new Error('invalid idle request clock');
590
+ const marker = join(root, 'orchestrator', 'idle-request.json');
591
+ const previous: IdleRequestState = existsSync(marker) ? JSON.parse(readFileSync(marker, 'utf8')) as IdleRequestState
592
+ : { checkedAt: new Date(now.getTime() - IDLE_REQUEST_INTERVAL_MS).toISOString(), idleSince: {} };
593
+ if (!previous || typeof previous !== 'object' || !previous.idleSince || typeof previous.idleSince !== 'object'
594
+ || !Number.isFinite(Date.parse(previous.checkedAt)) || (previous.notified !== undefined
595
+ && (!Array.isArray(previous.notified) || previous.notified.some(key => typeof key !== 'string')))
596
+ || Object.entries(previous.idleSince).some(([seat, value]) =>
597
+ !TRAFFIC_SEATS.some(name => name === seat) || typeof value !== 'string' || !Number.isFinite(Date.parse(value)))) {
598
+ throw new Error('invalid idle request marker');
599
+ }
600
+ const processes = (deps.processes ?? readHarnessQueueProcesses)();
601
+ const groups = groupQueueRuns(processes);
602
+ const reservations = items.filter(item => item.status === 'launching' || item.status === 'launched').filter(item => {
603
+ if (item.launchId && groups.some(group => group.launchIds.has(item.launchId!))) return false;
604
+ const state = item.launchId ? (deps.receipt ?? receipt)(root, item.launchId) : null;
605
+ return state !== 'finished' && state !== 'not-started';
606
+ });
607
+ const counts = Object.fromEntries(TRAFFIC_SEATS.map(seat => [seat,
608
+ groups.filter(group => !group.seat || group.seat === seat).length
609
+ + reservations.filter(item => item.seat === seat).length])) as Record<QueueSeat, number>;
610
+ const idleSince = { ...previous.idleSince };
611
+ for (const seat of TRAFFIC_SEATS) {
612
+ if (counts[seat] > 0) delete idleSince[seat];
613
+ else if (!idleSince[seat]) idleSince[seat] = now.toISOString();
614
+ }
615
+ const due = now.getTime() - Date.parse(previous.checkedAt) >= IDLE_REQUEST_INTERVAL_MS;
616
+ if (!due && Object.keys(idleSince).length === Object.keys(previous.idleSince).length) return;
617
+ const day = new Intl.DateTimeFormat('en-CA', { timeZone: 'Asia/Seoul', year: 'numeric', month: '2-digit', day: '2-digit' }).format(now);
618
+ const state: IdleRequestState = { checkedAt: due ? now.toISOString() : previous.checkedAt, idleSince,
619
+ notified: (previous.notified ?? []).filter(key => key.startsWith('orch-idle:') && key.split(':')[2] === day) };
620
+ mkdirSync(join(root, 'orchestrator'), { recursive: true });
621
+ const temporary = `${marker}.${randomUUID()}.tmp`;
622
+ try {
623
+ writeFileSync(temporary, JSON.stringify(state), { flag: 'wx', mode: 0o600 });
624
+ renameSync(temporary, marker);
625
+ } catch (error) {
626
+ try { unlinkSync(temporary); } catch { /* no temporary file */ }
627
+ throw error;
628
+ }
629
+ if (!due) return;
630
+ const cells = (deps.idleCells ?? ((clock: Date) => idleRequestCells(clock, root)))(now);
631
+ const eligible = (rows: typeof cells.current, seat: QueueSeat) => rows.filter(cell =>
632
+ cell.owner?.split('/')[0] === seat && cell.status === 'yellow' && !hasRunEvidence(cell));
633
+ const candidates: TrafficCell[] = TRAFFIC_SEATS.flatMap(seat => {
634
+ const current = eligible(cells.current, seat);
635
+ return (current.length ? current : eligible(cells.next, seat)).slice(0, 1);
636
+ });
637
+ const measured = trafficTick({ now, processes: TRAFFIC_SEATS.flatMap(seat => Array.from({ length: counts[seat] }, () =>
638
+ ({ seat, command: 'bun bin/elanous.mjs harness say observed', elapsedSeconds: 0 }))),
639
+ caps: config?.seatCaps ?? { OP: 4, TC: 8, MK: 6, UX: 6 }, idleSince: Object.fromEntries(TRAFFIC_SEATS.map(seat =>
640
+ [seat, idleSince[seat] ? new Date(idleSince[seat]) : null])), openCells: candidates, idleMinutes: IDLE_REQUEST_MINUTES });
641
+ const log = deps.idleLog ?? ((category: string, event: string, data: Record<string, unknown>) => debug.log(category, event, data));
642
+ for (const row of measured.seats) {
643
+ if (!row.idle || row.running !== 0) continue;
644
+ const cell = row.nextCell ? `${row.nextCell.id} ${row.nextCell.title.replace(/\s+/g, ' ').trim()}` : '칸 없음';
645
+ let outcome: 'shadow' | 'queued' | 'duplicate' | 'no-cell' = mode === 'live' ? 'no-cell' : 'shadow';
646
+ const key = `orch-idle:${row.seat}:${day}:${row.nextCell?.id ?? 'no-cell'}`;
647
+ if (state.notified!.includes(key)) continue;
648
+ if (mode === 'live' && row.nextCell) {
649
+ const path = join(root, 'seat-requests', 'requests.jsonl');
650
+ mkdirSync(join(root, 'seat-requests'), { recursive: true });
651
+ outcome = withFileLockSync(`${path}.lock`, () => {
652
+ if (existsSync(path) && readFileSync(path, 'utf8').split('\n').some(line => {
653
+ if (!line) return false;
654
+ try { return (JSON.parse(line) as { key?: string }).key === key; }
655
+ catch { throw new Error('invalid seat request journal'); }
656
+ })) return 'duplicate';
657
+ appendFileSync(path, JSON.stringify({ key, receiptId: key, seat: row.seat,
658
+ text: `다음 칸: ${cell} — 30분 놀았다`, status: 'queued', queuedAt: now.toISOString(), source: 'orchestrator-idle' }) + '\n');
659
+ return 'queued';
660
+ });
661
+ }
662
+ if (outcome !== 'duplicate') {
663
+ state.notified!.push(key);
664
+ try { log('loop.orchestrator', 'idle-requested', {
665
+ seat: row.seat, idleMinutes: IDLE_REQUEST_MINUTES, cell, mode, outcome }); }
666
+ catch { /* Observation cannot change the request or queue decision. */ }
667
+ }
668
+ }
669
+ try {
670
+ writeFileSync(temporary, JSON.stringify(state), { flag: 'wx', mode: 0o600 });
671
+ renameSync(temporary, marker);
672
+ } catch (error) {
673
+ try { unlinkSync(temporary); } catch { /* no temporary file */ }
674
+ throw error;
675
+ }
676
+ }
677
+
424
678
  export async function tickHarnessQueue(deps: HarnessQueueDeps = {}): Promise<QueueTick> {
425
679
  const root = deps.root ?? effectiveInstanceRoot();
426
680
  return locked(root, async (path) => {
681
+ try {
682
+ const now = (deps.now ?? (() => new Date()))();
683
+ // A test process must not run the real author shadow — it reads operational ledgers and blew the 5s test budget (10-05 P0).
684
+ if (!deps.authorShadow && process.env.ELANOUS_AUTHOR_SHADOW_LIVE !== '1' && (process.env.NODE_ENV === 'test' || process.env.ELANOUS_TEST_HOME)) {
685
+ debug.log('loops.author-depth', 'shadow-skipped-test', {});
686
+ } else {
687
+ const shadow = deps.authorShadow ?? (await import('../loops/orchestrator/author-depth.js')).runAuthorDepthShadow;
688
+ await shadow(root, now);
689
+ }
690
+ } catch (error) {
691
+ try { debug.log('loops.author-depth', 'shadow-failed', { reason: String(error) }); }
692
+ catch { /* Shadow observation cannot affect queue dispatch. */ }
693
+ }
427
694
  const items = read(path);
428
- const item = items.find((row) => row.status === 'queued');
429
- if (!item) { observe('skipped', { reason: 'empty' }, deps); return { outcome: 'skipped', reason: 'empty' }; }
695
+ try { await (deps.idleRequest ?? requestIdleSeats)(root, (deps.now ?? (() => new Date()))(), items, deps); }
696
+ catch (error) {
697
+ try { debug.log('loop.orchestrator', 'idle-request-failed', { reason: String(error) }); }
698
+ catch { /* Idle observation cannot affect queue dispatch. */ }
699
+ }
700
+ 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);
702
+ if (!heads.size) { observe('skipped', { reason: 'empty' }, deps); return { outcome: 'skipped', reason: 'empty' }; }
703
+ const lastSeat = lastLaunchedSeat(path);
704
+ const first = lastSeat ? (queueSeatNames.indexOf(lastSeat) + 1) % queueSeatNames.length
705
+ : queueSeatNames.indexOf(items.find((row) => row.status === 'queued')!.seat);
706
+ const candidates = [...queueSeatNames.slice(first), ...queueSeatNames.slice(0, first)]
707
+ .flatMap((name) => heads.get(name) ? [heads.get(name)!] : []);
430
708
  const config = deps.configPath === undefined ? getUserConfig() : getUserConfig(deps.configPath);
431
- const cap = (deps.cap ?? ((s: QueueSeat) => config.harness?.queue?.seatCap?.[s] ?? 8))(item.seat);
432
- if (!Number.isSafeInteger(cap) || cap < 1) throw new Error(`harness queue: invalid seat cap for ${item.seat}`);
709
+ warnLegacySeatCap(deps.configPath ?? userConfigPath());
433
710
  const caps = config.loops?.orchestrator?.seatCaps;
434
711
  const gate = config.loops?.orchestrator?.releaseGate;
435
712
  const attributed: Record<QueueSeat, number> = { OP: 0, TC: 0, MK: 0, UX: 0 };
436
713
  let unattributed = 0;
437
- const wait = (reason: string, active: number): QueueTick => {
438
- observe('waiting', { id: item.id, seat: item.seat, reason, active, cap, attributed, unattributed }, deps);
439
- return { outcome: 'waiting', item, reason };
714
+ const blocked = new Map<QueueSeat, { reason: string; active: number }>();
715
+ const waiting = (): QueueTick => {
716
+ const reason = candidates.map((item) => blocked.get(item.seat)!.reason).join('\n');
717
+ const updated = current.map((row) => {
718
+ const block = blocked.get(row.seat);
719
+ return heads.get(row.seat)?.id === row.id && block && row.waitingReason !== block.reason
720
+ ? { ...row, waitingReason: block.reason } : row;
721
+ });
722
+ if (updated.some((row, index) => row !== current[index])) save(path, updated);
723
+ for (const item of candidates) {
724
+ const { reason: seatReason, active } = blocked.get(item.seat)!;
725
+ const injectedCap = deps.cap?.(item.seat);
726
+ const details = seatCapDetails({ seat: item.seat, caps, gate, injectedCap });
727
+ observe('waiting', { id: item.id, seat: item.seat, reason: seatReason, active, cap: details.cap,
728
+ seatCaps: details.seatCaps, releaseGate: details.releaseGate,
729
+ ...(details.injectedCap === undefined ? {} : { injectedCap: details.injectedCap }), capKeys: details.winners,
730
+ attributed, unattributed }, deps);
731
+ }
732
+ return { outcome: 'waiting', item: { ...candidates[0]!, waitingReason: blocked.get(candidates[0]!.seat)!.reason }, reason };
440
733
  };
734
+ for (const item of candidates) {
735
+ const injectedCap = deps.cap?.(item.seat);
736
+ if (injectedCap !== undefined && (!Number.isSafeInteger(injectedCap) || injectedCap < 0)) throw new Error(`harness queue: invalid seat cap for ${item.seat}`);
737
+ }
738
+ let current = items;
441
739
  let processes: readonly QueueProcess[];
442
740
  try { processes = (deps.processes ?? readHarnessQueueProcesses)(); }
443
741
  catch (error) {
444
- const budget = decideSpawn({ seat: item.seat, running: null, caps, gate });
445
- return wait(`${budget.reason}: harness process inventory unavailable: ${String(error)}`, 0);
742
+ for (const item of candidates) {
743
+ const injectedCap = deps.cap?.(item.seat);
744
+ const budget = decideSpawn({ seat: item.seat, running: null, caps, gate, injectedCap });
745
+ blocked.set(item.seat, { reason: `${budget.reason}: harness process inventory unavailable: ${String(error)}`, active: 0 });
746
+ }
747
+ return waiting();
446
748
  }
447
- for (const row of processes) {
448
- if (row.seat) attributed[row.seat]++;
749
+ const runs = groupQueueRuns(processes);
750
+ for (const run of runs) {
751
+ if (run.seat) attributed[run.seat]++;
449
752
  else unattributed++;
450
753
  }
451
- const current = items.flatMap((row) => {
754
+ current = items.flatMap((row) => {
452
755
  if ((row.status !== 'launched' && row.status !== 'launching') || !row.launchId) return [row];
453
756
  const state = (deps.receipt ?? receipt)(root, row.launchId);
454
757
  if (state !== 'finished' && state !== 'not-started'
@@ -459,36 +762,63 @@ export async function tickHarnessQueue(deps: HarnessQueueDeps = {}): Promise<Que
459
762
  return [];
460
763
  });
461
764
  if (current.length !== items.length || current.some((row, index) => row !== items[index])) save(path, current);
462
- const charged = processes.filter((row) => !row.seat || row.seat === item.seat);
463
- const launchIds = new Set(charged.map((row) => row.launchId));
464
- const active = new Set(charged.map((row) => row.launchId ?? `pid:${row.pid}`)).size + current.filter((row) => row.seat === item.seat &&
465
- (row.status === 'launching' || row.status === 'launched')
466
- && (!row.launchId || !launchIds.has(row.launchId))).length;
467
- if (active >= cap) return wait(`seat ${item.seat}: ${active}/${cap}`, active);
468
- let pool: QueuePool;
469
- try { pool = (deps.pool ?? readHarnessQueuePool)(); }
470
- catch (error) { return wait(`pod lease status unavailable: ${String(error)}`, active); }
471
- if (![pool.running, pool.pending, pool.reserved, pool.limit].every((n) => Number.isSafeInteger(n) && n >= 0) || pool.limit < 1) {
472
- return wait('pod lease status incomplete', active);
473
- }
474
- const queueLaunching = current.filter((row) => row.status === 'launching' || row.status === 'launched').length;
475
- if (pool.running + pool.pending + pool.reserved + queueLaunching >= pool.limit) {
476
- return wait(`pool: ${pool.running}+${pool.pending}+${pool.reserved}+${queueLaunching}/${pool.limit}`, active);
477
- }
478
- const budget = decideSpawn({ seat: item.seat, running: active, caps, gate });
479
- if (!budget.allow) return wait(`${budget.reason}: seat ${item.seat}: ${active}/${budget.cap}`, active);
480
- const launching: QueueItem = { ...item, status: 'launching', launchId: `hq-${randomUUID()}` };
481
- save(path, current.map((row) => row.id === item.id ? launching : row));
482
- try {
483
- const pid = await (deps.launch ?? launch)(launching, queueLaunchArgs(launching), root);
484
- if (!Number.isSafeInteger(pid) || pid < 1) throw new Error('launcher returned no pid');
485
- const launched: QueueItem = { ...launching, pid, status: 'launched' };
486
- save(path, current.map((row) => row.id === item.id ? launched : row));
487
- observe('launched', { id: item.id, seat: item.seat, pid, kind: item.kind, launchId: launched.launchId }, deps);
488
- return { outcome: 'launched', item: launched, reason: 'spawned' };
489
- } catch (error) {
490
- observe('skipped', { id: item.id, seat: item.seat, reason: `launch uncertain: ${String(error)}`, launchId: launching.launchId }, deps);
491
- throw error;
765
+ let pool: QueuePool | undefined;
766
+ let poolReason: string | undefined;
767
+ let finishHeld: string | null | undefined;
768
+ for (const item of candidates) {
769
+ const injectedCap = deps.cap?.(item.seat);
770
+ 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;
776
+ if (active >= details.cap) { blocked.set(item.seat, { reason: seatCapReason(item.seat, active, details), active }); continue; }
777
+ if (!pool && !poolReason) {
778
+ try { pool = (deps.pool ?? readHarnessQueuePool)(); }
779
+ catch (error) { poolReason = `pod lease status unavailable: ${String(error)}`; }
780
+ if (pool && (![pool.running, pool.pending, pool.reserved, pool.limit].every((n) => Number.isSafeInteger(n) && n >= 0) || pool.limit < 1)) {
781
+ poolReason = 'pod lease status incomplete';
782
+ }
783
+ if (pool && !poolReason) {
784
+ const queueLaunching = current.filter((row) => row.status === 'launching' || row.status === 'launched').length;
785
+ if (pool.running + pool.pending + pool.reserved + queueLaunching >= pool.limit) {
786
+ poolReason = `pool: ${pool.running}+${pool.pending}+${pool.reserved}+${queueLaunching}/${pool.limit}`;
787
+ }
788
+ }
789
+ }
790
+ if (poolReason) { blocked.set(item.seat, { reason: poolReason, active }); continue; }
791
+ const budget = decideSpawn({ seat: item.seat, running: active, caps, gate, injectedCap });
792
+ if (!budget.allow) {
793
+ blocked.set(item.seat, { reason: `${budget.reason}: ${seatCapReason(item.seat, active, details)}`, active });
794
+ continue;
795
+ }
796
+ if (finishHeld === undefined) {
797
+ finishHeld = await finishGateReason(root, (deps.now ?? (() => new Date()))(), config.loops?.orchestrator?.finishGate ?? 'shadow',
798
+ pool!.running + pool!.pending + pool!.reserved
799
+ + current.filter((row) => row.status === 'launching' || row.status === 'launched').length, pool!.limit, deps);
800
+ }
801
+ if (finishHeld) { blocked.set(item.seat, { reason: finishHeld, active }); continue; }
802
+ const launching: QueueItem = { ...item, status: 'launching', launchId: `hq-${randomUUID()}` };
803
+ delete launching.waitingReason;
804
+ save(path, current.map((row) => row.id === item.id ? launching : row));
805
+ try {
806
+ const pid = await (deps.launch ?? launch)(launching, queueLaunchArgs(launching), root);
807
+ if (!Number.isSafeInteger(pid) || pid < 1) throw new Error('launcher returned no pid');
808
+ const launched: QueueItem = { ...launching, pid, status: 'launched' };
809
+ save(path, current.map((row) => row.id === item.id ? launched : row));
810
+ try { saveLastLaunchedSeat(path, item.seat); }
811
+ catch (error) {
812
+ try { debug.log('harness.queue', 'round-robin-save-failed', { id: item.id, seat: item.seat, reason: String(error) }, { level: 'warn' }); }
813
+ catch { /* Observation cannot change an already persisted launch. */ }
814
+ }
815
+ observe('launched', { id: item.id, seat: item.seat, pid, kind: item.kind, launchId: launched.launchId }, deps);
816
+ return { outcome: 'launched', item: launched, reason: 'spawned' };
817
+ } catch (error) {
818
+ observe('skipped', { id: item.id, seat: item.seat, reason: `launch uncertain: ${String(error)}`, launchId: launching.launchId }, deps);
819
+ throw error;
820
+ }
492
821
  }
822
+ return waiting();
493
823
  });
494
824
  }
@@ -727,10 +727,13 @@ export function buildHarnessSeams(deps: HarnessSeamsDeps): StagedHarnessSeams {
727
727
  let inspectionError: string | undefined;
728
728
  try {
729
729
  if (!deps.seams.readPrFiles) throw new Error('readPrFiles seam unavailable');
730
- const prFiles = await deps.seams.readPrFiles({ number: pr.number, cwd });
730
+ const prFiles = await deps.seams.readPrFiles({ number: pr.number, cwd, ...(deps.base ? { base: deps.base } : {}) });
731
731
  if (!Array.isArray(prFiles) || !prFiles.every((path) => typeof path === 'string')) throw new Error('invalid PR file list');
732
732
  holdPath = releasePathHold(prFiles);
733
- } catch (error) { inspectionError = String(error); }
733
+ } catch (error) {
734
+ const rateLimited = error instanceof Error && (error as { rateLimited?: boolean }).rateLimited === true;
735
+ inspectionError = rateLimited ? `rate-limit: ${String(error)}` : String(error);
736
+ }
734
737
  if (holdPath) {
735
738
  const protectedPath = holdPath;
736
739
  const prCwd = cwd;
@@ -745,7 +748,14 @@ export function buildHarnessSeams(deps: HarnessSeamsDeps): StagedHarnessSeams {
745
748
  observe('deployed-pr', { number: pr.number, url: pr.url, autoReview: false, mergeReason: 'release-path-hold' });
746
749
  } else if (inspectionError) {
747
750
  observe('release-path-inspection-failed', { number: pr.number, error: inspectionError });
748
- observe('deployed-pr', { number: pr.number, url: pr.url, autoReview: false, mergeReason: 'release-path-inspection-failed' });
751
+ observe('deployed-pr', {
752
+ number: pr.number,
753
+ url: pr.url,
754
+ autoReview: false,
755
+ mergeReason: inspectionError.startsWith('rate-limit:')
756
+ ? 'release-path-inspection-failed: rate-limit'
757
+ : 'release-path-inspection-failed',
758
+ });
749
759
  } else {
750
760
  let autoReviewLabeled = false;
751
761
  if (labels?.includes(AUTO_REVIEW_LABEL)) {