elanous 0.2.19 → 0.2.20

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 (388) 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/{J5H7BvqgSI8DUjtRRcA_S → Bam1YziK2iH9bN4K0utGk}/_buildManifest.js +1 -1
  4. package/apps/pwa/out/_next/static/chunks/{4323.c5dfb6f503304775.js → 1268.0840ff91e083fcbd.js} +1 -1
  5. package/apps/pwa/out/_next/static/chunks/1307-77fad80cf54477c6.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/1612-f60c0c93ec773f81.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/1826-5d279da64bbbc2de.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/1844-9ec4b1d12f6e80eb.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/1866-e7a6999f25548b30.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/1948.15f356b15a625597.js +5 -0
  11. package/apps/pwa/out/_next/static/chunks/195-fb1d7690bf30bab4.js +1 -0
  12. package/apps/pwa/out/_next/static/chunks/1986-33e13d648a9df765.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/{2977-e38442e86289f1d3.js → 211-9c9090c59e2c5907.js} +1 -1
  14. package/apps/pwa/out/_next/static/chunks/2116-6429c188eb846ab8.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/2196-4700653d12a44b40.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/224-e7be8f762fe74986.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/2489-ff55a35b202c0f94.js +1 -0
  18. package/apps/pwa/out/_next/static/chunks/2537-6c289376caa88ca1.js +1 -0
  19. package/apps/pwa/out/_next/static/chunks/2549-89c518a4c8ed4d00.js +1 -0
  20. package/apps/pwa/out/_next/static/chunks/2699-325cc923ba7b8631.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/2957-96ffab3d521fb08f.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/311.e685738f92e6e2ec.js +1 -0
  23. package/apps/pwa/out/_next/static/chunks/3194-807b212fb8eae065.js +1 -0
  24. package/apps/pwa/out/_next/static/chunks/3324-d1d2979821dfa845.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/3558-bbb5ab553a0471d3.js +1 -0
  26. package/apps/pwa/out/_next/static/chunks/3857-995ea9f5ef33d71e.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/3916-3d02dd1fc257dd3c.js +1 -0
  28. package/apps/pwa/out/_next/static/chunks/4144-fad095b48a1862ee.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/4565-3d7e3fdeb43686a8.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/4795-f372490b178583fc.js +1 -0
  31. package/apps/pwa/out/_next/static/chunks/5360-be1f1d8f9fcc9755.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/5755-ea7ed88eb956f47b.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/5977-94ed6f78e9814b2b.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/{4847.56f5bc454fbcc827.js → 6110.1297d4093fdfbb0d.js} +2 -2
  35. package/apps/pwa/out/_next/static/chunks/6158.af2e93b7adff709b.js +2 -0
  36. package/apps/pwa/out/_next/static/chunks/6877-74399f69c7da1433.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/{1470.98a6ab61ccb4131c.js → 726.83532d69452ee24b.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/7290-f09c928a06c52e2d.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/{6062-73e68343a9814f6a.js → 7327-2d69fb321ba73389.js} +1 -1
  40. package/apps/pwa/out/_next/static/chunks/{8870-9614a213b990b10b.js → 7748-6e51bfe927019f5c.js} +3 -3
  41. package/apps/pwa/out/_next/static/chunks/7797-df5aa0381fdc078f.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/8060-cc008cc6fedfa818.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/8085-d065d0393c1760f5.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/8401-35e94e9091f5eb8d.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/8682-7061dd4fa7bd62ed.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/8695-846034ce44852c70.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/9070.067c106528ec1ae6.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/9103-60fbef33e66f0a46.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/9247-c1753a059e7bac08.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/{7647.2fcd30d894d5ee30.js → 935.4ddaa99fc1d1126c.js} +1 -1
  51. package/apps/pwa/out/_next/static/chunks/{ae787e4c-e5b1ef00b2dbceca.js → a9f2b54d-33c0689b70ed8cf3.js} +1 -1
  52. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-c1a96669158471ec.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/app/approvals/page-c855c24de7db74fe.js +1 -0
  54. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-85b316bb4235980b.js +1 -0
  55. package/apps/pwa/out/_next/static/chunks/app/board/page-9b2b26a33cb2cd42.js +1 -0
  56. package/apps/pwa/out/_next/static/chunks/app/botlab/page-c9955fde67b1bcb5.js +1 -0
  57. package/apps/pwa/out/_next/static/chunks/app/bots/page-7a852956c74015a3.js +1 -0
  58. package/apps/pwa/out/_next/static/chunks/app/ceo/page-c5a5c621be0e04ae.js +1 -0
  59. package/apps/pwa/out/_next/static/chunks/app/chat/page-4ba4abe096b6c2a8.js +1 -0
  60. package/apps/pwa/out/_next/static/chunks/app/consult/page-575d4a8b4f7becb2.js +1 -0
  61. package/apps/pwa/out/_next/static/chunks/app/control/page-f5d56ff5f45ad164.js +1 -0
  62. package/apps/pwa/out/_next/static/chunks/app/decisions/page-2260e9eef0357421.js +1 -0
  63. package/apps/pwa/out/_next/static/chunks/app/design-check/page-7c058069c5e7fa63.js +1 -0
  64. package/apps/pwa/out/_next/static/chunks/app/editor/page-dc5da0b8a2edb8a4.js +1 -0
  65. package/apps/pwa/out/_next/static/chunks/app/exec/page-466bf0ee16ad1432.js +1 -0
  66. package/apps/pwa/out/_next/static/chunks/app/field/page-a23bc71713bcc08f.js +1 -0
  67. package/apps/pwa/out/_next/static/chunks/app/healthz/route-c1a96669158471ec.js +1 -0
  68. package/apps/pwa/out/_next/static/chunks/app/inside/{page-5b29d9b06ceadade.js → page-ed4a6f799f005d09.js} +1 -1
  69. package/apps/pwa/out/_next/static/chunks/app/intake/page-9cd565005b93aca6.js +1 -0
  70. package/apps/pwa/out/_next/static/chunks/app/layout-55f0f40c6db95f15.js +1 -0
  71. package/apps/pwa/out/_next/static/chunks/app/live/page-474cb5888ea0e8f3.js +1 -0
  72. package/apps/pwa/out/_next/static/chunks/app/loops/page-35bfec2e631d3eae.js +1 -0
  73. package/apps/pwa/out/_next/static/chunks/app/market/page-9ca21f9ad3208a61.js +1 -0
  74. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-f9ccb1b1adbea14d.js +1 -0
  75. package/apps/pwa/out/_next/static/chunks/app/morning/page-ec102c9b8b691e37.js +1 -0
  76. package/apps/pwa/out/_next/static/chunks/app/not-found-a9c2cfa8dda922a5.js +1 -0
  77. package/apps/pwa/out/_next/static/chunks/app/observatory/page-6da35144878feb6d.js +1 -0
  78. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-f6055495e9ed7ea8.js +1 -0
  79. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-25204694fa9984a5.js +1 -0
  80. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-a60e035f151d84d3.js +1 -0
  81. package/apps/pwa/out/_next/static/chunks/app/outputs/page-189c587f36874538.js +1 -0
  82. package/apps/pwa/out/_next/static/chunks/app/page-c56634bde705a65d.js +1 -0
  83. package/apps/pwa/out/_next/static/chunks/app/reflection/page-db03ae3589a81c9e.js +1 -0
  84. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-4a6463b97f0ad94f.js +1 -0
  85. package/apps/pwa/out/_next/static/chunks/app/sessions/page-75bffecc9aa1004e.js +1 -0
  86. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-a8d2fe0c669e0de4.js +1 -0
  87. package/apps/pwa/out/_next/static/chunks/app/settings/page-31d5b875760bb62d.js +1 -0
  88. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-c26fa10aac6c21c4.js +1 -0
  89. package/apps/pwa/out/_next/static/chunks/app/setup/layout-c1a96669158471ec.js +1 -0
  90. package/apps/pwa/out/_next/static/chunks/app/setup/page-68793c6e10331d66.js +1 -0
  91. package/apps/pwa/out/_next/static/chunks/app/share/page-46d5201d2207a511.js +1 -0
  92. package/apps/pwa/out/_next/static/chunks/app/showroom/page-d81c07a7b4f618ae.js +1 -0
  93. package/apps/pwa/out/_next/static/chunks/app/tasks/page-be1c4e8697f22360.js +1 -0
  94. package/apps/pwa/out/_next/static/chunks/app/term/page-cf0a4b4abead71ea.js +1 -0
  95. package/apps/pwa/out/_next/static/chunks/app/today/page-a4717e2c4bf7a28f.js +1 -0
  96. package/apps/pwa/out/_next/static/chunks/app/trace/page-0fd895fab9b62c1b.js +1 -0
  97. package/apps/pwa/out/_next/static/chunks/app/vault/page-8b6c1454a8e8f306.js +1 -0
  98. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-c6e4364d9de8f717.js +1 -0
  99. package/apps/pwa/out/_next/static/chunks/app/workflows/page-0acc8e198f7b0dfb.js +1 -0
  100. package/apps/pwa/out/_next/static/chunks/app/workspace/page-34882c84f84021c5.js +1 -0
  101. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-0506a1e0e4bdb2f5.js +1 -0
  102. package/apps/pwa/out/_next/static/chunks/{8046cadf-211ca3c0e00f2b72.js → f6231284-45726550aba08fca.js} +1 -1
  103. package/apps/pwa/out/_next/static/chunks/{2ea96958.e8c7e112de50b067.js → f6b7c9ba.64a895b8b2fedbdc.js} +1 -1
  104. package/apps/pwa/out/_next/static/chunks/framework-5c169757551134ed.js +1 -0
  105. package/apps/pwa/out/_next/static/chunks/main-59fe95fed40c7a01.js +1 -0
  106. package/apps/pwa/out/_next/static/chunks/main-app-4ca74dbbee7057c6.js +1 -0
  107. package/apps/pwa/out/_next/static/chunks/pages/_app-13a0a886df4d97a1.js +1 -0
  108. package/apps/pwa/out/_next/static/chunks/pages/_error-6af8591cd3a3f038.js +1 -0
  109. package/apps/pwa/out/_next/static/chunks/webpack-c260571513bf43c2.js +1 -0
  110. package/apps/pwa/out/_next/static/css/c23d959d3ee15e57.css +1 -0
  111. package/apps/pwa/out/approvals/index.html +1 -1
  112. package/apps/pwa/out/approvals/index.txt +23 -23
  113. package/apps/pwa/out/autopilot/index.html +1 -1
  114. package/apps/pwa/out/autopilot/index.txt +23 -23
  115. package/apps/pwa/out/board/index.html +1 -1
  116. package/apps/pwa/out/board/index.txt +23 -23
  117. package/apps/pwa/out/botlab/index.html +1 -1
  118. package/apps/pwa/out/botlab/index.txt +22 -22
  119. package/apps/pwa/out/bots/index.html +1 -1
  120. package/apps/pwa/out/bots/index.txt +22 -22
  121. package/apps/pwa/out/ceo/index.html +1 -1
  122. package/apps/pwa/out/ceo/index.txt +23 -23
  123. package/apps/pwa/out/chat/index.html +1 -1
  124. package/apps/pwa/out/chat/index.txt +22 -22
  125. package/apps/pwa/out/consult/index.html +1 -1
  126. package/apps/pwa/out/consult/index.txt +23 -23
  127. package/apps/pwa/out/control/index.html +1 -1
  128. package/apps/pwa/out/control/index.txt +23 -23
  129. package/apps/pwa/out/decisions/index.html +1 -1
  130. package/apps/pwa/out/decisions/index.txt +22 -22
  131. package/apps/pwa/out/design-check/index.html +1 -1
  132. package/apps/pwa/out/design-check/index.txt +23 -23
  133. package/apps/pwa/out/editor/index.html +1 -1
  134. package/apps/pwa/out/editor/index.txt +23 -23
  135. package/apps/pwa/out/exec/index.html +1 -1
  136. package/apps/pwa/out/exec/index.txt +23 -23
  137. package/apps/pwa/out/field/index.html +1 -1
  138. package/apps/pwa/out/field/index.txt +23 -23
  139. package/apps/pwa/out/healthz +1 -1
  140. package/apps/pwa/out/index.html +1 -1
  141. package/apps/pwa/out/index.txt +22 -22
  142. package/apps/pwa/out/inside/index.html +1 -1
  143. package/apps/pwa/out/inside/index.txt +22 -22
  144. package/apps/pwa/out/intake/index.html +1 -1
  145. package/apps/pwa/out/intake/index.txt +23 -23
  146. package/apps/pwa/out/live/index.html +1 -1
  147. package/apps/pwa/out/live/index.txt +23 -23
  148. package/apps/pwa/out/loops/index.html +1 -1
  149. package/apps/pwa/out/loops/index.txt +22 -22
  150. package/apps/pwa/out/market/index.html +1 -1
  151. package/apps/pwa/out/market/index.txt +22 -22
  152. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  153. package/apps/pwa/out/missions/placeholder/index.txt +22 -22
  154. package/apps/pwa/out/morning/index.html +1 -1
  155. package/apps/pwa/out/morning/index.txt +23 -23
  156. package/apps/pwa/out/observatory/index.html +1 -1
  157. package/apps/pwa/out/observatory/index.txt +23 -23
  158. package/apps/pwa/out/ops/checklist/index.html +1 -1
  159. package/apps/pwa/out/ops/checklist/index.txt +22 -22
  160. package/apps/pwa/out/ops/release/index.html +1 -1
  161. package/apps/pwa/out/ops/release/index.txt +22 -22
  162. package/apps/pwa/out/ops/seats/index.html +1 -1
  163. package/apps/pwa/out/ops/seats/index.txt +22 -22
  164. package/apps/pwa/out/outputs/index.html +1 -1
  165. package/apps/pwa/out/outputs/index.txt +22 -22
  166. package/apps/pwa/out/reflection/index.html +1 -1
  167. package/apps/pwa/out/reflection/index.txt +23 -23
  168. package/apps/pwa/out/scheduler/index.html +1 -1
  169. package/apps/pwa/out/scheduler/index.txt +23 -23
  170. package/apps/pwa/out/sessions/index.html +1 -1
  171. package/apps/pwa/out/sessions/index.txt +22 -22
  172. package/apps/pwa/out/settings/devices/index.html +1 -1
  173. package/apps/pwa/out/settings/devices/index.txt +23 -23
  174. package/apps/pwa/out/settings/index.html +1 -1
  175. package/apps/pwa/out/settings/index.txt +23 -23
  176. package/apps/pwa/out/setup/done/index.html +1 -1
  177. package/apps/pwa/out/setup/done/index.txt +23 -23
  178. package/apps/pwa/out/setup/index.html +1 -1
  179. package/apps/pwa/out/setup/index.txt +23 -23
  180. package/apps/pwa/out/share/index.html +1 -1
  181. package/apps/pwa/out/share/index.txt +23 -23
  182. package/apps/pwa/out/showroom/index.html +1 -1
  183. package/apps/pwa/out/showroom/index.txt +22 -22
  184. package/apps/pwa/out/tasks/index.html +1 -1
  185. package/apps/pwa/out/tasks/index.txt +23 -23
  186. package/apps/pwa/out/term/index.html +1 -1
  187. package/apps/pwa/out/term/index.txt +23 -23
  188. package/apps/pwa/out/today/index.html +1 -1
  189. package/apps/pwa/out/today/index.txt +23 -23
  190. package/apps/pwa/out/trace/index.html +1 -1
  191. package/apps/pwa/out/trace/index.txt +23 -23
  192. package/apps/pwa/out/vault/index.html +1 -1
  193. package/apps/pwa/out/vault/index.txt +23 -23
  194. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  195. package/apps/pwa/out/workflows/chat-ui/index.txt +23 -23
  196. package/apps/pwa/out/workflows/index.html +1 -1
  197. package/apps/pwa/out/workflows/index.txt +23 -23
  198. package/apps/pwa/out/workspace/index.html +1 -1
  199. package/apps/pwa/out/workspace/index.txt +23 -23
  200. package/apps/pwa/out/worktrees/index.html +1 -1
  201. package/apps/pwa/out/worktrees/index.txt +23 -23
  202. package/graphs/demo/editor-recipes.yaml +22 -0
  203. package/graphs/retro/daily.yaml +25 -0
  204. package/graphs/retro/recipes.yaml +24 -0
  205. package/package.json +1 -1
  206. package/scripts/ask-marker-check.ts +3 -1
  207. package/scripts/land-global-checks.ts +89 -0
  208. package/scripts/lib/pch-regress-map.ts +2 -1
  209. package/scripts/release-loop/gate-node.ts +296 -25
  210. package/scripts/release-loop/gate-timing.ts +116 -7
  211. package/scripts/release-loop/preflight.ts +6 -4
  212. package/scripts/release-loop/publish-window.ts +141 -0
  213. package/scripts/release-loop/release-docs.ts +1 -4
  214. package/scripts/release-loop/release-universe.ts +38 -0
  215. package/scripts/release-loop/resume-release.ts +4 -1
  216. package/scripts/release-loop/unattended-release.ts +10 -2
  217. package/scripts/release-story/draft.ts +5 -1
  218. package/scripts/retro/shadow-compare.ts +71 -0
  219. package/scripts/rhythm/daily.ts +6 -1
  220. package/src/agent-mission/driver.ts +34 -6
  221. package/src/autopilot/build/llm-conflict-merge.ts +132 -7
  222. package/src/autopilot/build/merge-intent.ts +135 -7
  223. package/src/away/release-watch.ts +20 -3
  224. package/src/cli/config-drift.ts +513 -0
  225. package/src/cli/log-event-names.ts +4 -0
  226. package/src/cli/pr-cli.ts +20 -0
  227. package/src/cli/release-cli.ts +20 -5
  228. package/src/cli/retro-cli.ts +78 -0
  229. package/src/cli/tasks-cli.ts +94 -8
  230. package/src/dashboard/slash-runtime/dashboard-handlers.ts +5 -4
  231. package/src/dashboard/slash-runtime/help-from-registry.ts +57 -31
  232. package/src/domains/outbound-log-probe.ts +6 -1
  233. package/src/graph-runner/graph-run-api.ts +205 -0
  234. package/src/graph-runner/runner.ts +11 -1
  235. package/src/harness/harness-cli-command.ts +419 -52
  236. package/src/harness/harness-queue.ts +315 -25
  237. package/src/harness/harness-salvage-cli.ts +62 -0
  238. package/src/hq/fence-outcomes.ts +63 -0
  239. package/src/hq/fence-wrapper.ts +33 -0
  240. package/src/hq/hq.ts +11 -1
  241. package/src/index.ts +69 -6
  242. package/src/maturity/feature-maturity.ts +2 -2
  243. package/src/nexus/api/graph-kinds.ts +10 -4
  244. package/src/nexus/api/graphs-api.ts +178 -6
  245. package/src/nexus/api/http-server.ts +38 -3
  246. package/src/nexus/api/loop-edges.ts +4 -2
  247. package/src/nexus/api/ops-api.ts +22 -7
  248. package/src/nexus/api/ops-board-route.ts +48 -0
  249. package/src/onboarding/entry-hints.ts +5 -0
  250. package/src/onboarding.ts +24 -6
  251. package/src/release-loop/feature-store.ts +14 -11
  252. package/src/release-loop/gate-shards.ts +158 -0
  253. package/src/release-loop/release-schedule.ts +6 -2
  254. package/src/retro/daily.ts +372 -0
  255. package/src/seat-loop/op-decide.ts +173 -0
  256. package/src/seat-loop/seat-loop.ts +60 -3
  257. package/src/seat-loop/seat-report.ts +58 -3
  258. package/src/self-dev/ask-launch-flow.ts +5 -4
  259. package/src/self-dev/ask-launch-io.ts +49 -1
  260. package/src/self-dev/ask-path-candidates.ts +40 -2
  261. package/src/self-dev/draft-sweep.ts +371 -16
  262. package/src/self-dev/draft-triage-rules.ts +19 -4
  263. package/src/self-dev/pr-preview.ts +84 -16
  264. package/src/self-implement/blocked-draft-policy.ts +101 -0
  265. package/src/self-implement/execution-clarification.ts +11 -0
  266. package/src/self-implement/gate-env-retry.ts +83 -0
  267. package/src/self-implement/orchestrator.ts +218 -5
  268. package/src/self-implement/run-stop.ts +123 -0
  269. package/src/self-implement/run-ttl.ts +258 -0
  270. package/src/self-implement/salvage-retention.ts +123 -0
  271. package/src/self-implement/seams.ts +4 -0
  272. package/src/self-implement/self-implement-cli.ts +2 -0
  273. package/src/self-implement/sibling-resync.ts +223 -0
  274. package/src/self-implement/stop-autoheal.ts +204 -0
  275. package/src/task-agent/board.ts +1 -1
  276. package/src/task-agent/card-evidence.ts +99 -10
  277. package/src/task-agent/judge.ts +7 -0
  278. package/src/task-agent/live-moves.ts +120 -20
  279. package/src/task-agent/mission.ts +1 -1
  280. package/src/task-agent/shadow.ts +10 -1
  281. package/src/task-agent/task-hand.ts +7 -2
  282. package/src/task-orchestrator/surfaces/pod-command-job.ts +19 -5
  283. package/src/task-orchestrator/surfaces/pod-failure-reason.ts +4 -0
  284. package/src/task-orchestrator/surfaces/pod-install-slots.ts +89 -0
  285. package/src/task-orchestrator/surfaces/pod-lease.ts +93 -10
  286. package/src/task-orchestrator/surfaces/pod-pool.ts +147 -34
  287. package/src/task-orchestrator/surfaces/self-implement-pod.ts +19 -3
  288. package/src/telegram-commands.ts +41 -0
  289. package/src/testing/real-cli-spawn.ts +106 -0
  290. package/src/user-config.ts +56 -3
  291. package/src/version/packed-revision.json +1 -1
  292. package/apps/pwa/out/_next/static/chunks/1105-c473ed46272fba79.js +0 -1
  293. package/apps/pwa/out/_next/static/chunks/1157-cba0e8553d35a980.js +0 -1
  294. package/apps/pwa/out/_next/static/chunks/171-23a877ed11720de9.js +0 -1
  295. package/apps/pwa/out/_next/static/chunks/216-5acd01cbde4acda7.js +0 -1
  296. package/apps/pwa/out/_next/static/chunks/2330-c2f07246c8788af1.js +0 -1
  297. package/apps/pwa/out/_next/static/chunks/2336-e830c777eb6bce04.js +0 -1
  298. package/apps/pwa/out/_next/static/chunks/2375-e061bb24cd2b5cde.js +0 -1
  299. package/apps/pwa/out/_next/static/chunks/2415.ed6428ff334c7ce2.js +0 -1
  300. package/apps/pwa/out/_next/static/chunks/3094.67339318c7f72a3a.js +0 -2
  301. package/apps/pwa/out/_next/static/chunks/338-ceb956612dcfaa86.js +0 -1
  302. package/apps/pwa/out/_next/static/chunks/3395-835e4ceaa6335ba4.js +0 -1
  303. package/apps/pwa/out/_next/static/chunks/3403.4bc97b4066945a94.js +0 -5
  304. package/apps/pwa/out/_next/static/chunks/3441-be82e3595aef3a20.js +0 -1
  305. package/apps/pwa/out/_next/static/chunks/3689-89905039bf3e7257.js +0 -1
  306. package/apps/pwa/out/_next/static/chunks/371-4ad6af55dad02ced.js +0 -1
  307. package/apps/pwa/out/_next/static/chunks/3822-6f268bc26c6a84ac.js +0 -1
  308. package/apps/pwa/out/_next/static/chunks/4114-3e63117452378807.js +0 -1
  309. package/apps/pwa/out/_next/static/chunks/4457-d7df81c90f9d0268.js +0 -1
  310. package/apps/pwa/out/_next/static/chunks/4551-77dadae76f6cbace.js +0 -1
  311. package/apps/pwa/out/_next/static/chunks/4978.6fd93b974441e87f.js +0 -1
  312. package/apps/pwa/out/_next/static/chunks/5012-f746f6549b869f91.js +0 -1
  313. package/apps/pwa/out/_next/static/chunks/5609-012bb3557211f07e.js +0 -1
  314. package/apps/pwa/out/_next/static/chunks/5791-bb82743fa6299562.js +0 -1
  315. package/apps/pwa/out/_next/static/chunks/62-5bfed1e23790b745.js +0 -1
  316. package/apps/pwa/out/_next/static/chunks/6320-9bc8561fde416e07.js +0 -1
  317. package/apps/pwa/out/_next/static/chunks/649-ca96d1c5d09e02b8.js +0 -1
  318. package/apps/pwa/out/_next/static/chunks/6725-68ab7cc0b1faf00d.js +0 -1
  319. package/apps/pwa/out/_next/static/chunks/6726-85a7db59b288c53f.js +0 -1
  320. package/apps/pwa/out/_next/static/chunks/6735-33267c4630d0d963.js +0 -1
  321. package/apps/pwa/out/_next/static/chunks/6998-a654fe5c58271600.js +0 -1
  322. package/apps/pwa/out/_next/static/chunks/7633-141682015873beb6.js +0 -1
  323. package/apps/pwa/out/_next/static/chunks/7673-2eda1507d38dadb8.js +0 -1
  324. package/apps/pwa/out/_next/static/chunks/7990-e12efba616f46e03.js +0 -1
  325. package/apps/pwa/out/_next/static/chunks/8067-143791231c99bd02.js +0 -1
  326. package/apps/pwa/out/_next/static/chunks/8142-744998d65d7bb17e.js +0 -1
  327. package/apps/pwa/out/_next/static/chunks/8430-a10cfba8ef282d4e.js +0 -1
  328. package/apps/pwa/out/_next/static/chunks/920-f7dceb78f1c585ef.js +0 -1
  329. package/apps/pwa/out/_next/static/chunks/9454-12c2f30dea62291c.js +0 -1
  330. package/apps/pwa/out/_next/static/chunks/976-4a9b4ebdca7c739a.js +0 -1
  331. package/apps/pwa/out/_next/static/chunks/9989-15cdf568bafa9fc8.js +0 -1
  332. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-859cea1570b6227a.js +0 -1
  333. package/apps/pwa/out/_next/static/chunks/app/approvals/page-deef5ee191830f14.js +0 -1
  334. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-b193da485f9a14b2.js +0 -1
  335. package/apps/pwa/out/_next/static/chunks/app/board/page-b6e1a79df0e214f4.js +0 -1
  336. package/apps/pwa/out/_next/static/chunks/app/botlab/page-e84df4d907f703c7.js +0 -1
  337. package/apps/pwa/out/_next/static/chunks/app/bots/page-a5fb9a5db89ae4ea.js +0 -1
  338. package/apps/pwa/out/_next/static/chunks/app/ceo/page-1499f481d7fc52d6.js +0 -1
  339. package/apps/pwa/out/_next/static/chunks/app/chat/page-c7cf753aeb648790.js +0 -1
  340. package/apps/pwa/out/_next/static/chunks/app/consult/page-4118469e514e10fd.js +0 -1
  341. package/apps/pwa/out/_next/static/chunks/app/control/page-e968f7630654332a.js +0 -1
  342. package/apps/pwa/out/_next/static/chunks/app/decisions/page-505915052f947022.js +0 -1
  343. package/apps/pwa/out/_next/static/chunks/app/design-check/page-c2f962fb4a3b06c6.js +0 -1
  344. package/apps/pwa/out/_next/static/chunks/app/editor/page-7b5ed0bd8a589ec4.js +0 -1
  345. package/apps/pwa/out/_next/static/chunks/app/exec/page-cf8193250dd42dd4.js +0 -1
  346. package/apps/pwa/out/_next/static/chunks/app/field/page-cf3f5dfa35dea8e9.js +0 -1
  347. package/apps/pwa/out/_next/static/chunks/app/healthz/route-859cea1570b6227a.js +0 -1
  348. package/apps/pwa/out/_next/static/chunks/app/intake/page-a15e7fcc283e9ca7.js +0 -1
  349. package/apps/pwa/out/_next/static/chunks/app/layout-d2915e7b70ff3d0e.js +0 -1
  350. package/apps/pwa/out/_next/static/chunks/app/live/page-b70cd005d9169e25.js +0 -1
  351. package/apps/pwa/out/_next/static/chunks/app/loops/page-2595f0eba29f57e1.js +0 -1
  352. package/apps/pwa/out/_next/static/chunks/app/market/page-c776322454156e36.js +0 -1
  353. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-77bbd17f4435aa10.js +0 -1
  354. package/apps/pwa/out/_next/static/chunks/app/morning/page-049689e611ecc936.js +0 -1
  355. package/apps/pwa/out/_next/static/chunks/app/not-found-85954a4fb09c44eb.js +0 -1
  356. package/apps/pwa/out/_next/static/chunks/app/observatory/page-e773e552e1068ee6.js +0 -1
  357. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-5633c4bfa7684b4a.js +0 -1
  358. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-d829b92bd01b4994.js +0 -1
  359. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-061c317fc771becf.js +0 -1
  360. package/apps/pwa/out/_next/static/chunks/app/outputs/page-3a45ece8865ed4b6.js +0 -1
  361. package/apps/pwa/out/_next/static/chunks/app/page-0be990618c0d56bd.js +0 -1
  362. package/apps/pwa/out/_next/static/chunks/app/reflection/page-bfea4865dc244b73.js +0 -1
  363. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-0d6ba8e337741484.js +0 -1
  364. package/apps/pwa/out/_next/static/chunks/app/sessions/page-76a88b55d816482e.js +0 -1
  365. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-bba1542df59f61ce.js +0 -1
  366. package/apps/pwa/out/_next/static/chunks/app/settings/page-4185c95689bb372e.js +0 -1
  367. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-b699e1a8d5d34b8b.js +0 -1
  368. package/apps/pwa/out/_next/static/chunks/app/setup/layout-859cea1570b6227a.js +0 -1
  369. package/apps/pwa/out/_next/static/chunks/app/setup/page-c225e338c31bf274.js +0 -1
  370. package/apps/pwa/out/_next/static/chunks/app/share/page-449353f70507ca1f.js +0 -1
  371. package/apps/pwa/out/_next/static/chunks/app/showroom/page-90b889714102476d.js +0 -1
  372. package/apps/pwa/out/_next/static/chunks/app/tasks/page-f1ee7fbeb7f96ddc.js +0 -1
  373. package/apps/pwa/out/_next/static/chunks/app/term/page-d59a95e1434dfaa0.js +0 -1
  374. package/apps/pwa/out/_next/static/chunks/app/today/page-71491a23414d1a47.js +0 -1
  375. package/apps/pwa/out/_next/static/chunks/app/trace/page-de7112bdb65fa4fe.js +0 -1
  376. package/apps/pwa/out/_next/static/chunks/app/vault/page-feb4d0d88079660d.js +0 -1
  377. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-f17d887b40dd6b1d.js +0 -1
  378. package/apps/pwa/out/_next/static/chunks/app/workflows/page-8da77be2192bcd5d.js +0 -1
  379. package/apps/pwa/out/_next/static/chunks/app/workspace/page-72c3b9a5e9582b29.js +0 -1
  380. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-c3afe4c8d0c48c39.js +0 -1
  381. package/apps/pwa/out/_next/static/chunks/framework-ea806bb57f2b8923.js +0 -1
  382. package/apps/pwa/out/_next/static/chunks/main-8d2302399c2c8f87.js +0 -1
  383. package/apps/pwa/out/_next/static/chunks/main-app-b9308f2d8a931531.js +0 -1
  384. package/apps/pwa/out/_next/static/chunks/pages/_app-b10928c60f0b8488.js +0 -1
  385. package/apps/pwa/out/_next/static/chunks/pages/_error-689039227d443fab.js +0 -1
  386. package/apps/pwa/out/_next/static/chunks/webpack-6f35f9d892fa630b.js +0 -1
  387. package/apps/pwa/out/_next/static/css/85f53c6f35d11382.css +0 -1
  388. /package/apps/pwa/out/_next/static/{J5H7BvqgSI8DUjtRRcA_S → Bam1YziK2iH9bN4K0utGk}/_ssgManifest.js +0 -0
@@ -0,0 +1,513 @@
1
+ // ── CFG-STORE1 · 읽기 전용 «유효 config ⊕ 출처» · 드리프트 표 (0.2.20 · 2026-10-08) ──
2
+ //
3
+ // RFC-universe-by-house-not-tree-and-config-store-2026-10-06 §2·§3.
4
+ //
5
+ // S: 집(운영 `~/.elanous`)과 파생 시험 우주(`<트리>/.elanous-test` · 10-06 실측 161)가 각자
6
+ // `config.json` 사본을 «물질화»해 갖는다. 운영 키를 바꿔도 사본은 그대로다.
7
+ // C: 10-08 밤 — 운영 `harness.podPool` 에서 node-c 를 뺐는데 자리·작업 트리의 시험 우주 사본 36개가
8
+ // 옛 podPool(node-c 포함)을 들고 있어, 그 트리에서 쏜 발사가 계속 node-c 에 Pod 를 놓았다.
9
+ // Q: 「어느 키가 어느 우주에서 운영과 다른가」를 한 명령으로 볼 수 있나.
10
+ // A: `elanous config drift` · `elanous config explain <key>` — 이 모듈이 그 계산이다.
11
+ //
12
+ // ⛔ 불변식
13
+ // • 읽기만 한다 — 레지스트리 prune(readLogInstances)·XDG 이주(userConfigPath)도 부르지 않는다.
14
+ // • 못 읽은 config 는 «못 읽음»으로 따로 센다 — 절대 「같다」로 접지 않는다.
15
+ // • 비밀 값은 찍지 않는다 — 경로의 한 마디라도 token/secret/key/password 면 값을 가린다.
16
+ // • config.json 이 «없는» 우주는 비교에서 빼되 수를 낸다(그 우주는 코드 기본값으로 돈다).
17
+
18
+ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
19
+ import { homedir } from 'node:os';
20
+ import { join, resolve } from 'node:path';
21
+ import { redactSecretText } from '../debug/log.js';
22
+ import { resolveInstanceKind, type LogInstanceEntry } from '../mss/logging/instance-registry.js';
23
+ import { buildTestSafeRawConfig } from './config-test-sync.js';
24
+ import { prodInstanceRoot } from '../instance/resolve.js';
25
+
26
+ export type UniverseOrigin = 'registry' | 'tree-scan';
27
+ export type UniverseStatus = 'ok' | 'no-config' | 'unreadable' | 'gone';
28
+
29
+ export interface UniverseConfig {
30
+ configDir: string;
31
+ configPath: string;
32
+ origins: UniverseOrigin[];
33
+ status: UniverseStatus;
34
+ /** status=unreadable 일 때 사유. */
35
+ error?: string;
36
+ /** status=ok 일 때 파일 원문(raw) — 겹 «파일» 층. */
37
+ raw?: Record<string, unknown>;
38
+ }
39
+
40
+ export interface DiscoverOptions {
41
+ prodRoot?: string;
42
+ /** 인스턴스 레지스트리(`~/.elanous/logs/instances.json`) — 읽기만. null 이면 안 읽는다. */
43
+ registryPath?: string | null;
44
+ /** 명시 트리 뿌리 — 각 자식 디렉토리의 `.elanous-test` 를 본다. */
45
+ treeScanRoots?: string[];
46
+ }
47
+
48
+ // 비밀 «이름» — 대소문자 무시 낱말 ⊕ 「auth」「pass」는 낱말 경계로만(authorOnPod·bypass 는 아니다).
49
+ const SECRET_NAME = /token|secret|key|password|passphrase|credential|authorization|oauth|bearer|cookie|session|dsn/i;
50
+ const SECRET_NAME_CAMEL = /(?:(?:^|[^a-zA-Z])(?:auth|AUTH|pass|PASS)|(?:Auth|Pass))(?![a-z])/;
51
+ // 비밀 «값» — 이름이 무해해도(notify.url) 값 모양으로 가린다.
52
+ const SECRET_VALUE_PATTERNS: readonly RegExp[] = [
53
+ /[a-z][a-z0-9+.-]*:\/\/[^/\s@]+@/i, // URL userinfo (https://user:pass@host)
54
+ /discord(?:app)?\.com\/api\/webhooks|hooks\.slack\.com/i, // Discord·Slack 웹훅
55
+ /^\s*(?:ghp_|gho_|ghs_|github_pat_|sk-|xoxb-|xoxp-|Bearer\s)/, // 토큰 접두
56
+ ];
57
+
58
+ /** 키 한 마디가 비밀 이름인가. */
59
+ export function isSecretName(segment: string): boolean {
60
+ return SECRET_NAME.test(segment) || SECRET_NAME_CAMEL.test(segment);
61
+ }
62
+
63
+ /** 문자열 값이 비밀 모양인가(이름과 무관). */
64
+ export function isSecretValue(value: unknown): boolean {
65
+ return typeof value === 'string' && SECRET_VALUE_PATTERNS.some((re) => re.test(value));
66
+ }
67
+ export const MASK = '<masked>';
68
+
69
+ export function defaultProdRoot(): string {
70
+ return prodInstanceRoot(); // 집 = 운영 뿌리(우주 리졸버 경유 · 격리 게이트)
71
+ }
72
+
73
+ export function defaultRegistryPath(prodRoot: string = defaultProdRoot()): string {
74
+ return join(prodRoot, 'logs', 'instances.json');
75
+ }
76
+
77
+ /** 레지스트리 밖에서 시험 우주가 사는 «알려진» 트리 뿌리(10-08 사건의 세 곳). 명시 목록 — 홈 전체를 훑지 않는다. */
78
+ export function defaultTreeScanRoots(home: string = homedir()): string[] {
79
+ return [join(home, 'elanous-hq', 'seats'), join(home, 'elanous-hq', 'work'), join(home, 'source', 'pilot')];
80
+ }
81
+
82
+ /** 읽기 실패 사유 — ⛔ 파서 메시지는 파일 «내용» 조각을 실을 수 있어 그대로 내지 않는다(비밀 노출). 종류·errno 코드만. */
83
+ export function safeReadError(err: unknown): string {
84
+ if (err instanceof SyntaxError) return 'JSON 파싱 실패';
85
+ const code = (err as { code?: unknown } | null)?.code;
86
+ if (typeof code === 'string' && /^[A-Z0-9_]+$/.test(code)) return `읽기 실패(${code})`;
87
+ return '읽기 실패';
88
+ }
89
+
90
+ function norm(p: string): string {
91
+ return resolve(p.trim().replace(/\/+$/, ''));
92
+ }
93
+
94
+ /** 레지스트리를 «읽기만» 한다(prune 쓰기 없음). 실패는 {ok:false}. */
95
+ export function readRegistryReadOnly(path: string): { ok: boolean; entries: LogInstanceEntry[]; error?: string } {
96
+ try {
97
+ const parsed = JSON.parse(readFileSync(path, 'utf-8')) as { instances?: unknown };
98
+ if (!Array.isArray(parsed.instances)) return { ok: false, entries: [], error: 'instances 배열 없음' };
99
+ const entries = parsed.instances.filter((e): e is LogInstanceEntry => !!e && typeof e === 'object'
100
+ && typeof (e as LogInstanceEntry).stateDir === 'string');
101
+ return { ok: true, entries };
102
+ } catch (err) {
103
+ return { ok: false, entries: [], error: safeReadError(err) };
104
+ }
105
+ }
106
+
107
+ export function readUniverseConfig(configDir: string, origins: UniverseOrigin[]): UniverseConfig {
108
+ const configPath = join(configDir, 'config.json');
109
+ if (!existsSync(configDir)) return { configDir, configPath, origins, status: 'gone' };
110
+ if (!existsSync(configPath)) return { configDir, configPath, origins, status: 'no-config' };
111
+ try {
112
+ const parsed = JSON.parse(readFileSync(configPath, 'utf-8')) as unknown;
113
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
114
+ return { configDir, configPath, origins, status: 'unreadable', error: '최상위가 객체가 아님' };
115
+ }
116
+ return { configDir, configPath, origins, status: 'ok', raw: parsed as Record<string, unknown> };
117
+ } catch (err) {
118
+ return { configDir, configPath, origins, status: 'unreadable', error: safeReadError(err) };
119
+ }
120
+ }
121
+
122
+ export interface DiscoveryResult {
123
+ prod: UniverseConfig;
124
+ derived: UniverseConfig[];
125
+ registry: { path: string | null; ok: boolean; entries: number; testEntries: number; error?: string };
126
+ treeScanRoots: string[];
127
+ }
128
+
129
+ /** 집 ⊕ 파생 시험 우주를 모은다 — 레지스트리(test 항목) ∪ 명시 트리 뿌리. configDir 로 중복 제거. */
130
+ export function discoverUniverses(opts: DiscoverOptions = {}): DiscoveryResult {
131
+ const prodRoot = norm(opts.prodRoot ?? defaultProdRoot());
132
+ const registryPath = opts.registryPath === undefined ? defaultRegistryPath(prodRoot) : opts.registryPath;
133
+ const treeScanRoots = (opts.treeScanRoots ?? defaultTreeScanRoots()).map(norm);
134
+ const found = new Map<string, Set<UniverseOrigin>>();
135
+ const add = (dir: string, origin: UniverseOrigin) => {
136
+ const d = norm(dir);
137
+ if (d === prodRoot) return;
138
+ const set = found.get(d) ?? new Set<UniverseOrigin>();
139
+ set.add(origin);
140
+ found.set(d, set);
141
+ };
142
+
143
+ let registry: DiscoveryResult['registry'] = { path: registryPath, ok: false, entries: 0, testEntries: 0 };
144
+ if (registryPath) {
145
+ const r = readRegistryReadOnly(registryPath);
146
+ let testEntries = 0;
147
+ for (const e of r.entries) {
148
+ if (resolveInstanceKind(e) !== 'test') continue;
149
+ testEntries++;
150
+ add(e.configDir ?? e.stateDir, 'registry');
151
+ }
152
+ registry = { path: registryPath, ok: r.ok, entries: r.entries.length, testEntries, ...(r.error ? { error: r.error } : {}) };
153
+ }
154
+
155
+ for (const root of treeScanRoots) {
156
+ let children: string[] = [];
157
+ try { children = readdirSync(root); } catch { continue; }
158
+ for (const child of children) {
159
+ const tree = join(root, child);
160
+ try { if (!statSync(tree).isDirectory()) continue; } catch { continue; }
161
+ const candidate = join(tree, '.elanous-test');
162
+ if (existsSync(candidate)) add(candidate, 'tree-scan');
163
+ }
164
+ }
165
+
166
+ const derived = [...found.entries()]
167
+ .sort(([a], [b]) => a.localeCompare(b))
168
+ .map(([dir, origins]) => readUniverseConfig(dir, [...origins].sort()));
169
+ return { prod: readUniverseConfig(prodRoot, []), derived, registry, treeScanRoots };
170
+ }
171
+
172
+ // ── 값 평탄화 · 가림 ──
173
+
174
+ export function isSecretPath(key: string): boolean {
175
+ return key.split('.').some(isSecretName);
176
+ }
177
+
178
+ /** 객체는 내려가고, 배열·원시값·빈 객체는 잎으로 둔다. */
179
+ export function flattenConfig(raw: Record<string, unknown>, prefix = '', out = new Map<string, unknown>()): Map<string, unknown> {
180
+ for (const [k, v] of Object.entries(raw)) {
181
+ const key = prefix ? `${prefix}.${k}` : k;
182
+ if (v && typeof v === 'object' && !Array.isArray(v) && Object.keys(v).length > 0) {
183
+ flattenConfig(v as Record<string, unknown>, key, out);
184
+ } else {
185
+ out.set(key, v);
186
+ }
187
+ }
188
+ return out;
189
+ }
190
+
191
+ function stableStringify(v: unknown): string {
192
+ if (Array.isArray(v)) return `[${v.map(stableStringify).join(',')}]`;
193
+ if (v && typeof v === 'object') {
194
+ return `{${Object.keys(v).sort().map((k) => `${JSON.stringify(k)}:${stableStringify((v as Record<string, unknown>)[k])}`).join(',')}}`;
195
+ }
196
+ return JSON.stringify(v) ?? 'undefined';
197
+ }
198
+
199
+ /** 잎 안의 구조(배열 속 객체 등)도 내려가 비밀 이름 키의 값을 가린다 — `telegram.channels[].botToken` 같은 자리. */
200
+ export function maskDeep(value: unknown): unknown {
201
+ if (isSecretValue(value)) return MASK;
202
+ if (Array.isArray(value)) return value.map(maskDeep);
203
+ if (value && typeof value === 'object') {
204
+ return Object.fromEntries(Object.entries(value as Record<string, unknown>)
205
+ .map(([k, v]) => [k, isSecretName(k) ? MASK : maskDeep(v)]));
206
+ }
207
+ return value;
208
+ }
209
+
210
+ /** 표시용 값 — 비밀 경로면 가린다, 아니면 구조 안 비밀 키 가림 ⊕ 텍스트 축 가림. */
211
+ export function displayValue(key: string, value: unknown, present = true): string {
212
+ if (!present) return '(없음)';
213
+ if (isSecretPath(key) || isSecretValue(value)) return MASK;
214
+ const s = typeof value === 'string' ? JSON.stringify(value) : stableStringify(maskDeep(value));
215
+ const red = redactSecretText(s);
216
+ return red.length > 160 ? `${red.slice(0, 157)}...` : red;
217
+ }
218
+
219
+ function matchesKey(key: string, filter?: string): boolean {
220
+ if (!filter) return true;
221
+ return key === filter || key.startsWith(`${filter}.`);
222
+ }
223
+
224
+ // ── 드리프트 ──
225
+
226
+ export interface DriftValueGroup {
227
+ /** 표시값(가림 적용). 없으면 '(없음)'. */
228
+ value: string;
229
+ present: boolean;
230
+ count: number;
231
+ examples: string[];
232
+ }
233
+
234
+ export interface DriftRow {
235
+ key: string;
236
+ prodPresent: boolean;
237
+ prodValue: string;
238
+ /** 운영과 다른 파생 우주 수(비교 대상 중). */
239
+ differing: number;
240
+ /** 운영엔 있는데 파생엔 없음. */
241
+ missingInDerived: number;
242
+ /** 운영엔 없는데 파생엔 있음. */
243
+ extraInDerived: number;
244
+ /** 둘 다 있는데 값이 다름. */
245
+ valueDiffers: number;
246
+ groups: DriftValueGroup[];
247
+ }
248
+
249
+ export interface DriftReport {
250
+ prodConfigPath: string;
251
+ prodStatus: UniverseStatus;
252
+ prodError?: string;
253
+ keyFilter?: string;
254
+ /** false(기본)면 «의도된 차이»를 드리프트로 안 센다 — `config sync-test` 의 test-safe 변환 결과와 같은 값 ⊕ `_test*` 메타 키. */
255
+ includeExpected: boolean;
256
+ universes: { total: number; compared: number; noConfig: number; unreadable: number; gone: number };
257
+ registry: DiscoveryResult['registry'];
258
+ treeScanRoots: string[];
259
+ unreadable: Array<{ configPath: string; error: string }>;
260
+ rows: DriftRow[];
261
+ }
262
+
263
+ const EXAMPLES = 3;
264
+
265
+ /** 사본 메타 키(`_testSyncedAt`·`_testSyncedFrom`·`_testSecretsStripped`) — 우주마다 다른 게 정상이다. */
266
+ export function isSyncMetaKey(key: string): boolean {
267
+ return key.startsWith('_test');
268
+ }
269
+
270
+ export function computeDrift(d: DiscoveryResult, opts: { key?: string; includeExpected?: boolean } = {}): DriftReport {
271
+ const includeExpected = opts.includeExpected === true;
272
+ const compared = d.derived.filter((u) => u.status === 'ok');
273
+ const unreadable = d.derived.filter((u) => u.status === 'unreadable');
274
+ const report: DriftReport = {
275
+ prodConfigPath: d.prod.configPath,
276
+ prodStatus: d.prod.status,
277
+ ...(d.prod.error ? { prodError: d.prod.error } : {}),
278
+ ...(opts.key ? { keyFilter: opts.key } : {}),
279
+ includeExpected,
280
+ universes: {
281
+ total: d.derived.length,
282
+ compared: compared.length,
283
+ noConfig: d.derived.filter((u) => u.status === 'no-config').length,
284
+ unreadable: unreadable.length,
285
+ gone: d.derived.filter((u) => u.status === 'gone').length,
286
+ },
287
+ registry: d.registry,
288
+ treeScanRoots: d.treeScanRoots,
289
+ unreadable: unreadable.map((u) => ({ configPath: u.configPath, error: u.error ?? '알 수 없음' })),
290
+ rows: [],
291
+ };
292
+ // ⛔ 운영을 못 읽으면 비교 자체를 하지 않는다 — 「드리프트 0」으로 보이면 안 된다.
293
+ if (d.prod.status !== 'ok' || !d.prod.raw) return report;
294
+
295
+ const prodFlat = flattenConfig(d.prod.raw);
296
+ // 「의도된 차이」의 자 — 오늘 운영 config 로 sync-test 를 돌리면 사본에 박힐 값.
297
+ const safeFlat = includeExpected ? null : flattenConfig(buildTestSafeRawConfig(d.prod.raw));
298
+ const derivedFlat = compared.map((u) => ({ u, flat: flattenConfig(u.raw!) }));
299
+ const keys = new Set<string>([...prodFlat.keys()].filter((k) => matchesKey(k, opts.key)));
300
+ for (const { flat } of derivedFlat) for (const k of flat.keys()) if (matchesKey(k, opts.key)) keys.add(k);
301
+
302
+ for (const key of [...keys].sort()) {
303
+ if (!includeExpected && isSyncMetaKey(key)) continue;
304
+ const prodPresent = prodFlat.has(key);
305
+ const prodSig = prodPresent ? stableStringify(prodFlat.get(key)) : undefined;
306
+ const safePresent = safeFlat?.has(key) ?? false;
307
+ const safeSig = safePresent ? stableStringify(safeFlat!.get(key)) : undefined;
308
+ const groups = new Map<string, DriftValueGroup>();
309
+ let missingInDerived = 0, extraInDerived = 0, valueDiffers = 0;
310
+ for (const { u, flat } of derivedFlat) {
311
+ const present = flat.has(key);
312
+ const sig = present ? stableStringify(flat.get(key)) : undefined;
313
+ if (present === prodPresent && sig === prodSig) continue;
314
+ if (safeFlat && present === safePresent && sig === safeSig) continue; // test-safe 변환 그대로 — 의도된 차이
315
+ if (!present) missingInDerived++;
316
+ else if (!prodPresent) extraInDerived++;
317
+ else valueDiffers++;
318
+ const groupKey = present ? `v:${sig}` : 'absent';
319
+ const g = groups.get(groupKey) ?? {
320
+ // 비밀 경로는 값 그룹마다 번호만 — 서로 다른지는 보이되 값은 안 보인다.
321
+ value: present ? (isSecretPath(key) ? `${MASK}#${groups.size + 1}` : displayValue(key, flat.get(key))) : '(없음)',
322
+ present, count: 0, examples: [],
323
+ };
324
+ g.count++;
325
+ if (g.examples.length < EXAMPLES) g.examples.push(u.configPath);
326
+ groups.set(groupKey, g);
327
+ }
328
+ const differing = missingInDerived + extraInDerived + valueDiffers;
329
+ if (differing === 0) continue;
330
+ report.rows.push({
331
+ key, prodPresent,
332
+ prodValue: displayValue(key, prodFlat.get(key), prodPresent),
333
+ differing, missingInDerived, extraInDerived, valueDiffers,
334
+ groups: [...groups.values()].sort((a, b) => b.count - a.count),
335
+ });
336
+ }
337
+ report.rows.sort((a, b) => b.differing - a.differing || a.key.localeCompare(b.key));
338
+ return report;
339
+ }
340
+
341
+ function walk(root: unknown, key: string): { present: boolean; value?: unknown } {
342
+ let cur: unknown = root;
343
+ for (const part of key.split('.').filter(Boolean)) {
344
+ if (cur === null || typeof cur !== 'object' || !(part in (cur as Record<string, unknown>))) return { present: false };
345
+ cur = (cur as Record<string, unknown>)[part];
346
+ }
347
+ return { present: true, value: cur };
348
+ }
349
+
350
+ function shortHome(p: string): string {
351
+ const h = homedir();
352
+ return p.startsWith(`${h}/`) ? `~${p.slice(h.length)}` : p;
353
+ }
354
+
355
+ function universeSummaryLine(r: DriftReport): string {
356
+ const u = r.universes;
357
+ return `파생 우주 ${u.total} (비교 ${u.compared} · config 없음 ${u.noConfig} · 못 읽음 ${u.unreadable} · 사라짐 ${u.gone})`
358
+ + ` · 레지스트리 ${r.registry.path === null ? '끔' : r.registry.ok ? `test ${r.registry.testEntries}/${r.registry.entries}` : `못 읽음${r.registry.error ? `(${r.registry.error})` : ''}`}`
359
+ + ` · 트리 뿌리 ${r.treeScanRoots.length}`;
360
+ }
361
+
362
+ export function renderDrift(r: DriftReport, opts: { limit?: number } = {}): string {
363
+ const lines: string[] = [];
364
+ lines.push(`config drift — 집 ${shortHome(r.prodConfigPath)}${r.keyFilter ? ` · 키 ${r.keyFilter}` : ''}`
365
+ + (r.includeExpected ? ' · 의도된 차이 포함' : ' · 의도된 차이(test-safe 변환·_test 메타) 제외 — --include-expected'));
366
+ lines.push(` ${universeSummaryLine(r)}`);
367
+ if (r.prodStatus !== 'ok') {
368
+ lines.push(` ⛔ 운영 config 못 읽음(${r.prodStatus}${r.prodError ? `: ${r.prodError}` : ''}) — 비교하지 않았다(「드리프트 0」이 아니다)`);
369
+ }
370
+ if (r.universes.compared === 0 && r.prodStatus === 'ok') lines.push(' ⚠️ 비교한 파생 우주 0 — 「드리프트 0」이 아니라 «잴 대상이 없었다»');
371
+ const limit = opts.limit ?? 50;
372
+ const shown = limit > 0 ? r.rows.slice(0, limit) : r.rows;
373
+ lines.push(` 드리프트 키 ${r.rows.length}`);
374
+ for (const row of shown) {
375
+ const parts: string[] = [];
376
+ if (row.missingInDerived) parts.push(`파생에 없음 ${row.missingInDerived}`);
377
+ if (row.extraInDerived) parts.push(`운영에 없음 ${row.extraInDerived}`);
378
+ if (row.valueDiffers) parts.push(`값 다름 ${row.valueDiffers}`);
379
+ lines.push(`\n ${row.key} ${row.differing}/${r.universes.compared} 우주 다름 (${parts.join(' · ')})`);
380
+ lines.push(` 운영: ${row.prodValue}`);
381
+ for (const g of row.groups.slice(0, 4)) {
382
+ lines.push(` ${g.value} × ${g.count} 예: ${g.examples.map(shortHome).join(', ')}`);
383
+ }
384
+ if (row.groups.length > 4) lines.push(` … 값 묶음 ${row.groups.length - 4}개 더 (--json)`);
385
+ }
386
+ if (shown.length < r.rows.length) lines.push(`\n … ${r.rows.length - shown.length}개 키 더 — --limit 0 또는 --json (⚠️ 잘린 표다)`);
387
+ if (r.unreadable.length) {
388
+ lines.push('\n 못 읽음 (같다고 보지 않음):');
389
+ for (const u of r.unreadable.slice(0, 20)) lines.push(` ${shortHome(u.configPath)} — ${u.error}`);
390
+ if (r.unreadable.length > 20) lines.push(` … ${r.unreadable.length - 20}개 더 (--json)`);
391
+ }
392
+ return lines.join('\n');
393
+ }
394
+
395
+ // ── explain ──
396
+
397
+ export type ProdSourceLayer = 'prod-file' | 'llm-fallback' | 'code-default' | 'unset' | 'prod-unreadable';
398
+
399
+ export interface ExplainReport {
400
+ key: string;
401
+ prod: { configPath: string; layer: ProdSourceLayer; value: string; note?: string };
402
+ codeDefault: { present: boolean; value: string };
403
+ /** false 면 운영을 못 읽어 파생과 «비교하지 않았다» — groups 는 비고 「다름」으로 세지 않는다. */
404
+ comparable?: boolean;
405
+ universes: DriftReport['universes'];
406
+ registry: DiscoveryResult['registry'];
407
+ treeScanRoots: string[];
408
+ /** 파생 우주 값 묶음 — 운영과 같은 것도 포함(sameAsProd). */
409
+ groups: Array<DriftValueGroup & { sameAsProd: boolean }>;
410
+ unreadable: Array<{ configPath: string; error: string }>;
411
+ }
412
+
413
+ export interface ExplainDeps {
414
+ /** 코드 기본값 객체(파일 없이 만든 유효 config). ⛔ 필수 — 없으면 「기본값 없음」과 「모름」이 섞인다. */
415
+ codeDefaults: () => unknown;
416
+ /** `<prodRoot>/llm-fallback.json` 원문. */
417
+ llmFallbackPath?: string;
418
+ }
419
+
420
+ /** `buildUserConfig` 의 llm-fallback 적용 조건과 같은 판정 — provider 가 없거나 auto/none 이면 «빈» llm 절. */
421
+ export function isLlmSectionEmptyRaw(llm: unknown): boolean {
422
+ if (!llm || typeof llm !== 'object' || Array.isArray(llm)) return true;
423
+ const provider = (llm as Record<string, unknown>).provider;
424
+ return typeof provider !== 'string' || provider.trim() === '' || provider === 'auto' || provider === 'none';
425
+ }
426
+
427
+ export function computeExplain(d: DiscoveryResult, key: string, deps: ExplainDeps): ExplainReport {
428
+ const defaults = walk(deps.codeDefaults(), key);
429
+ const codeDefault = { present: defaults.present, value: displayValue(key, defaults.value, defaults.present) };
430
+
431
+ let prod: ExplainReport['prod'];
432
+ if (d.prod.status !== 'ok' || !d.prod.raw) {
433
+ prod = { configPath: d.prod.configPath, layer: 'prod-unreadable', value: '(못 읽음)', note: d.prod.error ?? d.prod.status };
434
+ } else {
435
+ const fromFile = walk(d.prod.raw, key);
436
+ if (fromFile.present) {
437
+ prod = { configPath: d.prod.configPath, layer: 'prod-file', value: displayValue(key, fromFile.value) };
438
+ } else {
439
+ let fb: { present: boolean; value?: unknown } = { present: false };
440
+ const fbPath = deps.llmFallbackPath ?? join(d.prod.configDir, 'llm-fallback.json');
441
+ // 폴백 파일은 운영 llm 절이 «비었을 때만» 합쳐진다 — 그 조건이 아닐 땐 출처 층이 아니다.
442
+ if (key.startsWith('llm.') && isLlmSectionEmptyRaw(d.prod.raw.llm) && existsSync(fbPath)) {
443
+ try { fb = walk(JSON.parse(readFileSync(fbPath, 'utf-8')), key.slice(4)); } catch { /* 못 읽으면 폴백 층 아님 */ }
444
+ }
445
+ if (fb.present) {
446
+ prod = { configPath: fbPath, layer: 'llm-fallback', value: displayValue(key, fb.value), note: '운영 llm 절이 비어 폴백 파일이 쓰인다' };
447
+ } else if (defaults.present) {
448
+ prod = { configPath: d.prod.configPath, layer: 'code-default', value: codeDefault.value, note: '파일에 없음 — 코드 기본값' };
449
+ } else {
450
+ prod = { configPath: d.prod.configPath, layer: 'unset', value: '(없음)' };
451
+ }
452
+ }
453
+ }
454
+
455
+ const prodRaw = d.prod.raw ? walk(d.prod.raw, key) : { present: false };
456
+ const prodSig = prodRaw.present ? stableStringify(prodRaw.value) : undefined;
457
+ const groups = new Map<string, DriftValueGroup & { sameAsProd: boolean }>();
458
+ const compared = d.derived.filter((u) => u.status === 'ok');
459
+ const comparable = d.prod.status === 'ok' && !!d.prod.raw;
460
+ for (const u of comparable ? compared : []) {
461
+ const w = walk(u.raw, key);
462
+ const sig = w.present ? stableStringify(w.value) : undefined;
463
+ const gk = w.present ? `v:${sig}` : 'absent';
464
+ const sameAsProd = w.present === prodRaw.present && sig === prodSig;
465
+ const g = groups.get(gk) ?? {
466
+ value: w.present
467
+ ? (isSecretPath(key) ? `${MASK}#${groups.size + 1}` : displayValue(key, w.value))
468
+ : codeDefault.present ? `(없음 → 코드 기본값 ${codeDefault.value})` : '(없음 · 코드 기본값도 없음)',
469
+ present: w.present, count: 0, examples: [], sameAsProd,
470
+ };
471
+ g.count++;
472
+ if (g.examples.length < EXAMPLES) g.examples.push(u.configPath);
473
+ groups.set(gk, g);
474
+ }
475
+ const unreadable = d.derived.filter((u) => u.status === 'unreadable');
476
+ return {
477
+ key, prod, codeDefault, comparable,
478
+ universes: {
479
+ total: d.derived.length,
480
+ compared: compared.length,
481
+ noConfig: d.derived.filter((u) => u.status === 'no-config').length,
482
+ unreadable: unreadable.length,
483
+ gone: d.derived.filter((u) => u.status === 'gone').length,
484
+ },
485
+ registry: d.registry,
486
+ treeScanRoots: d.treeScanRoots,
487
+ groups: [...groups.values()].sort((a, b) => Number(a.sameAsProd) - Number(b.sameAsProd) || b.count - a.count),
488
+ unreadable: unreadable.map((u) => ({ configPath: u.configPath, error: u.error ?? '알 수 없음' })),
489
+ };
490
+ }
491
+
492
+ export function renderExplain(r: ExplainReport): string {
493
+ const lines: string[] = [];
494
+ lines.push(`config explain ${r.key}`);
495
+ lines.push(` 운영 값: ${r.prod.value}`);
496
+ lines.push(` 출처 층: ${r.prod.layer} (${shortHome(r.prod.configPath)})${r.prod.note ? ` — ${r.prod.note}` : ''}`);
497
+ lines.push(` 코드 기본값: ${r.codeDefault.value}`);
498
+ lines.push(` ${universeSummaryLine({ universes: r.universes, registry: r.registry, treeScanRoots: r.treeScanRoots } as DriftReport)}`);
499
+ if (r.comparable === false) {
500
+ lines.push(' ⛔ 운영 config 를 못 읽어 파생 우주와 비교하지 않았다(「다름」도 「같음」도 아니다)');
501
+ } else {
502
+ const differing = r.groups.filter((g) => !g.sameAsProd).reduce((s, g) => s + g.count, 0);
503
+ lines.push(` 운영과 다른 우주 ${differing}/${r.universes.compared}`);
504
+ }
505
+ for (const g of r.groups) {
506
+ lines.push(` ${g.sameAsProd ? '= 운영과 같음' : '≠'} ${g.value} × ${g.count} 예: ${g.examples.map(shortHome).join(', ')}`);
507
+ }
508
+ if (r.unreadable.length) {
509
+ lines.push(' 못 읽음 (같다고 보지 않음):');
510
+ for (const u of r.unreadable.slice(0, 20)) lines.push(` ${shortHome(u.configPath)} — ${u.error}`);
511
+ }
512
+ return lines.join('\n');
513
+ }
@@ -775,7 +775,9 @@ export const KNOWN_LOG_EVENT_NAMES: ReadonlySet<string> = new Set([
775
775
  "manifest-frame-fail",
776
776
  "manual-goal-evidence-requirement",
777
777
  "map",
778
+ "mark-failed",
778
779
  "mark-read",
780
+ "marked",
779
781
  "marker-readback",
780
782
  "marker-readback-exhausted",
781
783
  "materialized",
@@ -1217,6 +1219,7 @@ export const KNOWN_LOG_EVENT_NAMES: ReadonlySet<string> = new Set([
1217
1219
  "self-orchestrate-exposure",
1218
1220
  "self-turn",
1219
1221
  "send",
1222
+ "send-failed",
1220
1223
  "send.encoder.error",
1221
1224
  "send.error",
1222
1225
  "send.pcm.error",
@@ -1249,6 +1252,7 @@ export const KNOWN_LOG_EVENT_NAMES: ReadonlySet<string> = new Set([
1249
1252
  "showMenu.rejected",
1250
1253
  "showMenu.resolved",
1251
1254
  "shutdown",
1255
+ "sibling-resync-on-merge-failed",
1252
1256
  "sigint",
1253
1257
  "signal-cleared",
1254
1258
  "signal-consumed-cleared",
package/src/cli/pr-cli.ts CHANGED
@@ -22,6 +22,7 @@ import { exportLeakCheck, publicExportChangedFiles, type ExportLeakCheck } from
22
22
  import { exportImportCheck, type ExportImportCheck } from '../../scripts/ci-public-export-import-gate.js';
23
23
  import { checkCommands, extractElanousCommands, type Finding as DocsCliFinding } from '../../scripts/docs-cli-check.js';
24
24
  import { runTestInterferenceGate } from '../../scripts/ci-test-interference-gate.js';
25
+ import { landGlobalChecks, runLandGlobalChecksGate } from '../../scripts/land-global-checks.js';
25
26
  import { isGoalDocumentFileName } from '../self-implement/goal-document.js';
26
27
  import { queryFederatedUnfinishedRunLedgers, type FederatedUnfinishedRunLedgerEntry, type FederatedUnfinishedRunLedgerQuery } from '../self-implement/run-ledger.js';
27
28
  import { queryRunningRuns, type RunningRunsResult } from '../self-implement/running-runs.js';
@@ -120,6 +121,8 @@ export interface PrLandDeps {
120
121
  runDocsCliCheck?: (docFiles: readonly string[]) => DocsCliFinding[];
121
122
  /** 변경 시험 파일 간 간섭 검사 심(시험 주입용). */
122
123
  runTestInterferenceGate?: (out: { log: (message: string) => void; error: (message: string) => void }, changedFiles: readonly string[]) => Promise<number>;
124
+ /** 전역 검사 게이트 심(시험 주입용) — 바뀐 파일 꼴로 끌려온 전역 시험(scripts/land-global-checks.ts)을 돌린다. */
125
+ runLandGlobalChecks?: (out: { log: (message: string) => void; error: (message: string) => void }, changedFiles: readonly string[]) => boolean;
123
126
  /** 안드로이드 단위 시험 게이트 심(시험 주입용). */
124
127
  runAndroidGate?: (out: { log: (message: string) => void; error: (message: string) => void }, changedFiles: readonly string[]) => boolean;
125
128
  /** iOS 시험 게이트 심(시험 주입용). */
@@ -1471,6 +1474,23 @@ export async function runPrLand(opts: PrLandOpts = {}, deps: PrLandDeps = {}): P
1471
1474
  out,
1472
1475
  );
1473
1476
  record('test-interference-gate', true, { measured: testInterference.measured });
1477
+ // LAND-GLOBAL-CHECKS — 바뀐 파일의 경로 꼴이 부르는 «전역 검사»를 끌어와 돌린다(표 = scripts/land-global-checks.ts).
1478
+ // 시험이 실패하면 막는다 · 러너가 못 돌았으면 「검사 못 함」으로 말하고 통과시킨다(gateVerdict).
1479
+ const landGlobal = gateVerdict(
1480
+ 'land-global-checks',
1481
+ deps.runLandGlobalChecks
1482
+ ? (o) => deps.runLandGlobalChecks!(o, changedPaths)
1483
+ : (o) => runLandGlobalChecksGate({ changedFiles: changedPaths, cwd, log: o.log, error: o.error }),
1484
+ out,
1485
+ );
1486
+ record('land-global-checks', landGlobal.ok, { measured: landGlobal.measured });
1487
+ if (!landGlobal.ok) {
1488
+ out.error('✗ land-global-checks: scripts/land-global-checks.ts blocked pr land — 바뀐 파일이 끌어온 전역 시험이 실패했다.');
1489
+ return 1;
1490
+ }
1491
+ if (landGlobal.measured && landGlobalChecks(changedPaths).length > 0) {
1492
+ out.log(`✓ land-global-checks: scripts/land-global-checks.ts PASS — 끌려온 전역 시험 ${landGlobalChecks(changedPaths).join(' · ')} 실패 0.`);
1493
+ }
1474
1494
 
1475
1495
  const androidGate = gateVerdict(
1476
1496
  'android-gate',
@@ -45,6 +45,7 @@ import type { PrereleaseKind } from '../../scripts/release-loop/release-version.
45
45
  import { releaseReadiness } from '../../scripts/release-loop/release-readiness.js';
46
46
  import { runReleaseIfReady } from './release-run-if-ready.js';
47
47
  import { formatPreflight, releasePreflight } from '../../scripts/release-loop/preflight.js';
48
+ import { defaultReleaseGraphRunsRoot, formatPublishProposal, measurePublishWindow, proposePublishAt, publishWindowWarning } from '../../scripts/release-loop/publish-window.js';
48
49
  import { autoStartScheduledRelease, type AutoStartDeferral } from '../release-loop/auto-start.js';
49
50
  import { landingFreezeMessage, LandingFrozenError, readLandingFreeze } from '../release-loop/landing-freeze.js';
50
51
  import { formatLightRc, runLightRc, type LightRcDeps } from '../release-loop/light-rc.js';
@@ -609,6 +610,8 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
609
610
  }
610
611
  return hqCliWriteAllowed(command, override, ledgerFenceDeps());
611
612
  };
613
+ // PUBLISH-TIME-FROM-GATE: the measured cut→publish window comes from the same ledger as the schedule (prod by default).
614
+ const publishMeasure = () => measurePublishWindow({ root: defaultReleaseGraphRunsRoot(scheduleLedgerRoot()) });
612
615
  schedule.command('list').option('--json', '결과 JSON').action((o: { json?: boolean }) => {
613
616
  const rows = listSchedules(scheduleLedgerRoot());
614
617
  if (o.json) console.log(JSON.stringify(rows));
@@ -618,15 +621,27 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
618
621
  .action((o: { version: string; json?: boolean }) => {
619
622
  const row = getSchedule(o.version, scheduleLedgerRoot());
620
623
  if (!row) throw new CliUserError(`없는 판 일정: ${o.version}`, '판 일정 등록: elanous release schedule set --version <v> --cut-at <iso> --land-by <iso>');
621
- if (o.json) console.log(JSON.stringify(row)); else console.log(formatSchedule(row));
624
+ const measure = publishMeasure();
625
+ const proposedPublishAt = proposePublishAt(row.cutAt, measure);
626
+ if (o.json) console.log(JSON.stringify({ ...row, proposedPublishAt, measuredPublishMedianSeconds: measure.medianSeconds, measuredPublishSamples: measure.samples.map(({ version, seconds }) => ({ version, seconds })) }));
627
+ else { console.log(formatSchedule(row)); console.log(formatPublishProposal(row.cutAt, measure)); }
622
628
  });
623
629
  schedule.command('set').requiredOption('--version <v>', '갱신할 판')
624
630
  .option('--cut-at <iso>', '오프셋 포함 컷 시각').option('--land-by <iso>', '오프셋 포함 착지 마감')
625
- .option('--freeze-from <iso>', '동결 시작').option('--freeze-until <iso>', '동결 종료').option('--json', '결과 JSON').option('--hq-override')
626
- .action((o: { version: string; cutAt?: string; landBy?: string; freezeFrom?: string; freezeUntil?: string; json?: boolean; hqOverride?: boolean }) => {
627
- if (o.cutAt === undefined && o.landBy === undefined && o.freezeFrom === undefined && o.freezeUntil === undefined) throw new CliUserError('갱신할 시각을 지정하라', '--cut-at <iso> 또는 --land-by <iso>');
631
+ .option('--freeze-from <iso>', '동결 시작').option('--freeze-until <iso>', '동결 종료')
632
+ .option('--publish-at <iso>', '오프셋 포함 발행 시각(실측 컷→발행 중앙값보다 짧으면 경고 · 제안은 schedule show)')
633
+ .option('--json', '결과 JSON').option('--hq-override')
634
+ .action((o: { version: string; cutAt?: string; landBy?: string; freezeFrom?: string; freezeUntil?: string; publishAt?: string; json?: boolean; hqOverride?: boolean }) => {
635
+ if (o.cutAt === undefined && o.landBy === undefined && o.freezeFrom === undefined && o.freezeUntil === undefined && o.publishAt === undefined) throw new CliUserError('갱신할 시각을 지정하라', '--cut-at <iso> 또는 --land-by <iso> 또는 --publish-at <iso>');
628
636
  if (!mayWriteLedger('release schedule set', Boolean(o.hqOverride), scheduleLedgerRoot())) return;
629
- const row = setSchedule(o.version, { cutAt: o.cutAt, landBy: o.landBy, freezeFrom: o.freezeFrom, freezeUntil: o.freezeUntil }, process.env.ELANOUS_TRACK || 'cli', scheduleLedgerRoot());
637
+ const row = setSchedule(o.version, { cutAt: o.cutAt, landBy: o.landBy, freezeFrom: o.freezeFrom, freezeUntil: o.freezeUntil, publishAt: o.publishAt }, process.env.ELANOUS_TRACK || 'cli', scheduleLedgerRoot());
638
+ // A stored publish time shorter than the measured median warns on stderr (one line) — --json stdout stays parseable.
639
+ if (row.publishAt && (o.publishAt !== undefined || o.cutAt !== undefined)) {
640
+ const measure = publishMeasure();
641
+ const warning = publishWindowWarning(row.cutAt, row.publishAt, measure);
642
+ debug.log('release.schedule', 'publish-window', { version: row.version, cutAt: row.cutAt, publishAt: row.publishAt, medianSeconds: measure.medianSeconds, samples: measure.samples.length, warned: warning !== null });
643
+ if (warning) console.error(warning);
644
+ }
630
645
  if (o.json) console.log(JSON.stringify(row)); else console.log(formatSchedule(row));
631
646
  });
632
647
  release.command('place <id>').description('칸의 우선순위·마감·용량으로 판 배치')