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
@@ -2,6 +2,7 @@ import { spawnSync } from 'node:child_process';
2
2
  import { existsSync, readFileSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  import { CliUserError } from '../cli/cli-user-error.js';
5
+ import { resolveRepositoryName } from '../harness/repository-name.js';
5
6
  import { getElanousConfigDirOverride } from '../elanous-config-dir.js';
6
7
  import { effectiveInstanceRoot } from '../instance/resolve.js';
7
8
  import { userConfigPath } from '../user-config.js';
@@ -40,10 +41,41 @@ function versionOrder(a: string, b: string): number {
40
41
  return 0;
41
42
  }
42
43
 
44
+ /** Where each cell id lives. An id may repeat when one copy is in an already released version — history kept two
45
+ * different cells under one id (IA1 · EV10a … in 0.2.6~0.2.15) and later cells reused old ids (INSIDE1 0.2.11 ⊕
46
+ * 0.2.19). A released copy is history: an open copy wins over it, and between released copies the latest wins. Only
47
+ * two open copies are a real conflict (ORCH-LIVE-1008 rehearsal 1: every placement failed on «여러 판의 같은 칸»). */
48
+ function cellLocations(snapshots: Iterable<[string, readonly { id: string }[]]>, released: string | null | undefined, skip: readonly string[] = []): { locations: Map<string, string>; conflict: string | null } {
49
+ const locations = new Map<string, string>();
50
+ const shipped = (version: string) => !!released && versionOrder(version, released) <= 0;
51
+ for (const [version, items] of snapshots) {
52
+ if (skip.includes(version)) continue;
53
+ for (const item of items) {
54
+ const previous = locations.get(item.id);
55
+ if (previous !== undefined) {
56
+ if (!shipped(previous) && !shipped(version)) return { locations, conflict: item.id };
57
+ // Keep the open copy; between two released copies keep the later one.
58
+ const keepPrevious = shipped(version) && (!shipped(previous) || versionOrder(version, previous) < 0);
59
+ if (keepPrevious) continue;
60
+ }
61
+ locations.set(item.id, version);
62
+ }
63
+ }
64
+ return { locations, conflict: null };
65
+ }
66
+
43
67
  /** The merged PR list is a first-class GitHub query, not a count inferred from checklist status. */
68
+ // ORCH-LIVE-1008: the orchestrator tick runs from the installed package (not a git checkout), where a bare `gh pr list`
69
+ // fails «not a git repository» and placement silently left every cell unplaced — so the repository is named explicitly
70
+ // (--repo > harness.repo > GH_REPO > the checkout's remote · `resolveRepositoryName`).
44
71
  export function mergedPrsLast24h(now = new Date()): number {
45
72
  const since = new Date(now.getTime() - 86_400_000).toISOString();
46
- const r = spawnSync('gh', ['pr', 'list', '--state', 'merged', '--search', `merged:>=${since.slice(0, 10)}`, '--limit', '1000', '--json', 'number,mergedAt'], { encoding: 'utf8' });
73
+ let repo: string;
74
+ try { repo = resolveRepositoryName(); }
75
+ catch (error) {
76
+ throw new CliUserError(`병합 PR 속도 조회 실패: 저장소를 못 정했다 — ${(error instanceof Error ? error.message : String(error)).split('\n')[0]}`, 'harness.repo 설정(owner/name) 또는 GH_REPO');
77
+ }
78
+ const r = spawnSync('gh', ['pr', 'list', '--repo', repo, '--state', 'merged', '--search', `merged:>=${since.slice(0, 10)}`, '--limit', '1000', '--json', 'number,mergedAt'], { encoding: 'utf8' });
47
79
  if (r.status !== 0) throw new CliUserError(`병합 PR 속도 조회 실패: ${(r.stderr || r.error || '').toString().trim()}`);
48
80
  let rows: Array<{ number: number; mergedAt: string }>;
49
81
  try { rows = JSON.parse(r.stdout) as typeof rows; }
@@ -92,10 +124,16 @@ function dependencyViolation(
92
124
  locations: Map<string, string>,
93
125
  overrides: Map<string, string>,
94
126
  input: PlacementCell,
127
+ released?: string | null,
95
128
  ): string | null {
96
129
  for (const [version, snapshot] of snapshots) for (const item of snapshot.items) {
97
- const dependentVersion = overrides.get(item.id) ?? version;
130
+ // A released version is history — its cells (and what they once depended on) never constrain a new placement.
131
+ if (released && versionOrder(version, released) <= 0) continue;
98
132
  const predecessors = item.id === input.id ? input.predecessors : item.predecessors ?? [];
133
+ // Only the cells this placement moves, and cells that depend on them, can be made wrong by it — a pre-existing
134
+ // violation elsewhere on the board must not make every placement infeasible (ORCH-LIVE-1008 rehearsal 1).
135
+ if (!overrides.has(item.id) && !predecessors.some((predecessor) => overrides.has(predecessor))) continue;
136
+ const dependentVersion = overrides.get(item.id) ?? version;
99
137
  for (const predecessor of predecessors) {
100
138
  const predecessorVersion = overrides.get(predecessor) ?? locations.get(predecessor);
101
139
  if (!predecessorVersion || versionOrder(dependentVersion, predecessorVersion) <= 0)
@@ -131,12 +169,8 @@ export function placeCell(input: PlacementCell, deps: PlacementDeps = {}): Place
131
169
  const open = schedules.filter((row) => (!released || versionOrder(row.version, released) > 0) && row.landBy && available(row, now, backlog));
132
170
  const sources = [...new Set([...schedules.map((row) => row.version), ...backlog])];
133
171
  const snapshots = new Map(sources.map((version) => [version, (deps.checklist ?? listChecklist)(version)]));
134
- const locations = new Map<string, string>();
135
- for (const [version, snapshot] of snapshots) for (const item of snapshot.items) {
136
- if (backlog.includes(version)) continue;
137
- if (locations.has(item.id)) throw new CliUserError(`여러 판의 같은 칸: ${item.id}`);
138
- locations.set(item.id, version);
139
- }
172
+ const { locations, conflict } = cellLocations([...snapshots].map(([version, snapshot]) => [version, snapshot.items] as [string, readonly { id: string }[]]), released, backlog);
173
+ if (conflict) throw new CliUserError(`여러 판의 같은 칸: ${conflict}`);
140
174
  // A backlog copy never outranks a scheduled cell with the same id.
141
175
  for (const version of backlog) for (const item of snapshots.get(version)?.items ?? []) if (!locations.has(item.id)) locations.set(item.id, version);
142
176
  const from = locations.get(input.id) ?? null;
@@ -172,7 +206,7 @@ export function placeCell(input: PlacementCell, deps: PlacementDeps = {}): Place
172
206
  const displaced: PlacementDecision['displaced'] = [];
173
207
  let chosen: ReleaseSchedule | undefined;
174
208
  let loadIssue: string | null = null;
175
- const feasible = candidates.filter((row) => !dependencyViolation(snapshots, locations, new Map([[input.id, row.version]]), input));
209
+ const feasible = candidates.filter((row) => !dependencyViolation(snapshots, locations, new Map([[input.id, row.version]]), input, released));
176
210
  // 판은 «마감 안 가장 이른 판»부터 본다(기존 불변식). 가속 등급은 그 판이 찼을 때 같은 우선순위 비가속 칸을 밀 수 있게만 한다.
177
211
  for (const row of feasible) {
178
212
  const placement = new Map([[input.id, row.version]]);
@@ -192,7 +226,7 @@ export function placeCell(input: PlacementCell, deps: PlacementDeps = {}): Place
192
226
  || (accelerator && priority === input.priority);
193
227
  const p2 = [...items].reverse().find((item) => {
194
228
  if (item.accelerator === true) return false;
195
- if (!victimOk(item.priority) || dependencyViolation(snapshots, locations, new Map([...placement, [item.id, next.version]]), input)) return false;
229
+ if (!victimOk(item.priority) || dependencyViolation(snapshots, locations, new Map([...placement, [item.id, next.version]]), input, released)) return false;
196
230
  if (item.deadlineVersion && versionOrder(next.version, item.deadlineVersion) > 0) return false;
197
231
  if (items.length < capacity(row) && !(seatItems.length >= seatLimit && ownerMatches(item.owner, seat))) return false;
198
232
  if (!item.owner) return false;
@@ -235,6 +269,7 @@ export function placeCell(input: PlacementCell, deps: PlacementDeps = {}): Place
235
269
  function moveConstraint(
236
270
  item: ChecklistItem, to: ReleaseSchedule, backlog: readonly string[],
237
271
  snapshots: Map<string, ChecklistItem[]>, now: number, rate: number, caps: Record<string, number>, rows: ReleaseSchedule[], ceoCap: number,
272
+ released: string | null | undefined,
238
273
  ): string | null {
239
274
  if (!to.landBy || !available(to, now, backlog)) return '착지 마감이 지났거나 백로그 판이라 이동 불가';
240
275
  if (item.deadlineVersion && versionOrder(to.version, item.deadlineVersion) > 0) return '마감 판 뒤로 이월 불가';
@@ -246,16 +281,15 @@ function moveConstraint(
246
281
  const loadIssue = ceoOverload(item, to, new Map([...snapshots].map(([version, items]) => [version, { items }])),
247
282
  rows, ceoCap);
248
283
  if (loadIssue) return loadIssue;
249
- const locations = new Map<string, string>();
250
- for (const [version, items] of snapshots) for (const cell of items) {
251
- if (locations.has(cell.id)) return `여러 판의 같은 칸: ${cell.id}`;
252
- locations.set(cell.id, version);
253
- }
284
+ const { locations, conflict } = cellLocations(snapshots, released);
285
+ if (conflict) return `여러 판의 같은 칸: ${conflict}`;
254
286
  for (const predecessor of item.predecessors ?? []) {
255
287
  const location = locations.get(predecessor);
256
288
  if (!location || versionOrder(to.version, location) <= 0) return `선행 칸 ${predecessor} 이후 판으로만 이동 가능`;
257
289
  }
258
290
  for (const [version, items] of snapshots) for (const dependent of items) {
291
+ // A released version is history — its cells never pin a move (same rule as placement).
292
+ if (released && versionOrder(version, released) <= 0) continue;
259
293
  if (dependent.id !== item.id && dependent.predecessors?.includes(item.id) && versionOrder(version, to.version) <= 0)
260
294
  return `의존 칸 ${dependent.id} 이전 판으로만 이동 가능`;
261
295
  }
@@ -289,7 +323,7 @@ export function rebalance(version: string, deps: PlacementDeps = {}): RebalanceR
289
323
  const ceoCap = validCeoCap(deps.ceoDailyCap ?? placementCeoDailyCap());
290
324
  const result: RebalanceResult = { decisions: [], blocked: [] };
291
325
  for (const { item, score } of unstarted) {
292
- const violation = moveConstraint(item, next!, backlog, snapshots, now, rate, caps, rows, ceoCap);
326
+ const violation = moveConstraint(item, next!, backlog, snapshots, now, rate, caps, rows, ceoCap, deps.released ?? releasedVersion());
293
327
  if (violation) {
294
328
  result.blocked.push({ id: item.id, from: version, to: next!.version, reason: violation });
295
329
  continue;
@@ -315,7 +349,7 @@ export function seatMove(id: string, from: string, to: string, by: string, reaso
315
349
  if (!ownerMatches(item.owner, parseOwner(by).seat) || next?.version !== to) throw new CliUserError('남의 칸 당기기 거부 — COO 에 요청');
316
350
  const now = (deps.now ?? new Date()).getTime();
317
351
  const rate = deps.merged24h ?? mergedPrsLast24h(new Date(now));
318
- const violation = moveConstraint(item, next, backlog, snapshots, now, rate, deps.seatCap ?? placementSeatCap(), rows, validCeoCap(deps.ceoDailyCap ?? placementCeoDailyCap()));
352
+ const violation = moveConstraint(item, next, backlog, snapshots, now, rate, deps.seatCap ?? placementSeatCap(), rows, validCeoCap(deps.ceoDailyCap ?? placementCeoDailyCap()), deps.released ?? releasedVersion());
319
353
  if (violation) throw new CliUserError(`${id}: ${violation}`);
320
354
  if (!deps.dryRun) move(id, from, to, by, undefined, undefined, reason);
321
355
  }
@@ -35,12 +35,13 @@ export function listSchedules(root?: string): ReleaseSchedule[] {
35
35
  return schedules;
36
36
  }
37
37
 
38
- export function setSchedule(version: string, patch: { cutAt?: string; landBy?: string; freezeFrom?: string; freezeUntil?: string }, by: string, root?: string): ReleaseSchedule {
38
+ export function setSchedule(version: string, patch: { cutAt?: string; landBy?: string; freezeFrom?: string; freezeUntil?: string; publishAt?: string }, by: string, root?: string): ReleaseSchedule {
39
39
  const schedule = writeSchedule(version, {
40
40
  ...(patch.cutAt !== undefined ? { cutAt: utcIso(patch.cutAt) } : {}),
41
41
  ...(patch.landBy !== undefined ? { landBy: utcIso(patch.landBy) } : {}),
42
42
  ...(patch.freezeFrom !== undefined ? { freezeFrom: utcIso(patch.freezeFrom) } : {}),
43
43
  ...(patch.freezeUntil !== undefined ? { freezeUntil: utcIso(patch.freezeUntil) } : {}),
44
+ ...(patch.publishAt !== undefined ? { publishAt: utcIso(patch.publishAt) } : {}),
44
45
  }, by, root);
45
46
  debug.log('release.schedule', 'set', { version, cutAt: schedule.cutAt, landBy: schedule.landBy });
46
47
  return schedule;
@@ -51,5 +52,8 @@ export function formatSchedule(schedule: ReleaseSchedule): string {
51
52
  const deadline = schedule.landBy ? formatKst(schedule.landBy) : null;
52
53
  const sameDay = schedule.landBy && new Date(Date.parse(schedule.landBy) + 9 * 60 * 60_000).toISOString().slice(0, 10)
53
54
  === new Date(Date.parse(schedule.cutAt) + 9 * 60 * 60_000).toISOString().slice(0, 10);
54
- return `${schedule.version} 컷 ${cut}${deadline ? ` · 착지 마감 ${sameDay ? deadline.split(' ')[1] : deadline}` : ''}`;
55
+ const publish = schedule.publishAt ? formatKst(schedule.publishAt) : null;
56
+ const publishSameDay = schedule.publishAt && new Date(Date.parse(schedule.publishAt) + 9 * 60 * 60_000).toISOString().slice(0, 10)
57
+ === new Date(Date.parse(schedule.cutAt) + 9 * 60 * 60_000).toISOString().slice(0, 10);
58
+ return `${schedule.version} 컷 ${cut}${deadline ? ` · 착지 마감 ${sameDay ? deadline.split(' ')[1] : deadline}` : ''}${publish ? ` · 발행 ${publishSameDay ? publish.split(' ')[1] : publish}` : ''}`;
55
59
  }
@@ -2,6 +2,56 @@ import { Database } from 'bun:sqlite';
2
2
  import { existsSync, readFileSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  import type { ChecklistItem } from './checklist.js';
5
+ import { debug } from '../debug/log.js';
6
+ import { getUserConfig } from '../user-config.js';
7
+
8
+ export type RubricAxis = { key: string; weight: number };
9
+
10
+ export const DEFAULT_PROJECT_AXES: readonly RubricAxis[] = [
11
+ { key: 'V', weight: 2 }, { key: 'U', weight: 2 },
12
+ { key: 'R', weight: 2 }, { key: 'S', weight: -0.5 },
13
+ ];
14
+
15
+ const INTERNAL_AXES: readonly RubricAxis[] = [
16
+ { key: 'A', weight: 2 }, { key: 'E', weight: 2 },
17
+ { key: 'R', weight: 2 }, { key: 'D', weight: 1.5 },
18
+ { key: 'M', weight: 1 }, { key: 'B', weight: 1 },
19
+ { key: 'S', weight: -0.5 }, { key: 'X', weight: -0.5 },
20
+ ];
21
+
22
+ export function rubricScoreWith(axes: readonly RubricAxis[], values: Readonly<{ [key: string]: number } | Rubric>): number {
23
+ const scores: Readonly<Record<string, number>> = values as Readonly<Record<string, number>>;
24
+ return axes.reduce((score, { key, weight }) => {
25
+ const value = Object.hasOwn(scores, key) ? scores[key] : undefined;
26
+ if (typeof value !== 'number' || !Number.isFinite(value)) throw new Error(`루브릭 축 ${key} 값이 없다`);
27
+ if (typeof weight !== 'number' || !Number.isFinite(weight)) throw new Error(`루브릭 축 ${key} 가중이 유한수가 아니다`);
28
+ return score + weight * value;
29
+ }, 0);
30
+ }
31
+
32
+ /**
33
+ * RFC §A4b⑤ — unconfigured projects (and the internal pack `elanous`) keep the internal eight axes;
34
+ * a project with `projects.<id>.rubric.axes` starts from V/U/R/S and replaces matching keys or appends new ones.
35
+ * An invalid axes entry raises its config error text here (loading the config itself stays non-fatal).
36
+ */
37
+ export function resolveRubricAxes(projectId?: string): RubricAxis[] {
38
+ const projects = projectId && projectId !== 'elanous' ? getUserConfig().projects : undefined;
39
+ const rubric = projects && Object.hasOwn(projects, projectId!) ? projects[projectId!]?.rubric : undefined;
40
+ if (rubric?.error) {
41
+ debug.log('release.rubric', 'axes-invalid', { project: projectId, error: rubric.error });
42
+ throw new Error(rubric.error);
43
+ }
44
+ const configured = rubric?.axes;
45
+ const axes = (configured ? DEFAULT_PROJECT_AXES : INTERNAL_AXES).map((axis) => ({ ...axis }));
46
+ for (const axis of configured ?? []) {
47
+ const index = axes.findIndex((current) => current.key === axis.key);
48
+ if (index < 0) axes.push({ ...axis });
49
+ else axes[index] = { ...axis };
50
+ }
51
+ // An omitted project id is logged as null (unspecified) — not as `elanous` — even though it resolves to the internal axes.
52
+ debug.log('release.rubric', 'axes', { project: projectId ?? null, keys: axes.map(({ key }) => key) });
53
+ return axes;
54
+ }
5
55
 
6
56
  export interface Rubric {
7
57
  A: number;
@@ -33,7 +83,7 @@ export function parseRubric(text: string): Rubric | null {
33
83
  }
34
84
 
35
85
  export function rubricScore(r: Rubric): number {
36
- return 2 * r.A + 2 * r.E + 2 * r.R + 1.5 * r.D + r.M + r.B - 0.5 * r.S - 0.5 * r.X;
86
+ return rubricScoreWith(INTERNAL_AXES, r);
37
87
  }
38
88
 
39
89
  export function rubricGrade(score: number): 'P1' | 'P2' | 'P3' | 'P4' {
@@ -43,6 +93,12 @@ export function rubricGrade(score: number): 'P1' | 'P2' | 'P3' | 'P4' {
43
93
  return 'P4';
44
94
  }
45
95
 
96
+ /** The rule-only split uses the same numeric rubric as the release checklist. */
97
+ export function rubricPriority(text: string): ReturnType<typeof rubricGrade> {
98
+ const rubric = parseRubric(text);
99
+ return rubric ? rubricGrade(rubricScore(rubric)) : 'P2';
100
+ }
101
+
46
102
  /** Inspect an existing checklist without initializing, importing, or migrating its ledger. */
47
103
  export function readRubricItems(version: string, ledgerRoot: string): Pick<ChecklistItem, 'id' | 'title' | 'evidence' | 'priority'>[] {
48
104
  const dbPath = join(ledgerRoot, 'release', 'features.sqlite');
@@ -0,0 +1,372 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * RETRO-DAILY — 매일 회고 그래프(`graphs/retro/daily.yaml`)의 노드 본체.
4
+ *
5
+ * collect → cluster → recur → insight → act → report
6
+ *
7
+ * - 사실은 `elanous retro facts --since 24h --json`(RETRO-FACTS) 에서만 온다. 글은 읽지 않는다.
8
+ * - 반복(지난 7일 회고 파일)과 «초록으로 닫은 뒤 다시 깨진 것»을 1순위로 올린다.
9
+ * - 조치는 최대 셋, 칸 «초안 글»뿐이다 — 체크리스트에 쓰지 않는다(그림자).
10
+ * - `retro.daily.mode` = shadow(기본) | live. shadow 는 체크리스트·메시지·브리핑 어느 것도 건드리지 않는다.
11
+ * live 는 day 파일에 `briefing: true` 를 찍어 08:30 데일리 리뷰가 세 줄을 싣게 한다(체크리스트 쓰기는 아직 없다).
12
+ *
13
+ * RFC: 내부 문서 `RFC-retro-loop-daily-weekly-insight-2026-10-07` §1 (§0 표가 첫 판의 정답지).
14
+ */
15
+ import { createHash } from 'node:crypto';
16
+ import { spawnSync } from 'node:child_process';
17
+ import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
18
+ import { dirname, join, resolve } from 'node:path';
19
+ import { effectiveInstanceRoot, prodInstanceRoot } from '../instance/resolve.js';
20
+ import { getUserConfig } from '../user-config.js';
21
+ import { debug } from '../debug/log.js';
22
+
23
+ export const RETRO_DAILY_GRAPH_ID = 'retro-daily';
24
+ export const RETRO_MAX_ACTIONS = 3;
25
+ export const RETRO_LOOKBACK_DAYS = 7;
26
+ const MAX_FACTS = 2000;
27
+
28
+ export type RetroMode = 'shadow' | 'live';
29
+ export type RetroStage = 'collect' | 'cluster' | 'recur' | 'insight' | 'act' | 'report';
30
+ export const RETRO_STAGES: readonly RetroStage[] = ['collect', 'cluster', 'recur', 'insight', 'act', 'report'];
31
+
32
+ export type RetroFactIn = { store?: string; at: string; category?: string; event?: string; kind: string; data?: unknown };
33
+ export type RetroFact = RetroFactIn & { id: string; signature: string; text: string };
34
+ /** `greenAt` = 그 칸이 마지막으로 green/done 이 된 시각(체크리스트 history). 모르면 비운다 — «모름»은 «재발 아님»으로 읽지 않는다. */
35
+ export type GreenCell = { id: string; title?: string; status: string; version?: string; greenAt?: string };
36
+
37
+ export type RetroTheme = {
38
+ id: string;
39
+ title: string;
40
+ owner: string;
41
+ /** 사실의 서명·본문에 맞는 낱말. 순서대로 처음 맞는 것이 이긴다. */
42
+ pattern: RegExp;
43
+ /** RFC §0 의 «한 단 위 원인» 틀. 사실이 이 틀을 «증명»하지는 않는다 — insight 가 사실 id 로 근거를 단다. */
44
+ rootCause: string;
45
+ falsifier: string;
46
+ };
47
+
48
+ /**
49
+ * RFC §0 다섯 반복 문제의 서명 사전. 순서가 의미를 갖는다(조용한 실패가 draft 낱말을 같이 가져도 «조용한 실패»다).
50
+ * 새 반복이 생기면 여기에 줄을 더한다 — 사전에 없는 서명은 `sig:<서명>` 주제로 그대로 남는다(버리지 않는다).
51
+ */
52
+ export const RETRO_THEMES: readonly RetroTheme[] = [
53
+ { id: 'silent-failure', title: '조용한 실패', owner: 'TC',
54
+ pattern: /silent|조용|send-disabled|deliveryState|delivery-state|truncat|절단|no[-_ ]?alert|not[-_ ]?sent|\/tmp|log[-_ ]?only|repeated_failure/i,
55
+ rootCause: '«돌았다»를 «일했다»로 읽는다 — 실패가 알림으로 가지 않는다',
56
+ falsifier: '루프 failed·deliveryState≠sent 가 알림 없이 24h 넘게 이어진 수' },
57
+ { id: 'universe-confusion', title: '우주 혼동', owner: 'TC',
58
+ pattern: /universe|우주|test[-_ ]?instance|config-dir|empty[-_ ]?value|빈 값/i,
59
+ rootCause: '자리 트리 = test 우주라는 기본값이 사람·에이전트 직관과 반대다',
60
+ falsifier: 'test 우주를 읽어 «빈 값»으로 오판한 수/일' },
61
+ { id: 'serialization', title: '직렬화 재발', owner: 'OP',
62
+ pattern: /serial|직렬|overlap|겹침|hotPaths|hot-path|order[-_ ]?edge/i,
63
+ rootCause: '«병렬 우선»(R-RUN17)이 규칙에는 있는데 새 도구를 만들 때 검사하지 않는다',
64
+ falsifier: '겹침을 이유로 뺀 칸·순서 간선 수/일' },
65
+ { id: 'unreadable-stop', title: '멈춤을 읽을 수 없음', owner: 'TC',
66
+ pattern: /unreadable|unknown|읽을 수 없|non[-_ ]?convergen|unconvergeable|비수렴|no[-_ ]?reason/i,
67
+ rootCause: '멈춤이 사람이 읽는 글로만 남는다 — 기계가 다음 수를 고를 어휘가 없다',
68
+ falsifier: '멈춤 사유를 읽을 수 없는 draft 비율' },
69
+ { id: 'draft-backlog', title: 'draft 적체', owner: 'OP',
70
+ pattern: /draft|rework|supersede|적체|DRAFT/i,
71
+ rootCause: '고친 것을 «초록»으로 닫고 효과를 다시 재지 않았다',
72
+ falsifier: '열린 draft 수(7일 이동평균)' },
73
+ ];
74
+
75
+ // ── 작은 도구 ──────────────────────────────────────────────────────────
76
+ const record = (value: unknown): Record<string, unknown> => value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : {};
77
+ const str = (value: unknown): string => typeof value === 'string' ? value : '';
78
+ export const kstDay = (date: Date): string => new Date(date.getTime() + 9 * 3_600_000).toISOString().slice(0, 10);
79
+ const norm = (value: string): string => value.trim().toLowerCase().replace(/\s+/g, '-').replace(/[^\p{L}\p{N}._:/-]+/gu, '').slice(0, 80) || 'none';
80
+
81
+ export function retroDailyDir(root: string): string { return join(root, 'retro', 'daily'); }
82
+ export function retroDayFile(root: string, day: string): string { return join(retroDailyDir(root), `${day}.json`); }
83
+
84
+ export function factId(fact: RetroFactIn): string {
85
+ return `F-${createHash('sha1').update(JSON.stringify([fact.store ?? '', fact.at, fact.category ?? '', fact.event ?? '', fact.kind, fact.data ?? null])).digest('hex').slice(0, 8)}`;
86
+ }
87
+
88
+ /** 서명 = 묶음 열쇠. 실패 테스트 > 멈춤 사유 > 카드 종류 순으로 고른다(RFC §1 cluster). */
89
+ export function factSignature(fact: RetroFactIn): string {
90
+ const data = record(fact.data);
91
+ const test = str(data.failingTest) || str(data.test) || (Array.isArray(data.failingTests) ? str(data.failingTests[0]) : '');
92
+ if (test) return `test:${norm(test)}`;
93
+ if (fact.kind === 'draft-birth' || fact.kind === 'draft-death') return fact.kind;
94
+ if (fact.kind === 'stop-reason') return `stop:${norm(str(data.stopReason) || 'unknown')}`;
95
+ if (fact.kind === 'ta-outcome') {
96
+ const result = str(data.result);
97
+ // 결과를 모르면 ok 로 접지 않는다 — 판정할 낱말이 없으면 «no-result» 로 남긴다.
98
+ const verdict = !result.trim() ? 'no-result' : /fail|error|실패|abandon|stuck|멈춤/i.test(result) ? 'failed'
99
+ : /\b(ok|done|success|merged|landed|passed|updated|completed)\b|완료|성공|착지/i.test(result) ? 'ok' : 'unjudged';
100
+ return `card:${norm(str(data.cardKind) || str(data.card) || 'task-agent')}:${verdict}`;
101
+ }
102
+ return `${norm(fact.kind)}:${norm(str(data.signature) || str(data.cardKind) || str(data.reason) || str(data.stopReason) || '')}`;
103
+ }
104
+
105
+ function factText(fact: RetroFactIn): string {
106
+ const data = record(fact.data);
107
+ return [fact.category, fact.event, str(data.stopReason), str(data.result), str(data.reason), str(data.summary), str(data.title), str(data.cardKind)]
108
+ .filter(Boolean).join(' · ').slice(0, 300);
109
+ }
110
+
111
+ export function themeOf(signature: string, texts: readonly string[]): RetroTheme | undefined {
112
+ if (signature === 'draft-birth' || signature === 'draft-death') return RETRO_THEMES.find(t => t.id === 'draft-backlog');
113
+ // 서명이 먼저 말하면 서명을 믿는다 — 본문은 서명이 침묵할 때만 본다.
114
+ return RETRO_THEMES.find(t => t.pattern.test(signature)) ?? RETRO_THEMES.find(t => texts.some(text => t.pattern.test(text)));
115
+ }
116
+
117
+ // ── 단계 순수 함수 ─────────────────────────────────────────────────────
118
+ export function normalizeFacts(raw: unknown): { facts: RetroFact[]; truncated: boolean } {
119
+ const list = Array.isArray(raw) ? raw : Array.isArray(record(raw).facts) ? record(raw).facts as unknown[] : [];
120
+ const facts = list.map(record).filter(f => typeof f.at === 'string' && typeof f.kind === 'string')
121
+ .map(f => {
122
+ const fact = f as unknown as RetroFactIn;
123
+ return { ...fact, id: factId(fact), signature: factSignature(fact), text: factText(fact) };
124
+ });
125
+ return { facts: facts.slice(0, MAX_FACTS), truncated: facts.length > MAX_FACTS };
126
+ }
127
+
128
+ export type Cluster = { signature: string; theme: string; themeTitle: string; count: number; factIds: string[]; lastAt: string; samples: string[] };
129
+
130
+ export function clusterFacts(facts: readonly RetroFact[]): Cluster[] {
131
+ const groups = new Map<string, RetroFact[]>();
132
+ for (const fact of facts) groups.set(fact.signature, [...(groups.get(fact.signature) ?? []), fact]);
133
+ return [...groups.entries()].map(([signature, members]) => {
134
+ const theme = themeOf(signature, members.map(m => m.text));
135
+ return { signature, theme: theme?.id ?? `sig:${signature}`, themeTitle: theme?.title ?? signature, count: members.length,
136
+ factIds: members.map(m => m.id), lastAt: members.map(m => m.at).sort().at(-1) ?? '', samples: [...new Set(members.map(m => m.text).filter(Boolean))].slice(0, 3) };
137
+ }).sort((a, b) => b.count - a.count || a.signature.localeCompare(b.signature));
138
+ }
139
+
140
+ export type PriorDay = { day: string; themes: string[]; signatures: string[]; actedThemes: string[] };
141
+ export type Recurrence = {
142
+ theme: string; title: string; count: number; signatures: string[]; factIds: string[];
143
+ daysSeen: string[]; actedBefore: string[]; greenCells: string[];
144
+ /** 초록(green/done) 칸이 «사실보다 먼저» 있었는데 다시 나왔다. 지난 조치 «초안»은 초록이 아니다(→ recurAfterAction). */
145
+ reBrokeAfterGreen: boolean;
146
+ /** 지난 회고가 조치 초안을 냈는데 다시 나왔다. 1순위 다음. */
147
+ recurAfterAction: boolean; recurring: boolean; rank: number;
148
+ };
149
+
150
+ export function readPriorDays(root: string, day: string, lookback = RETRO_LOOKBACK_DAYS): PriorDay[] {
151
+ const dir = retroDailyDir(root);
152
+ if (!existsSync(dir)) return [];
153
+ return readdirSync(dir).filter(name => /^\d{4}-\d{2}-\d{2}\.json$/.test(name) && name.slice(0, 10) < day).sort().slice(-lookback)
154
+ .flatMap((name) => {
155
+ try {
156
+ const file = record(JSON.parse(readFileSync(join(dir, name), 'utf8')));
157
+ return [{ day: name.slice(0, 10),
158
+ themes: Array.isArray(file.themes) ? file.themes.map(t => str(record(t).theme)).filter(Boolean) : [],
159
+ signatures: Array.isArray(file.clusters) ? file.clusters.map(c => str(record(c).signature)).filter(Boolean) : [],
160
+ actedThemes: Array.isArray(file.actions) ? file.actions.map(a => str(record(a).theme)).filter(Boolean) : [] }];
161
+ } catch { return []; }
162
+ });
163
+ }
164
+
165
+ /** RFC §1 recur — ① 지난 회고 ② 이미 green/done 인 칸 ③ 지난 조치. «초록 뒤 재발»이 1순위다. */
166
+ export function findRecurrences(clusters: readonly Cluster[], prior: readonly PriorDay[], greenCells: readonly GreenCell[]): Recurrence[] {
167
+ const byTheme = new Map<string, Cluster[]>();
168
+ for (const cluster of clusters) byTheme.set(cluster.theme, [...(byTheme.get(cluster.theme) ?? []), cluster]);
169
+ const out = [...byTheme.entries()].map(([theme, members]) => {
170
+ const def = RETRO_THEMES.find(t => t.id === theme);
171
+ const signatures = members.map(m => m.signature);
172
+ const count = members.reduce((sum, m) => sum + m.count, 0);
173
+ const daysSeen = prior.filter(p => p.themes.includes(theme) || p.signatures.some(s => signatures.includes(s))).map(p => p.day);
174
+ const actedBefore = prior.filter(p => p.actedThemes.includes(theme)).map(p => p.day);
175
+ const lastAt = members.map(m => m.lastAt).sort().at(-1) ?? '';
176
+ // 초록이 된 시각이 그 주제의 마지막 사실보다 «앞»이어야 «초록 뒤 재발»이다. 시각을 모르면(greenAt 없음) 후보로 둔다.
177
+ const green = def ? greenCells.filter(c => (c.status === 'green' || c.status === 'done') && def.pattern.test(`${c.id} ${c.title ?? ''}`)
178
+ && (!c.greenAt || !lastAt || Date.parse(c.greenAt) < Date.parse(lastAt)))
179
+ .map(c => `${c.id}${c.version ? `@${c.version}` : ''}${c.greenAt ? '' : '(시각 모름)'}`) : [];
180
+ const reBrokeAfterGreen = green.length > 0;
181
+ const recurAfterAction = actedBefore.length > 0;
182
+ const recurring = reBrokeAfterGreen || recurAfterAction || daysSeen.length > 0 || count >= 2;
183
+ return { theme, title: def?.title ?? members[0]!.themeTitle, count, signatures, factIds: members.flatMap(m => m.factIds),
184
+ daysSeen, actedBefore, greenCells: green, reBrokeAfterGreen, recurAfterAction, recurring, rank: 0 };
185
+ }).sort((a, b) => Number(b.reBrokeAfterGreen) - Number(a.reBrokeAfterGreen) || Number(b.recurAfterAction) - Number(a.recurAfterAction) || Number(b.recurring) - Number(a.recurring)
186
+ || b.daysSeen.length - a.daysSeen.length || b.count - a.count || a.theme.localeCompare(b.theme));
187
+ return out.map((r, i) => ({ ...r, rank: i + 1 }));
188
+ }
189
+
190
+ export type Insight = { theme: string; title: string; text: string; rootCause: string; cites: string[] };
191
+
192
+ /** 결정적 insight — 사실 id 를 반드시 단다. (LLM 노드로 바꿀 때도 `cites` 검사는 그대로 남긴다.) */
193
+ export function buildInsights(recurrences: readonly Recurrence[]): Insight[] {
194
+ return recurrences.filter(r => r.recurring).map((r) => {
195
+ const def = RETRO_THEMES.find(t => t.id === r.theme);
196
+ const cites = r.factIds.slice(0, 5);
197
+ const why = [
198
+ r.greenCells.length ? `초록으로 닫은 칸(${r.greenCells.join(', ')}) 뒤 다시 나왔다` : '',
199
+ r.actedBefore.length ? `지난 회고 조치(${r.actedBefore.join(', ')}) 뒤에도 남았다` : '',
200
+ r.daysSeen.length ? `지난 ${r.daysSeen.length}일 회고에도 있었다` : '',
201
+ ].filter(Boolean).join(' · ');
202
+ const rootCause = def?.rootCause ?? `서명 ${r.signatures.join(', ')} 이 반복된다 — 사전에 없는 반복이라 원인 틀이 아직 없다`;
203
+ return { theme: r.theme, title: r.title, rootCause, cites,
204
+ text: `${r.title} ${r.count}건 [${cites.join(', ')}]${why ? ` — ${why}` : ''} ⇒ ${rootCause}` };
205
+ });
206
+ }
207
+
208
+ export function assertInsightsCite(insights: readonly Insight[], facts: readonly { id: string }[]): void {
209
+ const known = new Set(facts.map(f => f.id));
210
+ for (const insight of insights) {
211
+ if (!insight.cites.length || insight.cites.some(id => !known.has(id))) throw new Error(`insight ${insight.theme} cites unknown or no fact ids`);
212
+ }
213
+ }
214
+
215
+ export type DraftCell = { id: string; theme: string; title: string; owner: string; verdictLine: string; falsifier: string; evidence: string[]; status: 'draft' };
216
+
217
+ export function draftActions(insights: readonly Insight[], recurrences: readonly Recurrence[], day: string): DraftCell[] {
218
+ return insights.slice(0, RETRO_MAX_ACTIONS).map((insight) => {
219
+ const def = RETRO_THEMES.find(t => t.id === insight.theme);
220
+ const rec = recurrences.find(r => r.theme === insight.theme)!;
221
+ const slug = insight.theme.replace(/^sig:/, '').replace(/[^a-z0-9]+/gi, '-').toUpperCase().slice(0, 32);
222
+ return { id: `RETRO-${slug}-${day.replace(/-/g, '')}`, theme: insight.theme, status: 'draft' as const,
223
+ title: `${insight.title} 한 단 위 수리 — ${insight.rootCause}`, owner: def?.owner ?? 'OP',
224
+ verdictLine: rec.reBrokeAfterGreen || rec.recurAfterAction ? '다음 7일 회고에서 이 주제가 다시 나오지 않는다' : '다음 회고에서 이 주제의 건수가 줄어든다',
225
+ falsifier: def?.falsifier ?? `서명 ${rec.signatures.join(', ')} 건수/일`, evidence: insight.cites };
226
+ });
227
+ }
228
+
229
+ export function summaryLines(recurrences: readonly Recurrence[], actions: readonly DraftCell[], factCount: number, mode: RetroMode): string[] {
230
+ const top = recurrences.filter(r => r.recurring);
231
+ const first = top[0];
232
+ return [
233
+ first ? `회고 1순위: ${first.title} ${first.count}건${first.reBrokeAfterGreen ? ' · 초록 뒤 재발' : first.recurAfterAction ? ' · 조치 뒤 재발' : first.daysSeen.length ? ` · ${first.daysSeen.length}일째` : ''}` : '회고: 반복 없음',
234
+ `사실 ${factCount} · 반복 주제 ${top.length}(${top.slice(0, 5).map(r => r.title).join(' · ') || '없음'})`,
235
+ `조치 초안 ${actions.length}: ${actions.map(a => `${a.id}(${a.owner})`).join(', ') || '없음'} · ${mode}`,
236
+ ];
237
+ }
238
+
239
+ // ── 모드 ───────────────────────────────────────────────────────────────
240
+ export function resolveRetroMode(input: Record<string, unknown>, readConfig: () => unknown = () => getUserConfig().raw): RetroMode {
241
+ // 입력은 «더 안전한 쪽»으로만 바꿀 수 있다(shadow 강제). live 는 config 에서만 온다.
242
+ if (input.mode === 'shadow') return 'shadow';
243
+ try { return record(record(record(readConfig()).retro).daily).mode === 'live' ? 'live' : 'shadow'; } catch { return 'shadow'; }
244
+ }
245
+
246
+ // ── 노드 실행 ─────────────────────────────────────────────────────────
247
+ type Ctx = { graphId?: string; nodeId?: string; input: Record<string, unknown> | null; outputs: Record<string, Record<string, unknown> | null> };
248
+ export type RetroDeps = { now?: Date; readFacts?: (since: string, root: string) => unknown; readGreenCells?: () => GreenCell[]; readConfig?: () => unknown; repo?: string };
249
+
250
+ const repoRoot = resolve(import.meta.dir, '..', '..');
251
+
252
+ function readFactsViaCli(since: string, root: string, repo = repoRoot): unknown {
253
+ const args = ['bin/elanous.mjs', ...(root === prodInstanceRoot() ? [] : [`--test=${root}`]), 'retro', 'facts', '--since', since, '--json'];
254
+ const result = spawnSync('bun', args, { cwd: repo, encoding: 'utf8', timeout: 570_000, maxBuffer: 64 * 1024 * 1024 });
255
+ if (result.error || result.status !== 0) throw new Error(`retro facts failed: ${result.error?.message ?? result.stderr?.trim() ?? `exit ${result.status}`}`);
256
+ return JSON.parse(result.stdout);
257
+ }
258
+
259
+ /** 최근 판 열 개의 체크리스트에서 green/done 칸을 모은다(예: DRAFT3 0.2.12 · DRAFT-TRIAGE 0.2.18). 못 읽은 판은 건너뛴다. */
260
+ async function readGreenCellsFromChecklist(span = 10): Promise<GreenCell[]> {
261
+ const { listChecklist, checklistDevVersion } = await import('../release-loop/checklist.js');
262
+ const match = /^(\d+)\.(\d+)\.(\d+)/.exec(checklistDevVersion());
263
+ if (!match) throw new Error(`unparseable version: ${checklistDevVersion()}`);
264
+ const cells: GreenCell[] = [];
265
+ for (let patch = Number(match[3]); patch >= Math.max(0, Number(match[3]) - span); patch--) {
266
+ const version = `${match[1]}.${match[2]}.${patch}`;
267
+ try {
268
+ // 체크리스트는 릴리스 원장(releaseLedgerRoot — 기본 prod)에 있다. 회고 런의 우주가 아니라 원장 우주를 읽는다(쓰기 없음).
269
+ const list = listChecklist(version);
270
+ const greenAt = (id: string) => list.history.filter(h => h.id === id && h.field === 'status' && (h.to === 'green' || h.to === 'done')).map(h => h.at).sort().at(-1);
271
+ cells.push(...list.items.filter(i => i.status === 'green' || i.status === 'done').map(i => ({ id: i.id, title: i.title, status: i.status, version, greenAt: greenAt(i.id) })));
272
+ }
273
+ catch { /* 그 판의 체크리스트가 없다 */ }
274
+ }
275
+ return cells;
276
+ }
277
+
278
+ function writeAtomic(file: string, text: string): void {
279
+ mkdirSync(dirname(file), { recursive: true });
280
+ const tmp = `${file}.${process.pid}.tmp`;
281
+ writeFileSync(tmp, text);
282
+ renameSync(tmp, file);
283
+ }
284
+
285
+ export async function runRetroStage(stage: RetroStage, ctx: Ctx, root: string, dryRun: boolean, deps: RetroDeps = {}): Promise<Record<string, unknown>> {
286
+ const input = ctx.input ?? {};
287
+ const now = deps.now ?? new Date();
288
+ const out = ctx.outputs;
289
+ const day = str(out.collect?.day) || str(input.day) || kstDay(now);
290
+ switch (stage) {
291
+ case 'collect': {
292
+ const since = str(input.since) || '24h';
293
+ const sources: Record<string, string> = {};
294
+ let raw: unknown;
295
+ if (typeof input.factsFile === 'string') { raw = JSON.parse(readFileSync(input.factsFile, 'utf8')); sources.facts = `file:${input.factsFile}`; }
296
+ else if (Array.isArray(input.facts)) { raw = input.facts; sources.facts = 'input'; }
297
+ else { raw = (deps.readFacts ?? ((s, r) => readFactsViaCli(s, r, deps.repo)))(since, root); sources.facts = `retro facts --since ${since}`; }
298
+ const { facts, truncated } = normalizeFacts(raw);
299
+ let greenCells: GreenCell[] = [];
300
+ if (Array.isArray(input.greenCells)) { greenCells = input.greenCells.map(record).map(c => ({ id: str(c.id), title: str(c.title), status: str(c.status), version: str(c.version) || undefined, greenAt: str(c.greenAt) || undefined })); sources.green = 'input'; }
301
+ else {
302
+ try { greenCells = (deps.readGreenCells ? deps.readGreenCells() : await readGreenCellsFromChecklist()).map(c => ({ ...c, title: (c.title ?? "").slice(0, 120) })); sources.green = 'checklist'; }
303
+ catch (error) { sources.green = `unreadable: ${error instanceof Error ? error.message.split('\n')[0] : String(error)}`; }
304
+ }
305
+ const unavailable = Array.isArray(record(raw).unavailable) ? record(raw).unavailable : [];
306
+ return { outcome: 'ok', day, since, mode: resolveRetroMode(input, deps.readConfig), facts, factCount: facts.length, truncated, unavailable, greenCells, sources };
307
+ }
308
+ case 'cluster': {
309
+ const clusters = clusterFacts((out.collect?.facts ?? []) as RetroFact[]);
310
+ return { outcome: 'ok', clusters };
311
+ }
312
+ case 'recur': {
313
+ const prior = readPriorDays(root, day);
314
+ const recurrences = findRecurrences((out.cluster?.clusters ?? []) as Cluster[], prior, (out.collect?.greenCells ?? []) as GreenCell[]);
315
+ return { outcome: 'ok', priorDays: prior.map(p => p.day), recurrences };
316
+ }
317
+ case 'insight': {
318
+ const insights = buildInsights((out.recur?.recurrences ?? []) as Recurrence[]);
319
+ assertInsightsCite(insights, (out.collect?.facts ?? []) as RetroFact[]);
320
+ return { outcome: 'ok', insights };
321
+ }
322
+ case 'act': {
323
+ // 그림자든 live 든 이 노드는 «글»만 낸다 — `checklist add` 를 부르지 않는다(RETRO-DAILY 첫 조각).
324
+ const actions = draftActions((out.insight?.insights ?? []) as Insight[], (out.recur?.recurrences ?? []) as Recurrence[], day);
325
+ return { outcome: 'ok', actions, wroteChecklist: false };
326
+ }
327
+ case 'report': {
328
+ const mode = (str(out.collect?.mode) || 'shadow') as RetroMode;
329
+ const facts = (out.collect?.facts ?? []) as RetroFact[];
330
+ const recurrences = (out.recur?.recurrences ?? []) as Recurrence[];
331
+ const actions = (out.act?.actions ?? []) as DraftCell[];
332
+ const summary = summaryLines(recurrences, actions, facts.length, mode);
333
+ const themes = recurrences.map(r => ({ theme: r.theme, title: r.title, count: r.count, recurring: r.recurring, reBrokeAfterGreen: r.reBrokeAfterGreen, recurAfterAction: r.recurAfterAction, greenCells: r.greenCells, rank: r.rank }));
334
+ const dayFile = { day, mode, briefing: mode === 'live', generatedAt: now.toISOString(), factCount: facts.length,
335
+ truncated: out.collect?.truncated === true, unavailable: out.collect?.unavailable ?? [], sources: out.collect?.sources ?? {},
336
+ priorDays: out.recur?.priorDays ?? [], clusters: ((out.cluster?.clusters ?? []) as Cluster[]).map(({ samples: _s, ...c }) => c),
337
+ themes, insights: out.insight?.insights ?? [], actions, summary };
338
+ const file = retroDayFile(root, day);
339
+ const md = [`# 매일 회고 ${day} (${mode})`, '', ...summary.map(l => `- ${l}`), '', '## 반복 (순위)',
340
+ ...recurrences.filter(r => r.recurring).map(r => `${r.rank}. ${r.title} — ${r.count}건${r.reBrokeAfterGreen ? ' · 초록 뒤 재발' : r.recurAfterAction ? ' · 조치 뒤 재발' : ''}${r.daysSeen.length ? ` · ${r.daysSeen.length}일째` : ''} · ${r.signatures.join(', ')}`),
341
+ '', '## 인사이트', ...((out.insight?.insights ?? []) as Insight[]).map(i => `- ${i.text}`),
342
+ '', '## 조치 초안 (그림자 — 체크리스트에 쓰지 않았다)', ...actions.map(a => `- ${a.id} · ${a.owner} · ${a.title} · 판정선: ${a.verdictLine} · 반증: ${a.falsifier} · 근거: ${a.evidence.join(', ')}`), ''].join('\n');
343
+ if (!dryRun) {
344
+ writeAtomic(file, JSON.stringify(dayFile, null, 2) + '\n');
345
+ writeAtomic(file.replace(/\.json$/, '.md'), md);
346
+ }
347
+ debug.log('retro.daily', 'report', { day, mode, dryRun, facts: facts.length, recurring: themes.filter(t => t.recurring).length, actions: actions.length });
348
+ return { outcome: 'ok', day, mode, dryRun, file: dryRun ? null : file, summary, actions: actions.length, sent: false, wroteChecklist: false };
349
+ }
350
+ }
351
+ }
352
+
353
+ /** 08:30 데일리 리뷰가 싣는 세 줄 — live 로 쓴 회고 파일만 읽는다(shadow 는 브리핑에 안 나간다). */
354
+ export function retroBriefingLines(root: string, day: string): string[] {
355
+ try {
356
+ const file = record(JSON.parse(readFileSync(retroDayFile(root, day), 'utf8')));
357
+ if (file.briefing !== true || !Array.isArray(file.summary)) return [];
358
+ return file.summary.map(str).filter(Boolean).slice(0, 3);
359
+ } catch { return []; }
360
+ }
361
+
362
+ if (import.meta.main) {
363
+ const stage = process.argv[2] as RetroStage;
364
+ const path = process.env.ELANOUS_GRAPH_CONTEXT;
365
+ if (!path || !RETRO_STAGES.includes(stage)) throw new Error('invalid retro-daily graph stage');
366
+ const ctx = JSON.parse(readFileSync(path, 'utf8')) as Ctx;
367
+ if (ctx.graphId !== RETRO_DAILY_GRAPH_ID || ctx.nodeId !== stage) throw new Error('retro-daily graph context mismatch');
368
+ // <root>/graph-runs/<graphId>/<runId>.json.contexts/<n>.json — 런 원장의 우주가 곧 회고 파일의 우주다.
369
+ const statePath = resolve(path, '..', '..', '..', '..');
370
+ const root = statePath || effectiveInstanceRoot();
371
+ console.log(JSON.stringify(await runRetroStage(stage, ctx, root, process.env.ELANOUS_GRAPH_DRY_RUN === '1')));
372
+ }