elanous 0.2.18 → 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 (350) hide show
  1. package/apps/pwa/out/404/index.html +1 -1
  2. package/apps/pwa/out/404.html +1 -1
  3. package/apps/pwa/out/_next/static/chunks/{1307-30dff22c9e1f3073.js → 1307-77fad80cf54477c6.js} +1 -1
  4. package/apps/pwa/out/_next/static/chunks/1612-f60c0c93ec773f81.js +1 -0
  5. package/apps/pwa/out/_next/static/chunks/2116-6429c188eb846ab8.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/224-e7be8f762fe74986.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/2489-ff55a35b202c0f94.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/2549-89c518a4c8ed4d00.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/3857-995ea9f5ef33d71e.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/4144-fad095b48a1862ee.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/5755-ea7ed88eb956f47b.js +1 -0
  12. package/apps/pwa/out/_next/static/chunks/5977-94ed6f78e9814b2b.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/7748-6e51bfe927019f5c.js +25 -0
  14. package/apps/pwa/out/_next/static/chunks/8401-35e94e9091f5eb8d.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/9247-c1753a059e7bac08.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/a9f2b54d-33c0689b70ed8cf3.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/app/approvals/{page-d92f7ee3994e84c7.js → page-c855c24de7db74fe.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-4733c967b56a4277.js → page-85b316bb4235980b.js} +1 -1
  19. package/apps/pwa/out/_next/static/chunks/app/board/{page-d22fd849cc22f104.js → page-9b2b26a33cb2cd42.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/botlab/{page-bb742f995c6769e7.js → page-c9955fde67b1bcb5.js} +1 -1
  21. package/apps/pwa/out/_next/static/chunks/app/bots/{page-02b073a2ac793b83.js → page-7a852956c74015a3.js} +1 -1
  22. package/apps/pwa/out/_next/static/chunks/app/ceo/page-c5a5c621be0e04ae.js +1 -0
  23. package/apps/pwa/out/_next/static/chunks/app/chat/{page-8a9b3594bc23289b.js → page-4ba4abe096b6c2a8.js} +1 -1
  24. package/apps/pwa/out/_next/static/chunks/app/consult/page-575d4a8b4f7becb2.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/app/control/{page-308111abadf35670.js → page-f5d56ff5f45ad164.js} +1 -1
  26. package/apps/pwa/out/_next/static/chunks/app/decisions/page-2260e9eef0357421.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/app/design-check/{page-5d97b2901e0d1ea6.js → page-7c058069c5e7fa63.js} +1 -1
  28. package/apps/pwa/out/_next/static/chunks/app/editor/page-dc5da0b8a2edb8a4.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/app/exec/{page-8c3d8e63ae2e1c98.js → page-466bf0ee16ad1432.js} +1 -1
  30. package/apps/pwa/out/_next/static/chunks/app/field/{page-9b72a07d424701c6.js → page-a23bc71713bcc08f.js} +1 -1
  31. package/apps/pwa/out/_next/static/chunks/app/inside/page-ed4a6f799f005d09.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/app/intake/{page-a572f58cd3ca5864.js → page-9cd565005b93aca6.js} +1 -1
  33. package/apps/pwa/out/_next/static/chunks/app/layout-55f0f40c6db95f15.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/app/live/{page-be5ea965a07fbc41.js → page-474cb5888ea0e8f3.js} +1 -1
  35. package/apps/pwa/out/_next/static/chunks/app/loops/page-35bfec2e631d3eae.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/app/market/{page-fa244bd146a00ae8.js → page-9ca21f9ad3208a61.js} +1 -1
  37. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-44d56d6391965a6a.js → page-f9ccb1b1adbea14d.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/app/morning/{page-83a700d4c754d802.js → page-ec102c9b8b691e37.js} +1 -1
  39. package/apps/pwa/out/_next/static/chunks/app/{not-found-7b3170f374f77b9d.js → not-found-a9c2cfa8dda922a5.js} +1 -1
  40. package/apps/pwa/out/_next/static/chunks/app/observatory/page-6da35144878feb6d.js +1 -0
  41. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-f6055495e9ed7ea8.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-25204694fa9984a5.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-a60e035f151d84d3.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/app/outputs/page-189c587f36874538.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/app/page-c56634bde705a65d.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/app/reflection/{page-1467047104141297.js → page-db03ae3589a81c9e.js} +1 -1
  47. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-4a6463b97f0ad94f.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/app/sessions/{page-102310930791197a.js → page-75bffecc9aa1004e.js} +1 -1
  49. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-a8d2fe0c669e0de4.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/settings/{page-6ed0b73bae8351d8.js → page-31d5b875760bb62d.js} +1 -1
  51. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-c26fa10aac6c21c4.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/setup/{page-43336055451f6e37.js → page-68793c6e10331d66.js} +1 -1
  53. package/apps/pwa/out/_next/static/chunks/app/share/{page-3cc47dca0922ab9a.js → page-46d5201d2207a511.js} +1 -1
  54. package/apps/pwa/out/_next/static/chunks/app/showroom/{page-a77ee355a2304ff0.js → page-d81c07a7b4f618ae.js} +1 -1
  55. package/apps/pwa/out/_next/static/chunks/app/tasks/{page-b17cd46cf0e9b8cf.js → page-be1c4e8697f22360.js} +1 -1
  56. package/apps/pwa/out/_next/static/chunks/app/term/page-cf0a4b4abead71ea.js +1 -0
  57. package/apps/pwa/out/_next/static/chunks/app/today/page-a4717e2c4bf7a28f.js +1 -0
  58. package/apps/pwa/out/_next/static/chunks/app/trace/{page-7bf54c9bb9274af8.js → page-0fd895fab9b62c1b.js} +1 -1
  59. package/apps/pwa/out/_next/static/chunks/app/vault/page-8b6c1454a8e8f306.js +1 -0
  60. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-c6e4364d9de8f717.js +1 -0
  61. package/apps/pwa/out/_next/static/chunks/app/workflows/page-0acc8e198f7b0dfb.js +1 -0
  62. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-134da468538baf48.js → page-34882c84f84021c5.js} +1 -1
  63. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-0506a1e0e4bdb2f5.js +1 -0
  64. package/apps/pwa/out/_next/static/chunks/{webpack-c2782b330447ffb6.js → webpack-c260571513bf43c2.js} +1 -1
  65. package/apps/pwa/out/_next/static/css/{e99e3cff94ced78a.css → 599369d853c61df7.css} +1 -1
  66. package/apps/pwa/out/_next/static/css/c23d959d3ee15e57.css +1 -0
  67. package/apps/pwa/out/_next/static/css/d3427996da11e480.css +1 -0
  68. package/apps/pwa/out/approvals/index.html +1 -1
  69. package/apps/pwa/out/approvals/index.txt +14 -14
  70. package/apps/pwa/out/autopilot/index.html +1 -1
  71. package/apps/pwa/out/autopilot/index.txt +14 -14
  72. package/apps/pwa/out/board/index.html +1 -1
  73. package/apps/pwa/out/board/index.txt +14 -14
  74. package/apps/pwa/out/botlab/index.html +1 -1
  75. package/apps/pwa/out/botlab/index.txt +14 -14
  76. package/apps/pwa/out/bots/index.html +1 -1
  77. package/apps/pwa/out/bots/index.txt +14 -14
  78. package/apps/pwa/out/ceo/index.html +1 -1
  79. package/apps/pwa/out/ceo/index.txt +14 -14
  80. package/apps/pwa/out/chat/index.html +1 -1
  81. package/apps/pwa/out/chat/index.txt +14 -14
  82. package/apps/pwa/out/consult/index.html +1 -1
  83. package/apps/pwa/out/consult/index.txt +14 -14
  84. package/apps/pwa/out/control/index.html +1 -1
  85. package/apps/pwa/out/control/index.txt +14 -14
  86. package/apps/pwa/out/decisions/index.html +1 -1
  87. package/apps/pwa/out/decisions/index.txt +14 -14
  88. package/apps/pwa/out/design-check/index.html +1 -1
  89. package/apps/pwa/out/design-check/index.txt +14 -14
  90. package/apps/pwa/out/editor/index.html +1 -1
  91. package/apps/pwa/out/editor/index.txt +16 -15
  92. package/apps/pwa/out/exec/index.html +1 -1
  93. package/apps/pwa/out/exec/index.txt +14 -14
  94. package/apps/pwa/out/field/index.html +1 -1
  95. package/apps/pwa/out/field/index.txt +14 -14
  96. package/apps/pwa/out/healthz +1 -1
  97. package/apps/pwa/out/index.html +1 -1
  98. package/apps/pwa/out/index.txt +14 -14
  99. package/apps/pwa/out/inside/index.html +1 -1
  100. package/apps/pwa/out/inside/index.txt +16 -15
  101. package/apps/pwa/out/intake/index.html +1 -1
  102. package/apps/pwa/out/intake/index.txt +14 -14
  103. package/apps/pwa/out/live/index.html +1 -1
  104. package/apps/pwa/out/live/index.txt +14 -14
  105. package/apps/pwa/out/loops/index.html +1 -1
  106. package/apps/pwa/out/loops/index.txt +24 -23
  107. package/apps/pwa/out/market/index.html +1 -1
  108. package/apps/pwa/out/market/index.txt +14 -14
  109. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  110. package/apps/pwa/out/missions/placeholder/index.txt +14 -14
  111. package/apps/pwa/out/morning/index.html +1 -1
  112. package/apps/pwa/out/morning/index.txt +14 -14
  113. package/apps/pwa/out/observatory/index.html +1 -1
  114. package/apps/pwa/out/observatory/index.txt +14 -14
  115. package/apps/pwa/out/ops/checklist/index.html +1 -1
  116. package/apps/pwa/out/ops/checklist/index.txt +14 -14
  117. package/apps/pwa/out/ops/release/index.html +1 -1
  118. package/apps/pwa/out/ops/release/index.txt +14 -14
  119. package/apps/pwa/out/ops/seats/index.html +1 -1
  120. package/apps/pwa/out/ops/seats/index.txt +14 -14
  121. package/apps/pwa/out/outputs/index.html +1 -1
  122. package/apps/pwa/out/outputs/index.txt +14 -14
  123. package/apps/pwa/out/reflection/index.html +1 -1
  124. package/apps/pwa/out/reflection/index.txt +15 -15
  125. package/apps/pwa/out/scheduler/index.html +1 -1
  126. package/apps/pwa/out/scheduler/index.txt +14 -14
  127. package/apps/pwa/out/sessions/index.html +1 -1
  128. package/apps/pwa/out/sessions/index.txt +14 -14
  129. package/apps/pwa/out/settings/devices/index.html +1 -1
  130. package/apps/pwa/out/settings/devices/index.txt +14 -14
  131. package/apps/pwa/out/settings/index.html +1 -1
  132. package/apps/pwa/out/settings/index.txt +14 -14
  133. package/apps/pwa/out/setup/done/index.html +1 -1
  134. package/apps/pwa/out/setup/done/index.txt +14 -14
  135. package/apps/pwa/out/setup/index.html +1 -1
  136. package/apps/pwa/out/setup/index.txt +14 -14
  137. package/apps/pwa/out/share/index.html +1 -1
  138. package/apps/pwa/out/share/index.txt +14 -14
  139. package/apps/pwa/out/showroom/index.html +1 -1
  140. package/apps/pwa/out/showroom/index.txt +14 -14
  141. package/apps/pwa/out/tasks/index.html +1 -1
  142. package/apps/pwa/out/tasks/index.txt +14 -14
  143. package/apps/pwa/out/term/index.html +1 -1
  144. package/apps/pwa/out/term/index.txt +14 -14
  145. package/apps/pwa/out/today/index.html +1 -1
  146. package/apps/pwa/out/today/index.txt +14 -14
  147. package/apps/pwa/out/trace/index.html +1 -1
  148. package/apps/pwa/out/trace/index.txt +14 -14
  149. package/apps/pwa/out/vault/index.html +1 -1
  150. package/apps/pwa/out/vault/index.txt +14 -14
  151. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  152. package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
  153. package/apps/pwa/out/workflows/index.html +1 -1
  154. package/apps/pwa/out/workflows/index.txt +14 -14
  155. package/apps/pwa/out/workspace/index.html +1 -1
  156. package/apps/pwa/out/workspace/index.txt +14 -14
  157. package/apps/pwa/out/worktrees/index.html +1 -1
  158. package/apps/pwa/out/worktrees/index.txt +14 -14
  159. package/graphs/demo/editor-recipes.yaml +22 -0
  160. package/graphs/ops/guardian.yaml +26 -0
  161. package/graphs/ops/recipes.yaml +3 -0
  162. package/graphs/retro/daily.yaml +25 -0
  163. package/graphs/retro/recipes.yaml +24 -0
  164. package/package.json +1 -1
  165. package/scripts/ask-marker-check.ts +3 -1
  166. package/scripts/coord-channel-watch.sh +2 -2
  167. package/scripts/discord-seat-probe.ts +3 -1
  168. package/scripts/land-global-checks.ts +89 -0
  169. package/scripts/lib/pch-regress-map.ts +24 -0
  170. package/scripts/morning-report.ts +18 -4
  171. package/scripts/pch-regress.ts +88 -0
  172. package/scripts/release-loop/gate-diff.ts +18 -0
  173. package/scripts/release-loop/gate-node.ts +628 -65
  174. package/scripts/release-loop/gate-partial.ts +106 -0
  175. package/scripts/release-loop/gate-timing.ts +229 -0
  176. package/scripts/release-loop/preflight.ts +6 -4
  177. package/scripts/release-loop/publish-window.ts +141 -0
  178. package/scripts/release-loop/release-docs.ts +1 -4
  179. package/scripts/release-loop/release-universe.ts +38 -0
  180. package/scripts/release-loop/resume-release.ts +67 -5
  181. package/scripts/release-loop/shard-plan.ts +65 -0
  182. package/scripts/release-loop/unattended-release.ts +12 -2
  183. package/scripts/release-story/draft.ts +5 -1
  184. package/scripts/retro/shadow-compare.ts +71 -0
  185. package/scripts/rhythm/daily.ts +6 -1
  186. package/scripts/video/scene-table.ts +159 -0
  187. package/src/acp/core-turn-bridge.ts +39 -2
  188. package/src/agent-mission/driver.ts +34 -6
  189. package/src/autopilot/build/llm-conflict-merge.ts +132 -7
  190. package/src/autopilot/build/merge-intent.ts +135 -7
  191. package/src/away/presence.ts +40 -0
  192. package/src/away/release-watch.ts +272 -0
  193. package/src/chat/index.ts +2 -1
  194. package/src/chat/short-question.ts +3 -40
  195. package/src/cli/away-cli.ts +68 -0
  196. package/src/cli/config-drift.ts +513 -0
  197. package/src/cli/guardian-cli.ts +49 -0
  198. package/src/cli/intake-cli.ts +13 -7
  199. package/src/cli/log-event-names.ts +5 -0
  200. package/src/cli/pod-cli.ts +5 -1
  201. package/src/cli/pr-cli.ts +20 -0
  202. package/src/cli/pty-drive-cli.ts +14 -3
  203. package/src/cli/release-cli.ts +52 -12
  204. package/src/cli/retro-cli.ts +78 -0
  205. package/src/cli/setup-cli.ts +31 -1
  206. package/src/cli/tasks-cli.ts +193 -14
  207. package/src/context-bus/context-now-public.ts +1 -1
  208. package/src/context-bus/context-now-surfaces.ts +16 -6
  209. package/src/context-bus/context-now.ts +97 -3
  210. package/src/core-turn/short-question.ts +100 -0
  211. package/src/dashboard/dashboard-status-lines.ts +24 -4
  212. package/src/dashboard/index.ts +50 -5
  213. package/src/dashboard/input/slash-executor.ts +6 -2
  214. package/src/dashboard/slash-runtime/dashboard-handlers.ts +68 -40
  215. package/src/dashboard/slash-runtime/decide-slash.ts +60 -0
  216. package/src/dashboard/slash-runtime/help-from-registry.ts +62 -23
  217. package/src/dashboard/slash-runtime/help-groups.ts +44 -0
  218. package/src/dashboard/slash-runtime/model-choices.ts +125 -0
  219. package/src/domains/guardian-tick.ts +138 -0
  220. package/src/domains/outbound-alert.ts +44 -13
  221. package/src/domains/outbound-log-probe.ts +6 -1
  222. package/src/domains/schedule-registry.ts +23 -0
  223. package/src/domains/telegram-kind-route.ts +39 -0
  224. package/src/domains/watch-default-scheduler.ts +95 -0
  225. package/src/esc-abort-gate.ts +44 -0
  226. package/src/graph-runner/graph-run-api.ts +205 -0
  227. package/src/graph-runner/runner.ts +36 -6
  228. package/src/harness/harness-cli-command.ts +420 -51
  229. package/src/harness/harness-queue.ts +315 -25
  230. package/src/harness/harness-salvage-cli.ts +140 -0
  231. package/src/harness/harness-worktree-add.ts +68 -0
  232. package/src/harness/ledger-write-guard.ts +6 -16
  233. package/src/hq/fence-outcomes.ts +63 -0
  234. package/src/hq/fence-wrapper.ts +33 -0
  235. package/src/hq/hq.ts +17 -2
  236. package/src/index.ts +118 -6
  237. package/src/instance/test-write-guard-spy-first.fixture.ts +13 -0
  238. package/src/instance/test-write-guard.ts +200 -0
  239. package/src/intake-plane/digest.ts +39 -15
  240. package/src/intake-plane/harness-target.ts +4 -3
  241. package/src/intake-plane/intake-sources.ts +1 -0
  242. package/src/intake-plane/items.ts +1 -0
  243. package/src/loops/orchestrator/degradation.ts +57 -0
  244. package/src/loops/orchestrator/finish-rate.ts +33 -3
  245. package/src/loops/orchestrator/tick.ts +166 -45
  246. package/src/maturity/feature-maturity.ts +6 -4
  247. package/src/maturity/tui-slash-maturity.ts +1 -0
  248. package/src/mss/logging/log-store.ts +3 -0
  249. package/src/nexus/api/graph-kinds.ts +10 -4
  250. package/src/nexus/api/graphs-api.ts +178 -6
  251. package/src/nexus/api/http-server.ts +54 -8
  252. package/src/nexus/api/loop-edges.ts +292 -17
  253. package/src/nexus/api/ops-api.ts +112 -29
  254. package/src/nexus/api/ops-board-route.ts +48 -0
  255. package/src/nexus/index.ts +7 -0
  256. package/src/onboarding/entry-hints.ts +5 -0
  257. package/src/onboarding.ts +24 -6
  258. package/src/release-loop/checklist.ts +2 -0
  259. package/src/release-loop/feature-store.ts +68 -18
  260. package/src/release-loop/gate-shards.ts +158 -0
  261. package/src/release-loop/light-rc.ts +173 -0
  262. package/src/release-loop/placement.ts +51 -17
  263. package/src/release-loop/release-schedule.ts +6 -2
  264. package/src/release-loop/rubric.ts +57 -1
  265. package/src/retro/daily.ts +372 -0
  266. package/src/seat-loop/op-decide.ts +173 -0
  267. package/src/seat-loop/seat-loop.ts +60 -3
  268. package/src/seat-loop/seat-report.ts +58 -3
  269. package/src/self-dev/ask-launch-flow.ts +5 -4
  270. package/src/self-dev/ask-launch-io.ts +49 -1
  271. package/src/self-dev/ask-path-candidates.ts +40 -2
  272. package/src/self-dev/dev-cli.ts +36 -0
  273. package/src/self-dev/draft-sweep.ts +371 -16
  274. package/src/self-dev/draft-triage-rules.ts +19 -4
  275. package/src/self-dev/orchestrate.ts +1 -0
  276. package/src/self-dev/pr-preview.ts +247 -0
  277. package/src/self-dev/shard-run.ts +23 -7
  278. package/src/self-implement/blocked-draft-policy.ts +101 -0
  279. package/src/self-implement/execution-clarification.ts +11 -0
  280. package/src/self-implement/gate-cli.ts +29 -8
  281. package/src/self-implement/gate-env-retry.ts +83 -0
  282. package/src/self-implement/goal-author-clarification.ts +88 -13
  283. package/src/self-implement/goal-clarification-escalation.ts +2 -0
  284. package/src/self-implement/orchestrator.ts +220 -6
  285. package/src/self-implement/run-stop.ts +123 -0
  286. package/src/self-implement/run-ttl.ts +258 -0
  287. package/src/self-implement/salvage-branches.ts +254 -0
  288. package/src/self-implement/salvage-retention.ts +123 -0
  289. package/src/self-implement/seams.ts +4 -0
  290. package/src/self-implement/self-implement-cli.ts +2 -0
  291. package/src/self-implement/sibling-resync.ts +223 -0
  292. package/src/self-implement/stop-autoheal.ts +204 -0
  293. package/src/task-agent/actions.ts +63 -2
  294. package/src/task-agent/board.ts +132 -0
  295. package/src/task-agent/card-evidence.ts +292 -0
  296. package/src/task-agent/completion-evidence.ts +126 -0
  297. package/src/task-agent/judge.ts +25 -0
  298. package/src/task-agent/live-moves.ts +276 -0
  299. package/src/task-agent/mission.ts +33 -7
  300. package/src/task-agent/shadow.ts +192 -15
  301. package/src/task-agent/task-hand.ts +124 -17
  302. package/src/task-orchestrator/surfaces/pod-command-job.ts +82 -8
  303. package/src/task-orchestrator/surfaces/pod-failure-reason.ts +4 -0
  304. package/src/task-orchestrator/surfaces/pod-install-slots.ts +89 -0
  305. package/src/task-orchestrator/surfaces/pod-lease.ts +233 -21
  306. package/src/task-orchestrator/surfaces/pod-pool.ts +155 -34
  307. package/src/task-orchestrator/surfaces/self-implement-pod.ts +21 -5
  308. package/src/telegram-away-command.ts +38 -0
  309. package/src/telegram-commands.ts +52 -0
  310. package/src/telegram-report-log-probe.ts +21 -0
  311. package/src/telegram-report.ts +27 -3
  312. package/src/testing/real-cli-spawn.ts +106 -0
  313. package/src/tui-client/dashboard-session.ts +1 -0
  314. package/src/user-config.ts +190 -5
  315. package/src/version/packed-revision.json +1 -1
  316. package/apps/pwa/out/_next/static/chunks/224-798346143007ff65.js +0 -1
  317. package/apps/pwa/out/_next/static/chunks/3857-4f936cdb30322aa6.js +0 -1
  318. package/apps/pwa/out/_next/static/chunks/4144-a0041c8dc3be57ac.js +0 -1
  319. package/apps/pwa/out/_next/static/chunks/4554-5761612531d0146b.js +0 -1
  320. package/apps/pwa/out/_next/static/chunks/5977-a11f37254196b659.js +0 -1
  321. package/apps/pwa/out/_next/static/chunks/5994-ae92c0de046ab9ba.js +0 -1
  322. package/apps/pwa/out/_next/static/chunks/8401-8615a660c84cdb03.js +0 -1
  323. package/apps/pwa/out/_next/static/chunks/8868-10a3a4440fb0ccd8.js +0 -25
  324. package/apps/pwa/out/_next/static/chunks/9247-269e4464cc4b491c.js +0 -1
  325. package/apps/pwa/out/_next/static/chunks/a9f2b54d-7865aba0f4d2ee49.js +0 -1
  326. package/apps/pwa/out/_next/static/chunks/app/ceo/page-60c3da708f9a0ccc.js +0 -1
  327. package/apps/pwa/out/_next/static/chunks/app/consult/page-a0eb02638b1e6f31.js +0 -1
  328. package/apps/pwa/out/_next/static/chunks/app/decisions/page-6eaf7bac37830c21.js +0 -1
  329. package/apps/pwa/out/_next/static/chunks/app/editor/page-c39554c321576982.js +0 -1
  330. package/apps/pwa/out/_next/static/chunks/app/inside/page-bd5d586429167fea.js +0 -1
  331. package/apps/pwa/out/_next/static/chunks/app/layout-59b7834d497c2255.js +0 -1
  332. package/apps/pwa/out/_next/static/chunks/app/loops/page-c130b63862437814.js +0 -1
  333. package/apps/pwa/out/_next/static/chunks/app/observatory/page-5f23d0c66a3b4793.js +0 -1
  334. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-a3c58f5c72a5f3f9.js +0 -1
  335. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-a2c4625596f8d69c.js +0 -1
  336. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-0e9099531d239f11.js +0 -1
  337. package/apps/pwa/out/_next/static/chunks/app/outputs/page-e8e1ecaea7cd71d2.js +0 -1
  338. package/apps/pwa/out/_next/static/chunks/app/page-a1b7ffed710a93e0.js +0 -1
  339. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-2da6f9b8a38c6978.js +0 -1
  340. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-f331e38fefc00393.js +0 -1
  341. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-44d5f767491692bf.js +0 -1
  342. package/apps/pwa/out/_next/static/chunks/app/term/page-61553851aa606146.js +0 -1
  343. package/apps/pwa/out/_next/static/chunks/app/today/page-a1f585e154e61c59.js +0 -1
  344. package/apps/pwa/out/_next/static/chunks/app/vault/page-5150ef17b5383a57.js +0 -1
  345. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-b9d8ce70792c1672.js +0 -1
  346. package/apps/pwa/out/_next/static/chunks/app/workflows/page-a5a3de231f2cc7d5.js +0 -1
  347. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-31ac4afa2dcd915c.js +0 -1
  348. package/apps/pwa/out/_next/static/css/7fa358fec7da7c47.css +0 -1
  349. /package/apps/pwa/out/_next/static/{P6YTVgzuZz5oTZhY4Din_ → Bam1YziK2iH9bN4K0utGk}/_buildManifest.js +0 -0
  350. /package/apps/pwa/out/_next/static/{P6YTVgzuZz5oTZhY4Din_ → Bam1YziK2iH9bN4K0utGk}/_ssgManifest.js +0 -0
@@ -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
 
@@ -40,6 +40,7 @@ export interface ChecklistHistory {
40
40
  dev: string;
41
41
  force?: true;
42
42
  reason?: string;
43
+ generation?: number;
43
44
  }
44
45
  export interface Checklist {
45
46
  version: string;
@@ -101,6 +102,7 @@ export function statusChangesSince(history: readonly ChecklistHistory[], sinceIs
101
102
  }
102
103
 
103
104
  function mutate(v: string, apply: (data: Checklist, otherItems: (id: string) => store.ChecklistCollision[]) => boolean): Checklist {
105
+ // addItem/setItem/claimItem/removeItem/seedFromRoadmap reach the generation-fenced ledger write here.
104
106
  return decodeClaimHistory(store.mutate(v, store.releasedVersion(), devVersion(), apply));
105
107
  }
106
108
 
@@ -5,6 +5,8 @@ import { Database } from 'bun:sqlite';
5
5
  import { releaseLedgerRoot } from '../instance/resolve.js';
6
6
  import { CliUserError } from '../cli/cli-user-error.js';
7
7
  import { debug } from '../debug/log.js';
8
+ import { getUserConfig } from '../user-config.js';
9
+ import { hqSeenGenerationPath, readHqLease, readSeenGeneration } from '../hq/hq.js';
8
10
  import { devVersion, type Checklist, type ChecklistHistory, type ChecklistItem } from './checklist.js';
9
11
 
10
12
  export function validateVersion(version: string): void {
@@ -32,9 +34,9 @@ export function releasedVersion(ledgerRoot = releaseLedgerRoot()): string {
32
34
  const walWait = new Int32Array(new SharedArrayBuffer(4));
33
35
  const SCHEMA_COLUMNS: Record<string, readonly string[]> = {
34
36
  imported_versions: ['json_hash', 'imported_at'],
35
- events: ['reason'],
37
+ events: ['reason', 'generation'],
36
38
  assignments: ['priority', 'predecessors', 'deadline_version', 'ceo_minutes', 'ceo_date', 'accelerator', 'refs'],
37
- release_schedules: ['freeze_from', 'freeze_until'],
39
+ release_schedules: ['freeze_from', 'freeze_until', 'publish_at'],
38
40
  };
39
41
  function schemaCurrent(db: Database): boolean {
40
42
  return Object.entries(SCHEMA_COLUMNS).every(([table, names]) => {
@@ -72,23 +74,25 @@ function open(root = releaseLedgerRoot()): Database {
72
74
  const columns = db.query('PRAGMA table_info(imported_versions)').all() as Array<{ name: string }>;
73
75
  if (!columns.some((column) => column.name === 'json_hash')) db.exec('ALTER TABLE imported_versions ADD COLUMN json_hash TEXT');
74
76
  if (!columns.some((column) => column.name === 'imported_at')) db.exec('ALTER TABLE imported_versions ADD COLUMN imported_at TEXT');
75
- if (!(db.query('PRAGMA table_info(events)').all() as Array<{ name: string }>).some((column) => column.name === 'reason')) db.exec('ALTER TABLE events ADD COLUMN reason TEXT');
77
+ const eventColumns = db.query('PRAGMA table_info(events)').all() as Array<{ name: string }>;
78
+ if (!eventColumns.some((column) => column.name === 'reason')) db.exec('ALTER TABLE events ADD COLUMN reason TEXT');
79
+ if (!eventColumns.some((column) => column.name === 'generation')) db.exec('ALTER TABLE events ADD COLUMN generation INTEGER');
76
80
  db.exec('CREATE TABLE IF NOT EXISTS release_schedules (version TEXT PRIMARY KEY, cut_at TEXT NOT NULL, land_by TEXT, updated_at TEXT NOT NULL, updated_by TEXT NOT NULL)');
77
81
  const assignmentColumns = db.query('PRAGMA table_info(assignments)').all() as Array<{ name: string }>;
78
82
  for (const [name, sql] of [['priority', 'TEXT'], ['predecessors', 'TEXT'], ['deadline_version', 'TEXT'], ['ceo_minutes', 'INTEGER'], ['ceo_date', 'TEXT'], ['accelerator', 'INTEGER'], ['refs', 'TEXT']] as const) {
79
83
  if (!assignmentColumns.some((column) => column.name === name)) db.exec(`ALTER TABLE assignments ADD COLUMN ${name} ${sql}`);
80
84
  }
81
85
  const scheduleColumns = db.query('PRAGMA table_info(release_schedules)').all() as Array<{ name: string }>;
82
- 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`);
83
87
  }).immediate();
84
88
  return db;
85
89
  } catch (error) { db.close(); throw error; }
86
90
  }
87
91
 
88
- export interface ScheduleRow { version: string; cutAt: string; landBy: string | null; freezeFrom?: string | null; freezeUntil?: string | null; updatedAt: string; updatedBy: string }
89
- 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 };
90
94
  function scheduleRow(row: StoredSchedule): ScheduleRow {
91
- 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 };
92
96
  }
93
97
 
94
98
  export function readSchedule(version: string, root?: string): ScheduleRow | null {
@@ -115,7 +119,7 @@ export function assignedVersions(root = releaseLedgerRoot()): string[] {
115
119
  finally { db.close(); }
116
120
  }
117
121
 
118
- 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 {
119
123
  validateVersion(version);
120
124
  const db = open(root);
121
125
  try {
@@ -126,19 +130,22 @@ export function writeSchedule(version: string, patch: { cutAt?: string; landBy?:
126
130
  const landBy = patch.landBy ?? previous?.land_by ?? null;
127
131
  const freezeFrom = patch.freezeFrom ?? previous?.freeze_from ?? null;
128
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('발행 시각은 컷보다 뒤여야 한다');
129
135
  if (Boolean(freezeFrom) !== Boolean(freezeUntil) || (freezeFrom && freezeUntil && Date.parse(freezeFrom) >= Date.parse(freezeUntil))) throw new CliUserError('동결 시작·끝은 함께 주고 시작이 끝보다 앞서야 한다');
130
- 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);
131
137
  const at = new Date().toISOString();
132
- db.query(`INSERT INTO release_schedules (version, cut_at, land_by, freeze_from, freeze_until, updated_at, updated_by) VALUES (?, ?, ?, ?, ?, ?, ?)
133
- 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`)
134
- .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);
135
141
  for (const [field, from, to] of [
136
142
  ['cut_at', previous?.cut_at ?? null, cutAt], ['land_by', previous?.land_by ?? null, landBy],
137
143
  ['freeze_from', previous?.freeze_from ?? null, freezeFrom], ['freeze_until', previous?.freeze_until ?? null, freezeUntil],
144
+ ['publish_at', previous?.publish_at ?? null, publishAt],
138
145
  ] as const) {
139
146
  if (from !== to) record(db, version, { at, by, id: '@version', field, from, to, released: releasedVersion(root), dev: devVersion() });
140
147
  }
141
- return { version, cutAt, landBy, updatedAt: at, updatedBy: by };
148
+ return { version, cutAt, landBy, ...(publishAt ? { publishAt } : {}), updatedAt: at, updatedBy: by };
142
149
  });
143
150
  } finally { db.close(); }
144
151
  }
@@ -155,15 +162,15 @@ function readTransaction<T>(db: Database, work: () => T): T {
155
162
  catch (error) { db.exec('ROLLBACK'); throw error; }
156
163
  }
157
164
 
158
- type EventRow = { seq: number; at: string; by: string; feature_id: string; version: string; field: string; from: string; to: string; released: string; dev: string; reason: string | null };
165
+ type EventRow = { seq: number; at: string; by: string; feature_id: string; version: string; field: string; from: string; to: string; released: string; dev: string; reason: string | null; generation: number | null };
159
166
  function decode(value: string | null): unknown { return value === null ? null : JSON.parse(value); }
160
167
  function historyRow(row: EventRow): ChecklistHistory {
161
168
  return { at: row.at, by: row.by, id: row.feature_id, field: row.field, from: decode(row.from), to: decode(row.to), released: row.released, dev: row.dev,
162
- ...(row.reason !== null ? { reason: row.reason } : {}) };
169
+ ...(row.reason !== null ? { reason: row.reason } : {}), ...(row.generation !== null ? { generation: row.generation } : {}) };
163
170
  }
164
171
  function record(db: Database, version: string, entry: ChecklistHistory): void {
165
- db.query('INSERT INTO events (at, by, feature_id, version, field, "from", "to", released, dev, reason) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)')
166
- .run(entry.at, entry.by, entry.id, version, entry.field, JSON.stringify(entry.from ?? null), JSON.stringify(entry.to ?? null), entry.released, entry.dev, entry.reason ?? null);
172
+ db.query('INSERT INTO events (at, by, feature_id, version, field, "from", "to", released, dev, reason, generation) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)')
173
+ .run(entry.at, entry.by, entry.id, version, entry.field, JSON.stringify(entry.from ?? null), JSON.stringify(entry.to ?? null), entry.released, entry.dev, entry.reason ?? null, entry.generation ?? null);
167
174
  }
168
175
  function evidenceRefs(db: Database, version: string, id: string): string[] {
169
176
  return (db.query('SELECT ref FROM evidence WHERE feature_id = ? AND version = ? ORDER BY rowid').all(id, version) as Array<{ ref: string }>).map((row) => row.ref);
@@ -334,6 +341,30 @@ export interface ChecklistCollision { version: string; owner?: string; title: st
334
341
 
335
342
  export function mutate(version: string, released: string, dev: string, apply: (data: Checklist, otherItems: (id: string) => ChecklistCollision[]) => boolean): Checklist {
336
343
  validateVersion(version);
344
+ // COORD-HA ①(RFC-loop-agent-map §A8b): a write carries the generation this process saw; a stale one is refused only
345
+ // under hq.boardFencing 'enforce' (default shadow = write and log). The lease is read ONCE, before the write transaction —
346
+ // with an ssh arbiter a lease read inside the transaction would hold the ledger write lock across the network.
347
+ // Only 'enforce' (opt-in) re-reads once inside the transaction just before writing, so a lease taken over meanwhile
348
+ // still rolls the write back; shadow never touches the lease under the lock.
349
+ const hq = getUserConfig().hq;
350
+ const writerGeneration = hq ? readSeenGeneration(hqSeenGenerationPath(hq)) : null;
351
+ let recordedGeneration: number | null = null;
352
+ let firstMeasured = false;
353
+ if (hq) {
354
+ let lease: ReturnType<typeof readHqLease>['record'] | undefined;
355
+ try { lease = readHqLease({ config: hq }).record; }
356
+ catch (error) { debug.log('hq.fence', 'board-lease-unmeasured', { writer: writerGeneration, version, reason: String(error).slice(0, 200) }); }
357
+ // A missing lease (null) and a broken one (parseLease throws → caught above with its reason) are both «unmeasured».
358
+ if (lease === null) debug.log('hq.fence', 'board-lease-unmeasured', { writer: writerGeneration, version, reason: 'no-lease' });
359
+ if (lease) {
360
+ firstMeasured = true;
361
+ recordedGeneration = writerGeneration;
362
+ if (writerGeneration !== null && writerGeneration < lease.generation) {
363
+ debug.log('hq.fence', 'board-stale-write', { writer: writerGeneration, lease: lease.generation, version });
364
+ if (hq.boardFencing === 'enforce') throw new CliUserError(`낡은 세대 ${writerGeneration} < ${lease.generation} — 쓰기 거부`);
365
+ }
366
+ }
367
+ }
337
368
  const db = open();
338
369
  try {
339
370
  let imported = false;
@@ -352,6 +383,22 @@ export function mutate(version: string, released: string, dev: string, apply: (d
352
383
  return { version: item.version, ...(item.owner !== null ? { owner: item.owner } : {}), title: item.title };
353
384
  });
354
385
  })) {
386
+ if (hq?.boardFencing === 'enforce') {
387
+ let late: ReturnType<typeof readHqLease>['record'] = null;
388
+ let lateReason = 'no-lease';
389
+ try { late = readHqLease({ config: hq }).record; }
390
+ catch (error) { late = null; lateReason = String(error).slice(0, 200); }
391
+ if (!late) {
392
+ // Unmeasured right before the write: the write still goes through, but carries no generation it cannot vouch for.
393
+ // (Logged only when the first read had measured — an unmeasured first read was already logged once.)
394
+ if (firstMeasured) debug.log('hq.fence', 'board-lease-unmeasured', { writer: writerGeneration, version, reason: lateReason, phase: 'before-write' });
395
+ recordedGeneration = null;
396
+ }
397
+ if (late && writerGeneration !== null && writerGeneration < late.generation) {
398
+ debug.log('hq.fence', 'board-stale-write', { writer: writerGeneration, lease: late.generation, version });
399
+ throw new CliUserError(`낡은 세대 ${writerGeneration} < ${late.generation} — 쓰기 거부`);
400
+ }
401
+ }
355
402
  for (const item of data.items) if (before.get(item.id) !== JSON.stringify(item)) {
356
403
  const previous = before.get(item.id);
357
404
  const evidenceChanged = previous && (JSON.parse(previous) as ChecklistItem).evidence !== item.evidence;
@@ -365,7 +412,10 @@ export function mutate(version: string, released: string, dev: string, apply: (d
365
412
  db.query('DELETE FROM evidence WHERE feature_id = ? AND version = ?').run(id, version);
366
413
  db.query('DELETE FROM assignments WHERE feature_id = ? AND version = ?').run(id, version);
367
414
  }
368
- for (const entry of data.history.slice(count)) record(db, version, entry);
415
+ for (const entry of data.history.slice(count)) {
416
+ if (recordedGeneration !== null) entry.generation = recordedGeneration;
417
+ record(db, version, entry);
418
+ }
369
419
  }
370
420
  return data;
371
421
  });
@@ -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
+ }
@@ -0,0 +1,173 @@
1
+ /** LIGHT-RC (0.2.19) — a light release-candidate check between cuts: the changed-file self gate over everything that
2
+ * landed since the previous release's cut, plus an always-run test list (release/light-rc-always.txt), on the remote
3
+ * gate host (GATE-REMOTE). Read-only for the release ledger: it reads the previous published version and never writes.
4
+ * It does not touch the landing freeze or launch Pods. */
5
+ import { spawnSync } from 'node:child_process';
6
+ import { existsSync, readFileSync } from 'node:fs';
7
+ import { join } from 'node:path';
8
+ import { debug } from '../debug/log.js';
9
+ import { runSelfGateCli, type SelfGateCliOptions, type SelfGateCliResult } from '../self-implement/gate-cli.js';
10
+ import { dispatchHeavyCheck, isValidGateHost, type DispatchDeps, type DispatchOptions } from '../self-implement/gate-remote.js';
11
+ import { latestPublishedPreviousVersion } from '../../scripts/release-loop/unattended-release.js';
12
+ import { baseVersion, isReleaseVersion } from '../../scripts/release-loop/release-version.js';
13
+
14
+ export const LIGHT_RC_ALWAYS_FILE = 'release/light-rc-always.txt';
15
+ export const LIGHT_RC_DEFAULT_SHARDS = 4;
16
+ export const LIGHT_RC_NO_PREVIOUS_CUT = '직전 판 컷을 못 찾았다 — --base 로 준다';
17
+
18
+ export interface LightRcOptions {
19
+ base?: string;
20
+ /** Gate host; absent = config gateRemote.host (the dispatch's own default). */
21
+ remote?: string;
22
+ /** Run on this machine (the remote child is re-invoked with it). */
23
+ local?: boolean;
24
+ shards?: number;
25
+ /** Release ledger root for the previous-version lookup (release run's deps.ledgerRoot). */
26
+ ledgerRoot?: string;
27
+ log?: (line: string) => void;
28
+ }
29
+
30
+ export interface LightRcResult {
31
+ version: string;
32
+ base: string;
33
+ /** New failures vs. the base; null = the gate produced no measurement (never read as green). */
34
+ introduced: number | null;
35
+ preexisting: number | null;
36
+ /** Failures the baseline could not classify (unknown + precondition-unmet); null = unmeasured. Non-zero is not green. */
37
+ unclassified: number | null;
38
+ alwaysInclude: number;
39
+ alwaysIncludeMissing: string[];
40
+ shards: number;
41
+ /** Host the gate ran on; null = this machine. */
42
+ remote: string | null;
43
+ gateExitCode: number;
44
+ ok: boolean;
45
+ }
46
+
47
+ export interface LightRcDeps {
48
+ cwd?: string;
49
+ git?: (args: string[], cwd: string) => { rc: number | null; stdout: string };
50
+ previousVersion?: (version: string, ledgerRoot?: string) => string;
51
+ readFile?: (path: string) => string | undefined;
52
+ runGate?: (cwd: string, options: SelfGateCliOptions) => SelfGateCliResult;
53
+ dispatch?: (opts: DispatchOptions, deps?: DispatchDeps) => Promise<number>;
54
+ /** Extra dispatch deps (runner · settings) — tests drive the real dispatch with a fake host. */
55
+ dispatchDeps?: Omit<DispatchDeps, 'write'>;
56
+ }
57
+
58
+ /** Shown when the gate ran remotely but the host name did not come back. */
59
+ export const LIGHT_RC_UNKNOWN_HOST = '(remote · host unknown)';
60
+
61
+ const defaultGit = (args: string[], cwd: string) => {
62
+ const run = spawnSync('git', args, { cwd, encoding: 'utf8' });
63
+ return { rc: run.error ? null : run.status, stdout: run.stdout ?? '' };
64
+ };
65
+
66
+ /** One repository-relative path per line; `#` starts a comment; blank lines are skipped. */
67
+ export function parseAlwaysIncludeList(text: string): string[] {
68
+ return [...new Set(text.split('\n').map((line) => line.replace(/#.*/, '').trim()).filter(Boolean))];
69
+ }
70
+
71
+ /** The previous release's cut: merge-base of origin/main and origin/release/<previous published version>. */
72
+ export function previousCutBase(version: string, deps: LightRcDeps & { cwd: string; ledgerRoot?: string }): string {
73
+ let previous: string;
74
+ try {
75
+ previous = (deps.previousVersion ?? latestPublishedPreviousVersion)(baseVersion(version), deps.ledgerRoot);
76
+ } catch (error) {
77
+ throw new Error(`${LIGHT_RC_NO_PREVIOUS_CUT} (${error instanceof Error ? error.message : String(error)})`);
78
+ }
79
+ const mergeBase = (deps.git ?? defaultGit)(['merge-base', 'origin/main', `origin/release/${previous}`], deps.cwd);
80
+ const sha = mergeBase.stdout.trim();
81
+ if (mergeBase.rc !== 0 || !sha) throw new Error(`${LIGHT_RC_NO_PREVIOUS_CUT} (git merge-base origin/main origin/release/${previous} rc=${mergeBase.rc})`);
82
+ return sha;
83
+ }
84
+
85
+ function repoRoot(deps: LightRcDeps): string {
86
+ if (deps.cwd) return deps.cwd;
87
+ const top = defaultGit(['rev-parse', '--show-toplevel'], process.cwd());
88
+ if (top.rc !== 0 || !top.stdout.trim()) throw new Error('release light-rc: not inside a git repository');
89
+ return top.stdout.trim();
90
+ }
91
+
92
+ function parseRemoteResult(stdout: string): Partial<LightRcResult> | undefined {
93
+ const line = stdout.split('\n').map((l) => l.trim()).reverse().find((l) => l.startsWith('{'));
94
+ if (!line) return undefined;
95
+ try {
96
+ const parsed = JSON.parse(line) as Partial<LightRcResult> & { host?: unknown };
97
+ return parsed && typeof parsed === 'object' ? parsed : undefined;
98
+ } catch { return undefined; }
99
+ }
100
+
101
+ export async function runLightRc(version: string, opts: LightRcOptions = {}, deps: LightRcDeps = {}): Promise<LightRcResult> {
102
+ if (!isReleaseVersion(version)) throw new Error(`release version must be x.y.z: ${version}`);
103
+ const shards = opts.shards ?? LIGHT_RC_DEFAULT_SHARDS;
104
+ if (!Number.isSafeInteger(shards) || shards < 1) throw new Error(`invalid --shards count: ${opts.shards}`);
105
+ if (opts.remote !== undefined && !isValidGateHost(opts.remote)) throw new Error(`invalid --remote host: ${opts.remote}`);
106
+ const log = opts.log ?? ((line: string) => process.stderr.write(`${line}\n`));
107
+ const cwd = repoRoot(deps);
108
+ const base = opts.base ?? previousCutBase(version, { ...deps, cwd, ledgerRoot: opts.ledgerRoot });
109
+ const listPath = join(cwd, LIGHT_RC_ALWAYS_FILE);
110
+ const listText = (deps.readFile ?? ((path: string) => existsSync(path) ? readFileSync(path, 'utf8') : undefined))(listPath);
111
+ if (listText === undefined) throw new Error(`release light-rc: ${LIGHT_RC_ALWAYS_FILE} not found`);
112
+ const alwaysInclude = parseAlwaysIncludeList(listText);
113
+
114
+ let local: SelfGateCliResult | undefined;
115
+ const runLocal = () => {
116
+ local = (deps.runGate ?? runSelfGateCli)(cwd, { base, shards, alwaysInclude });
117
+ for (const line of local.lines) log(line);
118
+ return local.exitCode;
119
+ };
120
+ const captured: Buffer[] = [];
121
+ const rc = await (deps.dispatch ?? dispatchHeavyCheck)({
122
+ tool: 'self-gate', repo: cwd, installPwa: true, refs: [base], json: true,
123
+ // The host re-runs this command with --local (and ELANOUS_GATE_REMOTE_CHILD=1), so it never recurses.
124
+ remoteArgv: (sha) => ['bun', 'bin/elanous.mjs', 'release', 'light-rc', '--version', version, '--base', sha(base), '--shards', String(shards), '--local', '--json'],
125
+ flags: { remote: opts.local ? undefined : opts.remote ?? true, local: !!opts.local },
126
+ runLocal,
127
+ }, {
128
+ ...deps.dispatchDeps,
129
+ write: {
130
+ out: (data) => { captured.push(Buffer.from(data)); },
131
+ err: (data) => { process.stderr.write(data); },
132
+ },
133
+ });
134
+
135
+ let result: LightRcResult;
136
+ if (local) {
137
+ result = {
138
+ version, base,
139
+ introduced: local.baseline?.introduced ?? null,
140
+ preexisting: local.baseline?.preexisting ?? null,
141
+ unclassified: local.baseline ? local.baseline.unknown + local.baseline.preconditionUnmet : null,
142
+ alwaysInclude: alwaysInclude.length,
143
+ alwaysIncludeMissing: [...local.alwaysIncludeMissing],
144
+ shards, remote: null, gateExitCode: local.exitCode, ok: false,
145
+ };
146
+ } else {
147
+ const remote = parseRemoteResult(Buffer.concat(captured).toString('utf8'));
148
+ if (!remote) throw new Error(`release light-rc: remote gate returned no result (rc=${rc})`);
149
+ const host = (remote as { host?: unknown }).host;
150
+ result = {
151
+ version, base,
152
+ introduced: typeof remote.introduced === 'number' ? remote.introduced : null,
153
+ preexisting: typeof remote.preexisting === 'number' ? remote.preexisting : null,
154
+ unclassified: typeof remote.unclassified === 'number' ? remote.unclassified : null,
155
+ alwaysInclude: alwaysInclude.length,
156
+ alwaysIncludeMissing: Array.isArray(remote.alwaysIncludeMissing) ? remote.alwaysIncludeMissing.map(String) : [],
157
+ shards, remote: typeof host === 'string' && host ? host : opts.remote ?? LIGHT_RC_UNKNOWN_HOST,
158
+ gateExitCode: typeof remote.gateExitCode === 'number' ? remote.gateExitCode : rc, ok: false,
159
+ };
160
+ }
161
+ // Unmeasured (null) or unclassified failures are never green; policy-gate findings in the gate output are reported
162
+ // (gate rc), not a regression.
163
+ result.ok = result.introduced === 0 && result.unclassified === 0;
164
+ debug.log('release.light-rc', 'done', { version, base, introduced: result.introduced, preexisting: result.preexisting, unclassified: result.unclassified, shards, remote: result.remote });
165
+ return result;
166
+ }
167
+
168
+ export function formatLightRc(result: LightRcResult): string[] {
169
+ return [
170
+ `${result.ok ? '✅' : '⛔'} light-rc ${result.version} · base ${result.base.slice(0, 12)} · ${result.remote ? `host ${result.remote}` : 'local'} · shards ${result.shards}`,
171
+ ` introduced ${result.introduced ?? '못 쟀다'} · preexisting ${result.preexisting ?? '못 쟀다'} · unclassified ${result.unclassified ?? '못 쟀다'} · always-include ${result.alwaysInclude}${result.alwaysIncludeMissing.length ? ` (missing: ${result.alwaysIncludeMissing.join(', ')})` : ''} · gate rc ${result.gateExitCode}`,
172
+ ];
173
+ }