elanous 0.2.14 → 0.2.15

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 (293) 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/LZcstaO9_NXypPFmujolv/_buildManifest.js +1 -0
  4. package/apps/pwa/out/_next/static/chunks/{1307-6e7e0377c1db0bdc.js → 1307-33ffcc49ab45123b.js} +1 -1
  5. package/apps/pwa/out/_next/static/chunks/5977-c952712c87a1f757.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/899-46946fe8363e1262.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/app/approvals/{page-74383f890f075511.js → page-0e2c57fe6add4b2a.js} +1 -1
  8. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-7b2b4f378354869a.js → page-e05a77df7e005b04.js} +1 -1
  9. package/apps/pwa/out/_next/static/chunks/app/board/page-f544338193a9a762.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/app/botlab/page-5491c115b1f9a923.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/app/bots/{page-1512d91c7491ed62.js → page-b8c49f6b4fa2df91.js} +1 -1
  12. package/apps/pwa/out/_next/static/chunks/app/ceo/page-96398e364fbd4548.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/app/chat/{page-a627db94ec6904a0.js → page-976813c91d7394ea.js} +1 -1
  14. package/apps/pwa/out/_next/static/chunks/app/consult/{page-fb260b9283dbf646.js → page-28b9448091776c06.js} +1 -1
  15. package/apps/pwa/out/_next/static/chunks/app/control/page-1d4901eb7c2d8822.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/app/design-check/page-72d3e7ff35a8c954.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/app/editor/{page-fed69f9759d248d0.js → page-60c7a8e1a4fe62ce.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/exec/{page-15374909d8f1aebd.js → page-2a52b316cb143d9c.js} +1 -1
  19. package/apps/pwa/out/_next/static/chunks/app/field/{page-012a742f81218b01.js → page-c3ce483c8d5db370.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/inside/page-13c7b53e0c2519a9.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/app/intake/{page-99effd9d893a43dc.js → page-96f074f6538e8a2c.js} +1 -1
  22. package/apps/pwa/out/_next/static/chunks/app/{layout-938de57ae0315e46.js → layout-e97be5334d83a9f5.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/app/live/page-65e484302921a932.js +1 -0
  24. package/apps/pwa/out/_next/static/chunks/app/loops/page-34d08facaf08f556.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/app/market/{page-629e1ede482ec6eb.js → page-955ec2da3be4c560.js} +1 -1
  26. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-9f88fbc15d9058d1.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/app/morning/{page-27e99b4e54a849d4.js → page-c1343f9499d86f3f.js} +1 -1
  28. package/apps/pwa/out/_next/static/chunks/app/not-found-8739a2e0ae793130.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/app/observatory/page-c9fc5706b8abaa11.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-32cde713be47cddb.js → page-d23b12bb92242558.js} +1 -1
  31. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-8d47f45a0dc044ac.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-c7fea475488201a8.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/app/outputs/{page-04250f0e2c0d03df.js → page-e2ee2896811e98d0.js} +1 -1
  34. package/apps/pwa/out/_next/static/chunks/app/page-83b19dae8619de05.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/app/reflection/{page-e64caff084f7ce21.js → page-5efbf81124c474ae.js} +1 -1
  36. package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-db07eaffba0df7e3.js → page-85f1a1fc0a82738c.js} +1 -1
  37. package/apps/pwa/out/_next/static/chunks/app/sessions/{page-8ac8d71c2adf9c8a.js → page-da6c08aae0f92768.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-06ee266953d9c879.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/app/settings/{page-86f91034bd5cabe5.js → page-153c2b3bcc89e8de.js} +1 -1
  40. package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-94dc383d3d54db1a.js → page-87a11e6a05f15199.js} +1 -1
  41. package/apps/pwa/out/_next/static/chunks/app/setup/{page-b3759cc14da80908.js → page-7d4182964407c09c.js} +1 -1
  42. package/apps/pwa/out/_next/static/chunks/app/share/{page-ce4d8a1215ce53a3.js → page-0d6d23b40263a7ea.js} +1 -1
  43. package/apps/pwa/out/_next/static/chunks/app/showroom/page-7e8c39241f8eb6d0.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/app/tasks/page-008c1a96618a4887.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/app/term/{page-274703bc41b71333.js → page-f5597e894c02cd33.js} +1 -1
  46. package/apps/pwa/out/_next/static/chunks/app/today/page-0337fdf7fb5e37b6.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/app/trace/{page-35bdeae2e2c17147.js → page-73735b64bfe1ae67.js} +1 -1
  48. package/apps/pwa/out/_next/static/chunks/app/vault/{page-3b7e821dc492bdfd.js → page-8fcec4c9ea2e7f70.js} +1 -1
  49. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-f7e631e9b4daf66e.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/workflows/page-4881b8f9f63694b4.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-2f810b18d89ecba6.js → page-9817c7f9007466cc.js} +1 -1
  52. package/apps/pwa/out/_next/static/chunks/app/worktrees/{page-134c1bc762aee62c.js → page-97f9f43172a3a90e.js} +1 -1
  53. package/apps/pwa/out/_next/static/css/1f7c958c41ffa3b9.css +1 -0
  54. package/apps/pwa/out/approvals/index.html +1 -1
  55. package/apps/pwa/out/approvals/index.txt +14 -14
  56. package/apps/pwa/out/autopilot/index.html +1 -1
  57. package/apps/pwa/out/autopilot/index.txt +14 -14
  58. package/apps/pwa/out/board/index.html +1 -1
  59. package/apps/pwa/out/board/index.txt +14 -14
  60. package/apps/pwa/out/botlab/index.html +1 -1
  61. package/apps/pwa/out/botlab/index.txt +14 -14
  62. package/apps/pwa/out/bots/index.html +1 -1
  63. package/apps/pwa/out/bots/index.txt +14 -14
  64. package/apps/pwa/out/ceo/index.html +1 -0
  65. package/apps/pwa/out/ceo/index.txt +33 -0
  66. package/apps/pwa/out/chat/index.html +1 -1
  67. package/apps/pwa/out/chat/index.txt +14 -14
  68. package/apps/pwa/out/consult/index.html +1 -1
  69. package/apps/pwa/out/consult/index.txt +14 -14
  70. package/apps/pwa/out/control/index.html +1 -1
  71. package/apps/pwa/out/control/index.txt +14 -14
  72. package/apps/pwa/out/design-check/index.html +1 -1
  73. package/apps/pwa/out/design-check/index.txt +14 -14
  74. package/apps/pwa/out/editor/index.html +1 -1
  75. package/apps/pwa/out/editor/index.txt +14 -14
  76. package/apps/pwa/out/exec/index.html +1 -1
  77. package/apps/pwa/out/exec/index.txt +14 -14
  78. package/apps/pwa/out/field/index.html +1 -1
  79. package/apps/pwa/out/field/index.txt +14 -14
  80. package/apps/pwa/out/healthz +1 -1
  81. package/apps/pwa/out/index.html +1 -1
  82. package/apps/pwa/out/index.txt +14 -14
  83. package/apps/pwa/out/inside/index.html +1 -1
  84. package/apps/pwa/out/inside/index.txt +14 -14
  85. package/apps/pwa/out/intake/index.html +1 -1
  86. package/apps/pwa/out/intake/index.txt +14 -14
  87. package/apps/pwa/out/live/index.html +1 -1
  88. package/apps/pwa/out/live/index.txt +14 -14
  89. package/apps/pwa/out/loops/index.html +1 -1
  90. package/apps/pwa/out/loops/index.txt +14 -14
  91. package/apps/pwa/out/market/index.html +1 -1
  92. package/apps/pwa/out/market/index.txt +14 -14
  93. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  94. package/apps/pwa/out/missions/placeholder/index.txt +14 -14
  95. package/apps/pwa/out/morning/index.html +1 -1
  96. package/apps/pwa/out/morning/index.txt +14 -14
  97. package/apps/pwa/out/observatory/index.html +1 -1
  98. package/apps/pwa/out/observatory/index.txt +14 -14
  99. package/apps/pwa/out/ops/checklist/index.html +1 -1
  100. package/apps/pwa/out/ops/checklist/index.txt +14 -14
  101. package/apps/pwa/out/ops/release/index.html +1 -1
  102. package/apps/pwa/out/ops/release/index.txt +14 -14
  103. package/apps/pwa/out/ops/seats/index.html +1 -1
  104. package/apps/pwa/out/ops/seats/index.txt +14 -14
  105. package/apps/pwa/out/outputs/index.html +1 -1
  106. package/apps/pwa/out/outputs/index.txt +14 -14
  107. package/apps/pwa/out/reflection/index.html +1 -1
  108. package/apps/pwa/out/reflection/index.txt +15 -15
  109. package/apps/pwa/out/scheduler/index.html +1 -1
  110. package/apps/pwa/out/scheduler/index.txt +14 -14
  111. package/apps/pwa/out/sessions/index.html +1 -1
  112. package/apps/pwa/out/sessions/index.txt +14 -14
  113. package/apps/pwa/out/settings/devices/index.html +1 -1
  114. package/apps/pwa/out/settings/devices/index.txt +14 -14
  115. package/apps/pwa/out/settings/index.html +1 -1
  116. package/apps/pwa/out/settings/index.txt +14 -14
  117. package/apps/pwa/out/setup/done/index.html +1 -1
  118. package/apps/pwa/out/setup/done/index.txt +14 -14
  119. package/apps/pwa/out/setup/index.html +1 -1
  120. package/apps/pwa/out/setup/index.txt +14 -14
  121. package/apps/pwa/out/share/index.html +1 -1
  122. package/apps/pwa/out/share/index.txt +14 -14
  123. package/apps/pwa/out/showroom/index.html +1 -1
  124. package/apps/pwa/out/showroom/index.txt +14 -14
  125. package/apps/pwa/out/tasks/index.html +1 -1
  126. package/apps/pwa/out/tasks/index.txt +14 -14
  127. package/apps/pwa/out/term/index.html +1 -1
  128. package/apps/pwa/out/term/index.txt +14 -14
  129. package/apps/pwa/out/today/index.html +1 -1
  130. package/apps/pwa/out/today/index.txt +14 -14
  131. package/apps/pwa/out/trace/index.html +1 -1
  132. package/apps/pwa/out/trace/index.txt +14 -14
  133. package/apps/pwa/out/vault/index.html +1 -1
  134. package/apps/pwa/out/vault/index.txt +14 -14
  135. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  136. package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
  137. package/apps/pwa/out/workflows/index.html +1 -1
  138. package/apps/pwa/out/workflows/index.txt +14 -14
  139. package/apps/pwa/out/workspace/index.html +1 -1
  140. package/apps/pwa/out/workspace/index.txt +14 -14
  141. package/apps/pwa/out/worktrees/index.html +1 -1
  142. package/apps/pwa/out/worktrees/index.txt +14 -14
  143. package/graphs/heal/heal-loop.yaml +1 -1
  144. package/graphs/implement-loop.yaml +1 -1
  145. package/graphs/release/recipes.yaml +3 -0
  146. package/graphs/release/release-loop.yaml +4 -2
  147. package/package.json +1 -1
  148. package/scripts/brand/check.ts +7 -5
  149. package/scripts/ci-typecheck-changed.ts +50 -18
  150. package/scripts/docs/generate-graph-anatomy.ts +315 -0
  151. package/scripts/expose-rubric-suggest.ts +18 -12
  152. package/scripts/lecture-note/node.ts +31 -11
  153. package/scripts/lessons/handbook-page.ts +30 -3
  154. package/scripts/marketing/generate-ax-brief.ts +140 -0
  155. package/scripts/marketing/generate-feature-map.ts +118 -0
  156. package/scripts/release-loop/checklist-gate-node.ts +53 -14
  157. package/scripts/release-loop/gate-node.ts +80 -14
  158. package/scripts/release-loop/vault-note-node.ts +240 -8
  159. package/scripts/release-story/draft.ts +57 -9
  160. package/scripts/rhythm/daily.ts +263 -76
  161. package/scripts/rhythm/weekly.ts +30 -9
  162. package/scripts/smart-merge/resync-shadow.ts +147 -0
  163. package/src/agent-env/profile.ts +161 -0
  164. package/src/agent-substrate/pr-reviewer.ts +2 -0
  165. package/src/ask-user-question/pending-questions.ts +5 -0
  166. package/src/auth/github-app-token.ts +112 -13
  167. package/src/autopilot/build/llm-conflict-merge.ts +32 -6
  168. package/src/briefing/brief-items.ts +121 -0
  169. package/src/claims/claims-render.ts +14 -5
  170. package/src/cli/agent-env-cli.ts +21 -0
  171. package/src/cli/decisions-cli.ts +9 -0
  172. package/src/cli/doctor-cli.ts +23 -1
  173. package/src/cli/doctor-node-modules.ts +147 -0
  174. package/src/cli/intake-cli.ts +46 -17
  175. package/src/cli/pod-cli.ts +2 -0
  176. package/src/cli/release-cli.ts +61 -27
  177. package/src/cli/seat-requests-cli.ts +52 -0
  178. package/src/context-bus/context-now-public.ts +16 -6
  179. package/src/context-bus/context-now.ts +3 -1
  180. package/src/context-bus/coord-events.ts +6 -4
  181. package/src/decisions/decision-cards.ts +33 -7
  182. package/src/decisions/proact-meter.ts +286 -0
  183. package/src/decisions/proact1-lite.ts +86 -0
  184. package/src/decisions/web-push-decision-cards.ts +1 -1
  185. package/src/domains/alert-audit.ts +271 -0
  186. package/src/domains/core-tools.ts +4 -3
  187. package/src/domains/ops-facts-tool.ts +31 -1
  188. package/src/harness/harness-cli-command.ts +111 -15
  189. package/src/harness/harness-incidents.ts +2 -0
  190. package/src/harness/harness-pod-dispatch.ts +100 -11
  191. package/src/harness/harness-queue.ts +380 -50
  192. package/src/harness/helper-repair.ts +87 -0
  193. package/src/harness/helper-scan.ts +16 -4
  194. package/src/harness/launch-stamp.ts +236 -0
  195. package/src/harness/ledger-write-guard-child.fixture.ts +12 -0
  196. package/src/harness/ledger-write-guard.ts +35 -0
  197. package/src/harness/pod-memory-policy.ts +15 -0
  198. package/src/harness/repository-name.ts +55 -0
  199. package/src/hq/fence-audit.ts +90 -0
  200. package/src/hq/hq.ts +62 -4
  201. package/src/hq/lease.ts +4 -4
  202. package/src/hq/move-plan.ts +126 -0
  203. package/src/hq/promote.ts +31 -0
  204. package/src/index.ts +77 -7
  205. package/src/intake-plane/check.ts +63 -0
  206. package/src/intake-plane/digest.ts +37 -15
  207. package/src/intake-plane/front-classifier.ts +6 -0
  208. package/src/intake-plane/items.ts +3 -2
  209. package/src/intake-plane/lens.ts +177 -0
  210. package/src/intake-plane/route.ts +145 -1
  211. package/src/launch-head/seat-goal-queue-lock.fixture.ts +15 -0
  212. package/src/launch-head/seat-goal-queue.ts +198 -0
  213. package/src/loops/budget.ts +36 -5
  214. package/src/loops/checker.ts +2 -2
  215. package/src/loops/orchestrator/author-depth.ts +65 -9
  216. package/src/loops/orchestrator/author-input-check.ts +8 -3
  217. package/src/loops/orchestrator/author-ledger.ts +3 -1
  218. package/src/loops/orchestrator/seat-share.ts +56 -0
  219. package/src/loops/orchestrator/tick.ts +11 -4
  220. package/src/loops/orchestrator/traffic.ts +117 -11
  221. package/src/loops/registry.ts +142 -61
  222. package/src/maturity/feature-maturity.ts +2 -1
  223. package/src/maturity/tui-slash-availability.ts +31 -0
  224. package/src/mss/logging/log-store.ts +35 -4
  225. package/src/nexus/api/http-server.ts +28 -14
  226. package/src/nexus/api/loop-edges.ts +173 -0
  227. package/src/nexus/api/schedules-read.ts +13 -0
  228. package/src/nexus/api/seat-requests.ts +79 -56
  229. package/src/prompt-enhance/enhance.ts +6 -1
  230. package/src/release-loop/auto-start.ts +6 -3
  231. package/src/release-loop/checklist.ts +13 -4
  232. package/src/release-loop/feature-store.ts +58 -31
  233. package/src/release-loop/merged-pr-checklist.ts +125 -0
  234. package/src/release-loop/placement.ts +86 -26
  235. package/src/seat-dispatch/seat-request-eligibility.ts +19 -0
  236. package/src/seat-dispatch/seat-request-ledger.ts +133 -0
  237. package/src/seat-loop/seat-loop.ts +126 -10
  238. package/src/self-dev/dev-pipeline.ts +8 -4
  239. package/src/self-dev/draft-sweep.ts +90 -10
  240. package/src/self-dev/draft-triage-rules.ts +25 -8
  241. package/src/self-dev/orchestrate.ts +15 -1
  242. package/src/self-dev/review-provider-fallback.ts +6 -1
  243. package/src/self-dev/run-supervisor.ts +49 -2
  244. package/src/self-implement/frozen-merges.ts +3 -2
  245. package/src/self-implement/goal-author.ts +43 -5
  246. package/src/self-implement/goal-clarification-escalation.ts +5 -0
  247. package/src/self-implement/heal-intake.ts +120 -2
  248. package/src/self-implement/host-regate.ts +9 -2
  249. package/src/self-implement/l8-merge-queue.ts +132 -0
  250. package/src/self-implement/orchestrator.ts +187 -40
  251. package/src/self-implement/pipeline-shape.ts +1 -1
  252. package/src/self-implement/revert-guard.ts +42 -7
  253. package/src/self-implement/run-ledger.ts +2 -0
  254. package/src/self-implement/run-status-mapping.ts +2 -0
  255. package/src/self-implement/seams.ts +1 -1
  256. package/src/self-implement/self-implement-cli.ts +1 -0
  257. package/src/self-implement/speed-claims.ts +338 -0
  258. package/src/steward/triage.ts +74 -3
  259. package/src/steward/working-backwards.ts +4 -1
  260. package/src/task-orchestrator/surfaces/pod-artifact-return.ts +63 -48
  261. package/src/task-orchestrator/surfaces/pod-command-job.ts +84 -9
  262. package/src/task-orchestrator/surfaces/pod-failure-reason.ts +48 -0
  263. package/src/task-orchestrator/surfaces/pod-pool.ts +60 -8
  264. package/src/task-orchestrator/surfaces/self-implement-pod.ts +147 -36
  265. package/src/task-orchestrator/surfaces/self-implement.ts +5 -1
  266. package/src/telegram.ts +25 -4
  267. package/src/user-config.ts +29 -5
  268. package/src/version/packed-revision.json +1 -1
  269. package/src/workflow-runtime/nodes/bash.ts +9 -1
  270. package/apps/pwa/out/_next/static/chunks/4562-071d8bac44a01069.js +0 -1
  271. package/apps/pwa/out/_next/static/chunks/5977-2df489622a52a33a.js +0 -1
  272. package/apps/pwa/out/_next/static/chunks/app/board/page-c496bb16da85385f.js +0 -1
  273. package/apps/pwa/out/_next/static/chunks/app/botlab/page-1abfe3aec3be6bdc.js +0 -1
  274. package/apps/pwa/out/_next/static/chunks/app/control/page-e6b84ab07d3567d5.js +0 -1
  275. package/apps/pwa/out/_next/static/chunks/app/design-check/page-61ec1d2e92bdd50a.js +0 -1
  276. package/apps/pwa/out/_next/static/chunks/app/inside/page-fd7e200d4d83fbd7.js +0 -1
  277. package/apps/pwa/out/_next/static/chunks/app/live/page-d47266766766d227.js +0 -1
  278. package/apps/pwa/out/_next/static/chunks/app/loops/page-160089123a40dd91.js +0 -1
  279. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-2ce5471926761fdd.js +0 -1
  280. package/apps/pwa/out/_next/static/chunks/app/not-found-e995d30f6800b6bf.js +0 -1
  281. package/apps/pwa/out/_next/static/chunks/app/observatory/page-39f89c53038a51d4.js +0 -1
  282. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-e7e10cd3386222e5.js +0 -1
  283. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-5a753bb4ec0cc511.js +0 -1
  284. package/apps/pwa/out/_next/static/chunks/app/page-341480ed8e3c1ea8.js +0 -1
  285. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-e1e47d8d940aba3d.js +0 -1
  286. package/apps/pwa/out/_next/static/chunks/app/showroom/page-58043db78ba519ae.js +0 -1
  287. package/apps/pwa/out/_next/static/chunks/app/tasks/page-357750dfb91bf960.js +0 -1
  288. package/apps/pwa/out/_next/static/chunks/app/today/page-194dce4a83143fce.js +0 -1
  289. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-16bef3da05a900c3.js +0 -1
  290. package/apps/pwa/out/_next/static/chunks/app/workflows/page-f8da5dc6f3752276.js +0 -1
  291. package/apps/pwa/out/_next/static/css/2e33713c444d5b39.css +0 -1
  292. package/apps/pwa/out/_next/static/x6mjI6vqOkVVu0jFuDkz1/_buildManifest.js +0 -1
  293. /package/apps/pwa/out/_next/static/{x6mjI6vqOkVVu0jFuDkz1 → LZcstaO9_NXypPFmujolv}/_ssgManifest.js +0 -0
@@ -19,8 +19,38 @@ function publishedTime(value: string): { date: string; short: string; full: stri
19
19
  return { date, short: `${parts.month}-${parts.day} ${parts.hour}:${parts.minute}`, full: `${date} ${parts.hour}:${parts.minute} KST` };
20
20
  }
21
21
 
22
+ // 구분자(` — ` · ` · ` · 줄바꿈 · 。)는 짝이 맞는 «…» 밖에서만 자른다.
22
23
  function firstClause(title: string): string {
23
- return title.split(/\s+[—–·]\s+|[\n。]/, 1)[0]!.trim().replace(/\s+/g, ' ');
24
+ let depth = 0;
25
+ let end = title.length;
26
+ // Balanced «…», (…), […], {…}, “…” and "…" spans are kept whole — a separator inside them never ends the clause.
27
+ const closers: Record<string, string> = { '«': '»', '(': ')', '[': ']', '{': '}', '“': '”', '‘': '’' };
28
+ const stack: string[] = [];
29
+ let quoted = false;
30
+ let single = false;
31
+ for (let i = 0; i < title.length; i++) {
32
+ const char = title[i]!;
33
+ if (char === '"' && (quoted || title.indexOf('"', i + 1) > i)) { quoted = !quoted; continue; }
34
+ // ASCII ' quotes like ", except a word-internal apostrophe (doesn't).
35
+ if (char === "'" && !(/\p{L}/u.test(title[i - 1] ?? '') && /\p{L}/u.test(title[i + 1] ?? ''))
36
+ && (single || title.indexOf("'", i + 1) > i)) { single = !single; continue; }
37
+ if (single) continue;
38
+ if (closers[char] && title.indexOf(closers[char]!, i) > i) { stack.push(closers[char]!); continue; }
39
+ if (stack.length && char === stack.at(-1)) { stack.pop(); continue; }
40
+ if (quoted || stack.length) continue;
41
+ if (char === '«' && title.indexOf('»', i) > i) depth++;
42
+ else if (char === '»' && depth > 0) depth--;
43
+ else if (depth === 0 && (char === '\n' || char === '。' || (/[—–·]/.test(char) && /\s/.test(title[i - 1] ?? '') && /\s/.test(title[i + 1] ?? '')))) { end = i; break; }
44
+ }
45
+ const clause = title.slice(0, end);
46
+ const unmatched = new Set<number>();
47
+ const open: number[] = [];
48
+ for (let i = 0; i < clause.length; i++) {
49
+ if (clause[i] === '«') open.push(i);
50
+ else if (clause[i] === '»') { if (open.length) open.pop(); else unmatched.add(i); }
51
+ }
52
+ for (const i of open) unmatched.add(i);
53
+ return clause.split('').filter((_, i) => !unmatched.has(i)).join('').trim().replace(/\s+/g, ' ');
24
54
  }
25
55
 
26
56
  function changeCount(draft: string): number {
@@ -68,7 +98,190 @@ function releaseHistory(context: GraphContext, root: string): string[] {
68
98
  return history;
69
99
  }
70
100
 
101
+ type LedgerNode = { nodeId: string; ok: boolean; output?: string };
102
+ type LedgerRun = { graphId: string; runId: string; startedAt?: string; input?: { version?: string }; nodes?: LedgerNode[] };
103
+
104
+ function publishedPreviousRun(dir: string, root: string, previousVersion: string, current: LedgerRun): LedgerRun | undefined {
105
+ const record: unknown = JSON.parse(readFileSync(join(root, 'release', previousVersion, 'release.json'), 'utf8'));
106
+ if (!record || typeof record !== 'object') return undefined;
107
+ const published = record as Record<string, unknown>;
108
+ if (published.version !== previousVersion || published.tag !== `v${previousVersion}`
109
+ || typeof published.sourceCommit !== 'string' || !/^[0-9a-f]{8,40}$/i.test(published.sourceCommit)
110
+ || typeof published.publishedAt !== 'string' || !Number.isFinite(Date.parse(published.publishedAt))) return undefined;
111
+ const publishedAt = published.publishedAt as string;
112
+ const candidates = readdirSync(dir).filter((file) => /^[\w.-]+\.json$/.test(file) && !file.endsWith('.decision.json')).flatMap((file) => {
113
+ try {
114
+ const value = JSON.parse(readFileSync(join(dir, file), 'utf8')) as LedgerRun;
115
+ if (value.graphId !== 'release-loop' || value.input?.version !== previousVersion || !Array.isArray(value.nodes)
116
+ || !value.nodes.some((node) => node.nodeId === 'publish' && node.ok)
117
+ || ledgerOutput(value, 'publish')?.tag !== published.tag
118
+ || ledgerOutput(value, 'version-release')?.commit !== published.sourceCommit
119
+ || (typeof value.startedAt === 'string' && value.startedAt > publishedAt)
120
+ || (typeof current.startedAt === 'string' && typeof value.startedAt === 'string' && value.startedAt > current.startedAt)) return [];
121
+ return [value];
122
+ } catch { return []; }
123
+ });
124
+ // Repeated publications of the same tag/commit cannot identify which run supplied the published count.
125
+ if (new Set(candidates.map((entry) => ledgerOutput(entry, 'known-issues')?.count)).size > 1) return undefined;
126
+ return candidates[0];
127
+ }
128
+
129
+ function ledgerOutput(run: LedgerRun, nodeId: string): Record<string, unknown> | undefined {
130
+ const node = run.nodes?.filter((entry) => entry.nodeId === nodeId && entry.ok).at(-1);
131
+ try {
132
+ const raw = node?.output?.trim().split(/\r?\n/).at(-1);
133
+ const value: unknown = JSON.parse(raw ?? '');
134
+ return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : undefined;
135
+ } catch { return undefined; }
136
+ }
137
+
138
+ function stabilityLine(context: GraphContext, root: string): string {
139
+ const identity = context as GraphContext & { graphId?: string; runId?: string };
140
+ const fields: Record<string, string | number> = { introduced: '?', preexisting: '?', recut: '?', hosts: '?', knownIssuesDelta: '?' };
141
+ const unreadable: string[] = [];
142
+ let line: string;
143
+ try {
144
+ if (identity.graphId !== 'release-loop' || !identity.runId || !/^[\w.-]+$/.test(identity.runId)) throw new Error('런 식별자 없음');
145
+ const dir = join(root, 'graph-runs', 'release-loop');
146
+ const ledgerPath = join(dir, `${identity.runId}.json`);
147
+ if (!existsSync(ledgerPath)) throw new Error(`런 원장 없음(${identity.runId}.json)`);
148
+ const run = JSON.parse(readFileSync(ledgerPath, 'utf8')) as LedgerRun;
149
+ if (run.graphId !== 'release-loop' || run.runId !== identity.runId || run.input?.version !== context.input.version || !Array.isArray(run.nodes)) throw new Error('런 원장 식별 불일치');
150
+ const gate = ledgerOutput(run, 'gate');
151
+ if (Array.isArray(gate?.introduced)) fields.introduced = gate.introduced.length;
152
+ else unreadable.push('gate.introduced');
153
+ if (typeof gate?.preexisting === 'number' && Number.isSafeInteger(gate.preexisting) && gate.preexisting >= 0) fields.preexisting = gate.preexisting;
154
+ else unreadable.push('gate.preexisting');
155
+ // The cutoff manifest contains baseline/cutoff SHAs and included landings, not the cut branch's pick count.
156
+ // Neither a different SHA nor the number of included landings establishes whether there was a recut.
157
+ unreadable.push('cutoff.pick count not recorded');
158
+ // ops-upgrade is downstream of vault-note in the release graph; its host results do not exist yet.
159
+ unreadable.push('ops-upgrade.hosts not yet available');
160
+ const current = ledgerOutput(run, 'known-issues');
161
+ try {
162
+ // Same baseline as the landing section: the newest earlier version that was actually published (an unpublished, folded version is skipped).
163
+ const previousVersion = previousPublishedCut(root, context.input.version)?.version;
164
+ const previousRun = previousVersion ? publishedPreviousRun(dir, root, previousVersion, run) : undefined;
165
+ const previous = previousRun ? ledgerOutput(previousRun, 'known-issues')?.count : undefined;
166
+ if (typeof current?.count === 'number' && Number.isSafeInteger(current.count) && current.count >= 0
167
+ && typeof previous === 'number' && Number.isSafeInteger(previous) && previous >= 0) {
168
+ const delta = current.count - previous;
169
+ fields.knownIssuesDelta = `${delta >= 0 ? '+' : ''}${delta}`;
170
+ } else unreadable.push('known-issues.count(previous/current)');
171
+ } catch { unreadable.push('known-issues.count(previous/current)'); }
172
+ line = `안정: 게이트 introduced ${fields.introduced} · preexisting ${fields.preexisting} · 재컷 ${fields.recut} · 호스트 판올림 성공 ${fields.hosts} · known-issues 직전 판 대비 ${fields.knownIssuesDelta}`;
173
+ } catch (error) {
174
+ const reason = error instanceof Error ? error.message : String(error);
175
+ unreadable.push(reason);
176
+ line = `안정: 측정 불가: ${reason.replace(/[\r\n]+/g, ' ')}`;
177
+ }
178
+ try { debug.log('release-loop.vault-note', 'stability-line', { fields, unreadable }); } catch { /* Observation cannot block publication. */ }
179
+ return line;
180
+ }
181
+
71
182
  interface ReleaseRecord { version: string; tag: string; sourceCommit: string; publishedAt: string; publicRepo?: string }
183
+
184
+ const landingTopics = [
185
+ { name: '조직·루프', match: /조직|좌석|스튜어드|본부|운영|자율|루프|미션|cron|schedule|seat|steward|loop|mission|hq\b|ops\b/i },
186
+ { name: '하니스', match: /하니스|골|게이트|검증|테스트|리뷰|실행기|워크트리|harness|goal|gate|review|test|worktree|self-implement|self-dev|graph/i },
187
+ { name: '채널·PWA', match: /채널|화면|텔레그램|디스코드|메신저|발송|알림|모바일|음성|channel|pwa|telegram|discord|outbound|tui|web|ios|android/i },
188
+ { name: '지식·교훈', match: /지식|기억|교훈|관측|로그|리서치|검색|문서|knowledge|memory|lesson|research|observation|logs|docs|manual/i },
189
+ { name: '공개·브랜드', match: /공개|브랜드|발행|릴리스|홍보|광고|마케팅|public|brand|release|publish|marketing|npm/i },
190
+ { name: '그 밖', match: /[\s\S]*/ },
191
+ ] as const;
192
+
193
+ type LandingLine = { title: string; line?: string; sha: string; prNumber?: number };
194
+
195
+ function balanced(text: string): boolean {
196
+ const pairs: Record<string, string> = { '»': '«', ')': '(', ']': '[', '}': '{', '”': '“', '’': '‘' };
197
+ const stack: string[] = [];
198
+ let quote = false;
199
+ let single = false;
200
+ const chars = [...text];
201
+ for (const [i, char] of chars.entries()) {
202
+ if (char === '"') { quote = !quote; continue; }
203
+ // ASCII ' pairs like ", except a word-internal apostrophe (doesn't) which never opens a quote.
204
+ if (char === "'") {
205
+ if (!(/\p{L}/u.test(chars[i - 1] ?? '') && /\p{L}/u.test(chars[i + 1] ?? ''))) single = !single;
206
+ continue;
207
+ }
208
+ if (char in pairs) { if (stack.pop() !== pairs[char]) return false; }
209
+ else if ('«([{“‘'.includes(char)) stack.push(char);
210
+ }
211
+ return !quote && !single && stack.length === 0;
212
+ }
213
+
214
+ function landingClause(text: string): string {
215
+ // A candidate with broken punctuation is not repaired by deleting its unmatched character.
216
+ if (!balanced(text)) return '';
217
+ // release/next.md lines lead with their kind («feat — …»); the headline is the change, not the kind.
218
+ const body = text.replace(/^-\s+/, '').replace(/^(feat|fix|internal|docs|test|perf|refactor|chore|ops|security|breaking)\s+[—–-]\s+/i, '');
219
+ const clause = firstClause(body);
220
+ return balanced(clause) ? clause : '';
221
+ }
222
+
223
+ function versionKey(value: string): number[] | null {
224
+ const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(value);
225
+ return match ? match.slice(1).map(Number) : null;
226
+ }
227
+
228
+ /** Newest release below `version` with a readable release.json; null when there is none to compare against. */
229
+ function previousPublishedCut(root: string, version: string): { version: string; sourceCommit: string } | null {
230
+ const current = versionKey(version);
231
+ if (!current) return null;
232
+ const compare = (a: number[], b: number[]) => a[0]! - b[0]! || a[1]! - b[1]! || a[2]! - b[2]!;
233
+ const earlier = readdirSync(join(root, 'release'))
234
+ .map((name) => ({ name, key: versionKey(name) }))
235
+ .filter((row): row is { name: string; key: number[] } => !!row.key && compare(row.key, current) < 0)
236
+ .sort((a, b) => compare(b.key, a.key));
237
+ for (const { name } of earlier) {
238
+ const path = join(root, 'release', name, 'release.json');
239
+ if (!existsSync(path)) continue;
240
+ const record = JSON.parse(readFileSync(path, 'utf8')) as { sourceCommit?: unknown };
241
+ if (typeof record.sourceCommit !== 'string' || !/^[0-9a-f]{40}$/i.test(record.sourceCommit)) throw new Error(`v${name} release.json sourceCommit invalid`);
242
+ return { version: name, sourceCommit: record.sourceCommit };
243
+ }
244
+ // Earlier versions exist but none was published: the «직전 판 컷» claim cannot be checked, so it is not presented as settled.
245
+ if (earlier.length) throw new Error(`직전 판 컷 확인 못 함 — 이전 판 ${earlier.length}개에 release.json 없음`);
246
+ return null;
247
+ }
248
+
249
+ function landingsAtCut(root: string, version: string, cut: string): { groups: Array<{ name: string; lines: LandingLine[] }>; error?: string } {
250
+ try {
251
+ const manifest = JSON.parse(readFileSync(join(root, 'release', version, 'manifest.json'), 'utf8')) as {
252
+ version?: unknown; baseline?: { sha?: unknown }; cutoff?: { sha?: unknown }; in?: unknown;
253
+ };
254
+ if (manifest.version !== version || !/^[0-9a-f]{8,40}$/i.test(String(manifest.baseline?.sha ?? ''))
255
+ || manifest.cutoff?.sha !== cut || !Array.isArray(manifest.in)
256
+ || !manifest.in.every((entry: unknown) => {
257
+ const item = entry as Partial<LandingLine> | null;
258
+ return item && typeof item.title === 'string' && typeof item.sha === 'string'
259
+ && (item.line === undefined || typeof item.line === 'string')
260
+ && (item.prNumber === undefined || Number.isInteger(item.prNumber));
261
+ })) throw new Error('manifest.json 판·직전 컷·이번 컷 또는 착지 줄 불일치');
262
+ // The «직전 판 컷» claim is checked against the newest earlier published release, when one exists.
263
+ const previous = previousPublishedCut(root, version);
264
+ if (previous && previous.sourceCommit.toLowerCase() !== String(manifest.baseline!.sha).toLowerCase())
265
+ throw new Error(`직전 판 컷 불일치 — manifest baseline ${String(manifest.baseline!.sha).slice(0, 8)} ≠ v${previous.version} 컷 ${previous.sourceCommit.slice(0, 8)}`);
266
+ const groups = landingTopics.map(({ name }) => ({ name, lines: [] as LandingLine[] }));
267
+ for (const entry of manifest.in as LandingLine[]) {
268
+ const text = entry.line?.trim() || entry.title.trim();
269
+ const topic = landingTopics.findIndex(({ match }) => match.test(text));
270
+ groups[topic]!.lines.push(entry);
271
+ }
272
+ return { groups: groups.filter((group) => group.lines.length) };
273
+ } catch (error) {
274
+ const reason = error instanceof Error ? error.message : String(error);
275
+ return { groups: [], error: `manifest.json: ${reason.replace(/^manifest\.json\s*/, '').replace(/\s+/g, ' ')}` };
276
+ }
277
+ }
278
+
279
+ function representative(groups: Array<{ name: string; lines: LandingLine[] }>, fallback: string): string {
280
+ const candidates = [...groups].sort((a, b) => b.lines.length - a.lines.length);
281
+ const lines = candidates.map(({ lines }) => lines.map((entry) => landingClause(entry.line?.trim() || entry.title)).find(Boolean))
282
+ .filter((line): line is string => !!line).slice(0, 3);
283
+ return lines.join(' · ') || fallback;
284
+ }
72
285
  export interface VaultNoteDeps {
73
286
  instanceRoot?: string;
74
287
  productionRoot?: string;
@@ -104,12 +317,22 @@ export function runVaultNote(context: GraphContext = readGraphContext(), deps: V
104
317
  const published = publishedTime(record.publishedAt);
105
318
  const draft = readFileSync(join(root, 'release', version, 'prepared', 'notes-draft.md'), 'utf8');
106
319
  const items = (deps.checklist ?? ((v) => listChecklist(v).items))(version).filter((item) => item.status === 'green');
107
- const oneLine = items.slice(0, 3).map((item) => firstClause(item.title)).join(' · ');
320
+ const landings = landingsAtCut(root, version, record.sourceCommit);
321
+ const oneLine = representative(landings.groups, items.map((item) => landingClause(item.title)).filter(Boolean).slice(0, 3).join(' · ') || '착지 내용 확인 필요');
108
322
  const location = process.env.ELANOUS_GRAPH_CONTEXT;
109
- const identity = location && !('runId' in context)
110
- ? JSON.parse(location.trimStart().startsWith('{') ? location : readFileSync(location, 'utf8')) as { graphId?: string; runId?: string }
111
- : {};
112
- const history = releaseHistory({ ...identity, ...context }, root);
323
+ let identity: { graphId?: string; runId?: string } = {};
324
+ if (location && !('runId' in context)) {
325
+ try { identity = JSON.parse(location.trimStart().startsWith('{') ? location : readFileSync(location, 'utf8')) as typeof identity; }
326
+ catch { /* Missing or malformed graph identity is reported by the stability line, not by publication. */ }
327
+ }
328
+ const runContext = { ...identity, ...context };
329
+ const stability = stabilityLine(runContext, root);
330
+ let history: string[];
331
+ try { history = releaseHistory(runContext, root); }
332
+ catch (error) {
333
+ history = [];
334
+ try { debug.log('release.vault-note', 'history-unreadable', { reason: error instanceof Error ? error.message : String(error) }); } catch { /* Observation cannot block publication. */ }
335
+ }
113
336
  // OP 10-04 19:10 ①: group by feature family (cell id family: HQ-FENCE2 → HQ · LOOP-OBS1 → LOOP · K10i → K), not by seat.
114
337
  const groups = new Map<string, typeof items>();
115
338
  for (const item of items) {
@@ -125,6 +348,14 @@ export function runVaultNote(context: GraphContext = readGraphContext(), deps: V
125
348
  '',
126
349
  ]),
127
350
  ]);
351
+ const landingSection = landings.error
352
+ ? [`- 못 읽음 · ${landings.error}`]
353
+ : landings.groups.length
354
+ ? landings.groups.flatMap(({ name, lines }) => [
355
+ `### ${name} (${lines.length}건)`, '',
356
+ ...lines.map((entry) => `- ${entry.line?.trim() || entry.title.trim()}${entry.prNumber ? ` (#${entry.prNumber})` : ''}`), '',
357
+ ])
358
+ : ['- 착지 0건'];
128
359
  const target = join(vault, directory);
129
360
  const name = `엘라누스 v${version} (${published.date})`;
130
361
  // 대표 10-04 19:25: notes live under <major>.x/<major>.<minor>/ · the index stays at the top.
@@ -139,14 +370,15 @@ export function runVaultNote(context: GraphContext = readGraphContext(), deps: V
139
370
  if (!rowExists && !header.test(current)) throw new Error(`index table header missing: ${indexPath}`);
140
371
  mkdirSync(noteDir, { recursive: true });
141
372
  const content = [
142
- '---', `title: "엘라누스 v${version} — ${oneLine.replace(/"/g, '\\"')}"`,
373
+ '---', `title: ${JSON.stringify(`엘라누스 v${version} — ${oneLine}`)}`,
143
374
  `version: ${version}`, `published: ${published.full}`, `cut: ${record.sourceCommit.slice(0, 8)}`,
144
375
  'tags: [elanous, release]', 'links:', ` github: https://github.com/${publicRepo}/releases/tag/${record.tag}`,
145
376
  ` npm: elanous@${version}`, '---', '', `# 엘라누스 v${version} — ${oneLine}`, '',
146
377
  '## 한 줄', '', oneLine, '',
147
378
  '## 발행 경과', '', `- ${published.full} 발행`, ...history, '',
379
+ '## 이번 판 착지 (직전 판 컷 ~ 이번 판 컷)', '', ...landingSection,
148
380
  '## 이제 할 수 있는 것', '', ...abilities, '## 숫자', '',
149
- `- 변경 ${changeCount(draft)}건`, `- green 칸 ${items.length}개`, '',
381
+ `- 변경 ${changeCount(draft)}건`, `- green 칸 ${items.length}개`, stability, '',
150
382
  ].join('\n');
151
383
  // A handwritten note at the old flat location (e.g. OP's v0.2.13 sample) counts as existing — never duplicate it.
152
384
  let existed = existsSync(join(target, `${name}.md`));
@@ -1,11 +1,13 @@
1
1
  #!/usr/bin/env bun
2
2
  import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
3
- import { join, resolve } from 'node:path';
4
- import { checkBrand } from '../brand/check.js';
3
+ import { dirname, join, resolve } from 'node:path';
4
+ import { BRAND_RULES_UNMEASURED, checkBrand } from '../brand/check.js';
5
5
  import { ClaimsLedger } from '../../src/claims/claims-ledger.js';
6
6
  import { debug } from '../../src/debug/log.js';
7
7
  import { effectiveInstanceRoot } from '../../src/instance/resolve.js';
8
8
  import { listChecklist, type ChecklistItem } from '../../src/release-loop/checklist.js';
9
+ import { appendSeatRequestRows, listSeatRequests, withSeatRequestLedgerLock } from '../../src/seat-dispatch/seat-request-ledger.js';
10
+ import { finishNode, readGraphContext, type GraphContext } from '../release-loop/node-verdict.js';
9
11
 
10
12
  const repoRoot = resolve(import.meta.dir, '../..');
11
13
 
@@ -16,6 +18,7 @@ type DraftOptions = {
16
18
  checklistRoot?: string;
17
19
  stateDir?: string;
18
20
  manualRoot?: string;
21
+ rulesPath?: string;
19
22
  };
20
23
 
21
24
  type DraftResult = {
@@ -27,6 +30,7 @@ type DraftResult = {
27
30
  claims: number;
28
31
  manualCandidates: number;
29
32
  brandFindings: number;
33
+ brandUnmeasured?: string;
30
34
  files: string[];
31
35
  };
32
36
 
@@ -126,6 +130,7 @@ export function draftReleaseStory(options: DraftOptions): DraftResult {
126
130
  const outDir = options.outDir ?? join(effectiveInstanceRoot(), 'release', version, 'story');
127
131
  mkdirSync(outDir, { recursive: true });
128
132
  let brandFindings = 0;
133
+ let brandUnmeasured: string | undefined;
129
134
  const files: string[] = [];
130
135
  for (const [name, body, scope] of [
131
136
  ['announcement.md', announcement, 'release-notes'],
@@ -134,20 +139,63 @@ export function draftReleaseStory(options: DraftOptions): DraftResult {
134
139
  ] as const) {
135
140
  const file = join(outDir, name);
136
141
  writeFileSync(file, body);
137
- const findings = checkBrand(scope, [file]).findings;
138
- brandFindings += findings.length;
139
- if (findings.length) writeFileSync(file, findings.map(({ id, match }) => `> ⚠ 브랜드 규칙: ${id} «${match}»`).join('\n') + '\n\n' + body);
142
+ const checked = checkBrand(scope, [file], options.rulesPath);
143
+ if (checked.missing) {
144
+ brandUnmeasured = checked.message ?? BRAND_RULES_UNMEASURED;
145
+ writeFileSync(file, `> ⚠ ${brandUnmeasured}\n\n${body}`);
146
+ } else {
147
+ brandFindings += checked.findings.length;
148
+ if (checked.findings.length) writeFileSync(file, checked.findings.map(({ id, match }) => `> ⚠ 브랜드 규칙: ${id} «${match}»`).join('\n') + '\n\n' + body);
149
+ }
140
150
  files.push(file);
141
151
  }
142
152
  const result: DraftResult = { version, status: 'drafted', userLines: userLines.length, internalDropped, greenCells: green.length,
143
- claims: evidence.sentences.length, manualCandidates: candidates.length, brandFindings, files };
153
+ claims: evidence.sentences.length, manualCandidates: candidates.length, brandFindings, ...(brandUnmeasured ? { brandUnmeasured } : {}), files };
144
154
  debug.log('release.story', 'drafted', { version, userLines: result.userLines, internalDropped, greenCells: result.greenCells,
145
- claims: result.claims, manualCandidates: result.manualCandidates, brandFindings });
155
+ claims: result.claims, manualCandidates: result.manualCandidates, brandFindings, ...(brandUnmeasured ? { brandUnmeasured } : {}) });
146
156
  return result;
147
157
  }
148
158
 
149
- if (import.meta.main) {
159
+ export function runReleaseStory(context: GraphContext = readGraphContext(), draft: typeof draftReleaseStory = draftReleaseStory) {
160
+ const version = context.input.version;
150
161
  try {
162
+ if (context.outputs.publish?.outcome !== 'ok' || context.outputs.verify?.outcome !== 'ok')
163
+ throw new Error('published release and verification required');
164
+ const result = draft({ version });
165
+ if (result.status === 'drafted') {
166
+ const root = effectiveInstanceRoot();
167
+ const requestsPath = join(root, 'seat-requests', 'requests.jsonl');
168
+ const key = `release-story:${version}`;
169
+ const storyDir = dirname(result.files[0]!);
170
+ withSeatRequestLedgerLock(requestsPath, () => {
171
+ if (listSeatRequests(root).some((row) => row.key === key)) return;
172
+ appendSeatRequestRows(requestsPath, [{ key, seat: 'MK', source: 'release-story', version, status: 'pending',
173
+ text: `${version} 공지 초안 준비됨: ${storyDir}`, queuedAt: new Date().toISOString() }]);
174
+ });
175
+ }
176
+ return { outcome: 'ok' as const, verdict: 'pass' as const, summary: result.status,
177
+ files: result.files, status: result.status };
178
+ } catch (error) {
179
+ const reason = error instanceof Error ? error.message : String(error);
180
+ debug.log('release.story', 'draft-failed', { version, reason });
181
+ return { outcome: 'fail' as const, verdict: 'fail' as const, summary: `release story failed: ${reason}`, reason };
182
+ }
183
+ }
184
+
185
+ if (import.meta.main) {
186
+ if (process.argv.includes('--graph')) {
187
+ let version = '';
188
+ try {
189
+ if (process.argv.length !== 3) throw new Error('usage: bun scripts/release-story/draft.ts --graph');
190
+ const context = readGraphContext();
191
+ version = context.input.version;
192
+ process.exitCode = finishNode('release-story', version, runReleaseStory(context));
193
+ } catch (error) {
194
+ const reason = error instanceof Error ? error.message : String(error);
195
+ debug.log('release.story', 'draft-failed', { version, reason });
196
+ process.exitCode = finishNode('release-story', version, { outcome: 'fail', verdict: 'fail', summary: `release story failed: ${reason}`, reason });
197
+ }
198
+ } else try {
151
199
  const args = process.argv.slice(2);
152
200
  let version: string | undefined;
153
201
  let nextPath: string | undefined;
@@ -164,7 +212,7 @@ if (import.meta.main) {
164
212
  if (!version || (args.includes('--next') && !nextPath) || (args.includes('--out') && !outDir))
165
213
  throw new Error('usage: bun scripts/release-story/draft.ts --version <v> [--next <path>] [--out <dir>] [--json]');
166
214
  const result = draftReleaseStory({ version, ...(nextPath ? { nextPath } : {}), ...(outDir ? { outDir } : {}) });
167
- console.log(json ? JSON.stringify(result) : result.status === 'drafted' ? `drafted: ${result.files.join(', ')}` : result.status);
215
+ console.log(json ? JSON.stringify(result) : result.status === 'drafted' ? `drafted: ${result.files.join(', ')}${result.brandUnmeasured ? ` · ${result.brandUnmeasured}` : ''}` : result.status);
168
216
  } catch (error) {
169
217
  console.error(error instanceof Error ? error.message : String(error));
170
218
  process.exitCode = 1;