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
@@ -0,0 +1,56 @@
1
+ import type { ChecklistItem } from '../../release-loop/checklist.js';
2
+ import type { OrchestratorSeat } from '../../user-config.js';
3
+
4
+ const SEATS: readonly OrchestratorSeat[] = ['OP', 'TC', 'MK', 'UX'];
5
+ type WorkCell = Pick<ChecklistItem, 'owner' | 'status'>;
6
+
7
+ /** Allocate whole base slots by remaining work; this calculation does not decide launches. */
8
+ export function calculateSeatBaseShares({ totalSlots, currentRound, nextRound, seatCaps }: {
9
+ totalSlots: number;
10
+ currentRound: readonly WorkCell[];
11
+ nextRound: readonly WorkCell[];
12
+ seatCaps: Readonly<Record<OrchestratorSeat, number>>;
13
+ }): Record<OrchestratorSeat, number> {
14
+ if (!Number.isSafeInteger(totalSlots) || totalSlots < 0 ||
15
+ SEATS.some(seat => !Number.isSafeInteger(seatCaps[seat]) || seatCaps[seat] < 0)) {
16
+ throw new RangeError('totalSlots and seatCaps must be nonnegative safe integers');
17
+ }
18
+ const shares: Record<OrchestratorSeat, number> = { OP: 0, TC: 0, MK: 0, UX: 0 };
19
+ const weights: Record<OrchestratorSeat, number> = { OP: 0, TC: 0, MK: 0, UX: 0 };
20
+ for (const [cells, weight] of [[currentRound, 2], [nextRound, 1]] as const) {
21
+ for (const cell of cells) {
22
+ if (cell.status !== 'yellow') continue;
23
+ const seat = cell.owner?.split('/')[0];
24
+ if (SEATS.some(id => id === seat)) weights[seat as OrchestratorSeat] += weight;
25
+ }
26
+ }
27
+
28
+ let available = Math.min(totalSlots, SEATS.reduce((sum, seat) => sum + seatCaps[seat], 0));
29
+ let eligible = SEATS.filter(seat => weights[seat] > 0 && seatCaps[seat] > 0);
30
+ while (available > 0 && eligible.length > 0) {
31
+ const totalWeight = eligible.reduce((sum, seat) => sum + weights[seat], 0);
32
+ const ideal = (seat: OrchestratorSeat) => available * weights[seat] / totalWeight;
33
+ const capped = eligible.filter(seat => ideal(seat) >= seatCaps[seat]);
34
+ if (capped.length > 0) {
35
+ for (const seat of capped) {
36
+ shares[seat] = seatCaps[seat];
37
+ available -= seatCaps[seat];
38
+ }
39
+ eligible = eligible.filter(seat => !capped.includes(seat));
40
+ continue;
41
+ }
42
+ const remainders = eligible.map(seat => ({ seat, exact: ideal(seat) }));
43
+ for (const { seat, exact } of remainders) {
44
+ shares[seat] = Math.floor(exact);
45
+ available -= shares[seat];
46
+ }
47
+ remainders.sort((a, b) => (b.exact - Math.floor(b.exact)) - (a.exact - Math.floor(a.exact)));
48
+ for (const { seat } of remainders) {
49
+ if (available === 0) break;
50
+ shares[seat]++;
51
+ available--;
52
+ }
53
+ break;
54
+ }
55
+ return shares;
56
+ }
@@ -8,17 +8,22 @@ import { effectiveInstanceRoot } from '../../instance/resolve.js';
8
8
  import { getUserConfig } from '../../user-config.js';
9
9
  import { debug } from '../../debug/log.js';
10
10
  import { listSelfDevRuns, runSummaryLine, selfDevRunsDir } from '../../self-dev/run-store.js';
11
+ import { planAction, runSeatLoopOnce, type SeatItem } from '../../seat-loop/seat-loop.js';
12
+ import { harnessQueueOutcome } from '../../harness/harness-queue.js';
11
13
 
12
14
  export type Window = '08' | '12' | '18';
13
- export type Node = 'intake' | 'split' | 'place' | 'delegate' | 'reconcile' | 'report';
15
+ export type Node = 'intake' | 'split' | 'place' | 'delegate' | 'launch' | 'reconcile' | 'report';
14
16
  export type Mode = 'off' | 'shadow' | 'live';
15
17
  const SEAT_LOOP_IDS: Readonly<Record<string, string>> = { OP: 'op-seat', MK: 'cmo-seat', TC: 'tc-seat', UX: 'ux-seat' };
16
18
  export interface Cell { id: string; title: string; seat?: string; host?: string; version?: string; cardId: string; origin: 'candidate' | 'flow1a'; priority?: 'P0' | 'P1' | 'P2'; predecessors?: string[] }
17
19
  export interface TickState {
18
20
  runId: string; window: Window; mode: Mode; day: string;
19
- cards: TaskCard[]; skippedCards: Array<{ id: string; reason: string }>; cells: Cell[]; placed: number; unplaced?: number; delegated: number; wouldDelegate: number; rebalanced?: number;
21
+ cards: TaskCard[]; skippedCards: Array<{ id: string; reason: string }>; cells: Cell[]; placed: number; unplaced?: number; delegated: number; wouldDelegate: number; rebalanced?: number; rebalanceBlocked?: Array<{ id: string; reason: string }>;
20
22
  reconciled: { reached: number; progressing: number; blocked: number; unknown: number };
21
23
  runSummaries?: string[];
24
+ launched?: Array<{ seat: string; status: string; key?: string; queueId?: string; runId?: string }>;
25
+ queueOutcomes?: Array<{ queueId: string; key?: string; outcome: string }>;
26
+ steps: Array<{ node: Node; reason: string; count: number; targetLoopId?: string }>;
22
27
  nodes: Partial<Record<Node, 'ok' | 'skipped'>>;
23
28
  missing: string[];
24
29
  }
@@ -26,11 +31,13 @@ export interface TickState {
26
31
  export type SplitAdapter = (card: TaskCard, opts?: { shadow?: boolean }) => Promise<Array<{ id: string; title: string; seat?: string; host?: string }>> | Array<{ id: string; title: string; seat?: string; host?: string }>;
27
32
  /** RELPLAN1 `placeCell(input: PlacementCell)` — owner·priority·predecessors are required; the result carries no seat. */
28
33
  export type PlacementInput = { id: string; title: string; owner: string; priority: 'P0' | 'P1' | 'P2'; predecessors: string[] };
29
- export type PlaceAdapter = (cell: PlacementInput) => Promise<{ version?: string } | null> | { version?: string } | null;
30
- export type RebalanceAdapter = (version: string) => Promise<unknown[]> | unknown[];
34
+ export type PlaceAdapter = (cell: PlacementInput, deps?: { dryRun?: boolean; now?: Date }) => Promise<{ version?: string } | null> | { version?: string } | null;
35
+ export type RebalanceAdapter = (version: string) => Promise<{ decisions: unknown[]; blocked: Array<{ id: string; reason: string }> }> | { decisions: unknown[]; blocked: Array<{ id: string; reason: string }> };
31
36
  export interface TickDeps {
32
37
  root?: string; now?: Date; mode?: Mode; runId?: string; window?: Window;
33
38
  cards?: () => TaskCard[]; split?: SplitAdapter; placeCell?: PlaceAdapter;
39
+ seatTurn?: (seat: string, root: string) => Promise<{ status: string; queueId?: string; runId?: string; item?: { source: string; id: string } }>;
40
+ queueOutcome?: (queueId: string, root: string) => 'pending' | 'unknown' | 'retryable' | 'succeeded';
34
41
  loadAdapter?: (path: string, name: string) => Promise<unknown>;
35
42
  checklist?: (version: string) => Pick<ChecklistItem, 'id' | 'status'>[];
36
43
  observe?: (event: string, data: { loopId: string; runId: string; window: Window; node: Node; reason: string; count: number; targetLoopId?: string }) => void;
@@ -54,7 +61,10 @@ export function wishCards(cards: readonly TaskCard[]): TaskCard[] {
54
61
  return cards.filter(card => card.status === 'open' && card.goalId.startsWith('wish:') &&
55
62
  !card.sections.some(section => section.key === 'orch:split' || section.key.startsWith('orch:split:')));
56
63
  }
57
- export function reconciliation(cells: readonly Cell[], requests: readonly { key: string; status: string }[], checklist: (version: string) => Pick<ChecklistItem, 'id' | 'status'>[]): TickState['reconciled'] {
64
+ export function reconciliation(cells: readonly Cell[], requests: readonly { key: string; status: string }[], checklist: (version: string) => Pick<ChecklistItem, 'id' | 'status'>[],
65
+ queue: readonly { key: string; outcome: string }[] = []): TickState['reconciled'] {
66
+ // 체크리스트가 정본이고, 칸이 아직 체크리스트에 없을 때만 «그 칸의 의뢰 키»로 발사된 대기열 결과를 본다.
67
+ const queueByKey = new Map(queue.map(row => [row.key, row.outcome]));
58
68
  const result = { reached: 0, progressing: 0, blocked: 0, unknown: 0 };
59
69
  const byVersion = new Map<string, Pick<ChecklistItem, 'id' | 'status'>[]>();
60
70
  const keys = new Set(requests.filter(row => row.status === 'queued').map(row => row.key));
@@ -65,14 +75,20 @@ export function reconciliation(cells: readonly Cell[], requests: readonly { key:
65
75
  if (status === 'green' || status === 'done') result.reached++;
66
76
  else if (status === 'yellow') result.progressing++;
67
77
  else if (status === 'red') result.blocked++;
68
- else result.unknown++;
78
+ else {
79
+ const outcome = queueByKey.get(`orch:${cell.cardId}:${cell.id}`);
80
+ if (outcome === 'pending' || outcome === 'succeeded') result.progressing++;
81
+ else if (outcome === 'retryable') result.blocked++;
82
+ else result.unknown++;
83
+ }
69
84
  }
70
85
  return result;
71
86
  }
72
87
  export function reportLine(state: TickState): string {
73
88
  const r = state.reconciled;
74
89
  const line = `orchestrator ${state.window} cards=${state.cards.length} cells=${state.cells.length} placed=${state.missing.includes('relplan1-absent') ? 'missing' : state.placed} delegated=${state.mode === 'shadow' ? `would ${state.wouldDelegate}` : state.delegated} reconciled=${r.reached}/${r.progressing}/${r.blocked}/${r.unknown} mode=${state.mode}`;
75
- return state.runSummaries?.length ? `${line}\n${state.runSummaries.join('\n')}` : line;
90
+ const details = [...(state.rebalanceBlocked ?? []).map(row => `⛔ 이월 거부 ${row.id}: ${row.reason}`), ...(state.runSummaries ?? [])];
91
+ return details.length ? `${line}\n${details.join('\n')}` : line;
76
92
  }
77
93
  function readCardSnapshot(root: string): { cards: TaskCard[]; skippedCards: TickState['skippedCards'] } {
78
94
  const dir = taskCardsDir(root);
@@ -115,7 +131,7 @@ function stateFile(root: string, runId: string): string {
115
131
  if (!/^[a-zA-Z0-9._-]+$/.test(runId) || runId === '.' || runId === '..') throw new Error('Invalid orchestrator runId');
116
132
  return join(root, 'loop', 'orchestrator', `${runId}.json`);
117
133
  }
118
- function restorePrevious(root: string, day: string, mode: Mode): Pick<TickState, 'cards' | 'cells'> | null {
134
+ function restorePrevious(root: string, day: string, mode: Mode): Pick<TickState, 'cards' | 'cells' | 'launched'> | null {
119
135
  const dir = join(root, 'loop', 'orchestrator');
120
136
  if (!existsSync(dir)) return null;
121
137
  const states = readdirSync(dir).filter(name => /^[a-zA-Z0-9._-]+\.json$/.test(name))
@@ -125,15 +141,17 @@ function restorePrevious(root: string, day: string, mode: Mode): Pick<TickState,
125
141
  states.sort((a, b) => a.name.localeCompare(b.name));
126
142
  const cards = new Map<string, TaskCard>();
127
143
  const cells = new Map<string, Cell>();
144
+ const launched = new Map<string, NonNullable<TickState['launched']>[number]>();
128
145
  for (const { state } of states) {
129
146
  for (const card of state.cards) cards.set(card.id, card);
130
147
  for (const cell of state.cells) cells.set(`${cell.cardId}:${cell.id}`, cell);
148
+ for (const turn of state.launched ?? []) if (turn.queueId) launched.set(turn.queueId, turn);
131
149
  }
132
- return { cards: [...cards.values()], cells: [...cells.values()] };
150
+ return { cards: [...cards.values()], cells: [...cells.values()], launched: [...launched.values()] };
133
151
  }
134
152
  function initial(runId: string, window: Window, mode: Mode, day: string): TickState {
135
153
  return { runId, window, mode, day, cards: [], skippedCards: [], cells: [], placed: 0, delegated: 0, wouldDelegate: 0,
136
- reconciled: { reached: 0, progressing: 0, blocked: 0, unknown: 0 }, nodes: {}, missing: [] };
154
+ reconciled: { reached: 0, progressing: 0, blocked: 0, unknown: 0 }, steps: [], nodes: {}, missing: [] };
137
155
  }
138
156
  function graphRunId(): string | undefined {
139
157
  const context = process.env.ELANOUS_GRAPH_CONTEXT;
@@ -162,8 +180,9 @@ export async function runOrchestratorNode(node: Node, deps: TickDeps = {}): Prom
162
180
  const configured = deps.mode ?? getUserConfig().loops?.orchestrator?.mode;
163
181
  const mode: Mode = configured === 'off' || configured === 'live' ? configured : 'shadow';
164
182
  const state: TickState = existsSync(path) ? JSON.parse(readFileSync(path, 'utf8')) as TickState : initial(runId, window, mode, kstDay(now));
183
+ state.steps ??= [];
165
184
  if (state.window !== window || state.mode !== mode) throw new Error('orchestrator run window/mode changed');
166
- const predecessor: Partial<Record<Node, Node>> = { split: 'intake', place: 'split', delegate: 'place', reconcile: 'delegate', report: 'reconcile' };
185
+ const predecessor: Partial<Record<Node, Node>> = { split: 'intake', place: 'split', delegate: 'place', launch: 'delegate', reconcile: 'launch', report: 'reconcile' };
167
186
  const preceding = predecessor[node];
168
187
  if (preceding && !state.nodes[preceding]) throw new Error(`orchestrator ${node}: missing ${preceding} result`);
169
188
  const loadAdapter = deps.loadAdapter ?? optionalAdapter;
@@ -173,6 +192,7 @@ export async function runOrchestratorNode(node: Node, deps: TickDeps = {}): Prom
173
192
  };
174
193
  const observe = (event: string, reason = '', count = 0, targetLoopId?: string): void => {
175
194
  const data = { loopId: 'orchestrator', runId, window, node, reason, count, ...(targetLoopId ? { targetLoopId } : {}) };
195
+ if (event !== 'tick') state.steps.push({ node, reason, count, ...(targetLoopId ? { targetLoopId } : {}) });
176
196
  try { (deps.observe ?? ((name, payload) => debug.log('loop.orchestrator', name, payload)))(event, data); }
177
197
  catch { /* observation must not change the run */ }
178
198
  };
@@ -197,6 +217,7 @@ export async function runOrchestratorNode(node: Node, deps: TickDeps = {}): Prom
197
217
  if (!cell.id?.trim() || !cell.title?.trim()) throw new Error('invalid split cell');
198
218
  state.cells.push({ cardId: card.id, id: cell.id, title: cell.title, origin: split ? 'flow1a' : 'candidate', ...(cell.seat ? { seat: cell.seat } : {}), ...(cell.host !== undefined ? { host: cell.host } : {}) });
199
219
  }
220
+ observe('exchange', `split:${card.id}`, cells.length);
200
221
  if (split && cells.length && mode === 'live') {
201
222
  const store = new CardStore(root);
202
223
  try {
@@ -208,15 +229,24 @@ export async function runOrchestratorNode(node: Node, deps: TickDeps = {}): Prom
208
229
  const place = deps.placeCell ?? await adapter<PlaceAdapter>('../../release-loop/placement.js', 'placeCell');
209
230
  const available = typeof place === 'function';
210
231
  if (!available) { state.missing.push('relplan1-absent'); observe('node-missing', 'relplan1-absent', state.cells.length); }
211
- else if (mode === 'shadow') observe('exchange', 'relplan1-shadow-not-invoked', state.cells.length);
212
- if (mode === 'live' && available) for (const cell of state.cells) {
213
- if (cell.origin !== 'flow1a') continue;
214
- // TC review must-fix ②: map to the placer's required shape; a cell without an owner is unplaced, not a crash.
215
- if (!cell.seat) { state.unplaced = (state.unplaced ?? 0) + 1; observe('exchange', 'unplaced-no-owner', 1); continue; }
216
- const placed = await place({ id: cell.id, title: cell.title, owner: cell.seat, priority: cell.priority ?? 'P2', predecessors: cell.predecessors ?? [] });
217
- if (validVersion(placed?.version)) {
218
- cell.version = placed.version;
219
- state.placed++;
232
+ if (mode === 'shadow' && deps.placeCell) observe('exchange', 'relplan1-shadow-not-invoked', state.cells.length);
233
+ // 담당 없는 칸은 배치를 부르든 안 부르든 센다 — 배치 호출 여부와 집계를 묶지 않는다.
234
+ for (const cell of state.cells) {
235
+ if (cell.origin === 'flow1a' && (!cell.seat || !SEAT_LOOP_IDS[cell.seat])) { state.unplaced = (state.unplaced ?? 0) + 1; observe('exchange', 'unplaced-no-owner', 1); }
236
+ }
237
+ if (available && (mode === 'live' || !deps.placeCell)) for (const cell of state.cells) {
238
+ if (cell.origin !== 'flow1a' || !cell.seat || !SEAT_LOOP_IDS[cell.seat]) continue;
239
+ // RELPLAN's dryRun calculates the same release without writing a checklist in shadow.
240
+ try {
241
+ const placed = await place({ id: cell.id, title: cell.title, owner: cell.seat, priority: cell.priority ?? 'P2', predecessors: cell.predecessors ?? [] }, { dryRun: mode === 'shadow', now });
242
+ if (validVersion(placed?.version)) {
243
+ cell.version = placed.version;
244
+ state.placed++;
245
+ observe('exchange', mode === 'shadow' ? `would-place:${cell.id}:${cell.version}` : `placed:${cell.id}:${cell.version}`, 1);
246
+ } else { state.unplaced = (state.unplaced ?? 0) + 1; observe('exchange', 'unplaced-no-release', 1); }
247
+ } catch (error) {
248
+ state.unplaced = (state.unplaced ?? 0) + 1;
249
+ observe('exchange', `unplaced:${cell.id}:${String(error).slice(0, 160)}`, 1);
220
250
  }
221
251
  }
222
252
  } else if (node === 'delegate') {
@@ -228,9 +258,11 @@ export async function runOrchestratorNode(node: Node, deps: TickDeps = {}): Prom
228
258
  if (!targetLoopId) continue;
229
259
  const key = `orch:${cell.cardId}:${cell.id}`;
230
260
  if (keys.has(key)) continue;
231
- // Shadow shows what it would hand over even before a release is chosen (placement is not invoked in shadow).
232
- if (mode === 'shadow') { keys.add(key); state.wouldDelegate++; observe('would-delegate', validVersion(cell.version) ? key : `${key} (unplaced)`, 1, targetLoopId); continue; }
233
- if (!validVersion(cell.version)) continue;
261
+ if (!validVersion(cell.version)) {
262
+ if (mode === 'shadow') { keys.add(key); state.wouldDelegate++; observe('would-delegate', `${key} (unplaced)`, 1, targetLoopId); }
263
+ continue;
264
+ }
265
+ if (mode === 'shadow') { keys.add(key); state.wouldDelegate++; observe('would-delegate', key, 1, targetLoopId); continue; }
234
266
  keys.add(key);
235
267
  mkdirSync(dirname(path), { recursive: true });
236
268
  // TC review must-fix ①: the seat-requests journal contract needs receiptId, and appends are serialized under the
@@ -248,13 +280,53 @@ export async function runOrchestratorNode(node: Node, deps: TickDeps = {}): Prom
248
280
  state.delegated++;
249
281
  observe('exchange', 'queued', 1, targetLoopId);
250
282
  }
283
+ } else if (node === 'launch') {
284
+ const eligible = state.cells.filter(cell => cell.origin === 'flow1a' && cell.seat && SEAT_LOOP_IDS[cell.seat]
285
+ && validVersion(cell.version) && (mode === 'shadow' || readRequests(root).some(row => row.key === `orch:${cell.cardId}:${cell.id}` && row.status === 'queued')));
286
+ const seats = [...new Set(eligible.map(cell => cell.seat!))];
287
+ state.launched = [];
288
+ for (const seat of seats) {
289
+ if (mode === 'shadow') {
290
+ const cell = eligible.find(row => row.seat === seat)!;
291
+ const item: SeatItem = { source: 'request', id: `orch:${cell.cardId}:${cell.id}`, title: cell.title, text: cell.title };
292
+ const action = planAction(item, seat);
293
+ state.launched.push({ seat, key: item.id, status: action.kind === 'harness' ? 'would-launch' : `would-${action.kind}` });
294
+ observe('exchange', `would-${action.kind}:${seat}`, 1, SEAT_LOOP_IDS[seat]);
295
+ } else {
296
+ const turn = await (deps.seatTurn ?? ((assigned: string, stateRoot: string) => runSeatLoopOnce(assigned, {
297
+ root: stateRoot, config: { ...getUserConfig().loops?.seat, mode: 'live-safe', seats: [assigned] },
298
+ })))(seat, root);
299
+ // 자리 턴이 «이 틱이 넣은 의뢰»를 처리했을 때만 칸에 잇는다 — 대기열의 다른 의뢰는 이 카드의 발사가 아니다.
300
+ const targets = new Set(eligible.filter(row => row.seat === seat).map(row => `orch:${row.cardId}:${row.id}`));
301
+ const handled = 'item' in turn && turn.item?.source === 'request' ? turn.item.id : undefined;
302
+ const key = handled && targets.has(handled) ? handled : undefined;
303
+ if (handled && !key) observe('exchange', `seat-turn-other-request:${seat}`, 1, SEAT_LOOP_IDS[seat]);
304
+ state.launched.push({ seat, status: turn.status, ...(key ? { key } : {}),
305
+ ...('queueId' in turn && turn.queueId ? { queueId: turn.queueId } : {}),
306
+ ...('runId' in turn && turn.runId ? { runId: turn.runId } : {}) });
307
+ observe('exchange', `seat-turn:${seat}:${turn.status}`, 1, SEAT_LOOP_IDS[seat]);
308
+ }
309
+ }
251
310
  } else if (node === 'reconcile') {
252
- if (window !== '08' && state.cells.length === 0) {
311
+ if (window !== '08') {
312
+ // 새 카드가 있어도 앞 틱의 의뢰·발사를 같이 대조한다 — 지금 틱 것이 같은 키를 이긴다.
253
313
  const previous = restorePrevious(root, state.day, mode);
254
- if (previous) { state.cards = previous.cards; state.cells = previous.cells; }
314
+ if (previous) {
315
+ const merge = <T>(older: readonly T[], newer: readonly T[], key: (row: T) => string) =>
316
+ [...new Map([...older, ...newer].map(row => [key(row), row])).values()];
317
+ state.cards = merge(previous.cards, state.cards, card => card.id);
318
+ state.cells = merge(previous.cells, state.cells, cell => `${cell.cardId}:${cell.id}`);
319
+ state.launched = merge(previous.launched ?? [], state.launched ?? [], turn => turn.queueId ?? `${turn.seat}:${turn.key ?? ''}`);
320
+ }
255
321
  }
322
+ state.queueOutcomes = (state.launched ?? []).filter(turn => turn.queueId).map(turn => {
323
+ const outcome = (deps.queueOutcome ?? ((id: string, stateRoot: string) => harnessQueueOutcome(id, { root: stateRoot })))(turn.queueId!, root);
324
+ observe('exchange', `queue-outcome:${turn.queueId}:${outcome}`, 1, SEAT_LOOP_IDS[turn.seat]);
325
+ return { queueId: turn.queueId!, ...(turn.key ? { key: turn.key } : {}), outcome };
326
+ });
256
327
  const requests = readRequests(root);
257
- state.reconciled = reconciliation(state.cells, requests, mode === 'shadow' ? version => readChecklist(root, version) : (deps.checklist ?? (version => readChecklist(root, version))));
328
+ state.reconciled = reconciliation(state.cells, requests, mode === 'shadow' ? version => readChecklist(root, version) : (deps.checklist ?? (version => readChecklist(root, version))),
329
+ state.queueOutcomes.flatMap(row => row.key ? [{ key: row.key, outcome: row.outcome }] : []));
258
330
  const from = new Date(`${state.day}T00:00:00+09:00`).getTime();
259
331
  state.runSummaries = listSelfDevRuns(selfDevRunsDir(root))
260
332
  .filter(run => run.updatedAt >= from && run.updatedAt < from + 86_400_000)
@@ -268,7 +340,13 @@ export async function runOrchestratorNode(node: Node, deps: TickDeps = {}): Prom
268
340
  if (mode === 'shadow') observe('exchange', 'rebalance-shadow-not-invoked', versions.length);
269
341
  else {
270
342
  state.rebalanced = 0;
271
- for (const version of versions) state.rebalanced += (await rebalance(version)).length;
343
+ state.rebalanceBlocked = [];
344
+ for (const version of versions) {
345
+ const result = await rebalance(version);
346
+ state.rebalanced += result.decisions.length;
347
+ state.rebalanceBlocked.push(...result.blocked);
348
+ for (const blocked of result.blocked) observe('exchange', `rebalance-blocked: ${blocked.id} ${blocked.reason}`, 1);
349
+ }
272
350
  }
273
351
  }
274
352
  }
@@ -288,9 +366,9 @@ if (import.meta.main) {
288
366
  const node = process.argv[2];
289
367
  const index = process.argv.indexOf('--window');
290
368
  const window = index < 0 ? undefined : process.argv[index + 1];
291
- if (!['intake', 'split', 'place', 'delegate', 'reconcile', 'report'].includes(node ?? '') ||
369
+ if (!['intake', 'split', 'place', 'delegate', 'launch', 'reconcile', 'report'].includes(node ?? '') ||
292
370
  (window !== undefined && !['08', '12', '18'].includes(window))) {
293
- console.error('orchestrator: expected <intake|split|place|delegate|reconcile|report> [--window 08|12|18]');
371
+ console.error('orchestrator: expected <intake|split|place|delegate|launch|reconcile|report> [--window 08|12|18]');
294
372
  process.exitCode = 2;
295
373
  } else {
296
374
  void (async () => {
@@ -2,16 +2,19 @@ import { appendFileSync, existsSync, mkdirSync, readFileSync, realpathSync } fro
2
2
  import { dirname, isAbsolute, join, resolve, sep } from 'node:path';
3
3
  import { debug } from '../../debug/log.js';
4
4
  import { defaultListHarnessProcesses, type HarnessProcessRecord } from '../../harness/harness-cli-command.js';
5
- import { effectiveInstanceRoot } from '../../instance/resolve.js';
5
+ import { effectiveInstanceRoot, releaseLedgerRoot } from '../../instance/resolve.js';
6
6
  import { devVersion, listChecklist, type ChecklistItem } from '../../release-loop/checklist.js';
7
+ import { listSchedules } from '../../release-loop/release-schedule.js';
7
8
  import { withFileLockSync } from '../../storage/file-lock.js';
8
9
  import { getUserConfig, ORCHESTRATOR_DEFAULTS, type OrchestratorLoopConfig, type OrchestratorSeat } from '../../user-config.js';
9
10
  import { finishAdvice, measureFinish, type FinishAdvice, type FinishMetrics, type MeasureFinishDeps } from './finish-rate.js';
11
+ import { collectAuthorDepth, type AuthorDepthResult, type CollectAuthorDepthDeps } from './author-depth.js';
12
+ import { calculateSeatBaseShares } from './seat-share.js';
10
13
 
11
14
  export const TRAFFIC_SEATS = ['OP', 'TC', 'MK', 'UX'] as const;
12
15
  export type TrafficCell = Pick<ChecklistItem, 'id' | 'title' | 'owner' | 'status'>;
13
16
  export type TrafficProcess = Pick<HarnessProcessRecord, 'command' | 'elapsedSeconds' | 'cwd'> & { seat: OrchestratorSeat | null };
14
- export type TrafficSeatRow = { seat: OrchestratorSeat; running: number; cap: number; idleFor: number | null; idle: boolean; nextCell: TrafficCell | null };
17
+ export type TrafficSeatRow = { seat: OrchestratorSeat; running: number; cap: number; baseShare: number | null; borrowed: number; lent: number; launchCap: number; idleFor: number | null; idle: boolean; nextCell: TrafficCell | null };
15
18
  export type TrafficResult = { seats: TrafficSeatRow[]; unassigned: number; now: Date; idleMinutes: number; finish?: { metrics: FinishMetrics; advice: FinishAdvice } };
16
19
 
17
20
  /** Same bounded walk as the CTX1 hook: a marker at the git root is checked before stopping. */
@@ -49,15 +52,23 @@ function realOrResolved(path: string): string {
49
52
  const IS_LAUNCH = /(?:^|\s)(?:\S*\/)?elanous\.mjs\s+(?:(?:--test|--config-dir\s+\S+)\s+)?harness\s+(?:ask|say)(?=\s|$)/;
50
53
 
51
54
  /** Process-to-seat resolution occurs at the boundary; this decision does no IO. */
52
- export function trafficTick({ processes, now, caps, lastLaunchAt, openCells, idleMinutes = 30 }: {
55
+ export function trafficTick({ processes, now, caps, lastLaunchAt, idleSince, openCells, nextRound, totalSlots, idleMinutes = 30 }: {
53
56
  processes: readonly TrafficProcess[];
54
57
  now: Date;
55
58
  caps: Readonly<Record<OrchestratorSeat, number>>;
56
59
  lastLaunchAt?: Partial<Record<OrchestratorSeat, Date | null>>;
60
+ idleSince?: Partial<Record<OrchestratorSeat, Date | null>>;
57
61
  openCells: readonly TrafficCell[];
62
+ nextRound?: readonly TrafficCell[];
63
+ totalSlots?: number;
58
64
  idleMinutes?: number;
59
65
  }): TrafficResult {
60
66
  const launches = processes.filter(process => IS_LAUNCH.test(process.command));
67
+ const slotBudget = totalSlots ?? TRAFFIC_SEATS.reduce((sum, seat) => sum + caps[seat], 0);
68
+ // A missing next round is unknown, not an empty round with zero remaining work.
69
+ const baseShares = nextRound === undefined ? null : calculateSeatBaseShares({
70
+ totalSlots: slotBudget, currentRound: openCells, nextRound, seatCaps: caps,
71
+ });
61
72
  const seats = TRAFFIC_SEATS.map((seat): TrafficSeatRow => {
62
73
  const owned = launches.filter(process => process.seat === seat);
63
74
  const latest = owned.reduce<number | null>((current, process) => {
@@ -66,12 +77,57 @@ export function trafficTick({ processes, now, caps, lastLaunchAt, openCells, idl
66
77
  return current === null ? start : Math.max(current, start);
67
78
  }, null);
68
79
  const fallback = owned.length ? lastLaunchAt?.[seat]?.getTime() : undefined;
69
- const last = latest ?? (fallback !== undefined && Number.isFinite(fallback) ? fallback : null);
80
+ const idleStart = idleSince?.[seat]?.getTime();
81
+ const last = idleStart !== undefined && Number.isFinite(idleStart) ? idleStart
82
+ : latest ?? (fallback !== undefined && Number.isFinite(fallback) ? fallback : null);
70
83
  const idleFor = last === null ? null : Math.max(0, Math.floor((now.getTime() - last) / 60_000));
71
- const idle = owned.length < caps[seat] && last !== null && now.getTime() - last > idleMinutes * 60_000;
72
- return { seat, running: owned.length, cap: caps[seat], idleFor, idle,
73
- nextCell: idle ? openCells.find(cell => cell.status !== 'done' && cell.owner?.split('/')[0] === seat) ?? null : null };
84
+ const nextCell = openCells.find(cell => cell.status !== 'done' && cell.owner?.split('/')[0] === seat) ?? null;
85
+ const baseShare = baseShares?.[seat] ?? null;
86
+ return { seat, running: owned.length, cap: caps[seat], baseShare, borrowed: 0, lent: 0,
87
+ launchCap: baseShare === null ? caps[seat] : Math.max(owned.length, baseShare), idleFor,
88
+ idle: owned.length < caps[seat] && last !== null && now.getTime() - last > idleMinutes * 60_000, nextCell };
74
89
  });
90
+ if (baseShares !== null) {
91
+ // Account for outstanding loans before lending new capacity. A returning
92
+ // lender cannot launch into a slot still occupied by the borrower's run.
93
+ for (const borrower of seats) {
94
+ let excess = Math.max(0, borrower.running - borrower.baseShare!);
95
+ borrower.borrowed += excess;
96
+ for (const lender of seats) {
97
+ if (!excess) break;
98
+ if (lender === borrower) continue;
99
+ const amount = Math.min(excess, Math.max(0, lender.baseShare! - lender.running - lender.lent));
100
+ lender.lent += amount;
101
+ excess -= amount;
102
+ }
103
+ }
104
+ for (const lender of seats) {
105
+ if (lender.nextCell) continue;
106
+ let available = Math.max(0, lender.baseShare! - lender.running - lender.lent);
107
+ for (const borrower of seats) {
108
+ if (!available) break;
109
+ if (borrower === lender || !borrower.nextCell) continue;
110
+ const needed = Math.max(0, Math.min(borrower.cap, borrower.running + 1) - borrower.launchCap);
111
+ const amount = Math.min(available, needed);
112
+ lender.lent += amount;
113
+ borrower.borrowed += amount;
114
+ borrower.launchCap += amount;
115
+ available -= amount;
116
+ }
117
+ }
118
+ for (const lender of seats) lender.launchCap = Math.max(lender.running, lender.launchCap - lender.lent);
119
+ }
120
+ // Reserve one available global slot per possible request, including unknown-seat
121
+ // launches in the occupied count; returning a lent share does not end its old run.
122
+ let freeSlots = Math.max(0, slotBudget - launches.length);
123
+ for (const row of seats) {
124
+ if (row.nextCell && row.idle && row.running < row.launchCap) {
125
+ if (freeSlots > 0) freeSlots--;
126
+ else row.launchCap = row.running;
127
+ }
128
+ row.idle = row.idle && (row.nextCell === null || row.running < row.launchCap);
129
+ if (!row.idle) row.nextCell = null;
130
+ }
75
131
  return { seats, unassigned: launches.filter(process => process.seat === null).length, now, idleMinutes };
76
132
  }
77
133
 
@@ -92,7 +148,8 @@ export interface TrafficApplyDeps {
92
148
  export function applyTraffic(result: TrafficResult, deps: TrafficApplyDeps = {}): number {
93
149
  const log = deps.log ?? ((category: string, event: string, data: Record<string, unknown>) => debug.log(category, event, data));
94
150
  const mode = deps.mode ?? 'shadow';
95
- log('loop.orchestrator', 'tick', { mode, seats: result.seats.map(({ seat, running, cap, idleFor, idle }) => ({ seat, running, cap, idleFor, idle })), unassigned: result.unassigned });
151
+ log('loop.orchestrator', 'tick', { mode, seats: result.seats.map(({ seat, running, cap, baseShare, borrowed, lent, launchCap, idleFor, idle }) =>
152
+ ({ seat, running, cap, baseShare, borrowed, lent, launchCap, idleFor, idle })), unassigned: result.unassigned });
96
153
  if (result.finish) {
97
154
  const { finishSlots, launchSlots, state, reasons } = result.finish.advice;
98
155
  log('loop.orchestrator', 'would-rebalance', { finishSlots, launchSlots, state, reasons });
@@ -140,6 +197,49 @@ export function trafficLine(result: TrafficResult, mode: 'shadow' | 'live'): str
140
197
  return `${prefix} · finish rate=${rate(metrics.landingRate)} stale=${metrics.staleDrafts ?? '?'} conflict=${rate(metrics.conflictRatio)} → finish=${advice.finishSlots}/${advice.finishSlots + advice.launchSlots}`;
141
198
  }
142
199
 
200
+ export type AuthorShadow = { cells: number; held: number; queuedForAuthor: number; status: 'complete' | 'incomplete'; receiptFailures: number };
201
+
202
+ /** Traffic stays authoritative: shadow collection and its ledger cannot change its decision or exit status. */
203
+ export function runTrafficTick({ processes, now, caps, openCells, nextRound, totalSlots, mode, finishDeps, applyDeps, authorDeps, collectAuthor = collectAuthorDepth, log = debug.log.bind(debug) }: {
204
+ processes: readonly TrafficProcess[];
205
+ now: Date;
206
+ caps: Readonly<Record<OrchestratorSeat, number>>;
207
+ openCells: readonly TrafficCell[];
208
+ nextRound?: readonly TrafficCell[];
209
+ totalSlots?: number;
210
+ mode: 'shadow' | 'live';
211
+ finishDeps?: MeasureFinishDeps;
212
+ applyDeps?: TrafficApplyDeps;
213
+ authorDeps?: Omit<CollectAuthorDepthDeps, 'caps' | 'running' | 'now'>;
214
+ collectAuthor?: (deps: CollectAuthorDepthDeps) => AuthorDepthResult;
215
+ log?: (category: string, event: string, data: Record<string, unknown>) => void;
216
+ }): { result: TrafficResult; queued: number; authorShadow: AuthorShadow | null } {
217
+ const metrics = measureFinish(finishDeps, now);
218
+ const advice = finishAdvice(metrics, TRAFFIC_SEATS.reduce((sum, seat) => sum + caps[seat], 0));
219
+ const result = { ...trafficTick({ processes, now, caps, openCells, nextRound, totalSlots: totalSlots ?? advice.launchSlots }),
220
+ finish: { metrics, advice } };
221
+ const queued = applyTraffic(result, { ...applyDeps, mode });
222
+ let authorShadow: AuthorShadow | null = null;
223
+ let shadowFailure: Record<string, unknown> | null = null;
224
+ try {
225
+ const depth = collectAuthor({ ...authorDeps, now, caps: () => caps,
226
+ running: () => Object.fromEntries(result.seats.map(row => [row.seat, row.running])) });
227
+ const receiptFailures = depth.receiptFailures ?? depth.unreadable.filter(item => item.source === 'author-ledger').length;
228
+ const incomplete = depth.unreadable.length > 0 || !depth.receiptCounts;
229
+ authorShadow = { cells: depth.seats.reduce((total, row) => total + row.wouldAuthor.length, 0),
230
+ held: depth.receiptCounts?.held ?? 0, queuedForAuthor: depth.receiptCounts?.queuedForAuthor ?? 0,
231
+ status: incomplete ? 'incomplete' : 'complete', receiptFailures };
232
+ if (incomplete) shadowFailure = { unreadable: depth.unreadable, ...(depth.receiptCounts ? {} : { reason: 'receipt counts unavailable' }) };
233
+ } catch (error) {
234
+ shadowFailure = { reason: error instanceof Error ? error.message : String(error) };
235
+ }
236
+ if (shadowFailure) {
237
+ try { log('loops.author-depth', 'shadow-failed', shadowFailure); }
238
+ catch { /* Shadow logging cannot change traffic's outcome. */ }
239
+ }
240
+ return { result, queued, authorShadow };
241
+ }
242
+
143
243
  if (import.meta.main) {
144
244
  try {
145
245
  const args = process.argv.slice(2);
@@ -154,9 +254,15 @@ if (import.meta.main) {
154
254
  if (observation.status !== 'ok') throw new Error(`process observation ${observation.status}`);
155
255
  const now = new Date();
156
256
  const processes = observation.records.map(process => ({ ...process, seat: seatOfTree(process.cwdStatus === 'unknown' ? undefined : process.cwd, cfg) }));
157
- const result = withFinishAdvice(trafficTick({ processes, now, caps: cfg.seatCaps, openCells: listChecklist(version).items }));
158
- const queued = applyTraffic(result, { mode: cfg.trafficMode });
159
- if (args.includes('--json')) console.log(JSON.stringify({ ...result, mode: cfg.trafficMode, queued, version }));
257
+ const upcoming = listSchedules(releaseLedgerRoot()).filter(row => Date.parse(row.cutAt) > now.getTime())
258
+ .sort((a, b) => Date.parse(a.cutAt) - Date.parse(b.cutAt));
259
+ const currentIndex = upcoming.findIndex(row => row.version === version);
260
+ const nextVersion = currentIndex < 0 ? upcoming[0]?.version : upcoming[currentIndex + 1]?.version;
261
+ const { result, queued, authorShadow } = runTrafficTick({
262
+ processes, now, caps: cfg.seatCaps, openCells: listChecklist(version).items,
263
+ ...(nextVersion ? { nextRound: listChecklist(nextVersion).items } : {}), mode: cfg.trafficMode,
264
+ });
265
+ if (args.includes('--json')) console.log(JSON.stringify({ ...result, mode: cfg.trafficMode, queued, version, authorShadow }));
160
266
  else console.log(trafficLine(result, cfg.trafficMode));
161
267
  } catch (error) {
162
268
  console.error(`traffic: ${error instanceof Error ? error.message : String(error)}`);