elanous 0.2.12 → 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 (534) 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/{9906-482c6619b3fdda20.js → 8060-cc008cc6fedfa818.js} +1 -1
  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-1abfe3aec3be6bdc.js +1 -0
  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-a67e11ba962337b5.js → page-15374909d8f1aebd.js} +1 -1
  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-793fa536b3a6ef37.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-e1e47d8d940aba3d.js +1 -0
  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-e297866051595d81.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/examples/teams/personas/backend.yaml +4 -0
  194. package/examples/teams/personas/pm.yaml +4 -0
  195. package/examples/teams/personas/qa.yaml +4 -0
  196. package/examples/teams/small-product.yaml +20 -0
  197. package/graphs/lecture/lecture-note.yaml +28 -0
  198. package/graphs/lecture/recipes.yaml +17 -0
  199. package/graphs/orchestrator/orchestrator.yaml +59 -0
  200. package/graphs/orchestrator/recipes.yaml +18 -0
  201. package/graphs/release/recipes.yaml +3 -0
  202. package/graphs/release/release-loop.yaml +4 -1
  203. package/graphs/rhythm/daily.yaml +38 -0
  204. package/graphs/rhythm/recipes.yaml +18 -0
  205. package/graphs/rhythm/weekly.yaml +39 -0
  206. package/graphs/video/recipes.yaml +3 -0
  207. package/graphs/video/youtube-summary.yaml +17 -0
  208. package/package.json +2 -1
  209. package/scripts/audit-test-state-writes.ts +72 -17
  210. package/scripts/brand/check.ts +186 -0
  211. package/scripts/context-hooks/emit.ts +36 -14
  212. package/scripts/coord-channel-watch.sh +1 -0
  213. package/scripts/coord-post.sh +11 -1
  214. package/scripts/discord-seat-probe.ts +144 -0
  215. package/scripts/docs-cli-check.ts +23 -6
  216. package/scripts/expose-rubric-check.ts +118 -0
  217. package/scripts/expose-rubric-suggest.ts +163 -0
  218. package/scripts/heal/run-role.ts +14 -0
  219. package/scripts/hq/hq-standby.sh +65 -0
  220. package/scripts/hq/standby-promote.ts +173 -0
  221. package/scripts/hq/standby-role.ts +59 -0
  222. package/scripts/hq/standby-snapshot.ts +226 -0
  223. package/scripts/hq/standby-verify.ts +223 -0
  224. package/scripts/lecture-note/node.ts +256 -0
  225. package/scripts/lessons/handbook-page.ts +85 -0
  226. package/scripts/release-loop/gate-node.ts +62 -15
  227. package/scripts/release-loop/ops-upgrade-node.ts +17 -5
  228. package/scripts/release-loop/publish-node.ts +14 -1
  229. package/scripts/release-loop/unattended-release.ts +23 -1
  230. package/scripts/release-loop/vault-note-node.ts +196 -0
  231. package/scripts/release-story/draft.ts +172 -0
  232. package/scripts/rhythm/daily.ts +383 -0
  233. package/scripts/rhythm/weekly.ts +294 -0
  234. package/scripts/skills/public-check.ts +169 -0
  235. package/scripts/test-deterministic.ts +57 -11
  236. package/scripts/test-diet/effectiveness.ts +195 -0
  237. package/scripts/test-diet/lib.ts +16 -8
  238. package/scripts/test-diet/node.ts +77 -9
  239. package/scripts/ux/demo-rehearsal.ts +2 -2
  240. package/scripts/ux/lib/rehearsal-verdict.ts +1 -0
  241. package/scripts/video/youtube-summary.ts +133 -0
  242. package/skills/diagram-master/SKILL.md +42 -49
  243. package/skills/diagram-master/references/pyproject.toml +4 -1
  244. package/skills/diagram-master/references/routing-guide.md +4 -2
  245. package/skills/grill-me/SKILL.md +1 -0
  246. package/skills/omni-crawl/SKILL.md +16 -16
  247. package/skills/omni-crawl/package-lock.json +1 -544
  248. package/skills/omni-crawl/package.json +3 -4
  249. package/skills/omni-crawl/scripts/main.ts +13 -13
  250. package/skills/omni-crawl/scripts/monitor.ts +8 -8
  251. package/skills/omni-crawl/src/apify.ts +13 -2
  252. package/skills/omni-crawl/src/capture.ts +6 -0
  253. package/skills/omni-crawl/src/registry.ts +2 -2
  254. package/skills/omni-crawl/src/stdout-json.ts +4 -0
  255. package/skills/omni-digest/SKILL.md +10 -11
  256. package/skills/omni-digest/package-lock.json +0 -545
  257. package/skills/omni-digest/package.json +2 -5
  258. package/skills/omni-digest/scripts/main.ts +3 -3
  259. package/skills/omni-digest/src/delegate.ts +5 -5
  260. package/skills/omni-digest/src/fetch.ts +2 -2
  261. package/skills/omni-digest/src/media.ts +25 -11
  262. package/skills/omni-digest/src/visual.ts +3 -2
  263. package/skills/omni-market/scripts/main.ts +1 -1
  264. package/skills/omni-market/scripts/stdout-json.ts +4 -0
  265. package/skills/youtube-master/SKILL.md +13 -13
  266. package/skills/youtube-master/package-lock.json +0 -545
  267. package/skills/youtube-master/package.json +2 -5
  268. package/skills/youtube-master/scripts/main.ts +2 -2
  269. package/skills/youtube-master/scripts/transcribe-local.ts +1 -1
  270. package/skills/youtube-master/src/util.ts +30 -7
  271. package/src/agent/identity-env.ts +11 -0
  272. package/src/agent-mission/driver.ts +15 -10
  273. package/src/agent-mission/duo.ts +94 -0
  274. package/src/agent-mission/pty-decision.ts +1 -1
  275. package/src/autopilot/build/llm-conflict-merge.ts +45 -8
  276. package/src/autopilot/build/merge-intent.ts +53 -0
  277. package/src/autopilot/track-agent-shadow.ts +2 -1
  278. package/src/autopilot/track-agent.ts +4 -1
  279. package/src/boot/daemon-prompt-request.ts +3 -0
  280. package/src/boot/daemon-prompt-turn.ts +6 -2
  281. package/src/boot/daemon-runtime.ts +48 -16
  282. package/src/chat/index.ts +2 -0
  283. package/src/claims/claims-ledger.ts +232 -0
  284. package/src/claims/claims-render.ts +157 -0
  285. package/src/cli/claims-cli.ts +94 -0
  286. package/src/cli/decisions-cli.ts +84 -20
  287. package/src/cli/docs-cli.ts +2 -0
  288. package/src/cli/flow-cli.ts +11 -0
  289. package/src/cli/freeze-cli.ts +45 -0
  290. package/src/cli/lesson-cli.ts +108 -0
  291. package/src/cli/logs-cli.ts +104 -2
  292. package/src/cli/logs-model-input.ts +131 -0
  293. package/src/cli/outputs-cli.ts +92 -0
  294. package/src/cli/persona-cli.ts +29 -0
  295. package/src/cli/pod-cli.ts +10 -3
  296. package/src/cli/pr-cli.ts +44 -2
  297. package/src/cli/private-ledger-commands.ts +54 -4
  298. package/src/cli/pty-drive-verdict.ts +11 -2
  299. package/src/cli/release-cli.ts +248 -45
  300. package/src/cli/rfc-status.ts +91 -0
  301. package/src/connectors/linear.ts +5 -3
  302. package/src/context-bus/condense.ts +189 -0
  303. package/src/context-bus/context-day.ts +82 -0
  304. package/src/context-bus/context-now-public.ts +86 -0
  305. package/src/context-bus/context-now-surfaces.ts +70 -4
  306. package/src/context-bus/context-now.ts +4 -3
  307. package/src/context-bus/long-term-memory.ts +201 -24
  308. package/src/context-bus/nightly-condense.ts +1 -1
  309. package/src/dashboard/index.ts +75 -22
  310. package/src/dashboard/slash-runtime/chat-persona.ts +66 -0
  311. package/src/dashboard/slash-runtime/dashboard-handlers.ts +99 -0
  312. package/src/dashboard/slash-runtime/loops-table.ts +77 -0
  313. package/src/decisions/decision-cards.ts +13 -7
  314. package/src/decisions/decision-ledger.ts +83 -18
  315. package/src/discord/sprint21-runtime.ts +16 -0
  316. package/src/discord-self-message.ts +8 -0
  317. package/src/discord-slash-wire.ts +31 -2
  318. package/src/domains/outbound-alert.ts +1 -1
  319. package/src/elanous-config-dir.ts +29 -4
  320. package/src/execution-loop/launch-gate.ts +29 -14
  321. package/src/flow/cards.ts +239 -0
  322. package/src/graph-kinds/registry.ts +2 -2
  323. package/src/graph-runner/graph-approval-card.ts +2 -2
  324. package/src/graph-runner/graph-grow.ts +64 -11
  325. package/src/graph-runner/graph-tick.ts +13 -2
  326. package/src/graph-runner/runner.ts +111 -22
  327. package/src/harness/harness-cli-command.ts +178 -27
  328. package/src/harness/harness-incidents.ts +43 -12
  329. package/src/harness/harness-pod-dispatch.ts +2 -0
  330. package/src/harness/harness-queue-child.ts +37 -0
  331. package/src/harness/harness-queue.ts +494 -0
  332. package/src/harness/helper-scan.ts +139 -0
  333. package/src/harness/model-input-observation.ts +37 -0
  334. package/src/hooks/github-shadow.ts +72 -2
  335. package/src/hooks/receiver.ts +4 -2
  336. package/src/hq/hq.ts +318 -0
  337. package/src/hq/lease.ts +210 -0
  338. package/src/hq/ledger-write-target.ts +41 -0
  339. package/src/hq/promote.ts +301 -0
  340. package/src/index.ts +281 -27
  341. package/src/intake-plane/discord-seat-work.ts +13 -6
  342. package/src/intake-plane/linear-wish.ts +72 -0
  343. package/src/intake-plane/persona-answer.ts +5 -0
  344. package/src/intake-plane/telegram-seat-work.ts +13 -6
  345. package/src/intake-plane/wish-benchmark-ledger.ts +129 -0
  346. package/src/intake-plane/wish-card.ts +53 -2
  347. package/src/intake-plane/wish-folder.ts +5 -2
  348. package/src/intake-plane/wish-reply.ts +72 -0
  349. package/src/lessons/lesson-import.ts +106 -0
  350. package/src/lessons/lesson-ledger.ts +206 -0
  351. package/src/loops/activity.ts +203 -0
  352. package/src/loops/budget.ts +32 -0
  353. package/src/loops/checker.ts +180 -0
  354. package/src/loops/contract/examples/orchestrator.loop.yaml +1 -1
  355. package/src/loops/loop-cli.ts +72 -6
  356. package/src/loops/neighbors.ts +40 -0
  357. package/src/loops/observe.ts +59 -0
  358. package/src/loops/orchestrator/author-depth.ts +164 -0
  359. package/src/loops/orchestrator/author-input-check.ts +77 -0
  360. package/src/loops/orchestrator/author-ledger.ts +230 -0
  361. package/src/loops/orchestrator/finish-rate.ts +175 -0
  362. package/src/loops/orchestrator/overlap.ts +180 -0
  363. package/src/loops/orchestrator/resync-shadow.ts +227 -0
  364. package/src/loops/orchestrator/tick.ts +302 -0
  365. package/src/loops/orchestrator/traffic.ts +165 -0
  366. package/src/loops/package/verify.ts +125 -0
  367. package/src/loops/registry.ts +454 -9
  368. package/src/loops/status-rows.ts +118 -0
  369. package/src/loops/verdict.ts +34 -0
  370. package/src/maturity/bot-slash-catalog.ts +66 -0
  371. package/src/maturity/feature-maturity.ts +8 -5
  372. package/src/memory.ts +37 -4
  373. package/src/nexus/api/card-followup-route.ts +118 -0
  374. package/src/nexus/api/dashboard.ts +6 -1
  375. package/src/nexus/api/decisions-route.ts +68 -0
  376. package/src/nexus/api/http-server.ts +65 -2
  377. package/src/nexus/api/merge-approvals-gate.ts +1 -1
  378. package/src/nexus/api/personas.ts +2 -1
  379. package/src/nexus/api/schedules-read.ts +5 -1
  380. package/src/nexus/api/skill-exec-route.ts +97 -0
  381. package/src/nexus/api/task-cards-api.ts +44 -5
  382. package/src/nexus/api/workflows.ts +2 -1
  383. package/src/persona/loader.ts +18 -0
  384. package/src/persona/persona-todo.ts +14 -9
  385. package/src/persona/presets.ts +2 -1
  386. package/src/persona/team.ts +105 -0
  387. package/src/persona/types.ts +4 -0
  388. package/src/pod-lease/host-lease.ts +130 -20
  389. package/src/release-loop/auto-start.ts +48 -0
  390. package/src/release-loop/checklist.ts +13 -4
  391. package/src/release-loop/feature-store.ts +28 -16
  392. package/src/release-loop/landed-but-yellow.ts +12 -0
  393. package/src/release-loop/landing-freeze.ts +121 -0
  394. package/src/release-loop/placement.ts +242 -0
  395. package/src/release-loop/release-schedule.ts +3 -1
  396. package/src/seat-dispatch/ceo-commands.ts +2 -2
  397. package/src/seat-dispatch/seat-ask.ts +73 -15
  398. package/src/seat-dispatch/seat-questions.ts +58 -0
  399. package/src/seat-loop/seat-loop.ts +495 -46
  400. package/src/seat-loop/seat-pack.ts +127 -0
  401. package/src/self-dev/ask-launch-flow.ts +3 -0
  402. package/src/self-dev/dev-pipeline.ts +6 -2
  403. package/src/self-dev/draft-sweep.ts +76 -0
  404. package/src/self-dev/run-store.ts +73 -1
  405. package/src/self-dev/run-supervisor.ts +8 -0
  406. package/src/self-dev/self-orchestrate-runtime.ts +5 -1
  407. package/src/self-implement/budget-gate.ts +29 -0
  408. package/src/self-implement/expose-gate.ts +47 -0
  409. package/src/self-implement/frozen-merges.ts +294 -0
  410. package/src/self-implement/gate-cli.ts +10 -4
  411. package/src/self-implement/gate-scope.ts +46 -9
  412. package/src/self-implement/goal-archive.ts +222 -0
  413. package/src/self-implement/goal-lookup.ts +147 -0
  414. package/src/self-implement/heal-intake.ts +79 -0
  415. package/src/self-implement/host-regate.ts +25 -2
  416. package/src/self-implement/l8-merge-queue.ts +157 -1
  417. package/src/self-implement/orchestrator.ts +114 -10
  418. package/src/self-implement/revert-guard.ts +62 -0
  419. package/src/self-implement/run-ledger.ts +1 -1
  420. package/src/self-implement/seams.ts +28 -8
  421. package/src/session/index.ts +2 -0
  422. package/src/steward/launch.ts +5 -1
  423. package/src/steward/readiness.ts +125 -0
  424. package/src/steward/rescue.ts +75 -0
  425. package/src/steward/triage.ts +48 -9
  426. package/src/task-cards/card-cli.ts +86 -0
  427. package/src/task-cards/card-store.ts +10 -6
  428. package/src/task-orchestrator/surfaces/pod-lease.ts +23 -8
  429. package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +46 -6
  430. package/src/task-orchestrator/surfaces/pod-pool.ts +27 -10
  431. package/src/task-orchestrator/surfaces/self-implement-pod.ts +112 -18
  432. package/src/telegram-commands.ts +27 -3
  433. package/src/telegram-dispatch-forward.ts +1 -1
  434. package/src/telegram-fabric-plan.ts +90 -0
  435. package/src/telegram-loops-command.ts +44 -0
  436. package/src/telegram-project-command.ts +10 -4
  437. package/src/telegram.ts +5 -0
  438. package/src/tool-runtime/elanous-control-runtimes.ts +2 -1
  439. package/src/user-config.ts +204 -16
  440. package/src/version/packed-revision.json +1 -1
  441. package/src/workflow-runtime/executor.ts +4 -0
  442. package/src/workflow-runtime/node-catalog.ts +13 -0
  443. package/src/workflow-runtime/nodes/knowledge.ts +38 -0
  444. package/src/workflow-runtime/schema.ts +17 -1
  445. package/src/workflow-runtime/storage.ts +31 -13
  446. package/src/workflow-runtime/types.ts +13 -1
  447. package/apps/pwa/out/_next/static/-6XrAoD3eU7NyMHAE4xPS/_buildManifest.js +0 -1
  448. package/apps/pwa/out/_next/static/chunks/1147-466ec15afacc1a65.js +0 -1
  449. package/apps/pwa/out/_next/static/chunks/1159-0c5ef8bc21cb6afb.js +0 -1
  450. package/apps/pwa/out/_next/static/chunks/1394-ee3795b112907122.js +0 -1
  451. package/apps/pwa/out/_next/static/chunks/2176-c379e3ebce874151.js +0 -1
  452. package/apps/pwa/out/_next/static/chunks/2426.8c6ee0c668d53def.js +0 -1
  453. package/apps/pwa/out/_next/static/chunks/3170-7ec60a56fa0e4728.js +0 -1
  454. package/apps/pwa/out/_next/static/chunks/3278-ae6ef084769f6195.js +0 -1
  455. package/apps/pwa/out/_next/static/chunks/3454-988a53dac60a1763.js +0 -1
  456. package/apps/pwa/out/_next/static/chunks/3638-aa80b4641ea439db.js +0 -1
  457. package/apps/pwa/out/_next/static/chunks/3735-fe3402553ac1c9c6.js +0 -1
  458. package/apps/pwa/out/_next/static/chunks/3992-2e522a8ac5f2afde.js +0 -25
  459. package/apps/pwa/out/_next/static/chunks/4215-c9d91dd644ec1719.js +0 -1
  460. package/apps/pwa/out/_next/static/chunks/4508-5d0161d1e724d735.js +0 -1
  461. package/apps/pwa/out/_next/static/chunks/4733-623a79345b171e1c.js +0 -1
  462. package/apps/pwa/out/_next/static/chunks/4883.3bd969ea1d3aaaff.js +0 -2
  463. package/apps/pwa/out/_next/static/chunks/5532.6899a12958a89b72.js +0 -1
  464. package/apps/pwa/out/_next/static/chunks/5845-e6c003ce8d5fd36a.js +0 -1
  465. package/apps/pwa/out/_next/static/chunks/5978-b27004c677119115.js +0 -1
  466. package/apps/pwa/out/_next/static/chunks/6048-4c5c33579bc4ef78.js +0 -1
  467. package/apps/pwa/out/_next/static/chunks/6303-422dbf0f67c72aac.js +0 -1
  468. package/apps/pwa/out/_next/static/chunks/6751-bb3595948b2e311b.js +0 -1
  469. package/apps/pwa/out/_next/static/chunks/7274-19b7279f18b47ab3.js +0 -1
  470. package/apps/pwa/out/_next/static/chunks/7277-0b2fc7c16ba2d64b.js +0 -1
  471. package/apps/pwa/out/_next/static/chunks/7551-a0e0707b79164ddd.js +0 -1
  472. package/apps/pwa/out/_next/static/chunks/7758-0b018a9e4fc56200.js +0 -1
  473. package/apps/pwa/out/_next/static/chunks/7777-79f845045aa1ed86.js +0 -1
  474. package/apps/pwa/out/_next/static/chunks/8025-cc24ee5d8f2fb6f8.js +0 -1
  475. package/apps/pwa/out/_next/static/chunks/8206-c6684dcde12be721.js +0 -1
  476. package/apps/pwa/out/_next/static/chunks/8605-0259994816604c49.js +0 -1
  477. package/apps/pwa/out/_next/static/chunks/8725.bf46bb1395dd0fa2.js +0 -1
  478. package/apps/pwa/out/_next/static/chunks/8734-252d511cd276c0dc.js +0 -1
  479. package/apps/pwa/out/_next/static/chunks/8834-ca1cb09c1cb1a6bf.js +0 -1
  480. package/apps/pwa/out/_next/static/chunks/9269.d2a4795bc6812717.js +0 -1
  481. package/apps/pwa/out/_next/static/chunks/9538-2816b90923606459.js +0 -1
  482. package/apps/pwa/out/_next/static/chunks/9920-5cfdc5e8fd209825.js +0 -1
  483. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-d16993889b125c42.js +0 -1
  484. package/apps/pwa/out/_next/static/chunks/app/approvals/page-cb169620945d1952.js +0 -1
  485. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-cefe8169e36cf62a.js +0 -1
  486. package/apps/pwa/out/_next/static/chunks/app/board/page-d2e35228bc497119.js +0 -1
  487. package/apps/pwa/out/_next/static/chunks/app/botlab/page-5c05b0da4bc1392c.js +0 -1
  488. package/apps/pwa/out/_next/static/chunks/app/bots/page-e0ccdb576812b523.js +0 -1
  489. package/apps/pwa/out/_next/static/chunks/app/chat/page-813c4a52c9d05ca8.js +0 -1
  490. package/apps/pwa/out/_next/static/chunks/app/consult/page-4731f2c215358937.js +0 -1
  491. package/apps/pwa/out/_next/static/chunks/app/control/page-a022b257950f39e6.js +0 -1
  492. package/apps/pwa/out/_next/static/chunks/app/design-check/page-a2ffc717e04b8b0a.js +0 -1
  493. package/apps/pwa/out/_next/static/chunks/app/editor/page-b59ee4e70fc18fcf.js +0 -1
  494. package/apps/pwa/out/_next/static/chunks/app/field/page-c172dabd796eb7a3.js +0 -1
  495. package/apps/pwa/out/_next/static/chunks/app/healthz/route-d16993889b125c42.js +0 -1
  496. package/apps/pwa/out/_next/static/chunks/app/inside/page-1efd0cb9e0ed9d13.js +0 -1
  497. package/apps/pwa/out/_next/static/chunks/app/intake/page-d5dc7ff69a37b4cf.js +0 -1
  498. package/apps/pwa/out/_next/static/chunks/app/layout-e71c84189e8c683e.js +0 -1
  499. package/apps/pwa/out/_next/static/chunks/app/live/page-1d6a533ce5a6efcb.js +0 -1
  500. package/apps/pwa/out/_next/static/chunks/app/market/page-dfca9b7d2de0b135.js +0 -1
  501. package/apps/pwa/out/_next/static/chunks/app/morning/page-6dd3bfbbde2a5c2c.js +0 -1
  502. package/apps/pwa/out/_next/static/chunks/app/not-found-490653e8d4cc8852.js +0 -1
  503. package/apps/pwa/out/_next/static/chunks/app/observatory/page-77447356baad75d2.js +0 -1
  504. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-29e961191f6057df.js +0 -1
  505. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-c822f55b884d9104.js +0 -1
  506. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-fa4ccfc432424e52.js +0 -1
  507. package/apps/pwa/out/_next/static/chunks/app/outputs/page-6d604227e1664b71.js +0 -1
  508. package/apps/pwa/out/_next/static/chunks/app/page-9c7e8f744a765daf.js +0 -1
  509. package/apps/pwa/out/_next/static/chunks/app/reflection/page-4048543b4b7ad5e0.js +0 -1
  510. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-4c3a3126abed551a.js +0 -1
  511. package/apps/pwa/out/_next/static/chunks/app/sessions/page-64b0aecb1ca75e75.js +0 -1
  512. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-4292d0149020e8a7.js +0 -1
  513. package/apps/pwa/out/_next/static/chunks/app/settings/page-db14d0be7f1c460c.js +0 -1
  514. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-e5d8072cd7cfef73.js +0 -1
  515. package/apps/pwa/out/_next/static/chunks/app/setup/layout-d16993889b125c42.js +0 -1
  516. package/apps/pwa/out/_next/static/chunks/app/setup/page-9547b5cec7cf76bf.js +0 -1
  517. package/apps/pwa/out/_next/static/chunks/app/share/page-e6dcb83265530636.js +0 -1
  518. package/apps/pwa/out/_next/static/chunks/app/showroom/page-45248f08d6aecae5.js +0 -1
  519. package/apps/pwa/out/_next/static/chunks/app/tasks/page-2cddeb9e2aeedc55.js +0 -1
  520. package/apps/pwa/out/_next/static/chunks/app/today/page-baba2e047872fb0f.js +0 -1
  521. package/apps/pwa/out/_next/static/chunks/app/trace/page-e895da4ec9cac345.js +0 -1
  522. package/apps/pwa/out/_next/static/chunks/app/vault/page-4344faf20216233b.js +0 -1
  523. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-698bb29f11864b89.js +0 -1
  524. package/apps/pwa/out/_next/static/chunks/app/workflows/page-bd6a6dc1ddc273f2.js +0 -1
  525. package/apps/pwa/out/_next/static/chunks/app/workspace/page-2e9e2fb01c9da1c9.js +0 -1
  526. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-d62ceb13411ebba4.js +0 -1
  527. package/apps/pwa/out/_next/static/chunks/framework-172630e6aa5e2d7f.js +0 -1
  528. package/apps/pwa/out/_next/static/chunks/main-app-f0fe2d7477845181.js +0 -1
  529. package/apps/pwa/out/_next/static/chunks/main-fc98ac70b44eccb0.js +0 -1
  530. package/apps/pwa/out/_next/static/chunks/pages/_app-5bd7f10271f72413.js +0 -1
  531. package/apps/pwa/out/_next/static/chunks/pages/_error-97d0d85ea05fef9b.js +0 -1
  532. package/apps/pwa/out/_next/static/chunks/webpack-9e4d02d567b98240.js +0 -1
  533. package/apps/pwa/out/_next/static/css/128fb7fb670b2fbf.css +0 -1
  534. /package/apps/pwa/out/_next/static/{-6XrAoD3eU7NyMHAE4xPS → x6mjI6vqOkVVu0jFuDkz1}/_ssgManifest.js +0 -0
@@ -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';
@@ -18,7 +19,7 @@ import { POD_BUN_CACHE_HOST_PATH, parseInstallSeconds, podBunCacheVolume } from
18
19
  import { PodPoolScheduler, parsePodPool, checkPodPool } from '../../src/task-orchestrator/surfaces/pod-pool.js';
19
20
  import { defaultKubectl } from '../../src/task-orchestrator/surfaces/self-implement-pod.js';
20
21
 
21
- interface CommandResult { rc: number; output: string; passedIds?: string[] }
22
+ interface CommandResult { rc: number; output: string; passedIds?: string[]; stalledEnv?: Array<{ file: string; reason: 'no-output' }> }
22
23
  export interface GateRunner {
23
24
  command(cmd: string, args: string[], cwd: string): Promise<CommandResult>;
24
25
  localCommand(cmd: string, args: string[], cwd: string): Promise<CommandResult>;
@@ -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;
@@ -50,6 +52,7 @@ export interface GateResult {
50
52
  fixed: number;
51
53
  knownEnv: number;
52
54
  knownEnvCleared: string[];
55
+ stalledEnv: Array<{ file: string; reason: 'no-output'; local: 'passed' | 'failed' }>;
53
56
  baselineSource?: 'ledger' | 'instance' | 'cut-logs' | 'swept';
54
57
  durationMs: number;
55
58
  error?: string;
@@ -322,13 +325,15 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
322
325
  const ran = clean && lastMatch(clean, /Ran (\d+) tests? across (\d+) files?/);
323
326
  const passes = clean === undefined ? NaN : summaryCount(clean, 'pass');
324
327
  const fails = clean === undefined ? NaN : summaryCount(clean, 'fail');
328
+ const errors = clean === undefined ? NaN : summaryCount(clean, 'error');
325
329
  let reason: ShardReason | undefined;
326
330
  let namedFailures: number | undefined;
327
331
  let unattributedDetail: string | undefined;
328
332
  if (jobFailed || (jobExitCode !== undefined && jobExitCode !== 0) || (rc !== undefined && rc !== 0 && rc !== 1)) reason = 'job-failed';
329
333
  else if (!clean || rc === undefined) reason = 'no-output';
330
334
  else if (!ran || !Number.isFinite(passes) || !Number.isFinite(fails)
331
- || Number(ran[2]) === 0 || Number(ran[2]) > paths.length || Number(ran[1]) === 0) reason = 'incomplete';
335
+ || Number(ran[2]) === 0 || Number(ran[2]) > paths.length
336
+ || (Number(ran[1]) === 0 && !(errors > 0))) reason = 'incomplete';
332
337
  else {
333
338
  try {
334
339
  const attributed = failuresOf({ rc, output: clean }, `pod-${shard} shard`);
@@ -400,7 +405,7 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
400
405
  return { runs: children.flatMap((child) => child.runs), stalled: children.flatMap((child) => child.stalled) };
401
406
  }
402
407
  return { runs: [{ output: clean!, rc: rc!, pass: passes, fail: fails,
403
- errors: Number.isNaN(summaryCount(clean!, 'error')) ? 0 : summaryCount(clean!, 'error'),
408
+ errors: Number.isNaN(errors) ? 0 : errors,
404
409
  ran: Number(ran![1]), files: Number(ran![2]), passedIds: junit ? junitPassedIds(junit) : [] }], stalled: [] };
405
410
  };
406
411
  // 모든 조각이 끝난 뒤 판정한다 — 한 조각의 예외로 먼저 돌아가면 다른 Pod 가 도는 채로 정리가 시작된다(#22002 리뷰 R3).
@@ -413,11 +418,12 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
413
418
  const total = measured.reduce((sum, run) => ({ pass: sum.pass + run.pass, fail: sum.fail + run.fail,
414
419
  errors: sum.errors + run.errors, ran: sum.ran + run.ran, files: sum.files + run.files }),
415
420
  { pass: 0, fail: 0, errors: 0, ran: 0, files: 0 });
416
- if (stalledShards.length) throw new StalledPodShards(stalledShards, total);
417
- if (total.ran === 0 && total.errors === 0) throw new Error('sweep incomplete: no tests ran');
421
+ if (stalledShards.some((item) => item.reason !== 'no-output' || item.files.length !== 1)) throw new StalledPodShards(stalledShards, total);
422
+ if (total.ran === 0 && total.errors === 0 && !stalledShards.length) throw new Error('sweep incomplete: no tests ran');
418
423
  const outputs = measured.map((run) => run.output.replace(/(?:^|\n)\s*\d+ (?:pass|fail|errors?)\s*(?=\n|$)/g, '\n').replace(/Ran \d+ tests? across \d+ files?\.?/g, ''));
419
424
  return { rc: total.fail + total.errors ? 1 : 0, output: outputs.join('\n') + `\n${total.pass} pass\n${total.fail} fail\n${total.errors} errors\nRan ${total.ran} tests across ${total.files} files.\n`,
420
- passedIds: [...new Set(measured.flatMap((run) => run.passedIds))].sort() };
425
+ passedIds: [...new Set(measured.flatMap((run) => run.passedIds))].sort(),
426
+ stalledEnv: stalledShards.flatMap((item) => item.files.map((file) => ({ file, reason: 'no-output' as const }))) };
421
427
  };
422
428
  return {
423
429
  command,
@@ -533,6 +539,12 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
533
539
  const start = Date.now();
534
540
  const explicitConfig = getElanousConfigDirOverride();
535
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
+ }
536
548
  const ledger = explicitConfig ? root : (opts.ledgerRoot ?? releaseLedgerRoot());
537
549
  const roots = [...new Set([ledger, root])];
538
550
  const cachedBaseline = (version: string, commit: string) => {
@@ -550,7 +562,7 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
550
562
  return baselineCommit(root, version);
551
563
  };
552
564
  const repo = resolve(opts.repo ?? process.cwd());
553
- const result: GateResult = { outcome: 'error', commit: opts.commit, introduced: [], preexisting: 0, fixed: 0, knownEnv: 0, knownEnvCleared: [], durationMs: 0 };
565
+ const result: GateResult = { outcome: 'error', commit: opts.commit, introduced: [], preexisting: 0, fixed: 0, knownEnv: 0, knownEnvCleared: [], stalledEnv: [], durationMs: 0 };
554
566
  let work: string | undefined;
555
567
  let baseSnapshot = false;
556
568
  let cutFailures: SweepFailures | undefined;
@@ -601,7 +613,38 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
601
613
  for (const dir of [cutTree, join(cutTree, 'apps/pwa')]) check(await runner.command('bun', ['install'], dir), `bun install ${dir}`);
602
614
  const cutRun = await runner.sweep(cutTree, join(root, 'release', opts.version, 'gate-logs', 'cut'),
603
615
  opts.pod ? { ...opts.pod, durationSource: join(ledger, 'release', opts.baselineVersion, 'gate-logs', 'cut') } : undefined);
604
- const cut = failuresOf(cutRun, 'cut sweep');
616
+ const localStalled = async (run: CommandResult, tree: string, commit: string, label: string) => {
617
+ const stalled = run.stalledEnv ?? [];
618
+ const noCompletedShard = stalled.length && run.rc === 0
619
+ && /(?:^|\n)0 pass\n0 fail\n0 errors\nRan 0 tests across 0 files\./.test(run.output);
620
+ const measured: SweepFailures = noCompletedShard ? { failures: [], errors: [] } : failuresOf(run, `${label} sweep`);
621
+ if (!stalled.length) return measured;
622
+ let hostTree = tree;
623
+ const local: GateResult['stalledEnv'] = [];
624
+ try {
625
+ if (opts.remote) {
626
+ hostTree = join(mkdtempSync(join(tmpdir(), 'release-gate-local-')), 'cut');
627
+ check(await runner.localCommand('git', ['clone', '--quiet', '--shared', '--no-checkout', repo, hostTree], repo), 'host cut clone');
628
+ check(await runner.localCommand('git', ['checkout', '--quiet', '--detach', commit], hostTree), 'host cut checkout');
629
+ for (const dir of [hostTree, join(hostTree, 'apps/pwa')]) check(await runner.localCommand('bun', ['install'], dir), `host bun install ${dir}`);
630
+ }
631
+ for (const { file, reason } of stalled) {
632
+ if (!/^(?:[\w.-]+\/)+[\w.-]+\.test\.tsx?$/.test(file) || file.split('/').includes('..')) throw new Error(`unsafe test path: ${file}`);
633
+ const isolated = await runner.localCommand('bun', ['run', 'test:deterministic', asPath(file)], hostTree);
634
+ const parsed = failuresOf(isolated, `${label} host isolated ${file}`);
635
+ if ([...parsed.failures, ...parsed.errors].some((id) => fileOf(id) !== file)) throw new Error(`host isolated run attributed to another file: ${file}`);
636
+ measured.failures.push(...parsed.failures);
637
+ measured.errors.push(...parsed.errors);
638
+ local.push({ file, reason, local: parsed.failures.length || parsed.errors.length ? 'failed' : 'passed' });
639
+ }
640
+ } finally {
641
+ if (opts.remote && hostTree !== tree) rmSync(dirname(hostTree), { recursive: true, force: true });
642
+ }
643
+ result.stalledEnv.push(...local);
644
+ debug.log('release-loop.gate', 'pod-shard-stalled-env', { files: stalled.map(({ file, reason }) => ({ file, reason })), local });
645
+ return measured;
646
+ };
647
+ const cut = await localStalled(cutRun, cutTree, opts.commit, 'cut');
605
648
  cutFailures = cut;
606
649
  const baseSha = opts.baselineCommit ?? baselineReleaseCommit(opts.baselineVersion);
607
650
  const cached = cachedBaseline(opts.baselineVersion, baseSha);
@@ -630,22 +673,24 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
630
673
  // hour of local baseline after a 23-minute Pod cut).
631
674
  const baseRun = await runner.sweep(await getBaseTree(), join(root, 'release', opts.version, 'gate-logs', 'baseline'),
632
675
  opts.pod ? { ...opts.pod, durationSource: join(ledger, 'release', opts.baselineVersion, 'gate-logs', 'cut') } : undefined);
633
- baseline = failuresOf(baseRun, 'baseline sweep');
676
+ baseline = await localStalled(baseRun, baseTree!, baseSha, 'baseline');
634
677
  }
635
678
  for (const id of [...baseline.failures, ...baseline.errors, ...cut.failures, ...cut.errors]) fileOf(id);
636
679
  const known = opts.pod ? loadEnvKnownFailures(repo) : new Set<string>();
637
680
  const cutIds = [...cut.failures, ...cut.errors];
681
+ const cutHostFailed = new Set(cutRun.stalledEnv?.map(({ file }) => file) ?? []);
638
682
  const baselineIds = [...baseline.failures, ...baseline.errors];
639
683
  const countedCut = splitKnownEnv(cutIds, known);
640
684
  const countedBaseline = splitKnownEnv(baselineIds, known);
641
685
  result.knownEnv = countedCut.knownEnv.length;
642
- result.knownEnvCleared = [...new Set(cutRun.passedIds ?? [])].filter((id) => known.has(id) && !cutIds.includes(id)).sort();
686
+ result.knownEnvCleared = [...new Set(cutRun.passedIds ?? [])].filter((id) => known.has(id) && !cutIds.includes(id) && !cutRun.stalledEnv?.some(({ file }) => fileOf(id) === file)).sort();
643
687
  const diff = diffFailures(countedCut.counted, countedBaseline.counted);
644
688
  result.fixed = diff.fixed.length;
645
689
  result.preexisting = diff.common.length;
646
690
  for (const file of new Set(diff.newFailures.map(fileOf))) {
647
- const isolated = await runner.command('bun', ['run', 'test:deterministic', asPath(file)], cutTree);
648
- const isolatedCut = failuresOf(isolated, `cut isolated ${file}`);
691
+ const isolatedCut = cutHostFailed.has(file)
692
+ ? { failures: cut.failures.filter((id) => fileOf(id) === file), errors: cut.errors.filter((id) => fileOf(id) === file) }
693
+ : failuresOf(await runner.command('bun', ['run', 'test:deterministic', asPath(file)], cutTree), `cut isolated ${file}`);
649
694
  const reproduced = new Set([...isolatedCut.failures, ...isolatedCut.errors]);
650
695
  const candidates = diff.newFailures.filter((id) => fileOf(id) === file && reproduced.has(id));
651
696
  if (candidates.length === 0) continue;
@@ -690,7 +735,7 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
690
735
  if (opts.remote) runner.remoteMirror = undefined;
691
736
  result.durationMs = Date.now() - start;
692
737
  }
693
- if (cutFailures && !result.stalledShards?.length) {
738
+ if (cutFailures && !result.stalledShards?.length && !result.stalledEnv.length) {
694
739
  try {
695
740
  for (const location of roots) {
696
741
  const path = failureFile(location, opts.version);
@@ -714,7 +759,8 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
714
759
  export function graphGateResult(result: GateResult, graph: boolean) {
715
760
  return { ...result, ...(graph && result.outcome === 'regression' ? { outcome: 'fail' as const } : {}),
716
761
  verdict: result.outcome === 'ok' ? 'pass' as const : 'fail' as const,
717
- summary: (result.outcome === 'ok' ? `새 회귀 ${result.introduced.length} · 기존 ${result.preexisting} · 고침 ${result.fixed}` : `게이트 ${result.outcome}: ${result.error ?? result.introduced.length + ' new regressions'}`) + ` · 환경 알려진 실패 ${result.knownEnv}` };
762
+ summary: (result.outcome === 'ok' ? `새 회귀 ${result.introduced.length} · 기존 ${result.preexisting} · 고침 ${result.fixed}` : `게이트 ${result.outcome}: ${result.error ?? result.introduced.length + ' new regressions'}`) + ` · 환경 알려진 실패 ${result.knownEnv}`
763
+ + (result.stalledEnv.length ? ` · 환경 멈춤 ${result.stalledEnv.length} (Pod 밖 통과 ${result.stalledEnv.filter((item) => item.local === 'passed').length})` : '') };
718
764
  }
719
765
 
720
766
  export function parseOptions(args: string[], env: NodeJS.ProcessEnv): GateOptions | 'help' {
@@ -728,6 +774,7 @@ export function parseOptions(args: string[], env: NodeJS.ProcessEnv): GateOption
728
774
  commit: typeof fromGraph.commit === 'string' ? fromGraph.commit : '',
729
775
  version: typeof fromGraph.version === 'string' ? fromGraph.version : '',
730
776
  baselineVersion: typeof fromGraph.previousVersion === 'string' ? fromGraph.previousVersion : undefined,
777
+ forceFreeze: fromGraph.forceFreeze === true,
731
778
  baselineCommit: typeof fromGraph.previousCommit === 'string' ? fromGraph.previousCommit : undefined,
732
779
  remote: typeof fromGraph.gateRemote === 'string' ? fromGraph.gateRemote : undefined,
733
780
  remoteMirror: typeof fromGraph.gateRemoteMirror === 'string' ? fromGraph.gateRemoteMirror : undefined,
@@ -772,7 +819,7 @@ if (import.meta.main) {
772
819
  version = opts.version;
773
820
  result = await judgeGate(opts);
774
821
  } catch (error) {
775
- result = { outcome: 'error', commit: '', introduced: [], preexisting: 0, fixed: 0, knownEnv: 0, knownEnvCleared: [], durationMs: Date.now() - start, error: String(error) };
822
+ result = { outcome: 'error', commit: '', introduced: [], preexisting: 0, fixed: 0, knownEnv: 0, knownEnvCleared: [], stalledEnv: [], durationMs: Date.now() - start, error: String(error) };
776
823
  }
777
824
  if (result.error) console.error(result.error);
778
825
  debug.log('release-loop.gate', 'result', { version, outcome: result.outcome });
@@ -7,7 +7,7 @@ import { errorResult, finishNode, lastResult, nodeOutput, readGraphContext, runC
7
7
 
8
8
  interface HostResult { host: string; ok: boolean; before: string; after: string; error?: string; skipped?: string; hooks?: string; restart?: 'no-service' }
9
9
  /** Where the internal feed is packed from: a checkout of the release tag, never the run's working tree. */
10
- export interface FeedSource { checkout: string; commit: string; cleanup: () => void }
10
+ export interface FeedSource { checkout: string; commit: string; deps: 'linked' | 'installed'; cleanup: () => void }
11
11
  interface OpsUpgradeDeps { exists?: (path: string) => boolean; installSource?: () => string | null; feedSource?: (version: string) => FeedSource }
12
12
 
13
13
  /** Detached worktree at refs/tags/v<version> with the PWA built — the feed ships what was released (10-04 0.2.11:
@@ -26,16 +26,28 @@ export function tagFeedSource(run: CommandRunner, repo: string = process.cwd()):
26
26
  try {
27
27
  const added = run('git', ['worktree', 'add', '--detach', tree, commit], repo);
28
28
  if (added.status !== 0) throw new Error(`태그 체크아웃 실패: ${reason(added)}`);
29
- symlinkSync(join(repo, 'node_modules'), join(tree, 'node_modules'));
30
- if (existsSync(join(repo, 'apps/pwa/node_modules'))) symlinkSync(join(repo, 'apps/pwa/node_modules'), join(tree, 'apps/pwa/node_modules'));
29
+ const sameDeps = ['bun.lock', 'apps/pwa/bun.lock', 'package.json', 'apps/pwa/package.json'].every((file) => {
30
+ try { return readFileSync(join(repo, file), 'utf8') === readFileSync(join(tree, file), 'utf8'); }
31
+ catch { return false; }
32
+ });
33
+ const deps: FeedSource['deps'] = sameDeps ? 'linked' : 'installed';
34
+ if (sameDeps) {
35
+ symlinkSync(join(repo, 'node_modules'), join(tree, 'node_modules'));
36
+ if (existsSync(join(repo, 'apps/pwa/node_modules'))) symlinkSync(join(repo, 'apps/pwa/node_modules'), join(tree, 'apps/pwa/node_modules'));
37
+ } else {
38
+ for (const cwd of [tree, join(tree, 'apps/pwa')]) {
39
+ const installed = run('bun', ['install', '--frozen-lockfile'], cwd, 900_000);
40
+ if (installed.status !== 0) throw new Error(`태그 의존성 설치 실패: ${reason(installed)}`);
41
+ }
42
+ }
31
43
  // The feed carries the PWA build (apps/pwa/out is ignored, so a fresh tag checkout has none until built).
32
44
  const built = run('bun', ['bin/elanous.mjs', 'nexus', 'build'], tree, 900_000);
33
45
  if (built.status !== 0) throw new Error(`태그 PWA 빌드 실패: ${reason(built)}`);
46
+ return { checkout: tree, commit, deps, cleanup };
34
47
  } catch (error) {
35
48
  cleanup();
36
49
  throw error;
37
50
  }
38
- return { checkout: tree, commit, cleanup };
39
51
  };
40
52
  }
41
53
 
@@ -96,7 +108,7 @@ export function runOpsUpgrade(run: CommandRunner = runCommand, deps: OpsUpgradeD
96
108
  const data = lastResult(feed);
97
109
  if (feed.status !== 0 || data?.ok !== true || data.version !== version) feedError = `내부 피드 발행 실패: ${feed.status === 0 && data?.ok === true ? `판 불일치 (${String(data.version)})` : reason(feed)}`;
98
110
  else if (data.commit !== source.commit) feedError = `내부 피드 발행 실패: 묶은 커밋 ${String(data.commit).slice(0, 12)} ≠ 태그 ${source.commit.slice(0, 12)}`;
99
- debug.log('release-loop.ops-upgrade', 'feed-source', { version, commit: source.commit, source: `refs/tags/v${version}`, packed: data?.commit, pwaBuild: data?.pwaBuild });
111
+ debug.log('release-loop.ops-upgrade', 'feed-source', { version, commit: source.commit, source: `refs/tags/v${version}`, deps: source.deps, packed: data?.commit, pwaBuild: data?.pwaBuild });
100
112
  }
101
113
  } catch (error) { feedError = `내부 피드 발행 실패: ${firstLine(String(error))}`; }
102
114
  finally { source?.cleanup(); }
@@ -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
  }
@@ -0,0 +1,196 @@
1
+ #!/usr/bin/env bun
2
+ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { debug } from '../../src/debug/log.js';
5
+ import { effectiveInstanceRoot, prodInstanceRoot } from '../../src/instance/resolve.js';
6
+ import { listChecklist, type ChecklistItem } from '../../src/release-loop/checklist.js';
7
+ import { userConfigPath } from '../../src/user-config.js';
8
+ import { finishNode, readGraphContext, type GraphContext } from './node-verdict.js';
9
+
10
+ const directory = '40. Project/엘라누스 릴리스';
11
+ const indexName = '_색인 — 엘라누스 릴리스.md';
12
+ const kst = new Intl.DateTimeFormat('en-CA', { timeZone: 'Asia/Seoul', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hourCycle: 'h23' });
13
+
14
+ function publishedTime(value: string): { date: string; short: string; full: string } {
15
+ const instant = new Date(value);
16
+ if (Number.isNaN(instant.getTime())) throw new Error(`invalid publishedAt: ${value}`);
17
+ const parts = Object.fromEntries(kst.formatToParts(instant).map(({ type, value: part }) => [type, part]));
18
+ const date = `${parts.year}-${parts.month}-${parts.day}`;
19
+ return { date, short: `${parts.month}-${parts.day} ${parts.hour}:${parts.minute}`, full: `${date} ${parts.hour}:${parts.minute} KST` };
20
+ }
21
+
22
+ function firstClause(title: string): string {
23
+ return title.split(/\s+[—–·]\s+|[\n。]/, 1)[0]!.trim().replace(/\s+/g, ' ');
24
+ }
25
+
26
+ function changeCount(draft: string): number {
27
+ return draft.split(/\r?\n/).filter((line) => /^\s*(?:[-*] |\d+\. )/.test(line)).length;
28
+ }
29
+
30
+ function configuredVaultRoot(configPath: string): string | undefined {
31
+ if (!existsSync(configPath)) return undefined;
32
+ const raw = JSON.parse(readFileSync(configPath, 'utf8')) as { obsidian?: { vaultRoot?: unknown; vault?: unknown } };
33
+ const value = raw.obsidian?.vaultRoot ?? raw.obsidian?.vault;
34
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined;
35
+ }
36
+
37
+ function releaseHistory(context: GraphContext, root: string): string[] {
38
+ const identity = context as GraphContext & { graphId?: string; runId?: string };
39
+ if (identity.graphId !== 'release-loop' || !identity.runId || !/^[\w.-]+$/.test(identity.runId)) return [];
40
+ const dir = join(root, 'graph-runs', 'release-loop');
41
+ type Run = { graphId: string; runId: string; startedAt?: string; input?: { version?: string };
42
+ nodes?: Array<{ nodeId: string; ok: boolean; output?: string; error?: string }> };
43
+ const current = JSON.parse(readFileSync(join(dir, `${identity.runId}.json`), 'utf8')) as Run;
44
+ if (current.graphId !== 'release-loop' || current.runId !== identity.runId || current.input?.version !== context.input.version || !Array.isArray(current.nodes)) {
45
+ throw new Error('release graph run identity mismatch');
46
+ }
47
+ const runs = readdirSync(dir).filter((file) => file.endsWith('.json') && !file.includes('.decision.json') && /^[\w.-]+\.json$/.test(file))
48
+ .map((file) => {
49
+ try { return JSON.parse(readFileSync(join(dir, file), 'utf8')) as Run; }
50
+ catch { return undefined; }
51
+ }).filter((run): run is Run => !!run && run.graphId === 'release-loop' && run.input?.version === context.input.version
52
+ && Array.isArray(run.nodes) && (run.runId === identity.runId || !!current.startedAt && !!run.startedAt && run.startedAt <= current.startedAt));
53
+ const history: string[] = [];
54
+ for (const nodeId of ['gate', 'cutoff']) {
55
+ const attempts = runs.flatMap((run) => run.nodes!.filter((node) => node.nodeId === nodeId));
56
+ if (attempts.length < 2 || !current.nodes.some((node) => node.nodeId === nodeId && node.ok)) continue;
57
+ const failed = attempts.find((node) => !node.ok);
58
+ if (!failed) continue;
59
+ let reason = failed.error;
60
+ if (failed.output) {
61
+ try {
62
+ const data = JSON.parse(failed.output.trim().split('\n').at(-1)!) as { reason?: string; summary?: string };
63
+ reason = data.reason ?? data.summary ?? reason;
64
+ } catch { /* The graph retained an unstructured command failure. */ }
65
+ }
66
+ history.push(`- ${nodeId === 'gate' ? '게이트 재시도' : '재컷'}: ${reason ?? '사유 기록 없음'}`);
67
+ }
68
+ return history;
69
+ }
70
+
71
+ interface ReleaseRecord { version: string; tag: string; sourceCommit: string; publishedAt: string; publicRepo?: string }
72
+ export interface VaultNoteDeps {
73
+ instanceRoot?: string;
74
+ productionRoot?: string;
75
+ vaultRoot?: string;
76
+ configPath?: string;
77
+ checklist?: (version: string) => Pick<ChecklistItem, 'id' | 'title' | 'owner' | 'status'>[];
78
+ }
79
+
80
+ export function featureFamily(id: string): string {
81
+ const stem = id.replace(/[-_]?\d+[a-z]?$/i, '');
82
+ return (stem.split('-')[0] || id).toUpperCase();
83
+ }
84
+
85
+ export function runVaultNote(context: GraphContext = readGraphContext(), deps: VaultNoteDeps = {}) {
86
+ const version = context.input.version;
87
+ try {
88
+ if ((deps.instanceRoot ?? effectiveInstanceRoot()) !== (deps.productionRoot ?? prodInstanceRoot())) {
89
+ debug.log('release.vault-note', 'skipped', { version, reason: 'isolated' });
90
+ return { outcome: 'ok' as const, verdict: 'pass' as const, summary: 'skipped: isolated', skipped: 'isolated' };
91
+ }
92
+ const vault = deps.vaultRoot ?? configuredVaultRoot(deps.configPath ?? userConfigPath());
93
+ if (!vault) {
94
+ // OP 10-04 19:10 ③: no vault configured is not a failure — skip and say so.
95
+ debug.log('release.vault-note', 'skipped', { version, reason: 'no-vault' });
96
+ return { outcome: 'ok' as const, verdict: 'pass' as const, summary: 'skipped: no-vault', skipped: 'no-vault' };
97
+ }
98
+ const root = deps.instanceRoot ?? effectiveInstanceRoot();
99
+ const record: ReleaseRecord = JSON.parse(readFileSync(join(root, 'release', version, 'release.json'), 'utf8'));
100
+ if (record.version !== version || record.tag !== `v${version}` || !/^[0-9a-f]{8,40}$/i.test(record.sourceCommit))
101
+ throw new Error('invalid release.json version, tag or sourceCommit');
102
+ const publicRepo = record.publicRepo;
103
+ if (typeof publicRepo !== 'string' || !/^[\w.-]+\/[\w.-]+$/.test(publicRepo)) throw new Error('release.json publicRepo missing or invalid');
104
+ const published = publishedTime(record.publishedAt);
105
+ const draft = readFileSync(join(root, 'release', version, 'prepared', 'notes-draft.md'), 'utf8');
106
+ const items = (deps.checklist ?? ((v) => listChecklist(v).items))(version).filter((item) => item.status === 'green');
107
+ const oneLine = items.slice(0, 3).map((item) => firstClause(item.title)).join(' · ');
108
+ const location = process.env.ELANOUS_GRAPH_CONTEXT;
109
+ const identity = location && !('runId' in context)
110
+ ? JSON.parse(location.trimStart().startsWith('{') ? location : readFileSync(location, 'utf8')) as { graphId?: string; runId?: string }
111
+ : {};
112
+ const history = releaseHistory({ ...identity, ...context }, root);
113
+ // OP 10-04 19:10 ①: group by feature family (cell id family: HQ-FENCE2 → HQ · LOOP-OBS1 → LOOP · K10i → K), not by seat.
114
+ const groups = new Map<string, typeof items>();
115
+ for (const item of items) {
116
+ const heading = featureFamily(item.id);
117
+ if (!groups.has(heading)) groups.set(heading, []);
118
+ groups.get(heading)!.push(item);
119
+ }
120
+ const abilities = [...groups].flatMap(([heading, group]) => [
121
+ `### ${heading}`, '',
122
+ ...group.flatMap((item) => [
123
+ `- ${item.id} — ${firstClause(item.title)}`,
124
+ ...[...item.title.matchAll(/`([^`]+)`/g)].map((match) => ` - 명령: \`${match[1]}\``),
125
+ '',
126
+ ]),
127
+ ]);
128
+ const target = join(vault, directory);
129
+ const name = `엘라누스 v${version} (${published.date})`;
130
+ // 대표 10-04 19:25: notes live under <major>.x/<major>.<minor>/ · the index stays at the top.
131
+ const [major, minor] = version.split('.');
132
+ const noteDir = join(target, `${major}.x`, `${major}.${minor}`);
133
+ const note = join(noteDir, `${name}.md`);
134
+ const indexPath = join(target, indexName);
135
+ const current = existsSync(indexPath) ? readFileSync(indexPath, 'utf8') : '# 엘라누스 릴리스\n\n| 판 | 발행 | 한 줄 |\n|---|---|---|\n';
136
+ const row = `| [[${name}\\|v${version}]] | ${published.short} | ${oneLine.replace(/\|/g, '\\|')} |`;
137
+ const rowExists = current.split(/\r?\n/).some((line) => line.startsWith('| [[') && line.includes(`|v${version}]]`));
138
+ const header = /\| 판 \| 발행 \| 한 줄 \|\r?\n\|[- :|]+\|\r?\n/;
139
+ if (!rowExists && !header.test(current)) throw new Error(`index table header missing: ${indexPath}`);
140
+ mkdirSync(noteDir, { recursive: true });
141
+ const content = [
142
+ '---', `title: "엘라누스 v${version} — ${oneLine.replace(/"/g, '\\"')}"`,
143
+ `version: ${version}`, `published: ${published.full}`, `cut: ${record.sourceCommit.slice(0, 8)}`,
144
+ 'tags: [elanous, release]', 'links:', ` github: https://github.com/${publicRepo}/releases/tag/${record.tag}`,
145
+ ` npm: elanous@${version}`, '---', '', `# 엘라누스 v${version} — ${oneLine}`, '',
146
+ '## 한 줄', '', oneLine, '',
147
+ '## 발행 경과', '', `- ${published.full} 발행`, ...history, '',
148
+ '## 이제 할 수 있는 것', '', ...abilities, '## 숫자', '',
149
+ `- 변경 ${changeCount(draft)}건`, `- green 칸 ${items.length}개`, '',
150
+ ].join('\n');
151
+ // A handwritten note at the old flat location (e.g. OP's v0.2.13 sample) counts as existing — never duplicate it.
152
+ let existed = existsSync(join(target, `${name}.md`));
153
+ if (!existed) {
154
+ try { writeFileSync(note, content, { flag: 'wx' }); }
155
+ catch (error) {
156
+ if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;
157
+ existed = true;
158
+ }
159
+ }
160
+ if (!rowExists) {
161
+ const tableHeader = header.exec(current)!;
162
+ const tableStart = tableHeader.index + tableHeader[0].length;
163
+ let insertAt = tableStart;
164
+ const publishedKey = `${published.date} ${published.short.slice(6)}`;
165
+ const rows = current.slice(tableStart).matchAll(/^\| \[\[.*?\((\d{4}-\d{2}-\d{2})\)\\?\|v([^\]]+)\]\] \| (\d{2}-\d{2} \d{2}:\d{2}) \|[^\r\n]*(?:\r?\n|$)/gm);
166
+ for (const existing of rows) {
167
+ if (existing.index !== insertAt - tableStart) break;
168
+ const existingKey = `${existing[1]} ${existing[3]!.slice(6)}`;
169
+ if (existingKey < publishedKey || (existingKey === publishedKey && existing[2]!.localeCompare(version, 'en', { numeric: true }) < 0)) break;
170
+ insertAt += existing[0].length;
171
+ }
172
+ const newline = tableHeader[0].includes('\r\n') ? '\r\n' : '\n';
173
+ const separator = insertAt === current.length && !current.endsWith('\n') ? newline : '';
174
+ writeFileSync(indexPath, `${current.slice(0, insertAt)}${separator}${row}${newline}${current.slice(insertAt)}`);
175
+ }
176
+ debug.log('release.vault-note', existed ? 'skipped' : 'written', { version, reason: existed ? 'exists' : 'created' });
177
+ return { outcome: 'ok' as const, verdict: 'pass' as const, summary: existed ? 'skipped: exists' : `vault note written: ${name}`, ...(existed ? { skipped: 'exists' } : {}) };
178
+ } catch (error) {
179
+ const reason = error instanceof Error ? error.message : String(error);
180
+ debug.log('release.vault-note', 'failed', { version, reason });
181
+ return { outcome: 'fail' as const, verdict: 'fail' as const, summary: `vault note failed: ${reason}`, reason };
182
+ }
183
+ }
184
+
185
+ if (import.meta.main) {
186
+ let version = '';
187
+ try {
188
+ const context = readGraphContext();
189
+ version = context.input.version;
190
+ process.exitCode = finishNode('vault-note', version, runVaultNote(context));
191
+ } catch (error) {
192
+ const reason = error instanceof Error ? error.message : String(error);
193
+ debug.log('release.vault-note', 'failed', { version, reason });
194
+ process.exitCode = finishNode('vault-note', version, { outcome: 'fail', verdict: 'fail', summary: `vault note failed: ${reason}`, reason });
195
+ }
196
+ }