elanous 0.2.13 → 0.2.14

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 (445) 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/{7334.a6679d9168f37aa8.js → 1268.0840ff91e083fcbd.js} +1 -1
  4. package/apps/pwa/out/_next/static/chunks/1307-6e7e0377c1db0bdc.js +1 -0
  5. package/apps/pwa/out/_next/static/chunks/1826-5d279da64bbbc2de.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/{9656-c1540088f5859c80.js → 1844-9ec4b1d12f6e80eb.js} +1 -1
  7. package/apps/pwa/out/_next/static/chunks/1866-e7a6999f25548b30.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/{390.c373e31b3b9d0c41.js → 1948.15f356b15a625597.js} +2 -2
  9. package/apps/pwa/out/_next/static/chunks/211-9c9090c59e2c5907.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/2196-4700653d12a44b40.js +1 -0
  11. package/apps/pwa/out/_next/static/chunks/224-798346143007ff65.js +1 -0
  12. package/apps/pwa/out/_next/static/chunks/2699-63b73fa4d3bcf6ff.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/2957-96ffab3d521fb08f.js +1 -0
  14. package/apps/pwa/out/_next/static/chunks/311.e685738f92e6e2ec.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/3194-807b212fb8eae065.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/3324-d1d2979821dfa845.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/3558-bbb5ab553a0471d3.js +1 -0
  18. package/apps/pwa/out/_next/static/chunks/3916-3d02dd1fc257dd3c.js +1 -0
  19. package/apps/pwa/out/_next/static/chunks/4144-a0041c8dc3be57ac.js +1 -0
  20. package/apps/pwa/out/_next/static/chunks/4554-3565eea1f1495ba7.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/4562-071d8bac44a01069.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/{4685-c3c1c625b71accd1.js → 4614-21403aac730d4586.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/{8039-df92e08fede5de8f.js → 4795-f372490b178583fc.js} +1 -1
  24. package/apps/pwa/out/_next/static/chunks/5135-44008fe8b1f2aa41.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/5233-bf28bd6bd7a32099.js +1 -0
  26. package/apps/pwa/out/_next/static/chunks/5360-be1f1d8f9fcc9755.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/5977-2df489622a52a33a.js +1 -0
  28. package/apps/pwa/out/_next/static/chunks/5994-ae92c0de046ab9ba.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/{1452.93700483b9d4fc61.js → 6110.1297d4093fdfbb0d.js} +1 -1
  30. package/apps/pwa/out/_next/static/chunks/6158.9c0449a9a8d0064b.js +2 -0
  31. package/apps/pwa/out/_next/static/chunks/6877-7c6208705c284124.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/726.83532d69452ee24b.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/7290-f09c928a06c52e2d.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/{1829-b453a1b22d3999bb.js → 7327-2d69fb321ba73389.js} +1 -1
  35. package/apps/pwa/out/_next/static/chunks/7395-164e238f471b5d7b.js +1 -0
  36. package/apps/pwa/out/_next/static/chunks/7787-02a1c0b04e25e811.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/8060-cc008cc6fedfa818.js +1 -0
  38. package/apps/pwa/out/_next/static/chunks/8085-d065d0393c1760f5.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/8682-7061dd4fa7bd62ed.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/8827-e2f5b09a8d37e83b.js +1 -0
  41. package/apps/pwa/out/_next/static/chunks/8868-10a3a4440fb0ccd8.js +25 -0
  42. package/apps/pwa/out/_next/static/chunks/{8421-1c4acfb1395a0edf.js → 8949-ecb4128a0858b911.js} +1 -1
  43. package/apps/pwa/out/_next/static/chunks/9070.067c106528ec1ae6.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/9103-7a3ae7f60af3b99f.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/{3869-3cd46526879f454c.js → 9247-29d889b809288e65.js} +1 -1
  46. package/apps/pwa/out/_next/static/chunks/935.4ddaa99fc1d1126c.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/{79f8be9e-957d68913b590b08.js → a9f2b54d-7865aba0f4d2ee49.js} +1 -1
  48. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-c1a96669158471ec.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/app/approvals/page-74383f890f075511.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-7b2b4f378354869a.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/board/page-c496bb16da85385f.js +1 -0
  52. package/apps/pwa/out/_next/static/chunks/app/botlab/{page-f14987d5850a31e0.js → page-1abfe3aec3be6bdc.js} +1 -1
  53. package/apps/pwa/out/_next/static/chunks/app/bots/page-1512d91c7491ed62.js +1 -0
  54. package/apps/pwa/out/_next/static/chunks/app/chat/page-a627db94ec6904a0.js +1 -0
  55. package/apps/pwa/out/_next/static/chunks/app/consult/page-fb260b9283dbf646.js +1 -0
  56. package/apps/pwa/out/_next/static/chunks/app/control/page-e6b84ab07d3567d5.js +1 -0
  57. package/apps/pwa/out/_next/static/chunks/app/design-check/page-61ec1d2e92bdd50a.js +1 -0
  58. package/apps/pwa/out/_next/static/chunks/app/editor/page-fed69f9759d248d0.js +1 -0
  59. package/apps/pwa/out/_next/static/chunks/app/exec/page-15374909d8f1aebd.js +1 -0
  60. package/apps/pwa/out/_next/static/chunks/app/field/page-012a742f81218b01.js +1 -0
  61. package/apps/pwa/out/_next/static/chunks/app/healthz/route-c1a96669158471ec.js +1 -0
  62. package/apps/pwa/out/_next/static/chunks/app/inside/page-fd7e200d4d83fbd7.js +1 -0
  63. package/apps/pwa/out/_next/static/chunks/app/intake/page-99effd9d893a43dc.js +1 -0
  64. package/apps/pwa/out/_next/static/chunks/app/layout-938de57ae0315e46.js +1 -0
  65. package/apps/pwa/out/_next/static/chunks/app/live/page-d47266766766d227.js +1 -0
  66. package/apps/pwa/out/_next/static/chunks/app/loops/page-160089123a40dd91.js +1 -0
  67. package/apps/pwa/out/_next/static/chunks/app/market/page-629e1ede482ec6eb.js +1 -0
  68. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-66c27608f651b866.js → page-2ce5471926761fdd.js} +1 -1
  69. package/apps/pwa/out/_next/static/chunks/app/morning/page-27e99b4e54a849d4.js +1 -0
  70. package/apps/pwa/out/_next/static/chunks/app/not-found-e995d30f6800b6bf.js +1 -0
  71. package/apps/pwa/out/_next/static/chunks/app/observatory/page-39f89c53038a51d4.js +1 -0
  72. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-32cde713be47cddb.js +1 -0
  73. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-e7e10cd3386222e5.js +1 -0
  74. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-5a753bb4ec0cc511.js +1 -0
  75. package/apps/pwa/out/_next/static/chunks/app/outputs/page-04250f0e2c0d03df.js +1 -0
  76. package/apps/pwa/out/_next/static/chunks/app/page-341480ed8e3c1ea8.js +1 -0
  77. package/apps/pwa/out/_next/static/chunks/app/reflection/page-e64caff084f7ce21.js +1 -0
  78. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-db07eaffba0df7e3.js +1 -0
  79. package/apps/pwa/out/_next/static/chunks/app/sessions/page-8ac8d71c2adf9c8a.js +1 -0
  80. package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-78b40a344ed22f24.js → page-e1e47d8d940aba3d.js} +1 -1
  81. package/apps/pwa/out/_next/static/chunks/app/settings/page-86f91034bd5cabe5.js +1 -0
  82. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-94dc383d3d54db1a.js +1 -0
  83. package/apps/pwa/out/_next/static/chunks/app/setup/layout-c1a96669158471ec.js +1 -0
  84. package/apps/pwa/out/_next/static/chunks/app/setup/page-b3759cc14da80908.js +1 -0
  85. package/apps/pwa/out/_next/static/chunks/app/share/page-ce4d8a1215ce53a3.js +1 -0
  86. package/apps/pwa/out/_next/static/chunks/app/showroom/page-58043db78ba519ae.js +1 -0
  87. package/apps/pwa/out/_next/static/chunks/app/tasks/page-357750dfb91bf960.js +1 -0
  88. package/apps/pwa/out/_next/static/chunks/app/term/{page-63d14a4d8fe1e1c1.js → page-274703bc41b71333.js} +1 -1
  89. package/apps/pwa/out/_next/static/chunks/app/today/page-194dce4a83143fce.js +1 -0
  90. package/apps/pwa/out/_next/static/chunks/app/trace/page-35bdeae2e2c17147.js +1 -0
  91. package/apps/pwa/out/_next/static/chunks/app/vault/page-3b7e821dc492bdfd.js +1 -0
  92. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-16bef3da05a900c3.js +1 -0
  93. package/apps/pwa/out/_next/static/chunks/app/workflows/page-f8da5dc6f3752276.js +1 -0
  94. package/apps/pwa/out/_next/static/chunks/app/workspace/page-2f810b18d89ecba6.js +1 -0
  95. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-134c1bc762aee62c.js +1 -0
  96. package/apps/pwa/out/_next/static/chunks/{5cd7e3a9-404fd6b8d0ecd44c.js → f6231284-45726550aba08fca.js} +1 -1
  97. package/apps/pwa/out/_next/static/chunks/{f48170cc-d05d28bf87a43bf4.js → f6b7c9ba.64a895b8b2fedbdc.js} +1 -1
  98. package/apps/pwa/out/_next/static/chunks/framework-5c169757551134ed.js +1 -0
  99. package/apps/pwa/out/_next/static/chunks/main-59fe95fed40c7a01.js +1 -0
  100. package/apps/pwa/out/_next/static/chunks/main-app-4ca74dbbee7057c6.js +1 -0
  101. package/apps/pwa/out/_next/static/chunks/pages/_app-13a0a886df4d97a1.js +1 -0
  102. package/apps/pwa/out/_next/static/chunks/pages/_error-6af8591cd3a3f038.js +1 -0
  103. package/apps/pwa/out/_next/static/chunks/webpack-539cde99554be20f.js +1 -0
  104. package/apps/pwa/out/_next/static/css/2e33713c444d5b39.css +1 -0
  105. package/apps/pwa/out/_next/static/x6mjI6vqOkVVu0jFuDkz1/_buildManifest.js +1 -0
  106. package/apps/pwa/out/approvals/index.html +1 -1
  107. package/apps/pwa/out/approvals/index.txt +23 -23
  108. package/apps/pwa/out/autopilot/index.html +1 -1
  109. package/apps/pwa/out/autopilot/index.txt +23 -23
  110. package/apps/pwa/out/board/index.html +1 -1
  111. package/apps/pwa/out/board/index.txt +23 -23
  112. package/apps/pwa/out/botlab/index.html +1 -1
  113. package/apps/pwa/out/botlab/index.txt +22 -22
  114. package/apps/pwa/out/bots/index.html +1 -1
  115. package/apps/pwa/out/bots/index.txt +22 -22
  116. package/apps/pwa/out/chat/index.html +1 -1
  117. package/apps/pwa/out/chat/index.txt +22 -22
  118. package/apps/pwa/out/consult/index.html +1 -1
  119. package/apps/pwa/out/consult/index.txt +23 -23
  120. package/apps/pwa/out/control/index.html +1 -1
  121. package/apps/pwa/out/control/index.txt +23 -23
  122. package/apps/pwa/out/design-check/index.html +1 -1
  123. package/apps/pwa/out/design-check/index.txt +23 -23
  124. package/apps/pwa/out/editor/index.html +1 -1
  125. package/apps/pwa/out/editor/index.txt +23 -23
  126. package/apps/pwa/out/exec/index.html +1 -1
  127. package/apps/pwa/out/exec/index.txt +23 -23
  128. package/apps/pwa/out/field/index.html +1 -1
  129. package/apps/pwa/out/field/index.txt +23 -23
  130. package/apps/pwa/out/healthz +1 -1
  131. package/apps/pwa/out/index.html +1 -1
  132. package/apps/pwa/out/index.txt +22 -22
  133. package/apps/pwa/out/inside/index.html +1 -1
  134. package/apps/pwa/out/inside/index.txt +22 -22
  135. package/apps/pwa/out/intake/index.html +1 -1
  136. package/apps/pwa/out/intake/index.txt +23 -23
  137. package/apps/pwa/out/live/index.html +1 -1
  138. package/apps/pwa/out/live/index.txt +23 -23
  139. package/apps/pwa/out/loops/index.html +1 -0
  140. package/apps/pwa/out/loops/index.txt +30 -0
  141. package/apps/pwa/out/market/index.html +1 -1
  142. package/apps/pwa/out/market/index.txt +22 -22
  143. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  144. package/apps/pwa/out/missions/placeholder/index.txt +22 -22
  145. package/apps/pwa/out/morning/index.html +1 -1
  146. package/apps/pwa/out/morning/index.txt +23 -23
  147. package/apps/pwa/out/observatory/index.html +1 -1
  148. package/apps/pwa/out/observatory/index.txt +23 -23
  149. package/apps/pwa/out/ops/checklist/index.html +1 -1
  150. package/apps/pwa/out/ops/checklist/index.txt +22 -22
  151. package/apps/pwa/out/ops/release/index.html +1 -1
  152. package/apps/pwa/out/ops/release/index.txt +22 -22
  153. package/apps/pwa/out/ops/seats/index.html +1 -1
  154. package/apps/pwa/out/ops/seats/index.txt +22 -22
  155. package/apps/pwa/out/outputs/index.html +1 -1
  156. package/apps/pwa/out/outputs/index.txt +22 -22
  157. package/apps/pwa/out/reflection/index.html +1 -1
  158. package/apps/pwa/out/reflection/index.txt +23 -23
  159. package/apps/pwa/out/scheduler/index.html +1 -1
  160. package/apps/pwa/out/scheduler/index.txt +23 -23
  161. package/apps/pwa/out/sessions/index.html +1 -1
  162. package/apps/pwa/out/sessions/index.txt +22 -22
  163. package/apps/pwa/out/settings/devices/index.html +1 -1
  164. package/apps/pwa/out/settings/devices/index.txt +23 -23
  165. package/apps/pwa/out/settings/index.html +1 -1
  166. package/apps/pwa/out/settings/index.txt +23 -23
  167. package/apps/pwa/out/setup/done/index.html +1 -1
  168. package/apps/pwa/out/setup/done/index.txt +23 -23
  169. package/apps/pwa/out/setup/index.html +1 -1
  170. package/apps/pwa/out/setup/index.txt +23 -23
  171. package/apps/pwa/out/share/index.html +1 -1
  172. package/apps/pwa/out/share/index.txt +23 -23
  173. package/apps/pwa/out/showroom/index.html +1 -1
  174. package/apps/pwa/out/showroom/index.txt +22 -22
  175. package/apps/pwa/out/tasks/index.html +1 -1
  176. package/apps/pwa/out/tasks/index.txt +23 -23
  177. package/apps/pwa/out/term/index.html +1 -1
  178. package/apps/pwa/out/term/index.txt +23 -23
  179. package/apps/pwa/out/today/index.html +1 -1
  180. package/apps/pwa/out/today/index.txt +23 -23
  181. package/apps/pwa/out/trace/index.html +1 -1
  182. package/apps/pwa/out/trace/index.txt +23 -23
  183. package/apps/pwa/out/vault/index.html +1 -1
  184. package/apps/pwa/out/vault/index.txt +23 -23
  185. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  186. package/apps/pwa/out/workflows/chat-ui/index.txt +23 -23
  187. package/apps/pwa/out/workflows/index.html +1 -1
  188. package/apps/pwa/out/workflows/index.txt +23 -23
  189. package/apps/pwa/out/workspace/index.html +1 -1
  190. package/apps/pwa/out/workspace/index.txt +23 -23
  191. package/apps/pwa/out/worktrees/index.html +1 -1
  192. package/apps/pwa/out/worktrees/index.txt +23 -23
  193. package/graphs/orchestrator/orchestrator.yaml +59 -0
  194. package/graphs/orchestrator/recipes.yaml +18 -0
  195. package/graphs/release/recipes.yaml +3 -0
  196. package/graphs/release/release-loop.yaml +4 -1
  197. package/graphs/rhythm/daily.yaml +38 -0
  198. package/graphs/rhythm/recipes.yaml +18 -0
  199. package/graphs/rhythm/weekly.yaml +39 -0
  200. package/graphs/video/recipes.yaml +3 -0
  201. package/graphs/video/youtube-summary.yaml +17 -0
  202. package/package.json +1 -1
  203. package/scripts/audit-test-state-writes.ts +72 -17
  204. package/scripts/brand/check.ts +186 -0
  205. package/scripts/coord-post.sh +11 -1
  206. package/scripts/docs-cli-check.ts +23 -6
  207. package/scripts/expose-rubric-check.ts +118 -0
  208. package/scripts/expose-rubric-suggest.ts +163 -0
  209. package/scripts/heal/run-role.ts +14 -0
  210. package/scripts/hq/standby-verify.ts +11 -7
  211. package/scripts/lessons/handbook-page.ts +85 -0
  212. package/scripts/release-loop/gate-node.ts +9 -0
  213. package/scripts/release-loop/publish-node.ts +14 -1
  214. package/scripts/release-loop/unattended-release.ts +23 -1
  215. package/scripts/release-loop/vault-note-node.ts +196 -0
  216. package/scripts/release-story/draft.ts +172 -0
  217. package/scripts/rhythm/daily.ts +383 -0
  218. package/scripts/rhythm/weekly.ts +294 -0
  219. package/scripts/skills/public-check.ts +169 -0
  220. package/scripts/test-deterministic.ts +4 -8
  221. package/scripts/test-diet/effectiveness.ts +195 -0
  222. package/scripts/test-diet/lib.ts +16 -8
  223. package/scripts/test-diet/node.ts +77 -9
  224. package/scripts/ux/demo-rehearsal.ts +2 -2
  225. package/scripts/ux/lib/rehearsal-verdict.ts +1 -0
  226. package/scripts/video/youtube-summary.ts +133 -0
  227. package/skills/diagram-master/SKILL.md +42 -49
  228. package/skills/diagram-master/references/pyproject.toml +4 -1
  229. package/skills/diagram-master/references/routing-guide.md +4 -2
  230. package/skills/grill-me/SKILL.md +1 -0
  231. package/skills/youtube-master/SKILL.md +13 -13
  232. package/skills/youtube-master/package-lock.json +0 -545
  233. package/skills/youtube-master/package.json +2 -5
  234. package/skills/youtube-master/scripts/main.ts +2 -2
  235. package/skills/youtube-master/scripts/transcribe-local.ts +1 -1
  236. package/skills/youtube-master/src/util.ts +30 -7
  237. package/src/agent-mission/driver.ts +3 -3
  238. package/src/autopilot/build/llm-conflict-merge.ts +45 -8
  239. package/src/autopilot/build/merge-intent.ts +53 -0
  240. package/src/chat/index.ts +2 -0
  241. package/src/claims/claims-ledger.ts +232 -0
  242. package/src/claims/claims-render.ts +157 -0
  243. package/src/cli/claims-cli.ts +94 -0
  244. package/src/cli/decisions-cli.ts +35 -3
  245. package/src/cli/docs-cli.ts +2 -0
  246. package/src/cli/flow-cli.ts +11 -0
  247. package/src/cli/freeze-cli.ts +45 -0
  248. package/src/cli/lesson-cli.ts +108 -0
  249. package/src/cli/logs-cli.ts +104 -2
  250. package/src/cli/logs-model-input.ts +131 -0
  251. package/src/cli/pr-cli.ts +29 -1
  252. package/src/cli/private-ledger-commands.ts +54 -4
  253. package/src/cli/pty-drive-verdict.ts +11 -2
  254. package/src/cli/release-cli.ts +144 -21
  255. package/src/cli/rfc-status.ts +91 -0
  256. package/src/connectors/linear.ts +5 -3
  257. package/src/context-bus/condense.ts +15 -20
  258. package/src/context-bus/context-now-public.ts +86 -0
  259. package/src/context-bus/context-now-surfaces.ts +55 -2
  260. package/src/context-bus/context-now.ts +4 -3
  261. package/src/context-bus/long-term-memory.ts +92 -11
  262. package/src/dashboard/index.ts +75 -22
  263. package/src/dashboard/slash-runtime/chat-persona.ts +66 -0
  264. package/src/dashboard/slash-runtime/dashboard-handlers.ts +99 -0
  265. package/src/dashboard/slash-runtime/loops-table.ts +77 -0
  266. package/src/decisions/decision-ledger.ts +40 -1
  267. package/src/discord/sprint21-runtime.ts +16 -0
  268. package/src/discord-self-message.ts +8 -0
  269. package/src/discord-slash-wire.ts +31 -2
  270. package/src/domains/outbound-alert.ts +1 -1
  271. package/src/elanous-config-dir.ts +29 -4
  272. package/src/execution-loop/launch-gate.ts +29 -14
  273. package/src/flow/cards.ts +239 -0
  274. package/src/graph-runner/graph-tick.ts +4 -0
  275. package/src/graph-runner/runner.ts +30 -5
  276. package/src/harness/harness-cli-command.ts +152 -27
  277. package/src/harness/harness-incidents.ts +43 -12
  278. package/src/harness/harness-pod-dispatch.ts +2 -0
  279. package/src/harness/harness-queue-child.ts +3 -3
  280. package/src/harness/harness-queue.ts +216 -28
  281. package/src/harness/helper-scan.ts +139 -0
  282. package/src/harness/model-input-observation.ts +37 -0
  283. package/src/hq/hq.ts +1 -1
  284. package/src/hq/promote.ts +301 -0
  285. package/src/index.ts +83 -21
  286. package/src/intake-plane/linear-wish.ts +39 -11
  287. package/src/intake-plane/wish-benchmark-ledger.ts +129 -0
  288. package/src/intake-plane/wish-card.ts +53 -2
  289. package/src/intake-plane/wish-folder.ts +5 -2
  290. package/src/intake-plane/wish-reply.ts +72 -0
  291. package/src/lessons/lesson-import.ts +106 -0
  292. package/src/lessons/lesson-ledger.ts +206 -0
  293. package/src/loops/activity.ts +203 -0
  294. package/src/loops/budget.ts +32 -0
  295. package/src/loops/checker.ts +5 -3
  296. package/src/loops/contract/examples/orchestrator.loop.yaml +1 -1
  297. package/src/loops/loop-cli.ts +36 -4
  298. package/src/loops/neighbors.ts +40 -0
  299. package/src/loops/observe.ts +10 -0
  300. package/src/loops/orchestrator/author-depth.ts +164 -0
  301. package/src/loops/orchestrator/author-input-check.ts +77 -0
  302. package/src/loops/orchestrator/author-ledger.ts +230 -0
  303. package/src/loops/orchestrator/finish-rate.ts +175 -0
  304. package/src/loops/orchestrator/overlap.ts +51 -12
  305. package/src/loops/orchestrator/resync-shadow.ts +227 -0
  306. package/src/loops/orchestrator/tick.ts +302 -0
  307. package/src/loops/orchestrator/traffic.ts +18 -3
  308. package/src/loops/registry.ts +454 -9
  309. package/src/loops/status-rows.ts +118 -0
  310. package/src/loops/verdict.ts +17 -5
  311. package/src/maturity/bot-slash-catalog.ts +66 -0
  312. package/src/maturity/feature-maturity.ts +7 -4
  313. package/src/nexus/api/dashboard.ts +6 -1
  314. package/src/nexus/api/http-server.ts +18 -2
  315. package/src/nexus/api/merge-approvals-gate.ts +1 -1
  316. package/src/nexus/api/schedules-read.ts +5 -1
  317. package/src/nexus/api/task-cards-api.ts +44 -5
  318. package/src/nexus/api/workflows.ts +2 -1
  319. package/src/release-loop/auto-start.ts +48 -0
  320. package/src/release-loop/checklist.ts +13 -4
  321. package/src/release-loop/feature-store.ts +28 -16
  322. package/src/release-loop/landing-freeze.ts +121 -0
  323. package/src/release-loop/placement.ts +242 -0
  324. package/src/release-loop/release-schedule.ts +3 -1
  325. package/src/seat-dispatch/ceo-commands.ts +2 -2
  326. package/src/seat-dispatch/seat-ask.ts +16 -3
  327. package/src/seat-dispatch/seat-questions.ts +58 -0
  328. package/src/seat-loop/seat-loop.ts +406 -31
  329. package/src/self-dev/dev-pipeline.ts +6 -2
  330. package/src/self-dev/draft-sweep.ts +76 -0
  331. package/src/self-dev/run-store.ts +73 -1
  332. package/src/self-dev/run-supervisor.ts +8 -0
  333. package/src/self-dev/self-orchestrate-runtime.ts +5 -1
  334. package/src/self-implement/budget-gate.ts +29 -0
  335. package/src/self-implement/expose-gate.ts +47 -0
  336. package/src/self-implement/frozen-merges.ts +294 -0
  337. package/src/self-implement/gate-cli.ts +10 -4
  338. package/src/self-implement/gate-scope.ts +46 -9
  339. package/src/self-implement/goal-archive.ts +222 -0
  340. package/src/self-implement/goal-lookup.ts +147 -0
  341. package/src/self-implement/heal-intake.ts +79 -0
  342. package/src/self-implement/host-regate.ts +25 -2
  343. package/src/self-implement/orchestrator.ts +114 -10
  344. package/src/self-implement/revert-guard.ts +62 -0
  345. package/src/self-implement/run-ledger.ts +1 -1
  346. package/src/self-implement/seams.ts +28 -8
  347. package/src/session/index.ts +2 -0
  348. package/src/task-cards/card-cli.ts +10 -2
  349. package/src/task-cards/card-store.ts +10 -6
  350. package/src/task-orchestrator/surfaces/pod-lease.ts +5 -2
  351. package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +46 -6
  352. package/src/task-orchestrator/surfaces/self-implement-pod.ts +80 -16
  353. package/src/telegram-commands.ts +27 -3
  354. package/src/telegram-loops-command.ts +44 -0
  355. package/src/user-config.ts +120 -16
  356. package/src/version/packed-revision.json +1 -1
  357. package/src/workflow-runtime/storage.ts +31 -13
  358. package/apps/pwa/out/_next/static/D51l8jUBplJrIpWvENsDK/_buildManifest.js +0 -1
  359. package/apps/pwa/out/_next/static/chunks/1159-0c5ef8bc21cb6afb.js +0 -1
  360. package/apps/pwa/out/_next/static/chunks/1394-931c9827067b2276.js +0 -1
  361. package/apps/pwa/out/_next/static/chunks/2176-c379e3ebce874151.js +0 -1
  362. package/apps/pwa/out/_next/static/chunks/2426.8c6ee0c668d53def.js +0 -1
  363. package/apps/pwa/out/_next/static/chunks/3170-7ec60a56fa0e4728.js +0 -1
  364. package/apps/pwa/out/_next/static/chunks/3278-ae6ef084769f6195.js +0 -1
  365. package/apps/pwa/out/_next/static/chunks/3454-988a53dac60a1763.js +0 -1
  366. package/apps/pwa/out/_next/static/chunks/3638-aa80b4641ea439db.js +0 -1
  367. package/apps/pwa/out/_next/static/chunks/3735-fe3402553ac1c9c6.js +0 -1
  368. package/apps/pwa/out/_next/static/chunks/3992-fe5c06d6b31f8b25.js +0 -25
  369. package/apps/pwa/out/_next/static/chunks/4215-c9d91dd644ec1719.js +0 -1
  370. package/apps/pwa/out/_next/static/chunks/4508-5d0161d1e724d735.js +0 -1
  371. package/apps/pwa/out/_next/static/chunks/4733-623a79345b171e1c.js +0 -1
  372. package/apps/pwa/out/_next/static/chunks/4883.3bd969ea1d3aaaff.js +0 -2
  373. package/apps/pwa/out/_next/static/chunks/5250-c78ca295585bbf95.js +0 -1
  374. package/apps/pwa/out/_next/static/chunks/5532.6899a12958a89b72.js +0 -1
  375. package/apps/pwa/out/_next/static/chunks/5845-e6c003ce8d5fd36a.js +0 -1
  376. package/apps/pwa/out/_next/static/chunks/5978-b27004c677119115.js +0 -1
  377. package/apps/pwa/out/_next/static/chunks/6048-4c5c33579bc4ef78.js +0 -1
  378. package/apps/pwa/out/_next/static/chunks/6303-422dbf0f67c72aac.js +0 -1
  379. package/apps/pwa/out/_next/static/chunks/6511-ff8360be3d65aaa4.js +0 -1
  380. package/apps/pwa/out/_next/static/chunks/6588-ba380869b67c4fa7.js +0 -1
  381. package/apps/pwa/out/_next/static/chunks/6751-bb3595948b2e311b.js +0 -1
  382. package/apps/pwa/out/_next/static/chunks/7274-19b7279f18b47ab3.js +0 -1
  383. package/apps/pwa/out/_next/static/chunks/7551-a0e0707b79164ddd.js +0 -1
  384. package/apps/pwa/out/_next/static/chunks/7758-85c2b53f731da024.js +0 -1
  385. package/apps/pwa/out/_next/static/chunks/7777-79f845045aa1ed86.js +0 -1
  386. package/apps/pwa/out/_next/static/chunks/8025-cc24ee5d8f2fb6f8.js +0 -1
  387. package/apps/pwa/out/_next/static/chunks/8206-c6684dcde12be721.js +0 -1
  388. package/apps/pwa/out/_next/static/chunks/8605-9da4678f8a310d18.js +0 -1
  389. package/apps/pwa/out/_next/static/chunks/8725.bf46bb1395dd0fa2.js +0 -1
  390. package/apps/pwa/out/_next/static/chunks/8734-252d511cd276c0dc.js +0 -1
  391. package/apps/pwa/out/_next/static/chunks/8834-ca1cb09c1cb1a6bf.js +0 -1
  392. package/apps/pwa/out/_next/static/chunks/9269.d2a4795bc6812717.js +0 -1
  393. package/apps/pwa/out/_next/static/chunks/9538-2816b90923606459.js +0 -1
  394. package/apps/pwa/out/_next/static/chunks/9906-e7b8b5310a169dfe.js +0 -1
  395. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-d16993889b125c42.js +0 -1
  396. package/apps/pwa/out/_next/static/chunks/app/approvals/page-e5b4e1b256f41076.js +0 -1
  397. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-e49b9b76c565a0b1.js +0 -1
  398. package/apps/pwa/out/_next/static/chunks/app/board/page-b4f032c893b21cc9.js +0 -1
  399. package/apps/pwa/out/_next/static/chunks/app/bots/page-b433308aad13ceb6.js +0 -1
  400. package/apps/pwa/out/_next/static/chunks/app/chat/page-4c1dbb21861beab2.js +0 -1
  401. package/apps/pwa/out/_next/static/chunks/app/consult/page-c85b816e3b269dbb.js +0 -1
  402. package/apps/pwa/out/_next/static/chunks/app/control/page-f6bc66b919b2ea7d.js +0 -1
  403. package/apps/pwa/out/_next/static/chunks/app/design-check/page-60603c1e056c9b25.js +0 -1
  404. package/apps/pwa/out/_next/static/chunks/app/editor/page-a0ea8f5a2341ee6b.js +0 -1
  405. package/apps/pwa/out/_next/static/chunks/app/exec/page-45c5bf04b4a605e6.js +0 -1
  406. package/apps/pwa/out/_next/static/chunks/app/field/page-36c6c4183353ac9e.js +0 -1
  407. package/apps/pwa/out/_next/static/chunks/app/healthz/route-d16993889b125c42.js +0 -1
  408. package/apps/pwa/out/_next/static/chunks/app/inside/page-cb2eb0fd809980da.js +0 -1
  409. package/apps/pwa/out/_next/static/chunks/app/intake/page-31012264da8a341f.js +0 -1
  410. package/apps/pwa/out/_next/static/chunks/app/layout-50309aeb0bdd554a.js +0 -1
  411. package/apps/pwa/out/_next/static/chunks/app/live/page-bc00e964e855f4df.js +0 -1
  412. package/apps/pwa/out/_next/static/chunks/app/market/page-0696ee3fac9fe56c.js +0 -1
  413. package/apps/pwa/out/_next/static/chunks/app/morning/page-6ddd2109b7fc3a80.js +0 -1
  414. package/apps/pwa/out/_next/static/chunks/app/not-found-4e50b7298c1f69a5.js +0 -1
  415. package/apps/pwa/out/_next/static/chunks/app/observatory/page-7a2bf830038a2264.js +0 -1
  416. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-1f327f18694fabca.js +0 -1
  417. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-e5cef2fe6e4028df.js +0 -1
  418. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-cf596128d2b420ba.js +0 -1
  419. package/apps/pwa/out/_next/static/chunks/app/outputs/page-f672536f14f0969f.js +0 -1
  420. package/apps/pwa/out/_next/static/chunks/app/page-b52365662232db89.js +0 -1
  421. package/apps/pwa/out/_next/static/chunks/app/reflection/page-e7a6c80f288b1948.js +0 -1
  422. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-29a709616df9b050.js +0 -1
  423. package/apps/pwa/out/_next/static/chunks/app/sessions/page-fee842d7c2151129.js +0 -1
  424. package/apps/pwa/out/_next/static/chunks/app/settings/page-75d199f448171014.js +0 -1
  425. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-a011dd5b0bff29c5.js +0 -1
  426. package/apps/pwa/out/_next/static/chunks/app/setup/layout-d16993889b125c42.js +0 -1
  427. package/apps/pwa/out/_next/static/chunks/app/setup/page-d92e32d51ab5157e.js +0 -1
  428. package/apps/pwa/out/_next/static/chunks/app/share/page-5f21aed6932ef375.js +0 -1
  429. package/apps/pwa/out/_next/static/chunks/app/showroom/page-1e1a53946804debf.js +0 -1
  430. package/apps/pwa/out/_next/static/chunks/app/tasks/page-a1833cd43d832601.js +0 -1
  431. package/apps/pwa/out/_next/static/chunks/app/today/page-2a511d320f762e06.js +0 -1
  432. package/apps/pwa/out/_next/static/chunks/app/trace/page-1c3e03f88dac1e21.js +0 -1
  433. package/apps/pwa/out/_next/static/chunks/app/vault/page-c4d5289c2ea97a89.js +0 -1
  434. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-b91f8952deaeaf27.js +0 -1
  435. package/apps/pwa/out/_next/static/chunks/app/workflows/page-71b43a2588489987.js +0 -1
  436. package/apps/pwa/out/_next/static/chunks/app/workspace/page-2b256feed331546f.js +0 -1
  437. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-505bb96d69a8622c.js +0 -1
  438. package/apps/pwa/out/_next/static/chunks/framework-172630e6aa5e2d7f.js +0 -1
  439. package/apps/pwa/out/_next/static/chunks/main-app-f0fe2d7477845181.js +0 -1
  440. package/apps/pwa/out/_next/static/chunks/main-fc98ac70b44eccb0.js +0 -1
  441. package/apps/pwa/out/_next/static/chunks/pages/_app-5bd7f10271f72413.js +0 -1
  442. package/apps/pwa/out/_next/static/chunks/pages/_error-97d0d85ea05fef9b.js +0 -1
  443. package/apps/pwa/out/_next/static/chunks/webpack-9e4d02d567b98240.js +0 -1
  444. package/apps/pwa/out/_next/static/css/128fb7fb670b2fbf.css +0 -1
  445. /package/apps/pwa/out/_next/static/{D51l8jUBplJrIpWvENsDK → x6mjI6vqOkVVu0jFuDkz1}/_ssgManifest.js +0 -0
@@ -0,0 +1,118 @@
1
+ #!/usr/bin/env bun
2
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
3
+ import { join, relative, resolve, sep } from 'node:path';
4
+ import { parse } from 'yaml';
5
+ import { debug } from '../src/debug/log.js';
6
+
7
+ const DOCS = 'release/public/docs';
8
+ const LEDGER = 'release/public/expose-rubric.yaml';
9
+ const CRITERIA = ['maturity', 'value', 'reproducible', 'evidence', 'confidential', 'brand', 'support'] as const;
10
+ type Criterion = { status: 'pass' | 'fail' | 'n/a'; reason: string };
11
+ type Entry = {
12
+ path: string;
13
+ verdict: 'public' | 'beta' | 'internal';
14
+ criteria: Record<(typeof CRITERIA)[number], Criterion>;
15
+ judge: string;
16
+ techReview?: string;
17
+ at: string;
18
+ };
19
+ type RubricResult = { docs: number; missing: string[]; orphan: string[]; failing: Array<{ path: string; criteria: string[]; verdict?: 'beta' }>; internal: string[]; unreviewed: string[]; unjudged: string[] };
20
+
21
+ function validEntry(value: unknown): value is Entry {
22
+ if (!value || typeof value !== 'object') return false;
23
+ const entry = value as Partial<Entry>;
24
+ if (typeof entry.path !== 'string' || !/^release\/public\/docs\/[^/]+\.md$/.test(entry.path)
25
+ || !['public', 'beta', 'internal'].includes(entry.verdict ?? '')
26
+ // The exposure judge is the CMO seat (EXPOSE-RUBRIC); any other non-empty name is not a judgement.
27
+ || entry.judge !== 'MK'
28
+ || (entry.techReview !== undefined && (typeof entry.techReview !== 'string' || !entry.techReview.trim()))
29
+ || typeof entry.at !== 'string' || !/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d+)?(?:Z|[+-]\d\d:\d\d)$/.test(entry.at)
30
+ || !Number.isFinite(Date.parse(entry.at)) || !entry.criteria || typeof entry.criteria !== 'object') return false;
31
+ const criteria = entry.criteria as Record<string, unknown>;
32
+ if (Object.keys(criteria).length !== CRITERIA.length) return false;
33
+ return CRITERIA.every((key) => {
34
+ const item = criteria[key] as Partial<Criterion> | undefined;
35
+ return item && ['pass', 'fail', 'n/a'].includes(item.status ?? '')
36
+ && typeof item.reason === 'string' && !!item.reason.trim() && !item.reason.includes('\n');
37
+ });
38
+ }
39
+
40
+ function selectedPath(input: string, root: string): string | undefined {
41
+ const path = relative(root, resolve(root, input)).split(sep).join('/');
42
+ return /^release\/public\/docs\/[^/]+\.md$/.test(path) ? path : undefined;
43
+ }
44
+
45
+ export function checkExposeRubric(root: string, files?: readonly string[]): RubricResult {
46
+ const data = parse(readFileSync(join(root, LEDGER), 'utf8')) as unknown;
47
+ if (!data || typeof data !== 'object' || !('entries' in data) || !Array.isArray(data.entries)) throw new Error(`invalid ledger: ${LEDGER}`);
48
+ const entries = new Map<string, Entry>();
49
+ for (const value of data.entries as unknown[]) {
50
+ if (!validEntry(value) || entries.has(value.path)) throw new Error(`invalid or duplicate ledger entry: ${JSON.stringify(value)}`);
51
+ entries.set(value.path, value);
52
+ }
53
+ const scope = files === undefined ? undefined : new Set(files.map((file) => selectedPath(file, root)).filter((path): path is string => path !== undefined));
54
+ const docs = readdirSync(join(root, DOCS)).filter((name) => name.endsWith('.md'))
55
+ .map((name) => `${DOCS}/${name}`).filter((path) => !scope || scope.has(path)).sort();
56
+ const missing = docs.filter((path) => !entries.has(path));
57
+ const orphan: string[] = [];
58
+ const failing: RubricResult['failing'] = [];
59
+ const internal: string[] = [];
60
+ const unreviewed: string[] = [];
61
+ const unjudged: string[] = [];
62
+ for (const entry of entries.values()) {
63
+ if (scope && !scope.has(entry.path)) continue;
64
+ if (!existsSync(join(root, entry.path))) {
65
+ orphan.push(entry.path);
66
+ continue;
67
+ }
68
+ if (entry.verdict === 'internal') internal.push(entry.path);
69
+ if (entry.verdict === 'public' || entry.verdict === 'beta') {
70
+ const failed = entry.verdict === 'public'
71
+ ? CRITERIA.filter((key) => entry.criteria[key].status === 'fail')
72
+ : (entry.criteria.confidential.status === 'fail' ? ['confidential'] : []);
73
+ if (failed.length) failing.push({ path: entry.path, criteria: failed, ...(entry.verdict === 'beta' ? { verdict: 'beta' as const } : {}) });
74
+ if (entry.verdict === 'public' && entry.techReview !== 'TC') unreviewed.push(entry.path);
75
+ // An outward verdict with no criterion actually passed is a placeholder, not a judgement (all n/a = never checked).
76
+ if (!CRITERIA.some((key) => entry.criteria[key].status === 'pass')) unjudged.push(entry.path);
77
+ }
78
+ }
79
+ orphan.sort();
80
+ failing.sort((a, b) => a.path.localeCompare(b.path));
81
+ internal.sort();
82
+ unreviewed.sort();
83
+ unjudged.sort();
84
+ const result = { docs: docs.length, missing, orphan, failing, internal, unreviewed, unjudged };
85
+ debug.log('expose.rubric', 'checked', { docs: result.docs, missing: missing.length, orphan: orphan.length, failing: failing.length, unjudged: unjudged.length });
86
+ return result;
87
+ }
88
+
89
+ if (import.meta.main) {
90
+ try {
91
+ const args = process.argv.slice(2);
92
+ if (args.includes('--help')) {
93
+ console.log('Usage: bun scripts/expose-rubric-check.ts [--strict] [--json] [--files <path...>]');
94
+ } else {
95
+ const options = new Set(['--strict', '--json', '--files']);
96
+ const index = args.indexOf('--files');
97
+ const beforeFiles = args.slice(0, index < 0 ? args.length : index);
98
+ const files = index < 0 ? undefined : args.slice(index + 1);
99
+ if (beforeFiles.some((arg) => !options.has(arg) || arg === '--files')
100
+ || (files && (!files.length || files.some((file) => file.startsWith('--'))))) throw new Error('invalid arguments: use --files <path...> last');
101
+ const result = checkExposeRubric(process.cwd(), files);
102
+ if (args.includes('--json')) console.log(JSON.stringify(result));
103
+ else {
104
+ for (const path of result.missing) console.error(`경고: 원장 없음: ${path}`);
105
+ for (const path of result.orphan) console.error(`경고: 파일 없음: ${path}`);
106
+ for (const item of result.failing) console.error(`경고: ${item.verdict ?? 'public'} 인데 fail (${item.criteria.join(', ')}): ${item.path}`);
107
+ for (const path of result.internal) console.error(`경고: internal 인데 공개 경로: ${path}`);
108
+ for (const path of result.unreviewed) console.error(`경고: public 기술 정확성 TC 미검토: ${path}`);
109
+ for (const path of result.unjudged) console.error(`경고: 미판정(일곱 기준 중 pass 0): ${path}`);
110
+ console.error(`expose-rubric — docs=${result.docs} missing=${result.missing.length} orphan=${result.orphan.length} failing=${result.failing.length} internal=${result.internal.length} unreviewed=${result.unreviewed.length} unjudged=${result.unjudged.length}`);
111
+ }
112
+ if (args.includes('--strict') && (result.missing.length || result.orphan.length || result.failing.length || result.internal.length || result.unjudged.length)) process.exitCode = 1;
113
+ }
114
+ } catch (error) {
115
+ console.error(error instanceof Error ? error.message : String(error));
116
+ process.exitCode = 2;
117
+ }
118
+ }
@@ -0,0 +1,163 @@
1
+ #!/usr/bin/env bun
2
+ /** Machine-measured suggestions only; MK judges and merges the exposure ledger. */
3
+ import { readFileSync, readdirSync, realpathSync, statSync, writeFileSync } from 'node:fs';
4
+ import { spawnSync } from 'node:child_process';
5
+ import { join, relative, resolve, sep } from 'node:path';
6
+ import { stringify } from 'yaml';
7
+ import { debug } from '../src/debug/log.js';
8
+ import { checkBrand } from './brand/check.js';
9
+ import { checkCommands, extractElanousCommands, type HelpRunner } from './docs-cli-check.js';
10
+ import { scanLeaks } from './public-export.js';
11
+
12
+ const DOCS = 'release/public/docs';
13
+ const LEDGER = 'release/public/expose-rubric.yaml';
14
+ const KEYS = ['reproducible', 'confidential', 'brand'] as const;
15
+ type Criterion = { status: 'pass' | 'fail' | 'n/a'; reason: string };
16
+ type Suggestion = { path: string; criteria: Record<(typeof KEYS)[number], Criterion> };
17
+
18
+ function unmeasured(error: unknown): Criterion {
19
+ const detail = error instanceof Error ? error.message : String(error);
20
+ return { status: 'n/a', reason: `측정 불가: ${detail.split(/\r?\n/)[0] || '알 수 없는 오류'}` };
21
+ }
22
+
23
+ function measure(run: () => Criterion): Criterion {
24
+ try { return run(); } catch (error) { return unmeasured(error); }
25
+ }
26
+
27
+ /** Files are repository-relative paths (or absolute paths within root); the ledger is never read or written. */
28
+ export function suggestExposeRubric(root: string, files?: readonly string[], help?: HelpRunner): Suggestion[] {
29
+ const names = files ?? readdirSync(join(root, DOCS)).filter((name) => name.endsWith('.md')).map((name) => `${DOCS}/${name}`);
30
+ const paths = [...new Set(names.map((name) => relative(root, resolve(root, name)).split(sep).join('/')))];
31
+ if (paths.some((path) => !/^release\/public\/docs\/[^/]+\.md$/.test(path))) throw new Error('문서 경로는 release/public/docs/*.md 이어야 합니다');
32
+ type HelpResult = ReturnType<HelpRunner> & { failure?: string };
33
+ let helpRuns = 0;
34
+ const rootHelp: (args: string[]) => HelpResult = help ?? ((args) => {
35
+ helpRuns++;
36
+ const result = spawnSync('bun', ['bin/elanous.mjs', '--test', ...args, '--help'], {
37
+ cwd: resolve(import.meta.dir, '..'), encoding: 'utf8', timeout: 60_000, env: { ...process.env, NO_COLOR: '1' },
38
+ });
39
+ const error = result.error as NodeJS.ErrnoException | undefined;
40
+ return {
41
+ ok: result.status === 0 && !error,
42
+ out: `${result.stdout ?? ''}\n${result.stderr ?? ''}`,
43
+ failure: error?.code === 'ETIMEDOUT' ? 'timeout'
44
+ : error ? error.message
45
+ : result.status !== 0 ? `exit ${result.status ?? 'unknown'}` : undefined,
46
+ };
47
+ });
48
+ const helpCache = new Map<string, { ok: true; out: string } | { ok: false; reason: string }>();
49
+ let helpFailures = 0;
50
+ const checkedHelp: HelpRunner = (args) => {
51
+ const key = args.join(' ');
52
+ let cached = helpCache.get(key);
53
+ if (!cached) {
54
+ const runOnce = (): { ok: true; out: string } | { ok: false; reason: string } => {
55
+ try {
56
+ const result = rootHelp(args);
57
+ if (result.ok && result.out.trim()) return { ok: true, out: result.out };
58
+ helpFailures++;
59
+ return { ok: false, reason: result.failure ?? (result.out.trim() ? '실행 실패' : '빈 출력') };
60
+ } catch (error) {
61
+ helpFailures++;
62
+ return { ok: false, reason: error instanceof Error ? error.message : String(error) };
63
+ }
64
+ };
65
+ cached = runOnce();
66
+ if (!cached.ok) cached = runOnce();
67
+ helpCache.set(key, cached);
68
+ }
69
+ if (!cached.ok) throw new Error(`elanous ${key ? `${key} ` : ''}--help 실패(${cached.reason.split(/\r?\n/)[0] || '알 수 없는 오류'})`);
70
+ return cached;
71
+ };
72
+ const results = paths.sort().map((path): Suggestion => {
73
+ const absolute = join(root, path);
74
+ let text: string;
75
+ try { text = readFileSync(absolute, 'utf8'); }
76
+ catch (error) {
77
+ const failed = unmeasured(error);
78
+ return { path, criteria: { reproducible: failed, confidential: failed, brand: failed } };
79
+ }
80
+ const reproducible = measure(() => {
81
+ const refs = extractElanousCommands(path, text);
82
+ if (!refs.length) return { status: 'n/a', reason: '문서에 elanous 명령 없음' };
83
+ const findings = checkCommands(refs, checkedHelp);
84
+ const unable = findings.find((finding) => finding.kind === 'unmeasured');
85
+ if (unable) return unmeasured(unable.detail);
86
+ const missing = [...new Set(findings.map((finding) => finding.detail))].slice(0, 3);
87
+ return missing.length
88
+ ? { status: 'fail', reason: `없는 명령: ${missing.join(', ')}` }
89
+ : { status: 'pass', reason: `명령 ${refs.length}개 모두 --help 에 있음` };
90
+ });
91
+ const confidential = measure(() => {
92
+ const hits = scanLeaks(root, [path], undefined, new Map([[path, text]]));
93
+ const markers = [...new Set(hits.map((hit) => hit.marker))].slice(0, 3);
94
+ return markers.length
95
+ ? { status: 'fail', reason: `누출 표식: ${markers.join(', ')}` }
96
+ : { status: 'pass', reason: '누출 표식 없음' };
97
+ });
98
+ const brand = measure(() => {
99
+ const result = checkBrand('public-docs', [absolute]);
100
+ if (result.missing || result.files !== 1 || result.rules === 0) return unmeasured('브랜드 규칙 없음 또는 파일 검사 실패');
101
+ const ids = [...new Set(result.findings.map((finding) => finding.id))].slice(0, 3);
102
+ return ids.length
103
+ ? { status: 'fail', reason: `브랜드 규칙: ${ids.join(', ')}` }
104
+ : { status: 'pass', reason: '브랜드 규칙 위반 없음' };
105
+ });
106
+ return { path, criteria: { reproducible, confidential, brand } };
107
+ });
108
+ const statuses = results.flatMap((item) => KEYS.map((key) => item.criteria[key].status));
109
+ debug.log('expose.rubric', 'suggested', {
110
+ docs: results.length, pass: statuses.filter((status) => status === 'pass').length,
111
+ fail: statuses.filter((status) => status === 'fail').length,
112
+ unmeasured: statuses.filter((status) => status === 'n/a').length,
113
+ helpRuns, helpFailures,
114
+ });
115
+ return results;
116
+ }
117
+
118
+ export function run(argv: readonly string[], root = process.cwd(), help?: HelpRunner): number {
119
+ const json = argv.includes('--json');
120
+ let files: string[] | undefined;
121
+ let out: string | undefined;
122
+ for (let i = 0; i < argv.length; i++) {
123
+ const arg = argv[i]!;
124
+ if (arg === '--json') continue;
125
+ if (arg === '--yaml-patch') {
126
+ out = argv[++i];
127
+ if (!out || out.startsWith('--')) throw new Error('--yaml-patch 에 출력 경로가 필요합니다');
128
+ } else if (arg === '--files') {
129
+ files = [];
130
+ while (argv[i + 1] && !argv[i + 1]!.startsWith('--')) files.push(argv[++i]!);
131
+ if (!files.length) throw new Error('--files 에 문서 경로가 필요합니다');
132
+ } else throw new Error(`알 수 없는 옵션: ${arg}`);
133
+ }
134
+ const results = suggestExposeRubric(root, files, help);
135
+ if (out) {
136
+ const target = resolve(root, out);
137
+ const ledger = resolve(root, LEDGER);
138
+ if (target === ledger || realpathOrSelf(target) === realpathOrSelf(ledger) || sameFile(target, ledger)) throw new Error('원장 파일은 덮어쓸 수 없습니다');
139
+ writeFileSync(target, stringify({ entries: results }));
140
+ }
141
+ if (json) console.log(JSON.stringify(results));
142
+ else {
143
+ for (const { path, criteria } of results) console.log(`${path} · ${KEYS.map((key) => `${key} ${criteria[key].status}${criteria[key].status === 'fail' ? `(${criteria[key].reason.split(': ').slice(1).join(': ') || criteria[key].reason})` : ''}`).join(' · ')}`);
144
+ const statuses = results.flatMap((item) => KEYS.map((key) => item.criteria[key].status));
145
+ console.log(`합계 문서 ${results.length} · pass ${statuses.filter((status) => status === 'pass').length} · fail ${statuses.filter((status) => status === 'fail').length} · n/a ${statuses.filter((status) => status === 'n/a').length} · maturity/value/evidence/support 사람 판정 (MK)`);
146
+ }
147
+ return 0;
148
+ }
149
+
150
+ function realpathOrSelf(path: string): string {
151
+ try { return realpathSync(path); } catch { return path; }
152
+ }
153
+
154
+ function sameFile(target: string, ledger: string): boolean {
155
+ const a = statSync(target, { throwIfNoEntry: false });
156
+ const b = statSync(ledger, { throwIfNoEntry: false });
157
+ return !!a && !!b && a.dev === b.dev && a.ino === b.ino;
158
+ }
159
+
160
+ if (import.meta.main) {
161
+ try { process.exitCode = run(process.argv.slice(2)); }
162
+ catch (error) { console.error(error instanceof Error ? error.message : String(error)); process.exitCode = 2; }
163
+ }
@@ -5,6 +5,7 @@ import { readFileSync } from 'node:fs';
5
5
  import { homedir } from 'node:os';
6
6
  import { join } from 'node:path';
7
7
  import { debug } from '../../src/debug/log.js';
8
+ import { readFailureInbox } from '../../src/self-implement/heal-intake.js';
8
9
  import {
9
10
  fillMissingGateFacts,
10
11
  markDeletedUnverified,
@@ -101,6 +102,7 @@ export interface HealRoleDeps {
101
102
  loadLedger?: (runId: string) => RunLedgerEntry[] | null;
102
103
  ledgerDir?: string;
103
104
  search?: (query: string) => unknown;
105
+ stateRoot?: string;
104
106
  }
105
107
 
106
108
  function gitHeadExists(worktree: string, path: string): PathExists {
@@ -243,6 +245,18 @@ export function roleResult(role: string, ctx: GraphContext, deps: HealRoleDeps =
243
245
  const signature = failureOf(ctx);
244
246
  switch (role) {
245
247
  case 'observe-ledger': {
248
+ const selector = asRecord(input.failureEvent);
249
+ if (selector) {
250
+ if (typeof selector.source !== 'string' || typeof selector.ref !== 'string' || !selector.source || !selector.ref) {
251
+ return withAddedFacts({ outcome: 'fail', reason: 'invalid-failure-event-selector' }, []);
252
+ }
253
+ const since = typeof input.since === 'string' ? input.since : undefined;
254
+ const event = readFailureInbox({ since }, deps.stateRoot)
255
+ .filter(row => row.source === selector.source && row.ref === selector.ref)
256
+ .sort((a, b) => Date.parse(b.at) - Date.parse(a.at)).at(0);
257
+ if (!event) return withAddedFacts({ outcome: 'empty', reason: 'inbox-event-not-found' }, []);
258
+ return withAddedFacts({ outcome: 'ok', entries: [{ errorText: event.summary }], event }, ['errorText']);
259
+ }
246
260
  if (!asRecord(input.failure) && Object.keys(failure).length === 0) return withAddedFacts({ outcome: 'empty' }, []);
247
261
  const worktree = typeof input.worktree === 'string' ? input.worktree : '';
248
262
  const paths = signature.unverified ?? [];
@@ -10,6 +10,7 @@ import type { Manifest, Tier } from './standby-snapshot.js';
10
10
  export interface TierVerification {
11
11
  tier: Tier;
12
12
  generation: string | null;
13
+ manifestSha256: string | null;
13
14
  ageMin: number | null;
14
15
  host: string | null;
15
16
  entries: number | null;
@@ -27,7 +28,7 @@ export interface StandbyVerification {
27
28
  reasons: string[];
28
29
  }
29
30
 
30
- export interface VerifyOptions { root: string; tiers: Tier[]; marker?: string; maxAgeMin?: number | Partial<Record<Tier, number>>; now?: Date }
31
+ export interface VerifyOptions { root: string; tiers: Tier[]; generations?: Partial<Record<Tier, string>>; marker?: string; maxAgeMin?: number | Partial<Record<Tier, number>>; now?: Date }
31
32
 
32
33
  function errorCode(error: unknown): unknown {
33
34
  return typeof error === 'object' && error !== null && 'code' in error ? error.code : undefined;
@@ -46,8 +47,8 @@ async function hashFile(path: string): Promise<string> {
46
47
  return hash.digest('hex');
47
48
  }
48
49
 
49
- async function verifyTier(root: string, tier: Tier, maxAgeMin: number | undefined, now: Date): Promise<TierVerification> {
50
- const result: TierVerification = { tier, generation: null, ageMin: null, host: null, entries: null, checked: 0, mismatches: 0, missing: 0, unreadable: false, reasons: [] };
50
+ async function verifyTier(root: string, tier: Tier, maxAgeMin: number | undefined, now: Date, pinnedGeneration?: string): Promise<TierVerification> {
51
+ const result: TierVerification = { tier, generation: null, manifestSha256: null, ageMin: null, host: null, entries: null, checked: 0, mismatches: 0, missing: 0, unreadable: false, reasons: [] };
51
52
  const base = join(root, tier);
52
53
  try {
53
54
  await stat(base);
@@ -58,7 +59,7 @@ async function verifyTier(root: string, tier: Tier, maxAgeMin: number | undefine
58
59
  }
59
60
  let target: string;
60
61
  try {
61
- target = await readlink(join(base, 'latest'));
62
+ target = pinnedGeneration ?? await readlink(join(base, 'latest'));
62
63
  } catch (error) {
63
64
  if (errorCode(error) === 'ENOENT' || errorCode(error) === 'EINVAL') result.reasons.push(`${tier}:latest missing`);
64
65
  else {
@@ -76,10 +77,13 @@ async function verifyTier(root: string, tier: Tier, maxAgeMin: number | undefine
76
77
  let manifest: Manifest;
77
78
  let sums: string;
78
79
  try {
79
- [manifest, sums] = await Promise.all([
80
- readFile(join(dir, 'MANIFEST.json'), 'utf8').then(text => JSON.parse(text) as Manifest),
80
+ const [manifestBytes, sumsText] = await Promise.all([
81
+ readFile(join(dir, 'MANIFEST.json')),
81
82
  readFile(join(dir, 'SHA256SUMS'), 'utf8'),
82
83
  ]);
84
+ manifest = JSON.parse(manifestBytes.toString('utf8')) as Manifest;
85
+ sums = sumsText;
86
+ result.manifestSha256 = createHash('sha256').update(manifestBytes).digest('hex');
83
87
  } catch (error) {
84
88
  if (error instanceof SyntaxError) result.reasons.push(`${tier}:invalid manifest`);
85
89
  else {
@@ -128,7 +132,7 @@ async function verifyTier(root: string, tier: Tier, maxAgeMin: number | undefine
128
132
 
129
133
  export async function verifyStandby(opts: VerifyOptions): Promise<StandbyVerification> {
130
134
  const tiers = await Promise.all(opts.tiers.map(tier => verifyTier(opts.root, tier,
131
- typeof opts.maxAgeMin === 'number' ? opts.maxAgeMin : opts.maxAgeMin?.[tier], opts.now ?? new Date())));
135
+ typeof opts.maxAgeMin === 'number' ? opts.maxAgeMin : opts.maxAgeMin?.[tier], opts.now ?? new Date(), opts.generations?.[tier])));
132
136
  const reasons = tiers.flatMap(tier => tier.reasons);
133
137
  let marker: StandbyVerification['marker'];
134
138
  if (opts.marker !== undefined) {
@@ -0,0 +1,85 @@
1
+ import { mkdirSync, writeFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { debug } from '../../src/debug/log.js';
4
+ import { LessonLedger, type LessonRow, type LessonStatus } from '../../src/lessons/lesson-ledger.js';
5
+
6
+ const statuses: LessonStatus[] = ['candidate', 'open', 'enforced', 'promoted'];
7
+
8
+ function args(argv: string[]): { stateDir?: string; out?: string; json: boolean } {
9
+ const options: { stateDir?: string; out?: string; json: boolean } = { json: false };
10
+ for (let i = 0; i < argv.length; i++) {
11
+ const arg = argv[i];
12
+ if (arg === '--json') options.json = true;
13
+ else if (arg === '--state-dir' || arg === '--out') {
14
+ const value = argv[++i];
15
+ if (!value || value.startsWith('--')) throw new Error(`missing value for ${arg}`);
16
+ if (arg === '--state-dir') options.stateDir = value;
17
+ else options.out = value;
18
+ } else throw new Error(`unknown option: ${arg}`);
19
+ }
20
+ return options;
21
+ }
22
+
23
+ function inlineText(value: string): string {
24
+ return value.replace(/\s+/g, ' ').trim()
25
+ .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
26
+ .replace(/([\\`*_{}\[\]()~!|])/g, '\\$1')
27
+ .replace(/([a-z][a-z\d+.-]*:)(\/\/)|\b(www\.)(?=\S)|(@)(?=\S)/gi,
28
+ (_match, scheme: string | undefined, slashes: string | undefined, www: string | undefined, at: string | undefined) =>
29
+ www ? 'w\u200bww.' : `${scheme ?? at}\u200b${slashes ?? ''}`);
30
+ }
31
+
32
+ function section(title: string, rows: LessonRow[], ledger: LessonLedger, kind: LessonStatus): string[] {
33
+ const lines = [`## ${title}`, ''];
34
+ if (!rows.length) return [...lines, '없음', ''];
35
+ for (const row of rows) {
36
+ const last = ledger.get(row.id).occurrences.at(-1);
37
+ lines.push(`### ${inlineText(row.id)} — ${inlineText(row.incident)}`,
38
+ `- 원인: ${inlineText(row.cause)}`,
39
+ `- 처방: ${inlineText(row.remedy)}`,
40
+ `- 재발: ${row.occurrence_count}회`,
41
+ `- 마지막 발생: ${inlineText(last?.at ?? '없음')} · 출처: ${inlineText(last?.source ?? '없음')}`);
42
+ if (kind === 'candidate') lines.push(`- 반증: ${row.disproof?.trim() ? inlineText(row.disproof) : '반증 없음 — 승격 불가'}`);
43
+ if (kind === 'enforced' || kind === 'promoted') lines.push(`- 강제 자리: ${inlineText(row.enforced_by)}`);
44
+ lines.push('');
45
+ }
46
+ return lines;
47
+ }
48
+
49
+ export function generateHandbookPage(options: { stateDir?: string; out?: string } = {}): {
50
+ total: number; byStatus: Record<LessonStatus, number>; unenforced: number; out: string;
51
+ } | { skipped: 'empty-ledger' } {
52
+ const ledger = new LessonLedger({ stateDir: options.stateDir });
53
+ const rows = ledger.list();
54
+ if (!rows.length) return { skipped: 'empty-ledger' };
55
+ const out = options.out ?? join(dirname(ledger.path), 'handbook-lessons.md');
56
+ const byStatus: Record<LessonStatus, number> = { candidate: 0, open: 0, enforced: 0, promoted: 0 };
57
+ for (const row of rows) byStatus[row.status]++;
58
+ const unenforcedRows = rows.filter(row => row.status === 'open' && !row.enforced_by.trim());
59
+ const unenforced = unenforcedRows.length;
60
+ const lines = [
61
+ '# 교훈', '',
62
+ `생성 시각: ${new Intl.DateTimeFormat('ko-KR', { timeZone: 'Asia/Seoul', dateStyle: 'long', timeStyle: 'long' }).format(new Date())} (KST)`,
63
+ `총 ${rows.length} · ${statuses.map(status => `${status} ${byStatus[status]}`).join(' · ')} · 강제 자리 없음 ${unenforced}`,
64
+ '',
65
+ ...section('승격 후보', rows.filter(row => row.status === 'candidate'), ledger, 'candidate'),
66
+ ...section('강제 자리 없음', unenforcedRows, ledger, 'open'),
67
+ ...section('강제됨', rows.filter(row => row.status === 'enforced'), ledger, 'enforced'),
68
+ ...section('규칙으로 승격됨', rows.filter(row => row.status === 'promoted'), ledger, 'promoted'),
69
+ ];
70
+ mkdirSync(dirname(out), { recursive: true });
71
+ writeFileSync(out, `${lines.join('\n')}\n`, 'utf8');
72
+ debug.log('lessons.handbook', 'rendered', { total: rows.length, byStatus, unenforced, out });
73
+ return { total: rows.length, byStatus, unenforced, out };
74
+ }
75
+
76
+ if (import.meta.main) {
77
+ try {
78
+ const options = args(process.argv.slice(2));
79
+ const result = generateHandbookPage(options);
80
+ console.log(options.json ? JSON.stringify(result) : 'skipped' in result ? `skipped: ${result.skipped}` : `rendered: ${result.out}`);
81
+ } catch (error) {
82
+ console.error(`handbook-page: ${error instanceof Error ? error.message : String(error)}`);
83
+ process.exitCode = 1;
84
+ }
85
+ }
@@ -5,6 +5,7 @@ import { tmpdir } from 'node:os';
5
5
  import { randomUUID } from 'node:crypto';
6
6
  import { dirname, join, resolve } from 'node:path';
7
7
  import { debug } from '../../src/debug/log.js';
8
+ import { landingFreezeMessage, readLandingFreeze } from '../../src/release-loop/landing-freeze.js';
8
9
  import { effectiveInstanceRoot, releaseLedgerRoot } from '../../src/instance/resolve.js';
9
10
  import { getElanousConfigDirOverride } from '../../src/elanous-config-dir.js';
10
11
  import { diffFailures, junitFailures, parseFailures } from './gate-diff';
@@ -32,6 +33,7 @@ export interface GateRunner {
32
33
  }
33
34
  export interface GateOptions {
34
35
  commit: string;
36
+ forceFreeze?: boolean;
35
37
  version: string;
36
38
  baselineVersion?: string;
37
39
  baselineCommit?: string;
@@ -537,6 +539,12 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
537
539
  const start = Date.now();
538
540
  const explicitConfig = getElanousConfigDirOverride();
539
541
  const root = explicitConfig ? effectiveInstanceRoot() : (opts.instanceRoot ?? effectiveInstanceRoot());
542
+ // The freeze is read from the same root the gate runs in (a config-dir override must not move it elsewhere).
543
+ const frozen = readLandingFreeze(root);
544
+ if (frozen) {
545
+ debug.log('release.run', opts.forceFreeze ? 'freeze-forced' : 'frozen', { version: opts.version, reason: frozen.reason, until: frozen.until, node: 'gate' });
546
+ if (!opts.forceFreeze) throw new Error(landingFreezeMessage(frozen));
547
+ }
540
548
  const ledger = explicitConfig ? root : (opts.ledgerRoot ?? releaseLedgerRoot());
541
549
  const roots = [...new Set([ledger, root])];
542
550
  const cachedBaseline = (version: string, commit: string) => {
@@ -766,6 +774,7 @@ export function parseOptions(args: string[], env: NodeJS.ProcessEnv): GateOption
766
774
  commit: typeof fromGraph.commit === 'string' ? fromGraph.commit : '',
767
775
  version: typeof fromGraph.version === 'string' ? fromGraph.version : '',
768
776
  baselineVersion: typeof fromGraph.previousVersion === 'string' ? fromGraph.previousVersion : undefined,
777
+ forceFreeze: fromGraph.forceFreeze === true,
769
778
  baselineCommit: typeof fromGraph.previousCommit === 'string' ? fromGraph.previousCommit : undefined,
770
779
  remote: typeof fromGraph.gateRemote === 'string' ? fromGraph.gateRemote : undefined,
771
780
  remoteMirror: typeof fromGraph.gateRemoteMirror === 'string' ? fromGraph.gateRemoteMirror : undefined,
@@ -3,6 +3,8 @@ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
3
3
  import { tmpdir } from 'node:os';
4
4
  import { join } from 'node:path';
5
5
  import { WARNING_ONLY } from './auto-approve-node.js';
6
+ import { beginLandingMerge, landingFreezeMessage } from '../../src/release-loop/landing-freeze.js';
7
+ import { debug } from '../../src/debug/log.js';
6
8
  import { errorResult, finishNode, lastResult, nodeOutput, readGraphContext, runCommand, type CommandRunner } from './node-verdict.js';
7
9
 
8
10
  export function publicNotes(markdown: string, pages: { pages: Array<{ id: string; slug?: string; source?: string }> }): string {
@@ -64,7 +66,18 @@ export function runPublish(run: CommandRunner = runCommand) {
64
66
  try {
65
67
  const file = join(dir, 'notes.md');
66
68
  writeFileSync(file, body);
67
- const published = run('bun', ['bin/elanous.mjs', 'release', 'publish', '--dir', out, '--notes-file', file, '--yes', '--json']);
69
+ // Same in-flight marker as merges: `freeze on` waits for a publication that already passed this check.
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();
72
+ const frozen = landing.frozen;
73
+ if (frozen) {
74
+ landing.end(); // already released by beginLandingMerge when frozen; kept explicit so no path leaves a marker
75
+ debug.log('release.run', context.input.forceFreeze === true ? 'freeze-forced' : 'frozen', { version, reason: frozen.reason, until: frozen.until, node: 'publish' });
76
+ if (context.input.forceFreeze !== true) return { outcome: 'fail' as const, verdict: 'fail' as const, summary: `publish blocked: ${landingFreezeMessage(frozen)}` };
77
+ }
78
+ let published: ReturnType<typeof run>;
79
+ try { published = run('bun', ['bin/elanous.mjs', 'release', 'publish', '--dir', out, '--notes-file', file, '--yes', '--json']); }
80
+ finally { landing.end(); }
68
81
  if (published.stderr) process.stderr.write(published.stderr);
69
82
  const result = lastResult(published);
70
83
  if (published.status === 1) return { outcome: 'fail' as const, verdict: 'fail' as const, summary: `release publish failed: ${result?.error ?? published.stderr.trim()}` };
@@ -4,6 +4,8 @@ import { getElanousConfigDirOverride } from '../../src/elanous-config-dir.js';
4
4
  import { effectiveInstanceRoot, releaseLedgerRoot } from '../../src/instance/resolve.js';
5
5
  import { userConfigPath } from '../../src/user-config.js';
6
6
  import { checklistGate } from '../../src/release-loop/checklist.js';
7
+ import { isLandingFreezeRefusal, readLandingFreeze, LandingFrozenError } from '../../src/release-loop/landing-freeze.js';
8
+ import { debug } from '../../src/debug/log.js';
7
9
  import { runGraph, type GraphRunState } from '../../src/graph-runner/runner.js';
8
10
 
9
11
  const VERSION = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)$/;
@@ -26,6 +28,7 @@ export interface ReleaseRunInput extends ReleaseLoopConfig {
26
28
  previousVersion: string;
27
29
  cutCommit?: string;
28
30
  gatePodPool: string;
31
+ forceFreeze?: boolean;
29
32
  }
30
33
 
31
34
  export interface UnattendedReleaseDeps {
@@ -34,6 +37,7 @@ export interface UnattendedReleaseDeps {
34
37
  configPath?: string;
35
38
  graph?: (path: string, options: { input: ReleaseRunInput }) => Promise<GraphRunState>;
36
39
  checklist?: typeof checklistGate;
40
+ freezeRoot?: string;
37
41
  }
38
42
 
39
43
  function compareVersions(a: string, b: string): number {
@@ -83,13 +87,31 @@ export function buildReleaseRunInput(version: string, deps: UnattendedReleaseDep
83
87
 
84
88
  /** Fail closed at the entry boundary, before any graph node can change a release. */
85
89
  export async function runUnattendedRelease(
86
- opts: { version: string; dryRun?: boolean; cutCommit?: string },
90
+ opts: { version: string; dryRun?: boolean; cutCommit?: string; forceFreeze?: boolean },
87
91
  deps: UnattendedReleaseDeps = {},
88
92
  ): Promise<{ input: ReleaseRunInput; dryRun: boolean; state?: GraphRunState }> {
93
+ if (!opts.dryRun) {
94
+ const frozen = readLandingFreeze(deps.freezeRoot);
95
+ if (frozen) {
96
+ debug.log('release.run', opts.forceFreeze ? 'freeze-forced' : 'frozen', { version: opts.version, reason: frozen.reason, until: frozen.until });
97
+ if (!opts.forceFreeze) throw new LandingFrozenError(frozen);
98
+ }
99
+ }
89
100
  const input = buildReleaseRunInput(opts.version, deps, opts.cutCommit);
101
+ if (opts.forceFreeze) input.forceFreeze = true;
90
102
  if (opts.dryRun) return { input, dryRun: true };
91
103
  const gate = (deps.checklist ?? checklistGate)(opts.version);
92
104
  if (!gate.ok) throw new Error(`release checklist blocked: ${[...gate.red, ...gate.undecided, ...gate.blocked].join(', ')}`);
105
+ const beforeGraph = readLandingFreeze(deps.freezeRoot);
106
+ if (beforeGraph && !opts.forceFreeze) throw new LandingFrozenError(beforeGraph);
93
107
  const state = await (deps.graph ?? runGraph)(GRAPH, { input });
108
+ // A freeze switched on while the graph ran stops gate or publish inside it. Only a failed gate/publish node whose own
109
+ // record is the freeze refusal is reported as the freeze; any other failure keeps its own reason.
110
+ if (state.status === 'failed' && !opts.forceFreeze) {
111
+ const refused = (state.nodes ?? []).find((node) => (node.nodeId === 'gate' || node.nodeId === 'publish') && !node.ok
112
+ && isLandingFreezeRefusal(`${typeof node.output === 'string' ? node.output : JSON.stringify(node.output ?? '')} ${node.error ?? ''}`));
113
+ const during = refused ? readLandingFreeze(deps.freezeRoot) : null;
114
+ if (refused) throw new LandingFrozenError(during ?? { reason: 'frozen during the run', startedAt: new Date().toISOString(), until: null, by: 'release-run' });
115
+ }
94
116
  return { input, dryRun: false, state };
95
117
  }