elanous 0.2.12 → 0.2.13

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 (289) 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/{1394-ee3795b112907122.js → 1394-931c9827067b2276.js} +1 -1
  4. package/apps/pwa/out/_next/static/chunks/{3992-2e522a8ac5f2afde.js → 3992-fe5c06d6b31f8b25.js} +1 -1
  5. package/apps/pwa/out/_next/static/chunks/5250-c78ca295585bbf95.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/6511-ff8360be3d65aaa4.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/6588-ba380869b67c4fa7.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/{7758-0b018a9e4fc56200.js → 7758-85c2b53f731da024.js} +1 -1
  9. package/apps/pwa/out/_next/static/chunks/{8605-0259994816604c49.js → 8605-9da4678f8a310d18.js} +1 -1
  10. package/apps/pwa/out/_next/static/chunks/9906-e7b8b5310a169dfe.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/app/approvals/{page-cb169620945d1952.js → page-e5b4e1b256f41076.js} +1 -1
  12. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-cefe8169e36cf62a.js → page-e49b9b76c565a0b1.js} +1 -1
  13. package/apps/pwa/out/_next/static/chunks/app/board/page-b4f032c893b21cc9.js +1 -0
  14. package/apps/pwa/out/_next/static/chunks/app/botlab/page-f14987d5850a31e0.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/app/bots/page-b433308aad13ceb6.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/app/chat/{page-813c4a52c9d05ca8.js → page-4c1dbb21861beab2.js} +1 -1
  17. package/apps/pwa/out/_next/static/chunks/app/consult/{page-4731f2c215358937.js → page-c85b816e3b269dbb.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/control/{page-a022b257950f39e6.js → page-f6bc66b919b2ea7d.js} +1 -1
  19. package/apps/pwa/out/_next/static/chunks/app/design-check/{page-a2ffc717e04b8b0a.js → page-60603c1e056c9b25.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/editor/{page-b59ee4e70fc18fcf.js → page-a0ea8f5a2341ee6b.js} +1 -1
  21. package/apps/pwa/out/_next/static/chunks/app/exec/page-45c5bf04b4a605e6.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/app/field/{page-c172dabd796eb7a3.js → page-36c6c4183353ac9e.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/app/inside/{page-1efd0cb9e0ed9d13.js → page-cb2eb0fd809980da.js} +1 -1
  24. package/apps/pwa/out/_next/static/chunks/app/intake/{page-d5dc7ff69a37b4cf.js → page-31012264da8a341f.js} +1 -1
  25. package/apps/pwa/out/_next/static/chunks/app/layout-50309aeb0bdd554a.js +1 -0
  26. package/apps/pwa/out/_next/static/chunks/app/live/page-bc00e964e855f4df.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/app/market/{page-dfca9b7d2de0b135.js → page-0696ee3fac9fe56c.js} +1 -1
  28. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-793fa536b3a6ef37.js → page-66c27608f651b866.js} +1 -1
  29. package/apps/pwa/out/_next/static/chunks/app/morning/{page-6dd3bfbbde2a5c2c.js → page-6ddd2109b7fc3a80.js} +1 -1
  30. package/apps/pwa/out/_next/static/chunks/app/not-found-4e50b7298c1f69a5.js +1 -0
  31. package/apps/pwa/out/_next/static/chunks/app/observatory/{page-77447356baad75d2.js → page-7a2bf830038a2264.js} +1 -1
  32. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-1f327f18694fabca.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-e5cef2fe6e4028df.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-cf596128d2b420ba.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/app/outputs/{page-6d604227e1664b71.js → page-f672536f14f0969f.js} +1 -1
  36. package/apps/pwa/out/_next/static/chunks/app/page-b52365662232db89.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/app/reflection/{page-4048543b4b7ad5e0.js → page-e7a6c80f288b1948.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-4c3a3126abed551a.js → page-29a709616df9b050.js} +1 -1
  39. package/apps/pwa/out/_next/static/chunks/app/sessions/{page-64b0aecb1ca75e75.js → page-fee842d7c2151129.js} +1 -1
  40. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-78b40a344ed22f24.js +1 -0
  41. package/apps/pwa/out/_next/static/chunks/app/settings/page-75d199f448171014.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-a011dd5b0bff29c5.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/app/setup/{page-9547b5cec7cf76bf.js → page-d92e32d51ab5157e.js} +1 -1
  44. package/apps/pwa/out/_next/static/chunks/app/share/page-5f21aed6932ef375.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/app/showroom/page-1e1a53946804debf.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/app/tasks/{page-2cddeb9e2aeedc55.js → page-a1833cd43d832601.js} +1 -1
  47. package/apps/pwa/out/_next/static/chunks/app/term/{page-e297866051595d81.js → page-63d14a4d8fe1e1c1.js} +1 -1
  48. package/apps/pwa/out/_next/static/chunks/app/today/{page-baba2e047872fb0f.js → page-2a511d320f762e06.js} +1 -1
  49. package/apps/pwa/out/_next/static/chunks/app/trace/{page-e895da4ec9cac345.js → page-1c3e03f88dac1e21.js} +1 -1
  50. package/apps/pwa/out/_next/static/chunks/app/vault/page-c4d5289c2ea97a89.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-b91f8952deaeaf27.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/workflows/page-71b43a2588489987.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-2e9e2fb01c9da1c9.js → page-2b256feed331546f.js} +1 -1
  54. package/apps/pwa/out/_next/static/chunks/app/worktrees/{page-d62ceb13411ebba4.js → page-505bb96d69a8622c.js} +1 -1
  55. package/apps/pwa/out/approvals/index.html +1 -1
  56. package/apps/pwa/out/approvals/index.txt +13 -13
  57. package/apps/pwa/out/autopilot/index.html +1 -1
  58. package/apps/pwa/out/autopilot/index.txt +13 -13
  59. package/apps/pwa/out/board/index.html +1 -1
  60. package/apps/pwa/out/board/index.txt +13 -13
  61. package/apps/pwa/out/botlab/index.html +1 -1
  62. package/apps/pwa/out/botlab/index.txt +13 -13
  63. package/apps/pwa/out/bots/index.html +1 -1
  64. package/apps/pwa/out/bots/index.txt +13 -13
  65. package/apps/pwa/out/chat/index.html +1 -1
  66. package/apps/pwa/out/chat/index.txt +13 -13
  67. package/apps/pwa/out/consult/index.html +1 -1
  68. package/apps/pwa/out/consult/index.txt +13 -13
  69. package/apps/pwa/out/control/index.html +1 -1
  70. package/apps/pwa/out/control/index.txt +13 -13
  71. package/apps/pwa/out/design-check/index.html +1 -1
  72. package/apps/pwa/out/design-check/index.txt +13 -13
  73. package/apps/pwa/out/editor/index.html +1 -1
  74. package/apps/pwa/out/editor/index.txt +13 -13
  75. package/apps/pwa/out/exec/index.html +1 -1
  76. package/apps/pwa/out/exec/index.txt +13 -13
  77. package/apps/pwa/out/field/index.html +1 -1
  78. package/apps/pwa/out/field/index.txt +13 -13
  79. package/apps/pwa/out/healthz +1 -1
  80. package/apps/pwa/out/index.html +1 -1
  81. package/apps/pwa/out/index.txt +13 -13
  82. package/apps/pwa/out/inside/index.html +1 -1
  83. package/apps/pwa/out/inside/index.txt +13 -13
  84. package/apps/pwa/out/intake/index.html +1 -1
  85. package/apps/pwa/out/intake/index.txt +13 -13
  86. package/apps/pwa/out/live/index.html +1 -1
  87. package/apps/pwa/out/live/index.txt +13 -13
  88. package/apps/pwa/out/market/index.html +1 -1
  89. package/apps/pwa/out/market/index.txt +13 -13
  90. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  91. package/apps/pwa/out/missions/placeholder/index.txt +13 -13
  92. package/apps/pwa/out/morning/index.html +1 -1
  93. package/apps/pwa/out/morning/index.txt +13 -13
  94. package/apps/pwa/out/observatory/index.html +1 -1
  95. package/apps/pwa/out/observatory/index.txt +13 -13
  96. package/apps/pwa/out/ops/checklist/index.html +1 -1
  97. package/apps/pwa/out/ops/checklist/index.txt +13 -13
  98. package/apps/pwa/out/ops/release/index.html +1 -1
  99. package/apps/pwa/out/ops/release/index.txt +13 -13
  100. package/apps/pwa/out/ops/seats/index.html +1 -1
  101. package/apps/pwa/out/ops/seats/index.txt +13 -13
  102. package/apps/pwa/out/outputs/index.html +1 -1
  103. package/apps/pwa/out/outputs/index.txt +13 -13
  104. package/apps/pwa/out/reflection/index.html +1 -1
  105. package/apps/pwa/out/reflection/index.txt +14 -14
  106. package/apps/pwa/out/scheduler/index.html +1 -1
  107. package/apps/pwa/out/scheduler/index.txt +13 -13
  108. package/apps/pwa/out/sessions/index.html +1 -1
  109. package/apps/pwa/out/sessions/index.txt +13 -13
  110. package/apps/pwa/out/settings/devices/index.html +1 -1
  111. package/apps/pwa/out/settings/devices/index.txt +13 -13
  112. package/apps/pwa/out/settings/index.html +1 -1
  113. package/apps/pwa/out/settings/index.txt +13 -13
  114. package/apps/pwa/out/setup/done/index.html +1 -1
  115. package/apps/pwa/out/setup/done/index.txt +13 -13
  116. package/apps/pwa/out/setup/index.html +1 -1
  117. package/apps/pwa/out/setup/index.txt +13 -13
  118. package/apps/pwa/out/share/index.html +1 -1
  119. package/apps/pwa/out/share/index.txt +13 -13
  120. package/apps/pwa/out/showroom/index.html +1 -1
  121. package/apps/pwa/out/showroom/index.txt +13 -13
  122. package/apps/pwa/out/tasks/index.html +1 -1
  123. package/apps/pwa/out/tasks/index.txt +13 -13
  124. package/apps/pwa/out/term/index.html +1 -1
  125. package/apps/pwa/out/term/index.txt +13 -13
  126. package/apps/pwa/out/today/index.html +1 -1
  127. package/apps/pwa/out/today/index.txt +13 -13
  128. package/apps/pwa/out/trace/index.html +1 -1
  129. package/apps/pwa/out/trace/index.txt +13 -13
  130. package/apps/pwa/out/vault/index.html +1 -1
  131. package/apps/pwa/out/vault/index.txt +13 -13
  132. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  133. package/apps/pwa/out/workflows/chat-ui/index.txt +13 -13
  134. package/apps/pwa/out/workflows/index.html +1 -1
  135. package/apps/pwa/out/workflows/index.txt +13 -13
  136. package/apps/pwa/out/workspace/index.html +1 -1
  137. package/apps/pwa/out/workspace/index.txt +13 -13
  138. package/apps/pwa/out/worktrees/index.html +1 -1
  139. package/apps/pwa/out/worktrees/index.txt +13 -13
  140. package/examples/teams/personas/backend.yaml +4 -0
  141. package/examples/teams/personas/pm.yaml +4 -0
  142. package/examples/teams/personas/qa.yaml +4 -0
  143. package/examples/teams/small-product.yaml +20 -0
  144. package/graphs/lecture/lecture-note.yaml +28 -0
  145. package/graphs/lecture/recipes.yaml +17 -0
  146. package/package.json +2 -1
  147. package/scripts/context-hooks/emit.ts +36 -14
  148. package/scripts/coord-channel-watch.sh +1 -0
  149. package/scripts/discord-seat-probe.ts +144 -0
  150. package/scripts/hq/hq-standby.sh +65 -0
  151. package/scripts/hq/standby-promote.ts +173 -0
  152. package/scripts/hq/standby-role.ts +59 -0
  153. package/scripts/hq/standby-snapshot.ts +226 -0
  154. package/scripts/hq/standby-verify.ts +219 -0
  155. package/scripts/lecture-note/node.ts +256 -0
  156. package/scripts/release-loop/gate-node.ts +53 -15
  157. package/scripts/release-loop/ops-upgrade-node.ts +17 -5
  158. package/scripts/test-deterministic.ts +53 -3
  159. package/skills/omni-crawl/SKILL.md +16 -16
  160. package/skills/omni-crawl/package-lock.json +1 -544
  161. package/skills/omni-crawl/package.json +3 -4
  162. package/skills/omni-crawl/scripts/main.ts +13 -13
  163. package/skills/omni-crawl/scripts/monitor.ts +8 -8
  164. package/skills/omni-crawl/src/apify.ts +13 -2
  165. package/skills/omni-crawl/src/capture.ts +6 -0
  166. package/skills/omni-crawl/src/registry.ts +2 -2
  167. package/skills/omni-crawl/src/stdout-json.ts +4 -0
  168. package/skills/omni-digest/SKILL.md +10 -11
  169. package/skills/omni-digest/package-lock.json +0 -545
  170. package/skills/omni-digest/package.json +2 -5
  171. package/skills/omni-digest/scripts/main.ts +3 -3
  172. package/skills/omni-digest/src/delegate.ts +5 -5
  173. package/skills/omni-digest/src/fetch.ts +2 -2
  174. package/skills/omni-digest/src/media.ts +25 -11
  175. package/skills/omni-digest/src/visual.ts +3 -2
  176. package/skills/omni-market/scripts/main.ts +1 -1
  177. package/skills/omni-market/scripts/stdout-json.ts +4 -0
  178. package/src/agent/identity-env.ts +11 -0
  179. package/src/agent-mission/driver.ts +12 -7
  180. package/src/agent-mission/duo.ts +94 -0
  181. package/src/agent-mission/pty-decision.ts +1 -1
  182. package/src/autopilot/track-agent-shadow.ts +2 -1
  183. package/src/autopilot/track-agent.ts +4 -1
  184. package/src/boot/daemon-prompt-request.ts +3 -0
  185. package/src/boot/daemon-prompt-turn.ts +6 -2
  186. package/src/boot/daemon-runtime.ts +48 -16
  187. package/src/cli/decisions-cli.ts +51 -19
  188. package/src/cli/outputs-cli.ts +92 -0
  189. package/src/cli/persona-cli.ts +29 -0
  190. package/src/cli/pod-cli.ts +10 -3
  191. package/src/cli/pr-cli.ts +15 -1
  192. package/src/cli/release-cli.ts +108 -28
  193. package/src/context-bus/condense.ts +194 -0
  194. package/src/context-bus/context-day.ts +82 -0
  195. package/src/context-bus/context-now-surfaces.ts +15 -2
  196. package/src/context-bus/long-term-memory.ts +118 -22
  197. package/src/context-bus/nightly-condense.ts +1 -1
  198. package/src/decisions/decision-cards.ts +13 -7
  199. package/src/decisions/decision-ledger.ts +43 -17
  200. package/src/graph-kinds/registry.ts +2 -2
  201. package/src/graph-runner/graph-approval-card.ts +2 -2
  202. package/src/graph-runner/graph-grow.ts +64 -11
  203. package/src/graph-runner/graph-tick.ts +9 -2
  204. package/src/graph-runner/runner.ts +81 -17
  205. package/src/harness/harness-cli-command.ts +27 -1
  206. package/src/harness/harness-queue-child.ts +37 -0
  207. package/src/harness/harness-queue.ts +306 -0
  208. package/src/hooks/github-shadow.ts +72 -2
  209. package/src/hooks/receiver.ts +4 -2
  210. package/src/hq/hq.ts +318 -0
  211. package/src/hq/lease.ts +210 -0
  212. package/src/hq/ledger-write-target.ts +41 -0
  213. package/src/index.ts +199 -7
  214. package/src/intake-plane/discord-seat-work.ts +13 -6
  215. package/src/intake-plane/linear-wish.ts +44 -0
  216. package/src/intake-plane/persona-answer.ts +5 -0
  217. package/src/intake-plane/telegram-seat-work.ts +13 -6
  218. package/src/intake-plane/wish-card.ts +1 -1
  219. package/src/loops/checker.ts +178 -0
  220. package/src/loops/loop-cli.ts +37 -3
  221. package/src/loops/observe.ts +49 -0
  222. package/src/loops/orchestrator/overlap.ts +141 -0
  223. package/src/loops/orchestrator/traffic.ts +150 -0
  224. package/src/loops/package/verify.ts +125 -0
  225. package/src/loops/verdict.ts +22 -0
  226. package/src/maturity/feature-maturity.ts +3 -3
  227. package/src/memory.ts +37 -4
  228. package/src/nexus/api/card-followup-route.ts +118 -0
  229. package/src/nexus/api/decisions-route.ts +68 -0
  230. package/src/nexus/api/http-server.ts +47 -0
  231. package/src/nexus/api/personas.ts +2 -1
  232. package/src/nexus/api/skill-exec-route.ts +97 -0
  233. package/src/persona/loader.ts +18 -0
  234. package/src/persona/persona-todo.ts +14 -9
  235. package/src/persona/presets.ts +2 -1
  236. package/src/persona/team.ts +105 -0
  237. package/src/persona/types.ts +4 -0
  238. package/src/pod-lease/host-lease.ts +130 -20
  239. package/src/release-loop/landed-but-yellow.ts +12 -0
  240. package/src/seat-dispatch/seat-ask.ts +57 -12
  241. package/src/seat-loop/seat-loop.ts +90 -16
  242. package/src/seat-loop/seat-pack.ts +127 -0
  243. package/src/self-dev/ask-launch-flow.ts +3 -0
  244. package/src/self-implement/l8-merge-queue.ts +157 -1
  245. package/src/steward/launch.ts +5 -1
  246. package/src/steward/readiness.ts +125 -0
  247. package/src/steward/rescue.ts +75 -0
  248. package/src/steward/triage.ts +48 -9
  249. package/src/task-cards/card-cli.ts +78 -0
  250. package/src/task-orchestrator/surfaces/pod-lease.ts +18 -6
  251. package/src/task-orchestrator/surfaces/pod-pool.ts +27 -10
  252. package/src/task-orchestrator/surfaces/self-implement-pod.ts +38 -8
  253. package/src/telegram-dispatch-forward.ts +1 -1
  254. package/src/telegram-fabric-plan.ts +90 -0
  255. package/src/telegram-project-command.ts +10 -4
  256. package/src/telegram.ts +5 -0
  257. package/src/tool-runtime/elanous-control-runtimes.ts +2 -1
  258. package/src/user-config.ts +94 -10
  259. package/src/version/packed-revision.json +1 -1
  260. package/src/workflow-runtime/executor.ts +4 -0
  261. package/src/workflow-runtime/node-catalog.ts +13 -0
  262. package/src/workflow-runtime/nodes/knowledge.ts +38 -0
  263. package/src/workflow-runtime/schema.ts +17 -1
  264. package/src/workflow-runtime/types.ts +13 -1
  265. package/apps/pwa/out/_next/static/chunks/1147-466ec15afacc1a65.js +0 -1
  266. package/apps/pwa/out/_next/static/chunks/7277-0b2fc7c16ba2d64b.js +0 -1
  267. package/apps/pwa/out/_next/static/chunks/9906-482c6619b3fdda20.js +0 -1
  268. package/apps/pwa/out/_next/static/chunks/9920-5cfdc5e8fd209825.js +0 -1
  269. package/apps/pwa/out/_next/static/chunks/app/board/page-d2e35228bc497119.js +0 -1
  270. package/apps/pwa/out/_next/static/chunks/app/botlab/page-5c05b0da4bc1392c.js +0 -1
  271. package/apps/pwa/out/_next/static/chunks/app/bots/page-e0ccdb576812b523.js +0 -1
  272. package/apps/pwa/out/_next/static/chunks/app/exec/page-a67e11ba962337b5.js +0 -1
  273. package/apps/pwa/out/_next/static/chunks/app/layout-e71c84189e8c683e.js +0 -1
  274. package/apps/pwa/out/_next/static/chunks/app/live/page-1d6a533ce5a6efcb.js +0 -1
  275. package/apps/pwa/out/_next/static/chunks/app/not-found-490653e8d4cc8852.js +0 -1
  276. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-29e961191f6057df.js +0 -1
  277. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-c822f55b884d9104.js +0 -1
  278. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-fa4ccfc432424e52.js +0 -1
  279. package/apps/pwa/out/_next/static/chunks/app/page-9c7e8f744a765daf.js +0 -1
  280. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-4292d0149020e8a7.js +0 -1
  281. package/apps/pwa/out/_next/static/chunks/app/settings/page-db14d0be7f1c460c.js +0 -1
  282. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-e5d8072cd7cfef73.js +0 -1
  283. package/apps/pwa/out/_next/static/chunks/app/share/page-e6dcb83265530636.js +0 -1
  284. package/apps/pwa/out/_next/static/chunks/app/showroom/page-45248f08d6aecae5.js +0 -1
  285. package/apps/pwa/out/_next/static/chunks/app/vault/page-4344faf20216233b.js +0 -1
  286. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-698bb29f11864b89.js +0 -1
  287. package/apps/pwa/out/_next/static/chunks/app/workflows/page-bd6a6dc1ddc273f2.js +0 -1
  288. /package/apps/pwa/out/_next/static/{-6XrAoD3eU7NyMHAE4xPS → D51l8jUBplJrIpWvENsDK}/_buildManifest.js +0 -0
  289. /package/apps/pwa/out/_next/static/{-6XrAoD3eU7NyMHAE4xPS → D51l8jUBplJrIpWvENsDK}/_ssgManifest.js +0 -0
@@ -1,4 +1,10 @@
1
- import type { Command } from 'commander';
1
+ import { Option, type Command } from 'commander';
2
+ import { hqCliWriteAllowed, type HqDeps } from '../hq/hq.js';
3
+ import { fileLeaseStore } from '../hq/lease.js';
4
+ import { join } from 'node:path';
5
+ import { getElanousConfigDirOverride } from '../elanous-config-dir.js';
6
+ import { effectiveInstanceRoot } from '../instance/resolve.js';
7
+ import { getUserConfig } from '../user-config.js';
2
8
  import { projectDecisionsToLinear } from '../decisions/decision-linear-projection.js';
3
9
  import { DecisionLedger, importDecisionMarkdown, type DecisionCategory, type DecisionLedgerOptions, type DecisionOption, type DecisionTrack, type DecisionEntry } from '../decisions/decision-ledger.js';
4
10
 
@@ -46,13 +52,19 @@ export function formatDecisionDetail(e: DecisionEntry): string {
46
52
  ...(e.refs?.length ? [`참조: ${e.refs.join(' · ')}`] : [])].join('\n');
47
53
  }
48
54
 
49
- export function registerDecisionsCommands(program: Command, config: DecisionLedgerOptions = {}, out: Pick<Console, 'log'> = console): void {
50
- const root = program.command('decisions').description('대표 결정 원장 · 로컬 전용');
55
+ export function registerDecisionsCommands(program: Command, config: DecisionLedgerOptions = {}, out: Pick<Console, 'log'> = console, hqDeps: HqDeps = {}): void {
56
+ const noXcheck = new Option('--no-xcheck <이유>', '교차 확인 생략 이유');
57
+ // This flag takes a reason, not Commander's negated boolean default.
58
+ noXcheck.negate = false;
59
+ const root = program.command('decisions').description('대표 결정 원장 · 로컬 전용').option('--hq-override', '본부 임대 거부를 관측하며 수동 우회');
60
+ const mayWrite = (command: string, override?: boolean) => hqCliWriteAllowed(`decisions ${command}`, Boolean(override || root.opts().hqOverride),
61
+ (getElanousConfigDirOverride() || config.stateDir) && !hqDeps.store && !getUserConfig().hq?.arbiter ? { ...hqDeps, store: fileLeaseStore(join(config.stateDir ?? effectiveInstanceRoot(), 'hq', 'lease.json')), seenPath: join(config.stateDir ?? effectiveInstanceRoot(), 'hq', 'seen-generation'), localPath: join(config.stateDir ?? effectiveInstanceRoot(), 'hq', 'local.json') } : hqDeps);
51
62
  const emit = (value: unknown, json?: boolean, text?: string) => out.log(json ? JSON.stringify(value) : text ?? JSON.stringify(value));
52
63
  const fail = (action: () => void) => { try { action(); } catch (e) { throw new Error(`decisions: ${e instanceof Error ? e.message : String(e)}`); } };
53
64
  root.command('linear-sync').description('열린 결정을 COO Linear 프로젝트에 투영하고 닫힌 결정을 동기화한다')
54
- .option('--dry-run').option('--json')
55
- .action(async (o: { dryRun?: boolean; json?: boolean }) => {
65
+ .option('--dry-run').option('--json').option('--hq-override')
66
+ .action(async (o: { dryRun?: boolean; json?: boolean; hqOverride?: boolean }) => {
67
+ if (!o.dryRun && !mayWrite('linear-sync', o.hqOverride)) return;
56
68
  const result = await projectDecisionsToLinear({ stateDir: config.stateDir, dryRun: o.dryRun });
57
69
  emit(result, o.json, result.reason ?? `생성 ${result.created} · 닫음 ${result.closed} · 건너뜀 ${result.skipped} · 실패 ${result.failed}${result.plan?.length ? `\n${result.plan.map(p => `${p.action} ${p.decisionId}${p.issue ? ` ${p.issue}` : ''}`).join('\n')}` : ''}`);
58
70
  });
@@ -61,21 +73,36 @@ export function registerDecisionsCommands(program: Command, config: DecisionLedg
61
73
  .requiredOption('--s <text>').requiredOption('--c <text>').option('--q <text>').option('--a <text>')
62
74
  .requiredOption('--option <key=label:consequence>', '선택지 (두 번 이상)', repeat, [] as string[])
63
75
  .option('--recommend <key>').option('--why <text>').option('--skip-recommend <reason>')
76
+ .option('--xcheck <SEAT:메모>', '교차 확인 (반복)', repeat, [] as string[])
77
+ .addOption(noXcheck)
78
+ .option('--alternative <key>').option('--dissent <text>')
64
79
  .option('--track <track>').option('--agent <agent>').option('--session <id>')
65
80
  .option('--resume-question <qid>').option('--run <runId>')
66
81
  .option('--ref <url>', '참조 (반복)', repeat, [] as string[])
67
82
  .option('--due <when>', '기한 — UTC ISO(2026-10-04T09:00:00Z) 또는 +Nh(지금부터 N시간) · 기한 2시간 전 텔레그램·디스코드로 다시 알린다')
68
- .option('--json')
69
- .action((o: { due?: string; title: string; category: DecisionCategory; s: string; c: string; q?: string; a?: string; option: string[]; recommend?: string; why?: string; skipRecommend?: string; track?: DecisionTrack; agent?: string; session?: string; resumeQuestion?: string; run?: string; ref: string[]; json?: boolean }) => fail(() => {
83
+ .option('--json').option('--hq-override')
84
+ .action((o: { due?: string; title: string; category: DecisionCategory; s: string; c: string; q?: string; a?: string; option: string[]; recommend?: string; why?: string; skipRecommend?: string; xcheck: string[]; noXcheck?: string; alternative?: string; dissent?: string; track?: DecisionTrack; agent?: string; session?: string; resumeQuestion?: string; run?: string; ref: string[]; json?: boolean; hqOverride?: boolean }) => fail(() => {
70
85
  if (o.run !== undefined && o.resumeQuestion === undefined) throw new Error('--run requires --resume-question');
71
86
  if (o.skipRecommend !== undefined && (o.recommend !== undefined || o.why !== undefined)) throw new Error('choose recommendation or skip, not both');
72
87
  if (o.skipRecommend === undefined && (!o.recommend || !o.why)) throw new Error('--recommend and --why required, or --skip-recommend <reason>');
73
88
  const options: DecisionOption[] = o.option.map(parseOption);
89
+ if (o.xcheck.length && o.noXcheck !== undefined) throw new Error('choose --xcheck or --no-xcheck, not both');
90
+ const crossCheck = o.xcheck.map(raw => {
91
+ const match = /^([^:\s]+):(.+)$/.exec(raw);
92
+ if (!match || !match[2]!.trim()) throw new Error(`invalid --xcheck: ${raw} (expected SEAT:메모)`);
93
+ return { seat: match[1]!, at: new Date().toISOString(), note: match[2]!.trim() };
94
+ });
95
+ if (!crossCheck.length && o.noXcheck === undefined && getUserConfig().decisions?.requireCrossCheck) throw new Error('교차 확인 없음 — --xcheck SEAT:메모 또는 --no-xcheck 이유');
96
+ const who = agent(o.agent);
97
+ if (!mayWrite('raise', o.hqOverride)) return;
74
98
  const entry = new DecisionLedger(config).raise({ title: o.title, category: o.category, scqa: { s: o.s, c: o.c, ...(o.q ? { q: o.q } : {}), ...(o.a ? { a: o.a } : {}) }, options,
99
+ ...(crossCheck.length ? { crossCheck } : { crossCheckSkipped: o.noXcheck ?? 'missing' }),
100
+ ...(o.alternative !== undefined ? { alternative: o.alternative } : {}), ...(o.dissent !== undefined ? { dissent: o.dissent } : {}),
75
101
  recommendation: o.skipRecommend !== undefined ? { skipped: true, reason: o.skipRecommend } : { option: o.recommend!, why: o.why! },
76
- raisedBy: { agent: agent(o.agent), ...(o.track ? { track: o.track } : {}), ...(o.session ? { session: o.session } : {}) }, ...(o.ref.length ? { refs: o.ref } : {}),
102
+ raisedBy: { agent: who, ...(o.track ? { track: o.track } : {}), ...(o.session ? { session: o.session } : {}) }, ...(o.ref.length ? { refs: o.ref } : {}),
77
103
  ...(o.resumeQuestion !== undefined ? { resume: { questionId: o.resumeQuestion, ...(o.run !== undefined ? { runId: o.run } : {}) } } : {}),
78
104
  ...(o.due ? { dueAt: dueAt(o.due) } : {}) });
105
+ if (entry.crossCheckSkipped === 'missing') console.error('교차 확인 없음 — --xcheck SEAT:메모 또는 --no-xcheck 이유');
79
106
  emit(entry, o.json, `올림: ${formatDecisionRow(entry)}`);
80
107
  }));
81
108
  root.command('list').description('결정 목록 (기본 열린 것)')
@@ -88,11 +115,12 @@ export function registerDecisionsCommands(program: Command, config: DecisionLedg
88
115
  root.command('show <id>').description('SCQA · 선택지 · 권고 · 이력 · 판').option('--json')
89
116
  .action((id: string, o: { json?: boolean }) => fail(() => { const entry = new DecisionLedger(config).show(id); emit(entry, o.json, formatDecisionDetail(entry)); }));
90
117
  root.command('decide <id> <option>').description('사람 또는 AUTO 위임 결정 기록')
91
- .option('--note <text>').option('--auto').option('--delegation <reason>').option('--track <track>').option('--agent <agent>').option('--json')
92
- .action((id: string, choice: string, o: { note?: string; auto?: boolean; delegation?: string; track?: DecisionTrack; agent?: string; json?: boolean }) => fail(() => {
118
+ .option('--note <text>').option('--auto').option('--delegation <reason>').option('--track <track>').option('--agent <agent>').option('--json').option('--hq-override')
119
+ .action((id: string, choice: string, o: { note?: string; auto?: boolean; delegation?: string; track?: DecisionTrack; agent?: string; json?: boolean; hqOverride?: boolean }) => fail(() => {
93
120
  if (o.auto && !o.delegation?.trim()) throw new Error('--auto requires --delegation');
94
121
  if (!o.auto && (o.delegation || o.track || o.agent)) throw new Error('delegation/track/agent require --auto');
95
122
  const by = o.auto ? { kind: 'auto' as const, agent: agent(o.agent), delegation: o.delegation!, ...(o.track ? { track: o.track } : {}) } : { kind: 'human' as const };
123
+ if (!mayWrite('decide', o.hqOverride)) return;
96
124
  const { entry, delivery } = new DecisionLedger(config).decideWithDelivery(id, choice, by, o.note);
97
125
  if (delivery && !delivery.ok) {
98
126
  // The decision is recorded; only the answer to the waiting run failed — say so and how to retry, never «success».
@@ -103,22 +131,26 @@ export function registerDecisionsCommands(program: Command, config: DecisionLedg
103
131
  }
104
132
  emit(delivery ? { ...entry, delivery } : entry, o.json, `결정: ${formatDecisionDetail(entry)}`);
105
133
  }));
106
- root.command('retry-answer <id>').description('기록된 결정의 대기 질문 답 전달만 재시도한다').option('--json')
107
- .action((id: string, o: { json?: boolean }) => fail(() => {
134
+ root.command('retry-answer <id>').description('기록된 결정의 대기 질문 답 전달만 재시도한다').option('--json').option('--hq-override')
135
+ .action((id: string, o: { json?: boolean; hqOverride?: boolean }) => fail(() => {
136
+ if (!mayWrite('retry-answer', o.hqOverride)) return;
108
137
  const entry = new DecisionLedger(config).retryAnswer(id);
109
138
  emit(entry, o.json, `답 전달: ${entry.id} → ${entry.resume?.questionId}`);
110
139
  }));
111
140
  root.command('add-options <id>').description('선택지가 미기재된 과거 항목에 확인된 선택지를 추가')
112
141
  .requiredOption('--option <key=label:consequence>', '선택지 (두 번 이상)', repeat, [] as string[])
113
- .option('--agent <agent>').option('--json')
114
- .action((id: string, o: { option: string[]; agent?: string; json?: boolean }) => fail(() => {
115
- const entry = new DecisionLedger(config).addOptions(id, o.option.map(parseOption), agent(o.agent));
142
+ .option('--agent <agent>').option('--json').option('--hq-override')
143
+ .action((id: string, o: { option: string[]; agent?: string; json?: boolean; hqOverride?: boolean }) => fail(() => {
144
+ const who = agent(o.agent);
145
+ if (!mayWrite('add-options', o.hqOverride)) return;
146
+ const entry = new DecisionLedger(config).addOptions(id, o.option.map(parseOption), who);
116
147
  emit(entry, o.json, formatDecisionDetail(entry));
117
148
  }));
118
- root.command('withdraw <id>').description('결정 철회도 원장에 남긴다').requiredOption('--reason <reason>').option('--json')
119
- .action((id: string, o: { reason: string; json?: boolean }) => fail(() => { const entry = new DecisionLedger(config).withdraw(id, o.reason); emit(entry, o.json, `철회: ${formatDecisionDetail(entry)}`); }));
120
- root.command('import-markdown <file>').description('기존 대표 결정 문서 씨앗을 가져온다 (재실행 안전)').option('--json')
121
- .action((file: string, o: { json?: boolean }) => fail(() => {
149
+ root.command('withdraw <id>').description('결정 철회도 원장에 남긴다').requiredOption('--reason <reason>').option('--json').option('--hq-override')
150
+ .action((id: string, o: { reason: string; json?: boolean; hqOverride?: boolean }) => fail(() => { if (!mayWrite('withdraw', o.hqOverride)) return; const entry = new DecisionLedger(config).withdraw(id, o.reason); emit(entry, o.json, `철회: ${formatDecisionDetail(entry)}`); }));
151
+ root.command('import-markdown <file>').description('기존 대표 결정 문서 씨앗을 가져온다 (재실행 안전)').option('--json').option('--hq-override')
152
+ .action((file: string, o: { json?: boolean; hqOverride?: boolean }) => fail(() => {
153
+ if (!mayWrite('import-markdown', o.hqOverride)) return;
122
154
  const result = importDecisionMarkdown(new DecisionLedger(config), file);
123
155
  emit(result, o.json, `가져옴 ${result.imported.length} · 기존 ${result.existing.length} · 불완전 ${result.incomplete.length} · 못 읽음 ${result.unread.length}${result.incomplete.length ? `\n${result.incomplete.join('\n')}` : ''}${result.unread.length ? `\n${result.unread.join('\n')}` : ''}`);
124
156
  if (result.unread.length) process.exitCode = 1;
@@ -0,0 +1,92 @@
1
+ import { accessSync, constants, readdirSync, statSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import type { Command } from 'commander';
4
+ import { debug } from '../debug/log.js';
5
+ import { effectiveInstanceRoot } from '../instance/resolve.js';
6
+ import type { OutputEntry } from '../outputs/ledger.js';
7
+ import { writeStdoutJson } from './stdout-json.js';
8
+
9
+ type ListOptions = { since?: string; source?: string; limit?: string; json?: boolean };
10
+
11
+ function sinceIso(value: string): string {
12
+ if (value === '24h' || value === '7d') {
13
+ return new Date(Date.now() - (value === '24h' ? 24 : 7 * 24) * 60 * 60 * 1000).toISOString();
14
+ }
15
+ if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?(?:Z|[+-]\d{2}:\d{2})$/.test(value)
16
+ || !Number.isFinite(Date.parse(value))
17
+ || new Date(`${value.slice(0, 10)}T00:00:00Z`).toISOString().slice(0, 10) !== value.slice(0, 10)) {
18
+ throw new Error(`invalid --since: ${value} (ISO|24h|7d)`);
19
+ }
20
+ return new Date(value).toISOString();
21
+ }
22
+
23
+ // listOutputs intentionally tolerates unreadable months. Detect inaccessible ledger files here so the CLI
24
+ // does not report an incomplete read as a genuine zero (including when run as root under chmod 000).
25
+ function assertReadableLedger(root: string): void {
26
+ const dir = join(root, 'outputs');
27
+ let names: string[];
28
+ try {
29
+ const info = statSync(dir);
30
+ if (!info.isDirectory() || (info.mode & 0o444) === 0 || (info.mode & 0o111) === 0) {
31
+ throw new Error(`outputs ledger directory unreadable: ${dir}`);
32
+ }
33
+ accessSync(dir, constants.R_OK | constants.X_OK);
34
+ names = readdirSync(dir);
35
+ } catch (error) {
36
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return;
37
+ throw error;
38
+ }
39
+ for (const name of names.filter(name => /^outputs-\d{4}-(0[1-9]|1[0-2])\.jsonl$/.test(name) || name === 'outputs.jsonl')) {
40
+ const file = join(dir, name);
41
+ const info = statSync(file);
42
+ if (!info.isFile() || (info.mode & 0o444) === 0) throw new Error(`outputs ledger file unreadable: ${file}`);
43
+ accessSync(file, constants.R_OK);
44
+ }
45
+ }
46
+
47
+ export function registerOutputsCommands(program: Command): void {
48
+ program.command('outputs').description('산출물 원장 조회 (읽기 전용)')
49
+ .command('list').description('산출물 최신순 목록과 종류별 수')
50
+ .option('--since <ISO|24h|7d>', '이 시각 이후')
51
+ .option('--source <source>', 'exec|field-reel|field-feed')
52
+ .option('--limit <N>', '최대 건수 (기본 50)', '50')
53
+ .option('--json', 'JSON 출력')
54
+ .action(async (options: ListOptions) => {
55
+ try {
56
+ if (options.source !== undefined && !['exec', 'field-reel', 'field-feed'].includes(options.source)) {
57
+ throw new Error(`invalid --source: ${options.source} (exec|field-reel|field-feed)`);
58
+ }
59
+ if (!options.limit || !/^[1-9]\d*$/.test(options.limit) || !Number.isSafeInteger(Number(options.limit))) {
60
+ throw new Error(`invalid --limit: ${options.limit}`);
61
+ }
62
+ const scope = {
63
+ since: options.since === undefined ? undefined : sinceIso(options.since),
64
+ source: options.source as OutputEntry['source'] | undefined,
65
+ limit: Number(options.limit),
66
+ };
67
+ const displayedScope = { since: scope.since ?? null, source: scope.source ?? null, limit: scope.limit };
68
+ const root = effectiveInstanceRoot();
69
+ assertReadableLedger(root);
70
+ const { listOutputs } = await import('../outputs/ledger.js');
71
+ const items = listOutputs(scope, root);
72
+ const byKind: Record<string, number> = {};
73
+ for (const item of items) byKind[item.kind] = (byKind[item.kind] ?? 0) + 1;
74
+ debug.log('outputs.cli', 'list', { count: items.length, source: scope.source, since: scope.since });
75
+ if (options.json) {
76
+ await writeStdoutJson(`${JSON.stringify({ items, count: items.length, byKind, scope: displayedScope })}\n`);
77
+ return;
78
+ }
79
+ for (const item of items) {
80
+ const at = Number.isFinite(Date.parse(item.at))
81
+ ? new Intl.DateTimeFormat('sv-SE', { timeZone: 'Asia/Seoul', dateStyle: 'short', timeStyle: 'medium', hourCycle: 'h23' }).format(new Date(item.at))
82
+ : item.at;
83
+ console.log(`${at} KST · ${item.kind} · ${item.title} · ${item.source}/${item.sourceId} · ${item.seat ?? '—'} · ${item.path ?? item.url ?? '—'}`);
84
+ }
85
+ if (!items.length) console.log(`scope: ${JSON.stringify(displayedScope)}`);
86
+ console.log(`${items.length}개 · 종류별 수 ${JSON.stringify(byKind)}`);
87
+ } catch (error) {
88
+ console.error(`outputs list: ${error instanceof Error ? error.message : String(error)}`);
89
+ process.exitCode = 1;
90
+ }
91
+ });
92
+ }
@@ -9,6 +9,8 @@ import { parsePersonaYaml } from '../persona/loader.js';
9
9
  import { resolveRepositoryPersonaDir, resolveStatePersonaDir } from '../persona/global-registry.js';
10
10
  import { UnknownPersonaSchemaKeyError, choosePersonaId, clonePreset, describePreset, editPersona, findPreset, loadPresetIndex, loadPresets, presetToProfileYaml } from '../persona/presets.js';
11
11
  import { addTeamSet, listTeamSets, removeTeamSet } from '../persona/team-set.js';
12
+ import { validateTeamFile } from '../persona/team.js';
13
+ import { runPersonaLoopOnce } from '../seat-loop/seat-loop.js';
12
14
 
13
15
  interface StoredPersona { personaId: string; displayName: string; description?: string; preset?: string; title?: string; path: string }
14
16
 
@@ -68,6 +70,18 @@ function editOptions(fromFile: string | undefined, assignments: readonly string[
68
70
  }
69
71
 
70
72
  export function registerPersonaCommands(program: Command): void {
73
+ program.command('team').description('Validate a YAML team definition')
74
+ .command('validate <file>').description('Check team schema, persona references and handoff order')
75
+ .action((file: string) => {
76
+ try {
77
+ const definition = validateTeamFile(file);
78
+ console.log(`Valid team ${definition.name}`);
79
+ } catch (error) {
80
+ console.error(error instanceof Error ? error.message.replace(/\s+/g, ' ').trim() : String(error));
81
+ process.exitCode = 2;
82
+ }
83
+ });
84
+
71
85
  const team = program.command('team-set').description('Install, remove and list persona team sets');
72
86
  team.command('add <file>').description('Install a YAML team set').action((file: string) => {
73
87
  try { const result = addTeamSet(file); console.log(`Installed team-set ${result['team-set']}`); }
@@ -178,6 +192,21 @@ export function registerPersonaCommands(program: Command): void {
178
192
  }
179
193
  });
180
194
 
195
+ persona.command('loop <name>')
196
+ .description('Shadow-pick one todo for a saved persona without executing it')
197
+ .requiredOption('--once', 'Pick exactly one todo')
198
+ .option('--json', 'JSON output')
199
+ .action(async (name: string, opts: { json?: boolean }) => {
200
+ try {
201
+ const entry = await runPersonaLoopOnce(name);
202
+ if (opts.json) console.log(JSON.stringify(entry));
203
+ else console.log(entry.todo ? `그림자 선택 — ${entry.todo.title} (${entry.todo.id})` : '할 일 없음');
204
+ } catch (error) {
205
+ console.error(error instanceof Error ? error.message : String(error));
206
+ process.exitCode = 1;
207
+ }
208
+ });
209
+
181
210
  persona.command('show <name>')
182
211
  .description('Show one of your personas (id or name), or a preset')
183
212
  .option('--json', 'JSON output')
@@ -6,9 +6,10 @@ import { debug } from '../debug/log.js';
6
6
  import { readPodMemoryAdvice } from './pod-memory-advice.js';
7
7
  import { getUserConfig } from '../user-config.js';
8
8
  import { listCodexAccountsInStore } from '../oauth/codex-account-store.js';
9
- import { parsePodPool, resolvePodPoolSpec, type PoolKubectl } from '../task-orchestrator/surfaces/pod-pool.js';
9
+ import { parsePodPool, resolvePodPoolSpec, podPoolHostLease, type PoolKubectl } from '../task-orchestrator/surfaces/pod-pool.js';
10
10
  import { leaseKubectl, measurePoolLease, probePoolDns, recommendConcurrency, type PoolLeaseMeasure, type PoolDnsProbe } from '../task-orchestrator/surfaces/pod-lease.js';
11
11
  import { POD_COMMAND_DEADLINE_SECONDS, runPodCommand, type PodCommandResult, type RunPodCommandOptions } from '../task-orchestrator/surfaces/pod-command-job.js';
12
+ import { hostLeaseCounts, leaseHasPendingPod } from '../pod-lease/host-lease.js';
12
13
 
13
14
  export interface PodCliIo {
14
15
  log: (line: string) => void;
@@ -78,8 +79,13 @@ export function registerPodCommands(program: Command, deps: PodCliDeps = {}): vo
78
79
  const perAccount = (deps.perAccount ?? (() => getUserConfig().pod?.lease?.perAccount ?? 4))();
79
80
  const decision = recommendConcurrency(measure, { capacity: members.reduce((sum, m) => sum + m.capacity, 0), accounts: accounts ?? -1, perAccount });
80
81
  if (!context) { decision.recommended = null; decision.limitedBy = null; decision.reason = '측정 불가: cluster (current context)'; }
81
- const result = { pool: spec, ...measure, accounts, perAccount, ...decision };
82
- debug.log('pod.lease', 'status', { pool: spec ? members.map((m) => ({ capacity: m.capacity })) : null, running: decision.running, pending: decision.pending, unleasedRunning: decision.unleasedRunning, recommended: decision.recommended, limitedBy: decision.limitedBy });
82
+ const records = members.length ? podPoolHostLease(members).live() : [];
83
+ const host = hostLeaseCounts(records);
84
+ // Pending Pods and Job-only reservations are disjoint; a lease for an already Pending Job is the same Job.
85
+ const waitingJobs = decision.pending === null ? null : decision.pending +
86
+ records.filter((r) => r.stage === 'job' && !leaseHasPendingPod(r, decision.pendingJobs ?? [])).length;
87
+ const result = { pool: spec, ...measure, accounts, perAccount, ...decision, reserved: host.reserved, waitingJobs };
88
+ debug.log('pod.lease', 'status', { pool: spec ? members.map((m) => ({ capacity: m.capacity })) : null, running: decision.running, pending: decision.pending, reserved: host.reserved, waitingJobs, unleasedRunning: decision.unleasedRunning, recommended: decision.recommended, limitedBy: decision.limitedBy });
83
89
  if (opts.json) io.log(JSON.stringify(result));
84
90
  else {
85
91
  io.log(`풀: ${(spec ?? context) || '?'}`);
@@ -89,6 +95,7 @@ export function registerPodCommands(program: Command, deps: PodCliDeps = {}): vo
89
95
  io.log(`${m.context} | ${m.capacity} | ${m.running ?? '?'} | ${m.pending ?? '?'} | ${mem(m.memoryLimitBytes)}/${mem(m.allocatableMemoryBytes)} | ${m.allocatableCpuMillicores === null ? '?' : m.allocatableCpuMillicores / 1000}${m.reason ? ` (${m.reason})` : ''}`);
90
96
  }
91
97
  io.log(`권장 지금 ${decision.recommended ?? '?'} 개 더 (limitedBy=${decision.limitedBy ?? 'unknown'})${decision.reason ? ` · ${decision.reason}` : ''}`);
98
+ io.log(`예약(저작 중) ${host.reserved} · 대기 Job ${waitingJobs ?? '?'} · 실행 ${decision.running ?? '?'}`);
92
99
  io.log(`임대 없는 실행 ${decision.unleasedRunning ?? '?'}`);
93
100
  io.log(`capacity: ${decision.capacitySlots ?? '?'} 칸 (상한 ${members.reduce((sum, m) => sum + m.capacity, 0)} − Running ${decision.running ?? '?'} − Pending ${decision.pending ?? '?'}; 권장 수에서 건강한 멤버의 임대 없는 실행 차감)`);
94
101
  io.log(`memory: ${decision.memorySlots ?? '?'} 칸 (할당 가능 − Running 하니스 상한 합; 골당 16Gi · 모든 네임스페이스의 노드별 기존 예약을 반영해 배치 가능 ${decision.placeableSlots ?? '?'} 칸)`);
package/src/cli/pr-cli.ts CHANGED
@@ -53,6 +53,7 @@ import {
53
53
  } from './pr-granularity.js';
54
54
  import { branchLineageSlug, findSiblingPrs } from './pr-lineage.js';
55
55
  import { parseReleaseNoteSection } from '../release-loop/release-note.js';
56
+ import { enqueueL8ShadowQueue, runL8ShadowQueue, shadowPrNumber, statusL8ShadowQueue, type L8ShadowQueueDeps } from '../self-implement/l8-merge-queue.js';
56
57
 
57
58
  export { branchLineageSlug, findSiblingPrs };
58
59
 
@@ -1635,8 +1636,21 @@ export async function runPrLand(opts: PrLandOpts = {}, deps: PrLandDeps = {}): P
1635
1636
  return 0;
1636
1637
  }
1637
1638
 
1638
- export function registerPrCommands(program: Command, deps: PrLandDeps = {}): void {
1639
+ export function registerPrCommands(program: Command, deps: PrLandDeps = {}, queueDeps: L8ShadowQueueDeps = {}): void {
1639
1640
  const pr = program.command('pr').description('PR landing commands');
1641
+ const queue = pr.command('queue').description('Shadow merge queue (never lands PRs)');
1642
+ const queueAction = async (action: () => Promise<unknown>) => {
1643
+ try {
1644
+ (deps.out ?? liveOut).log(JSON.stringify(await action()));
1645
+ } catch (error) {
1646
+ (deps.out ?? liveOut).error(`✗ merge-queue: ${String(error)}`);
1647
+ (deps.setExitCode ?? ((code: number) => { process.exitCode = code; }))(1);
1648
+ }
1649
+ };
1650
+ queue.command('status').description('Show pending PRs and shadow verdicts').action(() => queueAction(() => statusL8ShadowQueue(process.cwd(), queueDeps)));
1651
+ queue.command('enqueue <pr>').description('Pin a ready PR at the tail of the shadow queue').action((prNumber: string) => queueAction(() => enqueueL8ShadowQueue(process.cwd(), shadowPrNumber(prNumber), queueDeps)));
1652
+ queue.command('run').description('Evaluate the first PR without merging').requiredOption('--shadow', 'Only evaluate; never land').action(() => queueAction(() => runL8ShadowQueue(process.cwd(), queueDeps)));
1653
+
1640
1654
  pr.command('land')
1641
1655
  .description('현재 브랜치 PR을 생성 또는 갱신해 ready 상태로 만들고 squash merge')
1642
1656
  .option('--base <branch>', 'PR base branch')
@@ -23,13 +23,15 @@ import { debug } from '../debug/log.js';
23
23
  import { effectiveInstanceRoot, releaseLedgerRoot } from '../instance/resolve.js';
24
24
  import { getElanousConfigDirOverride } from '../elanous-config-dir.js';
25
25
  import { envLiteral } from '../platform/env-literal.js';
26
- import { userConfigPath } from '../user-config.js';
26
+ import { getUserConfig, userConfigPath } from '../user-config.js';
27
27
  import { addItem, claimItem, decodeClaimHistoryEntry, devVersion, listChecklist, ownerMatches, parseOwner, parityGap, removeItem, seedFromRoadmap, setItem, summarizeChecklist, type ChecklistStatus, type ChecklistDisposition, type ChecklistKind } from '../release-loop/checklist.js';
28
28
  import * as features from '../release-loop/feature-store.js';
29
- import { landedButYellow, type MergedChecklistPr } from '../release-loop/landed-but-yellow.js';
29
+ import { evidencePlan, landedButYellow, type MergedChecklistPr } from '../release-loop/landed-but-yellow.js';
30
30
  import { formatSchedule, getSchedule, listSchedules, setSchedule } from '../release-loop/release-schedule.js';
31
31
  import { writeStdoutJson } from './stdout-json.js';
32
32
  import { CliUserError } from './cli-user-error.js';
33
+ import { hqCliWriteAllowed, type HqDeps } from '../hq/hq.js';
34
+ import { fileLeaseStore } from '../hq/lease.js';
33
35
  import { runUnattendedRelease, type UnattendedReleaseDeps } from '../../scripts/release-loop/unattended-release.js';
34
36
  import { cutReleaseBranch } from '../../scripts/release-loop/cut-branch.js';
35
37
  import { releaseReadiness } from '../../scripts/release-loop/release-readiness.js';
@@ -504,12 +506,44 @@ export interface LandedButYellowDeps {
504
506
  run?: Runner;
505
507
  checklist?: typeof listChecklist;
506
508
  now?: () => Date;
509
+ evidenceAdd?: typeof features.evidenceAdd;
510
+ schedules?: typeof features.readSchedules;
511
+ released?: typeof features.releasedVersion;
507
512
  }
508
513
 
509
- export function registerReleaseCommands(program: Command, releaseRunDeps: UnattendedReleaseDeps = {}, landedDeps: LandedButYellowDeps = {}): void {
514
+ function printLandedRows(rows: ReturnType<typeof landedButYellow>, items: ReturnType<typeof listChecklist>['items']): void {
515
+ const updatedAtById = new Map(items.map((item) => [item.id, item.updatedAt]));
516
+ const date = (iso: string) => {
517
+ const parts = new Intl.DateTimeFormat('en-US', { timeZone: 'Asia/Seoul', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hourCycle: 'h23' }).formatToParts(new Date(iso));
518
+ const part = (name: string) => parts.find((entry) => entry.type === name)?.value;
519
+ return `${part('month')}-${part('day')} ${part('hour')}:${part('minute')}`;
520
+ };
521
+ for (const row of rows) {
522
+ const icon = row.status === 'yellow' ? '🟡' : '🔴';
523
+ console.log(`${row.id} ${row.owner ?? '-'} ${icon} ← ${row.prs.map((pr, i) => `#${pr.number} «${pr.title}» (${date(pr.mergedAt)} · 근거에 ${row.alreadyInEvidence[i] ? '있음' : '없음'}${pr.mergedAt > (updatedAtById.get(row.id) ?? '') ? ' · 칸보다 새것' : ''})`).join(' · ')}`);
524
+ }
525
+ console.log(`${rows.length}칸 · 그중 근거에 없는 PR 이 있는 칸 ${rows.filter((row) => row.alreadyInEvidence.includes(false)).length}`);
526
+ }
527
+
528
+ function compareReleaseVersions(a: string, b: string): number {
529
+ const parse = (version: string) => {
530
+ const [core, pre] = version.split('-');
531
+ const [major, minor, patch] = core!.split('.').map(Number);
532
+ const [stage, sequence] = pre?.split('.') ?? [];
533
+ return [major!, minor!, patch!, stage === undefined ? 3 : ({ alpha: 0, beta: 1, rc: 2 } as Record<string, number>)[stage]!, Number(sequence ?? 0)];
534
+ };
535
+ const left = parse(a), right = parse(b);
536
+ for (let i = 0; i < left.length; i++) if (left[i] !== right[i]) return left[i]! - right[i]!;
537
+ return 0;
538
+ }
539
+
540
+ export function registerReleaseCommands(program: Command, releaseRunDeps: UnattendedReleaseDeps = {}, landedDeps: LandedButYellowDeps = {}, hqDeps: HqDeps = {}): void {
510
541
  const release = program.command('release').description('공개 배포 한 판 — prepare(로컬) → publish(--yes) → verify (docs/manual/MANUAL-versioning-and-release-2026-09-25.md)');
511
542
  const schedule = release.command('schedule').description('판별 컷·착지 마감 조회·갱신');
512
543
  const scheduleLedgerRoot = () => releaseRunDeps.ledgerRoot ?? releaseLedgerRoot();
544
+ const ledgerFenceDeps = (): HqDeps => getElanousConfigDirOverride() && !hqDeps.store && !getUserConfig().hq?.arbiter
545
+ ? { ...hqDeps, store: fileLeaseStore(join(effectiveInstanceRoot(), 'hq', 'lease.json')), seenPath: join(effectiveInstanceRoot(), 'hq', 'seen-generation'), localPath: join(effectiveInstanceRoot(), 'hq', 'local.json') }
546
+ : hqDeps;
513
547
  schedule.command('list').option('--json', '결과 JSON').action((o: { json?: boolean }) => {
514
548
  const rows = listSchedules(scheduleLedgerRoot());
515
549
  if (o.json) console.log(JSON.stringify(rows));
@@ -522,21 +556,25 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
522
556
  if (o.json) console.log(JSON.stringify(row)); else console.log(formatSchedule(row));
523
557
  });
524
558
  schedule.command('set').requiredOption('--version <v>', '갱신할 판')
525
- .option('--cut-at <iso>', '오프셋 포함 컷 시각').option('--land-by <iso>', '오프셋 포함 착지 마감').option('--json', '결과 JSON')
526
- .action((o: { version: string; cutAt?: string; landBy?: string; json?: boolean }) => {
559
+ .option('--cut-at <iso>', '오프셋 포함 컷 시각').option('--land-by <iso>', '오프셋 포함 착지 마감').option('--json', '결과 JSON').option('--hq-override')
560
+ .action((o: { version: string; cutAt?: string; landBy?: string; json?: boolean; hqOverride?: boolean }) => {
527
561
  if (o.cutAt === undefined && o.landBy === undefined) throw new CliUserError('갱신할 시각을 지정하라', '--cut-at <iso> 또는 --land-by <iso>');
562
+ if (!hqCliWriteAllowed('release schedule set', Boolean(o.hqOverride), ledgerFenceDeps())) return;
528
563
  const row = setSchedule(o.version, { cutAt: o.cutAt, landBy: o.landBy }, process.env.ELANOUS_TRACK || 'cli', scheduleLedgerRoot());
529
564
  if (o.json) console.log(JSON.stringify(row)); else console.log(formatSchedule(row));
530
565
  });
531
566
  const checklist = release.command('checklist').description('판별 확인표 조회·갱신')
532
567
  .option('--version <v>', '판 또는 별칭(기본: package.json 의 개발판에서 -dev.N 제거)')
533
- .option('--json', '결과 JSON');
568
+ .option('--json', '결과 JSON')
569
+ .option('--hq-override', '본부 임대 거부를 관측하며 수동 우회');
570
+ const mayWrite = (cmd: Command) => hqCliWriteAllowed(`release checklist ${cmd.name()}`, Boolean(cmd.optsWithGlobals().hqOverride), ledgerFenceDeps());
534
571
  const context = (cmd: Command) => {
535
572
  const opts = { ...(cmd.parent?.parent?.opts() as { version?: string; json?: boolean }), ...(cmd.parent?.opts() as { version?: string; json?: boolean }), ...(cmd.opts() as { version?: string; json?: boolean }) };
536
573
  const codenames = checklistCodenames();
537
574
  return { version: checklistVersion(opts.version, codenames), codenames, json: opts.json };
538
575
  };
539
- const withContext = (cmd: Command) => cmd.option('--version <v>', '판 또는 별칭').option('--json', '결과 JSON');
576
+ const withContext = (cmd: Command) => cmd.option('--version <v>', '판 또는 별칭').option('--json', '결과 JSON').option('--hq-override', '본부 임대 거부를 관측하며 수동 우회');
577
+ const actor = () => process.env.ELANOUS_TRACK || 'cli';
540
578
  withContext(checklist.command('list').description('모든 칸')).option('--owner <owner>', '자리 또는 하위 자리로 거르기').action(async (opts: { owner?: string }, cmd: Command) => {
541
579
  const { version, codenames, json } = context(cmd);
542
580
  await checklistOutput(version, codenames, 'list', json, opts.owner);
@@ -545,32 +583,66 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
545
583
  const { version, codenames, json } = context(cmd);
546
584
  await checklistOutput(version, codenames, 'status', json, opts.owner);
547
585
  });
548
- withContext(checklist.command('landed-but-yellow').description('병합 PR 이 있지만 노랑·빨강인 칸(읽기 전용)'))
586
+ withContext(checklist.command('landed-but-yellow').description('병합 PR 이 있지만 노랑·빨강인 칸(기본 읽기 전용)'))
549
587
  .option('--owner <owner>', '자리 또는 하위 자리로 거르기')
550
- .action(async (opts: { owner?: string }, cmd: Command) => {
551
- const { version, json } = context(cmd);
588
+ .option('--open', '미발행 일정 판 중 칸이 있는 판 전부')
589
+ .option('--apply-evidence', '확실한 PR 짝의 근거를 붙인다(상태는 그대로)')
590
+ .option('--dry-run', '근거 추가 없이 계획만 출력')
591
+ .action(async (opts: { owner?: string; open?: boolean; applyEvidence?: boolean; dryRun?: boolean }, cmd: Command) => {
552
592
  try {
593
+ const inherited = cmd.parent?.opts() as { version?: string; json?: boolean } | undefined;
594
+ const local = cmd.opts() as { version?: string; json?: boolean };
595
+ if (opts.open && (local.version !== undefined || inherited?.version !== undefined)) throw new CliUserError('--open 과 --version 을 함께 쓸 수 없다');
596
+ const json = local.json ?? inherited?.json;
553
597
  if (opts.owner !== undefined) parseOwner(opts.owner);
598
+ const released = opts.open ? (landedDeps.released ?? features.releasedVersion)() : '';
599
+ const openSnapshots = opts.open
600
+ ? (landedDeps.schedules ?? features.readSchedules)()
601
+ .filter(({ version }) => isReleaseVersion(version) && (!released || compareReleaseVersions(version, released) > 0))
602
+ .sort((a, b) => compareReleaseVersions(a.version, b.version))
603
+ .map(({ version }) => (landedDeps.checklist ?? listChecklist)(version)).filter((snapshot) => snapshot.items.length > 0)
604
+ : [];
605
+ if (opts.open && !openSnapshots.length) {
606
+ if (json) await writeStdoutJson('[]\n'); else console.log('열린 판 없음');
607
+ return;
608
+ }
609
+ if (opts.applyEvidence && !opts.dryRun && !mayWrite(cmd)) return;
554
610
  const since = new Date((landedDeps.now ?? (() => new Date()))().getTime() - 7 * 86_400_000).toISOString().slice(0, 10);
555
611
  const raw = must((landedDeps.run ?? defaultRunner)('gh', ['pr', 'list', '--state', 'merged', '--search', `merged:>=${since}`, '--limit', '400', '--json', 'number,title,body,mergedAt'], process.cwd()), '병합 PR 조회(gh)');
556
612
  const prs = JSON.parse(raw) as MergedChecklistPr[];
557
613
  if (!Array.isArray(prs)) throw new Error('병합 PR 조회(gh): 배열이 아니다');
558
- const items = (landedDeps.checklist ?? listChecklist)(version).items;
559
- const rows = landedButYellow(items, prs, { owner: opts.owner });
560
- if (json) await writeStdoutJson(`${JSON.stringify(rows)}\n`);
561
- else {
562
- const updatedAtById = new Map(items.map((item) => [item.id, item.updatedAt]));
563
- const date = (iso: string) => {
564
- const parts = new Intl.DateTimeFormat('en-US', { timeZone: 'Asia/Seoul', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hourCycle: 'h23' }).formatToParts(new Date(iso));
565
- const part = (name: string) => parts.find((entry) => entry.type === name)?.value;
566
- return `${part('month')}-${part('day')} ${part('hour')}:${part('minute')}`;
567
- };
568
- for (const row of rows) {
569
- const icon = row.status === 'yellow' ? '🟡' : '🔴';
570
- console.log(`${row.id} ${row.owner ?? '-'} ${icon} ← ${row.prs.map((pr, i) => `#${pr.number} «${pr.title}» (${date(pr.mergedAt)} · 근거에 ${row.alreadyInEvidence[i] ? '있음' : '없음'}${pr.mergedAt > (updatedAtById.get(row.id) ?? '') ? ' · 칸보다 새것' : ''})`).join(' · ')}`);
614
+ const snapshots = opts.open ? openSnapshots : [(landedDeps.checklist ?? listChecklist)(context(cmd).version)];
615
+ const results: Array<{ version: string; rows?: ReturnType<typeof landedButYellow>; plan?: ReturnType<typeof evidencePlan> }> = [];
616
+ let totalAdded = 0, totalSkipped = 0;
617
+ for (const snapshot of snapshots) {
618
+ const { version, items } = snapshot;
619
+ const rows = landedButYellow(items, prs, { owner: opts.owner });
620
+ const skippedMention = new Set(rows.flatMap((row) => row.prs.flatMap((pr) =>
621
+ pr.basis === 'mention' ? [JSON.stringify([row.id, pr.number])] : []))).size;
622
+ if (opts.open) totalSkipped += skippedMention;
623
+ if (opts.open && !json) console.log(`— ${version} —`);
624
+ if (opts.applyEvidence || opts.dryRun) {
625
+ const plan = evidencePlan(rows);
626
+ const log = json ? console.error : console.log;
627
+ for (const { id, ref } of plan) {
628
+ if (!opts.dryRun) (landedDeps.evidenceAdd ?? features.evidenceAdd)(id, version, ref, actor(), snapshot.released, snapshot.dev);
629
+ log(`${opts.dryRun ? '·' : '✅'} ${id} 근거 ${ref}`);
630
+ }
631
+ const added = opts.dryRun ? 0 : plan.length;
632
+ totalAdded += added;
633
+ log(`붙임 ${added} · 건너뜀(언급만) ${skippedMention}`);
634
+ if (!opts.open && !opts.dryRun) debug.log('release.checklist', 'evidence-synced', { added, skippedMention });
635
+ results.push({ version, plan });
636
+ } else {
637
+ if (!json) printLandedRows(rows, items);
638
+ results.push({ version, rows });
571
639
  }
572
- console.log(`${rows.length}칸 · 그중 근거에 없는 PR 이 있는 칸 ${rows.filter((row) => row.alreadyInEvidence.includes(false)).length}`);
573
640
  }
641
+ if (opts.open) {
642
+ if (opts.applyEvidence && !opts.dryRun) debug.log('release.checklist', 'evidence-synced', { versions: snapshots.map(({ version }) => version), added: totalAdded, skippedMention: totalSkipped });
643
+ if (!json) console.log(`판 ${snapshots.length} · 붙임 합 ${totalAdded} · 건너뜀(언급만) 합 ${totalSkipped}`);
644
+ else await writeStdoutJson(`${JSON.stringify(results)}\n`);
645
+ } else if (json) await writeStdoutJson(`${JSON.stringify(opts.applyEvidence || opts.dryRun ? results[0]!.plan : results[0]!.rows)}\n`);
574
646
  if (prs.length >= 400) console.error('⚠ 병합 PR 400개 상한: 목록이 잘렸을 수 있다');
575
647
  } catch (error) {
576
648
  console.error(`⛔ ${error instanceof Error ? error.message : String(error)}`);
@@ -581,6 +653,7 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
581
653
  .option('--allow-duplicate-id', '다른 판에 같은 id 가 있어도 경고 후 추가')
582
654
  .action((id: string, title: string, opts: { owner?: string; kind?: string; allowDuplicateId?: boolean }, cmd: Command) => {
583
655
  const { version, json } = context(cmd);
656
+ if (!mayWrite(cmd)) return;
584
657
  const data = addItem(version, { id, title, ...(opts.owner !== undefined ? { owner: opts.owner } : {}), ...(opts.kind !== undefined ? { kind: opts.kind as ChecklistKind } : {}) }, { allowDuplicateId: opts.allowDuplicateId });
585
658
  if (json) console.log(JSON.stringify(data)); else console.log(`✅ ${id} 추가`);
586
659
  });
@@ -592,6 +665,7 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
592
665
  if (opts.status !== undefined && !['green', 'yellow', 'red', 'done'].includes(opts.status)) throw new CliUserError(`잘못된 상태: ${opts.status}`, 'green|yellow|red|done');
593
666
  if (opts.disposition !== undefined && !['move', 'known-issue', 'block'].includes(opts.disposition)) throw new CliUserError(`잘못된 처분: ${opts.disposition}`, 'move|known-issue|block');
594
667
  if (opts.status === undefined && opts.evidence === undefined && opts.owner === undefined && opts.disposition === undefined && opts.kind === undefined) throw new CliUserError('갱신할 칸을 지정하라', '--status · --evidence · --owner · --disposition · --kind 중 하나');
668
+ if (!mayWrite(cmd)) return;
595
669
  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 } : {}) }, process.env.ELANOUS_TRACK || 'cli');
596
670
  const item = data.items.find((entry) => entry.id === id);
597
671
  const why = item?.kind === 'screen' && item.status === 'green' ? parityGap(item.evidence) : null;
@@ -605,40 +679,45 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
605
679
  .requiredOption('--by <owner>', '잡을 자리 또는 하위 자리').option('--force', '기존 주인에서 강제로 바꾼다')
606
680
  .action((id: string, opts: { by: string; force?: boolean }, cmd: Command) => {
607
681
  const { version, json } = context(cmd);
682
+ if (!mayWrite(cmd)) return;
608
683
  const data = claimItem(version, id, opts.by, { force: opts.force });
609
684
  if (json) console.log(JSON.stringify(data)); else console.log(`✅ ${id} 주인 ${opts.by}`);
610
685
  });
611
686
  withContext(checklist.command('rm <id>').description('칸 삭제')).action((id: string, _opts: unknown, cmd: Command) => {
612
687
  const { version, json } = context(cmd);
688
+ if (!mayWrite(cmd)) return;
613
689
  const data = removeItem(version, id, process.env.ELANOUS_TRACK || 'cli');
614
690
  if (json) console.log(JSON.stringify(data)); else console.log(`✅ ${id} 삭제`);
615
691
  });
616
692
  withContext(checklist.command('seed').description('마크다운 로드맵에서 빈 칸 들이기')).requiredOption('--from <file>', '로드맵 파일')
617
693
  .action((opts: { from: string }, cmd: Command) => {
618
694
  const { version, json } = context(cmd);
695
+ if (!mayWrite(cmd)) return;
619
696
  const data = seedFromRoadmap(version, readFileSync(opts.from, 'utf8'));
620
697
  if (json) console.log(JSON.stringify(data)); else console.log(`✅ ${version} ${data.items.length}칸`);
621
698
  });
622
- const actor = () => process.env.ELANOUS_TRACK || 'cli';
623
699
  withContext(checklist.command('move <id>').description('칸을 한 트랜잭션으로 다른 판에 옮긴다'))
624
700
  .requiredOption('--from <v>', '현재 판').requiredOption('--to <v>', '새 판').option('--reason <reason>', '이동 사유')
625
701
  .action((id: string, opts: { from: string; to: string; reason?: string }, cmd: Command) => {
626
702
  const { codenames, json } = context(cmd);
627
703
  const from = checklistVersion(opts.from, codenames), to = checklistVersion(opts.to, codenames);
704
+ if (!mayWrite(cmd)) return;
628
705
  features.move(id, from, to, actor(), undefined, undefined, opts.reason);
629
706
  if (json) console.log(JSON.stringify(listChecklist(to))); else console.log(`✅ ${id} ${from} → ${to}`);
630
707
  });
631
708
  withContext(checklist.command('retitle <id> <title>').description('칸 제목 수정')).action((id: string, title: string, _opts: unknown, cmd: Command) => {
632
709
  const { version, json } = context(cmd);
710
+ if (!mayWrite(cmd)) return;
633
711
  const snapshot = listChecklist(version);
634
712
  features.retitle(id, title, actor(), snapshot.released, snapshot.dev);
635
713
  if (json) console.log(JSON.stringify(listChecklist(version))); else console.log(`✅ ${id} 제목 수정`);
636
714
  });
637
715
  const evidence = checklist.command('evidence').description('칸의 PR·커밋 근거');
638
716
  withContext(evidence.command('add <id> <ref>').description('근거 참조 추가')).action((id: string, ref: string, _opts: unknown, cmd: Command) => {
639
- const { version, json } = context(cmd);
640
- const snapshot = listChecklist(version);
641
- features.evidenceAdd(id, version, ref, actor(), snapshot.released, snapshot.dev);
717
+ const { version, json } = context(cmd);
718
+ if (!mayWrite(cmd)) return;
719
+ const snapshot = listChecklist(version);
720
+ features.evidenceAdd(id, version, ref, actor(), snapshot.released, snapshot.dev);
642
721
  if (json) console.log(JSON.stringify(features.history(id).map(decodeClaimHistoryEntry))); else console.log(`✅ ${id} 근거 ${ref}`);
643
722
  });
644
723
  withContext(checklist.command('history <id>').description('칸의 판 이동·변경 이력')).action((id: string, _opts: unknown, cmd: Command) => {
@@ -654,6 +733,7 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
654
733
  });
655
734
  withContext(checklist.command('export').description('SQLite 에서 checklist.json 스냅샷 생성')).action((_opts: unknown, cmd: Command) => {
656
735
  const { version, json } = context(cmd);
736
+ if (!mayWrite(cmd)) return;
657
737
  const snapshot = listChecklist(version);
658
738
  features.exportJson(version, snapshot.released, snapshot.dev);
659
739
  if (json) console.log(JSON.stringify(listChecklist(version))); else console.log(`✅ ${version} checklist.json 내보냄`);