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
@@ -31,6 +31,8 @@ export interface LoopEdge {
31
31
  tick?: string;
32
32
  /** live claimed 뒤 handed·released 없이 N분 지남 = 발사 실패(끊긴 넘김). ref = 오케스트레이터 카드 id. */
33
33
  broken?: true;
34
+ /** launch 간선만 — 그 카드에 묶인 런 원장 runId(ID 정규식). 못 묶으면 생략(PWA 가 10분 뒤 «유령»으로 본다). */
35
+ run?: string;
34
36
  }
35
37
 
36
38
  /** 노드 접두 — 조율 루프는 레지스트리 행 `loop:orchestrator` 그대로 쓴다. `queue:` 는 대기열 대체 간선 자리(조각 A 밖). */
@@ -369,10 +371,10 @@ function taskAgentEdges(deps: LoopEdgesDeps, facts: readonly RunFact[], seats: R
369
371
  if (typeof card.mission === 'string' && ID.test(card.mission)) journey.links.push({ from: card.id, to: card.mission, both: false });
370
372
  if (Array.isArray(card.pieces)) journey.pieces.set(card.id, card.pieces.filter(piece => typeof piece === 'string' && ID.test(piece)));
371
373
  const launches = launchesOf(card);
374
+ const runId = linkedRun(card, launches[0]?.at, launchedTexts, facts, childIds);
372
375
  for (const launch of launches) {
373
- if (seat(card.seat, seats) && inWindow(launch.at, since, now)) edges.push({ at: launch.at, kind: 'launch', from: TASK_AGENT_NODE, to: card.seat, ref: card.id });
376
+ if (seat(card.seat, seats) && inWindow(launch.at, since, now)) edges.push({ at: launch.at, kind: 'launch', from: TASK_AGENT_NODE, to: card.seat, ref: card.id, ...(runId ? { run: runId } : {}) });
374
377
  }
375
- const runId = linkedRun(card, launches[0]?.at, launchedTexts, facts, childIds);
376
378
  if (runId) journey.links.push({ from: card.id, to: runId, both: true });
377
379
  const green = object(card.greenProposal);
378
380
  if (green && typeof green.at === 'string' && typeof green.checklistId === 'string' && ID.test(green.checklistId) && inWindow(green.at, since, now)) {
@@ -1,7 +1,8 @@
1
1
  import { lstatSync, readFileSync, readdirSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { debug, redactSecretText } from '../../debug/log.js';
4
- import { effectiveInstanceRoot } from '../../instance/resolve.js';
4
+ import { effectiveInstanceRoot, releaseLedgerRoot } from '../../instance/resolve.js';
5
+ import { gateShardsPath, readGateShards, summarizeShards, type GateShardsFile, type GateShardsSummary } from '../../release-loop/gate-shards.js';
5
6
  import { listChecklist } from '../../release-loop/checklist.js';
6
7
  import { getSchedule } from '../../release-loop/release-schedule.js';
7
8
  import { jsonResponse } from './json-response.js';
@@ -123,13 +124,25 @@ export function releaseNodeFacts(output: unknown): ReleaseNodeFacts {
123
124
  }
124
125
  export interface ReleaseRunView {
125
126
  runId: string; status: string; startedAt: string; version: string | null; path: string[]; nodes: ReleaseRunNodeView[];
127
+ /** GATE-LIVE-OBS — 지금 gate 노드에 있는 런에만 싣는다(발행 원장 `gate-logs/cut/shards.json`). 파일이 없으면 생략. */
128
+ gateShards?: GateShardsFile & { summary: GateShardsSummary };
129
+ }
130
+
131
+ /** 도는 gate 노드의 조각 표 — 다른 판의 파일을 붙이지 않게 version 이 같을 때만. */
132
+ function gateShardsFor(version: string | null, status: string, nodes: ReleaseRunNodeView[], ledgerRoot: string, now: number):
133
+ ReleaseRunView['gateShards'] | undefined {
134
+ if (!version || status !== 'running' || !nodes.some((node) => node.nodeId === 'gate' && node.ok === null)) return undefined;
135
+ const file = readGateShards(gateShardsPath(version, ledgerRoot));
136
+ if (!file || file.version !== version) return undefined;
137
+ return { ...file, shards: file.shards.map((shard) => shard.waitReason ? { ...shard, waitReason: redactSecretText(shard.waitReason) } : shard),
138
+ summary: summarizeShards(file, now) };
126
139
  }
127
140
 
128
141
  /** The release run ledger as `/v1/ops/release/runs` serves it (newest 20) — shared with Telegram /release and away mode
129
142
  * so every surface reads the same verdicts. `'unavailable'` = the directory exists but cannot be read. */
130
143
  export function readReleaseRuns(version: string | null = null,
131
144
  dir: string = join(effectiveInstanceRoot(), 'graph-runs', 'release-loop'),
132
- opts: { facts?: boolean } = {}): ReleaseRunView[] | 'unavailable' {
145
+ opts: { facts?: boolean; ledgerRoot?: string; now?: number } = {}): ReleaseRunView[] | 'unavailable' {
133
146
  let filenames: string[];
134
147
  try {
135
148
  filenames = readdirSync(dir, { withFileTypes: true })
@@ -146,15 +159,17 @@ export function readReleaseRuns(version: string | null = null,
146
159
  if (!run) return [];
147
160
  const v = runVersion(run.input);
148
161
  if (version !== null && v !== version) return [];
162
+ const nodes: ReleaseRunNodeView[] = [...run.nodes.filter((node) => node && typeof node.nodeId === 'string').map((node) => ({
163
+ nodeId: redactSecretText(node.nodeId), ok: typeof node.ok === 'boolean' ? node.ok : null,
164
+ summary: nodeSummary(node.output), ...nodeTimes(node),
165
+ ...(opts.facts ? { facts: releaseNodeFacts(node.output) } : {}),
166
+ })), ...runningNode(run, run.nodes.length)];
167
+ const gateShards = gateShardsFor(v, run.status, nodes, opts.ledgerRoot ?? releaseLedgerRoot(), opts.now ?? Date.now());
149
168
  return [{
150
169
  runId: redactSecretText(filename.slice(0, -5)), status: redactSecretText(run.status),
151
170
  startedAt: redactSecretText(run.startedAt), version: v,
152
171
  path: run.path.filter((step): step is string => typeof step === 'string').map(redactSecretText),
153
- nodes: [...run.nodes.filter((node) => node && typeof node.nodeId === 'string').map((node) => ({
154
- nodeId: redactSecretText(node.nodeId), ok: typeof node.ok === 'boolean' ? node.ok : null,
155
- summary: nodeSummary(node.output), ...nodeTimes(node),
156
- ...(opts.facts ? { facts: releaseNodeFacts(node.output) } : {}),
157
- })), ...runningNode(run, run.nodes.length)],
172
+ nodes, ...(gateShards ? { gateShards } : {}),
158
173
  }];
159
174
  }).sort((a, b) => b.startedAt.localeCompare(a.startedAt)).slice(0, 20);
160
175
  }
@@ -0,0 +1,48 @@
1
+ import { canonicalSeatId, openMsgStore, type Message, type MsgStore } from '../../msg/msg-store.js';
2
+ import { jsonResponse } from './json-response.js';
3
+
4
+ function ownerNotices(store: Pick<MsgStore, 'list'>): Message[] {
5
+ const notices: Message[] = [];
6
+ let after = 0;
7
+ let batch: Message[];
8
+ do {
9
+ batch = store.list('CEO', after, 1000);
10
+ notices.push(...batch.filter(message => message.from !== 'CEO'));
11
+ if (batch.length) after = batch[batch.length - 1]!.id;
12
+ } while (batch.length === 1000);
13
+ return notices;
14
+ }
15
+
16
+ export const OPS_BOARD_PATH = '/v1/ops-board/notices';
17
+
18
+ export interface OpsBoardDeps {
19
+ authorize: (request: Request) => boolean;
20
+ openStore?: () => Pick<MsgStore, 'list' | 'post' | 'close'>;
21
+ }
22
+
23
+ /** Owner reads the CEO inbox without acknowledging it; replies are addressed to the original sender. */
24
+ export async function handleOpsBoard(req: Request, deps: OpsBoardDeps): Promise<Response> {
25
+ if (!deps.authorize(req)) return jsonResponse({ error: 'unauthorized' }, 401);
26
+ if (req.method !== 'GET' && req.method !== 'POST') return jsonResponse({ error: 'method-not-allowed' }, 405);
27
+ const store = (deps.openStore ?? openMsgStore)();
28
+ try {
29
+ if (req.method === 'GET') {
30
+ const notices = ownerNotices(store);
31
+ return jsonResponse({ notices });
32
+ }
33
+ let body: unknown;
34
+ try { body = await req.json(); } catch { return jsonResponse({ error: 'bad_request' }, 400); }
35
+ if (!body || typeof body !== 'object' || Array.isArray(body)) return jsonResponse({ error: 'bad_request' }, 400);
36
+ const input = body as Record<string, unknown>;
37
+ if (!Number.isSafeInteger(input.noticeId) || (input.noticeId as number) < 1
38
+ || typeof input.body !== 'string' || !input.body.trim() || input.body.length > 3000
39
+ || Object.keys(input).some(key => key !== 'noticeId' && key !== 'body')) return jsonResponse({ error: 'bad_request' }, 400);
40
+ const notice: Message | undefined = ownerNotices(store).find(message => message.id === input.noticeId);
41
+ if (!notice) return jsonResponse({ error: 'not-found' }, 404);
42
+ const recipient = canonicalSeatId(notice.from);
43
+ const reply = store.post({ from: 'CEO', to: recipient, body: input.body.trim(), kind: 'reply' });
44
+ return jsonResponse({ reply }, 201);
45
+ } finally {
46
+ store.close();
47
+ }
48
+ }
@@ -5,4 +5,9 @@ export function unattendedSetupHint(): string {
5
5
  return `unattended: \`${UNATTENDED_SETUP_COMMAND}\``;
6
6
  }
7
7
 
8
+ /** One-line next step printed after a non-TTY onboarding refusal (FIRST-CHAT-NONTTY). */
9
+ export function unattendedNextStepLine(): string {
10
+ return `next: ${unattendedSetupHint()}`;
11
+ }
12
+
8
13
  export const SKILLS_LIST_DESCRIPTION = 'List elanous skills from configured skill directories (skills.activeSet / skills.dirs and packaged skills): subdirectories and the first non-heading line of each SKILL.md. Read-only.';
package/src/onboarding.ts CHANGED
@@ -59,7 +59,7 @@ import type {
59
59
  import { showStepOr, chooseFrom } from './onboarding/io-extended.js';
60
60
  import { askWithHelp } from './onboarding/wire-helpers.js';
61
61
  import { stepTransition } from './onboarding/transition.js';
62
- import { unattendedSetupHint } from './onboarding/entry-hints.js';
62
+ import { unattendedNextStepLine, unattendedSetupHint } from './onboarding/entry-hints.js';
63
63
  import { continueHereLines } from './onboarding/continue-here.js';
64
64
  import type { NexusShowResult } from './cli/nexus-show.js';
65
65
  import type { TailscaleProbe } from './nexus/onboarding/tailscale-probe.js';
@@ -1370,15 +1370,33 @@ export class OnboardingRefusedError extends Error {
1370
1370
  }
1371
1371
  }
1372
1372
 
1373
- /** Handle only typed refusals; the agent setup hint applies to its non-TTY path alone. */
1374
- export function handleOnboardingRefusal(error: unknown, entrance: 'agent' | 'other'): boolean {
1373
+ export interface OnboardingRefusalIO {
1374
+ printError: (line: string) => void;
1375
+ setExitCode: (code: number) => void;
1376
+ }
1377
+
1378
+ const defaultRefusalIO: OnboardingRefusalIO = {
1379
+ printError: (line) => console.error(line),
1380
+ setExitCode: (code) => { process.exitCode = code; },
1381
+ };
1382
+
1383
+ /** Handle only typed refusals; the agent setup hint applies to its non-TTY path alone.
1384
+ * FIRST-CHAT-NONTTY: on the non-TTY path of any other entrance the refusal ends with a
1385
+ * one-line next step (`unattendedNextStepLine()`), so a script or app that called first knows
1386
+ * what to run. The exit code stays 2. */
1387
+ export function handleOnboardingRefusal(
1388
+ error: unknown,
1389
+ entrance: 'agent' | 'other',
1390
+ io: OnboardingRefusalIO = defaultRefusalIO,
1391
+ ): boolean {
1375
1392
  if (!(error instanceof OnboardingRefusedError) || error.code !== 'onboarding-refused') return false;
1376
1393
  if (entrance === 'agent' && error.reason === 'non-tty') {
1377
- console.error(`elanous agent needs a configured LLM. Run \`elanous onboarding\` in a terminal, or ${unattendedSetupHint()} for unattended setup.`);
1394
+ io.printError(`elanous agent needs a configured LLM. Run \`elanous onboarding\` in a terminal, or ${unattendedSetupHint()} for unattended setup.`);
1378
1395
  } else {
1379
- console.error(error.message);
1396
+ io.printError(error.message);
1397
+ if (error.reason === 'non-tty') io.printError(unattendedNextStepLine());
1380
1398
  }
1381
- process.exitCode = 2;
1399
+ io.setExitCode(2);
1382
1400
  return true;
1383
1401
  }
1384
1402
 
@@ -36,7 +36,7 @@ const SCHEMA_COLUMNS: Record<string, readonly string[]> = {
36
36
  imported_versions: ['json_hash', 'imported_at'],
37
37
  events: ['reason', 'generation'],
38
38
  assignments: ['priority', 'predecessors', 'deadline_version', 'ceo_minutes', 'ceo_date', 'accelerator', 'refs'],
39
- release_schedules: ['freeze_from', 'freeze_until'],
39
+ release_schedules: ['freeze_from', 'freeze_until', 'publish_at'],
40
40
  };
41
41
  function schemaCurrent(db: Database): boolean {
42
42
  return Object.entries(SCHEMA_COLUMNS).every(([table, names]) => {
@@ -83,16 +83,16 @@ function open(root = releaseLedgerRoot()): Database {
83
83
  if (!assignmentColumns.some((column) => column.name === name)) db.exec(`ALTER TABLE assignments ADD COLUMN ${name} ${sql}`);
84
84
  }
85
85
  const scheduleColumns = db.query('PRAGMA table_info(release_schedules)').all() as Array<{ name: string }>;
86
- for (const name of ['freeze_from', 'freeze_until']) if (!scheduleColumns.some((column) => column.name === name)) db.exec(`ALTER TABLE release_schedules ADD COLUMN ${name} TEXT`);
86
+ for (const name of ['freeze_from', 'freeze_until', 'publish_at']) if (!scheduleColumns.some((column) => column.name === name)) db.exec(`ALTER TABLE release_schedules ADD COLUMN ${name} TEXT`);
87
87
  }).immediate();
88
88
  return db;
89
89
  } catch (error) { db.close(); throw error; }
90
90
  }
91
91
 
92
- export interface ScheduleRow { version: string; cutAt: string; landBy: string | null; freezeFrom?: string | null; freezeUntil?: string | null; updatedAt: string; updatedBy: string }
93
- type StoredSchedule = { version: string; cut_at: string; land_by: string | null; freeze_from: string | null; freeze_until: string | null; updated_at: string; updated_by: string };
92
+ export interface ScheduleRow { version: string; cutAt: string; landBy: string | null; freezeFrom?: string | null; freezeUntil?: string | null; publishAt?: string | null; updatedAt: string; updatedBy: string }
93
+ type StoredSchedule = { version: string; cut_at: string; land_by: string | null; freeze_from: string | null; freeze_until: string | null; publish_at?: string | null; updated_at: string; updated_by: string };
94
94
  function scheduleRow(row: StoredSchedule): ScheduleRow {
95
- return { version: row.version, cutAt: row.cut_at, landBy: row.land_by, ...(row.freeze_from ? { freezeFrom: row.freeze_from } : {}), ...(row.freeze_until ? { freezeUntil: row.freeze_until } : {}), updatedAt: row.updated_at, updatedBy: row.updated_by };
95
+ return { version: row.version, cutAt: row.cut_at, landBy: row.land_by, ...(row.freeze_from ? { freezeFrom: row.freeze_from } : {}), ...(row.freeze_until ? { freezeUntil: row.freeze_until } : {}), ...(row.publish_at ? { publishAt: row.publish_at } : {}), updatedAt: row.updated_at, updatedBy: row.updated_by };
96
96
  }
97
97
 
98
98
  export function readSchedule(version: string, root?: string): ScheduleRow | null {
@@ -119,7 +119,7 @@ export function assignedVersions(root = releaseLedgerRoot()): string[] {
119
119
  finally { db.close(); }
120
120
  }
121
121
 
122
- export function writeSchedule(version: string, patch: { cutAt?: string; landBy?: string; freezeFrom?: string; freezeUntil?: string }, by: string, root?: string): ScheduleRow {
122
+ export function writeSchedule(version: string, patch: { cutAt?: string; landBy?: string; freezeFrom?: string; freezeUntil?: string; publishAt?: string }, by: string, root?: string): ScheduleRow {
123
123
  validateVersion(version);
124
124
  const db = open(root);
125
125
  try {
@@ -130,19 +130,22 @@ export function writeSchedule(version: string, patch: { cutAt?: string; landBy?:
130
130
  const landBy = patch.landBy ?? previous?.land_by ?? null;
131
131
  const freezeFrom = patch.freezeFrom ?? previous?.freeze_from ?? null;
132
132
  const freezeUntil = patch.freezeUntil ?? previous?.freeze_until ?? null;
133
+ const publishAt = patch.publishAt ?? previous?.publish_at ?? null;
134
+ if (publishAt && Date.parse(publishAt) <= Date.parse(cutAt)) throw new CliUserError('발행 시각은 컷보다 뒤여야 한다');
133
135
  if (Boolean(freezeFrom) !== Boolean(freezeUntil) || (freezeFrom && freezeUntil && Date.parse(freezeFrom) >= Date.parse(freezeUntil))) throw new CliUserError('동결 시작·끝은 함께 주고 시작이 끝보다 앞서야 한다');
134
- if (previous && previous.cut_at === cutAt && previous.land_by === landBy && previous.freeze_from === freezeFrom && previous.freeze_until === freezeUntil) return scheduleRow(previous);
136
+ if (previous && previous.cut_at === cutAt && previous.land_by === landBy && previous.freeze_from === freezeFrom && previous.freeze_until === freezeUntil && (previous.publish_at ?? null) === publishAt) return scheduleRow(previous);
135
137
  const at = new Date().toISOString();
136
- db.query(`INSERT INTO release_schedules (version, cut_at, land_by, freeze_from, freeze_until, updated_at, updated_by) VALUES (?, ?, ?, ?, ?, ?, ?)
137
- ON CONFLICT(version) DO UPDATE SET cut_at=excluded.cut_at, land_by=excluded.land_by, freeze_from=excluded.freeze_from, freeze_until=excluded.freeze_until, updated_at=excluded.updated_at, updated_by=excluded.updated_by`)
138
- .run(version, cutAt, landBy, freezeFrom, freezeUntil, at, by);
138
+ db.query(`INSERT INTO release_schedules (version, cut_at, land_by, freeze_from, freeze_until, publish_at, updated_at, updated_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
139
+ ON CONFLICT(version) DO UPDATE SET cut_at=excluded.cut_at, land_by=excluded.land_by, freeze_from=excluded.freeze_from, freeze_until=excluded.freeze_until, publish_at=excluded.publish_at, updated_at=excluded.updated_at, updated_by=excluded.updated_by`)
140
+ .run(version, cutAt, landBy, freezeFrom, freezeUntil, publishAt, at, by);
139
141
  for (const [field, from, to] of [
140
142
  ['cut_at', previous?.cut_at ?? null, cutAt], ['land_by', previous?.land_by ?? null, landBy],
141
143
  ['freeze_from', previous?.freeze_from ?? null, freezeFrom], ['freeze_until', previous?.freeze_until ?? null, freezeUntil],
144
+ ['publish_at', previous?.publish_at ?? null, publishAt],
142
145
  ] as const) {
143
146
  if (from !== to) record(db, version, { at, by, id: '@version', field, from, to, released: releasedVersion(root), dev: devVersion() });
144
147
  }
145
- return { version, cutAt, landBy, updatedAt: at, updatedBy: by };
148
+ return { version, cutAt, landBy, ...(publishAt ? { publishAt } : {}), updatedAt: at, updatedBy: by };
146
149
  });
147
150
  } finally { db.close(); }
148
151
  }
@@ -0,0 +1,158 @@
1
+ // GATE-LIVE-OBS — 게이트 «조각 상태» 한 장(`<ledger>/release/<v>/gate-logs/cut/shards.json`)의 계약.
2
+ // 쓰는 쪽 = gate-node(TC · GATE-TIMEOUT-HEAL) · 읽는 쪽 = /v1/ops/release/runs ⊕ 외출 알림(UX).
3
+ // 로그가 아니라 발행 원장 파일인 이유: 게이트 사건은 실행 트리의 test 우주로 가지만 원장은 운영 한 곳이다(TC·OP 10-07 19:12).
4
+ // ⛔ 못 읽으면 `null` — 「조각 0」으로 읽지 않는다.
5
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
6
+ import { dirname, join } from 'node:path';
7
+ import { releaseLedgerRoot } from '../instance/resolve.js';
8
+
9
+ export const SHARD_STATES = ['pending', 'running', 'done', 'retry', 'timeout', 'failed'] as const;
10
+ export type ShardState = typeof SHARD_STATES[number];
11
+
12
+ export interface GateShard {
13
+ id: string;
14
+ state: ShardState;
15
+ startedAt?: string;
16
+ endedAt?: string;
17
+ /** 설치(install) 단계에 쓴 초. */
18
+ installSec?: number;
19
+ rc?: number;
20
+ /** 대기 이유(예: «CPU 부족 31.2/32»). pending 일 때 화면이 그대로 보인다. */
21
+ waitReason?: string;
22
+ /** 계획 분 — 남은 시간 추정의 재료. */
23
+ plannedMin?: number;
24
+ }
25
+
26
+ export interface GateShardsFile {
27
+ v: 1;
28
+ version: string;
29
+ updatedAt: string;
30
+ shards: GateShard[];
31
+ }
32
+
33
+ export function gateShardsPath(version: string, root: string = releaseLedgerRoot()): string {
34
+ return join(root, 'release', version, 'gate-logs', 'cut', 'shards.json');
35
+ }
36
+
37
+ const str = (value: unknown, max = 160): string | undefined =>
38
+ typeof value === 'string' && value.trim() ? value.trim().slice(0, max) : undefined;
39
+ const num = (value: unknown): number | undefined => typeof value === 'number' && Number.isFinite(value) ? value : undefined;
40
+
41
+ function shardOf(value: unknown): GateShard | null {
42
+ if (!value || typeof value !== 'object') return null;
43
+ const raw = value as Record<string, unknown>;
44
+ const id = str(raw.id, 80);
45
+ const state = SHARD_STATES.find((s) => s === raw.state);
46
+ if (!id || !state) return null;
47
+ const startedAt = str(raw.startedAt, 40);
48
+ const endedAt = str(raw.endedAt, 40);
49
+ const installSec = num(raw.installSec);
50
+ const rc = num(raw.rc);
51
+ const waitReason = str(raw.waitReason);
52
+ const plannedMin = num(raw.plannedMin);
53
+ return {
54
+ id, state,
55
+ ...(startedAt ? { startedAt } : {}), ...(endedAt ? { endedAt } : {}),
56
+ ...(installSec !== undefined ? { installSec } : {}), ...(rc !== undefined ? { rc } : {}),
57
+ ...(waitReason ? { waitReason } : {}), ...(plannedMin !== undefined ? { plannedMin } : {}),
58
+ };
59
+ }
60
+
61
+ /** 파일이 없거나 깨졌으면 null. 알 수 없는 상태값의 조각은 버리되 파일 전체는 살린다. */
62
+ export function readGateShards(path: string): GateShardsFile | null {
63
+ let parsed: unknown;
64
+ try { parsed = JSON.parse(readFileSync(path, 'utf8')); } catch { return null; }
65
+ if (!parsed || typeof parsed !== 'object') return null;
66
+ const raw = parsed as Record<string, unknown>;
67
+ if (raw.v !== 1 || !Array.isArray(raw.shards)) return null;
68
+ const version = str(raw.version, 40);
69
+ const updatedAt = str(raw.updatedAt, 40);
70
+ if (!version || !updatedAt) return null;
71
+ return { v: 1, version, updatedAt, shards: raw.shards.map(shardOf).filter((s): s is GateShard => s !== null) };
72
+ }
73
+
74
+ /** tmp → rename — 읽는 쪽이 반쯤 쓴 파일을 보지 않게. */
75
+ export function writeGateShards(path: string, file: GateShardsFile): void {
76
+ mkdirSync(dirname(path), { recursive: true });
77
+ const tmp = `${path}.${process.pid}.tmp`;
78
+ writeFileSync(tmp, `${JSON.stringify(file)}\n`);
79
+ renameSync(tmp, path);
80
+ }
81
+
82
+ export interface GateShardsSummary {
83
+ total: number;
84
+ counts: Record<ShardState, number>;
85
+ /** 대기 이유별 조각 수(많은 순). */
86
+ waitReasons: Array<{ reason: string; count: number }>;
87
+ /** 남은 분 추정 — 계획 분이 모자라면 null(«추정 불가»). */
88
+ etaMin: number | null;
89
+ /** 도는 조각 중 계획 분을 가장 많이 넘긴 분(없으면 0). 넘겼으면 etaMin 은 «하한»일 뿐이라 화면이 그렇게 말한다
90
+ * — canary 10-07 실측: 계획 0.1분 · 실제 22분 → 넘김을 안 보이면 내내 «남은 약 0분». */
91
+ overrunMin: number;
92
+ /** 파일이 마지막으로 바뀐 뒤 지난 분. 크면 쓰는 쪽이 멎었다는 뜻이다. */
93
+ staleMin: number | null;
94
+ }
95
+
96
+ const minutesBetween = (fromIso: string | undefined, now: number): number | null => {
97
+ const at = fromIso ? Date.parse(fromIso) : Number.NaN;
98
+ return Number.isFinite(at) ? Math.max(0, (now - at) / 60_000) : null;
99
+ };
100
+
101
+ /**
102
+ * 남은 시간 추정: 도는 조각은 «계획 − 경과», 기다리는 조각(pending·retry)은 계획 분을 지금 도는 자리 수로 나눠 뒤에 잇는다.
103
+ * 도는 조각이 없으면 자리 1로 본다. 계획 분이 없는 조각이 하나라도 남아 있으면 null.
104
+ */
105
+ function estimate(shards: GateShard[], now: number): number | null {
106
+ const running = shards.filter((s) => s.state === 'running');
107
+ const waiting = shards.filter((s) => s.state === 'pending' || s.state === 'retry');
108
+ if (!running.length && !waiting.length) return 0;
109
+ if ([...running, ...waiting].some((s) => s.plannedMin === undefined)) return null;
110
+ const runningLeft = running.map((s) => Math.max(0, s.plannedMin! - (minutesBetween(s.startedAt, now) ?? 0)));
111
+ const slots = Math.max(1, running.length);
112
+ const waitingSum = waiting.reduce((sum, s) => sum + s.plannedMin!, 0);
113
+ return Math.ceil(Math.max(0, ...runningLeft) + waitingSum / slots);
114
+ }
115
+
116
+ export function summarizeShards(file: GateShardsFile, now: number = Date.now()): GateShardsSummary {
117
+ const counts = Object.fromEntries(SHARD_STATES.map((s) => [s, 0])) as Record<ShardState, number>;
118
+ const reasons = new Map<string, number>();
119
+ for (const shard of file.shards) {
120
+ counts[shard.state] += 1;
121
+ if (shard.waitReason && (shard.state === 'pending' || shard.state === 'retry')) {
122
+ reasons.set(shard.waitReason, (reasons.get(shard.waitReason) ?? 0) + 1);
123
+ }
124
+ }
125
+ const stale = minutesBetween(file.updatedAt, now);
126
+ const overrun = Math.max(0, ...file.shards
127
+ .filter((s) => s.state === 'running' && s.plannedMin !== undefined)
128
+ .map((s) => (minutesBetween(s.startedAt, now) ?? 0) - s.plannedMin!));
129
+ return {
130
+ total: file.shards.length,
131
+ counts,
132
+ waitReasons: [...reasons].map(([reason, count]) => ({ reason, count })).sort((a, b) => b.count - a.count),
133
+ etaMin: estimate(file.shards, now),
134
+ overrunMin: Math.floor(overrun),
135
+ staleMin: stale === null ? null : Math.floor(stale),
136
+ };
137
+ }
138
+
139
+ export const SHARD_WORD: Record<ShardState, string> = {
140
+ pending: '대기', running: '돌기', done: '통과', retry: '재시도', timeout: '잘림', failed: '실패',
141
+ };
142
+
143
+ /** 외출 알림·/release 한 줄 — «조각 24 · 돌기 5 · 대기 8(CPU 부족 8) · 잘림 2 · 통과 9 · 남은 약 40분». */
144
+ export function shardsLine(summary: GateShardsSummary): string {
145
+ const order: ShardState[] = ['running', 'pending', 'retry', 'timeout', 'failed', 'done'];
146
+ const parts = [`조각 ${summary.total}`];
147
+ for (const state of order) {
148
+ const n = summary.counts[state];
149
+ if (!n) continue;
150
+ const why = state === 'pending' && summary.waitReasons.length
151
+ ? `(${summary.waitReasons.slice(0, 2).map((w) => `${w.reason} ${w.count}`).join(' · ')})` : '';
152
+ parts.push(`${SHARD_WORD[state]} ${n}${why}`);
153
+ }
154
+ if (summary.overrunMin >= 1) parts.push(`계획보다 ${summary.overrunMin}분 넘게 도는 중 — 남은 시간 추정 불가`);
155
+ else parts.push(summary.etaMin === null ? '남은 시간 추정 불가(계획 분 없음)' : `남은 약 ${summary.etaMin}분`);
156
+ if (summary.staleMin !== null && summary.staleMin >= 15) parts.push(`⚠️ ${summary.staleMin}분째 갱신 없음`);
157
+ return parts.join(' · ');
158
+ }
@@ -35,12 +35,13 @@ export function listSchedules(root?: string): ReleaseSchedule[] {
35
35
  return schedules;
36
36
  }
37
37
 
38
- export function setSchedule(version: string, patch: { cutAt?: string; landBy?: string; freezeFrom?: string; freezeUntil?: string }, by: string, root?: string): ReleaseSchedule {
38
+ export function setSchedule(version: string, patch: { cutAt?: string; landBy?: string; freezeFrom?: string; freezeUntil?: string; publishAt?: string }, by: string, root?: string): ReleaseSchedule {
39
39
  const schedule = writeSchedule(version, {
40
40
  ...(patch.cutAt !== undefined ? { cutAt: utcIso(patch.cutAt) } : {}),
41
41
  ...(patch.landBy !== undefined ? { landBy: utcIso(patch.landBy) } : {}),
42
42
  ...(patch.freezeFrom !== undefined ? { freezeFrom: utcIso(patch.freezeFrom) } : {}),
43
43
  ...(patch.freezeUntil !== undefined ? { freezeUntil: utcIso(patch.freezeUntil) } : {}),
44
+ ...(patch.publishAt !== undefined ? { publishAt: utcIso(patch.publishAt) } : {}),
44
45
  }, by, root);
45
46
  debug.log('release.schedule', 'set', { version, cutAt: schedule.cutAt, landBy: schedule.landBy });
46
47
  return schedule;
@@ -51,5 +52,8 @@ export function formatSchedule(schedule: ReleaseSchedule): string {
51
52
  const deadline = schedule.landBy ? formatKst(schedule.landBy) : null;
52
53
  const sameDay = schedule.landBy && new Date(Date.parse(schedule.landBy) + 9 * 60 * 60_000).toISOString().slice(0, 10)
53
54
  === new Date(Date.parse(schedule.cutAt) + 9 * 60 * 60_000).toISOString().slice(0, 10);
54
- return `${schedule.version} 컷 ${cut}${deadline ? ` · 착지 마감 ${sameDay ? deadline.split(' ')[1] : deadline}` : ''}`;
55
+ const publish = schedule.publishAt ? formatKst(schedule.publishAt) : null;
56
+ const publishSameDay = schedule.publishAt && new Date(Date.parse(schedule.publishAt) + 9 * 60 * 60_000).toISOString().slice(0, 10)
57
+ === new Date(Date.parse(schedule.cutAt) + 9 * 60 * 60_000).toISOString().slice(0, 10);
58
+ return `${schedule.version} 컷 ${cut}${deadline ? ` · 착지 마감 ${sameDay ? deadline.split(' ')[1] : deadline}` : ''}${publish ? ` · 발행 ${publishSameDay ? publish.split(' ')[1] : publish}` : ''}`;
55
59
  }