elanous 0.2.15 → 0.2.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (254) hide show
  1. package/apps/pwa/out/404/index.html +1 -1
  2. package/apps/pwa/out/404.html +1 -1
  3. package/apps/pwa/out/_next/static/chunks/{1307-33ffcc49ab45123b.js → 1307-b46d45b705e8c9ae.js} +1 -1
  4. package/apps/pwa/out/_next/static/chunks/{9103-7a3ae7f60af3b99f.js → 9103-60fbef33e66f0a46.js} +1 -1
  5. package/apps/pwa/out/_next/static/chunks/app/approvals/page-79c3ed84baaedd03.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-e05a77df7e005b04.js → page-3cb52e5ea8fc2fa5.js} +1 -1
  7. package/apps/pwa/out/_next/static/chunks/app/board/page-a424ef80885d1743.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/app/botlab/{page-5491c115b1f9a923.js → page-c1e20d7d338713f1.js} +1 -1
  9. package/apps/pwa/out/_next/static/chunks/app/bots/{page-b8c49f6b4fa2df91.js → page-482c5dbde699947c.js} +1 -1
  10. package/apps/pwa/out/_next/static/chunks/app/ceo/page-1e962b31d1f11081.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/app/chat/{page-976813c91d7394ea.js → page-8c2d9526164f5d09.js} +1 -1
  12. package/apps/pwa/out/_next/static/chunks/app/consult/page-ffff6d819fbc6e0f.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/app/control/page-a0e2a6d1bfb52fc5.js +1 -0
  14. package/apps/pwa/out/_next/static/chunks/app/design-check/{page-72d3e7ff35a8c954.js → page-c53598cfce6156f2.js} +1 -1
  15. package/apps/pwa/out/_next/static/chunks/app/editor/{page-60c7a8e1a4fe62ce.js → page-13a63953f53c6a23.js} +1 -1
  16. package/apps/pwa/out/_next/static/chunks/app/exec/{page-2a52b316cb143d9c.js → page-5d3c8429d04f3831.js} +1 -1
  17. package/apps/pwa/out/_next/static/chunks/app/field/{page-c3ce483c8d5db370.js → page-3193bdc0bbab047a.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/inside/page-76270efaee638f17.js +1 -0
  19. package/apps/pwa/out/_next/static/chunks/app/intake/{page-96f074f6538e8a2c.js → page-6107ca8ddd5e3bdd.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/{layout-e97be5334d83a9f5.js → layout-dce686e582c23937.js} +1 -1
  21. package/apps/pwa/out/_next/static/chunks/app/live/page-d3d53b146416e422.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/app/loops/page-1a61c7ca1ceb3ae7.js +1 -0
  23. package/apps/pwa/out/_next/static/chunks/app/market/{page-955ec2da3be4c560.js → page-51f811fa119308b6.js} +1 -1
  24. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-9f88fbc15d9058d1.js → page-dacff46049e1557e.js} +1 -1
  25. package/apps/pwa/out/_next/static/chunks/app/morning/{page-c1343f9499d86f3f.js → page-4167348049166852.js} +1 -1
  26. package/apps/pwa/out/_next/static/chunks/app/{not-found-8739a2e0ae793130.js → not-found-de52e8fdf9095f7a.js} +1 -1
  27. package/apps/pwa/out/_next/static/chunks/app/observatory/{page-c9fc5706b8abaa11.js → page-36a7d994f4ca0299.js} +1 -1
  28. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-d23b12bb92242558.js → page-ca3952cd78a83ca2.js} +1 -1
  29. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-5b042736aa113756.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/app/ops/seats/{page-c7fea475488201a8.js → page-acb749f1e432bcf3.js} +1 -1
  31. package/apps/pwa/out/_next/static/chunks/app/outputs/page-cd78862707999911.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/app/page-54b9ef8afe6278fd.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/app/reflection/{page-5efbf81124c474ae.js → page-1ebfe049c640e171.js} +1 -1
  34. package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-85f1a1fc0a82738c.js → page-57924393e3e4e539.js} +1 -1
  35. package/apps/pwa/out/_next/static/chunks/app/sessions/page-9e0ce6c8129fa641.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-06ee266953d9c879.js → page-31fa1ce4c4aff9ae.js} +1 -1
  37. package/apps/pwa/out/_next/static/chunks/app/settings/{page-153c2b3bcc89e8de.js → page-0f5cd57ca928f42b.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-abe8e3b74bea1ffc.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/app/setup/page-4364ef9f4e7c7f0f.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/app/share/{page-0d6d23b40263a7ea.js → page-d28573d6792b4275.js} +1 -1
  41. package/apps/pwa/out/_next/static/chunks/app/showroom/page-59f03fda76c03d22.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/app/tasks/page-d751de5f8062ff5e.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/app/term/{page-f5597e894c02cd33.js → page-fe1759f94003b32b.js} +1 -1
  44. package/apps/pwa/out/_next/static/chunks/app/today/{page-0337fdf7fb5e37b6.js → page-d64c984e855c8c68.js} +1 -1
  45. package/apps/pwa/out/_next/static/chunks/app/trace/page-86914b0dee8ab7f8.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/app/vault/{page-8fcec4c9ea2e7f70.js → page-9afdee66b820c76e.js} +1 -1
  47. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/{page-f7e631e9b4daf66e.js → page-d4aef67abf264874.js} +1 -1
  48. package/apps/pwa/out/_next/static/chunks/app/workflows/page-1ea64acadd704ace.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-9817c7f9007466cc.js → page-3544f154c87b43ee.js} +1 -1
  50. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-30c2073bfc21c004.js +1 -0
  51. package/apps/pwa/out/_next/static/css/001d5fbaee388a25.css +1 -0
  52. package/apps/pwa/out/_next/static/css/{04dabcbcc4bfa6db.css → 085673d826b98b62.css} +1 -1
  53. package/apps/pwa/out/approvals/index.html +1 -1
  54. package/apps/pwa/out/approvals/index.txt +15 -15
  55. package/apps/pwa/out/autopilot/index.html +1 -1
  56. package/apps/pwa/out/autopilot/index.txt +15 -15
  57. package/apps/pwa/out/board/index.html +1 -1
  58. package/apps/pwa/out/board/index.txt +15 -15
  59. package/apps/pwa/out/botlab/index.html +1 -1
  60. package/apps/pwa/out/botlab/index.txt +15 -15
  61. package/apps/pwa/out/bots/index.html +1 -1
  62. package/apps/pwa/out/bots/index.txt +15 -15
  63. package/apps/pwa/out/ceo/index.html +1 -1
  64. package/apps/pwa/out/ceo/index.txt +15 -15
  65. package/apps/pwa/out/chat/index.html +1 -1
  66. package/apps/pwa/out/chat/index.txt +15 -15
  67. package/apps/pwa/out/consult/index.html +1 -1
  68. package/apps/pwa/out/consult/index.txt +15 -15
  69. package/apps/pwa/out/control/index.html +1 -1
  70. package/apps/pwa/out/control/index.txt +15 -15
  71. package/apps/pwa/out/design-check/index.html +1 -1
  72. package/apps/pwa/out/design-check/index.txt +15 -15
  73. package/apps/pwa/out/editor/index.html +1 -1
  74. package/apps/pwa/out/editor/index.txt +15 -15
  75. package/apps/pwa/out/exec/index.html +1 -1
  76. package/apps/pwa/out/exec/index.txt +15 -15
  77. package/apps/pwa/out/field/index.html +1 -1
  78. package/apps/pwa/out/field/index.txt +15 -15
  79. package/apps/pwa/out/healthz +1 -1
  80. package/apps/pwa/out/index.html +1 -1
  81. package/apps/pwa/out/index.txt +15 -15
  82. package/apps/pwa/out/inside/index.html +1 -1
  83. package/apps/pwa/out/inside/index.txt +15 -15
  84. package/apps/pwa/out/intake/index.html +1 -1
  85. package/apps/pwa/out/intake/index.txt +15 -15
  86. package/apps/pwa/out/live/index.html +1 -1
  87. package/apps/pwa/out/live/index.txt +15 -15
  88. package/apps/pwa/out/loops/index.html +1 -1
  89. package/apps/pwa/out/loops/index.txt +15 -15
  90. package/apps/pwa/out/market/index.html +1 -1
  91. package/apps/pwa/out/market/index.txt +15 -15
  92. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  93. package/apps/pwa/out/missions/placeholder/index.txt +15 -15
  94. package/apps/pwa/out/morning/index.html +1 -1
  95. package/apps/pwa/out/morning/index.txt +15 -15
  96. package/apps/pwa/out/observatory/index.html +1 -1
  97. package/apps/pwa/out/observatory/index.txt +15 -15
  98. package/apps/pwa/out/ops/checklist/index.html +1 -1
  99. package/apps/pwa/out/ops/checklist/index.txt +15 -15
  100. package/apps/pwa/out/ops/release/index.html +1 -1
  101. package/apps/pwa/out/ops/release/index.txt +15 -15
  102. package/apps/pwa/out/ops/seats/index.html +1 -1
  103. package/apps/pwa/out/ops/seats/index.txt +15 -15
  104. package/apps/pwa/out/outputs/index.html +1 -1
  105. package/apps/pwa/out/outputs/index.txt +15 -15
  106. package/apps/pwa/out/reflection/index.html +1 -1
  107. package/apps/pwa/out/reflection/index.txt +16 -16
  108. package/apps/pwa/out/scheduler/index.html +1 -1
  109. package/apps/pwa/out/scheduler/index.txt +15 -15
  110. package/apps/pwa/out/sessions/index.html +1 -1
  111. package/apps/pwa/out/sessions/index.txt +15 -15
  112. package/apps/pwa/out/settings/devices/index.html +1 -1
  113. package/apps/pwa/out/settings/devices/index.txt +15 -15
  114. package/apps/pwa/out/settings/index.html +1 -1
  115. package/apps/pwa/out/settings/index.txt +15 -15
  116. package/apps/pwa/out/setup/done/index.html +1 -1
  117. package/apps/pwa/out/setup/done/index.txt +15 -15
  118. package/apps/pwa/out/setup/index.html +1 -1
  119. package/apps/pwa/out/setup/index.txt +15 -15
  120. package/apps/pwa/out/share/index.html +1 -1
  121. package/apps/pwa/out/share/index.txt +15 -15
  122. package/apps/pwa/out/showroom/index.html +1 -1
  123. package/apps/pwa/out/showroom/index.txt +15 -15
  124. package/apps/pwa/out/tasks/index.html +1 -1
  125. package/apps/pwa/out/tasks/index.txt +15 -15
  126. package/apps/pwa/out/term/index.html +1 -1
  127. package/apps/pwa/out/term/index.txt +15 -15
  128. package/apps/pwa/out/today/index.html +1 -1
  129. package/apps/pwa/out/today/index.txt +15 -15
  130. package/apps/pwa/out/trace/index.html +1 -1
  131. package/apps/pwa/out/trace/index.txt +15 -15
  132. package/apps/pwa/out/vault/index.html +1 -1
  133. package/apps/pwa/out/vault/index.txt +15 -15
  134. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  135. package/apps/pwa/out/workflows/chat-ui/index.txt +15 -15
  136. package/apps/pwa/out/workflows/index.html +1 -1
  137. package/apps/pwa/out/workflows/index.txt +15 -15
  138. package/apps/pwa/out/workspace/index.html +1 -1
  139. package/apps/pwa/out/workspace/index.txt +15 -15
  140. package/apps/pwa/out/worktrees/index.html +1 -1
  141. package/apps/pwa/out/worktrees/index.txt +15 -15
  142. package/graphs/doc-gen/doc-gen.yaml +24 -0
  143. package/graphs/doc-gen/recipes.yaml +3 -0
  144. package/graphs/orchestrator/orchestrator.yaml +7 -0
  145. package/graphs/orchestrator/recipes.yaml +3 -0
  146. package/graphs/steward/recipes.yaml +2 -1
  147. package/package.json +1 -1
  148. package/scripts/ci-public-export-import-gate.ts +154 -0
  149. package/scripts/ci-test-interference-gate.ts +5 -1
  150. package/scripts/ci-typecheck-changed.ts +34 -15
  151. package/scripts/docs/doc-rot.ts +164 -0
  152. package/scripts/docs/generate-graph-anatomy.ts +131 -9
  153. package/scripts/docs-cli-check.ts +4 -4
  154. package/scripts/goals/goal-census.ts +343 -0
  155. package/scripts/measure/author-section-contribution.ts +84 -0
  156. package/scripts/release-loop/cut-branch.ts +43 -13
  157. package/scripts/release-loop/gate-node.ts +3 -1
  158. package/scripts/release-loop/vault-note-node.ts +19 -2
  159. package/scripts/rhythm/monthly.ts +97 -0
  160. package/src/acp/client.ts +17 -2
  161. package/src/agent-mission/driver.ts +51 -1
  162. package/src/agent-mission/supervisor-metrics-cli.ts +40 -0
  163. package/src/agent-mission/supervisor-metrics.ts +218 -0
  164. package/src/autopilot/track-agent-shadow.ts +2 -1
  165. package/src/briefing/brief-items.ts +191 -36
  166. package/src/budget/usage-outcomes.ts +204 -0
  167. package/src/cli/brief-cli.ts +73 -0
  168. package/src/cli/know-cli.ts +20 -0
  169. package/src/cli/logs-cli.ts +43 -1
  170. package/src/cli/pr-cli.ts +80 -12
  171. package/src/cli/release-cli.ts +11 -8
  172. package/src/cli/self-update.ts +200 -1
  173. package/src/cli/usage-cli.ts +26 -0
  174. package/src/discord-self-message.ts +9 -0
  175. package/src/flow-loop/split.ts +24 -0
  176. package/src/harness/harness-cli-command.ts +36 -2
  177. package/src/harness/harness-pod-dispatch.ts +25 -55
  178. package/src/harness/harness-seams.ts +13 -3
  179. package/src/harness/helper-repair.ts +139 -8
  180. package/src/harness/helper-scan.ts +13 -2
  181. package/src/harness/nested-elanous-policy.ts +114 -0
  182. package/src/hq/hq.ts +8 -2
  183. package/src/hq/lease.ts +5 -3
  184. package/src/hq/transfer.ts +185 -0
  185. package/src/index.ts +112 -6
  186. package/src/intake-plane/digest.ts +48 -26
  187. package/src/knowledge/know-find.ts +105 -0
  188. package/src/llm.ts +75 -19
  189. package/src/loops/orchestrator/finish-rate.ts +14 -3
  190. package/src/loops/orchestrator/tick.ts +96 -25
  191. package/src/maturity/bot-slash-catalog.ts +11 -2
  192. package/src/maturity/feature-maturity.ts +2 -2
  193. package/src/maturity/tui-slash-availability.ts +9 -0
  194. package/src/oauth/overload-failover-launch.ts +75 -0
  195. package/src/oauth/overload-failover.ts +137 -0
  196. package/src/release-loop/checklist.ts +10 -5
  197. package/src/release-loop/feature-store.ts +10 -9
  198. package/src/release-loop/placement.ts +17 -8
  199. package/src/seat-dispatch/seat-ask.ts +33 -6
  200. package/src/seat-loop/seat-loop.ts +90 -3
  201. package/src/self-dev/ask-launch-flow.ts +22 -1
  202. package/src/self-dev/authored-goal-trace.ts +64 -0
  203. package/src/self-dev/dev-cli.ts +19 -1
  204. package/src/self-dev/dev-pipeline.ts +6 -5
  205. package/src/self-dev/entrance-registry.ts +10 -6
  206. package/src/self-dev/old-door.ts +73 -0
  207. package/src/self-implement/app-api-budget.ts +166 -0
  208. package/src/self-implement/budget-gate.ts +7 -5
  209. package/src/self-implement/follow-up-goals.ts +289 -0
  210. package/src/self-implement/goal-author.ts +19 -13
  211. package/src/self-implement/heal-triage.ts +44 -0
  212. package/src/self-implement/orchestrator.ts +138 -7
  213. package/src/self-implement/run-ledger.ts +4 -0
  214. package/src/self-implement/seams.ts +44 -15
  215. package/src/session-runtime/index.ts +2 -0
  216. package/src/session-runtime/retry-api.ts +87 -0
  217. package/src/session-runtime/retry-policy.ts +107 -6
  218. package/src/steward/working-backwards.ts +16 -1
  219. package/src/task-cards/card-cli.ts +25 -0
  220. package/src/task-cards/card-store.ts +5 -3
  221. package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +4 -3
  222. package/src/task-orchestrator/surfaces/pod-ledger-prod-sink.ts +75 -0
  223. package/src/task-orchestrator/surfaces/pod-pool.ts +14 -5
  224. package/src/task-orchestrator/surfaces/pod-source-receive.ts +37 -1
  225. package/src/task-orchestrator/surfaces/self-implement-pod.ts +19 -10
  226. package/src/task-orchestrator/surfaces/self-implement.ts +2 -0
  227. package/src/telegram-commands.ts +5 -0
  228. package/src/telegram.ts +47 -0
  229. package/src/user-config.ts +36 -3
  230. package/src/version/packed-revision.json +1 -1
  231. package/apps/pwa/out/_next/static/chunks/app/approvals/page-0e2c57fe6add4b2a.js +0 -1
  232. package/apps/pwa/out/_next/static/chunks/app/board/page-f544338193a9a762.js +0 -1
  233. package/apps/pwa/out/_next/static/chunks/app/ceo/page-96398e364fbd4548.js +0 -1
  234. package/apps/pwa/out/_next/static/chunks/app/consult/page-28b9448091776c06.js +0 -1
  235. package/apps/pwa/out/_next/static/chunks/app/control/page-1d4901eb7c2d8822.js +0 -1
  236. package/apps/pwa/out/_next/static/chunks/app/inside/page-13c7b53e0c2519a9.js +0 -1
  237. package/apps/pwa/out/_next/static/chunks/app/live/page-65e484302921a932.js +0 -1
  238. package/apps/pwa/out/_next/static/chunks/app/loops/page-34d08facaf08f556.js +0 -1
  239. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-8d47f45a0dc044ac.js +0 -1
  240. package/apps/pwa/out/_next/static/chunks/app/outputs/page-e2ee2896811e98d0.js +0 -1
  241. package/apps/pwa/out/_next/static/chunks/app/page-83b19dae8619de05.js +0 -1
  242. package/apps/pwa/out/_next/static/chunks/app/sessions/page-da6c08aae0f92768.js +0 -1
  243. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-87a11e6a05f15199.js +0 -1
  244. package/apps/pwa/out/_next/static/chunks/app/setup/page-7d4182964407c09c.js +0 -1
  245. package/apps/pwa/out/_next/static/chunks/app/showroom/page-7e8c39241f8eb6d0.js +0 -1
  246. package/apps/pwa/out/_next/static/chunks/app/tasks/page-008c1a96618a4887.js +0 -1
  247. package/apps/pwa/out/_next/static/chunks/app/trace/page-73735b64bfe1ae67.js +0 -1
  248. package/apps/pwa/out/_next/static/chunks/app/workflows/page-4881b8f9f63694b4.js +0 -1
  249. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-97f9f43172a3a90e.js +0 -1
  250. package/apps/pwa/out/_next/static/css/1f7c958c41ffa3b9.css +0 -1
  251. package/src/launch-head/seat-goal-queue-lock.fixture.ts +0 -15
  252. package/src/launch-head/seat-goal-queue.ts +0 -198
  253. /package/apps/pwa/out/_next/static/{LZcstaO9_NXypPFmujolv → rv9cierV0jSOvDBm1g0XB}/_buildManifest.js +0 -0
  254. /package/apps/pwa/out/_next/static/{LZcstaO9_NXypPFmujolv → rv9cierV0jSOvDBm1g0XB}/_ssgManifest.js +0 -0
@@ -715,7 +715,7 @@ export function podObservationGapNotice(
715
715
  .some((prefix) => category.startsWith(prefix));
716
716
  const categories = [...(query.categories ?? []), ...(query.exactCategories ?? [])];
717
717
  if (!categories.some(childCategory) && !(categories.length === 0 && (query.events?.length ?? 0) > 0)) return null;
718
- return `안내: 이 창에 Pod 에서 돈 런 ${podRuns}개 — Pod 자식의 관측은 이 logs.db 에 수집되지 않는다(POD-OBS). 원장으로 본다: elanous self run-ledger <runId>`;
718
+ return `안내: 이 창에 Pod 에서 돈 런 ${podRuns}개 — Pod 자식의 관측은 «발사한 트리의 파생 우주» logs.db 로 회수된다(POD-OBS) — 전체: elanous logs --all --include-test · 런 하나: elanous self run-ledger <runId>`;
719
719
  }
720
720
 
721
721
  /** Select ledger roots independently of DB existence; --all includes registered roots with no logs.db. */
@@ -1038,6 +1038,24 @@ export function otherInstanceHint(matches: readonly { name: string; count: numbe
1038
1038
  return ` ↳ 다른 인스턴스에는 있다 — ${found}\n 전체를 보려면: elanous logs --all --include-test${filters ? ` ${filters}` : ''}`;
1039
1039
  }
1040
1040
 
1041
+ /** POD-OBS(10-06): 결과가 «0이 아닌데 적을 때»도 조용하지 않게 — 다른 우주에 더 많으면 경고한다.
1042
+ * 실측: 24h `pre-pr-sync` 운영 1건 vs 발사 트리 파생 우주 276건(Pod 자식 재방출은 부모 하니스의 우주에 쌓인다). */
1043
+ export function otherInstanceMajorityWarning(
1044
+ shown: number,
1045
+ matches: readonly { name: string; count: number }[],
1046
+ opts: LogsCliOpts,
1047
+ ): string | null {
1048
+ const elsewhere = matches.reduce((sum, match) => sum + match.count, 0);
1049
+ if (shown <= 0 || elsewhere <= shown) return null;
1050
+ const top = [...matches].sort((a, b) => b.count - a.count);
1051
+ const named = top.slice(0, 3).map((match) => `${match.name} ${match.count}건`).join(' · ');
1052
+ const more = top.length > 3 ? ` 외 ${top.length - 3}곳` : '';
1053
+ const filters = requeryFilterArgs(opts);
1054
+ return `⚠ 여기 ${shown}건 · 다른 우주에 ${elsewhere}건이 더 있다 — ${named}${more}\n`
1055
+ + ' (Pod 자식 관측은 발사한 트리의 파생 우주에 쌓인다 · 이 수만 보고 «적다»로 읽지 말 것)\n'
1056
+ + ` 전체를 보려면: elanous logs --all --include-test${filters ? ` ${filters}` : ''}`;
1057
+ }
1058
+
1041
1059
  export function buildQuery(opts: LogsCliOpts): { query: LogQuery; error?: string } {
1042
1060
  const q: LogQuery = {};
1043
1061
  if (opts.level) {
@@ -1789,6 +1807,30 @@ export async function runLogsCli(opts: LogsCliOpts, deps: RunLogsCliDeps = {}):
1789
1807
  }
1790
1808
  const coverageHint = runCoverageHint(out.map(({ row }) => row));
1791
1809
  if (coverageHint) console.error(coverageHint);
1810
+ // 좁힌 조회(이벤트·카테고리)만 — 넓은 조회까지 매번 전 우주를 열지 않는다.
1811
+ if (out.length > 0 && ((query.events?.length ?? 0) > 0 || (query.categories?.length ?? 0) > 0
1812
+ || (query.exactCategories?.length ?? 0) > 0)) {
1813
+ const matches = probeOtherInstanceMatches(opts, query, resolved.targets.map((target) => target.dbPath));
1814
+ // 표시 상한(--limit)이 아니라 «여기» 실제 일치 수와 비교한다(상한 100 을 «100건»으로 말하지 않는다).
1815
+ let here: number | null = null;
1816
+ try {
1817
+ here = Math.max(out.length, (deps.countMatching ?? ((candidate: LogQuery) => opened.reduce(
1818
+ (count, { store }) => count + store.countMatching(candidate), 0)))(query));
1819
+ } catch (error) {
1820
+ // 셈을 못 했으면 경고하지 않는다 — 잘린 표시 수를 «여기 N건»으로 말하지 않는다.
1821
+ debug.log('logs.cli', 'other-instance-count-failed', { shown: out.length, error: error instanceof Error ? error.message : String(error) });
1822
+ }
1823
+ const majority = here === null ? null : otherInstanceMajorityWarning(here, matches, opts);
1824
+ if (majority) {
1825
+ console.error(majority);
1826
+ debug.log('logs.cli', 'other-instance-majority', {
1827
+ shown: out.length,
1828
+ here,
1829
+ elsewhere: matches.reduce((sum, match) => sum + match.count, 0),
1830
+ instances: matches.length,
1831
+ });
1832
+ }
1833
+ }
1792
1834
  if (out.length === 0) {
1793
1835
  console.error('(일치하는 로그 없음)');
1794
1836
  const instanceHint = otherInstanceHint(
package/src/cli/pr-cli.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { existsSync, readFileSync } from 'node:fs';
1
+ import { existsSync, readFileSync, statSync } from 'node:fs';
2
2
  import { basename, join, relative, resolve } from 'node:path';
3
3
  import { createInterface } from 'node:readline';
4
4
  import type { Command } from 'commander';
@@ -19,6 +19,7 @@ import { runModelHardcodeGate } from '../../scripts/ci-model-hardcode-gate.js';
19
19
  import { runDaemonPortGate } from '../../scripts/ci-daemon-port-gate.js';
20
20
  import { runPublicLeakGate } from '../../scripts/ci-public-leak-gate.js';
21
21
  import { exportLeakCheck, publicExportChangedFiles, type ExportLeakCheck } from '../../scripts/ci-public-export-leak-gate.js';
22
+ import { exportImportCheck, type ExportImportCheck } from '../../scripts/ci-public-export-import-gate.js';
22
23
  import { checkCommands, extractElanousCommands, type Finding as DocsCliFinding } from '../../scripts/docs-cli-check.js';
23
24
  import { runTestInterferenceGate } from '../../scripts/ci-test-interference-gate.js';
24
25
  import { isGoalDocumentFileName } from '../self-implement/goal-document.js';
@@ -111,6 +112,8 @@ export interface PrLandDeps {
111
112
  runPublicLeakGate?: (changedFiles: readonly string[], out: { log: (message: string) => void; error: (message: string) => void }) => number;
112
113
  /** Test seam — the post-transform export leak check on changed files. */
113
114
  runExportLeakCheck?: (changedFiles: readonly string[], cwd: string) => ExportLeakCheck;
115
+ /** Test seam — included files must not import a module the public export excludes. */
116
+ runExportImportCheck?: (changedFiles: readonly string[], cwd: string) => ExportImportCheck | Promise<ExportImportCheck>;
114
117
  /** 공개 문서의 `elanous …` 호출 ↔ 실제 `--help` 대조(경고 전용) — 바뀐 공개 문서 경로를 받아 어긋남 목록을 돌려준다. */
115
118
  runDocsCliCheck?: (docFiles: readonly string[]) => DocsCliFinding[];
116
119
  /** 변경 시험 파일 간 간섭 검사 심(시험 주입용). */
@@ -231,14 +234,14 @@ export async function decideOverlapLanding(input: {
231
234
 
232
235
  const liveOut = { log: (message: string) => console.log(message), error: (message: string) => console.error(message) };
233
236
 
234
- function runPrLandTypecheckGate(out: { log: (message: string) => void; error: (message: string) => void }): boolean {
237
+ function runPrLandTypecheckGate(out: { log: (message: string) => void; error: (message: string) => void }, cwd: string = process.cwd()): boolean {
235
238
  let failed = false;
236
239
  const exit = ((code: number): never => {
237
240
  failed = code !== 0;
238
241
  throw new Error(`ci-typecheck-changed exited ${code}`);
239
242
  });
240
243
  try {
241
- runChangedTypecheckGate({ log: out.log, warn: out.error, error: out.error, exit });
244
+ runChangedTypecheckGate({ log: out.log, warn: out.error, error: out.error, exit, cwd });
242
245
  } catch (error) {
243
246
  if (!failed) throw error;
244
247
  }
@@ -290,7 +293,67 @@ export function runPrLandExportLeakCheck(changedFiles: readonly string[], cwd: s
290
293
  return exportLeakCheck(changedFiles, top.status === 0 && String(top.stdout ?? '').trim() ? String(top.stdout).trim() : cwd);
291
294
  }
292
295
 
296
+ /** Included files importing an excluded module — scripts/ci-public-export-import-gate.ts. cwd is the tree being landed. */
297
+ export function runPrLandExportImportCheck(changedFiles: readonly string[], cwd: string): Promise<ExportImportCheck> {
298
+ const top = runGitCommand(cwd, ['rev-parse', '--show-toplevel'], { encoding: 'utf-8' });
299
+ return exportImportCheck(changedFiles, top.status === 0 && String(top.stdout ?? '').trim() ? String(top.stdout).trim() : cwd);
300
+ }
301
+
302
+ /** Blocks the landing when a changed public file imports a module the public export excludes. */
303
+ export async function exportImportBlock(
304
+ changedFiles: readonly string[] | undefined,
305
+ check: () => ExportImportCheck | Promise<ExportImportCheck>,
306
+ out: { log: (message: string) => void; error: (message: string) => void },
307
+ ): Promise<boolean> {
308
+ if (!changedFiles || changedFiles.length === 0) return true;
309
+ let result: ExportImportCheck;
310
+ try { result = await check(); } catch { result = { measured: false, hits: [], unseen: 0, detail: '검사 실행 실패' }; }
311
+ const blocked = !result.measured || result.hits.length > 0;
312
+ record('public-export-import', !blocked, { measured: result.measured, hits: result.hits.length, unseen: result.unseen });
313
+ debug.log('pr.land', 'import-gate', { blocked, hits: result.hits.map(({ file, target }) => ({ file, target })), measured: result.measured, unseen: result.unseen });
314
+ if (!result.measured) {
315
+ out.error(`✗ public-export-import: 못 쟀다 — 막는다(${result.detail ?? '?'})`);
316
+ return false;
317
+ }
318
+ if (result.unseen > 0) out.log(`⚠ public-export-import: 비리터럴 지정자 ${result.unseen}곳은 못 봄`);
319
+ if (result.hits.length === 0) {
320
+ out.log('✓ public-export-import: 바뀐 공개 파일이 공개 내보내기에서 제외된 모듈을 import 하지 않는다.');
321
+ return true;
322
+ }
323
+ out.error(`✗ public-export-import: 공개 파일이 공개 내보내기에서 제외된 모듈을 import 한다 (${result.hits.length}곳) — 공개본 빌드가 여기서 깨진다:`);
324
+ for (const hit of result.hits.slice(0, 20)) out.error(` ${hit.file}:${hit.line} → ${hit.target}`);
325
+ if (result.hits.length > 20) out.error(` … 외 ${result.hits.length - 20}곳`);
326
+ return false;
327
+ }
328
+
293
329
  /** Blocks changed public files that leak or cannot be measured. */
330
+ /**
331
+ * MARKER-GUARD — a changed file that still holds merge-conflict markers is refused before landing.
332
+ * A file counts only when it has both a `<<<<<<< ` and a `>>>>>>> ` line (`=======` alone is a Markdown underline).
333
+ * 10-06 02:4x: a hand landing put three marker lines into release/next.md on main (hotfix #24333).
334
+ */
335
+ export function conflictMarkerFiles(changedFiles: readonly string[] | undefined, cwd: string, read: (path: string) => string | undefined = (path) => {
336
+ // No size cutoff: a large file with markers must still be refused (ACP must-fix). A deleted file reads as undefined.
337
+ try { const full = resolve(cwd, path); return existsSync(full) && statSync(full).isFile() ? readFileSync(full, 'utf8') : undefined; }
338
+ catch { return undefined; }
339
+ }): string[] {
340
+ return (changedFiles ?? []).filter((path) => {
341
+ const text = read(path);
342
+ return text !== undefined && /^<{7} /m.test(text) && /^>{7} /m.test(text);
343
+ });
344
+ }
345
+
346
+ export function conflictMarkerBlock(changedFiles: readonly string[] | undefined, cwd: string,
347
+ out: { log: (message: string) => void; error: (message: string) => void }, read?: (path: string) => string | undefined): boolean {
348
+ const hits = read ? conflictMarkerFiles(changedFiles, cwd, read) : conflictMarkerFiles(changedFiles, cwd);
349
+ if (hits.length) {
350
+ debug.log('pr.land', 'conflict-markers-blocked', { files: hits });
351
+ out.error(`✗ conflict-markers: 충돌 표시(<<<<<<< · >>>>>>>)가 남은 파일 ${hits.length}개 — ${hits.join(', ')} · 표시를 풀고 다시 착지하라`);
352
+ return false;
353
+ }
354
+ return true;
355
+ }
356
+
294
357
  export function exportLeakBlock(
295
358
  changedFiles: readonly string[] | undefined,
296
359
  check: () => ExportLeakCheck,
@@ -401,8 +464,10 @@ function runPrLandDocsCliCheck(root: string): (docFiles: readonly string[]) => D
401
464
  return (docFiles) => checkCommands(docFiles.flatMap((f) => extractElanousCommands(f, readFileSync(join(root, f), 'utf8'))));
402
465
  }
403
466
 
404
- function runPrLandMockModuleRestoreGate(out: { log: (message: string) => void; error: (message: string) => void }): boolean {
405
- return runMockModuleRestoreGate({ args: [], log: out.log, error: out.error }) === 0;
467
+ function runPrLandMockModuleRestoreGate(out: { log: (message: string) => void; error: (message: string) => void }, cwd: string = process.cwd()): boolean {
468
+ const top = runGitCommand(cwd, ['rev-parse', '--show-toplevel'], { encoding: 'utf-8', env: { ...process.env, GIT_DIR: undefined, GIT_WORK_TREE: undefined, GIT_COMMON_DIR: undefined } as NodeJS.ProcessEnv });
469
+ const root = top.status === 0 && String(top.stdout ?? '').trim() ? String(top.stdout).trim() : cwd;
470
+ return runMockModuleRestoreGate({ args: [], cwd: root, log: out.log, error: out.error }) === 0;
406
471
  }
407
472
 
408
473
  // 🚨 안드로이드 축 — `bun test` 우주 «밖»이라 다른 게이트가 원리상 못 본다.
@@ -415,8 +480,9 @@ function runPrLandMockModuleRestoreGate(out: { log: (message: string) => void; e
415
480
  function runPrLandAndroidGate(
416
481
  out: { log: (message: string) => void; error: (message: string) => void },
417
482
  changedFiles: readonly string[] = [],
483
+ cwd: string = process.cwd(),
418
484
  ): boolean {
419
- return runAndroidUnitTestGate({ args: ['--changed-files', ...changedFiles], log: out.log, error: out.error }) === 0;
485
+ return runAndroidUnitTestGate({ args: ['--changed-files', ...changedFiles], cwd, log: out.log, error: out.error }) === 0;
420
486
  }
421
487
 
422
488
  /** ⛔ 보호는 «호출부»에 둔다 — 주입된 게이트가 던져도 같은 계약이 서야 한다.
@@ -1320,7 +1386,7 @@ export async function runPrLand(opts: PrLandOpts = {}, deps: PrLandDeps = {}): P
1320
1386
  }
1321
1387
  }
1322
1388
 
1323
- const typecheckOk = (deps.runTypecheckGate ?? runPrLandTypecheckGate)(out);
1389
+ const typecheckOk = (deps.runTypecheckGate ?? ((o) => runPrLandTypecheckGate(o, cwd)))(out);
1324
1390
  record('typecheck', typecheckOk);
1325
1391
  if (!typecheckOk) {
1326
1392
  // ⛔⭐ 「왜 막혔나」를 «단정하지 않는다» — 이 게이트는 불리언만 돌려주므로 여기서는 이유를 모른다.
@@ -1347,7 +1413,7 @@ export async function runPrLand(opts: PrLandOpts = {}, deps: PrLandDeps = {}): P
1347
1413
  out.log('✓ isolation-gate: scripts/ci-isolation-hardcode-gate.ts PASS — no new homedir+.elanous hardcoding.');
1348
1414
  }
1349
1415
 
1350
- const mockModuleRestore = gateVerdict('mock-module-restore-gate', deps.runMockModuleRestoreGate ?? runPrLandMockModuleRestoreGate, out);
1416
+ const mockModuleRestore = gateVerdict('mock-module-restore-gate', deps.runMockModuleRestoreGate ?? ((o) => runPrLandMockModuleRestoreGate(o, cwd)), out);
1351
1417
  record('mock-module-restore-gate', mockModuleRestore.ok, { measured: mockModuleRestore.measured });
1352
1418
  if (!mockModuleRestore.ok) {
1353
1419
  out.error('✗ mock-module-restore-gate: scripts/ci-mock-module-restore-gate.ts blocked pr land because changed tests introduce mock.module without R-TST23 restoration.');
@@ -1361,7 +1427,7 @@ export async function runPrLand(opts: PrLandOpts = {}, deps: PrLandDeps = {}): P
1361
1427
  // 근본은 훈계가 아니라 ***분포***였다: 스크립트에 박힌 이름 중 «가장 흔한» 것이 ***낡은 것***이라
1362
1428
  // 옆 파일을 베끼면 낡은 이름을 물려받는다. ⇒ 이 자가 «신규»만 막는다(ratchet).
1363
1429
  // ⚠️ `.github/workflows` 가 없어 `pr land` 가 «유일한» 강제점이다.
1364
- const modelHardcode = gateVerdict('model-hardcode-gate', deps.runModelHardcodeGate ?? ((o) => runModelHardcodeGate({ log: o.log, error: o.error, args: [] }) === 0), out);
1430
+ const modelHardcode = gateVerdict('model-hardcode-gate', deps.runModelHardcodeGate ?? ((o) => runModelHardcodeGate({ log: o.log, error: o.error, args: [], cwd }) === 0), out);
1365
1431
  record('model-hardcode-gate', modelHardcode.ok, { measured: modelHardcode.measured });
1366
1432
  if (!modelHardcode.ok) {
1367
1433
  out.error('✗ model-hardcode-gate: scripts/ci-model-hardcode-gate.ts blocked pr land because changed files hardcode a model id — derive it from tierModel()/config instead.');
@@ -1382,15 +1448,17 @@ export async function runPrLand(opts: PrLandOpts = {}, deps: PrLandDeps = {}): P
1382
1448
 
1383
1449
  const changedPaths = currentChangePaths(run, cwd, staged, base, out);
1384
1450
  nextMdWarning(body ?? '', changedPaths, out);
1451
+ if (!conflictMarkerBlock(changedPaths, cwd, out)) return 1;
1385
1452
  publicLeakWarning(changedPaths, deps.runPublicLeakGate ?? ((files, sink) => runPrLandPublicLeakGate(files, sink, cwd, base)), out);
1386
1453
  if (!exportLeakBlock(changedPaths, () => (deps.runExportLeakCheck ?? runPrLandExportLeakCheck)(changedPaths ?? [], cwd), opts.allowPublicLeak === true, out, cwd)) return 1;
1454
+ if (!await exportImportBlock(changedPaths, () => (deps.runExportImportCheck ?? runPrLandExportImportCheck)(changedPaths ?? [], cwd), out)) return 1;
1387
1455
  if (publicDocPaths(changedPaths, () => true).length > 0) {
1388
1456
  const top = run('git', ['rev-parse', '--show-toplevel'], { cwd });
1389
1457
  const docsRoot = top.ok && top.out.trim() ? top.out.trim() : cwd;
1390
1458
  docsCliWarning(publicDocPaths(changedPaths, (f) => existsSync(join(docsRoot, f))), deps.runDocsCliCheck ?? runPrLandDocsCliCheck(docsRoot), out);
1391
1459
  }
1392
1460
  const testInterference = await testInterferenceGateVerdict(
1393
- deps.runTestInterferenceGate ?? ((o, files) => runTestInterferenceGate({ args: ['--changed-files', ...files], log: o.log })),
1461
+ deps.runTestInterferenceGate ?? ((o, files) => runTestInterferenceGate({ args: ['--changed-files', ...files], log: o.log, cwd })),
1394
1462
  changedPaths,
1395
1463
  out,
1396
1464
  );
@@ -1400,7 +1468,7 @@ export async function runPrLand(opts: PrLandOpts = {}, deps: PrLandDeps = {}): P
1400
1468
  'android-gate',
1401
1469
  deps.runAndroidGate
1402
1470
  ? (o) => deps.runAndroidGate!(o, changedPaths)
1403
- : (o) => runPrLandAndroidGate(o, changedPaths),
1471
+ : (o) => runPrLandAndroidGate(o, changedPaths, cwd),
1404
1472
  out,
1405
1473
  );
1406
1474
  record('android-gate', androidGate.ok, { measured: androidGate.measured });
@@ -1418,7 +1486,7 @@ export async function runPrLand(opts: PrLandOpts = {}, deps: PrLandDeps = {}): P
1418
1486
  'ios-gate',
1419
1487
  deps.runIosGate
1420
1488
  ? (o) => deps.runIosGate!(o, changedPaths)
1421
- : (o) => runIosUnitTestGate({ args: ['--changed-files', ...changedPaths], log: o.log, error: o.error }) === 0,
1489
+ : (o) => runIosUnitTestGate({ args: ['--changed-files', ...changedPaths], cwd, log: o.log, error: o.error }) === 0,
1422
1490
  out,
1423
1491
  );
1424
1492
  record('ios-gate', iosGate.ok, { measured: iosGate.measured });
@@ -631,7 +631,7 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
631
631
  if (!opts.dryRun && !mayWriteLedger('release place', false, scheduleLedgerRoot())) return;
632
632
  const priority = opts.priority ?? item.priority;
633
633
  if (!priority) throw new CliUserError(`우선순위가 없는 칸: ${id} — --priority P0|P1|P2 를 지정하라`);
634
- const result = placeCell({ id, title: item.title, owner: item.owner, priority: priority as PlacementPriority, predecessors: item.predecessors ?? [], deadlineVersion: item.deadlineVersion, ceoMinutes: item.ceoMinutes, ceoDate: item.ceoDate }, { schedules: rows, dryRun: opts.dryRun, by: who ?? 'cli' });
634
+ const result = placeCell({ id, title: item.title, owner: item.owner, priority: priority as PlacementPriority, predecessors: item.predecessors ?? [], deadlineVersion: item.deadlineVersion, ceoMinutes: item.ceoMinutes, ceoDate: item.ceoDate, ...(item.accelerator === true ? { accelerator: true } : {}) }, { schedules: rows, dryRun: opts.dryRun, by: who ?? 'cli' });
635
635
  console.log(`${opts.dryRun ? '· 드라이런' : '✅'} ${id} ${result.from ?? '-'} → ${result.version} · ${result.reason}${result.displaced.length ? ` · P2 이월 ${result.displaced.map((row) => row.id).join(', ')}` : ''}`);
636
636
  });
637
637
  release.command('rebalance').description('마감 2시간 전 미시작 칸을 다음 판으로')
@@ -740,11 +740,12 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
740
740
  withContext(checklist.command('add <id> <title>').description('칸 추가')).option('--owner <owner>', '담당').option('--kind <kind>', 'screen = 다섯 화면 짝 칸 · 근거에 짝: PWA … · 데스크톱 … · 폴드 … · 아이폰 … · 아이패드 … 한 줄')
741
741
  .option('--priority <priority>', 'P0|P1|P2').option('--deadline-version <v>', '마감 판').option('--predecessor <id...>', '선행 칸 id')
742
742
  .option('--ceo-minutes <minutes>', '대표 손 분량(분)').option('--ceo-date <date>', '대표 손 날짜 YYYY-MM-DD (없으면 판 착지일 KST)')
743
+ .option('--accelerator', '가속 등급 — 다른 칸의 처리량을 올리는 칸(자율성·효율성·동시성). 생략하면 등급 없음')
743
744
  .option('--allow-duplicate-id', '다른 판에 같은 id 가 있어도 경고 후 추가')
744
- .action((id: string, title: string, opts: { owner?: string; kind?: string; priority?: string; deadlineVersion?: string; predecessor?: string[]; ceoMinutes?: string; ceoDate?: string; allowDuplicateId?: boolean }, cmd: Command) => {
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) => {
745
746
  const { version, json } = context(cmd);
746
747
  if (!mayWrite(cmd)) return;
747
- 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 } : {}) }, { allowDuplicateId: opts.allowDuplicateId });
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 });
748
749
  if (json) console.log(JSON.stringify(data)); else console.log(`✅ ${id} 추가`);
749
750
  });
750
751
  withContext(checklist.command('set <id>').description('칸 상태·근거·담당·처분 갱신'))
@@ -752,13 +753,14 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
752
753
  .option('--disposition <disposition>', 'move|known-issue|block').option('--kind <kind>', 'screen = 다섯 화면 짝 칸 · 근거에 짝: PWA … · 데스크톱 … · 폴드 … · 아이폰 … · 아이패드 … 한 줄')
753
754
  .option('--priority <priority>', 'P0|P1|P2').option('--deadline-version <v>', '마감 판').option('--predecessor <id...>', '선행 칸 id')
754
755
  .option('--ceo-minutes <minutes>', '대표 손 분량(분)').option('--ceo-date <date>', '대표 손 날짜 YYYY-MM-DD')
755
- .action((id: string, opts: { status?: string; evidence?: string; owner?: string; disposition?: string; kind?: string; priority?: string; deadlineVersion?: string; predecessor?: string[]; ceoMinutes?: string; ceoDate?: string }, cmd: Command) => {
756
+ .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) => {
756
758
  const { version, json } = context(cmd);
757
759
  if (opts.status !== undefined && !['green', 'yellow', 'red', 'done'].includes(opts.status)) throw new CliUserError(`잘못된 상태: ${opts.status}`, 'green|yellow|red|done');
758
760
  if (opts.disposition !== undefined && !['move', 'known-issue', 'block'].includes(opts.disposition)) throw new CliUserError(`잘못된 처분: ${opts.disposition}`, 'move|known-issue|block');
759
- 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) throw new CliUserError('갱신할 칸을 지정하라', '--status · --evidence · --owner · --disposition · --kind · --priority · --deadline-version · --predecessor · --ceo-minutes · --ceo-date 중 하나');
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 중 하나');
760
762
  if (!mayWrite(cmd)) return;
761
- 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 } : {}) }, process.env.ELANOUS_TRACK || 'cli');
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');
762
764
  const item = data.items.find((entry) => entry.id === id);
763
765
  const why = item?.kind === 'screen' && item.status === 'green' ? parityGap(item.evidence) : null;
764
766
  if (why) {
@@ -897,13 +899,14 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
897
899
  console.log(renderReleaseNotes(draftReleaseNotes(readLandedCommits(o.from, o.to), o.from, o.to)));
898
900
  });
899
901
  release.command('cut-branch')
900
- .description('원래 컷에 main 의 수리 커밋만 얹은 release/<v> 가지를 만들고 push')
902
+ .description('원래 컷에 main 의 수리 커밋만 얹은 release/<v> 가지를 만들고 push. --append 는 이미 있는 가지에 fast-forward 로만 더 얹는다')
901
903
  .requiredOption('--version <v>', '릴리스 판(x.y.z)')
902
904
  .requiredOption('--base <sha>', '원래 컷 SHA')
903
905
  .requiredOption('--pick <sha...>', '차례로 얹을 main 커밋 SHA')
906
+ .option('--append', '이미 있는 release/<v> 를 출발점으로 fast-forward push(강제 push 없음)')
904
907
  .option('--dry-run', '가지·worktree·push 없이 계획만 출력')
905
908
  .option('--json', '결과 한 줄 JSON(stdout)')
906
- .action(async (o: { version: string; base: string; pick: string[]; dryRun?: boolean; json?: boolean }) => {
909
+ .action(async (o: { version: string; base: string; pick: string[]; append?: boolean; dryRun?: boolean; json?: boolean }) => {
907
910
  await jsonAction(o.json, async (log) => cutReleaseBranch({ ...o, log }), () => true);
908
911
  });
909
912
  const runAction = async (o: { version: string; cutCommit?: string; dryRun?: boolean; ifReady?: boolean; forceFreeze?: boolean; json?: boolean }, logRun = console.log): Promise<boolean | AutoStartDeferral | undefined> => {
@@ -1,4 +1,4 @@
1
- import { existsSync, readFileSync, realpathSync, readlinkSync, readdirSync, statSync, rmSync, symlinkSync, renameSync } from 'node:fs';
1
+ import { existsSync, mkdirSync, readFileSync, realpathSync, readlinkSync, readdirSync, statSync, rmSync, symlinkSync, renameSync, writeFileSync, lstatSync } from 'node:fs';
2
2
  import { resolve, join, basename, dirname } from 'node:path';
3
3
  import { homedir, platform, userInfo } from 'node:os';
4
4
  import { spawnSync } from 'node:child_process';
@@ -18,6 +18,10 @@ export interface SelfUpdateOptions {
18
18
  skipPwaBuild?: boolean;
19
19
  /** 설치된 릴리스에서만 사용한다. 체크아웃 갱신에는 적용하지 않는다. */
20
20
  version?: string;
21
+ /** 판 사이 dev 빌드 설치 — versions/<판>-dev.<sha12> · install.json channel:'dev'. 사람 승인 명령. */
22
+ dev?: boolean;
23
+ /** install.json 의 이전 versionDir 로 current 를 되돌린다. 이전이 없으면 거부. */
24
+ rollback?: boolean;
21
25
  }
22
26
 
23
27
  export interface SelfUpdateDeps {
@@ -45,6 +49,15 @@ export interface SelfUpdateDeps {
45
49
  out?: { log: (s: string) => void; error: (s: string) => void };
46
50
  /** 판올림 성공 알림(주입 안 하면 sendOutbound 'report' · 시험에선 안 보냄). */
47
51
  notice?: (text: string) => void;
52
+ /**
53
+ * 설치 루트(versions · current · install.json). 주입하지 않으면 ~/.local/share/elanous.
54
+ * 시험은 임시 루트만 넘긴다 — 실물 심링크·재시작을 만지지 않는다.
55
+ */
56
+ installRoot?: string;
57
+ /** 패키지 버전(package.json). 주입하지 않으면 체크아웃 package.json. */
58
+ packageVersion?: (checkout: string) => string;
59
+ /** 재시작을 실제로 호출해도 되는지. NODE_ENV=test · ELANOUS_TEST_HOME 이면 기본 false. */
60
+ allowLiveRestart?: boolean;
48
61
  }
49
62
 
50
63
  export interface SelfUpdateResult {
@@ -63,6 +76,12 @@ export interface SelfUpdateResult {
63
76
  prune?: VersionPruneOutcome;
64
77
  /** 재시작 뒤 건강 — ok · rolled-back(직전 판으로 되돌려 회복) · rollback-failed · no-rollback-target. */
65
78
  health?: 'ok' | 'rolled-back' | 'rollback-failed' | 'no-rollback-target' | 'unmeasured';
79
+ /** --dev 설치 또는 --rollback 이 옮긴 current 대상(versions/<name>). */
80
+ current?: string;
81
+ /** install.json channel. dev 설치면 'dev'. */
82
+ channel?: string;
83
+ /** --dev 가 남긴 이전 versionDir. */
84
+ previous?: string;
66
85
  rolledBackTo?: string;
67
86
  /** relay(`com.elanous.openai-relay`) 판정 — 넥서스와 «따로» 본다(T5 · 2026-09-24). */
68
87
  relay?: RelayUpdateOutcome;
@@ -182,6 +201,142 @@ export function rollbackTarget(versions: readonly string[], daemonSha: string, i
182
201
 
183
202
  const VERSIONS_DIR = () => join(homedir(), '.local/share/elanous/versions');
184
203
 
204
+ /** 시험·ELANOUS_TEST_HOME 에서는 실물 재시작을 호출하지 않는다. */
205
+ export function liveRestartBlocked(env: NodeJS.ProcessEnv = process.env): boolean {
206
+ return env.NODE_ENV === 'test' || Boolean(env.ELANOUS_TEST_HOME?.trim());
207
+ }
208
+
209
+ export interface InstallRecord {
210
+ version?: string;
211
+ versionDir?: string;
212
+ source?: string;
213
+ installedAt?: string;
214
+ commit?: string;
215
+ channel?: string;
216
+ previous?: string;
217
+ }
218
+
219
+ export function defaultInstallRoot(): string {
220
+ return join(homedir(), '.local/share/elanous');
221
+ }
222
+
223
+ export function readInstallRecord(root: string): InstallRecord | null {
224
+ try {
225
+ const parsed: unknown = JSON.parse(readFileSync(join(root, 'install.json'), 'utf8'));
226
+ if (typeof parsed !== 'object' || parsed === null) return null;
227
+ return parsed as InstallRecord;
228
+ } catch {
229
+ return null;
230
+ }
231
+ }
232
+
233
+ /** install.json 의 version·commit. 못 읽으면 둘 다 «unknown». */
234
+ export function readInstalledIdentity(root: string = defaultInstallRoot()): { installedVersion: string; installedCommit: string } {
235
+ const record = readInstallRecord(root);
236
+ const version = typeof record?.version === 'string' && record.version.trim() ? record.version.trim() : '';
237
+ const commit = typeof record?.commit === 'string' && record.commit.trim() ? record.commit.trim() : '';
238
+ return { installedVersion: version || 'unknown', installedCommit: commit || 'unknown' };
239
+ }
240
+
241
+ const VERSION_DIR_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
242
+
243
+ function versionDirName(versionDir: string | undefined): string | null {
244
+ if (typeof versionDir !== 'string') return null;
245
+ if (!/^versions\/[A-Za-z0-9][A-Za-z0-9._-]*$/.test(versionDir) || versionDir.includes('..')) return null;
246
+ return basename(versionDir);
247
+ }
248
+
249
+ /** 판 디렉터리에 실행 진입 파일이 있나(설치기가 실제로 내용을 깔았나). */
250
+ export function installEntryExists(root: string, versionName: string): boolean {
251
+ return existsSync(join(root, 'versions', versionName, 'node_modules', 'elanous', 'bin', 'elanous.mjs'));
252
+ }
253
+
254
+ /** 판 이름 «<판>-<커밋12>» · «<판>-dev.<커밋12>» 에서 판·커밋을 읽는다(되돌림 메타 일치용). */
255
+ export function parseVersionDirName(name: string): { version: string; commit: string } | null {
256
+ const m = /^(.+?)-(?:dev\.)?([0-9a-f]{12})(?:-dirty)?$/.exec(name);
257
+ return m ? { version: m[1]!, commit: m[2]! } : null;
258
+ }
259
+
260
+ /** current 심링크를 versions/<name> 으로 원자적으로 옮긴다. 대상 폴더가 없으면 거부. */
261
+ export function relinkInstallCurrent(root: string, versionName: string): void {
262
+ if (!VERSION_DIR_NAME.test(versionName) || versionName.includes('..')) throw new Error(`잘못된 판 이름: ${versionName}`);
263
+ if (!existsSync(join(root, 'versions', versionName))) throw new Error(`판 디렉터리 없음: ${versionName}`);
264
+ // EDGE-RAIL-VERIFY(10-06 05:30 사고): 진입 파일이 없는 판으로는 current 를 옮기지 않는다(빈 판 → 전역 elanous·크론 전부 실패).
265
+ if (!installEntryExists(root, versionName)) throw new Error(`빈 판(진입 파일 없음): ${versionName} — current 를 옮기지 않는다`);
266
+ const tmp = join(root, `.current-dev-${process.pid}`);
267
+ rmSync(tmp, { force: true });
268
+ symlinkSync(`versions/${versionName}`, tmp);
269
+ renameSync(tmp, join(root, 'current'));
270
+ }
271
+
272
+ function checkoutPackageVersion(checkout: string): string {
273
+ const parsed = JSON.parse(readFileSync(join(checkout, 'package.json'), 'utf8')) as { version?: unknown };
274
+ if (typeof parsed.version !== 'string' || !parsed.version.trim()) throw new Error('package.json version 없음');
275
+ return parsed.version.trim();
276
+ }
277
+
278
+ /**
279
+ * 판 사이 dev 빌드 — versions/<판>-dev.<sha12> 를 만들고 current 를 옮긴다.
280
+ * install.json 에 channel:'dev' 와 이전 versionDir(previous) 를 남긴다. 실제 설치기·재시작은 부르지 않는다.
281
+ */
282
+ export function installDevBuild(checkout: string, commit: string, deps: Pick<SelfUpdateDeps, 'installRoot' | 'packageVersion'> & { runInstaller?: () => { status: number | null; stderr: string } } = {}): { versionName: string; versionDir: string; previous: string; record: InstallRecord } {
283
+ const sha12 = commit.trim().toLowerCase().slice(0, 12);
284
+ if (!/^[0-9a-f]{12}$/.test(sha12)) throw new Error(`dev 설치 커밋이 12자가 아님: ${commit}`);
285
+ const root = deps.installRoot ?? defaultInstallRoot();
286
+ const prior = readInstallRecord(root);
287
+ const previous = versionDirName(prior?.versionDir) ? prior!.versionDir! : (typeof prior?.previous === 'string' ? prior.previous : '');
288
+ // EDGE-RAIL-VERIFY: 빈 디렉터리를 만들고 current 만 옮기던 길(10-06 05:30 사고)을 버리고, 정식 길과 같은 설치기로 내용을 깐다.
289
+ if (!deps.runInstaller) throw new Error('dev 설치기 없음 — 빈 판을 깔지 않는다');
290
+ // install.sh 는 current 를 «먼저» 옮기고 뒤에서 실패할 수 있다 — 실패·빈 판 어느 쪽이든 이전 판·install.json 으로 되돌린다.
291
+ const restore = (why: string, versionName: string | null): never => {
292
+ const previousName = versionDirName(previous);
293
+ if (previousName && installEntryExists(root, previousName)) relinkInstallCurrent(root, previousName);
294
+ if (prior) writeFileSync(join(root, 'install.json'), `${JSON.stringify(prior, null, 2)}\n`);
295
+ try { debug.log('self-update', 'dev-install-refused', { why, versionName, kept: previousName ?? null }); } catch { /* 관측 실패가 막지 않는다 */ }
296
+ throw new Error(`${why}: ${versionName ?? '?'} — current 를 이전 판(${previousName ?? '없음'})으로 유지`);
297
+ };
298
+ let result: { status: number | null; stderr: string };
299
+ try { result = deps.runInstaller(); }
300
+ catch (error) { return restore(`설치기 예외(${String(error).slice(0, 200)})`, null); }
301
+ const after = readInstallRecord(root);
302
+ const versionName = versionDirName(after?.versionDir);
303
+ if (result.status !== 0) return restore(`설치기 실패(rc=${result.status}: ${result.stderr.slice(0, 200)})`, versionName);
304
+ if (!versionName || !installEntryExists(root, versionName)) return restore('빈 판(진입 파일 없음)', versionName);
305
+ const versionDir = `versions/${versionName}`;
306
+ const record: InstallRecord = {
307
+ ...(after ?? {}),
308
+ versionDir,
309
+ channel: 'dev',
310
+ ...(previous && previous !== versionDir ? { previous } : {}),
311
+ };
312
+ writeFileSync(join(root, 'install.json'), `${JSON.stringify(record, null, 2)}\n`);
313
+ return { versionName, versionDir, previous, record };
314
+ }
315
+
316
+ /** install.json.previous 의 판으로 current 를 되돌린다. previous 가 없으면 거부(exit 2). */
317
+ export function rollbackDevInstall(deps: Pick<SelfUpdateDeps, 'installRoot'> = {}): { ok: true; versionName: string; record: InstallRecord } | { ok: false; reason: string } {
318
+ const root = deps.installRoot ?? defaultInstallRoot();
319
+ const prior = readInstallRecord(root);
320
+ const previousName = versionDirName(prior?.previous);
321
+ if (!prior || !previousName) return { ok: false, reason: '이전 판(install.json previous)이 없다 — rollback 거부' };
322
+ if (!existsSync(join(root, 'versions', previousName))) return { ok: false, reason: `이전 판 디렉터리 없음: ${previousName}` };
323
+ const currentName = versionDirName(prior.versionDir);
324
+ relinkInstallCurrent(root, previousName);
325
+ const parsed = parseVersionDirName(previousName);
326
+ const record: InstallRecord = {
327
+ ...prior,
328
+ ...(parsed ? { version: parsed.version, commit: parsed.commit } : {}),
329
+ versionDir: `versions/${previousName}`,
330
+ channel: prior.channel === 'dev' ? undefined : prior.channel,
331
+ previous: currentName ? `versions/${currentName}` : undefined,
332
+ installedAt: new Date().toISOString(),
333
+ };
334
+ if (!record.channel) delete record.channel;
335
+ if (!record.previous) delete record.previous;
336
+ writeFileSync(join(root, 'install.json'), `${JSON.stringify(record, null, 2)}\n`);
337
+ return { ok: true, versionName: previousName, record };
338
+ }
339
+
185
340
  /** `unmeasured` = 건강을 «잴 수» 없었다(REST 주소를 한 번도 못 얻음) — 「건강하지 않다」와 다르다. 되돌리지 않는다. */
186
341
  export interface VerifyRestartResult { ok: boolean; daemonSha?: string; reason?: string; unmeasured?: boolean }
187
342
 
@@ -399,9 +554,14 @@ export async function runReleaseUpdate(options: ReleaseUpdateOptions = {}, deps:
399
554
  }
400
555
  let previous = '';
401
556
  let previousBuild = '';
557
+ let replacedDev: { from: string; to: string } | null = null;
402
558
  try {
403
559
  const metadata: unknown = JSON.parse(readFileSync(join(prefix, 'install.json'), 'utf8'));
404
560
  previousBuild = installedBuild(metadata);
561
+ if (typeof metadata === 'object' && metadata !== null && (metadata as { channel?: unknown }).channel === 'dev') {
562
+ const fromDir = typeof (metadata as { versionDir?: unknown }).versionDir === 'string' ? (metadata as { versionDir: string }).versionDir : '';
563
+ replacedDev = { from: fromDir, to: '' };
564
+ }
405
565
  if (typeof metadata === 'object' && metadata !== null && 'versionDir' in metadata && typeof metadata.versionDir === 'string') {
406
566
  const dir = metadata.versionDir;
407
567
  if (/^versions\/[a-zA-Z0-9._-]+$/.test(dir) && !dir.includes('..')) previous = basename(dir);
@@ -418,6 +578,12 @@ export async function runReleaseUpdate(options: ReleaseUpdateOptions = {}, deps:
418
578
  const installedVersion = typeof metadata === 'object' && metadata !== null && 'version' in metadata && typeof metadata.version === 'string'
419
579
  ? metadata.version : null;
420
580
  if (!installedVersion) return fail(1, '설치판 확인 실패: install.json version 없음');
581
+ if (replacedDev) {
582
+ const toDir = typeof metadata === 'object' && metadata !== null && typeof (metadata as { versionDir?: unknown }).versionDir === 'string'
583
+ ? (metadata as { versionDir: string }).versionDir : installedVersion;
584
+ replacedDev = { from: replacedDev.from, to: toDir };
585
+ try { debug.log('self-update', 'dev-replaced', replacedDev); } catch { /* 관측 실패가 갱신을 막지 않는다 */ }
586
+ }
421
587
  const internalRevision = isTailnet(source) && version?.match(/^(.*)-([0-9a-f]{12})$/);
422
588
  if (version && installedVersion !== version && (!internalRevision || installedVersion !== internalRevision[1])) {
423
589
  return fail(1, `설치판 버전 불일치: 요청 ${version}, 설치 ${installedVersion}`);
@@ -491,6 +657,9 @@ export async function runUpdateForInstallation(
491
657
  out.log(options.json ? JSON.stringify(result) : `self-update: ${result.reason}`);
492
658
  return result;
493
659
  }
660
+ if (options.dev || options.rollback) {
661
+ return runSelfUpdate(options, { cliRoot, ...deps.checkout });
662
+ }
494
663
  if (options.version !== undefined) {
495
664
  const result: SelfUpdateResult = { exitCode: 2, installedVersion: null, decision: null, restarted: false, reason: '--version 은 릴리스 설치본에서만 사용 가능' };
496
665
  const out = deps.checkout?.out ?? console;
@@ -537,6 +706,15 @@ export async function runSelfUpdate(options: SelfUpdateOptions = {}, deps: SelfU
537
706
  return result;
538
707
  };
539
708
  const reject = (reason: string): SelfUpdateResult => emit({ exitCode: 2, installedVersion: null, decision: null, restarted: false, reason });
709
+ if (options.rollback) {
710
+ const rolled = rollbackDevInstall(deps);
711
+ if (!rolled.ok) return reject(rolled.reason);
712
+ return emit({
713
+ exitCode: 0, installedVersion: rolled.versionName, decision: null, restarted: false,
714
+ current: `versions/${rolled.versionName}`, previous: rolled.record.previous, channel: rolled.record.channel,
715
+ reason: `rollback: current → ${rolled.versionName}`,
716
+ });
717
+ }
540
718
  try {
541
719
  if (!existsSync(join(checkout, 'scripts/install.sh'))) return reject(`체크아웃 없음 또는 설치기 없음: ${checkout}`);
542
720
  const root = git(checkout, ['rev-parse', '--show-toplevel']);
@@ -546,6 +724,24 @@ export async function runSelfUpdate(options: SelfUpdateOptions = {}, deps: SelfU
546
724
  const dirty = git(checkout, ['diff', '--quiet', 'HEAD', '--']);
547
725
  if (dirty.status !== 0) return reject(dirty.status === 1 ? '추적 파일 변경: 체크아웃이 더러움' : `git diff 실패: ${dirty.stderr}`);
548
726
 
727
+ if (options.dev) {
728
+ try {
729
+ const installed = installDevBuild(checkout, head.stdout.trim(), { ...deps, runInstaller: () => run('bash', [join(checkout, 'scripts/install.sh'), '--no-modify-path'], checkout) });
730
+ return emit({
731
+ exitCode: 0,
732
+ installedVersion: installed.versionName,
733
+ decision: null,
734
+ restarted: false,
735
+ current: installed.versionDir,
736
+ channel: 'dev',
737
+ previous: installed.previous || undefined,
738
+ reason: `dev 설치: current → ${installed.versionName}`,
739
+ });
740
+ } catch (error) {
741
+ return emit({ exitCode: 1, installedVersion: null, decision: null, restarted: false, reason: `dev 설치 실패: ${error instanceof Error ? error.message : String(error)}` });
742
+ }
743
+ }
744
+
549
745
  const silence = { log: (_s: string) => {}, error: (_s: string) => {} };
550
746
  let decision: RestartNeededResult;
551
747
  try {
@@ -628,6 +824,9 @@ export async function runSelfUpdate(options: SelfUpdateOptions = {}, deps: SelfU
628
824
  const os = deps.os ?? platform();
629
825
  const command = os === 'darwin' ? 'launchctl' : os === 'linux' ? 'systemctl' : null;
630
826
  if (!command) return emit({ exitCode: 1, installedVersion, decision, restarted: false, prune, reason: `지원하지 않는 플랫폼: ${os}` });
827
+ if (deps.allowLiveRestart === false || (deps.allowLiveRestart !== true && liveRestartBlocked())) {
828
+ return emit({ exitCode: 0, installedVersion, decision, restarted: false, prune, reason: '테스트 가드: 실물 재시작 0' });
829
+ }
631
830
  const args = os === 'darwin' ? ['kickstart', '-k', `gui/${deps.uid ?? process.getuid?.() ?? userInfo().uid}/com.elanous.nexus`] : ['--user', 'restart', 'elanous-nexus'];
632
831
  try {
633
832
  const restarted = run(command, args, checkout);