elanous 0.2.17 → 0.2.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (309) hide show
  1. package/apps/pwa/out/404/index.html +1 -1
  2. package/apps/pwa/out/404.html +1 -1
  3. package/apps/pwa/out/_next/static/P6YTVgzuZz5oTZhY4Din_/_buildManifest.js +1 -0
  4. package/apps/pwa/out/_next/static/chunks/{1307-aac7332576c4d6fc.js → 1307-30dff22c9e1f3073.js} +1 -1
  5. package/apps/pwa/out/_next/static/chunks/195-fb1d7690bf30bab4.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/1986-33e13d648a9df765.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/2537-6c289376caa88ca1.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/2699-325cc923ba7b8631.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/3857-4f936cdb30322aa6.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/4554-5761612531d0146b.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/4565-3d7e3fdeb43686a8.js +1 -0
  12. package/apps/pwa/out/_next/static/chunks/5977-a11f37254196b659.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/{6158.9c0449a9a8d0064b.js → 6158.af2e93b7adff709b.js} +1 -1
  14. package/apps/pwa/out/_next/static/chunks/6877-74399f69c7da1433.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/7797-df5aa0381fdc078f.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/8401-8615a660c84cdb03.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/8695-846034ce44852c70.js +1 -0
  18. package/apps/pwa/out/_next/static/chunks/app/approvals/{page-3143b07a817d1da9.js → page-d92f7ee3994e84c7.js} +1 -1
  19. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-17ba086ef82d339b.js → page-4733c967b56a4277.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/board/{page-7eb661ff23af7da2.js → page-d22fd849cc22f104.js} +1 -1
  21. package/apps/pwa/out/_next/static/chunks/app/botlab/page-bb742f995c6769e7.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/app/bots/{page-6e5c3a867557298b.js → page-02b073a2ac793b83.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/app/ceo/page-60c3da708f9a0ccc.js +1 -0
  24. package/apps/pwa/out/_next/static/chunks/app/chat/page-8a9b3594bc23289b.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/app/consult/{page-5e77cd1802567c89.js → page-a0eb02638b1e6f31.js} +1 -1
  26. package/apps/pwa/out/_next/static/chunks/app/control/page-308111abadf35670.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/app/decisions/page-6eaf7bac37830c21.js +1 -0
  28. package/apps/pwa/out/_next/static/chunks/app/design-check/{page-4187666132fc98c4.js → page-5d97b2901e0d1ea6.js} +1 -1
  29. package/apps/pwa/out/_next/static/chunks/app/editor/{page-09328d499162bae4.js → page-c39554c321576982.js} +1 -1
  30. package/apps/pwa/out/_next/static/chunks/app/exec/{page-29ff3a8f84f03b6a.js → page-8c3d8e63ae2e1c98.js} +1 -1
  31. package/apps/pwa/out/_next/static/chunks/app/field/{page-80ec7401bc83f511.js → page-9b72a07d424701c6.js} +1 -1
  32. package/apps/pwa/out/_next/static/chunks/app/inside/page-bd5d586429167fea.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/app/intake/{page-fc7d86467291ae38.js → page-a572f58cd3ca5864.js} +1 -1
  34. package/apps/pwa/out/_next/static/chunks/app/layout-59b7834d497c2255.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/app/live/page-be5ea965a07fbc41.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/app/loops/{page-0e2d6985fb42cd2f.js → page-c130b63862437814.js} +1 -1
  37. package/apps/pwa/out/_next/static/chunks/app/market/{page-13e4108999f58d13.js → page-fa244bd146a00ae8.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-e8296ee284aa1293.js → page-44d56d6391965a6a.js} +1 -1
  39. package/apps/pwa/out/_next/static/chunks/app/morning/{page-043a9111e74f6b24.js → page-83a700d4c754d802.js} +1 -1
  40. package/apps/pwa/out/_next/static/chunks/app/{not-found-23a4601ceb7f452e.js → not-found-7b3170f374f77b9d.js} +1 -1
  41. package/apps/pwa/out/_next/static/chunks/app/observatory/page-5f23d0c66a3b4793.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-10d958605a77b3bd.js → page-a3c58f5c72a5f3f9.js} +1 -1
  43. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-a2c4625596f8d69c.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-0e9099531d239f11.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/app/outputs/{page-7a1d85e121065cc8.js → page-e8e1ecaea7cd71d2.js} +1 -1
  46. package/apps/pwa/out/_next/static/chunks/app/page-a1b7ffed710a93e0.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/app/reflection/{page-12916918988ac97e.js → page-1467047104141297.js} +1 -1
  48. package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-d8a3347a9d0c3f4a.js → page-2da6f9b8a38c6978.js} +1 -1
  49. package/apps/pwa/out/_next/static/chunks/app/sessions/page-102310930791197a.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-f331e38fefc00393.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/settings/page-6ed0b73bae8351d8.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-1d824510df025d18.js → page-44d5f767491692bf.js} +1 -1
  53. package/apps/pwa/out/_next/static/chunks/app/setup/page-43336055451f6e37.js +1 -0
  54. package/apps/pwa/out/_next/static/chunks/app/share/{page-7f49fe645d5132f7.js → page-3cc47dca0922ab9a.js} +1 -1
  55. package/apps/pwa/out/_next/static/chunks/app/showroom/page-a77ee355a2304ff0.js +1 -0
  56. package/apps/pwa/out/_next/static/chunks/app/tasks/{page-27d71f131acd9320.js → page-b17cd46cf0e9b8cf.js} +1 -1
  57. package/apps/pwa/out/_next/static/chunks/app/term/page-61553851aa606146.js +1 -0
  58. package/apps/pwa/out/_next/static/chunks/app/today/page-a1f585e154e61c59.js +1 -0
  59. package/apps/pwa/out/_next/static/chunks/app/trace/{page-424659bacfb8cdd7.js → page-7bf54c9bb9274af8.js} +1 -1
  60. package/apps/pwa/out/_next/static/chunks/app/vault/page-5150ef17b5383a57.js +1 -0
  61. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-b9d8ce70792c1672.js +1 -0
  62. package/apps/pwa/out/_next/static/chunks/app/workflows/page-a5a3de231f2cc7d5.js +1 -0
  63. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-5d4fef9ccd7d406b.js → page-134da468538baf48.js} +1 -1
  64. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-31ac4afa2dcd915c.js +1 -0
  65. package/apps/pwa/out/_next/static/chunks/{webpack-539cde99554be20f.js → webpack-c2782b330447ffb6.js} +1 -1
  66. package/apps/pwa/out/_next/static/css/7fa358fec7da7c47.css +1 -0
  67. package/apps/pwa/out/approvals/index.html +1 -1
  68. package/apps/pwa/out/approvals/index.txt +14 -14
  69. package/apps/pwa/out/autopilot/index.html +1 -1
  70. package/apps/pwa/out/autopilot/index.txt +14 -14
  71. package/apps/pwa/out/board/index.html +1 -1
  72. package/apps/pwa/out/board/index.txt +14 -14
  73. package/apps/pwa/out/botlab/index.html +1 -1
  74. package/apps/pwa/out/botlab/index.txt +14 -14
  75. package/apps/pwa/out/bots/index.html +1 -1
  76. package/apps/pwa/out/bots/index.txt +14 -14
  77. package/apps/pwa/out/ceo/index.html +1 -1
  78. package/apps/pwa/out/ceo/index.txt +14 -14
  79. package/apps/pwa/out/chat/index.html +1 -1
  80. package/apps/pwa/out/chat/index.txt +14 -14
  81. package/apps/pwa/out/consult/index.html +1 -1
  82. package/apps/pwa/out/consult/index.txt +14 -14
  83. package/apps/pwa/out/control/index.html +1 -1
  84. package/apps/pwa/out/control/index.txt +14 -14
  85. package/apps/pwa/out/decisions/index.html +1 -0
  86. package/apps/pwa/out/decisions/index.txt +30 -0
  87. package/apps/pwa/out/design-check/index.html +1 -1
  88. package/apps/pwa/out/design-check/index.txt +14 -14
  89. package/apps/pwa/out/editor/index.html +1 -1
  90. package/apps/pwa/out/editor/index.txt +14 -14
  91. package/apps/pwa/out/exec/index.html +1 -1
  92. package/apps/pwa/out/exec/index.txt +14 -14
  93. package/apps/pwa/out/field/index.html +1 -1
  94. package/apps/pwa/out/field/index.txt +14 -14
  95. package/apps/pwa/out/healthz +1 -1
  96. package/apps/pwa/out/index.html +1 -1
  97. package/apps/pwa/out/index.txt +14 -14
  98. package/apps/pwa/out/inside/index.html +1 -1
  99. package/apps/pwa/out/inside/index.txt +14 -14
  100. package/apps/pwa/out/intake/index.html +1 -1
  101. package/apps/pwa/out/intake/index.txt +14 -14
  102. package/apps/pwa/out/live/index.html +1 -1
  103. package/apps/pwa/out/live/index.txt +14 -14
  104. package/apps/pwa/out/loops/index.html +1 -1
  105. package/apps/pwa/out/loops/index.txt +14 -14
  106. package/apps/pwa/out/market/index.html +1 -1
  107. package/apps/pwa/out/market/index.txt +14 -14
  108. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  109. package/apps/pwa/out/missions/placeholder/index.txt +14 -14
  110. package/apps/pwa/out/morning/index.html +1 -1
  111. package/apps/pwa/out/morning/index.txt +14 -14
  112. package/apps/pwa/out/observatory/index.html +1 -1
  113. package/apps/pwa/out/observatory/index.txt +14 -14
  114. package/apps/pwa/out/ops/checklist/index.html +1 -1
  115. package/apps/pwa/out/ops/checklist/index.txt +14 -14
  116. package/apps/pwa/out/ops/release/index.html +1 -1
  117. package/apps/pwa/out/ops/release/index.txt +14 -14
  118. package/apps/pwa/out/ops/seats/index.html +1 -1
  119. package/apps/pwa/out/ops/seats/index.txt +14 -14
  120. package/apps/pwa/out/outputs/index.html +1 -1
  121. package/apps/pwa/out/outputs/index.txt +14 -14
  122. package/apps/pwa/out/reflection/index.html +1 -1
  123. package/apps/pwa/out/reflection/index.txt +15 -15
  124. package/apps/pwa/out/scheduler/index.html +1 -1
  125. package/apps/pwa/out/scheduler/index.txt +14 -14
  126. package/apps/pwa/out/sessions/index.html +1 -1
  127. package/apps/pwa/out/sessions/index.txt +14 -14
  128. package/apps/pwa/out/settings/devices/index.html +1 -1
  129. package/apps/pwa/out/settings/devices/index.txt +14 -14
  130. package/apps/pwa/out/settings/index.html +1 -1
  131. package/apps/pwa/out/settings/index.txt +14 -14
  132. package/apps/pwa/out/setup/done/index.html +1 -1
  133. package/apps/pwa/out/setup/done/index.txt +14 -14
  134. package/apps/pwa/out/setup/index.html +1 -1
  135. package/apps/pwa/out/setup/index.txt +14 -14
  136. package/apps/pwa/out/share/index.html +1 -1
  137. package/apps/pwa/out/share/index.txt +14 -14
  138. package/apps/pwa/out/showroom/index.html +1 -1
  139. package/apps/pwa/out/showroom/index.txt +14 -14
  140. package/apps/pwa/out/tasks/index.html +1 -1
  141. package/apps/pwa/out/tasks/index.txt +14 -14
  142. package/apps/pwa/out/term/index.html +1 -1
  143. package/apps/pwa/out/term/index.txt +14 -14
  144. package/apps/pwa/out/today/index.html +1 -1
  145. package/apps/pwa/out/today/index.txt +14 -14
  146. package/apps/pwa/out/trace/index.html +1 -1
  147. package/apps/pwa/out/trace/index.txt +14 -14
  148. package/apps/pwa/out/vault/index.html +1 -1
  149. package/apps/pwa/out/vault/index.txt +14 -14
  150. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  151. package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
  152. package/apps/pwa/out/workflows/index.html +1 -1
  153. package/apps/pwa/out/workflows/index.txt +14 -14
  154. package/apps/pwa/out/workspace/index.html +1 -1
  155. package/apps/pwa/out/workspace/index.txt +14 -14
  156. package/apps/pwa/out/worktrees/index.html +1 -1
  157. package/apps/pwa/out/worktrees/index.txt +14 -14
  158. package/package.json +2 -1
  159. package/samples/workflows/templates/approval-gated-deploy.yaml +30 -0
  160. package/samples/workflows/templates/chat-driven-research.yaml +35 -0
  161. package/samples/workflows/templates/discord-listen-and-respond.yaml +29 -0
  162. package/samples/workflows/templates/http-poll-then-alert.yaml +30 -0
  163. package/samples/workflows/templates/manual-quickstart.yaml +21 -0
  164. package/samples/workflows/templates/scheduled-daily-digest.yaml +30 -0
  165. package/samples/workflows/templates/webhook-deploy-hook.yaml +27 -0
  166. package/scripts/ci-public-export-import-gate.ts +7 -2
  167. package/scripts/ci-typecheck-changed.ts +21 -6
  168. package/scripts/discord-seat-probe.ts +3 -2
  169. package/scripts/docs/generate-cli-anatomy.ts +162 -0
  170. package/scripts/goals/authoring-metrics.ts +73 -0
  171. package/scripts/landing-heal/must-fix-match.ts +7 -7
  172. package/scripts/landing-heal/run-mission.ts +3 -2
  173. package/scripts/public-export-test-run.ts +37 -2
  174. package/scripts/release-loop/checklist-gate-node.ts +13 -11
  175. package/scripts/release-loop/cut-branch.ts +2 -1
  176. package/scripts/release-loop/cutoff.ts +4 -2
  177. package/scripts/release-loop/docs-land-node.ts +8 -1
  178. package/scripts/release-loop/docs-node.ts +1 -1
  179. package/scripts/release-loop/gate-node.ts +99 -14
  180. package/scripts/release-loop/node-verdict.ts +2 -1
  181. package/scripts/release-loop/npm-publish-node.ts +11 -7
  182. package/scripts/release-loop/ops-upgrade-node.ts +8 -1
  183. package/scripts/release-loop/preflight.ts +246 -0
  184. package/scripts/release-loop/publish-node.ts +8 -1
  185. package/scripts/release-loop/release-readiness.ts +11 -6
  186. package/scripts/release-loop/release-version.ts +48 -0
  187. package/scripts/release-loop/resume-release.ts +115 -0
  188. package/scripts/release-loop/unattended-release.ts +43 -7
  189. package/scripts/release-loop/version-node.ts +145 -40
  190. package/scripts/rhythm/daily.ts +25 -4
  191. package/scripts/skills/public-check.ts +62 -12
  192. package/scripts/test-deterministic.ts +51 -33
  193. package/scripts/test-diet/effectiveness.ts +62 -1
  194. package/scripts/test-diet/lib.ts +1 -1
  195. package/scripts/test-diet/node.ts +8 -1
  196. package/src/agent-mission/review-loop.ts +4 -3
  197. package/src/agent-substrate/pr-comment-meta.ts +20 -0
  198. package/src/autopilot/build/llm-conflict-merge.ts +28 -8
  199. package/src/briefing/brief-items.ts +86 -17
  200. package/src/cli/autopilot-cli.ts +30 -11
  201. package/src/cli/brief-cli.ts +36 -2
  202. package/src/cli/freeze-cli.ts +3 -2
  203. package/src/cli/intake-cli.ts +73 -1
  204. package/src/cli/notify-cli.ts +75 -0
  205. package/src/cli/provider-cli.ts +12 -2
  206. package/src/cli/release-cli.ts +101 -17
  207. package/src/cli/tasks-cli.ts +309 -3
  208. package/src/context-bus/condense.ts +19 -3
  209. package/src/context-bus/context-day.ts +77 -24
  210. package/src/context-bus/context-first.ts +33 -0
  211. package/src/discord-self-message.ts +20 -2
  212. package/src/discord-slash-wire.ts +1 -0
  213. package/src/domains/outbound-alert.ts +200 -27
  214. package/src/domains/outbound-log-probe.ts +16 -1
  215. package/src/domains/release-tool.ts +2 -2
  216. package/src/execution-loop/gates.ts +11 -2
  217. package/src/flow-loop/split.ts +8 -4
  218. package/src/git-fs/worktree.ts +23 -3
  219. package/src/harness/harness-cli-command.ts +10 -2
  220. package/src/harness/harness-queue.ts +388 -33
  221. package/src/harness/helper-scan.ts +10 -3
  222. package/src/hq/seats.ts +369 -0
  223. package/src/index.ts +133 -15
  224. package/src/intake-plane/adopt-gate.ts +49 -0
  225. package/src/intake-plane/check.ts +3 -0
  226. package/src/intake-plane/collect-github.ts +57 -25
  227. package/src/intake-plane/discord-seat-work.ts +8 -2
  228. package/src/intake-plane/requirement-funnel.ts +316 -0
  229. package/src/knowledge/know-find.ts +10 -1
  230. package/src/loops/budget.ts +36 -0
  231. package/src/loops/orchestrator/finish-rate.ts +73 -1
  232. package/src/loops/orchestrator/grid-place.ts +11 -0
  233. package/src/loops/orchestrator/tick.ts +295 -20
  234. package/src/maturity/feature-maturity.ts +3 -2
  235. package/src/nexus/api/decisions-route.ts +11 -1
  236. package/src/nexus/api/loop-edges.ts +69 -13
  237. package/src/nexus/api/ops-seats-sources.ts +2 -2
  238. package/src/nexus/api/workflow-templates.ts +3 -0
  239. package/src/oauth/codex.ts +38 -0
  240. package/src/onboarding/first-chat-gate.ts +29 -0
  241. package/src/release-loop/checklist.ts +104 -6
  242. package/src/release-loop/feature-store.ts +10 -9
  243. package/src/release-loop/landing-freeze.ts +6 -4
  244. package/src/release-loop/placement.ts +13 -3
  245. package/src/release-loop/rubric.ts +80 -0
  246. package/src/seat-dispatch/seat-ask.ts +79 -16
  247. package/src/seat-loop/seat-loop.ts +12 -6
  248. package/src/self-dev/ask-launch-flow.ts +24 -3
  249. package/src/self-dev/ask-path-candidates.ts +68 -0
  250. package/src/self-dev/dev-pipeline.ts +4 -0
  251. package/src/self-dev/draft-triage-rules.ts +1 -1
  252. package/src/self-dev/orchestrate-cli.ts +22 -0
  253. package/src/self-dev/round-pr-supersede.ts +244 -0
  254. package/src/self-dev/run-supervisor.ts +14 -0
  255. package/src/self-implement/follow-up-goals.ts +2 -2
  256. package/src/self-implement/gate-remote.ts +427 -0
  257. package/src/self-implement/goal-author.ts +8 -3
  258. package/src/self-implement/l8-merge-queue.ts +29 -5
  259. package/src/self-implement/orchestrator.ts +59 -12
  260. package/src/self-implement/prior-draft-findings.ts +4 -3
  261. package/src/skills/skill-feedback.ts +76 -0
  262. package/src/task-agent/actions.ts +39 -6
  263. package/src/task-agent/judge.ts +31 -5
  264. package/src/task-agent/mission.ts +423 -0
  265. package/src/task-agent/shadow.ts +90 -0
  266. package/src/task-agent/task-hand.ts +235 -0
  267. package/src/task-orchestrator/surfaces/pod-failure-reason.ts +10 -0
  268. package/src/task-orchestrator/surfaces/self-implement-pod.ts +86 -18
  269. package/src/telegram-loops-command.ts +1 -1
  270. package/src/telegram.ts +5 -20
  271. package/src/user-config.ts +63 -3
  272. package/src/version/packed-revision.json +1 -1
  273. package/apps/pwa/out/_next/static/chunks/2699-63b73fa4d3bcf6ff.js +0 -1
  274. package/apps/pwa/out/_next/static/chunks/4554-3565eea1f1495ba7.js +0 -1
  275. package/apps/pwa/out/_next/static/chunks/4614-21403aac730d4586.js +0 -1
  276. package/apps/pwa/out/_next/static/chunks/5135-44008fe8b1f2aa41.js +0 -1
  277. package/apps/pwa/out/_next/static/chunks/5233-bf28bd6bd7a32099.js +0 -1
  278. package/apps/pwa/out/_next/static/chunks/5977-c952712c87a1f757.js +0 -1
  279. package/apps/pwa/out/_next/static/chunks/6877-fe94e76a5841ee9b.js +0 -1
  280. package/apps/pwa/out/_next/static/chunks/7395-164e238f471b5d7b.js +0 -1
  281. package/apps/pwa/out/_next/static/chunks/7787-02a1c0b04e25e811.js +0 -1
  282. package/apps/pwa/out/_next/static/chunks/8401-4ce1c42c8d89903f.js +0 -1
  283. package/apps/pwa/out/_next/static/chunks/8949-ecb4128a0858b911.js +0 -1
  284. package/apps/pwa/out/_next/static/chunks/899-f41be85e8a680651.js +0 -1
  285. package/apps/pwa/out/_next/static/chunks/app/botlab/page-6b8444914da74ca6.js +0 -1
  286. package/apps/pwa/out/_next/static/chunks/app/ceo/page-7110f732f45bf944.js +0 -1
  287. package/apps/pwa/out/_next/static/chunks/app/chat/page-9a41d65355fc25b9.js +0 -1
  288. package/apps/pwa/out/_next/static/chunks/app/control/page-41afec0b255ed2bb.js +0 -1
  289. package/apps/pwa/out/_next/static/chunks/app/inside/page-ab93b240f687d69a.js +0 -1
  290. package/apps/pwa/out/_next/static/chunks/app/layout-de43a186e5ba5683.js +0 -1
  291. package/apps/pwa/out/_next/static/chunks/app/live/page-8b08cafad27759c9.js +0 -1
  292. package/apps/pwa/out/_next/static/chunks/app/observatory/page-1ed1324b1dc36754.js +0 -1
  293. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-166745e51ac0c7b0.js +0 -1
  294. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-a04260b8ccc968d4.js +0 -1
  295. package/apps/pwa/out/_next/static/chunks/app/page-1f50e3d4e13435f4.js +0 -1
  296. package/apps/pwa/out/_next/static/chunks/app/sessions/page-a9117d2cd3093d36.js +0 -1
  297. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-5a64cb5208c6a8fd.js +0 -1
  298. package/apps/pwa/out/_next/static/chunks/app/settings/page-1407ec83ea64c1f9.js +0 -1
  299. package/apps/pwa/out/_next/static/chunks/app/setup/page-6e878ebbe1737d00.js +0 -1
  300. package/apps/pwa/out/_next/static/chunks/app/showroom/page-81a4e7f8dbfe74cb.js +0 -1
  301. package/apps/pwa/out/_next/static/chunks/app/term/page-3ab59d19913b200e.js +0 -1
  302. package/apps/pwa/out/_next/static/chunks/app/today/page-bf85b2f5bf077925.js +0 -1
  303. package/apps/pwa/out/_next/static/chunks/app/vault/page-ab32a8f522a60371.js +0 -1
  304. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-fc2df261f8a2c7df.js +0 -1
  305. package/apps/pwa/out/_next/static/chunks/app/workflows/page-09461366af82ef13.js +0 -1
  306. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-ea90a8ac73ce2749.js +0 -1
  307. package/apps/pwa/out/_next/static/css/001d5fbaee388a25.css +0 -1
  308. package/apps/pwa/out/_next/static/yRA5ur6DcPl0YHSRheH2O/_buildManifest.js +0 -1
  309. /package/apps/pwa/out/_next/static/{yRA5ur6DcPl0YHSRheH2O → P6YTVgzuZz5oTZhY4Din_}/_ssgManifest.js +0 -0
@@ -1,5 +1,7 @@
1
- import { existsSync, readdirSync, statSync } from 'node:fs';
1
+ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
+ import { effectiveInstanceRoot } from '../../instance/resolve.js';
4
+ import { listSeatRequests, type SeatRequestRow } from '../../seat-dispatch/seat-request-ledger.js';
3
5
  import { listCoordEvents } from '../../context-bus/coord-events.js';
4
6
  import { CardStore, cardIndexPath, type TaskCard } from '../../task-cards/card-store.js';
5
7
  import { loadRunLedger, resolveFederatedRunLedgerDirectories, type RunLedgerEntry } from '../../self-implement/run-ledger.js';
@@ -21,7 +23,9 @@ export interface LoopEdgesDeps {
21
23
  cardRoot?: string;
22
24
  ledgerDir?: string;
23
25
  listCards?: () => TaskCard[];
24
- listRunStarts?: (since: string) => Array<{ runId: string; at: string; seat: string }>;
26
+ listRunStarts?: (since: string) => Array<{ runId: string; at: string; seat: string; mergedAt?: string }>;
27
+ listSeatRequests?: () => SeatRequestRow[];
28
+ listOrchestratorLaunches?: () => Array<{ key: string; runId: string }>;
25
29
  listLoopOwners?: () => Array<{ id: string; owner?: string | null }>;
26
30
  seatIds?: () => string[];
27
31
  }
@@ -47,7 +51,9 @@ function inWindow(at: string, since: number, now: number): boolean {
47
51
  return Number.isFinite(ms) && ms >= since && ms <= now;
48
52
  }
49
53
 
50
- function cardEdges(card: TaskCard, since: number, now: number, seats: ReadonlySet<string>): LoopEdge[] {
54
+ type PlacedCell = { cardId: string; cellId: string; owner: string };
55
+
56
+ function cardEdges(card: TaskCard, since: number, now: number, seats: ReadonlySet<string>, placedCells: PlacedCell[]): LoopEdge[] {
51
57
  if (!card.goalId.startsWith('wish:') || !ID.test(card.id)) return [];
52
58
  const node = `card:${card.id}`;
53
59
  const ref = card.id;
@@ -75,6 +81,7 @@ function cardEdges(card: TaskCard, since: number, now: number, seats: ReadonlySe
75
81
  if (!owner || !placement || placement.id !== cellId || !ID.test(cellId)
76
82
  || typeof placement.version !== 'string' || !/^\d+\.\d+\.\d+$/.test(placement.version)) continue;
77
83
  placedOwners.add(owner);
84
+ placedCells.push({ cardId: card.id, cellId, owner });
78
85
  if (inWindow(section.createdAt, since, now)) {
79
86
  const cellNode = `loop:${cellId}`;
80
87
  edges.push({ at: section.createdAt, kind: 'card', from: node, to: cellNode, ref });
@@ -93,9 +100,9 @@ function cardEdges(card: TaskCard, since: number, now: number, seats: ReadonlySe
93
100
  return edges;
94
101
  }
95
102
 
96
- function runStarts(since: string, dir: string, owners: ReadonlyMap<string, string>, seats: ReadonlySet<string>): Array<{ runId: string; at: string; seat: string }> {
103
+ function runStarts(since: string, dir: string, owners: ReadonlyMap<string, string>, seats: ReadonlySet<string>): Array<{ runId: string; at: string; seat: string; mergedAt?: string }> {
97
104
  if (!existsSync(dir)) return [];
98
- const result: Array<{ runId: string; at: string; seat: string }> = [];
105
+ const result: Array<{ runId: string; at: string; seat: string; mergedAt?: string }> = [];
99
106
  let names: string[];
100
107
  try { names = readdirSync(dir); } catch { return []; }
101
108
  for (const name of names) {
@@ -110,14 +117,42 @@ function runStarts(since: string, dir: string, owners: ReadonlyMap<string, strin
110
117
  const feature = typeof start?.data.feature === 'string' ? start.data.feature : '';
111
118
  const launchSeat = start?.data.launchSeat ?? start?.data.seat ?? owners.get(feature)
112
119
  ?? start?.data.originAgent ?? start?.data.controller;
113
- if (start?.timestamp && start.timestamp >= since && seat(launchSeat, seats)) {
114
- result.push({ runId, at: start.timestamp, seat: launchSeat });
120
+ if (start?.timestamp && seat(launchSeat, seats)) {
121
+ const merged = entries?.find(entry => entry.event === 'merged' && entry.data.merged === true && typeof entry.timestamp === 'string');
122
+ if (start.timestamp >= since || (merged?.timestamp && merged.timestamp >= since)) {
123
+ result.push({ runId, at: start.timestamp, seat: launchSeat, ...(merged?.timestamp ? { mergedAt: merged.timestamp } : {}) });
124
+ }
115
125
  }
116
126
  }
117
127
  return result;
118
128
  }
119
129
 
120
- /** GET only; project an allowlist of fields from the three ledgers. Never serialize ledger records. */
130
+ /** Project only the key and runId of orchestrator launches, never their card snapshots or prose. */
131
+ function orchestratorLaunches(root: string): Array<{ key: string; runId: string }> {
132
+ const dir = join(root, 'loop', 'orchestrator');
133
+ if (!existsSync(dir)) return [];
134
+ const launches: Array<{ key: string; runId: string }> = [];
135
+ let names: string[];
136
+ try { names = readdirSync(dir); } catch { return []; }
137
+ for (const name of names) {
138
+ if (!/^[a-zA-Z0-9._-]+\.json$/.test(name)) continue;
139
+ const path = join(dir, name);
140
+ try {
141
+ if (!statSync(path).isFile() || statSync(path).size > 5 * 1024 * 1024) continue;
142
+ const state = parse(readFileSync(path, 'utf8'));
143
+ if (state?.mode !== 'live' || !Array.isArray(state.launched)) continue;
144
+ for (const item of state.launched) {
145
+ const launch = object(item);
146
+ if (typeof launch?.key === 'string' && typeof launch.runId === 'string') {
147
+ launches.push({ key: launch.key, runId: launch.runId });
148
+ }
149
+ }
150
+ } catch { /* An unreadable tick cannot establish a launch. */ }
151
+ }
152
+ return launches;
153
+ }
154
+
155
+ /** GET only; project an allowlist of fields from the ledgers. Never serialize ledger records. */
121
156
  export function handleLoopEdgesGet(req: Request, deps: LoopEdgesDeps = {}): Response {
122
157
  const params = new URL(req.url).searchParams;
123
158
  const rawSince = params.get('since');
@@ -143,13 +178,14 @@ export function handleLoopEdgesGet(req: Request, deps: LoopEdgesDeps = {}): Resp
143
178
  if (seat(recipient, seats)) edges.push({ at: event.at, kind, from: event.refs.seat, to: recipient, ref });
144
179
  }
145
180
  }
181
+ const placedCells: PlacedCell[] = [];
146
182
  if (deps.listCards) {
147
- for (const card of deps.listCards()) edges.push(...cardEdges(card, since, now, seats));
183
+ for (const card of deps.listCards()) edges.push(...cardEdges(card, since, now, seats, placedCells));
148
184
  } else {
149
185
  const root = deps.cardRoot;
150
186
  if (existsSync(cardIndexPath(root))) {
151
187
  const store = new CardStore(root, true);
152
- try { for (const card of store.listCards()) edges.push(...cardEdges(card, since, now, seats)); }
188
+ try { for (const card of store.listCards()) edges.push(...cardEdges(card, since, now, seats, placedCells)); }
153
189
  finally { store.close(); }
154
190
  }
155
191
  }
@@ -163,9 +199,29 @@ export function handleLoopEdgesGet(req: Request, deps: LoopEdgesDeps = {}): Resp
163
199
  return (deps.ledgerDir ? [deps.ledgerDir] : resolveFederatedRunLedgerDirectories({ includeTest: false }))
164
200
  .flatMap(dir => runStarts(from, dir, owners, seats));
165
201
  });
166
- for (const run of readStarts(sinceIso)) {
167
- if (seat(run.seat, seats) && ID.test(run.runId) && inWindow(run.at, since, now)) {
168
- edges.push({ at: run.at, kind: 'run', from: run.seat, to: `loop:${run.runId}`, ref: run.runId });
202
+ const runs = readStarts(sinceIso).filter(run => seat(run.seat, seats) && ID.test(run.runId)
203
+ && (inWindow(run.at, since, now) || (run.mergedAt !== undefined && inWindow(run.mergedAt, since, now))));
204
+ for (const run of runs) {
205
+ if (inWindow(run.at, since, now)) edges.push({ at: run.at, kind: 'run', from: run.seat, to: `loop:${run.runId}`, ref: run.runId });
206
+ }
207
+ if (placedCells.length && runs.length) {
208
+ const root = deps.cardRoot ?? effectiveInstanceRoot();
209
+ let requests: SeatRequestRow[] = [];
210
+ let launches: Array<{ key: string; runId: string }> = [];
211
+ try { requests = (deps.listSeatRequests ?? (() => listSeatRequests(root)))(); } catch { /* Missing request evidence is not a match. */ }
212
+ try { launches = (deps.listOrchestratorLaunches ?? (() => orchestratorLaunches(root)))(); } catch { /* Missing launch evidence is not a match. */ }
213
+ const confirmed = new Set(launches.filter(item => ID.test(item.runId) && runs.some(run => run.runId === item.runId))
214
+ .map(item => JSON.stringify([item.key, item.runId])));
215
+ for (const cell of placedCells) {
216
+ const key = `orch:${cell.cardId}:${cell.cellId}`;
217
+ if (!requests.some(row => row.key === key && row.cell === cell.cellId && row.source === 'orchestrator' && row.seat === cell.owner)) continue;
218
+ for (const run of runs) {
219
+ if (run.seat !== cell.owner || !confirmed.has(JSON.stringify([key, run.runId]))) continue;
220
+ if (inWindow(run.at, since, now)) edges.push({ at: run.at, kind: 'card', from: cell.owner, to: `loop:${run.runId}`, ref: cell.cardId });
221
+ if (run.mergedAt && inWindow(run.mergedAt, since, now)) {
222
+ edges.push({ at: run.mergedAt, kind: 'card', from: `loop:${run.runId}`, to: `landed:${cell.cellId}`, ref: cell.cardId });
223
+ }
224
+ }
169
225
  }
170
226
  }
171
227
  edges.sort((a, b) => b.at.localeCompare(a.at) || a.kind.localeCompare(b.kind) || a.from.localeCompare(b.from) || a.to.localeCompare(b.to));
@@ -2,7 +2,7 @@
2
2
  // removed — same rule as decision replies), the release checklist, and the decision ledger. Each failure becomes null upstream.
3
3
  import { DecisionLedger } from '../../decisions/decision-ledger.js';
4
4
  import { listCoordEvents, type CoordEvent } from '../../context-bus/coord-events.js';
5
- import { devVersion, listChecklist, type ChecklistItem } from '../../release-loop/checklist.js';
5
+ import { checklistDevVersion, listChecklist, type ChecklistItem } from '../../release-loop/checklist.js';
6
6
  import { getUserConfig } from '../../user-config.js';
7
7
  import { SEATS, kstDayRange, type ChannelComment, type ChannelSource, type MergedPr, type SeatsSources } from './ops-seats.js';
8
8
 
@@ -82,7 +82,7 @@ export function liveSeatsSources(deps: { listEvents?: typeof listCoordEvents; gi
82
82
  return lines?.map((line) => JSON.parse(line) as MergedPr) ?? null;
83
83
  },
84
84
  checklist() {
85
- const dev = devVersion();
85
+ const dev = checklistDevVersion();
86
86
  const next = listChecklist(nextPatch(dev)).items;
87
87
  const current = listChecklist(dev).items;
88
88
  const all: ChecklistItem[] = [...current, ...next];
@@ -4,6 +4,7 @@
4
4
 
5
5
  import { readdirSync, readFileSync } from 'fs';
6
6
  import { join } from 'path';
7
+ import { debug } from '../../debug/log.js';
7
8
  import { jsonResponse } from './http-server.js';
8
9
  import { checkAuth, type MetaApiOpts } from './meta-api.js';
9
10
 
@@ -47,6 +48,8 @@ export function listTemplateCatalog(dir = findTemplateDir()): WorkflowTemplateEn
47
48
  try {
48
49
  files = readdirSync(dir).filter((f) => f.endsWith('.yaml'));
49
50
  } catch {
51
+ // An installed package without this folder used to answer «No templates available» silently (10-06 · files list).
52
+ debug.log('nexus.workflow-templates', 'template-dir-missing', { dir });
50
53
  return [];
51
54
  }
52
55
  const out: WorkflowTemplateEntry[] = [];
@@ -554,6 +554,44 @@ export function codexAccessHoursLeft(accessToken: unknown, now: number = Date.no
554
554
  return typeof exp === 'number' ? Math.round(((exp * 1000 - now) / 3_600_000) * 10) / 10 : null;
555
555
  }
556
556
 
557
+ /** PREREFRESH-LOCK (10-06 · post-review of #24449) — the per-account host refresh lock path.
558
+ * 🩸 Two Pod launches on one host picked the same expiring account at once: both refreshed, the server
559
+ * rotated the refresh token under the second, its refresh failed and that run dropped the account.
560
+ * The lock file sits NEXT TO the account's credential state (`<codexHome>/.elanous-prerefresh.lock`) —
561
+ * ⛔ never inside a token file, and it carries only the holder's pid/nonce. null = no named home to lock. */
562
+ export function codexAccountRefreshLockPath(name: string, storePath: string = authStorePath()): string | null {
563
+ if (name === DEFAULT_CODEX_ACCOUNT || !isValidAccountName(name)) return null;
564
+ const codexHome = loadTokens(codexStoreKey(name), storePath)?.codexHome;
565
+ return codexHome ? join(codexHome, '.elanous-prerefresh.lock') : null;
566
+ }
567
+
568
+ /** A refresh (network + rotate + atomic write + import) can take tens of seconds — wait up to ~90 s,
569
+ * and treat a lock as stale only after 2 min (the async holder heartbeats its mtime meanwhile). */
570
+ export const CODEX_REFRESH_LOCK_OPTS = { staleMs: 120_000, retryBusyMs: 100, maxTries: 900 } as const;
571
+
572
+ /** The refresh lock could not be acquired (timeout · ENOENT · EACCES …) — `fn` never ran, so nothing was refreshed. */
573
+ export class CodexRefreshLockUnavailableError extends Error {
574
+ constructor(readonly reason: 'lock-timeout' | 'lock-error', cause: unknown) {
575
+ super(`codex refresh lock unavailable (${reason}): ${cause instanceof Error ? cause.message : String(cause)}`);
576
+ this.name = 'CodexRefreshLockUnavailableError';
577
+ }
578
+ }
579
+
580
+ /** Runs `fn` while holding the account's refresh lock (cross-process · same-process). null path = no lock.
581
+ * Any failure to acquire the lock throws `CodexRefreshLockUnavailableError` (fn not run); fn's own errors pass through. */
582
+ export async function withCodexAccountRefreshLock<T>(lockPath: string | null, fn: () => Promise<T>, opts: { staleMs?: number; retryBusyMs?: number; maxTries?: number } = CODEX_REFRESH_LOCK_OPTS): Promise<T> {
583
+ if (!lockPath) return fn();
584
+ const { acquireLockAsync } = await import('../storage/file-lock.js');
585
+ let handle: Awaited<ReturnType<typeof acquireLockAsync>>;
586
+ try { handle = await acquireLockAsync(lockPath, opts); }
587
+ catch (error) {
588
+ const timedOut = error instanceof Error && error.message.startsWith('acquireLockAsync: timed out');
589
+ throw new CodexRefreshLockUnavailableError(timedOut ? 'lock-timeout' : 'lock-error', error);
590
+ }
591
+ try { return await fn(); }
592
+ finally { handle.release(); }
593
+ }
594
+
557
595
  export async function refreshCodexAccountHome(
558
596
  name: string,
559
597
  opts: {
@@ -0,0 +1,29 @@
1
+ import { debug } from '../debug/log.js';
2
+ import { needsFirstRun, needsOnboarding } from '../onboarding.js';
3
+ import type { UserConfig } from '../user-config.js';
4
+ import type { FirstRunDeps, runFirstRun } from './first-run.js';
5
+ import { UNATTENDED_SETUP_COMMAND } from './entry-hints.js';
6
+
7
+ export interface FirstChatGateDeps {
8
+ isTTY: boolean;
9
+ runFirstRun: (deps: FirstRunDeps) => ReturnType<typeof runFirstRun>;
10
+ runInteractive: () => Promise<boolean>;
11
+ print: (line: string) => void;
12
+ }
13
+
14
+ export async function firstChatGate(cfg: UserConfig, deps: FirstChatGateDeps): Promise<'continue' | 'stop'> {
15
+ const decide = (decision: 'continue' | 'stop', outcome: string): 'continue' | 'stop' => {
16
+ debug.log('onboarding.first-chat', decision, { isTTY: deps.isTTY, outcome });
17
+ return decision;
18
+ };
19
+ if (!needsOnboarding(cfg)) return decide('continue', 'completed');
20
+ if (deps.isTTY) {
21
+ return decide(await deps.runInteractive() ? 'continue' : 'stop', 'interactive');
22
+ }
23
+ if (!needsFirstRun(cfg)) return decide('continue', 'already-ready');
24
+ const result = await deps.runFirstRun({ config: cfg, isTTY: false, print: () => {} });
25
+ if (result.outcome === 'ready') return decide('continue', result.outcome);
26
+ deps.print(`다음: elanous setup llm (또는 ${UNATTENDED_SETUP_COMMAND})`);
27
+ process.exitCode = 2;
28
+ return decide('stop', result.outcome);
29
+ }
@@ -1,5 +1,6 @@
1
- import { readFileSync } from 'node:fs';
2
- import { join } from 'node:path';
1
+ import { spawnSync } from 'node:child_process';
2
+ import { readFileSync, statSync } from 'node:fs';
3
+ import { isAbsolute, join } from 'node:path';
3
4
  import * as store from './feature-store.js';
4
5
  import { debug } from '../debug/log.js';
5
6
  import { CliUserError } from '../cli/cli-user-error.js';
@@ -20,6 +21,8 @@ export interface ChecklistItem {
20
21
  ceoDate?: string;
21
22
  /** 다른 칸의 처리량을 올리는 칸(자율성·효율성·동시성). 없으면 가속 등급이 아니다. */
22
23
  accelerator?: true;
24
+ /** 이 칸이 근거로 삼는 문서(저장소 상대 경로, 선택적으로 `#절`). 없으면 칸에 실리지 않는다. */
25
+ refs?: string[];
23
26
  evidence?: string;
24
27
  disposition?: ChecklistDisposition;
25
28
  kind?: ChecklistKind;
@@ -50,6 +53,12 @@ export function devVersion(): string {
50
53
  return (JSON.parse(readFileSync(join(import.meta.dir, '..', '..', 'package.json'), 'utf8')) as { version: string }).version;
51
54
  }
52
55
 
56
+ /** The checklist version of the development tree: package.json says `0.2.18-dev.0` once a release branch is cut,
57
+ * but the checklist only knows `0.2.18` (10-07: every server caller that passed devVersion() raw got null/throw). */
58
+ export function checklistDevVersion(version = devVersion()): string {
59
+ return version.replace(/-dev\.\d+$/, '');
60
+ }
61
+
53
62
  export function parseOwner(value: string): { seat: string; sub?: string } {
54
63
  const match = /^([A-Z]{2,8})(?:\/([a-z][a-z0-9-]{0,31}))?$/.exec(value);
55
64
  if (!match || match[0] !== value) throw new CliUserError(`잘못된 담당: ${value}`, 'TC 또는 TC/rel');
@@ -100,12 +109,82 @@ function change(data: Checklist, id: string, field: string, from: unknown, to: u
100
109
  debug.log('release-loop.checklist', 'change', { version: data.version, id, field, from: from ?? null, to: to ?? null, by });
101
110
  }
102
111
 
112
+ const REPO_ROOT = join(import.meta.dir, '..', '..');
113
+
114
+ /** `--ref` 문서를 찾는 뿌리들 — 이 코드의 트리 ⊕ 지금 작업 디렉토리의 git 뿌리.
115
+ * ⭐ 설치본(`~/.local/share/elanous/current`)에는 `docs/` 가 없다 — 저장소 안에서 부른 설치본도 저장소 문서를 인용할 수 있게. */
116
+ export function refRoots(cwd = process.cwd()): string[] {
117
+ const roots = [REPO_ROOT];
118
+ try {
119
+ const top = spawnSync('git', ['rev-parse', '--show-toplevel'], { cwd, encoding: 'utf8', timeout: 5_000 });
120
+ const path = top.status === 0 ? top.stdout.trim() : '';
121
+ if (path && !roots.includes(path)) roots.push(path);
122
+ } catch { /* git 없음 — 코드 트리만 */ }
123
+ return roots;
124
+ }
125
+
126
+ /** `--ref` 값 검사: 저장소 상대 경로(⊕ `#절`)이고 그 파일이 뿌리들 중 하나에 있어야 한다. 중복은 순서를 지키며 지운다. */
127
+ export function normalizeRefs(refs: readonly string[], root: string | readonly string[] = refRoots()): string[] {
128
+ const roots = typeof root === 'string' ? [root] : root;
129
+ const out: string[] = [];
130
+ for (const raw of refs) {
131
+ const ref = raw.trim();
132
+ const path = ref.split('#')[0] ?? '';
133
+ if (!path || isAbsolute(path) || path.split(/[\\/]/).some((part) => part === '..')) throw new CliUserError(`잘못된 문서 참조: ${raw}`, '저장소 상대 경로[#절] — 예: docs/manual/MANUAL-x.md#§1');
134
+ let isFile = false;
135
+ for (const base of roots) {
136
+ try { if (statSync(join(base, path)).isFile()) { isFile = true; break; } } catch { /* 없음 */ }
137
+ }
138
+ if (!isFile) throw new CliUserError(`없는 문서: ${path}`, '저장소 상대 «파일» 경로로 준다(폴더는 안 된다)');
139
+ if (!out.includes(ref)) out.push(ref);
140
+ }
141
+ return out;
142
+ }
143
+
144
+ /** 한 문서를 인용하는 칸 한 줄 — `release checklist refs <doc>` 의 «구현 현황» 행. */
145
+ export interface DocRefRow { version: string; id: string; title: string; status: ChecklistStatus; owner: string | null; section: string | null }
146
+
147
+ function splitRef(ref: string): { path: string; section: string | null } {
148
+ const at = ref.indexOf('#');
149
+ const path = (at < 0 ? ref : ref.slice(0, at)).trim().replace(/^\.\//, '');
150
+ return { path, section: at < 0 ? null : ref.slice(at + 1) };
151
+ }
152
+
153
+ /** 읽기 전용: 칸의 refs 중 경로(`#` 앞)가 doc 의 경로와 같은 것을 행으로 낸다. doc 에 `#절`이 붙으면 그 절과 정확히 같은 ref 만. */
154
+ export function cellsReferencingDoc(doc: string, checklists: readonly Checklist[]): DocRefRow[] {
155
+ const want = splitRef(doc.trim());
156
+ const rows: DocRefRow[] = [];
157
+ for (const data of checklists) {
158
+ for (const item of data.items) {
159
+ // 칸마다 한 행 — 같은 문서의 절을 여럿 인용해도 절을 모아 한 줄로(절 없는 인용이 섞이면 문서 전체로 본다).
160
+ const sections: string[] = [];
161
+ let whole = false;
162
+ for (const ref of item.refs ?? []) {
163
+ const got = splitRef(ref);
164
+ if (got.path !== want.path) continue;
165
+ if (want.section !== null && got.section !== want.section) continue;
166
+ if (got.section === null) whole = true;
167
+ else if (!sections.includes(got.section)) sections.push(got.section);
168
+ }
169
+ if (!whole && sections.length === 0) continue;
170
+ rows.push({ version: data.version, id: item.id, title: item.title, status: item.status, owner: item.owner ?? null, section: whole ? null : sections.join(' · ') });
171
+ }
172
+ }
173
+ return rows;
174
+ }
175
+
176
+ /** `| 판 | 칸 | 상태 | 절 | 제목 |` 마크다운 표 — 행이 없으면 머리줄 둘만. */
177
+ export function renderRefsStatus(rows: readonly DocRefRow[]): string {
178
+ const cell = (value: string) => value.replace(/\|/g, '\\|').replace(/\r?\n/g, ' ');
179
+ return ['| 판 | 칸 | 상태 | 절 | 제목 |', '| --- | --- | --- | --- | --- |', ...rows.map((row) => `| ${cell(row.version)} | ${cell(row.id)} | ${row.status} | ${cell(row.section ?? '-')} | ${cell(row.title)} |`)].join('\n');
180
+ }
181
+
103
182
  export function validateCeoLoad(input: { ceoMinutes?: number; ceoDate?: string }): void {
104
183
  if (input.ceoMinutes !== undefined && (!Number.isSafeInteger(input.ceoMinutes) || input.ceoMinutes < 0)) throw new CliUserError('대표 손 분량은 0 이상의 정수 분이어야 한다');
105
184
  if (input.ceoDate !== undefined && (!/^\d{4}-\d{2}-\d{2}$/.test(input.ceoDate) || !Number.isFinite(Date.parse(`${input.ceoDate}T00:00:00Z`)) || new Date(`${input.ceoDate}T00:00:00Z`).toISOString().slice(0, 10) !== input.ceoDate)) throw new CliUserError('대표 손 날짜는 YYYY-MM-DD 이어야 한다');
106
185
  }
107
186
 
108
- export function addItem(v: string, input: { id: string; title: string; owner?: string; kind?: ChecklistKind; priority?: ChecklistItem['priority']; predecessors?: string[]; deadlineVersion?: string; ceoMinutes?: number; ceoDate?: string; accelerator?: boolean }, options: { allowDuplicateId?: boolean } = {}): Checklist {
187
+ export function addItem(v: string, input: { id: string; title: string; refs?: string[]; owner?: string; kind?: ChecklistKind; priority?: ChecklistItem['priority']; predecessors?: string[]; deadlineVersion?: string; ceoMinutes?: number; ceoDate?: string; accelerator?: boolean }, options: { allowDuplicateId?: boolean } = {}): Checklist {
109
188
  if (!input.id.trim()) throw new CliUserError('칸 id 가 비었다');
110
189
  if (!input.title.trim()) throw new CliUserError('칸 제목이 비었다');
111
190
  if (input.kind !== undefined && input.kind !== 'screen') throw new CliUserError(`잘못된 종류: ${input.kind}`, 'screen');
@@ -115,6 +194,7 @@ export function addItem(v: string, input: { id: string; title: string; owner?: s
115
194
  if (input.deadlineVersion !== undefined) store.validateVersion(input.deadlineVersion);
116
195
  if (input.accelerator !== undefined && input.accelerator !== true) throw new CliUserError('가속 등급은 true 이거나 생략한다');
117
196
  validateCeoLoad(input);
197
+ const refs = input.refs !== undefined && input.refs.length ? normalizeRefs(input.refs) : undefined;
118
198
  return mutate(v, (data, otherItems) => {
119
199
  if (data.items.some((item) => item.id === input.id)) throw new CliUserError(`이미 있는 칸: ${input.id}`, 'set <id> 로 고친다');
120
200
  const collisions = otherItems(input.id);
@@ -125,14 +205,14 @@ export function addItem(v: string, input: { id: string; title: string; owner?: s
125
205
  }
126
206
  const by = process.env.ELANOUS_TRACK || 'cli';
127
207
  const at = new Date().toISOString();
128
- const item: ChecklistItem = { id: input.id, title: input.title, status: 'yellow', ...(input.owner !== undefined ? { owner: input.owner } : {}), ...(input.kind !== undefined ? { kind: input.kind } : {}), ...(input.priority !== undefined ? { priority: input.priority } : {}), ...(input.predecessors !== undefined ? { predecessors: input.predecessors } : {}), ...(input.deadlineVersion !== undefined ? { deadlineVersion: input.deadlineVersion } : {}), ...(input.ceoMinutes !== undefined ? { ceoMinutes: input.ceoMinutes } : {}), ...(input.ceoDate !== undefined ? { ceoDate: input.ceoDate } : {}), ...(input.accelerator === true ? { accelerator: true as const } : {}), updatedAt: at, updatedBy: by };
208
+ const item: ChecklistItem = { id: input.id, title: input.title, status: 'yellow', ...(input.owner !== undefined ? { owner: input.owner } : {}), ...(input.kind !== undefined ? { kind: input.kind } : {}), ...(input.priority !== undefined ? { priority: input.priority } : {}), ...(input.predecessors !== undefined ? { predecessors: input.predecessors } : {}), ...(input.deadlineVersion !== undefined ? { deadlineVersion: input.deadlineVersion } : {}), ...(input.ceoMinutes !== undefined ? { ceoMinutes: input.ceoMinutes } : {}), ...(input.ceoDate !== undefined ? { ceoDate: input.ceoDate } : {}), ...(input.accelerator === true ? { accelerator: true as const } : {}), ...(refs ? { refs } : {}), updatedAt: at, updatedBy: by };
129
209
  data.items.push(item);
130
210
  change(data, item.id, 'add', null, item, by, at);
131
211
  return true;
132
212
  });
133
213
  }
134
214
 
135
- export function setItem(v: string, id: string, patch: { status?: ChecklistStatus; evidence?: string; owner?: string; disposition?: ChecklistDisposition; kind?: ChecklistKind; priority?: ChecklistItem['priority']; predecessors?: string[]; deadlineVersion?: string; ceoMinutes?: number; ceoDate?: string; accelerator?: boolean | null }, by: string): Checklist {
215
+ export function setItem(v: string, id: string, patch: { status?: ChecklistStatus; evidence?: string; refs?: string[]; owner?: string; disposition?: ChecklistDisposition; kind?: ChecklistKind; priority?: ChecklistItem['priority']; predecessors?: string[]; deadlineVersion?: string; ceoMinutes?: number; ceoDate?: string; accelerator?: boolean | null }, by: string): Checklist {
136
216
  return mutate(v, (data) => {
137
217
  const item = data.items.find((i) => i.id === id);
138
218
  if (!item) throw new CliUserError(`없는 칸: ${id}`, 'list 로 칸 목록을 본다');
@@ -144,13 +224,15 @@ export function setItem(v: string, id: string, patch: { status?: ChecklistStatus
144
224
  if (patch.deadlineVersion !== undefined) store.validateVersion(patch.deadlineVersion);
145
225
  if (patch.accelerator !== undefined && patch.accelerator !== true && patch.accelerator !== null) throw new CliUserError('가속 등급은 true(부여) 또는 null(해제)이다');
146
226
  validateCeoLoad(patch);
227
+ // --ref 는 «더하기»다 — 기존 참조 뒤에 붙이고 중복을 지운다.
228
+ if (patch.refs !== undefined) patch = { ...patch, refs: normalizeRefs([...(item.refs ?? []), ...patch.refs]) };
147
229
  if (patch.owner !== undefined) {
148
230
  parseOwner(patch.owner);
149
231
  if (item.owner && patch.owner !== item.owner) {
150
232
  throw new CliUserError(`지금 주인: ${item.owner} — --force 로만 바꾼다`, 'release checklist claim <id> --by <자리> --force');
151
233
  }
152
234
  }
153
- const fields = (['evidence', 'owner', 'status', 'disposition', 'kind', 'priority', 'predecessors', 'deadlineVersion', 'ceoMinutes', 'ceoDate', 'accelerator'] as const).filter((field) => patch[field] !== undefined && JSON.stringify(patch[field]) !== JSON.stringify(item[field] ?? null));
235
+ const fields = (['evidence', 'owner', 'status', 'disposition', 'kind', 'priority', 'predecessors', 'deadlineVersion', 'ceoMinutes', 'ceoDate', 'accelerator', 'refs'] as const).filter((field) => patch[field] !== undefined && JSON.stringify(patch[field]) !== JSON.stringify(item[field] ?? null));
154
236
  if (fields.length === 0) return false;
155
237
  const at = new Date().toISOString();
156
238
  for (const field of fields) {
@@ -269,6 +351,22 @@ export function checklistGate(v: string): ChecklistGate {
269
351
  return result;
270
352
  }
271
353
 
354
+ /** GATE-ENTRY-ALIGN: the cut-time judgement shared by `release run`'s entry check and the checklist-gate node —
355
+ * past the landing deadline a non-P0 yellow without a disposition is carried (moved), and a P0 yellow always blocks. */
356
+ export function cutChecklistGate(v: string, landBy: string | null | undefined, now: Date = new Date()): ChecklistGate & { autoMoved: string[] } {
357
+ const items = listChecklist(v).items;
358
+ const overdue = landBy !== null && landBy !== undefined && now.getTime() > Date.parse(landBy);
359
+ const autoMoved = overdue ? items.filter((item) => item.status === 'yellow' && item.priority !== 'P0' && item.disposition === undefined).map((item) => item.id) : [];
360
+ const p0 = items.filter((item) => item.status === 'yellow' && item.priority === 'P0').map((item) => item.id);
361
+ const gate = checklistGate(v);
362
+ gate.undecided = gate.undecided.filter((id) => !autoMoved.includes(id) && !p0.includes(id));
363
+ gate.moved = gate.moved.filter((id) => !p0.includes(id)).concat(autoMoved);
364
+ gate.knownIssues = gate.knownIssues.filter((item) => !p0.includes(item.id));
365
+ gate.blocked.push(...p0.filter((id) => !gate.blocked.includes(id)));
366
+ gate.ok = gate.red.length === 0 && gate.undecided.length === 0 && gate.blocked.length === 0;
367
+ return { ...gate, autoMoved };
368
+ }
369
+
272
370
  export function seedFromRoadmap(v: string, markdown: string): Checklist {
273
371
  return mutate(v, (data) => {
274
372
  let added = false;
@@ -33,7 +33,7 @@ const walWait = new Int32Array(new SharedArrayBuffer(4));
33
33
  const SCHEMA_COLUMNS: Record<string, readonly string[]> = {
34
34
  imported_versions: ['json_hash', 'imported_at'],
35
35
  events: ['reason'],
36
- assignments: ['priority', 'predecessors', 'deadline_version', 'ceo_minutes', 'ceo_date', 'accelerator'],
36
+ assignments: ['priority', 'predecessors', 'deadline_version', 'ceo_minutes', 'ceo_date', 'accelerator', 'refs'],
37
37
  release_schedules: ['freeze_from', 'freeze_until'],
38
38
  };
39
39
  function schemaCurrent(db: Database): boolean {
@@ -75,7 +75,7 @@ function open(root = releaseLedgerRoot()): Database {
75
75
  if (!(db.query('PRAGMA table_info(events)').all() as Array<{ name: string }>).some((column) => column.name === 'reason')) db.exec('ALTER TABLE events ADD COLUMN reason TEXT');
76
76
  db.exec('CREATE TABLE IF NOT EXISTS release_schedules (version TEXT PRIMARY KEY, cut_at TEXT NOT NULL, land_by TEXT, updated_at TEXT NOT NULL, updated_by TEXT NOT NULL)');
77
77
  const assignmentColumns = db.query('PRAGMA table_info(assignments)').all() as Array<{ name: string }>;
78
- for (const [name, sql] of [['priority', 'TEXT'], ['predecessors', 'TEXT'], ['deadline_version', 'TEXT'], ['ceo_minutes', 'INTEGER'], ['ceo_date', 'TEXT'], ['accelerator', 'INTEGER']] as const) {
78
+ for (const [name, sql] of [['priority', 'TEXT'], ['predecessors', 'TEXT'], ['deadline_version', 'TEXT'], ['ceo_minutes', 'INTEGER'], ['ceo_date', 'TEXT'], ['accelerator', 'INTEGER'], ['refs', 'TEXT']] as const) {
79
79
  if (!assignmentColumns.some((column) => column.name === name)) db.exec(`ALTER TABLE assignments ADD COLUMN ${name} ${sql}`);
80
80
  }
81
81
  const scheduleColumns = db.query('PRAGMA table_info(release_schedules)').all() as Array<{ name: string }>;
@@ -169,7 +169,7 @@ function evidenceRefs(db: Database, version: string, id: string): string[] {
169
169
  return (db.query('SELECT ref FROM evidence WHERE feature_id = ? AND version = ? ORDER BY rowid').all(id, version) as Array<{ ref: string }>).map((row) => row.ref);
170
170
  }
171
171
  function items(db: Database, version: string): ChecklistItem[] {
172
- const rows = db.query(`SELECT f.id, COALESCE(a.title_override, f.title) AS title, a.owner, a.kind, a.priority, a.predecessors, a.deadline_version, a.ceo_minutes, a.ceo_date, a.accelerator, a.status, a.disposition, a.evidence, a.updated_at, a.updated_by
172
+ const rows = db.query(`SELECT f.id, COALESCE(a.title_override, f.title) AS title, a.owner, a.kind, a.priority, a.predecessors, a.deadline_version, a.ceo_minutes, a.ceo_date, a.accelerator, a.refs, a.status, a.disposition, a.evidence, a.updated_at, a.updated_by
173
173
  FROM assignments a JOIN features f ON f.id = a.feature_id WHERE a.version = ? ORDER BY a.rowid`).all(version) as Array<Record<string, string | number | null>>;
174
174
  return rows.map((r) => {
175
175
  const evidence = [r.evidence, ...evidenceRefs(db, version, r.id as string)].filter((value) => value !== null).join('\n');
@@ -179,6 +179,7 @@ function items(db: Database, version: string): ChecklistItem[] {
179
179
  ...(r.deadline_version !== null ? { deadlineVersion: r.deadline_version as string } : {}),
180
180
  ...(r.ceo_minutes !== null ? { ceoMinutes: r.ceo_minutes as number } : {}), ...(r.ceo_date !== null ? { ceoDate: r.ceo_date as string } : {}),
181
181
  ...(r.accelerator === 1 ? { accelerator: true as const } : {}),
182
+ ...(r.refs !== null && r.refs !== undefined ? { refs: JSON.parse(r.refs as string) as string[] } : {}),
182
183
  ...(r.evidence !== null || evidence ? { evidence } : {}), ...(r.disposition !== null ? { disposition: r.disposition as ChecklistItem['disposition'] } : {}),
183
184
  updatedAt: r.updated_at as string, updatedBy: r.updated_by as string };
184
185
  });
@@ -192,9 +193,9 @@ function putItem(db: Database, version: string, item: ChecklistItem): void {
192
193
  if (!feature) db.query('INSERT INTO features (id, title, owner, kind, created_at) VALUES (?, ?, ?, ?, ?)').run(item.id, item.title, item.owner ?? null, item.kind ?? null, item.updatedAt);
193
194
  // features.owner/kind follow the latest assignment so details() and the checklist never disagree after a set.
194
195
  else if (item.owner !== undefined || item.kind !== undefined) db.query('UPDATE features SET owner = COALESCE(?, owner), kind = COALESCE(?, kind) WHERE id = ?').run(item.owner ?? null, item.kind ?? null, item.id);
195
- db.query(`INSERT INTO assignments (feature_id, version, status, disposition, evidence, title_override, owner, kind, priority, predecessors, deadline_version, ceo_minutes, ceo_date, accelerator, updated_at, updated_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
196
- ON CONFLICT(feature_id, version) DO UPDATE SET status=excluded.status, disposition=excluded.disposition, evidence=excluded.evidence, owner=excluded.owner, kind=excluded.kind, priority=excluded.priority, predecessors=excluded.predecessors, deadline_version=excluded.deadline_version, ceo_minutes=excluded.ceo_minutes, ceo_date=excluded.ceo_date, accelerator=excluded.accelerator, updated_at=excluded.updated_at, updated_by=excluded.updated_by`)
197
- .run(item.id, version, item.status, item.disposition ?? null, item.evidence ?? null, feature && feature.title !== item.title ? item.title : null, item.owner ?? null, item.kind ?? null, item.priority ?? null, item.predecessors ? JSON.stringify(item.predecessors) : null, item.deadlineVersion ?? null, item.ceoMinutes ?? null, item.ceoDate ?? null, item.accelerator === true ? 1 : null, item.updatedAt, item.updatedBy);
196
+ db.query(`INSERT INTO assignments (feature_id, version, status, disposition, evidence, title_override, owner, kind, priority, predecessors, deadline_version, ceo_minutes, ceo_date, accelerator, refs, updated_at, updated_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
197
+ ON CONFLICT(feature_id, version) DO UPDATE SET status=excluded.status, disposition=excluded.disposition, evidence=excluded.evidence, owner=excluded.owner, kind=excluded.kind, priority=excluded.priority, predecessors=excluded.predecessors, deadline_version=excluded.deadline_version, ceo_minutes=excluded.ceo_minutes, ceo_date=excluded.ceo_date, accelerator=excluded.accelerator, refs=excluded.refs, updated_at=excluded.updated_at, updated_by=excluded.updated_by`)
198
+ .run(item.id, version, item.status, item.disposition ?? null, item.evidence ?? null, feature && feature.title !== item.title ? item.title : null, item.owner ?? null, item.kind ?? null, item.priority ?? null, item.predecessors ? JSON.stringify(item.predecessors) : null, item.deadlineVersion ?? null, item.ceoMinutes ?? null, item.ceoDate ?? null, item.accelerator === true ? 1 : null, item.refs?.length ? JSON.stringify(item.refs) : null, item.updatedAt, item.updatedBy);
198
199
  }
199
200
  function jsonHash(contents: string | Buffer): string { return createHash('sha256').update(contents).digest('hex'); }
200
201
 
@@ -235,8 +236,8 @@ function importOnDb(db: Database, version: string): boolean {
235
236
  // Keep a legacy per-version title until an explicit retitle unifies the feature identity.
236
237
  if (!existing) putItem(db, version, item);
237
238
  else {
238
- db.query('INSERT OR IGNORE INTO assignments (feature_id, version, status, disposition, evidence, title_override, owner, kind, priority, predecessors, deadline_version, ceo_minutes, ceo_date, accelerator, updated_at, updated_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)')
239
- .run(item.id, version, item.status, item.disposition ?? null, item.evidence ?? null, item.title === existing.title ? null : item.title, item.owner ?? null, item.kind ?? null, item.priority ?? null, item.predecessors ? JSON.stringify(item.predecessors) : null, item.deadlineVersion ?? null, item.ceoMinutes ?? null, item.ceoDate ?? null, item.accelerator === true ? 1 : null, item.updatedAt, item.updatedBy);
239
+ db.query('INSERT OR IGNORE INTO assignments (feature_id, version, status, disposition, evidence, title_override, owner, kind, priority, predecessors, deadline_version, ceo_minutes, ceo_date, accelerator, refs, updated_at, updated_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)')
240
+ .run(item.id, version, item.status, item.disposition ?? null, item.evidence ?? null, item.title === existing.title ? null : item.title, item.owner ?? null, item.kind ?? null, item.priority ?? null, item.predecessors ? JSON.stringify(item.predecessors) : null, item.deadlineVersion ?? null, item.ceoMinutes ?? null, item.ceoDate ?? null, item.accelerator === true ? 1 : null, item.refs?.length ? JSON.stringify(item.refs) : null, item.updatedAt, item.updatedBy);
240
241
  }
241
242
  }
242
243
  for (const entry of data.history) record(db, version, entry);
@@ -263,7 +264,7 @@ function importOnDb(db: Database, version: string): boolean {
263
264
  record(db, version, { at: new Date().toISOString(), by: item.updatedBy, id: item.id, field: 'reimport', from: null, to: item, released: data.released, dev: data.dev });
264
265
  continue;
265
266
  }
266
- const fields = ['title', 'status', 'evidence', 'owner', 'disposition', 'kind', 'priority', 'predecessors', 'deadlineVersion', 'ceoMinutes', 'ceoDate', 'accelerator'] as const;
267
+ const fields = ['title', 'status', 'evidence', 'owner', 'disposition', 'kind', 'priority', 'predecessors', 'deadlineVersion', 'ceoMinutes', 'ceoDate', 'accelerator', 'refs'] as const;
267
268
  const baseEvidence = (db.query('SELECT evidence FROM assignments WHERE feature_id = ? AND version = ?').get(item.id, version) as { evidence: string | null }).evidence ?? undefined;
268
269
  const preserveRefs = item.evidence === baseEvidence || item.evidence === current.evidence;
269
270
  const differs = fields.some((field) => field === 'evidence'
@@ -8,6 +8,7 @@ export interface LandingFreeze {
8
8
  startedAt: string;
9
9
  until: string | null;
10
10
  by: string;
11
+ holdLaunches?: boolean;
11
12
  }
12
13
 
13
14
  export function landingFreezePath(root = effectiveInstanceRoot()): string {
@@ -25,21 +26,22 @@ export function readLandingFreeze(root = effectiveInstanceRoot(), now = new Date
25
26
  const freeze = record as Partial<LandingFreeze>;
26
27
  if (typeof freeze.reason !== 'string' || typeof freeze.by !== 'string' ||
27
28
  typeof freeze.startedAt !== 'string' || !Number.isFinite(Date.parse(freeze.startedAt)) ||
28
- !(freeze.until === null || (typeof freeze.until === 'string' && Number.isFinite(Date.parse(freeze.until))))) {
29
+ !(freeze.until === null || (typeof freeze.until === 'string' && Number.isFinite(Date.parse(freeze.until)))) ||
30
+ (freeze.holdLaunches !== undefined && typeof freeze.holdLaunches !== 'boolean')) {
29
31
  throw new Error(`invalid landing freeze: ${path}`);
30
32
  }
31
33
  // An expired freeze reads as «off». The file is left alone: deleting it here could race a concurrent `freeze on`
32
34
  // that just wrote a new one (only `freeze off` removes it; the next `freeze on` overwrites it).
33
35
  if (freeze.until && Date.parse(freeze.until) <= now.getTime()) return null;
34
- return freeze as LandingFreeze;
36
+ return { ...freeze, holdLaunches: freeze.holdLaunches ?? false } as LandingFreeze;
35
37
  }
36
38
 
37
- export function enableLandingFreeze(opts: { reason?: string; until?: string; by?: string }, root = effectiveInstanceRoot(), now = new Date()): LandingFreeze {
39
+ export function enableLandingFreeze(opts: { reason?: string; until?: string; by?: string; holdLaunches?: boolean }, root = effectiveInstanceRoot(), now = new Date()): LandingFreeze {
38
40
  const until = opts.until === undefined ? null : opts.until;
39
41
  if (until !== null && (!/^\d{4}-\d\d-\d\dT\d\d:\d\d(?::\d\d(?:\.\d+)?)?(?:Z|[+-]\d\d:\d\d)$/.test(until) || !Number.isFinite(Date.parse(until)) || Date.parse(until) <= now.getTime())) {
40
42
  throw new Error('--until must be a future ISO timestamp with timezone');
41
43
  }
42
- const freeze: LandingFreeze = { reason: opts.reason?.trim() || 'operator freeze', startedAt: now.toISOString(), until: until === null ? null : new Date(until).toISOString(), by: opts.by?.trim() || process.env.ELANOUS_TRACK || process.env.USER || 'cli' };
44
+ const freeze: LandingFreeze = { reason: opts.reason?.trim() || 'operator freeze', startedAt: now.toISOString(), until: until === null ? null : new Date(until).toISOString(), by: opts.by?.trim() || process.env.ELANOUS_TRACK || process.env.USER || 'cli', holdLaunches: opts.holdLaunches ?? false };
43
45
  const path = landingFreezePath(root);
44
46
  mkdirSync(root, { recursive: true });
45
47
  const tmp = `${path}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
@@ -8,6 +8,7 @@ import { userConfigPath } from '../user-config.js';
8
8
  import { addItem, listChecklist, ownerMatches, parseOwner, setItem, validateCeoLoad, type ChecklistItem } from './checklist.js';
9
9
  import { move, releasedVersion } from './feature-store.js';
10
10
  import { listSchedules, type ReleaseSchedule } from './release-schedule.js';
11
+ import { parseRubric, rubricScore } from './rubric.js';
11
12
 
12
13
  export type PlacementPriority = 'P0' | 'P1' | 'P2';
13
14
  export interface PlacementCell { id: string; title: string; owner: string; priority: PlacementPriority; predecessors: string[]; deadlineVersion?: string; ceoMinutes?: number; ceoDate?: string; accelerator?: boolean }
@@ -271,20 +272,29 @@ export function rebalance(version: string, deps: PlacementDeps = {}): RebalanceR
271
272
  const backlog = deps.backlog ?? BACKLOG_VERSIONS;
272
273
  const next = rows.find((row) => versionOrder(row.version, version) > 0 && !backlog.includes(row.version));
273
274
  const snapshots = new Map(rows.map((row) => [row.version, [...(deps.checklist ?? listChecklist)(row.version).items]]));
274
- const unstarted = snapshots.get(version)!.filter((item) => item.status === 'yellow' && !item.evidence);
275
+ const unstarted = snapshots.get(version)!.filter((item) => item.status === 'yellow' && !item.evidence)
276
+ .map((item, index) => {
277
+ const rubric = parseRubric(`${item.title}\n${item.evidence ?? ''}`);
278
+ return { item, index, score: rubric ? rubricScore(rubric) : null };
279
+ })
280
+ .sort((a, b) => {
281
+ if (a.score === null) return b.score === null ? a.index - b.index : 1;
282
+ if (b.score === null) return -1;
283
+ return a.score - b.score || a.index - b.index;
284
+ });
275
285
  if (unstarted.length && !next) throw new CliUserError(`${version} 다음 판이 없다`);
276
286
  if (!unstarted.length) return { decisions: [], blocked: [] };
277
287
  const rate = deps.merged24h ?? mergedPrsLast24h(new Date(now));
278
288
  const caps = deps.seatCap ?? placementSeatCap();
279
289
  const ceoCap = validCeoCap(deps.ceoDailyCap ?? placementCeoDailyCap());
280
290
  const result: RebalanceResult = { decisions: [], blocked: [] };
281
- for (const item of unstarted) {
291
+ for (const { item, score } of unstarted) {
282
292
  const violation = moveConstraint(item, next!, backlog, snapshots, now, rate, caps, rows, ceoCap);
283
293
  if (violation) {
284
294
  result.blocked.push({ id: item.id, from: version, to: next!.version, reason: violation });
285
295
  continue;
286
296
  }
287
- const reason = `${version} 착지 마감 2시간 전 미시작 칸 이월`;
297
+ const reason = `${version} 착지 마감 2시간 전 미시작 칸 이월${score === null ? '' : ` · 루브릭 ${score}`}`;
288
298
  result.decisions.push({ id: item.id, from: version, version: next!.version, reason, displaced: [] });
289
299
  snapshots.set(version, snapshots.get(version)!.filter((cell) => cell.id !== item.id));
290
300
  snapshots.get(next!.version)!.push(item);