elanous 0.2.15 → 0.2.17

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 (309) 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-33ffcc49ab45123b.js → 1307-aac7332576c4d6fc.js} +1 -1
  4. package/apps/pwa/out/_next/static/chunks/{6877-7c6208705c284124.js → 6877-fe94e76a5841ee9b.js} +1 -1
  5. package/apps/pwa/out/_next/static/chunks/8401-4ce1c42c8d89903f.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/899-f41be85e8a680651.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/{9103-7a3ae7f60af3b99f.js → 9103-60fbef33e66f0a46.js} +1 -1
  8. package/apps/pwa/out/_next/static/chunks/9247-269e4464cc4b491c.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/app/approvals/{page-0e2c57fe6add4b2a.js → page-3143b07a817d1da9.js} +1 -1
  10. package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-e05a77df7e005b04.js → page-17ba086ef82d339b.js} +1 -1
  11. package/apps/pwa/out/_next/static/chunks/app/board/page-7eb661ff23af7da2.js +1 -0
  12. package/apps/pwa/out/_next/static/chunks/app/botlab/page-6b8444914da74ca6.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/app/bots/{page-b8c49f6b4fa2df91.js → page-6e5c3a867557298b.js} +1 -1
  14. package/apps/pwa/out/_next/static/chunks/app/ceo/page-7110f732f45bf944.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/app/chat/{page-976813c91d7394ea.js → page-9a41d65355fc25b9.js} +1 -1
  16. package/apps/pwa/out/_next/static/chunks/app/consult/page-5e77cd1802567c89.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/app/control/{page-1d4901eb7c2d8822.js → page-41afec0b255ed2bb.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/design-check/{page-72d3e7ff35a8c954.js → page-4187666132fc98c4.js} +1 -1
  19. package/apps/pwa/out/_next/static/chunks/app/editor/{page-60c7a8e1a4fe62ce.js → page-09328d499162bae4.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/exec/{page-2a52b316cb143d9c.js → page-29ff3a8f84f03b6a.js} +1 -1
  21. package/apps/pwa/out/_next/static/chunks/app/field/{page-c3ce483c8d5db370.js → page-80ec7401bc83f511.js} +1 -1
  22. package/apps/pwa/out/_next/static/chunks/app/inside/page-ab93b240f687d69a.js +1 -0
  23. package/apps/pwa/out/_next/static/chunks/app/intake/{page-96f074f6538e8a2c.js → page-fc7d86467291ae38.js} +1 -1
  24. package/apps/pwa/out/_next/static/chunks/app/{layout-e97be5334d83a9f5.js → layout-de43a186e5ba5683.js} +1 -1
  25. package/apps/pwa/out/_next/static/chunks/app/live/page-8b08cafad27759c9.js +1 -0
  26. package/apps/pwa/out/_next/static/chunks/app/loops/page-0e2d6985fb42cd2f.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/app/market/page-13e4108999f58d13.js +1 -0
  28. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-e8296ee284aa1293.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/app/morning/{page-c1343f9499d86f3f.js → page-043a9111e74f6b24.js} +1 -1
  30. package/apps/pwa/out/_next/static/chunks/app/{not-found-8739a2e0ae793130.js → not-found-23a4601ceb7f452e.js} +1 -1
  31. package/apps/pwa/out/_next/static/chunks/app/observatory/page-1ed1324b1dc36754.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-d23b12bb92242558.js → page-10d958605a77b3bd.js} +1 -1
  33. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-166745e51ac0c7b0.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/app/ops/seats/{page-c7fea475488201a8.js → page-a04260b8ccc968d4.js} +1 -1
  35. package/apps/pwa/out/_next/static/chunks/app/outputs/page-7a1d85e121065cc8.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/app/page-1f50e3d4e13435f4.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/app/reflection/{page-5efbf81124c474ae.js → page-12916918988ac97e.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-d8a3347a9d0c3f4a.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/app/sessions/page-a9117d2cd3093d36.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-06ee266953d9c879.js → page-5a64cb5208c6a8fd.js} +1 -1
  41. package/apps/pwa/out/_next/static/chunks/app/settings/{page-153c2b3bcc89e8de.js → page-1407ec83ea64c1f9.js} +1 -1
  42. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-1d824510df025d18.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/app/setup/page-6e878ebbe1737d00.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/app/share/{page-0d6d23b40263a7ea.js → page-7f49fe645d5132f7.js} +1 -1
  45. package/apps/pwa/out/_next/static/chunks/app/showroom/page-81a4e7f8dbfe74cb.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/app/tasks/page-27d71f131acd9320.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/app/term/page-3ab59d19913b200e.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/app/today/page-bf85b2f5bf077925.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/app/trace/{page-73735b64bfe1ae67.js → page-424659bacfb8cdd7.js} +1 -1
  50. package/apps/pwa/out/_next/static/chunks/app/vault/page-ab32a8f522a60371.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/{page-f7e631e9b4daf66e.js → page-fc2df261f8a2c7df.js} +1 -1
  52. package/apps/pwa/out/_next/static/chunks/app/workflows/page-09461366af82ef13.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-9817c7f9007466cc.js → page-5d4fef9ccd7d406b.js} +1 -1
  54. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-ea90a8ac73ce2749.js +1 -0
  55. package/apps/pwa/out/_next/static/css/001d5fbaee388a25.css +1 -0
  56. package/apps/pwa/out/_next/static/css/{04dabcbcc4bfa6db.css → 085673d826b98b62.css} +1 -1
  57. package/apps/pwa/out/approvals/index.html +1 -1
  58. package/apps/pwa/out/approvals/index.txt +15 -15
  59. package/apps/pwa/out/autopilot/index.html +1 -1
  60. package/apps/pwa/out/autopilot/index.txt +15 -15
  61. package/apps/pwa/out/board/index.html +1 -1
  62. package/apps/pwa/out/board/index.txt +15 -15
  63. package/apps/pwa/out/botlab/index.html +1 -1
  64. package/apps/pwa/out/botlab/index.txt +15 -15
  65. package/apps/pwa/out/bots/index.html +1 -1
  66. package/apps/pwa/out/bots/index.txt +15 -15
  67. package/apps/pwa/out/ceo/index.html +1 -1
  68. package/apps/pwa/out/ceo/index.txt +15 -15
  69. package/apps/pwa/out/chat/index.html +1 -1
  70. package/apps/pwa/out/chat/index.txt +15 -15
  71. package/apps/pwa/out/consult/index.html +1 -1
  72. package/apps/pwa/out/consult/index.txt +15 -15
  73. package/apps/pwa/out/control/index.html +1 -1
  74. package/apps/pwa/out/control/index.txt +15 -15
  75. package/apps/pwa/out/design-check/index.html +1 -1
  76. package/apps/pwa/out/design-check/index.txt +15 -15
  77. package/apps/pwa/out/editor/index.html +1 -1
  78. package/apps/pwa/out/editor/index.txt +15 -15
  79. package/apps/pwa/out/exec/index.html +1 -1
  80. package/apps/pwa/out/exec/index.txt +15 -15
  81. package/apps/pwa/out/field/index.html +1 -1
  82. package/apps/pwa/out/field/index.txt +15 -15
  83. package/apps/pwa/out/healthz +1 -1
  84. package/apps/pwa/out/index.html +1 -1
  85. package/apps/pwa/out/index.txt +15 -15
  86. package/apps/pwa/out/inside/index.html +1 -1
  87. package/apps/pwa/out/inside/index.txt +15 -15
  88. package/apps/pwa/out/intake/index.html +1 -1
  89. package/apps/pwa/out/intake/index.txt +15 -15
  90. package/apps/pwa/out/live/index.html +1 -1
  91. package/apps/pwa/out/live/index.txt +15 -15
  92. package/apps/pwa/out/loops/index.html +1 -1
  93. package/apps/pwa/out/loops/index.txt +15 -15
  94. package/apps/pwa/out/market/index.html +1 -1
  95. package/apps/pwa/out/market/index.txt +15 -15
  96. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  97. package/apps/pwa/out/missions/placeholder/index.txt +15 -15
  98. package/apps/pwa/out/morning/index.html +1 -1
  99. package/apps/pwa/out/morning/index.txt +15 -15
  100. package/apps/pwa/out/observatory/index.html +1 -1
  101. package/apps/pwa/out/observatory/index.txt +15 -15
  102. package/apps/pwa/out/ops/checklist/index.html +1 -1
  103. package/apps/pwa/out/ops/checklist/index.txt +15 -15
  104. package/apps/pwa/out/ops/release/index.html +1 -1
  105. package/apps/pwa/out/ops/release/index.txt +15 -15
  106. package/apps/pwa/out/ops/seats/index.html +1 -1
  107. package/apps/pwa/out/ops/seats/index.txt +15 -15
  108. package/apps/pwa/out/outputs/index.html +1 -1
  109. package/apps/pwa/out/outputs/index.txt +15 -15
  110. package/apps/pwa/out/reflection/index.html +1 -1
  111. package/apps/pwa/out/reflection/index.txt +16 -16
  112. package/apps/pwa/out/scheduler/index.html +1 -1
  113. package/apps/pwa/out/scheduler/index.txt +15 -15
  114. package/apps/pwa/out/sessions/index.html +1 -1
  115. package/apps/pwa/out/sessions/index.txt +15 -15
  116. package/apps/pwa/out/settings/devices/index.html +1 -1
  117. package/apps/pwa/out/settings/devices/index.txt +15 -15
  118. package/apps/pwa/out/settings/index.html +1 -1
  119. package/apps/pwa/out/settings/index.txt +15 -15
  120. package/apps/pwa/out/setup/done/index.html +1 -1
  121. package/apps/pwa/out/setup/done/index.txt +15 -15
  122. package/apps/pwa/out/setup/index.html +1 -1
  123. package/apps/pwa/out/setup/index.txt +15 -15
  124. package/apps/pwa/out/share/index.html +1 -1
  125. package/apps/pwa/out/share/index.txt +15 -15
  126. package/apps/pwa/out/showroom/index.html +1 -1
  127. package/apps/pwa/out/showroom/index.txt +15 -15
  128. package/apps/pwa/out/tasks/index.html +1 -1
  129. package/apps/pwa/out/tasks/index.txt +15 -15
  130. package/apps/pwa/out/term/index.html +1 -1
  131. package/apps/pwa/out/term/index.txt +15 -15
  132. package/apps/pwa/out/today/index.html +1 -1
  133. package/apps/pwa/out/today/index.txt +15 -15
  134. package/apps/pwa/out/trace/index.html +1 -1
  135. package/apps/pwa/out/trace/index.txt +15 -15
  136. package/apps/pwa/out/vault/index.html +1 -1
  137. package/apps/pwa/out/vault/index.txt +15 -15
  138. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  139. package/apps/pwa/out/workflows/chat-ui/index.txt +15 -15
  140. package/apps/pwa/out/workflows/index.html +1 -1
  141. package/apps/pwa/out/workflows/index.txt +15 -15
  142. package/apps/pwa/out/workspace/index.html +1 -1
  143. package/apps/pwa/out/workspace/index.txt +15 -15
  144. package/apps/pwa/out/worktrees/index.html +1 -1
  145. package/apps/pwa/out/worktrees/index.txt +15 -15
  146. package/graphs/doc-gen/doc-gen.yaml +24 -0
  147. package/graphs/doc-gen/recipes.yaml +3 -0
  148. package/graphs/intake/intake-daily.yaml +9 -2
  149. package/graphs/intake/news-sources.yaml +4 -0
  150. package/graphs/intake/recipes.yaml +6 -1
  151. package/graphs/orchestrator/orchestrator.yaml +7 -0
  152. package/graphs/orchestrator/recipes.yaml +3 -0
  153. package/graphs/steward/recipes.yaml +2 -1
  154. package/package.json +1 -1
  155. package/scripts/__fixtures__/intake-news.xml +7 -0
  156. package/scripts/ci-public-export-import-gate.ts +154 -0
  157. package/scripts/ci-test-interference-gate.ts +5 -1
  158. package/scripts/ci-typecheck-changed.ts +34 -15
  159. package/scripts/docs/doc-rot.ts +164 -0
  160. package/scripts/docs/generate-graph-anatomy.ts +131 -9
  161. package/scripts/docs-cli-check.ts +4 -4
  162. package/scripts/goals/goal-census.ts +343 -0
  163. package/scripts/intake-news.ts +169 -0
  164. package/scripts/measure/author-section-contribution.ts +84 -0
  165. package/scripts/release-loop/cut-branch.ts +43 -13
  166. package/scripts/release-loop/docs-land-node.ts +69 -10
  167. package/scripts/release-loop/gate-node.ts +3 -1
  168. package/scripts/release-loop/publish-node.ts +2 -2
  169. package/scripts/release-loop/release-docs.ts +134 -0
  170. package/scripts/release-loop/vault-note-node.ts +19 -2
  171. package/scripts/release-loop/version-node.ts +7 -4
  172. package/scripts/release-story/draft.ts +42 -14
  173. package/scripts/rhythm/monthly.ts +97 -0
  174. package/src/acp/client.ts +17 -2
  175. package/src/agent-mission/driver.ts +104 -11
  176. package/src/agent-mission/supervisor-metrics-cli.ts +40 -0
  177. package/src/agent-mission/supervisor-metrics.ts +218 -0
  178. package/src/autopilot/track-agent-shadow.ts +2 -1
  179. package/src/autopilot/track-agent.ts +5 -3
  180. package/src/briefing/brief-items.ts +191 -36
  181. package/src/budget/usage-outcomes.ts +204 -0
  182. package/src/cli/autopilot-cli.ts +66 -0
  183. package/src/cli/brief-cli.ts +73 -0
  184. package/src/cli/decisions-cli.ts +4 -1
  185. package/src/cli/intake-cli.ts +1 -1
  186. package/src/cli/know-cli.ts +20 -0
  187. package/src/cli/log-event-names.ts +1 -0
  188. package/src/cli/logs-cli.ts +43 -1
  189. package/src/cli/pr-cli.ts +92 -13
  190. package/src/cli/provider-cli.ts +14 -0
  191. package/src/cli/release-cli.ts +11 -8
  192. package/src/cli/self-update-canary.ts +46 -0
  193. package/src/cli/self-update.ts +399 -1
  194. package/src/cli/usage-cli.ts +26 -0
  195. package/src/context-card/index.ts +45 -0
  196. package/src/context-card/task.ts +76 -0
  197. package/src/dashboard/slash-runtime/dashboard-handlers.ts +2 -0
  198. package/src/decisions/decision-ledger.ts +10 -1
  199. package/src/discord-self-message.ts +9 -0
  200. package/src/flow-loop/split.ts +24 -0
  201. package/src/harness/harness-cli-command.ts +178 -12
  202. package/src/harness/harness-pod-dispatch.ts +36 -55
  203. package/src/harness/harness-queue.ts +86 -7
  204. package/src/harness/harness-seams.ts +13 -3
  205. package/src/harness/helper-repair.ts +139 -8
  206. package/src/harness/helper-scan.ts +13 -2
  207. package/src/harness/nested-elanous-policy.ts +133 -0
  208. package/src/hq/fence-wrapper.ts +123 -0
  209. package/src/hq/hq.ts +16 -2
  210. package/src/hq/lease.ts +5 -3
  211. package/src/hq/transfer.ts +185 -0
  212. package/src/index.ts +168 -13
  213. package/src/intake-plane/digest.ts +85 -28
  214. package/src/knowledge/know-find.ts +105 -0
  215. package/src/llm.ts +75 -19
  216. package/src/loops/orchestrator/finish-rate.ts +14 -3
  217. package/src/loops/orchestrator/tick.ts +96 -25
  218. package/src/maturity/bot-slash-catalog.ts +11 -2
  219. package/src/maturity/feature-maturity.ts +2 -2
  220. package/src/maturity/tui-slash-availability.ts +9 -0
  221. package/src/nexus/api/grid.ts +61 -0
  222. package/src/nexus/api/http-server.ts +19 -1
  223. package/src/nexus/api/task-cards-api.ts +19 -0
  224. package/src/oauth/codex.ts +82 -2
  225. package/src/oauth/overload-failover-launch.ts +75 -0
  226. package/src/oauth/overload-failover.ts +137 -0
  227. package/src/release-loop/checklist.ts +10 -5
  228. package/src/release-loop/feature-store.ts +10 -9
  229. package/src/release-loop/landing-freeze.ts +18 -10
  230. package/src/release-loop/placement.ts +17 -8
  231. package/src/seat-dispatch/seat-ask.ts +33 -6
  232. package/src/seat-loop/seat-loop.ts +172 -21
  233. package/src/self-dev/ask-launch-flow.ts +48 -8
  234. package/src/self-dev/authored-goal-trace.ts +64 -0
  235. package/src/self-dev/dev-cli.ts +19 -1
  236. package/src/self-dev/dev-pipeline.ts +6 -5
  237. package/src/self-dev/entrance-registry.ts +10 -6
  238. package/src/self-dev/old-door.ts +73 -0
  239. package/src/self-dev/run-store.ts +5 -1
  240. package/src/self-dev/run-supervisor.ts +7 -1
  241. package/src/self-dev/supervisor-verdict-edges.ts +46 -0
  242. package/src/self-implement/app-api-budget.ts +166 -0
  243. package/src/self-implement/budget-gate.ts +7 -5
  244. package/src/self-implement/follow-up-goals.ts +289 -0
  245. package/src/self-implement/frozen-merges.ts +25 -20
  246. package/src/self-implement/goal-author-clarification.ts +81 -0
  247. package/src/self-implement/goal-author-cli.ts +33 -5
  248. package/src/self-implement/goal-author.ts +107 -25
  249. package/src/self-implement/goal-clarification-escalation.ts +37 -1
  250. package/src/self-implement/goal-document-write.ts +28 -0
  251. package/src/self-implement/heal-intake.ts +4 -0
  252. package/src/self-implement/heal-triage.ts +44 -0
  253. package/src/self-implement/host-regate.ts +3 -1
  254. package/src/self-implement/orchestrator.ts +157 -8
  255. package/src/self-implement/run-ledger.ts +4 -0
  256. package/src/self-implement/seams.ts +44 -15
  257. package/src/session-runtime/index.ts +2 -0
  258. package/src/session-runtime/retry-api.ts +87 -0
  259. package/src/session-runtime/retry-policy.ts +107 -6
  260. package/src/steward/working-backwards.ts +16 -1
  261. package/src/task-agent/actions.ts +118 -0
  262. package/src/task-agent/judge.ts +96 -0
  263. package/src/task-cards/card-cli.ts +25 -0
  264. package/src/task-cards/card-store.ts +9 -4
  265. package/src/task-orchestrator/surfaces/pod-failure-reason.ts +33 -4
  266. package/src/task-orchestrator/surfaces/pod-lease.ts +5 -2
  267. package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +4 -3
  268. package/src/task-orchestrator/surfaces/pod-ledger-prod-sink.ts +75 -0
  269. package/src/task-orchestrator/surfaces/pod-pool.ts +95 -18
  270. package/src/task-orchestrator/surfaces/pod-source-receive.ts +37 -1
  271. package/src/task-orchestrator/surfaces/self-implement-pod.ts +168 -37
  272. package/src/task-orchestrator/surfaces/self-implement.ts +2 -0
  273. package/src/telegram-commands.ts +6 -1
  274. package/src/telegram-loops-command.ts +21 -14
  275. package/src/telegram.ts +47 -0
  276. package/src/user-config.ts +59 -4
  277. package/src/version/packed-revision.json +1 -1
  278. package/apps/pwa/out/_next/static/chunks/8827-e2f5b09a8d37e83b.js +0 -1
  279. package/apps/pwa/out/_next/static/chunks/899-46946fe8363e1262.js +0 -1
  280. package/apps/pwa/out/_next/static/chunks/9247-29d889b809288e65.js +0 -1
  281. package/apps/pwa/out/_next/static/chunks/app/board/page-f544338193a9a762.js +0 -1
  282. package/apps/pwa/out/_next/static/chunks/app/botlab/page-5491c115b1f9a923.js +0 -1
  283. package/apps/pwa/out/_next/static/chunks/app/ceo/page-96398e364fbd4548.js +0 -1
  284. package/apps/pwa/out/_next/static/chunks/app/consult/page-28b9448091776c06.js +0 -1
  285. package/apps/pwa/out/_next/static/chunks/app/inside/page-13c7b53e0c2519a9.js +0 -1
  286. package/apps/pwa/out/_next/static/chunks/app/live/page-65e484302921a932.js +0 -1
  287. package/apps/pwa/out/_next/static/chunks/app/loops/page-34d08facaf08f556.js +0 -1
  288. package/apps/pwa/out/_next/static/chunks/app/market/page-955ec2da3be4c560.js +0 -1
  289. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-9f88fbc15d9058d1.js +0 -1
  290. package/apps/pwa/out/_next/static/chunks/app/observatory/page-c9fc5706b8abaa11.js +0 -1
  291. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-8d47f45a0dc044ac.js +0 -1
  292. package/apps/pwa/out/_next/static/chunks/app/outputs/page-e2ee2896811e98d0.js +0 -1
  293. package/apps/pwa/out/_next/static/chunks/app/page-83b19dae8619de05.js +0 -1
  294. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-85f1a1fc0a82738c.js +0 -1
  295. package/apps/pwa/out/_next/static/chunks/app/sessions/page-da6c08aae0f92768.js +0 -1
  296. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-87a11e6a05f15199.js +0 -1
  297. package/apps/pwa/out/_next/static/chunks/app/setup/page-7d4182964407c09c.js +0 -1
  298. package/apps/pwa/out/_next/static/chunks/app/showroom/page-7e8c39241f8eb6d0.js +0 -1
  299. package/apps/pwa/out/_next/static/chunks/app/tasks/page-008c1a96618a4887.js +0 -1
  300. package/apps/pwa/out/_next/static/chunks/app/term/page-f5597e894c02cd33.js +0 -1
  301. package/apps/pwa/out/_next/static/chunks/app/today/page-0337fdf7fb5e37b6.js +0 -1
  302. package/apps/pwa/out/_next/static/chunks/app/vault/page-8fcec4c9ea2e7f70.js +0 -1
  303. package/apps/pwa/out/_next/static/chunks/app/workflows/page-4881b8f9f63694b4.js +0 -1
  304. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-97f9f43172a3a90e.js +0 -1
  305. package/apps/pwa/out/_next/static/css/1f7c958c41ffa3b9.css +0 -1
  306. package/src/launch-head/seat-goal-queue-lock.fixture.ts +0 -15
  307. package/src/launch-head/seat-goal-queue.ts +0 -198
  308. /package/apps/pwa/out/_next/static/{LZcstaO9_NXypPFmujolv → yRA5ur6DcPl0YHSRheH2O}/_buildManifest.js +0 -0
  309. /package/apps/pwa/out/_next/static/{LZcstaO9_NXypPFmujolv → yRA5ur6DcPl0YHSRheH2O}/_ssgManifest.js +0 -0
@@ -4,7 +4,7 @@ import { tmpdir } from 'node:os';
4
4
  import { join, resolve } from 'node:path';
5
5
  import { debug } from '../../src/debug/log.js';
6
6
 
7
- export interface CutBranchOptions { version: string; base: string; pick: string[]; dryRun?: boolean; repoRoot?: string; log?: (line: string) => void }
7
+ export interface CutBranchOptions { version: string; base: string; pick: string[]; append?: boolean; dryRun?: boolean; repoRoot?: string; log?: (line: string) => void }
8
8
 
9
9
  // git-spawn-allow: release branch operations happen only in an isolated temporary worktree; main is never checked out or changed.
10
10
  function git(repo: string, args: string[]): string {
@@ -23,8 +23,8 @@ export function cutReleaseBranch(opts: CutBranchOptions): { branch: string; comm
23
23
  const repo = resolve(opts.repoRoot ?? process.cwd());
24
24
  const branch = `release/${opts.version}`;
25
25
  const log = opts.log ?? console.log;
26
- const report = (event: 'created' | 'refused' | 'conflict', reason?: string, commit?: string) =>
27
- debug.log('release-loop.cut-branch', event, { version: opts.version, branch, ...(commit ? { commit } : {}), ...(reason ? { reason } : {}) });
26
+ const report = (event: 'created' | 'refused' | 'conflict' | 'appended', reason?: string, commit?: string, extra?: Record<string, string | string[]>) =>
27
+ debug.log('release-loop.cut-branch', event, { version: opts.version, branch, ...(commit ? { commit } : {}), ...(reason ? { reason } : {}), ...extra });
28
28
  try {
29
29
  if (!/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test(opts.version)) throw new Error(`invalid release version: ${opts.version}`);
30
30
  if (!opts.pick.length) throw new Error('at least one --pick commit is required');
@@ -37,18 +37,31 @@ export function cutReleaseBranch(opts: CutBranchOptions): { branch: string; comm
37
37
  for (const sha of picks) if (ancestor(repo, sha, base)) throw new Error(`pick ${sha} is already in base ${base}`);
38
38
  if (new Set(picks).size !== picks.length) throw new Error('duplicate --pick commit');
39
39
  for (const sha of picks) if (git(repo, ['rev-list', '--parents', '-n', '1', sha]).split(' ').length !== 2) throw new Error(`pick ${sha} must have exactly one parent`);
40
- if (git(repo, ['ls-remote', '--heads', 'origin', `refs/heads/${branch}`])) throw new Error(`${branch} already exists on origin`);
40
+ const remoteLine = git(repo, ['ls-remote', '--heads', 'origin', `refs/heads/${branch}`]);
41
+ if (remoteLine && !opts.append) throw new Error(`${branch} already exists on origin — use --append to fast-forward more picks onto it`);
42
+ if (!remoteLine && opts.append) throw new Error(`${branch} does not exist on origin — omit --append to create it`);
41
43
  if (spawnSync('git', ['show-ref', '--verify', '--quiet', `refs/heads/${branch}`], { cwd: repo }).status === 0) throw new Error(`${branch} already exists locally`);
42
- const pkg = JSON.parse(git(repo, ['show', `${base}:package.json`])) as { version?: unknown };
43
- if (pkg.version !== opts.version) throw new Error(`base package.json version ${pkg.version} is not ${opts.version}`);
44
- log(`${branch}: ${base} + ${picks.join(' + ')}${opts.dryRun ? ' (dry-run)' : ''}`);
44
+ let remoteTip: string | undefined;
45
+ if (opts.append) {
46
+ const advertised = remoteLine.split(/\s+/)[0] ?? '';
47
+ if (!/^[0-9a-f]{40}$/i.test(advertised)) throw new Error(`${branch} remote tip is not a commit SHA`);
48
+ git(repo, ['fetch', 'origin', `refs/heads/${branch}:refs/remotes/origin/${branch}`]);
49
+ remoteTip = git(repo, ['rev-parse', '--verify', `refs/remotes/origin/${branch}^{commit}`]);
50
+ if (advertised !== remoteTip) throw new Error(`${branch} remote tip moved — fast-forward refused (no force push)`);
51
+ for (const sha of picks) if (ancestor(repo, sha, remoteTip)) throw new Error(`pick ${sha} is already in ${branch}`);
52
+ }
53
+ const versionAt = remoteTip ?? base;
54
+ const pkg = JSON.parse(git(repo, ['show', `${versionAt}:package.json`])) as { version?: unknown };
55
+ if (pkg.version !== opts.version) throw new Error(`${opts.append ? 'release branch' : 'base'} package.json version ${pkg.version} is not ${opts.version}`);
56
+ const start = remoteTip ?? base;
57
+ log(`${branch}: ${start} + ${picks.join(' + ')}${opts.append ? ' (append)' : ''}${opts.dryRun ? ' (dry-run)' : ''}`);
45
58
  if (opts.dryRun) return { branch, commit: null, dryRun: true };
46
59
  const temp = mkdtempSync(join(tmpdir(), 'release-cut-branch-'));
47
60
  const tree = join(temp, 'tree');
48
61
  let added = false;
49
62
  let commit: string | undefined;
50
63
  try {
51
- git(repo, ['worktree', 'add', '--detach', tree, base]);
64
+ git(repo, ['worktree', 'add', '--detach', tree, start]);
52
65
  added = true;
53
66
  for (const sha of picks) {
54
67
  try { git(tree, ['cherry-pick', sha]); }
@@ -68,13 +81,30 @@ export function cutReleaseBranch(opts: CutBranchOptions): { branch: string; comm
68
81
  } finally { rmSync(temp, { recursive: true, force: true }); }
69
82
  }
70
83
  if (!commit) throw new Error('cherry-pick produced no commit');
71
- const push = git(repo, ['push', '--porcelain', `--force-with-lease=refs/heads/${branch}:`, 'origin', `${commit}:refs/heads/${branch}`]);
72
- const newBranch = push.split('\n').some((line) => {
84
+ const lease = opts.append ? remoteTip! : '';
85
+ let push: string;
86
+ try {
87
+ push = git(repo, ['push', '--porcelain', `--force-with-lease=refs/heads/${branch}:${lease}`, 'origin', `${commit}:refs/heads/${branch}`]);
88
+ } catch (error) {
89
+ const message = error instanceof Error ? error.message : String(error);
90
+ if (opts.append && /stale info|rejected|non-fast-forward|failed to push/i.test(message)) {
91
+ throw new Error(`${branch} remote tip moved — fast-forward refused (no force push)`);
92
+ }
93
+ throw error;
94
+ }
95
+ const landed = push.split('\n').some((line) => {
73
96
  const [flag, ref, summary] = line.split('\t');
74
- return flag === '*' && ref?.endsWith(`:refs/heads/${branch}`) && summary === '[new branch]';
97
+ if (flag !== ' ' && flag !== '*') return false;
98
+ if (!ref?.endsWith(`:refs/heads/${branch}`)) return false;
99
+ if (!opts.append) return flag === '*' && summary === '[new branch]';
100
+ const range = summary?.match(/^([0-9a-f]+)\.\.([0-9a-f]+)$/i);
101
+ return flag === ' ' && !!range && remoteTip!.startsWith(range[1]!) && commit.startsWith(range[2]!);
75
102
  });
76
- if (!newBranch) throw new Error(`${branch} already exists on origin (push did not create a new branch)`);
77
- report('created', undefined, commit);
103
+ if (!landed) throw new Error(opts.append
104
+ ? `${branch} remote tip moved — fast-forward refused (no force push)`
105
+ : `${branch} already exists on origin (push did not create a new branch)`);
106
+ if (opts.append) report('appended', undefined, commit, { from: remoteTip!, to: commit, picks });
107
+ else report('created', undefined, commit);
78
108
  log(`${branch} → ${commit}`);
79
109
  return { branch, commit, dryRun: false };
80
110
  } catch (error) {
@@ -1,10 +1,14 @@
1
1
  #!/usr/bin/env bun
2
- import { existsSync, mkdtempSync, rmSync } from 'node:fs';
2
+ import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
3
+ import { effectiveInstanceRoot, releaseLedgerRoot } from '../../src/instance/resolve.js';
4
+ import { listChecklist } from '../../src/release-loop/checklist.js';
5
+ import type { ReleaseManifest } from '../../src/release-loop/manifest.js';
6
+ import { recordDocsFollow, type DocsFollowItem } from './release-docs.js';
3
7
  import { dirname, join } from 'node:path';
4
8
  import { tmpdir } from 'node:os';
5
9
  import { errorResult, finishNode, nodeOutput, readGraphContext, runCommand, type CommandRunner } from './node-verdict.js';
6
10
 
7
- export function runDocsLand(run: CommandRunner = runCommand) {
11
+ export function runDocsLand(run: CommandRunner = runCommand, stateRoot = effectiveInstanceRoot(), ledgerRoot = releaseLedgerRoot()) {
8
12
  const context = readGraphContext();
9
13
  const version = context.input.version;
10
14
  if (context.outputs.publish?.outcome !== 'ok') throw new Error('publish must succeed before docs land');
@@ -14,6 +18,37 @@ export function runDocsLand(run: CommandRunner = runCommand) {
14
18
  const worktree = nodeOutput(context, 'docs', 'worktree');
15
19
  const scratch = dirname(worktree);
16
20
  if (dirname(scratch) !== tmpdir() || !/^release-docs-[A-Za-z0-9]+$/.test(scratch.split('/').at(-1) ?? '') || worktree !== join(scratch, 'tree')) throw new Error('invalid docs worktree');
21
+ const recordFollow = (deployed: boolean) => {
22
+ const releaseDir = join(stateRoot, 'release', version);
23
+ const manifest: ReleaseManifest = JSON.parse(readFileSync(join(releaseDir, 'manifest.json'), 'utf8'));
24
+ if (manifest.version !== version || !Array.isArray(manifest.in)) throw new Error('docs-follow manifest/version mismatch');
25
+ // MK attestations are independent of generated notes; missing proof is unmeasured.
26
+ const evidencePath = join(releaseDir, 'docs-follow-evidence.json');
27
+ // 증거 파일이 깨졌으면 «증거 없음»으로 센다 — 보고서·MK 대기열은 그래도 남긴다(미측정으로).
28
+ let raw: unknown = { items: [] };
29
+ try { if (existsSync(evidencePath)) raw = JSON.parse(readFileSync(evidencePath, 'utf8')); }
30
+ catch (error) { process.stderr.write(`docs-follow evidence unreadable — counted as no evidence: ${String(error)}\n`); }
31
+ const valid = !!raw && typeof raw === 'object' && 'items' in raw && Array.isArray((raw as { items: unknown }).items);
32
+ if (!valid) process.stderr.write('docs-follow evidence malformed — counted as no evidence\n');
33
+ // 항목도 하나씩 본다 — 객체이고 문자열 id 가 있는 것만 증거로 센다(나머지는 «증거 없음»).
34
+ const evidence: { items: unknown[] } = { items: valid ? (raw as { items: unknown[] }).items
35
+ .filter((item) => !!item && typeof item === 'object' && typeof (item as { id?: unknown }).id === 'string') : [] };
36
+ const checklist = listChecklist(version, ledgerRoot);
37
+ const attestations = new Map((evidence.items as DocsFollowItem[]).map((item) => [item.id, item]));
38
+ const items = checklist.items.map((item) => ({ ...attestations.get(item.id), id: item.id, status: item.status,
39
+ ...(deployed ? {} : { docs: undefined }) }));
40
+ const publicationFile = join(ledgerRoot, 'release', version, 'release.json');
41
+ const publication: unknown = existsSync(publicationFile) ? JSON.parse(readFileSync(publicationFile, 'utf8')) : null;
42
+ const publishedAt = publication && typeof publication === 'object' && 'publishedAt' in publication
43
+ && typeof publication.publishedAt === 'string' ? publication.publishedAt : undefined;
44
+ const follow = recordDocsFollow(stateRoot, manifest, items, new Date(), publishedAt);
45
+ return { pending: follow.pending.length, unassessed: follow.unassessed.length, ratio: follow.ratio, verdict: follow.verdict };
46
+ };
47
+ // The deployment error is authoritative even if the follow-up ledger cannot be written.
48
+ const recordFailure = () => {
49
+ try { return recordFollow(false); }
50
+ catch (error) { process.stderr.write(`docs-follow recording failed: ${String(error)}\n`); return undefined; }
51
+ };
17
52
  // 재시도 멱등: 노트가 이미 main 에 있으면(앞 판에서 머지됐고 배포만 실패) 착지를 건너뛰고 배포부터.
18
53
  const before = run('git', ['fetch', 'origin', 'main']);
19
54
  if (before.status !== 0) throw new Error(`docs landing fetch failed: ${before.stderr}`);
@@ -22,16 +57,20 @@ export function runDocsLand(run: CommandRunner = runCommand) {
22
57
  if (!existsSync(worktree)) throw new Error(`docs worktree missing and notes not on main: ${worktree}`);
23
58
  const land = run('bun', ['bin/elanous.mjs', 'pr', 'land', '--cwd', worktree]);
24
59
  if (land.stderr) process.stderr.write(land.stderr);
25
- if (land.status === 1) return { outcome: 'fail' as const, verdict: 'fail' as const, summary: `docs land failed: ${land.stderr || land.stdout}` };
60
+ if (land.status === 1) return { outcome: 'fail' as const, verdict: 'fail' as const, summary: `docs land failed: ${land.stderr || land.stdout}`, docsFollow: recordFailure() };
26
61
  if (land.status !== 0 || !land.stdout.includes('✓ merge:')) throw new Error(`docs land incomplete (rc=${land.status}): ${land.stderr || land.stdout}`);
27
62
  }
63
+ // 착지(병합)는 이미 끝났다 — 이 뒤의 모든 실패는 후속 기록을 시도하고 원래 오류를 그대로 던진다.
28
64
  const fetch = run('git', ['fetch', 'origin', 'main']);
29
- if (fetch.status !== 0) throw new Error(`docs landing fetch failed: ${fetch.stderr}`);
65
+ if (fetch.status !== 0) { recordFailure(); throw new Error(`docs landing fetch failed: ${fetch.stderr}`); }
30
66
  const merged = run('git', ['rev-parse', 'origin/main']);
31
- if (merged.status !== 0 || !/^[0-9a-f]{40}\s*$/i.test(merged.stdout)) throw new Error(`merged main commit unavailable: ${merged.stderr}`);
32
- const deployDir = mkdtempSync(join(tmpdir(), 'release-docs-deploy-'));
67
+ if (merged.status !== 0 || !/^[0-9a-f]{40}\s*$/i.test(merged.stdout)) { recordFailure(); throw new Error(`merged main commit unavailable: ${merged.stderr}`); }
68
+ let deployDir: string;
69
+ try { deployDir = mkdtempSync(join(tmpdir(), 'release-docs-deploy-')); }
70
+ catch (error) { recordFailure(); throw error; }
33
71
  const deployTree = join(deployDir, 'tree');
34
72
  let added = false;
73
+ let deployed = false;
35
74
  try {
36
75
  const add = run('git', ['worktree', 'add', '--detach', deployTree, merged.stdout.trim()]);
37
76
  if (add.status !== 0) throw new Error(`docs deploy worktree failed: ${add.stderr}`);
@@ -40,21 +79,41 @@ export function runDocsLand(run: CommandRunner = runCommand) {
40
79
  if (install.status !== 0) throw new Error(`docs deploy install failed: ${install.stderr}`);
41
80
  const deploy = run('bun', ['website/scripts/deploy-pages.ts', '--remote', 'node-b', '--yes'], deployTree);
42
81
  if (deploy.stderr) process.stderr.write(deploy.stderr);
43
- if (deploy.status === 1) return { outcome: 'fail' as const, verdict: 'fail' as const, summary: `docs deploy failed: ${deploy.stderr || deploy.stdout}` };
82
+ if (deploy.status === 1) return { outcome: 'fail' as const, verdict: 'fail' as const,
83
+ summary: `docs deploy failed: ${deploy.stderr || deploy.stdout}`, docsFollow: recordFailure() };
44
84
  if (deploy.status !== 0) throw new Error(`docs deploy incomplete (rc=${deploy.status}): ${deploy.stderr || deploy.stdout}`);
85
+ deployed = true;
45
86
  if (existsSync(worktree)) {
46
87
  const remove = run('git', ['worktree', 'remove', '--force', worktree]);
47
88
  if (remove.status !== 0) throw new Error(`docs worktree cleanup failed: ${remove.stderr}`);
48
89
  }
49
90
  rmSync(scratch, { recursive: true, force: true });
50
- return { outcome: 'ok' as const, verdict: 'pass' as const, summary: `docs landed and deployed v${version}` };
91
+ // 배포 성공 — 후속 기록 실패(manifest 없음 등)는 결과를 «실패»로 바꾸지 않는다. 알리고 넘어간다.
92
+ let follow: ReturnType<typeof recordFollow> | undefined;
93
+ try { follow = recordFollow(true); }
94
+ catch (error) { process.stderr.write(`docs-follow recording failed after deploy: ${String(error)}\n`); }
95
+ return { outcome: 'ok' as const, verdict: 'pass' as const,
96
+ summary: `docs landed and deployed v${version}${follow ? ` · docs-follow pending=${follow.pending} unassessed=${follow.unassessed}` : ' · docs-follow not recorded'}`,
97
+ docsFollow: follow };
98
+ } catch (error) {
99
+ if (!deployed) recordFailure();
100
+ else {
101
+ // 배포는 됐는데 정리에서 실패 — 보고서·MK 대기열은 «배포됨»으로 남기고 원래 오류를 보존한다.
102
+ try { recordFollow(true); }
103
+ catch (recordError) { process.stderr.write(`docs-follow recording failed: ${String(recordError)}\n`); }
104
+ }
105
+ throw error;
51
106
  } finally {
52
107
  try {
53
108
  if (added) {
54
109
  const remove = run('git', ['worktree', 'remove', '--force', deployTree]);
55
- if (remove.status !== 0) throw new Error(`docs deploy cleanup failed: ${remove.stderr}`);
110
+ // 배포 임시 트리 정리 실패는 앞선 결과(반환·원래 오류)를 덮지 않는다 — 알리고 넘어간다.
111
+ if (remove.status !== 0) process.stderr.write(`docs deploy cleanup failed (kept the original result): ${remove.stderr}\n`);
56
112
  }
57
- } finally { rmSync(deployDir, { recursive: true, force: true }); }
113
+ } finally {
114
+ try { rmSync(deployDir, { recursive: true, force: true }); }
115
+ catch (error) { process.stderr.write(`docs deploy temp cleanup failed (kept the original result): ${String(error)}\n`); }
116
+ }
58
117
  }
59
118
  }
60
119
 
@@ -242,8 +242,10 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
242
242
  // GNU timeout signals its own process group: -s KILL would kill timeout too (ssh 255), so TERM first then KILL after 10s (124 / 137).
243
243
  // No coreutils timeout on the remote → run unlimited there and rely on the local ssh deadline.
244
244
  const limitPrefix = limitMs ? `$(command -v timeout >/dev/null 2>&1 && echo 'timeout -k 10 ${Math.ceil(limitMs / 1000)}') ` : '';
245
+ // Start in /tmp, not the caller checkout. macOS resolves that symlink to /private/tmp before the child sees it;
246
+ // the remote command still `cd`s to the quoted path, so either spelling is the same directory.
245
247
  const run = spawnSync('ssh', [remote, `PATH=$HOME/.bun/bin:/opt/homebrew/bin:$PATH; export PATH; cd ${quote(cwd)} && ${limitPrefix}${[cmd, ...args].map(quote).join(' ')}`],
246
- { encoding: 'utf8', maxBuffer: 128 * 1024 * 1024, ...(limitMs ? { timeout: limitMs + 60_000, killSignal: 'SIGKILL' as const } : {}) });
248
+ { cwd: '/tmp', encoding: 'utf8', maxBuffer: 128 * 1024 * 1024, ...(limitMs ? { timeout: limitMs + 60_000, killSignal: 'SIGKILL' as const } : {}) });
247
249
  const timedOut = !!limitMs && (run.status === 124 || run.status === 137 || (run.error as NodeJS.ErrnoException | undefined)?.code === 'ETIMEDOUT');
248
250
  return { rc: run.status ?? 2, output: `${run.stdout ?? ''}\n${run.stderr ?? ''}${run.error ? `\n${run.error}` : ''}`, ...(timedOut ? { timedOut } : {}) };
249
251
  }
@@ -36,7 +36,7 @@ export function waitForAssets(run: CommandRunner, repo: string, tag: string, nam
36
36
  }
37
37
  }
38
38
 
39
- export function runPublish(run: CommandRunner = runCommand) {
39
+ export function runPublish(run: CommandRunner = runCommand, prodFreezeRoot?: string) {
40
40
  const context = readGraphContext();
41
41
  const version = context.input.version;
42
42
  // A measured automatic decision or the existing human approval must authorize publication.
@@ -68,7 +68,7 @@ export function runPublish(run: CommandRunner = runCommand) {
68
68
  writeFileSync(file, body);
69
69
  // Same in-flight marker as merges: `freeze on` waits for a publication that already passed this check.
70
70
  // Unlike a merge, a frozen publication is not queued: it fails, and the release run is run again after `freeze off`.
71
- const landing = beginLandingMerge();
71
+ const landing = beginLandingMerge(undefined, undefined, prodFreezeRoot);
72
72
  const frozen = landing.frozen;
73
73
  if (frozen) {
74
74
  landing.end(); // already released by beginLandingMerge when frozen; kept explicit so no path leaves a marker
@@ -1,6 +1,140 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { existsSync, mkdirSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { appendSeatRequestRows, listSeatRequests, withSeatRequestLedgerLock } from '../../src/seat-dispatch/seat-request-ledger.js';
1
5
  import { debug } from '../../src/debug/log.js';
2
6
  import type { ReleaseManifest } from '../../src/release-loop/manifest.js';
3
7
 
8
+ type DocsFollowEvidence = { location: string; verifiedAt: string; publishedAt: string };
9
+ type DocsFollowExposure = {
10
+ verdict: 'public' | 'beta' | 'internal';
11
+ judge: 'MK';
12
+ rubric: string;
13
+ verifiedAt: string;
14
+ };
15
+ export type DocsFollowItem = {
16
+ id: string;
17
+ status: string;
18
+ sha?: string;
19
+ exposure?: DocsFollowExposure;
20
+ docs?: DocsFollowEvidence;
21
+ homepage?: { elanous: DocsFollowEvidence; elanvital: DocsFollowEvidence };
22
+ readme?: DocsFollowEvidence;
23
+ };
24
+ type DocsFollowCoverage = {
25
+ id: string;
26
+ sha: string | null;
27
+ docs: boolean;
28
+ homepage: boolean;
29
+ readme: boolean;
30
+ missing: Array<'docs' | 'homepage' | 'readme'>;
31
+ };
32
+ type DocsFollowReport = {
33
+ coverage: DocsFollowCoverage[];
34
+ pending: DocsFollowCoverage[];
35
+ unassessed: string[];
36
+ ratio: number | null;
37
+ verdict: 'pass' | 'fail' | 'unmeasured';
38
+ };
39
+
40
+ /** An attestation is not coverage unless it points to a measured, timestamped surface. */
41
+ function evidenced(value: DocsFollowEvidence | undefined, measuredAt?: string): boolean {
42
+ const at = value?.verifiedAt ? Date.parse(value.verifiedAt) : NaN;
43
+ const published = value?.publishedAt ? Date.parse(value.publishedAt) : NaN;
44
+ return !!value?.location?.trim() && Number.isFinite(at) && Number.isFinite(published)
45
+ && /(?:Z|[+-]\d\d:\d\d)$/.test(value.verifiedAt)
46
+ && /(?:Z|[+-]\d\d:\d\d)$/.test(value.publishedAt)
47
+ && published <= at && (measuredAt === undefined || at <= Date.parse(measuredAt));
48
+ }
49
+
50
+ /** Unknown exposure/landing never becomes a published claim; it remains visible as an unassessed item. */
51
+ export function calculateDocsFollow(manifest: ReleaseManifest, items: readonly DocsFollowItem[], publishedAt?: string, measuredAt?: string): DocsFollowReport {
52
+ const landed = new Set(manifest.in.map((entry) => entry.sha).filter(Boolean));
53
+ const green = items.filter((item) => item.status === 'green');
54
+ const ids = new Set<string>();
55
+ for (const item of items) {
56
+ if (!item.id?.trim() || ids.has(item.id)) throw new Error('invalid or duplicate docs-follow item');
57
+ ids.add(item.id);
58
+ }
59
+ const judged = (item: DocsFollowItem) => item.exposure?.judge === 'MK'
60
+ && !!item.exposure.rubric?.trim() && evidenced({ location: item.exposure.rubric, verifiedAt: item.exposure.verifiedAt,
61
+ publishedAt: item.exposure.verifiedAt }, measuredAt)
62
+ && ['public', 'beta', 'internal'].includes(item.exposure.verdict);
63
+ const unassessed = green.filter((item) => !judged(item) || !item.sha || !landed.has(item.sha))
64
+ .map((item) => item.id);
65
+ const supplied = new Set(items.map((item) => item.sha));
66
+ for (const entry of manifest.in) {
67
+ if (entry.sha && (entry.kind === 'feat' || entry.kind === 'fix' || entry.kind === 'security') && entry.line && !supplied.has(entry.sha)) {
68
+ unassessed.push(`landing:${entry.sha}`);
69
+ }
70
+ }
71
+ const visible = green.filter((item) => judged(item) && (item.exposure?.verdict === 'public' || item.exposure?.verdict === 'beta') && item.sha && landed.has(item.sha));
72
+ const coverage: DocsFollowCoverage[] = visible.map((item) => {
73
+ const docs = evidenced(item.docs, measuredAt);
74
+ const homepage = evidenced(item.homepage?.elanous, measuredAt) && evidenced(item.homepage?.elanvital, measuredAt);
75
+ const readme = evidenced(item.readme, measuredAt);
76
+ return { id: item.id, sha: item.sha!, docs, homepage, readme,
77
+ missing: ([['docs', docs], ['homepage', homepage], ['readme', readme]] as const)
78
+ .filter(([, present]) => !present).map(([surface]) => surface) };
79
+ });
80
+ const pending = coverage.filter((item) => !item.docs || !item.homepage);
81
+ const covered = coverage.filter((item) => item.docs && item.homepage && item.readme).length;
82
+ const start = publishedAt ? Date.parse(publishedAt) : NaN;
83
+ const end = measuredAt ? Date.parse(measuredAt) : NaN;
84
+ const deadline = start + 24 * 60 * 60 * 1000;
85
+ const validWindow = Number.isFinite(start) && Number.isFinite(end) && end >= start
86
+ && /(?:Z|[+-]\d\d:\d\d)$/.test(publishedAt ?? '')
87
+ && /(?:Z|[+-]\d\d:\d\d)$/.test(measuredAt ?? '');
88
+ const atDeadline = validWindow && end > deadline
89
+ ? calculateDocsFollow(manifest, items, publishedAt, new Date(deadline).toISOString()) : null;
90
+ const timely = atDeadline ?? { coverage, pending, unassessed,
91
+ ratio: coverage.length && !unassessed.length ? covered / coverage.length : null };
92
+ const pass = validWindow && timely.coverage.length > 0 && !timely.unassessed.length
93
+ && timely.ratio !== null && timely.ratio >= 0.8 && !timely.pending.length;
94
+ // Missing proof may still establish timely coverage; only a verified late publication rules it out.
95
+ const lateProof = (proof: DocsFollowEvidence | undefined) => evidenced(proof, measuredAt)
96
+ && Date.parse(proof!.publishedAt) > deadline;
97
+ const impossible = validWindow && end > deadline && !pass && !timely.unassessed.length
98
+ && visible.length > 0 && (
99
+ visible.some((item) => lateProof(item.docs) || lateProof(item.homepage?.elanous) || lateProof(item.homepage?.elanvital))
100
+ || visible.filter((item) => ![item.docs, item.homepage?.elanous, item.homepage?.elanvital, item.readme]
101
+ .some(lateProof)).length / visible.length < 0.8
102
+ );
103
+ return { coverage, pending, unassessed,
104
+ ratio: coverage.length && !unassessed.length ? covered / coverage.length : null,
105
+ verdict: pass ? 'pass' as const : impossible ? 'fail' as const : 'unmeasured' as const };
106
+ }
107
+
108
+ /** Persist a per-release report and queue each missing docs/home item once for MK. */
109
+ export function recordDocsFollow(root: string, manifest: ReleaseManifest, items: readonly DocsFollowItem[], now: Date = new Date(), publishedAt?: string) {
110
+ if (!Number.isFinite(now.getTime())) throw new Error('invalid docs-follow time');
111
+ if (!/^\d+\.\d+\.\d+$/.test(manifest.version)) throw new Error('invalid docs-follow version');
112
+ const report = calculateDocsFollow(manifest, items, publishedAt, now.toISOString());
113
+ const path = join(root, 'release', manifest.version, 'docs-follow.json');
114
+ const journal = join(root, 'seat-requests', 'requests.jsonl');
115
+ withSeatRequestLedgerLock(journal, () => {
116
+ const known = new Set(listSeatRequests(root).map((row) => row.key));
117
+ const missing = [
118
+ ...report.pending.map((item) => ({ id: item.id, text: `문서 반영 대기 목록 v${manifest.version} · ${item.id}: ${item.missing.join(', ')} — 착지 ${item.sha}; docs·홈 실측 뒤 닫기` })),
119
+ ...report.unassessed.map((id) => ({ id, text: `문서 반영 대기 목록 v${manifest.version} · ${id}: 착지·green·EXPOSE-RUBRIC 근거 미판정 — 확인 전 공개 문면 금지` })),
120
+ ];
121
+ const rows = missing.filter((item) => !known.has(`docs-follow:${manifest.version}:${item.id}`)).map((item) => ({
122
+ key: `docs-follow:${manifest.version}:${item.id}`, seat: 'MK', status: 'queued' as const,
123
+ text: item.text, queuedAt: now.toISOString(), ref: `release:${manifest.version}:docs-follow`,
124
+ }));
125
+ mkdirSync(join(root, 'release', manifest.version), { recursive: true });
126
+ const temp = `${path}.${createHash('sha256').update(now.toISOString()).digest('hex').slice(0, 12)}.tmp`;
127
+ try {
128
+ writeFileSync(temp, JSON.stringify({ version: manifest.version, measuredAt: now.toISOString(), ...report }, null, 2) + '\n');
129
+ renameSync(temp, path);
130
+ } finally {
131
+ if (existsSync(temp)) unlinkSync(temp);
132
+ }
133
+ appendSeatRequestRows(journal, rows);
134
+ });
135
+ return report;
136
+ }
137
+
4
138
  /** Render public IN lines only; keep private titles and PR numbers out of release notes. */
5
139
  export function renderReleaseNotes(manifest: ReleaseManifest, version: string): string {
6
140
  const sections = [
@@ -215,11 +215,28 @@ function landingClause(text: string): string {
215
215
  // A candidate with broken punctuation is not repaired by deleting its unmatched character.
216
216
  if (!balanced(text)) return '';
217
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);
218
+ const trimmed = text.replace(/^-\s+/, '');
219
+ // Release bookkeeping (version bumps, «release 0.2.14» notes) is never this release's headline (0.2.15 real note).
220
+ if (/^(?:version|release)\s*:/i.test(trimmed) || /^(?:docs|chore)(?:\([^)]*\))?\s*:\s*release\s+v?\d/i.test(trimmed)) return '';
221
+ // Commit subjects lead with a conventional prefix («docs(marketing): …»); the headline is the change itself.
222
+ const body = trimmed.replace(/^(feat|fix|internal|docs|test|perf|refactor|chore|ops|security|breaking)\s+[—–-]\s+/i, '')
223
+ .replace(/^[a-z]+(?:\([^)]*\))?!?:\s+/i, '');
224
+ const clause = shortClause(firstClause(body));
220
225
  return balanced(clause) ? clause : '';
221
226
  }
222
227
 
228
+ /** A headline clause longer than 60 characters is cut at its first comma/semicolon, else at a word boundary with «…». */
229
+ function shortClause(clause: string): string {
230
+ const chars = [...clause];
231
+ if (chars.length <= 60) return clause;
232
+ const head = chars.slice(0, 60).join('');
233
+ // The first comma/semicolon after a minimal 10-character lead ends the clause.
234
+ const stop = head.slice(10).search(/[,;,;]/);
235
+ if (stop >= 0) return head.slice(0, 10 + stop);
236
+ const space = head.lastIndexOf(' ');
237
+ return `${(space >= 20 ? head.slice(0, space) : head).trimEnd()}…`;
238
+ }
239
+
223
240
  function versionKey(value: string): number[] | null {
224
241
  const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(value);
225
242
  return match ? match.slice(1).map(Number) : null;
@@ -64,7 +64,7 @@ function shippedNextMdLines(repo: string, cut: string | undefined): { lines: Set
64
64
  return { lines: new Set(shown.stdout.split('\n').map((line) => line.trim()).filter((line) => line.startsWith('- '))) };
65
65
  }
66
66
 
67
- function landing(repo: string, kind: Kind, releaseVersion: string, cut?: string, cutCommit?: string): Output {
67
+ function landing(repo: string, kind: Kind, releaseVersion: string, cut?: string, cutCommit?: string, forceFreeze = false): Output {
68
68
  const target = kind === 'release' ? releaseVersion : nextDevVersion(releaseVersion);
69
69
  const source = kind === 'release' ? `${releaseVersion}-dev.N` : releaseVersion;
70
70
  fetchMain(repo);
@@ -183,7 +183,8 @@ function landing(repo: string, kind: Kind, releaseVersion: string, cut?: string,
183
183
  }
184
184
  }
185
185
  const title = kind === 'release' ? `release: ${target}` : `version: ${target}`;
186
- const land = spawnSync('bun', ['bin/elanous.mjs', 'pr', 'land', '--commit-message', title, '--title', title, '--body', `Set package.json version to ${target}.`], {
186
+ const land = spawnSync('bun', ['bin/elanous.mjs', 'pr', 'land', '--commit-message', title, '--title', title, '--body', `Set package.json version to ${target}.`,
187
+ ...(forceFreeze ? ['--force-freeze', `release run version bump ${target}`] : [])], {
187
188
  cwd: worktree, encoding: 'utf8', maxBuffer: 16 * 1024 * 1024,
188
189
  });
189
190
  const transcript = `${land.stdout ?? ''}\n${land.stderr ?? ''}`;
@@ -211,6 +212,7 @@ function main(args: string[] = process.argv.slice(2), repo = process.cwd()): Out
211
212
  let version: string | null = null;
212
213
  let cut: string | undefined;
213
214
  let cutCommit: string | undefined;
215
+ let forceFreeze = false;
214
216
  try {
215
217
  if (args[0] !== 'release' && args[0] !== 'dev-bump') throw new Error('usage: version-node.ts <release|dev-bump> [--version <v>] [--cut <sha>] [--cut-commit <sha>] --json');
216
218
  kind = args[0];
@@ -224,8 +226,9 @@ function main(args: string[] = process.argv.slice(2), repo = process.cwd()): Out
224
226
  if (process.env.ELANOUS_GRAPH_CONTEXT) {
225
227
  const location = process.env.ELANOUS_GRAPH_CONTEXT;
226
228
  const context = JSON.parse(location.trimStart().startsWith('{') ? location : readFileSync(location, 'utf8')) as {
227
- input?: { version?: unknown; cutCommit?: unknown }; outputs?: Record<string, { commit?: unknown } | undefined> };
229
+ input?: { version?: unknown; cutCommit?: unknown; forceFreeze?: unknown }; outputs?: Record<string, { commit?: unknown } | undefined> };
228
230
  if (!version && typeof context.input?.version === 'string') version = context.input.version;
231
+ forceFreeze = context.input?.forceFreeze === true;
229
232
  if (kind === 'release' && cutCommit === undefined && context.input?.cutCommit !== undefined) {
230
233
  if (typeof context.input.cutCommit !== 'string') throw new Error('cutCommit must be a commit SHA');
231
234
  cutCommit = context.input.cutCommit;
@@ -237,7 +240,7 @@ function main(args: string[] = process.argv.slice(2), repo = process.cwd()): Out
237
240
  if (!version) throw new Error('version required (--version or ELANOUS_GRAPH_CONTEXT.input.version)');
238
241
  nextDevVersion(version);
239
242
  if (kind === 'dev-bump' && cutCommit !== undefined) throw new Error('--cut-commit is only valid for release');
240
- return landing(resolve(repo), kind, version, cut, cutCommit);
243
+ return landing(resolve(repo), kind, version, cut, cutCommit, forceFreeze);
241
244
  } catch (error) {
242
245
  const message = error instanceof Error ? error.message : String(error);
243
246
  const worktree = /\[worktree: ([^\]]+)\]$/.exec(message)?.[1];
@@ -86,18 +86,25 @@ function manualTexts(dir: string): string[] {
86
86
  return texts;
87
87
  }
88
88
 
89
- function linkedClaims(stateDir: string, version: string, ids: Set<string>): { sentences: string[]; error?: string } {
89
+ type StoryClaim = { id: string; sentence: string; command: string | null; firstPublicCandidate: boolean };
90
+
91
+ function linkedClaims(stateDir: string, version: string, ids: Set<string>): { claims: StoryClaim[]; error?: string } {
90
92
  const ledger = new ClaimsLedger({ stateDir });
91
93
  try {
92
- if (!existsSync(stateDir)) return { sentences: [], error: '상태 경로 없음' };
93
- if (!statSync(stateDir).isDirectory()) return { sentences: [], error: '상태 경로가 폴더가 아님' };
94
- if (!existsSync(ledger.path)) return { sentences: [], error: '원장 없음' };
95
- const sentences = [...ledger.list({ status: 'verified' }), ...ledger.list({ status: 'public' })]
96
- .filter((row) => ledger.get(row.id).links.some((link) => link.version === version && ids.has(link.cell)))
97
- .map((row) => row.claim);
98
- return { sentences };
94
+ if (!existsSync(stateDir)) return { claims: [], error: '상태 경로 없음' };
95
+ if (!statSync(stateDir).isDirectory()) return { claims: [], error: '상태 경로가 폴더가 아님' };
96
+ if (!existsSync(ledger.path)) return { claims: [], error: '원장 없음' };
97
+ const claims = [...ledger.list({ status: 'verified' }), ...ledger.list({ status: 'public' })]
98
+ .map((row): StoryClaim | null => {
99
+ const detail = ledger.get(row.id);
100
+ if (!detail.links.some((link) => link.version === version && ids.has(link.cell))) return null;
101
+ return { id: row.id, sentence: row.claim, command: detail.evidence.at(-1)?.command ?? null,
102
+ firstPublicCandidate: !detail.history.some((entry) => entry.event === 'publish')
103
+ && !detail.links.some((link) => link.version !== version && link.version !== null) };
104
+ }).filter((claim): claim is StoryClaim => claim !== null);
105
+ return { claims };
99
106
  } catch (error) {
100
- return { sentences: [], error: error instanceof Error ? error.message : String(error) };
107
+ return { claims: [], error: error instanceof Error ? error.message : String(error) };
101
108
  }
102
109
  }
103
110
 
@@ -117,15 +124,34 @@ export function draftReleaseStory(options: DraftOptions): DraftResult {
117
124
  const candidates = green.filter((item) => !mentions.some((text) => text.includes(item.id)));
118
125
  const evidence = linkedClaims(options.stateDir ?? effectiveInstanceRoot(), version, new Set(publicCells.map((item) => item.id)));
119
126
  const changes = [...userLines, ...publicCells.map((item) => firstPhrase(item.title))];
127
+ const numeric = evidence.claims.find((claim) => claim.firstPublicCandidate && /\d/.test(claim.sentence) && claim.command);
128
+ const kindPattern = /\bnew\b|새로운|신규|새 종류/i;
129
+ const kindFromNext = userLines.find((line) => kindPattern.test(line));
130
+ const kindFromCell = publicCells.map((item) => firstPhrase(item.title)).find((line) => kindPattern.test(line));
131
+ const kindFromClaim = evidence.claims.find((claim) => kindPattern.test(claim.sentence));
132
+ const newKind = kindFromNext ? { text: kindFromNext, source: 'release/next.md', command: null }
133
+ : kindFromCell ? { text: kindFromCell, source: 'green 칸', command: null }
134
+ : kindFromClaim ? { text: kindFromClaim.sentence, source: `소구점 ${kindFromClaim.id}`, command: kindFromClaim.command } : null;
135
+ const card = numeric ? { kind: '첫 공개 숫자 후보', text: numeric.sentence, source: `소구점 ${numeric.id}`, command: numeric.command }
136
+ : newKind ? { kind: '새 종류 후보', ...newKind } : null;
120
137
  const announcement = [
121
- `# ${version} 판 공지 초안`, '', '## 무엇이 달라졌나',
138
+ `# ${version} 판 공지 초안`, '', '## 대표 카드 · CMO 확인 전',
139
+ ...(card ? [`- ${card.kind}: ${card.text}`, `- 출처: ${card.source}`, `- 재측정: ${card.command ?? '확인 명령 없음 — 게시 전 확인'}`]
140
+ : ['- 후보 없음 — CMO가 이번 판의 대표 문장을 고른다']),
141
+ '- 첫 공개 여부·새 종류 여부는 앞 판의 공개 문장과 대조한 뒤 CMO가 확정한다.',
142
+ '', '## 무엇이 달라졌나',
122
143
  ...(changes.length ? changes.map((line) => `- ${line}`) : ['- 사용자 쪽 변경 없음']),
123
144
  '', '## 근거',
124
- ...(evidence.error ? [`근거: 소구점 원장 못 읽음(${evidence.error})`] : evidence.sentences.length ? evidence.sentences.map((claim) => `- ${claim}`) : ['근거: 연결된 소구점 없음']),
145
+ ...(evidence.error ? [`근거: 소구점 원장 못 읽음(${evidence.error})`] : evidence.claims.length ? evidence.claims.map((claim) => `- ${claim.sentence}`) : ['근거: 연결된 소구점 없음']),
146
+ '', '## CMO 게시 판단 · 초안은 게시가 아니다',
147
+ '- 착지·출처: release/next.md와 green 칸의 공개 상태, 연결된 검증 소구점을 대조한다. 근거가 없거나 원장을 못 읽었으면 보류한다.',
148
+ '- 사실·톤: 숫자는 근거 명령으로 재측정하고 첫 공개·새 종류는 앞 판 공개 문장과 대조한다. 브랜드 규칙과 금지 표현을 확인한다.',
149
+ '- 채널·결정: 공지·사이트·매뉴얼 문장의 대상과 영어 원문을 대조하고 CMO가 게시/수정/보류와 이유를 기록한다. 실제 바깥 게시는 역할 파일의 사람 결정 관문을 따른다.',
125
150
  '',
126
151
  ].join('\n');
127
152
  const siteChanges = [...userLines, ...publicCells.map((item) => firstPhrase(item.title)).filter((title) => !/[가-힣]/.test(title))];
128
- const siteNews = [`# What's new in ${version}`, '', ...siteChanges.slice(0, 3).map((line) => `- ${line}`), '', `Release: ${version}`, ''].join('\n');
153
+ const siteCard = card && !/[가-힣]/.test(card.text) ? [`## Lead card candidate (CMO review required)`, card.text, ''] : [];
154
+ const siteNews = [`# What's new in ${version}`, '', ...siteCard, ...siteChanges.slice(0, 3).map((line) => `- ${line}`), '', `Release: ${version}`, ''].join('\n');
129
155
  const manual = [`# ${version} 매뉴얼 갱신 후보`, '', ...candidates.map((item: ChecklistItem) => `- ${item.id} · ${firstPhrase(item.title)} · 매뉴얼 언급 없음`), ''].join('\n');
130
156
  const outDir = options.outDir ?? join(effectiveInstanceRoot(), 'release', version, 'story');
131
157
  mkdirSync(outDir, { recursive: true });
@@ -150,7 +176,7 @@ export function draftReleaseStory(options: DraftOptions): DraftResult {
150
176
  files.push(file);
151
177
  }
152
178
  const result: DraftResult = { version, status: 'drafted', userLines: userLines.length, internalDropped, greenCells: green.length,
153
- claims: evidence.sentences.length, manualCandidates: candidates.length, brandFindings, ...(brandUnmeasured ? { brandUnmeasured } : {}), files };
179
+ claims: evidence.claims.length, manualCandidates: candidates.length, brandFindings, ...(brandUnmeasured ? { brandUnmeasured } : {}), files };
154
180
  debug.log('release.story', 'drafted', { version, userLines: result.userLines, internalDropped, greenCells: result.greenCells,
155
181
  claims: result.claims, manualCandidates: result.manualCandidates, brandFindings, ...(brandUnmeasured ? { brandUnmeasured } : {}) });
156
182
  return result;
@@ -161,7 +187,9 @@ export function runReleaseStory(context: GraphContext = readGraphContext(), draf
161
187
  try {
162
188
  if (context.outputs.publish?.outcome !== 'ok' || context.outputs.verify?.outcome !== 'ok')
163
189
  throw new Error('published release and verification required');
164
- const result = draft({ version });
190
+ // 판 노트 원천은 기본이 저장소의 release/next.md — 시험·재현은 env 로 다른 파일을 준다(실제 next.md 는 판마다 비워진다).
191
+ const nextPath = process.env.ELANOUS_RELEASE_NEXT_PATH?.trim();
192
+ const result = draft({ version, ...(nextPath ? { nextPath } : {}) });
165
193
  if (result.status === 'drafted') {
166
194
  const root = effectiveInstanceRoot();
167
195
  const requestsPath = join(root, 'seat-requests', 'requests.jsonl');