elanous 0.2.16 → 0.2.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (354) hide show
  1. package/apps/pwa/out/404/index.html +1 -1
  2. package/apps/pwa/out/404.html +1 -1
  3. package/apps/pwa/out/_next/static/P6YTVgzuZz5oTZhY4Din_/_buildManifest.js +1 -0
  4. package/apps/pwa/out/_next/static/chunks/{1307-b46d45b705e8c9ae.js → 1307-30dff22c9e1f3073.js} +1 -1
  5. package/apps/pwa/out/_next/static/chunks/195-fb1d7690bf30bab4.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/1986-33e13d648a9df765.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/2537-6c289376caa88ca1.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/2699-325cc923ba7b8631.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/3857-4f936cdb30322aa6.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/4554-5761612531d0146b.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/4565-3d7e3fdeb43686a8.js +1 -0
  12. package/apps/pwa/out/_next/static/chunks/5977-a11f37254196b659.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/{6158.9c0449a9a8d0064b.js → 6158.af2e93b7adff709b.js} +1 -1
  14. package/apps/pwa/out/_next/static/chunks/6877-74399f69c7da1433.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/7797-df5aa0381fdc078f.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/8401-8615a660c84cdb03.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/8695-846034ce44852c70.js +1 -0
  18. package/apps/pwa/out/_next/static/chunks/9247-269e4464cc4b491c.js +1 -0
  19. package/apps/pwa/out/_next/static/chunks/app/approvals/page-d92f7ee3994e84c7.js +1 -0
  20. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-3cb52e5ea8fc2fa5.js → page-4733c967b56a4277.js} +1 -1
  21. package/apps/pwa/out/_next/static/chunks/app/board/page-d22fd849cc22f104.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/app/botlab/{page-c1e20d7d338713f1.js → page-bb742f995c6769e7.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/app/bots/{page-482c5dbde699947c.js → page-02b073a2ac793b83.js} +1 -1
  24. package/apps/pwa/out/_next/static/chunks/app/ceo/page-60c3da708f9a0ccc.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/app/chat/page-8a9b3594bc23289b.js +1 -0
  26. package/apps/pwa/out/_next/static/chunks/app/consult/page-a0eb02638b1e6f31.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/app/control/{page-a0e2a6d1bfb52fc5.js → page-308111abadf35670.js} +1 -1
  28. package/apps/pwa/out/_next/static/chunks/app/decisions/page-6eaf7bac37830c21.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/app/design-check/{page-c53598cfce6156f2.js → page-5d97b2901e0d1ea6.js} +1 -1
  30. package/apps/pwa/out/_next/static/chunks/app/editor/{page-13a63953f53c6a23.js → page-c39554c321576982.js} +1 -1
  31. package/apps/pwa/out/_next/static/chunks/app/exec/{page-5d3c8429d04f3831.js → page-8c3d8e63ae2e1c98.js} +1 -1
  32. package/apps/pwa/out/_next/static/chunks/app/field/{page-3193bdc0bbab047a.js → page-9b72a07d424701c6.js} +1 -1
  33. package/apps/pwa/out/_next/static/chunks/app/inside/page-bd5d586429167fea.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/app/intake/{page-6107ca8ddd5e3bdd.js → page-a572f58cd3ca5864.js} +1 -1
  35. package/apps/pwa/out/_next/static/chunks/app/layout-59b7834d497c2255.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/app/live/page-be5ea965a07fbc41.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/app/loops/page-c130b63862437814.js +1 -0
  38. package/apps/pwa/out/_next/static/chunks/app/market/page-fa244bd146a00ae8.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-44d56d6391965a6a.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/app/morning/{page-4167348049166852.js → page-83a700d4c754d802.js} +1 -1
  41. package/apps/pwa/out/_next/static/chunks/app/{not-found-de52e8fdf9095f7a.js → not-found-7b3170f374f77b9d.js} +1 -1
  42. package/apps/pwa/out/_next/static/chunks/app/observatory/page-5f23d0c66a3b4793.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-ca3952cd78a83ca2.js → page-a3c58f5c72a5f3f9.js} +1 -1
  44. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-a2c4625596f8d69c.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-0e9099531d239f11.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/app/outputs/page-e8e1ecaea7cd71d2.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/app/page-a1b7ffed710a93e0.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/app/reflection/{page-1ebfe049c640e171.js → page-1467047104141297.js} +1 -1
  49. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-2da6f9b8a38c6978.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/sessions/page-102310930791197a.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-f331e38fefc00393.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/settings/page-6ed0b73bae8351d8.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-abe8e3b74bea1ffc.js → page-44d5f767491692bf.js} +1 -1
  54. package/apps/pwa/out/_next/static/chunks/app/setup/page-43336055451f6e37.js +1 -0
  55. package/apps/pwa/out/_next/static/chunks/app/share/{page-d28573d6792b4275.js → page-3cc47dca0922ab9a.js} +1 -1
  56. package/apps/pwa/out/_next/static/chunks/app/showroom/page-a77ee355a2304ff0.js +1 -0
  57. package/apps/pwa/out/_next/static/chunks/app/tasks/{page-d751de5f8062ff5e.js → page-b17cd46cf0e9b8cf.js} +1 -1
  58. package/apps/pwa/out/_next/static/chunks/app/term/{page-fe1759f94003b32b.js → page-61553851aa606146.js} +1 -1
  59. package/apps/pwa/out/_next/static/chunks/app/today/{page-d64c984e855c8c68.js → page-a1f585e154e61c59.js} +1 -1
  60. package/apps/pwa/out/_next/static/chunks/app/trace/page-7bf54c9bb9274af8.js +1 -0
  61. package/apps/pwa/out/_next/static/chunks/app/vault/{page-9afdee66b820c76e.js → page-5150ef17b5383a57.js} +1 -1
  62. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-b9d8ce70792c1672.js +1 -0
  63. package/apps/pwa/out/_next/static/chunks/app/workflows/page-a5a3de231f2cc7d5.js +1 -0
  64. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-3544f154c87b43ee.js → page-134da468538baf48.js} +1 -1
  65. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-31ac4afa2dcd915c.js +1 -0
  66. package/apps/pwa/out/_next/static/chunks/{webpack-539cde99554be20f.js → webpack-c2782b330447ffb6.js} +1 -1
  67. package/apps/pwa/out/_next/static/css/7fa358fec7da7c47.css +1 -0
  68. package/apps/pwa/out/approvals/index.html +1 -1
  69. package/apps/pwa/out/approvals/index.txt +14 -14
  70. package/apps/pwa/out/autopilot/index.html +1 -1
  71. package/apps/pwa/out/autopilot/index.txt +14 -14
  72. package/apps/pwa/out/board/index.html +1 -1
  73. package/apps/pwa/out/board/index.txt +14 -14
  74. package/apps/pwa/out/botlab/index.html +1 -1
  75. package/apps/pwa/out/botlab/index.txt +14 -14
  76. package/apps/pwa/out/bots/index.html +1 -1
  77. package/apps/pwa/out/bots/index.txt +14 -14
  78. package/apps/pwa/out/ceo/index.html +1 -1
  79. package/apps/pwa/out/ceo/index.txt +14 -14
  80. package/apps/pwa/out/chat/index.html +1 -1
  81. package/apps/pwa/out/chat/index.txt +14 -14
  82. package/apps/pwa/out/consult/index.html +1 -1
  83. package/apps/pwa/out/consult/index.txt +14 -14
  84. package/apps/pwa/out/control/index.html +1 -1
  85. package/apps/pwa/out/control/index.txt +14 -14
  86. package/apps/pwa/out/decisions/index.html +1 -0
  87. package/apps/pwa/out/decisions/index.txt +30 -0
  88. package/apps/pwa/out/design-check/index.html +1 -1
  89. package/apps/pwa/out/design-check/index.txt +14 -14
  90. package/apps/pwa/out/editor/index.html +1 -1
  91. package/apps/pwa/out/editor/index.txt +14 -14
  92. package/apps/pwa/out/exec/index.html +1 -1
  93. package/apps/pwa/out/exec/index.txt +14 -14
  94. package/apps/pwa/out/field/index.html +1 -1
  95. package/apps/pwa/out/field/index.txt +14 -14
  96. package/apps/pwa/out/healthz +1 -1
  97. package/apps/pwa/out/index.html +1 -1
  98. package/apps/pwa/out/index.txt +14 -14
  99. package/apps/pwa/out/inside/index.html +1 -1
  100. package/apps/pwa/out/inside/index.txt +14 -14
  101. package/apps/pwa/out/intake/index.html +1 -1
  102. package/apps/pwa/out/intake/index.txt +14 -14
  103. package/apps/pwa/out/live/index.html +1 -1
  104. package/apps/pwa/out/live/index.txt +14 -14
  105. package/apps/pwa/out/loops/index.html +1 -1
  106. package/apps/pwa/out/loops/index.txt +14 -14
  107. package/apps/pwa/out/market/index.html +1 -1
  108. package/apps/pwa/out/market/index.txt +14 -14
  109. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  110. package/apps/pwa/out/missions/placeholder/index.txt +14 -14
  111. package/apps/pwa/out/morning/index.html +1 -1
  112. package/apps/pwa/out/morning/index.txt +14 -14
  113. package/apps/pwa/out/observatory/index.html +1 -1
  114. package/apps/pwa/out/observatory/index.txt +14 -14
  115. package/apps/pwa/out/ops/checklist/index.html +1 -1
  116. package/apps/pwa/out/ops/checklist/index.txt +14 -14
  117. package/apps/pwa/out/ops/release/index.html +1 -1
  118. package/apps/pwa/out/ops/release/index.txt +14 -14
  119. package/apps/pwa/out/ops/seats/index.html +1 -1
  120. package/apps/pwa/out/ops/seats/index.txt +14 -14
  121. package/apps/pwa/out/outputs/index.html +1 -1
  122. package/apps/pwa/out/outputs/index.txt +14 -14
  123. package/apps/pwa/out/reflection/index.html +1 -1
  124. package/apps/pwa/out/reflection/index.txt +15 -15
  125. package/apps/pwa/out/scheduler/index.html +1 -1
  126. package/apps/pwa/out/scheduler/index.txt +14 -14
  127. package/apps/pwa/out/sessions/index.html +1 -1
  128. package/apps/pwa/out/sessions/index.txt +14 -14
  129. package/apps/pwa/out/settings/devices/index.html +1 -1
  130. package/apps/pwa/out/settings/devices/index.txt +14 -14
  131. package/apps/pwa/out/settings/index.html +1 -1
  132. package/apps/pwa/out/settings/index.txt +14 -14
  133. package/apps/pwa/out/setup/done/index.html +1 -1
  134. package/apps/pwa/out/setup/done/index.txt +14 -14
  135. package/apps/pwa/out/setup/index.html +1 -1
  136. package/apps/pwa/out/setup/index.txt +14 -14
  137. package/apps/pwa/out/share/index.html +1 -1
  138. package/apps/pwa/out/share/index.txt +14 -14
  139. package/apps/pwa/out/showroom/index.html +1 -1
  140. package/apps/pwa/out/showroom/index.txt +14 -14
  141. package/apps/pwa/out/tasks/index.html +1 -1
  142. package/apps/pwa/out/tasks/index.txt +14 -14
  143. package/apps/pwa/out/term/index.html +1 -1
  144. package/apps/pwa/out/term/index.txt +14 -14
  145. package/apps/pwa/out/today/index.html +1 -1
  146. package/apps/pwa/out/today/index.txt +14 -14
  147. package/apps/pwa/out/trace/index.html +1 -1
  148. package/apps/pwa/out/trace/index.txt +14 -14
  149. package/apps/pwa/out/vault/index.html +1 -1
  150. package/apps/pwa/out/vault/index.txt +14 -14
  151. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  152. package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
  153. package/apps/pwa/out/workflows/index.html +1 -1
  154. package/apps/pwa/out/workflows/index.txt +14 -14
  155. package/apps/pwa/out/workspace/index.html +1 -1
  156. package/apps/pwa/out/workspace/index.txt +14 -14
  157. package/apps/pwa/out/worktrees/index.html +1 -1
  158. package/apps/pwa/out/worktrees/index.txt +14 -14
  159. package/graphs/intake/intake-daily.yaml +9 -2
  160. package/graphs/intake/news-sources.yaml +4 -0
  161. package/graphs/intake/recipes.yaml +6 -1
  162. package/package.json +2 -1
  163. package/samples/workflows/templates/approval-gated-deploy.yaml +30 -0
  164. package/samples/workflows/templates/chat-driven-research.yaml +35 -0
  165. package/samples/workflows/templates/discord-listen-and-respond.yaml +29 -0
  166. package/samples/workflows/templates/http-poll-then-alert.yaml +30 -0
  167. package/samples/workflows/templates/manual-quickstart.yaml +21 -0
  168. package/samples/workflows/templates/scheduled-daily-digest.yaml +30 -0
  169. package/samples/workflows/templates/webhook-deploy-hook.yaml +27 -0
  170. package/scripts/__fixtures__/intake-news.xml +7 -0
  171. package/scripts/ci-public-export-import-gate.ts +7 -2
  172. package/scripts/ci-typecheck-changed.ts +21 -6
  173. package/scripts/discord-seat-probe.ts +3 -2
  174. package/scripts/docs/generate-cli-anatomy.ts +162 -0
  175. package/scripts/goals/authoring-metrics.ts +73 -0
  176. package/scripts/intake-news.ts +169 -0
  177. package/scripts/landing-heal/must-fix-match.ts +7 -7
  178. package/scripts/landing-heal/run-mission.ts +3 -2
  179. package/scripts/public-export-test-run.ts +37 -2
  180. package/scripts/release-loop/checklist-gate-node.ts +13 -11
  181. package/scripts/release-loop/cut-branch.ts +2 -1
  182. package/scripts/release-loop/cutoff.ts +4 -2
  183. package/scripts/release-loop/docs-land-node.ts +77 -11
  184. package/scripts/release-loop/docs-node.ts +1 -1
  185. package/scripts/release-loop/gate-node.ts +99 -14
  186. package/scripts/release-loop/node-verdict.ts +2 -1
  187. package/scripts/release-loop/npm-publish-node.ts +11 -7
  188. package/scripts/release-loop/ops-upgrade-node.ts +8 -1
  189. package/scripts/release-loop/preflight.ts +246 -0
  190. package/scripts/release-loop/publish-node.ts +10 -3
  191. package/scripts/release-loop/release-docs.ts +134 -0
  192. package/scripts/release-loop/release-readiness.ts +11 -6
  193. package/scripts/release-loop/release-version.ts +48 -0
  194. package/scripts/release-loop/resume-release.ts +115 -0
  195. package/scripts/release-loop/unattended-release.ts +43 -7
  196. package/scripts/release-loop/version-node.ts +149 -41
  197. package/scripts/release-story/draft.ts +42 -14
  198. package/scripts/rhythm/daily.ts +25 -4
  199. package/scripts/skills/public-check.ts +62 -12
  200. package/scripts/test-deterministic.ts +51 -33
  201. package/scripts/test-diet/effectiveness.ts +62 -1
  202. package/scripts/test-diet/lib.ts +1 -1
  203. package/scripts/test-diet/node.ts +8 -1
  204. package/src/agent-mission/driver.ts +53 -10
  205. package/src/agent-mission/review-loop.ts +4 -3
  206. package/src/agent-substrate/pr-comment-meta.ts +20 -0
  207. package/src/autopilot/build/llm-conflict-merge.ts +28 -8
  208. package/src/autopilot/track-agent.ts +5 -3
  209. package/src/briefing/brief-items.ts +86 -17
  210. package/src/cli/autopilot-cli.ts +86 -1
  211. package/src/cli/brief-cli.ts +36 -2
  212. package/src/cli/decisions-cli.ts +4 -1
  213. package/src/cli/freeze-cli.ts +3 -2
  214. package/src/cli/intake-cli.ts +74 -2
  215. package/src/cli/log-event-names.ts +1 -0
  216. package/src/cli/notify-cli.ts +75 -0
  217. package/src/cli/pr-cli.ts +12 -1
  218. package/src/cli/provider-cli.ts +24 -0
  219. package/src/cli/release-cli.ts +101 -17
  220. package/src/cli/self-update-canary.ts +46 -0
  221. package/src/cli/self-update.ts +200 -1
  222. package/src/cli/tasks-cli.ts +309 -3
  223. package/src/context-bus/condense.ts +19 -3
  224. package/src/context-bus/context-day.ts +77 -24
  225. package/src/context-bus/context-first.ts +33 -0
  226. package/src/context-card/index.ts +45 -0
  227. package/src/context-card/task.ts +76 -0
  228. package/src/dashboard/slash-runtime/dashboard-handlers.ts +2 -0
  229. package/src/decisions/decision-ledger.ts +10 -1
  230. package/src/discord-self-message.ts +20 -2
  231. package/src/discord-slash-wire.ts +1 -0
  232. package/src/domains/outbound-alert.ts +200 -27
  233. package/src/domains/outbound-log-probe.ts +16 -1
  234. package/src/domains/release-tool.ts +2 -2
  235. package/src/execution-loop/gates.ts +11 -2
  236. package/src/flow-loop/split.ts +8 -4
  237. package/src/git-fs/worktree.ts +23 -3
  238. package/src/harness/harness-cli-command.ts +151 -11
  239. package/src/harness/harness-pod-dispatch.ts +11 -0
  240. package/src/harness/harness-queue.ts +466 -32
  241. package/src/harness/helper-scan.ts +10 -3
  242. package/src/harness/nested-elanous-policy.ts +23 -4
  243. package/src/hq/fence-wrapper.ts +123 -0
  244. package/src/hq/hq.ts +8 -0
  245. package/src/hq/seats.ts +369 -0
  246. package/src/index.ts +190 -23
  247. package/src/intake-plane/adopt-gate.ts +49 -0
  248. package/src/intake-plane/check.ts +3 -0
  249. package/src/intake-plane/collect-github.ts +57 -25
  250. package/src/intake-plane/digest.ts +39 -4
  251. package/src/intake-plane/discord-seat-work.ts +8 -2
  252. package/src/intake-plane/requirement-funnel.ts +316 -0
  253. package/src/knowledge/know-find.ts +10 -1
  254. package/src/loops/budget.ts +36 -0
  255. package/src/loops/orchestrator/finish-rate.ts +73 -1
  256. package/src/loops/orchestrator/grid-place.ts +11 -0
  257. package/src/loops/orchestrator/tick.ts +295 -20
  258. package/src/maturity/feature-maturity.ts +3 -2
  259. package/src/nexus/api/decisions-route.ts +11 -1
  260. package/src/nexus/api/grid.ts +61 -0
  261. package/src/nexus/api/http-server.ts +19 -1
  262. package/src/nexus/api/loop-edges.ts +69 -13
  263. package/src/nexus/api/ops-seats-sources.ts +2 -2
  264. package/src/nexus/api/task-cards-api.ts +19 -0
  265. package/src/nexus/api/workflow-templates.ts +3 -0
  266. package/src/oauth/codex.ts +120 -2
  267. package/src/onboarding/first-chat-gate.ts +29 -0
  268. package/src/release-loop/checklist.ts +104 -6
  269. package/src/release-loop/feature-store.ts +10 -9
  270. package/src/release-loop/landing-freeze.ts +24 -14
  271. package/src/release-loop/placement.ts +13 -3
  272. package/src/release-loop/rubric.ts +80 -0
  273. package/src/seat-dispatch/seat-ask.ts +79 -16
  274. package/src/seat-loop/seat-loop.ts +107 -37
  275. package/src/self-dev/ask-launch-flow.ts +50 -10
  276. package/src/self-dev/ask-path-candidates.ts +68 -0
  277. package/src/self-dev/dev-pipeline.ts +4 -0
  278. package/src/self-dev/draft-triage-rules.ts +1 -1
  279. package/src/self-dev/orchestrate-cli.ts +22 -0
  280. package/src/self-dev/round-pr-supersede.ts +244 -0
  281. package/src/self-dev/run-store.ts +5 -1
  282. package/src/self-dev/run-supervisor.ts +21 -1
  283. package/src/self-dev/supervisor-verdict-edges.ts +46 -0
  284. package/src/self-implement/follow-up-goals.ts +2 -2
  285. package/src/self-implement/frozen-merges.ts +25 -20
  286. package/src/self-implement/gate-remote.ts +427 -0
  287. package/src/self-implement/goal-author-clarification.ts +81 -0
  288. package/src/self-implement/goal-author-cli.ts +33 -5
  289. package/src/self-implement/goal-author.ts +93 -12
  290. package/src/self-implement/goal-clarification-escalation.ts +37 -1
  291. package/src/self-implement/goal-document-write.ts +28 -0
  292. package/src/self-implement/heal-intake.ts +4 -0
  293. package/src/self-implement/host-regate.ts +3 -1
  294. package/src/self-implement/l8-merge-queue.ts +29 -5
  295. package/src/self-implement/orchestrator.ts +78 -13
  296. package/src/self-implement/prior-draft-findings.ts +4 -3
  297. package/src/skills/skill-feedback.ts +76 -0
  298. package/src/task-agent/actions.ts +151 -0
  299. package/src/task-agent/judge.ts +122 -0
  300. package/src/task-agent/mission.ts +423 -0
  301. package/src/task-agent/shadow.ts +90 -0
  302. package/src/task-agent/task-hand.ts +235 -0
  303. package/src/task-cards/card-store.ts +4 -1
  304. package/src/task-orchestrator/surfaces/pod-failure-reason.ts +43 -4
  305. package/src/task-orchestrator/surfaces/pod-lease.ts +5 -2
  306. package/src/task-orchestrator/surfaces/pod-pool.ts +81 -13
  307. package/src/task-orchestrator/surfaces/self-implement-pod.ts +225 -35
  308. package/src/telegram-commands.ts +1 -1
  309. package/src/telegram-loops-command.ts +22 -15
  310. package/src/telegram.ts +5 -20
  311. package/src/user-config.ts +86 -4
  312. package/src/version/packed-revision.json +1 -1
  313. package/apps/pwa/out/_next/static/chunks/2699-63b73fa4d3bcf6ff.js +0 -1
  314. package/apps/pwa/out/_next/static/chunks/4554-3565eea1f1495ba7.js +0 -1
  315. package/apps/pwa/out/_next/static/chunks/4614-21403aac730d4586.js +0 -1
  316. package/apps/pwa/out/_next/static/chunks/5135-44008fe8b1f2aa41.js +0 -1
  317. package/apps/pwa/out/_next/static/chunks/5233-bf28bd6bd7a32099.js +0 -1
  318. package/apps/pwa/out/_next/static/chunks/5977-c952712c87a1f757.js +0 -1
  319. package/apps/pwa/out/_next/static/chunks/6877-7c6208705c284124.js +0 -1
  320. package/apps/pwa/out/_next/static/chunks/7395-164e238f471b5d7b.js +0 -1
  321. package/apps/pwa/out/_next/static/chunks/7787-02a1c0b04e25e811.js +0 -1
  322. package/apps/pwa/out/_next/static/chunks/8827-e2f5b09a8d37e83b.js +0 -1
  323. package/apps/pwa/out/_next/static/chunks/8949-ecb4128a0858b911.js +0 -1
  324. package/apps/pwa/out/_next/static/chunks/899-46946fe8363e1262.js +0 -1
  325. package/apps/pwa/out/_next/static/chunks/9247-29d889b809288e65.js +0 -1
  326. package/apps/pwa/out/_next/static/chunks/app/approvals/page-79c3ed84baaedd03.js +0 -1
  327. package/apps/pwa/out/_next/static/chunks/app/board/page-a424ef80885d1743.js +0 -1
  328. package/apps/pwa/out/_next/static/chunks/app/ceo/page-1e962b31d1f11081.js +0 -1
  329. package/apps/pwa/out/_next/static/chunks/app/chat/page-8c2d9526164f5d09.js +0 -1
  330. package/apps/pwa/out/_next/static/chunks/app/consult/page-ffff6d819fbc6e0f.js +0 -1
  331. package/apps/pwa/out/_next/static/chunks/app/inside/page-76270efaee638f17.js +0 -1
  332. package/apps/pwa/out/_next/static/chunks/app/layout-dce686e582c23937.js +0 -1
  333. package/apps/pwa/out/_next/static/chunks/app/live/page-d3d53b146416e422.js +0 -1
  334. package/apps/pwa/out/_next/static/chunks/app/loops/page-1a61c7ca1ceb3ae7.js +0 -1
  335. package/apps/pwa/out/_next/static/chunks/app/market/page-51f811fa119308b6.js +0 -1
  336. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-dacff46049e1557e.js +0 -1
  337. package/apps/pwa/out/_next/static/chunks/app/observatory/page-36a7d994f4ca0299.js +0 -1
  338. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-5b042736aa113756.js +0 -1
  339. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-acb749f1e432bcf3.js +0 -1
  340. package/apps/pwa/out/_next/static/chunks/app/outputs/page-cd78862707999911.js +0 -1
  341. package/apps/pwa/out/_next/static/chunks/app/page-54b9ef8afe6278fd.js +0 -1
  342. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-57924393e3e4e539.js +0 -1
  343. package/apps/pwa/out/_next/static/chunks/app/sessions/page-9e0ce6c8129fa641.js +0 -1
  344. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-31fa1ce4c4aff9ae.js +0 -1
  345. package/apps/pwa/out/_next/static/chunks/app/settings/page-0f5cd57ca928f42b.js +0 -1
  346. package/apps/pwa/out/_next/static/chunks/app/setup/page-4364ef9f4e7c7f0f.js +0 -1
  347. package/apps/pwa/out/_next/static/chunks/app/showroom/page-59f03fda76c03d22.js +0 -1
  348. package/apps/pwa/out/_next/static/chunks/app/trace/page-86914b0dee8ab7f8.js +0 -1
  349. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-d4aef67abf264874.js +0 -1
  350. package/apps/pwa/out/_next/static/chunks/app/workflows/page-1ea64acadd704ace.js +0 -1
  351. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-30c2073bfc21c004.js +0 -1
  352. package/apps/pwa/out/_next/static/css/001d5fbaee388a25.css +0 -1
  353. package/apps/pwa/out/_next/static/rv9cierV0jSOvDBm1g0XB/_buildManifest.js +0 -1
  354. /package/apps/pwa/out/_next/static/{rv9cierV0jSOvDBm1g0XB → P6YTVgzuZz5oTZhY4Din_}/_ssgManifest.js +0 -0
@@ -0,0 +1,75 @@
1
+ import type { Command } from 'commander';
2
+ import { inQuietHours, sendOutbound, userRecentlyActive } from '../domains/outbound-alert.js';
3
+ import { isTradingKind } from '../domains/telegram-kind-route.js';
4
+
5
+ export type NotifyPlan = { ok: true; kind: string } | { ok: false; exitCode: 2; message: string };
6
+
7
+ export function planNotify({ kind = 'op-report', text }: { kind?: string; text: string }): NotifyPlan {
8
+ if (isTradingKind(kind)) {
9
+ return { ok: false, exitCode: 2, message: `매매 kind(${kind})는 운영 보고에 쓸 수 없습니다 — op-report 또는 ops-alert 를 쓰세요` };
10
+ }
11
+ if (!kind.trim() || /[\r\n]/.test(kind)) {
12
+ return { ok: false, exitCode: 2, message: 'kind 가 비었거나 줄바꿈을 포함합니다 — op-report 또는 ops-alert 를 쓰세요' };
13
+ }
14
+ if (!text.trim()) return { ok: false, exitCode: 2, message: '본문이 비었습니다 — elanous notify <text...> 또는 --stdin' };
15
+ return { ok: true, kind };
16
+ }
17
+
18
+ export interface NotifyCliDeps {
19
+ send?: (text: string, kind: string) => boolean | Promise<boolean>;
20
+ quiet?: () => boolean;
21
+ readStdin?: () => Promise<string>;
22
+ output?: (line: string) => void;
23
+ error?: (line: string) => void;
24
+ setExitCode?: (code: number) => void;
25
+ }
26
+
27
+ export function registerNotifyCommand(program: Command, deps: NotifyCliDeps = {}): void {
28
+ const send = deps.send ?? sendOutbound;
29
+ const quiet = deps.quiet ?? (() => inQuietHours() && !userRecentlyActive());
30
+ const readStdin = deps.readStdin ?? (async () => {
31
+ if (process.stdin.isTTY) throw new Error('--stdin requires redirected input');
32
+ const chunks: Buffer[] = [];
33
+ for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk));
34
+ return Buffer.concat(chunks).toString('utf8');
35
+ });
36
+ const output = deps.output ?? ((line: string) => console.log(line));
37
+ const error = deps.error ?? ((line: string) => console.error(line));
38
+ const setExitCode = deps.setExitCode ?? ((code: number) => { process.exitCode = code; });
39
+
40
+ program.command('notify [text...]')
41
+ .description('대표에게 운영 보고를 발송한다 (기본 kind: op-report)')
42
+ .option('--kind <kind>', '운영 알림 kind (매매 report|alert|digest 거부)')
43
+ .option('--stdin', '본문을 표준 입력에서 읽는다')
44
+ .option('--json', '결과를 JSON 한 줄로 출력한다')
45
+ .action(async (parts: string[], opts: { kind?: string; stdin?: boolean; json?: boolean }) => {
46
+ const kind = opts.kind ?? 'op-report';
47
+ if (opts.stdin && parts.length) {
48
+ if (opts.json) output(JSON.stringify({ ok: false, kind, chars: 0, deferred: false }));
49
+ else error('--stdin 과 본문 인자는 함께 쓸 수 없습니다');
50
+ setExitCode(2);
51
+ return;
52
+ }
53
+ try {
54
+ const text = opts.stdin ? await readStdin() : parts.join(' ');
55
+ const plan = planNotify({ kind, text });
56
+ if (!plan.ok) {
57
+ if (opts.json) output(JSON.stringify({ ok: false, kind, chars: [...text].length, deferred: false }));
58
+ else error(plan.message);
59
+ setExitCode(plan.exitCode);
60
+ return;
61
+ }
62
+ const deferred = quiet();
63
+ const ok = await send(text, plan.kind);
64
+ const chars = [...text].length;
65
+ if (opts.json) output(JSON.stringify({ ok, kind: plan.kind, chars, deferred: ok && deferred }));
66
+ else if (ok) output(`${deferred ? '보류됨' : '보냄'}(${plan.kind}) · ${chars}자`);
67
+ else error(`못 보냄(${plan.kind}) — elanous logs --category outbound.send 로 확인`);
68
+ setExitCode(ok ? 0 : 1);
69
+ } catch {
70
+ if (opts.json) output(JSON.stringify({ ok: false, kind, chars: 0, deferred: false }));
71
+ else error(`못 보냄(${kind}) — elanous logs --category outbound.send 로 확인`);
72
+ setExitCode(1);
73
+ }
74
+ });
75
+ }
package/src/cli/pr-cli.ts CHANGED
@@ -75,6 +75,8 @@ export interface PrLandOpts {
75
75
  excludeActiveRunFiles?: boolean;
76
76
  excludeActiveRunSources?: boolean;
77
77
  landReason?: string;
78
+ /** Explicit reason for bypassing the landing freeze. */
79
+ forceFreeze?: string;
78
80
  /** Land even though changed files would leak into the public export (the reason goes in --land-reason). */
79
81
  allowPublicLeak?: boolean;
80
82
  }
@@ -130,6 +132,8 @@ export interface PrLandDeps {
130
132
  overlapConfirmChannels?: ConfirmChannel[];
131
133
  overlapConfirmTimeoutMs?: number;
132
134
  decideOverlapLanding?: () => OverlapLandingDecision | Promise<OverlapLandingDecision>;
135
+ /** Operational freeze authority (isolated in tests). */
136
+ prodFreezeRoot?: string;
133
137
  }
134
138
 
135
139
  export const OVERLAP_DECISION_PROMPT = '겹친 파일이 있습니다. 이번 착지를 그대로 병합할까요?';
@@ -1132,6 +1136,10 @@ export async function runPrLand(opts: PrLandOpts = {}, deps: PrLandDeps = {}): P
1132
1136
  out.error('✗ body: --body와 --body-file은 함께 사용할 수 없습니다.');
1133
1137
  return 1;
1134
1138
  }
1139
+ if (opts.forceFreeze !== undefined && !opts.forceFreeze.trim()) {
1140
+ out.error('✗ freeze: --force-freeze 에는 이유가 필요합니다.');
1141
+ return 1;
1142
+ }
1135
1143
  if (opts.allowPublicLeak && !opts.landReason?.trim()) {
1136
1144
  out.error('✗ public-export-leak: --allow-public-leak 에는 --land-reason "<이유>" 가 필요합니다.');
1137
1145
  return 1;
@@ -1649,7 +1657,8 @@ export async function runPrLand(opts: PrLandOpts = {}, deps: PrLandDeps = {}): P
1649
1657
  const headCommit = landedHead.ok && /^[a-f0-9]{40}$/i.test(landedHead.out.trim()) ? landedHead.out.trim() : undefined;
1650
1658
  const validPr = Number.isSafeInteger(prNumber) && prNumber > 0;
1651
1659
  const heldEntry = validPr && headCommit ? { prNumber, headCommit, repoRoot: cwd } : null;
1652
- const admission = admitLandingMerge(heldEntry, undefined, {}, validPr ? { prNumber, repoRoot: cwd, ...(headCommit ? { headCommit } : {}) } : undefined);
1660
+ const admission = admitLandingMerge(heldEntry, undefined, {}, validPr ? { prNumber, repoRoot: cwd, ...(headCommit ? { headCommit } : {}) } : undefined,
1661
+ { prodFreezeRoot: deps.prodFreezeRoot, forceReason: opts.forceFreeze });
1653
1662
  if (admission.kind === 'held' && !heldEntry) {
1654
1663
  out.error(`✗ freeze: ready PR의 검증 HEAD/번호를 보존하지 못했습니다: ${upsert.url}`);
1655
1664
  return 1;
@@ -1712,6 +1721,7 @@ export async function runPrLand(opts: PrLandOpts = {}, deps: PrLandDeps = {}): P
1712
1721
  }
1713
1722
  return 1;
1714
1723
  }
1724
+ if (opts.forceFreeze) debug.log('release-loop.freeze', 'force-merge', { pr: prNumber, reason: opts.forceFreeze.trim() });
1715
1725
  record('merge', true, { result: merged.kind, ...(merged.remoteBranchDeletion ? { remoteBranchDeletion: merged.remoteBranchDeletion.detail } : {}) });
1716
1726
  if (merged.remoteBranchDeletion) {
1717
1727
  out.log(`⚠ remote branch deletion: ${merged.remoteBranchDeletion.detail}`);
@@ -1763,6 +1773,7 @@ export function registerPrCommands(program: Command, deps: PrLandDeps = {}, queu
1763
1773
  .option('--exclude-active-run-files', '도는 런과 겹친 파일을 이번 착지에서 제외')
1764
1774
  .option('--exclude-active-run-sources', '⚠ 위험: --exclude-active-run-files 가 골 문서가 아닌 파일(소스)을 빼도 착지를 강행')
1765
1775
  .option('--land-reason <text>', '권고를 보고도 지금 내는 이유(예: 남이 기다리는 차단 해제)')
1776
+ .option('--force-freeze <reason>', '동결을 넘겨 병합할 명시적 이유(필수)')
1766
1777
  .option('--allow-public-leak', '바뀐 파일이 공개본에 사적 흔적을 실어도 착지(이유는 --land-reason)')
1767
1778
  .action(async (opts: PrLandOpts) => {
1768
1779
  // ⛔⭐⭐⭐ **관측 sink 를 먼저 건다** — 라이브 도그푸드가 잡은 결함(2026-08-03).
@@ -392,6 +392,30 @@ export function registerProviderCommands(program: Command): void {
392
392
  console.log(' 확인: bun bin/elanous.mjs provider codex account list · bun bin/elanous.mjs usage');
393
393
  });
394
394
 
395
+ // POD-TOKEN-PREREFRESH (10-06): 사람이 두 번 손으로 한 «백업 → refresh → 원자적 0600 쓰기 → import» 를 한 명령으로.
396
+ // ⛔ 토큰 값은 안 찍는다 — 남은 시간 전→후만. ⛔ default(~/.codex · 대표 기본 계정)는 거부.
397
+ accountCmd
398
+ .command('refresh <name>')
399
+ .description('그 계정의 codex 토큰을 호스트에서 갱신해 정본 스토어로 반영한다 (⛔ default 거부 · 토큰 값은 안 찍는다 · ⚠️ refresh 토큰은 갱신마다 회전 — 본부 한 곳에서만)')
400
+ .action(async (name: string) => {
401
+ const { refreshCodexAccountHome, codexAccountRefreshLockPath, withCodexAccountRefreshLock, CodexRefreshLockUnavailableError } = await import('../oauth/codex.js');
402
+ // PREREFRESH-LOCK: the same per-account lock as the Pod pre-refresh — a hand refresh never races a launch.
403
+ // 이름 있는 계정의 잠금 자리를 못 구하면 잠금 없이 갱신하지 않는다(Pod 선갱신과 경합하는 자리) — default 는 refreshCodexAccountHome 이 거부한다.
404
+ const lockPath = name === 'default' ? null : codexAccountRefreshLockPath(name);
405
+ if (name !== 'default' && !lockPath) { console.error(`갱신 거부: ${name} 의 잠금 자리를 못 구했다(계정 홈 없음) — 잠금 없이 갱신하지 않는다`); process.exitCode = 1; return; }
406
+ let r: Awaited<ReturnType<typeof refreshCodexAccountHome>>;
407
+ try { r = await withCodexAccountRefreshLock(lockPath, () => refreshCodexAccountHome(name)); }
408
+ catch (lockError) {
409
+ if (!(lockError instanceof CodexRefreshLockUnavailableError)) throw lockError;
410
+ const why = lockError.reason === 'lock-timeout' ? '다른 갱신이 진행 중 — 잠시 뒤 다시' : `잠금 파일을 못 만들었다: ${lockError.message}`;
411
+ console.error(`갱신 거부: ${name} 의 갱신 잠금을 못 잡았다(${why})`); process.exitCode = 1; return;
412
+ }
413
+ if (!r.ok) { console.error(`갱신 실패(${r.kind}): ${r.message}`); process.exitCode = 1; return; }
414
+ const h = (v: number | null) => (v === null ? '?' : `${v}h`);
415
+ console.log(`✅ ${name} 갱신 — 남은 시간 ${h(r.beforeH)} → ${h(r.afterH)} · storeKey=${r.storeKey}`);
416
+ console.log(` 백업: ${r.backupPath}`);
417
+ });
418
+
395
419
  const resetCreditsCmd = codexCmd.command('reset-credits').description('리셋 크레딧 — 조회 · 관측 · 사용(⛔ 사용은 되돌릴 수 없다)');
396
420
 
397
421
  resetCreditsCmd
@@ -5,8 +5,10 @@
5
5
  // elanous release tag --version <x.y.z> --source <commit> [--yes]
6
6
  // elanous release verify [--version <x.y.z>]
7
7
  // elanous release notes --from <ref> [--to <ref>]
8
- // elanous release run --version <x.y.z> [--cut-commit <sha>] [--dry-run] [--json] [--if-ready]
8
+ // elanous release run --version <x.y.z> [--prerelease rc] [--main-cut | --cut-commit <sha>] [--dry-run] [--json] [--if-ready]
9
+ // elanous release resume --run <runId> --from <node> [--json] (release/<v> 수리 뒤 새 끝으로 이어 달리기)
9
10
  // elanous release auto-start [--window-minutes <n>] [--apply] [--json]
11
+ // elanous release preflight --version <x.y.z> [--publish-at <iso>] [--json] (컷 30분 전 사전 점검 · 읽기 전용 · ⛔ 있으면 exit 1)
10
12
  // elanous release cut-branch --version <x.y.z> --base <sha> --pick <sha>... [--dry-run]
11
13
  //
12
14
  // 🩸 계기(2026-09-25 v0.1.0 첫 공개): 손으로 밟은 절차에서 둘을 빠뜨릴 뻔했다 — 공개본 git 커밋(판 커밋이 공개본 커밋이 된다) ·
@@ -25,11 +27,12 @@ import { effectiveInstanceRoot, releaseLedgerRoot } from '../instance/resolve.js
25
27
  import { getElanousConfigDirOverride } from '../elanous-config-dir.js';
26
28
  import { envLiteral } from '../platform/env-literal.js';
27
29
  import { getUserConfig, userConfigPath } from '../user-config.js';
28
- import { addItem, claimItem, decodeClaimHistoryEntry, devVersion, listChecklist, ownerMatches, parseOwner, parityGap, removeItem, seedFromRoadmap, setItem, summarizeChecklist, type ChecklistStatus, type ChecklistDisposition, type ChecklistKind } from '../release-loop/checklist.js';
30
+ import { addItem, cellsReferencingDoc, claimItem, decodeClaimHistoryEntry, devVersion, listChecklist, ownerMatches, parseOwner, parityGap, removeItem, renderRefsStatus, seedFromRoadmap, setItem, summarizeChecklist, type ChecklistStatus, type ChecklistDisposition, type ChecklistKind } from '../release-loop/checklist.js';
29
31
  import * as features from '../release-loop/feature-store.js';
30
32
  import { evidencePlan, landedButYellow, type MergedChecklistPr } from '../release-loop/landed-but-yellow.js';
31
33
  import { formatSchedule, getSchedule, listSchedules, setSchedule } from '../release-loop/release-schedule.js';
32
34
  import { placeCell, rebalance, seatMove, type PlacementPriority } from '../release-loop/placement.js';
35
+ import { parseRubric, readRubricItems, rubricGrade, rubricScore } from '../release-loop/rubric.js';
33
36
  import { writeStdoutJson } from './stdout-json.js';
34
37
  import { CliUserError } from './cli-user-error.js';
35
38
  import { hqCliWriteAllowed, type HqDeps } from '../hq/hq.js';
@@ -37,8 +40,11 @@ import { fileLeaseStore } from '../hq/lease.js';
37
40
  import { isIsolatedLedgerWriteRoot } from '../hq/ledger-write-target.js';
38
41
  import { runUnattendedRelease, type UnattendedReleaseDeps } from '../../scripts/release-loop/unattended-release.js';
39
42
  import { cutReleaseBranch } from '../../scripts/release-loop/cut-branch.js';
43
+ import { resumeReleaseRun } from '../../scripts/release-loop/resume-release.js';
44
+ import type { PrereleaseKind } from '../../scripts/release-loop/release-version.js';
40
45
  import { releaseReadiness } from '../../scripts/release-loop/release-readiness.js';
41
46
  import { runReleaseIfReady } from './release-run-if-ready.js';
47
+ import { formatPreflight, releasePreflight } from '../../scripts/release-loop/preflight.js';
42
48
  import { autoStartScheduledRelease, type AutoStartDeferral } from '../release-loop/auto-start.js';
43
49
  import { landingFreezeMessage, LandingFrozenError, readLandingFreeze } from '../release-loop/landing-freeze.js';
44
50
 
@@ -151,7 +157,7 @@ export async function prepareRelease(opts: PrepareOptions, run: Runner = default
151
157
  must(run('git', ['add', '-A'], publicDir), 'git add');
152
158
  must(run('git', ['-c', `user.name=${name}`, '-c', `user.email=${email}`, 'commit', '-q', '--allow-empty', '-m', `elanous ${tag}`], publicDir), 'git commit');
153
159
  const publicCommit = must(run('git', ['rev-parse', 'HEAD'], publicDir), 'git rev-parse');
154
- log(`③ 공개 커밋 ${publicCommit.slice(0, 12)} (${publicRepo} main 위)`);
160
+ log(`③ 공개 커밋 ${publicCommit.slice(0, 12)} (${publicRepo} main 위${isPrerelease(opts.version) ? ` · 미리보기 — 공개 main 이 아니라 release/${opts.version} 가지 ⊕ 태그로 나간다` : ''})`);
155
161
 
156
162
  // ④ PWA 빌드 — ⛔ 빠뜨리면 «웹 화면 없는 판»(빌드 산출은 공개본에 안 실린다).
157
163
  symlinkSync(join(modulesRoot, 'node_modules'), join(publicDir, 'node_modules'), 'dir');
@@ -216,12 +222,17 @@ export function planPublish(m: ReleaseManifest, notesFile: string): PublishStep[
216
222
  // 🩸 2026-09-25 v0.1.1 드라이런: release-build 의 `files` 에는 SHA256SUMS 가 없어(체크섬 대상 목록이라) «자산 4» 로 올릴 뻔했다.
217
223
  const assets = [...new Set([...m.files.map((f) => f.name), 'SHA256SUMS'])];
218
224
  return [
219
- { what: `공개 저장소 ${m.publicRepo} main 푸시(${m.publicCommit.slice(0, 12)})`, command: 'git', args: ['push', 'origin', 'HEAD:main'], cwd: m.publicDir },
225
+ // RELEASE-REHEARSAL-RC: a prerelease never moves public main — its export goes to release/<v> plus its tag (one atomic push).
226
+ m.prerelease
227
+ ? { what: `공개 저장소 ${m.publicRepo} release/${m.version} 가지 ⊕ 태그 ${m.tag} 푸시(${m.publicCommit.slice(0, 12)} · main 무접촉)`, command: 'git',
228
+ args: ['push', '--atomic', 'origin', `${m.publicCommit}:refs/heads/release/${m.version}`, `${m.publicCommit}:refs/tags/${m.tag}`], cwd: m.publicDir }
229
+ : { what: `공개 저장소 ${m.publicRepo} main 푸시(${m.publicCommit.slice(0, 12)})`, command: 'git', args: ['push', 'origin', 'HEAD:main'], cwd: m.publicDir },
220
230
  {
221
231
  what: `릴리스 ${m.tag}${m.prerelease ? ' (prerelease)' : ''} 생성 ⊕ 자산 ${assets.length}(${assets.join(' · ')})`,
222
232
  command: 'gh',
223
233
  args: ['release', 'create', m.tag, ...assets.map((name) => join(m.distDir, name)), '--repo', m.publicRepo, '--target', m.publicCommit,
224
- '--title', `elanous ${m.tag}`, '--notes-file', notesFile, ...(m.prerelease ? ['--prerelease'] : [])],
234
+ // RELEASE-REHEARSAL-RC: a prerelease is never Latest — the one-line installer keeps serving the last stable release.
235
+ '--title', `elanous ${m.tag}${m.prerelease ? ' (pre-release)' : ''}`, '--notes-file', notesFile, ...(m.prerelease ? ['--prerelease', '--latest=false', '--verify-tag'] : [])],
225
236
  cwd: m.publicDir,
226
237
  },
227
238
  ];
@@ -660,6 +671,36 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
660
671
  };
661
672
  const withContext = (cmd: Command) => cmd.option('--version <v>', '판 또는 별칭').option('--json', '결과 JSON').option('--hq-override', '본부 임대 거부를 관측하며 수동 우회');
662
673
  const actor = () => process.env.ELANOUS_TRACK || 'cli';
674
+ withContext(checklist.command('rubric').description('루브릭 채점(읽기 전용)'))
675
+ .action(async (_opts: unknown, cmd: Command) => {
676
+ if (cmd.opts().version === undefined && cmd.parent?.opts().version === undefined) throw new CliUserError('조회할 판을 지정하라', '--version <v>');
677
+ const { version, json } = context(cmd);
678
+ const items = readRubricItems(version, releaseLedgerRoot());
679
+ const rows = items.flatMap((item) => {
680
+ const rubric = parseRubric(`${item.title}\n${item.evidence ?? ''}`);
681
+ if (!rubric) return [];
682
+ const score = rubricScore(rubric);
683
+ return [{ id: item.id, score, grade: rubricGrade(score), priority: item.priority ?? null }];
684
+ }).sort((a, b) => b.score - a.score || a.id.localeCompare(b.id));
685
+ const missing = items.length - rows.length;
686
+ if (json) { await writeStdoutJson(`${JSON.stringify({ version, rows, missing })}\n`); return; }
687
+ console.log('id · 점수 · 등급 · 현재 우선순위');
688
+ for (const row of rows) console.log(`${row.id} · ${row.score} · ${row.grade} · ${row.priority ?? '-'}`);
689
+ console.log(`루브릭 없는 칸 ${missing}`);
690
+ });
691
+ withContext(checklist.command('refs <doc>').description('문서를 인용하는 칸 — 모든 판의 구현 현황(읽기 전용)'))
692
+ .action(async (doc: string, _opts: unknown, cmd: Command) => {
693
+ if (!doc.trim() || doc.trim().startsWith('#')) throw new CliUserError(`잘못된 문서: ${doc}`, '저장소 상대 경로[#절] — 예: docs/RFC-x.md#§3');
694
+ const { json } = context(cmd);
695
+ const ledgerRoot = releaseLedgerRoot();
696
+ const rows = cellsReferencingDoc(doc, features.assignedVersions(ledgerRoot).map((version) => listChecklist(version, ledgerRoot)));
697
+ const byStatus = { green: 0, yellow: 0, red: 0, done: 0 } as Record<ChecklistStatus, number>;
698
+ for (const row of rows) byStatus[row.status] += 1;
699
+ debug.log('release.checklist', 'refs-read', { doc, rows: rows.length });
700
+ if (json) { await writeStdoutJson(`${JSON.stringify({ doc, rows, byStatus })}\n`); return; }
701
+ console.log(renderRefsStatus(rows));
702
+ console.log(`칸 ${rows.length} · 🟢 ${byStatus.green} · 🟡 ${byStatus.yellow} · 🔴 ${byStatus.red} · ✅ ${byStatus.done}`);
703
+ });
663
704
  withContext(checklist.command('list').description('모든 칸')).option('--owner <owner>', '자리 또는 하위 자리로 거르기').action(async (opts: { owner?: string }, cmd: Command) => {
664
705
  const { version, codenames, json } = context(cmd);
665
706
  await checklistOutput(version, codenames, 'list', json, opts.owner);
@@ -742,10 +783,11 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
742
783
  .option('--ceo-minutes <minutes>', '대표 손 분량(분)').option('--ceo-date <date>', '대표 손 날짜 YYYY-MM-DD (없으면 판 착지일 KST)')
743
784
  .option('--accelerator', '가속 등급 — 다른 칸의 처리량을 올리는 칸(자율성·효율성·동시성). 생략하면 등급 없음')
744
785
  .option('--allow-duplicate-id', '다른 판에 같은 id 가 있어도 경고 후 추가')
745
- .action((id: string, title: string, opts: { owner?: string; kind?: string; priority?: string; deadlineVersion?: string; predecessor?: string[]; ceoMinutes?: string; ceoDate?: string; accelerator?: boolean; allowDuplicateId?: boolean }, cmd: Command) => {
786
+ .option('--ref <path...>', '근거 문서 — 저장소 상대 경로[#절] (반복 가능)')
787
+ .action((id: string, title: string, opts: { ref?: string[]; owner?: string; kind?: string; priority?: string; deadlineVersion?: string; predecessor?: string[]; ceoMinutes?: string; ceoDate?: string; accelerator?: boolean; allowDuplicateId?: boolean }, cmd: Command) => {
746
788
  const { version, json } = context(cmd);
747
789
  if (!mayWrite(cmd)) return;
748
- const data = addItem(version, { id, title, ...(opts.owner !== undefined ? { owner: opts.owner } : {}), ...(opts.kind !== undefined ? { kind: opts.kind as ChecklistKind } : {}), ...(opts.priority !== undefined ? { priority: opts.priority as PlacementPriority } : {}), ...(opts.deadlineVersion !== undefined ? { deadlineVersion: opts.deadlineVersion } : {}), ...(opts.predecessor !== undefined ? { predecessors: opts.predecessor } : {}), ...(opts.ceoMinutes !== undefined ? { ceoMinutes: ceoMinutes(opts.ceoMinutes) } : {}), ...(opts.ceoDate !== undefined ? { ceoDate: opts.ceoDate } : {}), ...(opts.accelerator ? { accelerator: true } : {}) }, { allowDuplicateId: opts.allowDuplicateId });
790
+ const data = addItem(version, { id, title, ...(opts.ref !== undefined ? { refs: opts.ref } : {}), ...(opts.owner !== undefined ? { owner: opts.owner } : {}), ...(opts.kind !== undefined ? { kind: opts.kind as ChecklistKind } : {}), ...(opts.priority !== undefined ? { priority: opts.priority as PlacementPriority } : {}), ...(opts.deadlineVersion !== undefined ? { deadlineVersion: opts.deadlineVersion } : {}), ...(opts.predecessor !== undefined ? { predecessors: opts.predecessor } : {}), ...(opts.ceoMinutes !== undefined ? { ceoMinutes: ceoMinutes(opts.ceoMinutes) } : {}), ...(opts.ceoDate !== undefined ? { ceoDate: opts.ceoDate } : {}), ...(opts.accelerator ? { accelerator: true } : {}) }, { allowDuplicateId: opts.allowDuplicateId });
749
791
  if (json) console.log(JSON.stringify(data)); else console.log(`✅ ${id} 추가`);
750
792
  });
751
793
  withContext(checklist.command('set <id>').description('칸 상태·근거·담당·처분 갱신'))
@@ -754,13 +796,14 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
754
796
  .option('--priority <priority>', 'P0|P1|P2').option('--deadline-version <v>', '마감 판').option('--predecessor <id...>', '선행 칸 id')
755
797
  .option('--ceo-minutes <minutes>', '대표 손 분량(분)').option('--ceo-date <date>', '대표 손 날짜 YYYY-MM-DD')
756
798
  .option('--accelerator', '가속 등급 부여').option('--no-accelerator', '가속 등급 해제(기본은 없음)')
757
- .action((id: string, opts: { status?: string; evidence?: string; owner?: string; disposition?: string; kind?: string; priority?: string; deadlineVersion?: string; predecessor?: string[]; ceoMinutes?: string; ceoDate?: string; accelerator?: boolean }, cmd: Command) => {
799
+ .option('--ref <path...>', '근거 문서 더하기 — 저장소 상대 경로[#절] (반복 가능 · 중복 제거)')
800
+ .action((id: string, opts: { ref?: string[]; status?: string; evidence?: string; owner?: string; disposition?: string; kind?: string; priority?: string; deadlineVersion?: string; predecessor?: string[]; ceoMinutes?: string; ceoDate?: string; accelerator?: boolean }, cmd: Command) => {
758
801
  const { version, json } = context(cmd);
759
802
  if (opts.status !== undefined && !['green', 'yellow', 'red', 'done'].includes(opts.status)) throw new CliUserError(`잘못된 상태: ${opts.status}`, 'green|yellow|red|done');
760
803
  if (opts.disposition !== undefined && !['move', 'known-issue', 'block'].includes(opts.disposition)) throw new CliUserError(`잘못된 처분: ${opts.disposition}`, 'move|known-issue|block');
761
- if (opts.status === undefined && opts.evidence === undefined && opts.owner === undefined && opts.disposition === undefined && opts.kind === undefined && opts.priority === undefined && opts.deadlineVersion === undefined && opts.predecessor === undefined && opts.ceoMinutes === undefined && opts.ceoDate === undefined && opts.accelerator === undefined) throw new CliUserError('갱신할 칸을 지정하라', '--status · --evidence · --owner · --disposition · --kind · --priority · --deadline-version · --predecessor · --ceo-minutes · --ceo-date · --accelerator · --no-accelerator 중 하나');
804
+ if (opts.status === undefined && opts.evidence === undefined && opts.owner === undefined && opts.disposition === undefined && opts.kind === undefined && opts.priority === undefined && opts.deadlineVersion === undefined && opts.predecessor === undefined && opts.ceoMinutes === undefined && opts.ceoDate === undefined && opts.accelerator === undefined && opts.ref === undefined) throw new CliUserError('갱신할 칸을 지정하라', '--ref · --status · --evidence · --owner · --disposition · --kind · --priority · --deadline-version · --predecessor · --ceo-minutes · --ceo-date · --accelerator · --no-accelerator 중 하나');
762
805
  if (!mayWrite(cmd)) return;
763
- const data = setItem(version, id, { ...(opts.status !== undefined ? { status: opts.status as ChecklistStatus } : {}), ...(opts.evidence !== undefined ? { evidence: opts.evidence } : {}), ...(opts.owner !== undefined ? { owner: opts.owner } : {}), ...(opts.disposition !== undefined ? { disposition: opts.disposition as ChecklistDisposition } : {}), ...(opts.kind !== undefined ? { kind: opts.kind as ChecklistKind } : {}), ...(opts.priority !== undefined ? { priority: opts.priority as PlacementPriority } : {}), ...(opts.deadlineVersion !== undefined ? { deadlineVersion: opts.deadlineVersion } : {}), ...(opts.predecessor !== undefined ? { predecessors: opts.predecessor } : {}), ...(opts.ceoMinutes !== undefined ? { ceoMinutes: ceoMinutes(opts.ceoMinutes) } : {}), ...(opts.ceoDate !== undefined ? { ceoDate: opts.ceoDate } : {}), ...(opts.accelerator !== undefined ? { accelerator: opts.accelerator ? true : null } : {}) }, process.env.ELANOUS_TRACK || 'cli');
806
+ const data = setItem(version, id, { ...(opts.ref !== undefined ? { refs: opts.ref } : {}), ...(opts.status !== undefined ? { status: opts.status as ChecklistStatus } : {}), ...(opts.evidence !== undefined ? { evidence: opts.evidence } : {}), ...(opts.owner !== undefined ? { owner: opts.owner } : {}), ...(opts.disposition !== undefined ? { disposition: opts.disposition as ChecklistDisposition } : {}), ...(opts.kind !== undefined ? { kind: opts.kind as ChecklistKind } : {}), ...(opts.priority !== undefined ? { priority: opts.priority as PlacementPriority } : {}), ...(opts.deadlineVersion !== undefined ? { deadlineVersion: opts.deadlineVersion } : {}), ...(opts.predecessor !== undefined ? { predecessors: opts.predecessor } : {}), ...(opts.ceoMinutes !== undefined ? { ceoMinutes: ceoMinutes(opts.ceoMinutes) } : {}), ...(opts.ceoDate !== undefined ? { ceoDate: opts.ceoDate } : {}), ...(opts.accelerator !== undefined ? { accelerator: opts.accelerator ? true : null } : {}) }, process.env.ELANOUS_TRACK || 'cli');
764
807
  const item = data.items.find((entry) => entry.id === id);
765
808
  const why = item?.kind === 'screen' && item.status === 'green' ? parityGap(item.evidence) : null;
766
809
  if (why) {
@@ -909,7 +952,16 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
909
952
  .action(async (o: { version: string; base: string; pick: string[]; append?: boolean; dryRun?: boolean; json?: boolean }) => {
910
953
  await jsonAction(o.json, async (log) => cutReleaseBranch({ ...o, log }), () => true);
911
954
  });
912
- const runAction = async (o: { version: string; cutCommit?: string; dryRun?: boolean; ifReady?: boolean; forceFreeze?: boolean; json?: boolean }, logRun = console.log): Promise<boolean | AutoStartDeferral | undefined> => {
955
+ const runAction = async (o: { version: string; cutCommit?: string; mainCut?: boolean; prerelease?: string; dryRun?: boolean; ifReady?: boolean; forceFreeze?: boolean; json?: boolean }, logRun = console.log): Promise<boolean | AutoStartDeferral | undefined> => {
956
+ // Only the rc rehearsal channel is operated for now (alpha/beta stay valid version shapes, not run options).
957
+ if (o.prerelease !== undefined && o.prerelease !== 'rc') {
958
+ const message = `--prerelease supports rc only: ${o.prerelease}`;
959
+ if (o.json) await writeStdoutJson(`${JSON.stringify({ ok: false, error: message })}\n`);
960
+ else console.error(`⛔ ${message}`);
961
+ process.exitCode = 1;
962
+ return false;
963
+ }
964
+ const prerelease = o.prerelease as PrereleaseKind | undefined;
913
965
  if (!o.dryRun) {
914
966
  try {
915
967
  const frozen = readLandingFreeze(releaseRunDeps.freezeRoot);
@@ -935,12 +987,13 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
935
987
  return false;
936
988
  }
937
989
  }
938
- if (o.ifReady && !o.dryRun) {
990
+ // A prerelease rehearsal never reads the stable version's readiness (checklist cells): it runs directly.
991
+ if (o.ifReady && !o.dryRun && !prerelease) {
939
992
  try {
940
993
  const outcome = await runReleaseIfReady(o.version, {
941
994
  ledgerRoot: releaseRunDeps.ledgerRoot,
942
995
  readiness: (version, options) => releaseReadiness(version, { ledgerRoot: releaseRunDeps.ledgerRoot, checklist: releaseRunDeps.checklist, ...options }),
943
- run: () => runUnattendedRelease({ version: o.version, cutCommit: o.cutCommit, forceFreeze: o.forceFreeze }, releaseRunDeps),
996
+ run: () => runUnattendedRelease({ version: o.version, cutCommit: o.cutCommit, mainCut: o.mainCut, prerelease, forceFreeze: o.forceFreeze }, releaseRunDeps),
944
997
  });
945
998
  if (outcome.skipped) {
946
999
  if (o.json) await writeStdoutJson(`${JSON.stringify(outcome)}\n`);
@@ -968,7 +1021,7 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
968
1021
  }
969
1022
  }
970
1023
  await jsonAction(o.json, async (log) => {
971
- const result = await runUnattendedRelease({ version: o.version, dryRun: o.dryRun, cutCommit: o.cutCommit, forceFreeze: o.forceFreeze }, releaseRunDeps);
1024
+ const result = await runUnattendedRelease({ version: o.version, dryRun: o.dryRun, cutCommit: o.cutCommit, mainCut: o.mainCut, prerelease, forceFreeze: o.forceFreeze }, releaseRunDeps);
972
1025
  log(`${result.dryRun ? '· 드라이런' : '▶ 릴리스 루프'} ${result.input.version} · 입력 ${JSON.stringify(result.input)}`);
973
1026
  return result;
974
1027
  }, (r) => r.dryRun || r.state?.status === 'done' || r.state?.status === 'awaiting-approval');
@@ -976,12 +1029,43 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
976
1029
  release.command('run')
977
1030
  .description('릴리스 루프 실행 — 원장 직전 판과 release.loop 설정으로 그래프 입력 구성 · --dry-run 은 입력만 출력')
978
1031
  .requiredOption('--version <v>', '공개할 판(x.y.z)')
979
- .option('--cut-commit <sha>', 'origin/main HEAD 대신 컷으로 쓸 커밋')
1032
+ .option('--cut-commit <sha>', 'origin/main HEAD 대신 컷으로 쓸 커밋(이미 자른 release/<v> 끝 — 옛 경로)')
1033
+ .option('--main-cut', '옛 경로: 판 올림을 main 에 착지(기본은 release/<v> 가지에만 올린다)')
1034
+ .option('--prerelease <kind>', '미리보기 판(rc) — <v>-rc.<다음 빈 번호> · npm next · GitHub pre-release · docs-land/ops-upgrade/dev-bump 건너뜀')
980
1035
  .option('--dry-run', '체크리스트·그래프 실행 없이 전체 입력 보기')
981
1036
  .option('--if-ready', '준비되지 않았으면 사유를 출력하고 성공으로 건너뛴다')
982
- .option('--force-freeze', '동결 중 게이트·발행 강행(관측 기록)')
1037
+ .option('--force-freeze', '동결 중 게이트·발행 강행(관측 기록) — 가지 컷은 동결이 필요 없다 · 동결은 비상 스위치로 남는다')
983
1038
  .option('--json', '결과 한 줄 JSON(stdout) · 사람 줄은 stderr')
984
- .action(async (o: { version: string; cutCommit?: string; dryRun?: boolean; ifReady?: boolean; forceFreeze?: boolean; json?: boolean }) => { await runAction(o); });
1039
+ .action(async (o: { version: string; cutCommit?: string; mainCut?: boolean; prerelease?: string; dryRun?: boolean; ifReady?: boolean; forceFreeze?: boolean; json?: boolean }) => { await runAction(o); });
1040
+ release.command('preflight')
1041
+ .description('컷 30분 전 사전 점검(읽기 전용) — 체크리스트 게이트 모의 · 발행 뒤 칸 · run --dry-run 입력 · 동결 · 일정/게이트 실측. ⛔ 있으면 exit 1')
1042
+ .requiredOption('--version <v>', '점검할 판(x.y.z)')
1043
+ .option('--publish-at <iso>', '계획 발행 시각(기본: 컷 뒤 첫 07·18시 KST)')
1044
+ .option('--json', '결과 한 줄 JSON(stdout)')
1045
+ .action(async (o: { version: string; publishAt?: string; json?: boolean }) => {
1046
+ try {
1047
+ const result = await releasePreflight(o.version, { publishAt: o.publishAt, releaseRunDeps });
1048
+ if (o.json) await writeStdoutJson(`${JSON.stringify(result)}\n`);
1049
+ else for (const line of formatPreflight(result)) console.log(line);
1050
+ if (result.blockers > 0) process.exitCode = 1;
1051
+ } catch (e) {
1052
+ if (o.json) await writeStdoutJson(`${JSON.stringify({ ok: false, error: (e as Error).message })}\n`);
1053
+ else console.error(`⛔ ${(e as Error).message}`);
1054
+ process.exitCode = 1;
1055
+ }
1056
+ });
1057
+ release.command('resume')
1058
+ .description('실패한 릴리스 런을 이어 달린다 — cut-branch --append 로 고친 release/<v> 새 끝을 version-release 기록에 반영(빨리감기만·상태 백업) 뒤 --from 노드부터')
1059
+ .requiredOption('--run <runId>', '실패한 release-loop 런 ID')
1060
+ .requiredOption('--from <node>', '다시 시작할 노드(예: gate)')
1061
+ .option('--json', '결과 한 줄 JSON(stdout)')
1062
+ .action(async (o: { run: string; from: string; json?: boolean }) => {
1063
+ await jsonAction(o.json, async (log) => {
1064
+ const result = await resumeReleaseRun({ runId: o.run, from: o.from });
1065
+ log(`${result.tip.changed ? `↻ ${result.tip.branch} ${result.tip.from.slice(0, 12)} → ${result.tip.to.slice(0, 12)} (백업 ${result.tip.backup})` : `· ${result.tip.branch} 끝 그대로 ${result.tip.to.slice(0, 12)}`} · ${o.from} 부터 → ${result.state.status}`);
1066
+ return { tip: result.tip, status: result.state.status, runId: result.state.runId };
1067
+ }, (r) => r.status === 'done' || r.status === 'awaiting-approval');
1068
+ });
985
1069
  release.command('auto-start')
986
1070
  .description('판 일정의 컷 창에서 릴리스 시작을 미리 본다 · --apply 로 release run --if-ready 실행')
987
1071
  .option('--window-minutes <n>', '컷 뒤 선택 창(분)', '15')
@@ -0,0 +1,46 @@
1
+ export type EdgeRailDecision = 'hold' | 'canary-install' | 'promote-all' | 'rollback' | 'wait-quiet';
2
+ export interface EdgeRailPlan { decision: EdgeRailDecision; reason: string }
3
+ export interface EdgeRailInput {
4
+ now: Date;
5
+ mainCommit: string;
6
+ installed: string;
7
+ seat: string;
8
+ canary: { seat: string; okRuns: number; startedAt: string } | null;
9
+ failureRate: { before: number; after: number; samples: number } | null;
10
+ quietWindow: boolean;
11
+ canaryOkRuns?: number;
12
+ failureMultiplier?: number;
13
+ minSamples?: number;
14
+ promoted?: boolean;
15
+ }
16
+
17
+ /** No I/O: unknown health, insufficient samples and unknown revision never trigger a cutover. */
18
+ export function planEdgeRail(input: EdgeRailInput): EdgeRailPlan {
19
+ const { mainCommit, installed, seat, canary, failureRate, quietWindow } = input;
20
+ const okRuns = input.canaryOkRuns ?? 3;
21
+ const multiplier = input.failureMultiplier ?? 2;
22
+ const minSamples = input.minSamples ?? 30;
23
+ if (!Number.isFinite(input.now.getTime()) || !/^[a-f0-9]{12,40}$/i.test(mainCommit) || !/^[a-f0-9]{12,40}$/i.test(installed)
24
+ || !Number.isSafeInteger(okRuns) || okRuns < 1 || !Number.isFinite(multiplier) || multiplier <= 1
25
+ || !Number.isSafeInteger(minSamples) || minSamples < 1) return { decision: 'hold', reason: 'invalid or unknown rollout input' };
26
+ const same = mainCommit.startsWith(installed) || installed.startsWith(mainCommit);
27
+ if (canary && Date.parse(canary.startedAt) > input.now.getTime()) return { decision: 'hold', reason: 'canary started in the future' };
28
+ if (canary && (canary.seat !== 'OP' || !Number.isSafeInteger(canary.okRuns) || canary.okRuns < 0
29
+ || !Number.isFinite(Date.parse(canary.startedAt)))) return { decision: 'hold', reason: 'invalid canary state' };
30
+ if (failureRate) {
31
+ if (![failureRate.before, failureRate.after, failureRate.samples].every(Number.isFinite)
32
+ || failureRate.before < 0 || failureRate.after < 0 || failureRate.before > 1 || failureRate.after > 1
33
+ || !Number.isSafeInteger(failureRate.samples) || failureRate.samples < 0) return { decision: 'hold', reason: 'invalid failure-rate measurement' };
34
+ if (failureRate.samples < minSamples) return { decision: 'hold', reason: `failure-rate samples ${failureRate.samples} < ${minSamples}` };
35
+ if (same && canary && failureRate.after > 0 && failureRate.after >= failureRate.before * multiplier) return quietWindow
36
+ ? { decision: 'rollback', reason: `failure rate ${failureRate.after} >= ${multiplier}x ${failureRate.before}` }
37
+ : { decision: 'wait-quiet', reason: 'rollback requires quiet window for resident restart' };
38
+ }
39
+ if (canary && canary.okRuns < okRuns) return { decision: 'hold', reason: `OP canary ${canary.okRuns}/${okRuns} successful runs` };
40
+ if (same) return { decision: 'hold', reason: 'installed commit already matches main' };
41
+ if (input.promoted && canary && seat !== 'OP') return { decision: 'hold', reason: 'fleet promotion already completed' };
42
+ if (!canary && seat !== 'OP') return { decision: 'hold', reason: 'OP canary has not installed' };
43
+ if (!quietWindow) return { decision: 'wait-quiet', reason: 'resident restart requires quiet window' };
44
+ if (!canary) return { decision: 'canary-install', reason: 'install OP canary first' };
45
+ return { decision: 'promote-all', reason: `OP canary passed ${canary.okRuns}/${okRuns} successful runs` };
46
+ }