elanous 0.2.11 → 0.2.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (342) hide show
  1. package/apps/pwa/out/404/index.html +1 -1
  2. package/apps/pwa/out/404.html +1 -1
  3. package/apps/pwa/out/_next/static/chunks/{1394-556de6d71bbdb0fb.js → 1394-931c9827067b2276.js} +1 -1
  4. package/apps/pwa/out/_next/static/chunks/3869-3cd46526879f454c.js +1 -0
  5. package/apps/pwa/out/_next/static/chunks/{3992-2e522a8ac5f2afde.js → 3992-fe5c06d6b31f8b25.js} +1 -1
  6. package/apps/pwa/out/_next/static/chunks/5250-c78ca295585bbf95.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/6511-ff8360be3d65aaa4.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/6588-ba380869b67c4fa7.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/7758-85c2b53f731da024.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/{8605-ea5bee9dfae512c7.js → 8605-9da4678f8a310d18.js} +1 -1
  11. package/apps/pwa/out/_next/static/chunks/9906-e7b8b5310a169dfe.js +1 -0
  12. package/apps/pwa/out/_next/static/chunks/app/approvals/page-e5b4e1b256f41076.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-6a061bd97f0b9398.js → page-e49b9b76c565a0b1.js} +1 -1
  14. package/apps/pwa/out/_next/static/chunks/app/board/page-b4f032c893b21cc9.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/app/botlab/page-f14987d5850a31e0.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/app/bots/page-b433308aad13ceb6.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/app/chat/{page-7c97dcdfe2e37a60.js → page-4c1dbb21861beab2.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/consult/page-c85b816e3b269dbb.js +1 -0
  19. package/apps/pwa/out/_next/static/chunks/app/control/{page-5721537ca8a3598c.js → page-f6bc66b919b2ea7d.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/design-check/{page-b654240a572e4c48.js → page-60603c1e056c9b25.js} +1 -1
  21. package/apps/pwa/out/_next/static/chunks/app/editor/{page-b9750abc897508c3.js → page-a0ea8f5a2341ee6b.js} +1 -1
  22. package/apps/pwa/out/_next/static/chunks/app/exec/page-45c5bf04b4a605e6.js +1 -0
  23. package/apps/pwa/out/_next/static/chunks/app/field/page-36c6c4183353ac9e.js +1 -0
  24. package/apps/pwa/out/_next/static/chunks/app/inside/page-cb2eb0fd809980da.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/app/intake/{page-c8b1ccda6ba7ae98.js → page-31012264da8a341f.js} +1 -1
  26. package/apps/pwa/out/_next/static/chunks/app/{layout-d6b1d156fb95e74b.js → layout-50309aeb0bdd554a.js} +1 -1
  27. package/apps/pwa/out/_next/static/chunks/app/live/page-bc00e964e855f4df.js +1 -0
  28. package/apps/pwa/out/_next/static/chunks/app/market/{page-9688f5b4f1ff950f.js → page-0696ee3fac9fe56c.js} +1 -1
  29. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-66c27608f651b866.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/app/morning/{page-35414e0a007b4082.js → page-6ddd2109b7fc3a80.js} +1 -1
  31. package/apps/pwa/out/_next/static/chunks/app/not-found-4e50b7298c1f69a5.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/app/observatory/{page-e001823c35c9dd67.js → page-7a2bf830038a2264.js} +1 -1
  33. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-371423d1a573d64b.js → page-1f327f18694fabca.js} +1 -1
  34. package/apps/pwa/out/_next/static/chunks/app/ops/release/{page-4b764b125339ac17.js → page-e5cef2fe6e4028df.js} +1 -1
  35. package/apps/pwa/out/_next/static/chunks/app/ops/seats/{page-cdf8e4b01067bcf1.js → page-cf596128d2b420ba.js} +1 -1
  36. package/apps/pwa/out/_next/static/chunks/app/outputs/{page-3a24072b35470157.js → page-f672536f14f0969f.js} +1 -1
  37. package/apps/pwa/out/_next/static/chunks/app/page-b52365662232db89.js +1 -0
  38. package/apps/pwa/out/_next/static/chunks/app/reflection/page-e7a6c80f288b1948.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-d37cb0d52f9b6bd5.js → page-29a709616df9b050.js} +1 -1
  40. package/apps/pwa/out/_next/static/chunks/app/sessions/{page-686da0df9bf23e5c.js → page-fee842d7c2151129.js} +1 -1
  41. package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-4dc614f3ce043e63.js → page-78b40a344ed22f24.js} +1 -1
  42. package/apps/pwa/out/_next/static/chunks/app/settings/page-75d199f448171014.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-d02c782fc792e2af.js → page-a011dd5b0bff29c5.js} +1 -1
  44. package/apps/pwa/out/_next/static/chunks/app/setup/{page-239c17b911b84e72.js → page-d92e32d51ab5157e.js} +1 -1
  45. package/apps/pwa/out/_next/static/chunks/app/share/{page-dc6a79fb003f746c.js → page-5f21aed6932ef375.js} +1 -1
  46. package/apps/pwa/out/_next/static/chunks/app/showroom/{page-0fffb84cafdc9da6.js → page-1e1a53946804debf.js} +1 -1
  47. package/apps/pwa/out/_next/static/chunks/app/tasks/{page-1218753c82d585f6.js → page-a1833cd43d832601.js} +1 -1
  48. package/apps/pwa/out/_next/static/chunks/app/term/{page-4b5935e67531efb5.js → page-63d14a4d8fe1e1c1.js} +1 -1
  49. package/apps/pwa/out/_next/static/chunks/app/today/{page-3285bc87caf1eaa5.js → page-2a511d320f762e06.js} +1 -1
  50. package/apps/pwa/out/_next/static/chunks/app/trace/{page-ad553b0e06505ecd.js → page-1c3e03f88dac1e21.js} +1 -1
  51. package/apps/pwa/out/_next/static/chunks/app/vault/{page-fca096e3e860306f.js → page-c4d5289c2ea97a89.js} +1 -1
  52. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/{page-9ebfa78f0d9885c3.js → page-b91f8952deaeaf27.js} +1 -1
  53. package/apps/pwa/out/_next/static/chunks/app/workflows/{page-3dc5f21f7e39622d.js → page-71b43a2588489987.js} +1 -1
  54. package/apps/pwa/out/_next/static/chunks/app/workspace/page-2b256feed331546f.js +1 -0
  55. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-505bb96d69a8622c.js +1 -0
  56. package/apps/pwa/out/_next/static/css/128fb7fb670b2fbf.css +1 -0
  57. package/apps/pwa/out/approvals/index.html +1 -1
  58. package/apps/pwa/out/approvals/index.txt +14 -14
  59. package/apps/pwa/out/autopilot/index.html +1 -1
  60. package/apps/pwa/out/autopilot/index.txt +14 -14
  61. package/apps/pwa/out/board/index.html +1 -1
  62. package/apps/pwa/out/board/index.txt +14 -14
  63. package/apps/pwa/out/botlab/index.html +1 -1
  64. package/apps/pwa/out/botlab/index.txt +14 -14
  65. package/apps/pwa/out/bots/index.html +1 -1
  66. package/apps/pwa/out/bots/index.txt +14 -14
  67. package/apps/pwa/out/chat/index.html +1 -1
  68. package/apps/pwa/out/chat/index.txt +14 -14
  69. package/apps/pwa/out/consult/index.html +1 -1
  70. package/apps/pwa/out/consult/index.txt +14 -14
  71. package/apps/pwa/out/control/index.html +1 -1
  72. package/apps/pwa/out/control/index.txt +14 -14
  73. package/apps/pwa/out/design-check/index.html +1 -1
  74. package/apps/pwa/out/design-check/index.txt +14 -14
  75. package/apps/pwa/out/editor/index.html +1 -1
  76. package/apps/pwa/out/editor/index.txt +14 -14
  77. package/apps/pwa/out/exec/index.html +1 -1
  78. package/apps/pwa/out/exec/index.txt +14 -14
  79. package/apps/pwa/out/field/index.html +1 -1
  80. package/apps/pwa/out/field/index.txt +14 -14
  81. package/apps/pwa/out/healthz +1 -1
  82. package/apps/pwa/out/index.html +1 -1
  83. package/apps/pwa/out/index.txt +14 -14
  84. package/apps/pwa/out/inside/index.html +1 -1
  85. package/apps/pwa/out/inside/index.txt +14 -14
  86. package/apps/pwa/out/intake/index.html +1 -1
  87. package/apps/pwa/out/intake/index.txt +14 -14
  88. package/apps/pwa/out/live/index.html +1 -1
  89. package/apps/pwa/out/live/index.txt +14 -14
  90. package/apps/pwa/out/market/index.html +1 -1
  91. package/apps/pwa/out/market/index.txt +14 -14
  92. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  93. package/apps/pwa/out/missions/placeholder/index.txt +14 -14
  94. package/apps/pwa/out/morning/index.html +1 -1
  95. package/apps/pwa/out/morning/index.txt +14 -14
  96. package/apps/pwa/out/observatory/index.html +1 -1
  97. package/apps/pwa/out/observatory/index.txt +14 -14
  98. package/apps/pwa/out/ops/checklist/index.html +1 -1
  99. package/apps/pwa/out/ops/checklist/index.txt +14 -14
  100. package/apps/pwa/out/ops/release/index.html +1 -1
  101. package/apps/pwa/out/ops/release/index.txt +14 -14
  102. package/apps/pwa/out/ops/seats/index.html +1 -1
  103. package/apps/pwa/out/ops/seats/index.txt +14 -14
  104. package/apps/pwa/out/outputs/index.html +1 -1
  105. package/apps/pwa/out/outputs/index.txt +14 -14
  106. package/apps/pwa/out/reflection/index.html +1 -1
  107. package/apps/pwa/out/reflection/index.txt +15 -15
  108. package/apps/pwa/out/scheduler/index.html +1 -1
  109. package/apps/pwa/out/scheduler/index.txt +14 -14
  110. package/apps/pwa/out/sessions/index.html +1 -1
  111. package/apps/pwa/out/sessions/index.txt +14 -14
  112. package/apps/pwa/out/settings/devices/index.html +1 -1
  113. package/apps/pwa/out/settings/devices/index.txt +14 -14
  114. package/apps/pwa/out/settings/index.html +1 -1
  115. package/apps/pwa/out/settings/index.txt +14 -14
  116. package/apps/pwa/out/setup/done/index.html +1 -1
  117. package/apps/pwa/out/setup/done/index.txt +14 -14
  118. package/apps/pwa/out/setup/index.html +1 -1
  119. package/apps/pwa/out/setup/index.txt +14 -14
  120. package/apps/pwa/out/share/index.html +1 -1
  121. package/apps/pwa/out/share/index.txt +14 -14
  122. package/apps/pwa/out/showroom/index.html +1 -1
  123. package/apps/pwa/out/showroom/index.txt +14 -14
  124. package/apps/pwa/out/tasks/index.html +1 -1
  125. package/apps/pwa/out/tasks/index.txt +14 -14
  126. package/apps/pwa/out/term/index.html +1 -1
  127. package/apps/pwa/out/term/index.txt +14 -14
  128. package/apps/pwa/out/today/index.html +1 -1
  129. package/apps/pwa/out/today/index.txt +14 -14
  130. package/apps/pwa/out/trace/index.html +1 -1
  131. package/apps/pwa/out/trace/index.txt +14 -14
  132. package/apps/pwa/out/vault/index.html +1 -1
  133. package/apps/pwa/out/vault/index.txt +14 -14
  134. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  135. package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
  136. package/apps/pwa/out/workflows/index.html +1 -1
  137. package/apps/pwa/out/workflows/index.txt +14 -14
  138. package/apps/pwa/out/workspace/index.html +1 -1
  139. package/apps/pwa/out/workspace/index.txt +14 -14
  140. package/apps/pwa/out/worktrees/index.html +1 -1
  141. package/apps/pwa/out/worktrees/index.txt +14 -14
  142. package/examples/teams/personas/backend.yaml +4 -0
  143. package/examples/teams/personas/pm.yaml +4 -0
  144. package/examples/teams/personas/qa.yaml +4 -0
  145. package/examples/teams/small-product.yaml +20 -0
  146. package/graphs/context/nightly-condense.yaml +21 -0
  147. package/graphs/context/recipes.yaml +12 -0
  148. package/graphs/demo/inside-seed.yaml +20 -0
  149. package/graphs/demo/recipes.yaml +12 -0
  150. package/graphs/lecture/lecture-note.yaml +28 -0
  151. package/graphs/lecture/recipes.yaml +17 -0
  152. package/package.json +2 -1
  153. package/plugins/card-followup/examples/sample-card.png +0 -0
  154. package/scripts/context-hooks/emit.ts +36 -14
  155. package/scripts/coord-channel-watch.sh +1 -0
  156. package/scripts/discord-seat-probe.ts +144 -0
  157. package/scripts/hq/hq-standby.sh +65 -0
  158. package/scripts/hq/standby-promote.ts +173 -0
  159. package/scripts/hq/standby-role.ts +59 -0
  160. package/scripts/hq/standby-snapshot.ts +226 -0
  161. package/scripts/hq/standby-verify.ts +219 -0
  162. package/scripts/lecture-note/node.ts +256 -0
  163. package/scripts/lib/telegram-inject-args.ts +31 -0
  164. package/scripts/measure-pod-memory-by-goal.ts +130 -0
  165. package/scripts/release-loop/auto-approve-node.ts +1 -1
  166. package/scripts/release-loop/checklist-gate-node.ts +2 -1
  167. package/scripts/release-loop/gate-node.ts +61 -17
  168. package/scripts/release-loop/ops-upgrade-node.ts +59 -6
  169. package/scripts/release-loop/publish-node.ts +4 -1
  170. package/scripts/release-loop/version-node.ts +13 -3
  171. package/scripts/telegram-inject.ts +14 -6
  172. package/scripts/test-deterministic.ts +53 -3
  173. package/scripts/test-diet/remeasure-list.ts +112 -0
  174. package/scripts/ux/demo-rehearsal.ts +17 -13
  175. package/scripts/ux/demo-seed.ts +108 -0
  176. package/scripts/ux/lib/rehearsal-verdict.ts +12 -5
  177. package/skills/omni-crawl/SKILL.md +16 -16
  178. package/skills/omni-crawl/package-lock.json +1 -544
  179. package/skills/omni-crawl/package.json +3 -4
  180. package/skills/omni-crawl/scripts/main.ts +13 -13
  181. package/skills/omni-crawl/scripts/monitor.ts +8 -8
  182. package/skills/omni-crawl/src/apify.ts +13 -2
  183. package/skills/omni-crawl/src/capture.ts +6 -0
  184. package/skills/omni-crawl/src/registry.ts +2 -2
  185. package/skills/omni-crawl/src/stdout-json.ts +4 -0
  186. package/skills/omni-digest/SKILL.md +10 -11
  187. package/skills/omni-digest/package-lock.json +0 -545
  188. package/skills/omni-digest/package.json +2 -5
  189. package/skills/omni-digest/scripts/main.ts +3 -3
  190. package/skills/omni-digest/src/delegate.ts +5 -5
  191. package/skills/omni-digest/src/fetch.ts +2 -2
  192. package/skills/omni-digest/src/media.ts +25 -11
  193. package/skills/omni-digest/src/visual.ts +3 -2
  194. package/skills/omni-market/scripts/main.ts +1 -1
  195. package/skills/omni-market/scripts/stdout-json.ts +4 -0
  196. package/src/agent/identity-env.ts +11 -0
  197. package/src/agent-mission/driver.ts +150 -23
  198. package/src/agent-mission/duo.ts +94 -0
  199. package/src/agent-mission/pty-decision.ts +2 -2
  200. package/src/autopilot/pty-control-loop.ts +4 -1
  201. package/src/autopilot/track-agent-shadow.ts +2 -1
  202. package/src/autopilot/track-agent.ts +4 -1
  203. package/src/boot/daemon-prompt-request.ts +3 -0
  204. package/src/boot/daemon-prompt-turn.ts +6 -2
  205. package/src/boot/daemon-runtime.ts +48 -16
  206. package/src/chat/index.ts +1 -0
  207. package/src/cli/decisions-cli.ts +51 -19
  208. package/src/cli/outputs-cli.ts +92 -0
  209. package/src/cli/persona-cli.ts +45 -13
  210. package/src/cli/pod-cli.ts +27 -3
  211. package/src/cli/pod-memory-advice.ts +35 -0
  212. package/src/cli/pr-cli.ts +15 -1
  213. package/src/cli/release-cli.ts +135 -15
  214. package/src/context-bus/condense.ts +194 -0
  215. package/src/context-bus/context-day.ts +82 -0
  216. package/src/context-bus/context-now-surfaces.ts +32 -2
  217. package/src/context-bus/context-now.ts +2 -2
  218. package/src/context-bus/coord-events.ts +2 -2
  219. package/src/context-bus/long-term-memory.ts +118 -22
  220. package/src/context-bus/nightly-condense.ts +153 -0
  221. package/src/context-bus/session-events.ts +35 -0
  222. package/src/dashboard/dashboard-status-lines.ts +2 -0
  223. package/src/dashboard/index.ts +8 -0
  224. package/src/dashboard/slash-runtime/dashboard-handlers.ts +24 -0
  225. package/src/decisions/decision-cards.ts +13 -7
  226. package/src/decisions/decision-ledger.ts +43 -17
  227. package/src/domains/outbound-alert.ts +99 -31
  228. package/src/domains/outbound-log-probe.ts +8 -0
  229. package/src/execution-loop/launch-gate.ts +17 -8
  230. package/src/field-note/telegram.ts +142 -0
  231. package/src/graph-kinds/registry.ts +2 -2
  232. package/src/graph-runner/graph-approval-card.ts +2 -2
  233. package/src/graph-runner/graph-grow-llm.ts +23 -13
  234. package/src/graph-runner/graph-grow.ts +69 -13
  235. package/src/graph-runner/graph-tick.ts +9 -2
  236. package/src/graph-runner/runner.ts +92 -24
  237. package/src/harness/harness-cli-command.ts +165 -17
  238. package/src/harness/harness-queue-child.ts +37 -0
  239. package/src/harness/harness-queue.ts +306 -0
  240. package/src/hooks/consult-intake.ts +153 -0
  241. package/src/hooks/github-shadow.ts +72 -2
  242. package/src/hooks/receiver.ts +82 -3
  243. package/src/hq/hq.ts +318 -0
  244. package/src/hq/lease.ts +210 -0
  245. package/src/hq/ledger-write-target.ts +41 -0
  246. package/src/index.ts +232 -16
  247. package/src/intake-plane/discord-seat-work.ts +36 -3
  248. package/src/intake-plane/linear-wish.ts +44 -0
  249. package/src/intake-plane/persona-answer.ts +5 -0
  250. package/src/intake-plane/telegram-seat-work.ts +13 -6
  251. package/src/intake-plane/wish-card.ts +33 -0
  252. package/src/llm/difficulty.ts +1 -1
  253. package/src/loops/checker.ts +178 -0
  254. package/src/loops/loop-cli.ts +37 -3
  255. package/src/loops/observe.ts +49 -0
  256. package/src/loops/orchestrator/overlap.ts +141 -0
  257. package/src/loops/orchestrator/traffic.ts +150 -0
  258. package/src/loops/package/verify.ts +125 -0
  259. package/src/loops/verdict.ts +22 -0
  260. package/src/maturity/feature-maturity.ts +5 -4
  261. package/src/memory.ts +37 -4
  262. package/src/nexus/api/card-followup-route.ts +118 -0
  263. package/src/nexus/api/decisions-route.ts +68 -0
  264. package/src/nexus/api/http-server.ts +78 -8
  265. package/src/nexus/api/outbound-report.ts +40 -7
  266. package/src/nexus/api/personas.ts +97 -54
  267. package/src/nexus/api/skill-exec-route.ts +97 -0
  268. package/src/nexus/api/task-cards-api.ts +19 -0
  269. package/src/nexus/api/workflow-history.ts +88 -0
  270. package/src/nexus/api/workflows.ts +52 -0
  271. package/src/nexus/outbound/delivery-ledger.ts +17 -8
  272. package/src/nexus/outbound/router.ts +8 -3
  273. package/src/persona/loader.ts +18 -0
  274. package/src/persona/persona-todo.ts +14 -9
  275. package/src/persona/presets.ts +10 -2
  276. package/src/persona/team.ts +105 -0
  277. package/src/persona/types.ts +4 -0
  278. package/src/plugins/maker/market-bundle.ts +2 -1
  279. package/src/pod-lease/host-lease.ts +130 -20
  280. package/src/release-loop/checklist.ts +15 -3
  281. package/src/release-loop/feature-store.ts +24 -10
  282. package/src/release-loop/landed-but-yellow.ts +60 -0
  283. package/src/seat-dispatch/seat-ask.ts +57 -12
  284. package/src/seat-loop/seat-loop.ts +253 -18
  285. package/src/seat-loop/seat-pack.ts +127 -0
  286. package/src/self-dev/ask-launch-flow.ts +3 -0
  287. package/src/self-dev/draft-sweep.ts +44 -10
  288. package/src/self-dev/draft-triage-rules.ts +54 -2
  289. package/src/self-implement/budget-gate.ts +29 -12
  290. package/src/self-implement/gate-candidate-ranking.ts +60 -0
  291. package/src/self-implement/gate-scope.ts +9 -8
  292. package/src/self-implement/headless-elanous-driver.ts +7 -2
  293. package/src/self-implement/l8-merge-queue.ts +157 -1
  294. package/src/self-implement/orchestrator.ts +15 -0
  295. package/src/self-implement/test-seams.ts +1 -0
  296. package/src/steward/launch.ts +5 -1
  297. package/src/steward/readiness.ts +125 -0
  298. package/src/steward/rescue.ts +75 -0
  299. package/src/steward/triage.ts +48 -9
  300. package/src/task-cards/card-cli.ts +78 -0
  301. package/src/task-orchestrator/surfaces/pod-command-job.ts +14 -10
  302. package/src/task-orchestrator/surfaces/pod-lease.ts +18 -6
  303. package/src/task-orchestrator/surfaces/pod-pool.ts +27 -10
  304. package/src/task-orchestrator/surfaces/self-implement-pod.ts +46 -8
  305. package/src/telegram-commands.ts +21 -0
  306. package/src/telegram-dispatch-forward.ts +1 -1
  307. package/src/telegram-fabric-plan.ts +90 -0
  308. package/src/telegram-project-command.ts +84 -4
  309. package/src/telegram.ts +37 -5
  310. package/src/tool-runtime/elanous-control-runtimes.ts +2 -1
  311. package/src/user-config.ts +101 -13
  312. package/src/version/packed-revision.json +1 -1
  313. package/src/workflow-runtime/executor.ts +4 -0
  314. package/src/workflow-runtime/node-catalog.ts +13 -0
  315. package/src/workflow-runtime/nodes/knowledge.ts +38 -0
  316. package/src/workflow-runtime/schema.ts +17 -1
  317. package/src/workflow-runtime/types.ts +13 -1
  318. package/apps/pwa/out/_next/static/chunks/2310-7a59f043f6db207e.js +0 -1
  319. package/apps/pwa/out/_next/static/chunks/2548-6e316138d347cefe.js +0 -1
  320. package/apps/pwa/out/_next/static/chunks/3869-a39d1d9921e10d43.js +0 -1
  321. package/apps/pwa/out/_next/static/chunks/507-5acd98bd6dab2d8a.js +0 -1
  322. package/apps/pwa/out/_next/static/chunks/5445-745eb795c5963d79.js +0 -1
  323. package/apps/pwa/out/_next/static/chunks/9906-482c6619b3fdda20.js +0 -1
  324. package/apps/pwa/out/_next/static/chunks/app/approvals/page-1dcdc58fc2de2454.js +0 -1
  325. package/apps/pwa/out/_next/static/chunks/app/board/page-cb95a7432c9ee744.js +0 -1
  326. package/apps/pwa/out/_next/static/chunks/app/botlab/page-b12b6d2f8d2d1aea.js +0 -1
  327. package/apps/pwa/out/_next/static/chunks/app/bots/page-b9efe868bfd36255.js +0 -1
  328. package/apps/pwa/out/_next/static/chunks/app/consult/page-8fd36984f549270a.js +0 -1
  329. package/apps/pwa/out/_next/static/chunks/app/exec/page-4cd1de371f8ad048.js +0 -1
  330. package/apps/pwa/out/_next/static/chunks/app/field/page-d99f89657828421c.js +0 -1
  331. package/apps/pwa/out/_next/static/chunks/app/inside/page-50cb2a0eb15e6c83.js +0 -1
  332. package/apps/pwa/out/_next/static/chunks/app/live/page-2a9932ddb62d76bd.js +0 -1
  333. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-6b15dd6456c5df09.js +0 -1
  334. package/apps/pwa/out/_next/static/chunks/app/not-found-e7a529e0507ac86c.js +0 -1
  335. package/apps/pwa/out/_next/static/chunks/app/page-94c82eb0ae5abb32.js +0 -1
  336. package/apps/pwa/out/_next/static/chunks/app/reflection/page-0be8d4b2ced75ba0.js +0 -1
  337. package/apps/pwa/out/_next/static/chunks/app/settings/page-53f80edc6b8dc07c.js +0 -1
  338. package/apps/pwa/out/_next/static/chunks/app/workspace/page-c6da2b468454532d.js +0 -1
  339. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-93bc70e36f595414.js +0 -1
  340. package/apps/pwa/out/_next/static/css/543f56bcb1f79fca.css +0 -1
  341. /package/apps/pwa/out/_next/static/{Y4zKzL62sakG5momz3SoQ → D51l8jUBplJrIpWvENsDK}/_buildManifest.js +0 -0
  342. /package/apps/pwa/out/_next/static/{Y4zKzL62sakG5momz3SoQ → D51l8jUBplJrIpWvENsDK}/_ssgManifest.js +0 -0
@@ -0,0 +1,35 @@
1
+ import { readPodMemoryMeasurement, type MemoryMeasurement } from '../../scripts/measure-pod-memory-by-goal.js';
2
+
3
+ export type PodMemoryAdvice = {
4
+ window: MemoryMeasurement['window'];
5
+ sources: MemoryMeasurement['sources'];
6
+ byGoalType: Array<{
7
+ goalType: MemoryMeasurement['byGoalType'][number]['goalType'];
8
+ recommended: 'standard' | 'high';
9
+ limit: '16Gi' | '32Gi';
10
+ reason: string;
11
+ evidence: MemoryMeasurement['byGoalType'][number];
12
+ }>;
13
+ };
14
+
15
+ /** Advice only: the launch selector and its configuration remain independent. */
16
+ export function advisePodMemory(measurement: MemoryMeasurement): PodMemoryAdvice {
17
+ return {
18
+ window: measurement.window,
19
+ sources: measurement.sources,
20
+ byGoalType: measurement.byGoalType.map((evidence) => {
21
+ const high = evidence.oomKilled > 0 || (evidence.peakMiB.p95 !== null && evidence.peakMiB.p95 >= 16 * 1024);
22
+ const reason = evidence.oomKilled > 0
23
+ ? `${evidence.oomKilled}/${evidence.runs} OOMKilled; measured peak p50=${evidence.peakMiB.p50 ?? 'n/a'} MiB, p95=${evidence.peakMiB.p95 ?? 'n/a'} MiB (${evidence.measured}/${evidence.runs} measured)`
24
+ : evidence.measured === 0
25
+ ? `No measured peaks (${evidence.missingPeak}/${evidence.runs} missing); standard is provisional, not evidence of safety`
26
+ : `Measured peak p50=${evidence.peakMiB.p50} MiB, p95=${evidence.peakMiB.p95} MiB (${evidence.measured}/${evidence.runs} measured, ${evidence.missingPeak} missing); ${high ? 'at or above' : 'below'} 16Gi`;
27
+ return { goalType: evidence.goalType, recommended: high ? 'high' as const : 'standard' as const,
28
+ limit: high ? '32Gi' as const : '16Gi' as const, reason, evidence };
29
+ }),
30
+ };
31
+ }
32
+
33
+ export function readPodMemoryAdvice(path?: string): PodMemoryAdvice {
34
+ return advisePodMemory(readPodMemoryMeasurement(path));
35
+ }
package/src/cli/pr-cli.ts CHANGED
@@ -53,6 +53,7 @@ import {
53
53
  } from './pr-granularity.js';
54
54
  import { branchLineageSlug, findSiblingPrs } from './pr-lineage.js';
55
55
  import { parseReleaseNoteSection } from '../release-loop/release-note.js';
56
+ import { enqueueL8ShadowQueue, runL8ShadowQueue, shadowPrNumber, statusL8ShadowQueue, type L8ShadowQueueDeps } from '../self-implement/l8-merge-queue.js';
56
57
 
57
58
  export { branchLineageSlug, findSiblingPrs };
58
59
 
@@ -1635,8 +1636,21 @@ export async function runPrLand(opts: PrLandOpts = {}, deps: PrLandDeps = {}): P
1635
1636
  return 0;
1636
1637
  }
1637
1638
 
1638
- export function registerPrCommands(program: Command, deps: PrLandDeps = {}): void {
1639
+ export function registerPrCommands(program: Command, deps: PrLandDeps = {}, queueDeps: L8ShadowQueueDeps = {}): void {
1639
1640
  const pr = program.command('pr').description('PR landing commands');
1641
+ const queue = pr.command('queue').description('Shadow merge queue (never lands PRs)');
1642
+ const queueAction = async (action: () => Promise<unknown>) => {
1643
+ try {
1644
+ (deps.out ?? liveOut).log(JSON.stringify(await action()));
1645
+ } catch (error) {
1646
+ (deps.out ?? liveOut).error(`✗ merge-queue: ${String(error)}`);
1647
+ (deps.setExitCode ?? ((code: number) => { process.exitCode = code; }))(1);
1648
+ }
1649
+ };
1650
+ queue.command('status').description('Show pending PRs and shadow verdicts').action(() => queueAction(() => statusL8ShadowQueue(process.cwd(), queueDeps)));
1651
+ queue.command('enqueue <pr>').description('Pin a ready PR at the tail of the shadow queue').action((prNumber: string) => queueAction(() => enqueueL8ShadowQueue(process.cwd(), shadowPrNumber(prNumber), queueDeps)));
1652
+ queue.command('run').description('Evaluate the first PR without merging').requiredOption('--shadow', 'Only evaluate; never land').action(() => queueAction(() => runL8ShadowQueue(process.cwd(), queueDeps)));
1653
+
1640
1654
  pr.command('land')
1641
1655
  .description('현재 브랜치 PR을 생성 또는 갱신해 ready 상태로 만들고 squash merge')
1642
1656
  .option('--base <branch>', 'PR base branch')
@@ -23,12 +23,15 @@ import { debug } from '../debug/log.js';
23
23
  import { effectiveInstanceRoot, releaseLedgerRoot } from '../instance/resolve.js';
24
24
  import { getElanousConfigDirOverride } from '../elanous-config-dir.js';
25
25
  import { envLiteral } from '../platform/env-literal.js';
26
- import { userConfigPath } from '../user-config.js';
26
+ import { getUserConfig, userConfigPath } from '../user-config.js';
27
27
  import { addItem, claimItem, decodeClaimHistoryEntry, devVersion, listChecklist, ownerMatches, parseOwner, parityGap, removeItem, seedFromRoadmap, setItem, summarizeChecklist, type ChecklistStatus, type ChecklistDisposition, type ChecklistKind } from '../release-loop/checklist.js';
28
28
  import * as features from '../release-loop/feature-store.js';
29
+ import { evidencePlan, landedButYellow, type MergedChecklistPr } from '../release-loop/landed-but-yellow.js';
29
30
  import { formatSchedule, getSchedule, listSchedules, setSchedule } from '../release-loop/release-schedule.js';
30
31
  import { writeStdoutJson } from './stdout-json.js';
31
32
  import { CliUserError } from './cli-user-error.js';
33
+ import { hqCliWriteAllowed, type HqDeps } from '../hq/hq.js';
34
+ import { fileLeaseStore } from '../hq/lease.js';
32
35
  import { runUnattendedRelease, type UnattendedReleaseDeps } from '../../scripts/release-loop/unattended-release.js';
33
36
  import { cutReleaseBranch } from '../../scripts/release-loop/cut-branch.js';
34
37
  import { releaseReadiness } from '../../scripts/release-loop/release-readiness.js';
@@ -499,10 +502,48 @@ async function checklistOutput(v: string, codenames: Record<string, string>, mod
499
502
  }
500
503
  }
501
504
 
502
- export function registerReleaseCommands(program: Command, releaseRunDeps: UnattendedReleaseDeps = {}): void {
505
+ export interface LandedButYellowDeps {
506
+ run?: Runner;
507
+ checklist?: typeof listChecklist;
508
+ now?: () => Date;
509
+ evidenceAdd?: typeof features.evidenceAdd;
510
+ schedules?: typeof features.readSchedules;
511
+ released?: typeof features.releasedVersion;
512
+ }
513
+
514
+ function printLandedRows(rows: ReturnType<typeof landedButYellow>, items: ReturnType<typeof listChecklist>['items']): void {
515
+ const updatedAtById = new Map(items.map((item) => [item.id, item.updatedAt]));
516
+ const date = (iso: string) => {
517
+ const parts = new Intl.DateTimeFormat('en-US', { timeZone: 'Asia/Seoul', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hourCycle: 'h23' }).formatToParts(new Date(iso));
518
+ const part = (name: string) => parts.find((entry) => entry.type === name)?.value;
519
+ return `${part('month')}-${part('day')} ${part('hour')}:${part('minute')}`;
520
+ };
521
+ for (const row of rows) {
522
+ const icon = row.status === 'yellow' ? '🟡' : '🔴';
523
+ console.log(`${row.id} ${row.owner ?? '-'} ${icon} ← ${row.prs.map((pr, i) => `#${pr.number} «${pr.title}» (${date(pr.mergedAt)} · 근거에 ${row.alreadyInEvidence[i] ? '있음' : '없음'}${pr.mergedAt > (updatedAtById.get(row.id) ?? '') ? ' · 칸보다 새것' : ''})`).join(' · ')}`);
524
+ }
525
+ console.log(`${rows.length}칸 · 그중 근거에 없는 PR 이 있는 칸 ${rows.filter((row) => row.alreadyInEvidence.includes(false)).length}`);
526
+ }
527
+
528
+ function compareReleaseVersions(a: string, b: string): number {
529
+ const parse = (version: string) => {
530
+ const [core, pre] = version.split('-');
531
+ const [major, minor, patch] = core!.split('.').map(Number);
532
+ const [stage, sequence] = pre?.split('.') ?? [];
533
+ return [major!, minor!, patch!, stage === undefined ? 3 : ({ alpha: 0, beta: 1, rc: 2 } as Record<string, number>)[stage]!, Number(sequence ?? 0)];
534
+ };
535
+ const left = parse(a), right = parse(b);
536
+ for (let i = 0; i < left.length; i++) if (left[i] !== right[i]) return left[i]! - right[i]!;
537
+ return 0;
538
+ }
539
+
540
+ export function registerReleaseCommands(program: Command, releaseRunDeps: UnattendedReleaseDeps = {}, landedDeps: LandedButYellowDeps = {}, hqDeps: HqDeps = {}): void {
503
541
  const release = program.command('release').description('공개 배포 한 판 — prepare(로컬) → publish(--yes) → verify (docs/manual/MANUAL-versioning-and-release-2026-09-25.md)');
504
542
  const schedule = release.command('schedule').description('판별 컷·착지 마감 조회·갱신');
505
543
  const scheduleLedgerRoot = () => releaseRunDeps.ledgerRoot ?? releaseLedgerRoot();
544
+ const ledgerFenceDeps = (): HqDeps => getElanousConfigDirOverride() && !hqDeps.store && !getUserConfig().hq?.arbiter
545
+ ? { ...hqDeps, store: fileLeaseStore(join(effectiveInstanceRoot(), 'hq', 'lease.json')), seenPath: join(effectiveInstanceRoot(), 'hq', 'seen-generation'), localPath: join(effectiveInstanceRoot(), 'hq', 'local.json') }
546
+ : hqDeps;
506
547
  schedule.command('list').option('--json', '결과 JSON').action((o: { json?: boolean }) => {
507
548
  const rows = listSchedules(scheduleLedgerRoot());
508
549
  if (o.json) console.log(JSON.stringify(rows));
@@ -515,21 +556,25 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
515
556
  if (o.json) console.log(JSON.stringify(row)); else console.log(formatSchedule(row));
516
557
  });
517
558
  schedule.command('set').requiredOption('--version <v>', '갱신할 판')
518
- .option('--cut-at <iso>', '오프셋 포함 컷 시각').option('--land-by <iso>', '오프셋 포함 착지 마감').option('--json', '결과 JSON')
519
- .action((o: { version: string; cutAt?: string; landBy?: string; json?: boolean }) => {
559
+ .option('--cut-at <iso>', '오프셋 포함 컷 시각').option('--land-by <iso>', '오프셋 포함 착지 마감').option('--json', '결과 JSON').option('--hq-override')
560
+ .action((o: { version: string; cutAt?: string; landBy?: string; json?: boolean; hqOverride?: boolean }) => {
520
561
  if (o.cutAt === undefined && o.landBy === undefined) throw new CliUserError('갱신할 시각을 지정하라', '--cut-at <iso> 또는 --land-by <iso>');
562
+ if (!hqCliWriteAllowed('release schedule set', Boolean(o.hqOverride), ledgerFenceDeps())) return;
521
563
  const row = setSchedule(o.version, { cutAt: o.cutAt, landBy: o.landBy }, process.env.ELANOUS_TRACK || 'cli', scheduleLedgerRoot());
522
564
  if (o.json) console.log(JSON.stringify(row)); else console.log(formatSchedule(row));
523
565
  });
524
566
  const checklist = release.command('checklist').description('판별 확인표 조회·갱신')
525
567
  .option('--version <v>', '판 또는 별칭(기본: package.json 의 개발판에서 -dev.N 제거)')
526
- .option('--json', '결과 JSON');
568
+ .option('--json', '결과 JSON')
569
+ .option('--hq-override', '본부 임대 거부를 관측하며 수동 우회');
570
+ const mayWrite = (cmd: Command) => hqCliWriteAllowed(`release checklist ${cmd.name()}`, Boolean(cmd.optsWithGlobals().hqOverride), ledgerFenceDeps());
527
571
  const context = (cmd: Command) => {
528
572
  const opts = { ...(cmd.parent?.parent?.opts() as { version?: string; json?: boolean }), ...(cmd.parent?.opts() as { version?: string; json?: boolean }), ...(cmd.opts() as { version?: string; json?: boolean }) };
529
573
  const codenames = checklistCodenames();
530
574
  return { version: checklistVersion(opts.version, codenames), codenames, json: opts.json };
531
575
  };
532
- const withContext = (cmd: Command) => cmd.option('--version <v>', '판 또는 별칭').option('--json', '결과 JSON');
576
+ const withContext = (cmd: Command) => cmd.option('--version <v>', '판 또는 별칭').option('--json', '결과 JSON').option('--hq-override', '본부 임대 거부를 관측하며 수동 우회');
577
+ const actor = () => process.env.ELANOUS_TRACK || 'cli';
533
578
  withContext(checklist.command('list').description('모든 칸')).option('--owner <owner>', '자리 또는 하위 자리로 거르기').action(async (opts: { owner?: string }, cmd: Command) => {
534
579
  const { version, codenames, json } = context(cmd);
535
580
  await checklistOutput(version, codenames, 'list', json, opts.owner);
@@ -538,10 +583,78 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
538
583
  const { version, codenames, json } = context(cmd);
539
584
  await checklistOutput(version, codenames, 'status', json, opts.owner);
540
585
  });
586
+ withContext(checklist.command('landed-but-yellow').description('병합 PR 이 있지만 노랑·빨강인 칸(기본 읽기 전용)'))
587
+ .option('--owner <owner>', '자리 또는 하위 자리로 거르기')
588
+ .option('--open', '미발행 일정 판 중 칸이 있는 판 전부')
589
+ .option('--apply-evidence', '확실한 PR 짝의 근거를 붙인다(상태는 그대로)')
590
+ .option('--dry-run', '근거 추가 없이 계획만 출력')
591
+ .action(async (opts: { owner?: string; open?: boolean; applyEvidence?: boolean; dryRun?: boolean }, cmd: Command) => {
592
+ try {
593
+ const inherited = cmd.parent?.opts() as { version?: string; json?: boolean } | undefined;
594
+ const local = cmd.opts() as { version?: string; json?: boolean };
595
+ if (opts.open && (local.version !== undefined || inherited?.version !== undefined)) throw new CliUserError('--open 과 --version 을 함께 쓸 수 없다');
596
+ const json = local.json ?? inherited?.json;
597
+ if (opts.owner !== undefined) parseOwner(opts.owner);
598
+ const released = opts.open ? (landedDeps.released ?? features.releasedVersion)() : '';
599
+ const openSnapshots = opts.open
600
+ ? (landedDeps.schedules ?? features.readSchedules)()
601
+ .filter(({ version }) => isReleaseVersion(version) && (!released || compareReleaseVersions(version, released) > 0))
602
+ .sort((a, b) => compareReleaseVersions(a.version, b.version))
603
+ .map(({ version }) => (landedDeps.checklist ?? listChecklist)(version)).filter((snapshot) => snapshot.items.length > 0)
604
+ : [];
605
+ if (opts.open && !openSnapshots.length) {
606
+ if (json) await writeStdoutJson('[]\n'); else console.log('열린 판 없음');
607
+ return;
608
+ }
609
+ if (opts.applyEvidence && !opts.dryRun && !mayWrite(cmd)) return;
610
+ const since = new Date((landedDeps.now ?? (() => new Date()))().getTime() - 7 * 86_400_000).toISOString().slice(0, 10);
611
+ const raw = must((landedDeps.run ?? defaultRunner)('gh', ['pr', 'list', '--state', 'merged', '--search', `merged:>=${since}`, '--limit', '400', '--json', 'number,title,body,mergedAt'], process.cwd()), '병합 PR 조회(gh)');
612
+ const prs = JSON.parse(raw) as MergedChecklistPr[];
613
+ if (!Array.isArray(prs)) throw new Error('병합 PR 조회(gh): 배열이 아니다');
614
+ const snapshots = opts.open ? openSnapshots : [(landedDeps.checklist ?? listChecklist)(context(cmd).version)];
615
+ const results: Array<{ version: string; rows?: ReturnType<typeof landedButYellow>; plan?: ReturnType<typeof evidencePlan> }> = [];
616
+ let totalAdded = 0, totalSkipped = 0;
617
+ for (const snapshot of snapshots) {
618
+ const { version, items } = snapshot;
619
+ const rows = landedButYellow(items, prs, { owner: opts.owner });
620
+ const skippedMention = new Set(rows.flatMap((row) => row.prs.flatMap((pr) =>
621
+ pr.basis === 'mention' ? [JSON.stringify([row.id, pr.number])] : []))).size;
622
+ if (opts.open) totalSkipped += skippedMention;
623
+ if (opts.open && !json) console.log(`— ${version} —`);
624
+ if (opts.applyEvidence || opts.dryRun) {
625
+ const plan = evidencePlan(rows);
626
+ const log = json ? console.error : console.log;
627
+ for (const { id, ref } of plan) {
628
+ if (!opts.dryRun) (landedDeps.evidenceAdd ?? features.evidenceAdd)(id, version, ref, actor(), snapshot.released, snapshot.dev);
629
+ log(`${opts.dryRun ? '·' : '✅'} ${id} 근거 ${ref}`);
630
+ }
631
+ const added = opts.dryRun ? 0 : plan.length;
632
+ totalAdded += added;
633
+ log(`붙임 ${added} · 건너뜀(언급만) ${skippedMention}`);
634
+ if (!opts.open && !opts.dryRun) debug.log('release.checklist', 'evidence-synced', { added, skippedMention });
635
+ results.push({ version, plan });
636
+ } else {
637
+ if (!json) printLandedRows(rows, items);
638
+ results.push({ version, rows });
639
+ }
640
+ }
641
+ if (opts.open) {
642
+ if (opts.applyEvidence && !opts.dryRun) debug.log('release.checklist', 'evidence-synced', { versions: snapshots.map(({ version }) => version), added: totalAdded, skippedMention: totalSkipped });
643
+ if (!json) console.log(`판 ${snapshots.length} · 붙임 합 ${totalAdded} · 건너뜀(언급만) 합 ${totalSkipped}`);
644
+ else await writeStdoutJson(`${JSON.stringify(results)}\n`);
645
+ } else if (json) await writeStdoutJson(`${JSON.stringify(opts.applyEvidence || opts.dryRun ? results[0]!.plan : results[0]!.rows)}\n`);
646
+ if (prs.length >= 400) console.error('⚠ 병합 PR 400개 상한: 목록이 잘렸을 수 있다');
647
+ } catch (error) {
648
+ console.error(`⛔ ${error instanceof Error ? error.message : String(error)}`);
649
+ process.exitCode = 1;
650
+ }
651
+ });
541
652
  withContext(checklist.command('add <id> <title>').description('칸 추가')).option('--owner <owner>', '담당').option('--kind <kind>', 'screen = 다섯 화면 짝 칸 · 근거에 짝: PWA … · 데스크톱 … · 폴드 … · 아이폰 … · 아이패드 … 한 줄')
542
- .action((id: string, title: string, opts: { owner?: string; kind?: string }, cmd: Command) => {
653
+ .option('--allow-duplicate-id', '다른 판에 같은 id 가 있어도 경고 후 추가')
654
+ .action((id: string, title: string, opts: { owner?: string; kind?: string; allowDuplicateId?: boolean }, cmd: Command) => {
543
655
  const { version, json } = context(cmd);
544
- const data = addItem(version, { id, title, ...(opts.owner !== undefined ? { owner: opts.owner } : {}), ...(opts.kind !== undefined ? { kind: opts.kind as ChecklistKind } : {}) });
656
+ if (!mayWrite(cmd)) return;
657
+ const data = addItem(version, { id, title, ...(opts.owner !== undefined ? { owner: opts.owner } : {}), ...(opts.kind !== undefined ? { kind: opts.kind as ChecklistKind } : {}) }, { allowDuplicateId: opts.allowDuplicateId });
545
658
  if (json) console.log(JSON.stringify(data)); else console.log(`✅ ${id} 추가`);
546
659
  });
547
660
  withContext(checklist.command('set <id>').description('칸 상태·근거·담당·처분 갱신'))
@@ -552,6 +665,7 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
552
665
  if (opts.status !== undefined && !['green', 'yellow', 'red', 'done'].includes(opts.status)) throw new CliUserError(`잘못된 상태: ${opts.status}`, 'green|yellow|red|done');
553
666
  if (opts.disposition !== undefined && !['move', 'known-issue', 'block'].includes(opts.disposition)) throw new CliUserError(`잘못된 처분: ${opts.disposition}`, 'move|known-issue|block');
554
667
  if (opts.status === undefined && opts.evidence === undefined && opts.owner === undefined && opts.disposition === undefined && opts.kind === undefined) throw new CliUserError('갱신할 칸을 지정하라', '--status · --evidence · --owner · --disposition · --kind 중 하나');
668
+ if (!mayWrite(cmd)) return;
555
669
  const data = setItem(version, id, { ...(opts.status !== undefined ? { status: opts.status as ChecklistStatus } : {}), ...(opts.evidence !== undefined ? { evidence: opts.evidence } : {}), ...(opts.owner !== undefined ? { owner: opts.owner } : {}), ...(opts.disposition !== undefined ? { disposition: opts.disposition as ChecklistDisposition } : {}), ...(opts.kind !== undefined ? { kind: opts.kind as ChecklistKind } : {}) }, process.env.ELANOUS_TRACK || 'cli');
556
670
  const item = data.items.find((entry) => entry.id === id);
557
671
  const why = item?.kind === 'screen' && item.status === 'green' ? parityGap(item.evidence) : null;
@@ -565,40 +679,45 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
565
679
  .requiredOption('--by <owner>', '잡을 자리 또는 하위 자리').option('--force', '기존 주인에서 강제로 바꾼다')
566
680
  .action((id: string, opts: { by: string; force?: boolean }, cmd: Command) => {
567
681
  const { version, json } = context(cmd);
682
+ if (!mayWrite(cmd)) return;
568
683
  const data = claimItem(version, id, opts.by, { force: opts.force });
569
684
  if (json) console.log(JSON.stringify(data)); else console.log(`✅ ${id} 주인 ${opts.by}`);
570
685
  });
571
686
  withContext(checklist.command('rm <id>').description('칸 삭제')).action((id: string, _opts: unknown, cmd: Command) => {
572
687
  const { version, json } = context(cmd);
688
+ if (!mayWrite(cmd)) return;
573
689
  const data = removeItem(version, id, process.env.ELANOUS_TRACK || 'cli');
574
690
  if (json) console.log(JSON.stringify(data)); else console.log(`✅ ${id} 삭제`);
575
691
  });
576
692
  withContext(checklist.command('seed').description('마크다운 로드맵에서 빈 칸 들이기')).requiredOption('--from <file>', '로드맵 파일')
577
693
  .action((opts: { from: string }, cmd: Command) => {
578
694
  const { version, json } = context(cmd);
695
+ if (!mayWrite(cmd)) return;
579
696
  const data = seedFromRoadmap(version, readFileSync(opts.from, 'utf8'));
580
697
  if (json) console.log(JSON.stringify(data)); else console.log(`✅ ${version} ${data.items.length}칸`);
581
698
  });
582
- const actor = () => process.env.ELANOUS_TRACK || 'cli';
583
699
  withContext(checklist.command('move <id>').description('칸을 한 트랜잭션으로 다른 판에 옮긴다'))
584
- .requiredOption('--from <v>', '현재 판').requiredOption('--to <v>', '새 판')
585
- .action((id: string, opts: { from: string; to: string }, cmd: Command) => {
700
+ .requiredOption('--from <v>', '현재 판').requiredOption('--to <v>', '새 판').option('--reason <reason>', '이동 사유')
701
+ .action((id: string, opts: { from: string; to: string; reason?: string }, cmd: Command) => {
586
702
  const { codenames, json } = context(cmd);
587
703
  const from = checklistVersion(opts.from, codenames), to = checklistVersion(opts.to, codenames);
588
- features.move(id, from, to, actor());
704
+ if (!mayWrite(cmd)) return;
705
+ features.move(id, from, to, actor(), undefined, undefined, opts.reason);
589
706
  if (json) console.log(JSON.stringify(listChecklist(to))); else console.log(`✅ ${id} ${from} → ${to}`);
590
707
  });
591
708
  withContext(checklist.command('retitle <id> <title>').description('칸 제목 수정')).action((id: string, title: string, _opts: unknown, cmd: Command) => {
592
709
  const { version, json } = context(cmd);
710
+ if (!mayWrite(cmd)) return;
593
711
  const snapshot = listChecklist(version);
594
712
  features.retitle(id, title, actor(), snapshot.released, snapshot.dev);
595
713
  if (json) console.log(JSON.stringify(listChecklist(version))); else console.log(`✅ ${id} 제목 수정`);
596
714
  });
597
715
  const evidence = checklist.command('evidence').description('칸의 PR·커밋 근거');
598
716
  withContext(evidence.command('add <id> <ref>').description('근거 참조 추가')).action((id: string, ref: string, _opts: unknown, cmd: Command) => {
599
- const { version, json } = context(cmd);
600
- const snapshot = listChecklist(version);
601
- features.evidenceAdd(id, version, ref, actor(), snapshot.released, snapshot.dev);
717
+ const { version, json } = context(cmd);
718
+ if (!mayWrite(cmd)) return;
719
+ const snapshot = listChecklist(version);
720
+ features.evidenceAdd(id, version, ref, actor(), snapshot.released, snapshot.dev);
602
721
  if (json) console.log(JSON.stringify(features.history(id).map(decodeClaimHistoryEntry))); else console.log(`✅ ${id} 근거 ${ref}`);
603
722
  });
604
723
  withContext(checklist.command('history <id>').description('칸의 판 이동·변경 이력')).action((id: string, _opts: unknown, cmd: Command) => {
@@ -614,6 +733,7 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
614
733
  });
615
734
  withContext(checklist.command('export').description('SQLite 에서 checklist.json 스냅샷 생성')).action((_opts: unknown, cmd: Command) => {
616
735
  const { version, json } = context(cmd);
736
+ if (!mayWrite(cmd)) return;
617
737
  const snapshot = listChecklist(version);
618
738
  features.exportJson(version, snapshot.released, snapshot.dev);
619
739
  if (json) console.log(JSON.stringify(listChecklist(version))); else console.log(`✅ ${version} checklist.json 내보냄`);
@@ -0,0 +1,194 @@
1
+ import { Database } from 'bun:sqlite';
2
+ import { closeSync, constants, existsSync, fstatSync, lstatSync, mkdirSync, openSync } from 'node:fs';
3
+ import { dlopen, FFIType } from 'bun:ffi';
4
+ import { join } from 'node:path';
5
+ import { listMemories, memoryRoot, saveMemory, type MemoryEntry } from '../memory.js';
6
+ import { surfaceEventsDbPath } from '../domains/surface-events.js';
7
+ import { DecisionLedger } from '../decisions/decision-ledger.js';
8
+ import { listCoordEvents } from './coord-events.js';
9
+ import { condenseContextWindowWithReport, type CondenseSkipped, type MemoryDeps, type MemoryItem, type MemorySource, type MemorySummary } from './long-term-memory.js';
10
+
11
+ const summarize = async (source: MemorySource): Promise<MemorySummary> =>
12
+ (await import('./nightly-condense.js')).summarize(source);
13
+
14
+ /** Reuse the context-bus reader against a strictly read-only SQLite handle. */
15
+ function readContextEvents(since: string) {
16
+ const path = surfaceEventsDbPath();
17
+ if (!existsSync(path)) return [];
18
+ const db = new Database(path, { readonly: true, strict: true });
19
+ try { return listCoordEvents({ since }, { db }); }
20
+ finally { db.close(); }
21
+ }
22
+
23
+ export interface CondenseReport {
24
+ since: string;
25
+ until: string;
26
+ cards: MemoryItem[];
27
+ conflicts: number;
28
+ skipped: CondenseSkipped;
29
+ folded: number;
30
+ markdown: string;
31
+ applied?: { written: number; retired: number; decisions: string[] };
32
+ }
33
+
34
+ function mdCell(value: string): string {
35
+ return value.replaceAll('|', '\\|').replaceAll('\n', ' ');
36
+ }
37
+
38
+ export function renderCondenseMarkdown(report: Pick<CondenseReport, 'since' | 'until' | 'cards' | 'conflicts' | 'skipped'>): string {
39
+ const lines = [
40
+ '# Long-term context memory candidates',
41
+ '',
42
+ `Window: ${report.since} → ${report.until} · Cards: ${report.cards.length} · Conflicts: ${report.conflicts}`,
43
+ `Skipped: ${JSON.stringify(report.skipped)}`,
44
+ '',
45
+ ];
46
+ for (const card of report.cards) {
47
+ lines.push(`## ${mdCell(card.project)} · ${mdCell(card.seat)} · ${mdCell(card.topic)}`,
48
+ `- Updated: ${card.updatedAt}`,
49
+ `- Candidate: ${mdCell(card.summary)}`,
50
+ `- Source: ${card.source}`);
51
+ if (card.conflict) lines.push(`- 충돌 (${card.conflict.owner}): ${card.conflict.sources.join(' ↔ ')}`);
52
+ lines.push('');
53
+ }
54
+ return lines.join('\n').trimEnd() + '\n';
55
+ }
56
+
57
+ type CondenseStore = {
58
+ list(): MemoryEntry[];
59
+ save(input: Parameters<typeof saveMemory>[0]): MemoryEntry;
60
+ raise(input: Parameters<DecisionLedger['raiseOnce']>[0], ref: string): string;
61
+ withLock<T>(action: () => T): T;
62
+ };
63
+
64
+ /** Persistent inode in the existing memory directory: all condense writers share one cross-process lock. */
65
+ function withMemoryLock<T>(dir: string, action: () => T): T {
66
+ mkdirSync(dir, { recursive: true });
67
+ const libc = process.platform === 'darwin' ? '/usr/lib/libSystem.B.dylib' : process.platform === 'linux' ? 'libc.so.6' : null;
68
+ if (!libc) throw new Error('context memory locking unavailable on this platform');
69
+ const lib = dlopen(libc, { flock: { args: [FFIType.i32, FFIType.i32], returns: FFIType.i32 } });
70
+ const flock = lib.symbols.flock as (fd: number, op: number) => number;
71
+ const path = join(dir, '.context-condense.lock');
72
+ let fd: number | undefined;
73
+ let acquired = false;
74
+ try {
75
+ fd = openSync(path, constants.O_CREAT | constants.O_RDWR | constants.O_NOFOLLOW, 0o600);
76
+ const stat = fstatSync(fd);
77
+ const current = lstatSync(path);
78
+ if (!stat.isFile() || stat.ino !== current.ino || stat.dev !== current.dev || (stat.mode & 0o077) !== 0) {
79
+ throw new Error('context memory lock must be a private regular file');
80
+ }
81
+ const deadline = Date.now() + 10_000;
82
+ const pause = new Int32Array(new SharedArrayBuffer(4));
83
+ while (flock(fd, 2 | 4) !== 0) {
84
+ if (Date.now() >= deadline) throw new Error('context memory lock timed out');
85
+ Atomics.wait(pause, 0, 0, 10);
86
+ }
87
+ acquired = true;
88
+ const locked = lstatSync(path);
89
+ if (stat.ino !== locked.ino || stat.dev !== locked.dev) throw new Error('context memory lock changed');
90
+ return action();
91
+ } finally {
92
+ if (fd !== undefined) {
93
+ try { if (acquired) flock(fd, 8); } finally { closeSync(fd); }
94
+ }
95
+ lib.close();
96
+ }
97
+ }
98
+
99
+ function defaultStore(root?: string): CondenseStore {
100
+ const memoryDir = root ? join(root, 'elanous', 'memory') : memoryRoot();
101
+ const ledger = new DecisionLedger(root ? { stateDir: root } : {});
102
+ return {
103
+ list: () => listMemories({}, memoryDir),
104
+ save: input => saveMemory(input, memoryDir),
105
+ raise: (input, ref) => ledger.raiseOnce(input, ref).id,
106
+ withLock: action => withMemoryLock(memoryDir, action),
107
+ };
108
+ }
109
+
110
+ function cardKey(card: Pick<MemoryItem, 'project' | 'seat' | 'topic'>): string {
111
+ return JSON.stringify([card.project, card.seat, card.topic]);
112
+ }
113
+
114
+ function newestActive(entries: readonly MemoryEntry[]): MemoryEntry | undefined {
115
+ return entries.reduce<MemoryEntry | undefined>((best, entry) => {
116
+ const current = entry.contextCard!;
117
+ const previous = best?.contextCard;
118
+ return !previous || current.updatedAt > previous.updatedAt
119
+ || (current.updatedAt === previous.updatedAt && current.source > previous.source)
120
+ || (current.updatedAt === previous.updatedAt && current.source === previous.source && entry.id > best!.id)
121
+ ? entry : best;
122
+ }, undefined);
123
+ }
124
+
125
+ /** Apply candidate cards through the existing curated memory and decision ledger paths. */
126
+ export function applyCondensedCards(cards: readonly MemoryItem[], store: CondenseStore): NonNullable<CondenseReport['applied']> {
127
+ const decisions: string[] = [];
128
+ let written = 0;
129
+ let retired = 0;
130
+ for (const card of cards) {
131
+ if (card.conflict) {
132
+ const ref = `context-condense:${JSON.stringify([card.project, card.seat, card.topic, card.source, card.updatedAt])}`;
133
+ decisions.push(store.raise({
134
+ title: `Context conflict: ${card.project} / ${card.seat} / ${card.topic}`.slice(0, 120),
135
+ category: 'scope',
136
+ scqa: { s: `Candidate (${card.updatedAt}): ${card.summary}`.slice(0, 220), c: `Conflicting sources: ${card.conflict.sources.join(' ↔ ')}`.slice(0, 220) },
137
+ options: [
138
+ { key: 'a', label: 'Keep the existing claim', consequence: 'Candidate is not stored' },
139
+ { key: 'b', label: 'Reconcile and apply the new claim', consequence: 'Resolve source disagreement before memory update' },
140
+ ],
141
+ recommendation: { skipped: true, reason: 'Conflicting claims require OP judgment' },
142
+ raisedBy: { agent: 'context-condense', track: 'OP' },
143
+ refs: [ref, ...card.conflict.sources],
144
+ }, ref));
145
+ continue;
146
+ }
147
+ if (card.status !== 'active') continue;
148
+ const applyTopic = () => {
149
+ const prior = store.list().filter(entry => entry.contextCard?.status === 'active' && cardKey(entry.contextCard) === cardKey(card));
150
+ const winner = newestActive(prior);
151
+ const alreadyPresent = prior.some(entry => entry.contextCard!.updatedAt === card.updatedAt && entry.contextCard!.source === card.source);
152
+ if (!alreadyPresent && (!winner || winner.contextCard!.updatedAt < card.updatedAt
153
+ || (winner.contextCard!.updatedAt === card.updatedAt && winner.contextCard!.source < card.source))) {
154
+ store.save({ type: 'project', name: `${card.project} · ${card.seat} · ${card.topic}`,
155
+ description: card.summary, body: `${card.summary}\n\nSource: ${card.source}\nUpdated: ${card.updatedAt}\n`,
156
+ contextCard: { project: card.project, seat: card.seat, topic: card.topic, source: card.source, updatedAt: card.updatedAt, status: 'active' } });
157
+ written++;
158
+ }
159
+ const active = store.list().filter(entry => entry.contextCard?.status === 'active' && cardKey(entry.contextCard) === cardKey(card));
160
+ const keep = newestActive(active);
161
+ for (const old of active) {
162
+ if (old.id === keep?.id) continue;
163
+ store.save({ type: old.type, name: old.name, description: old.description, body: old.body, id: old.id,
164
+ pinned: false, priority: old.priority, contextCard: { ...old.contextCard!, status: 'retired' } });
165
+ retired++;
166
+ }
167
+ };
168
+ store.withLock(applyTopic);
169
+ }
170
+ return { written, retired, decisions };
171
+ }
172
+
173
+ /** Produce a rolling-window candidate snapshot; no store is opened for writing in dry-run. */
174
+ export async function runContextCondense(options: {
175
+ hours: number; apply?: boolean; now?: Date; root?: string;
176
+ }, deps: MemoryDeps = { summarize }, store?: CondenseStore): Promise<CondenseReport> {
177
+ if (!Number.isSafeInteger(options.hours) || options.hours <= 0 || options.hours > 24 * 30) {
178
+ throw new Error('--since must be a positive number of hours (at most 720h)');
179
+ }
180
+ const now = options.now ?? new Date();
181
+ const until = now.toISOString();
182
+ const since = new Date(now.getTime() - options.hours * 3_600_000).toISOString();
183
+ const { cards, skipped, folded } = await condenseContextWindowWithReport(since, until, [], {
184
+ summarize: deps.summarize,
185
+ events: deps.events ?? readContextEvents,
186
+ decisions: deps.decisions ?? (() => new DecisionLedger(options.root ? { stateDir: options.root } : {}).list({ status: 'all' })
187
+ .filter(decision => decision.raisedBy.agent !== 'context-condense')),
188
+ }, now);
189
+ const conflicts = cards.filter(card => card.conflict !== undefined).length;
190
+ const report: CondenseReport = { since, until, cards, conflicts, skipped, folded,
191
+ markdown: renderCondenseMarkdown({ since, until, cards, conflicts, skipped }) };
192
+ if (options.apply === true) report.applied = applyCondensedCards(cards, store ?? defaultStore(options.root));
193
+ return report;
194
+ }
@@ -0,0 +1,82 @@
1
+ import { Database } from 'bun:sqlite';
2
+ import { existsSync } from 'node:fs';
3
+ import { surfaceEventsDbPath } from '../domains/surface-events.js';
4
+
5
+ export const CONTEXT_DAY_SOURCES = ['OP', 'TC', 'MK', 'UX', 'claude-code', 'codex-agent-mission', 'harness-child'] as const;
6
+ export type ContextDaySource = typeof CONTEXT_DAY_SOURCES[number];
7
+ export type ContextDayKind = 'task-claimed' | 'done' | 'asked' | 'guidance-changed';
8
+ export interface ContextDayRow {
9
+ source: ContextDaySource;
10
+ counts: Record<ContextDayKind, number>;
11
+ lastAt: string | null;
12
+ status: '들어옴' | '안 들어옴';
13
+ }
14
+ export interface ContextDayReport { since: string; until: string; rows: ContextDayRow[] }
15
+
16
+ /** Reject malformed windows instead of silently changing the time range being judged. */
17
+ export function contextDayHours(since: string): number {
18
+ if (!/^[1-9]\d*h$/.test(since)) throw new Error('--since requires positive hours, e.g. 24h');
19
+ const hours = Number(since.slice(0, -1));
20
+ if (!Number.isSafeInteger(hours) || hours * 3_600_000 > 8.64e15) throw new Error('--since is out of range');
21
+ return hours;
22
+ }
23
+
24
+ function kindFor(surface: string, kind: string): ContextDayKind | null {
25
+ if (surface === 'context:external') {
26
+ if (kind === 'started' || kind === 'claimed') return 'task-claimed';
27
+ if (kind === 'finished' || kind === 'done') return 'done';
28
+ if (kind === 'guide-changed') return 'guidance-changed';
29
+ }
30
+ if (kind === 'task-claimed' || kind === 'claimed') return 'task-claimed';
31
+ if (kind === 'task-done' || kind === 'done') return 'done';
32
+ if (kind === 'asked' || (surface === 'coord:channel' && kind === '요청')) return 'asked';
33
+ if (kind === 'guidance-changed' || kind === 'guide-changed') return 'guidance-changed';
34
+ return null;
35
+ }
36
+
37
+ /** Read only the context journal, not recallEvents (which mutates recall counts) or the writable DB opener. */
38
+ export function contextDay(since = '24h', deps: { now?: () => Date; dbPath?: string } = {}): ContextDayReport {
39
+ const hours = contextDayHours(since);
40
+ const now = (deps.now ?? (() => new Date()))();
41
+ const until = now.toISOString();
42
+ const from = new Date(now.getTime() - hours * 3_600_000).toISOString();
43
+ const rows: ContextDayRow[] = CONTEXT_DAY_SOURCES.map(source => ({
44
+ source, counts: { 'task-claimed': 0, done: 0, asked: 0, 'guidance-changed': 0 }, lastAt: null, status: '안 들어옴',
45
+ }));
46
+ const bySource = new Map(rows.map(row => [row.source, row]));
47
+ const path = deps.dbPath ?? surfaceEventsDbPath();
48
+ if (!existsSync(path)) return { since: from, until, rows };
49
+ const db = new Database(path, { readonly: true });
50
+ try {
51
+ const events = db.prepare(`SELECT surface, kind, ts, refs FROM events
52
+ WHERE surface IN ('coord:channel', 'context:session', 'context:external')
53
+ AND direction='outbound' AND ts>=? AND ts<=? ORDER BY ts ASC`).all(from, until) as Array<{
54
+ surface: string; kind: string; ts: string; refs: string | null;
55
+ }>;
56
+ for (const event of events) {
57
+ let refs: { seat?: unknown; origin?: unknown };
58
+ try { refs = JSON.parse(event.refs ?? '{}') as typeof refs; } catch { continue; }
59
+ const source = event.surface === 'context:external' ? refs.origin : refs.seat;
60
+ if (typeof source !== 'string') continue;
61
+ const row = bySource.get(source as ContextDaySource);
62
+ if (!row) continue;
63
+ // Do not attribute an external hook to a seat (or a seat event to an external origin).
64
+ if ((event.surface === 'context:external') !== (rows.indexOf(row) >= 4)) continue;
65
+ row.lastAt = event.ts;
66
+ row.status = '들어옴';
67
+ const kind = kindFor(event.surface, event.kind);
68
+ if (kind) row.counts[kind]++;
69
+ }
70
+ } finally { db.close(); }
71
+ return { since: from, until, rows };
72
+ }
73
+
74
+ export function renderContextDay(report: ContextDayReport): string {
75
+ return [
76
+ `맥락 이벤트 ${report.since} ~ ${report.until} (UTC)`,
77
+ '| 출처 | task-claimed | done | asked | guidance-changed | 마지막 시각 (UTC) | 상태 |',
78
+ '| --- | ---: | ---: | ---: | ---: | --- | --- |',
79
+ ...report.rows.map(row => `| ${row.source} | ${row.counts['task-claimed']} | ${row.counts.done} | ${row.counts.asked} | ${row.counts['guidance-changed']} | ${row.lastAt ?? '—'} | ${row.status} |`),
80
+ '안 들어옴 = 이 시간 창에서 해당 출처 이벤트 0건 (Claude Code 훅 미설치 가능성; 설치 여부는 미확인). 카운트 열은 네 종류만 셈.',
81
+ ].join('\n');
82
+ }
@@ -4,6 +4,23 @@ export function readSlashContextNow(args: string[], deps?: ContextNowDeps): Cont
4
4
  return contextNow({ topic: args.join(' ').trim() }, deps);
5
5
  }
6
6
 
7
+ export function seatsNowLine(answer: ContextNowAnswer, now: number): string | null {
8
+ const seats = [ ['OP', 'COO'], ['MK', 'CMO'], ['TC', 'CTO'], ['UX', 'CXO'] ] as const;
9
+ const facts = answer.facts.filter(fact => fact.kind === 'seat');
10
+ const lines = seats.flatMap(([seat, label]) => {
11
+ const latest = facts.filter(fact => fact.seat === seat)
12
+ .sort((a, b) => b.at.localeCompare(a.at))[0];
13
+ if (!latest) return [];
14
+ const minutes = Math.max(0, Math.floor((now - Date.parse(latest.at)) / 60_000));
15
+ const age = minutes >= 24 * 60 ? '하루 넘음'
16
+ : minutes >= 60 ? `${Math.floor(minutes / 60)}시간 전`
17
+ : minutes > 0 ? `${minutes}분 전` : '방금';
18
+ const title = latest.title?.split(/\r?\n/, 1)[0]?.trim();
19
+ return [`${label} ${title ? title.slice(0, 20) : latest.status} · ${age}`];
20
+ });
21
+ return lines.length ? `지금 자리들: ${lines.join(' | ')}` : null;
22
+ }
23
+
7
24
  function factText(fact: ContextFact): string {
8
25
  switch (fact.kind) {
9
26
  case 'version': return fact.version;
@@ -26,10 +43,23 @@ export function renderTelegramNow(answer: ContextNowAnswer): string {
26
43
  }
27
44
 
28
45
  export function renderTuiNow(answer: ContextNowAnswer): string[] {
46
+ const shortText = (text: string) => {
47
+ const characters = Array.from(text);
48
+ return characters.length > 100 ? `${characters.slice(0, 100).join('')}…` : text;
49
+ };
50
+ const shortSource = (source: string) => {
51
+ if (/^https?:\/\/github\.com\/[^/]+\/[^/]+\/pull\/\d+#issuecomment-/.test(source)) return '채널';
52
+ if (source.startsWith('elanous://release/')) return '원장';
53
+ return Array.from(source).slice(0, 40).join('');
54
+ };
55
+ const factLabels = { version: '판', cell: '칸', decision: '결정', seat: '자리' } as const;
56
+ const eventLabels: Record<string, string> = { report: '보고', dispatch: '발사', decision: '결정' };
57
+ const events = answer.events.slice().sort((a, b) => Date.parse(b.at) - Date.parse(a.at));
29
58
  const rows = [
30
- ...answer.facts.map(f => [f.kind, factText(f), f.source]),
31
- ...answer.events.map(e => [e.kind, e.summary, e.source]),
59
+ ...answer.facts.map(f => [factLabels[f.kind], shortText(factText(f)), shortSource(f.source)]),
60
+ ...events.slice(0, 8).map(e => [eventLabels[e.kind] ?? '소식', shortText(e.summary), shortSource(e.source)]),
32
61
  ...answer.guide.map(guide => ['안내', guide, '']),
62
+ ...(events.length > 8 ? [['안내', `… 사건 ${events.length - 8}개 더(/now <주제> 로 좁히기)`, '']] : []),
33
63
  ];
34
64
  return [
35
65
  `지금${answer.topic ? ` · ${answer.topic}` : ''} (${answer.at})`,