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
@@ -1,11 +1,32 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { resolve } from 'node:path';
3
3
  import { Command } from 'commander';
4
+ import { elanousStateRoot } from '../autopilot/state-paths.js';
5
+ import { debug } from '../debug/log.js';
6
+ import { hqCliWriteAllowed, type HqDeps } from '../hq/hq.js';
7
+ import { isIsolatedLedgerWriteRoot } from '../hq/ledger-write-target.js';
4
8
 
5
- // Private local ledgers (decisions · directives) are absent from the public export;
9
+ // Private local ledgers (decisions · directives · claims · lesson) are absent from the public export;
6
10
  // JS-only distributions still register them. Kept out of src/index.ts so the `self`
7
11
  // block stays free of direct sink registration (see self-cli-sink-surface.test.ts).
8
- export function registerPrivateLedgerCommands(program: Command): void {
12
+ const privateLedgerWrites: Record<string, readonly string[]> = {
13
+ claims: ['add', 'verify', 'publish', 'link', 'retract', 'list', 'show', 'render', 'recheck'], // list/show can mark expired evidence stale
14
+ lesson: ['add', 'recur', 'enforce', 'promote'],
15
+ directives: ['sync', 'search', 'list'], // search/list refresh the persistent index too
16
+ };
17
+
18
+ function mayWritePrivateLedger(name: string, args: readonly string[], hqDeps: HqDeps, ledgerRoot: string): boolean {
19
+ const action = args[0];
20
+ if (!action || !privateLedgerWrites[name]?.includes(action)) return true;
21
+ const command = `${name} ${action}`;
22
+ if (isIsolatedLedgerWriteRoot(ledgerRoot)) {
23
+ try { debug.log('hq.fence', 'skipped-isolated', { root: ledgerRoot, command }); } catch { /* observation is fail-soft */ }
24
+ return true;
25
+ }
26
+ return hqCliWriteAllowed(command, false, hqDeps);
27
+ }
28
+
29
+ export function registerPrivateLedgerCommands(program: Command, hqDeps: HqDeps = {}, ledgerRoot = elanousStateRoot()): void {
9
30
  if (existsSync(resolve(import.meta.dir, 'decisions-cli.ts')) || existsSync(resolve(import.meta.dir, 'decisions-cli.js'))) {
10
31
  program.command('decisions').description('대표 결정 로컬 원장').allowUnknownOption().allowExcessArguments(true)
11
32
  .action(async (_opts: unknown, command: Command) => {
@@ -18,13 +39,42 @@ export function registerPrivateLedgerCommands(program: Command): void {
18
39
  await privateProgram.parseAsync(['decisions', ...command.args], { from: 'user' });
19
40
  });
20
41
  }
42
+ if (existsSync(resolve(import.meta.dir, 'claims-cli.ts')) || existsSync(resolve(import.meta.dir, 'claims-cli.js'))) {
43
+ program.command('claims').description('소구점 실측 근거 로컬 원장').allowUnknownOption().allowExcessArguments(true)
44
+ .addHelpText('after', '\n add <id> 주장 등록\n verify <id> 실측 근거 기록\n publish <id> 유효 근거로 공개\n link <id> 체크리스트 연결\n retract <id> 소구점 철회\n list 소구점 목록\n show <id> 근거·연결·이력 보기\n render deck·site·notice 문면 생성\n recheck 낡은 근거 재측 요청\n\n각 명령의 옵션: elanous claims <명령> --help')
45
+ .action(async (_opts: unknown, command: Command) => {
46
+ if (!mayWritePrivateLedger('claims', command.args, hqDeps, ledgerRoot)) return;
47
+ try { await (await import('../domains/standalone-log-sink.js')).registerStandaloneLogSink('claims'); }
48
+ catch { /* Logging must not prevent recording a claim. */ }
49
+ const privateClaimsModule: string = './claims-cli.js';
50
+ const { registerClaimsCommands } = await import(privateClaimsModule) as { registerClaimsCommands: (program: Command, options: { stateDir: string; instanceRoot: string }) => void };
51
+ const privateProgram = new Command();
52
+ registerClaimsCommands(privateProgram, { stateDir: ledgerRoot, instanceRoot: ledgerRoot });
53
+ await privateProgram.parseAsync(['claims', ...command.args], { from: 'user' });
54
+ });
55
+ }
56
+ if (existsSync(resolve(import.meta.dir, 'lesson-cli.ts')) || existsSync(resolve(import.meta.dir, 'lesson-cli.js'))) {
57
+ program.command('lesson').description('교훈 · 재발 로컬 원장').allowUnknownOption().allowExcessArguments(true)
58
+ .addHelpText('after', '\n import 문서 교훈 미리 보기 (--apply 로 적재)\n add <id> 사고·원인·처방 등록\n recur <id> 재발 기록\n enforce <id> 강제 자리 기록\n promote <id> 규칙 경로 기록\n find <query> 교훈 검색\n show <id> 발생·이력 보기\n candidates 반복 교훈 목록\n\n각 명령의 옵션: elanous lesson <명령> --help')
59
+ .action(async (_opts: unknown, command: Command) => {
60
+ if (!mayWritePrivateLedger('lesson', command.args, hqDeps, ledgerRoot)) return;
61
+ try { await (await import('../domains/standalone-log-sink.js')).registerStandaloneLogSink('lesson'); }
62
+ catch { /* Logging must not prevent recording a lesson. */ }
63
+ const privateLessonModule: string = './lesson-cli.js';
64
+ const { registerLessonCommands } = await import(privateLessonModule) as { registerLessonCommands: (program: Command, options: { stateDir: string }) => void };
65
+ const privateProgram = new Command();
66
+ registerLessonCommands(privateProgram, { stateDir: ledgerRoot });
67
+ await privateProgram.parseAsync(['lesson', ...command.args], { from: 'user' });
68
+ });
69
+ }
21
70
  if (existsSync(resolve(import.meta.dir, 'directives-cli.ts')) || existsSync(resolve(import.meta.dir, 'directives-cli.js'))) {
22
71
  program.command('directives').description('대표 지시 로컬 색인·검색').allowUnknownOption().allowExcessArguments(true)
23
72
  .action(async (_opts: unknown, command: Command) => {
73
+ if (!mayWritePrivateLedger('directives', command.args, hqDeps, ledgerRoot)) return;
24
74
  const privateDirectivesModule: string = './directives-cli.js';
25
- const { registerDirectivesCommands } = await import(privateDirectivesModule) as { registerDirectivesCommands: (program: Command) => void };
75
+ const { registerDirectivesCommands } = await import(privateDirectivesModule) as { registerDirectivesCommands: (program: Command, options: { stateDir: string }) => void };
26
76
  const privateProgram = new Command();
27
- registerDirectivesCommands(privateProgram);
77
+ registerDirectivesCommands(privateProgram, { stateDir: ledgerRoot });
28
78
  await privateProgram.parseAsync(['directives', ...command.args], { from: 'user' });
29
79
  });
30
80
  }
@@ -8,6 +8,8 @@ export interface FinalPtyEvidence {
8
8
  readonly screen: string;
9
9
  readonly inputHistory: readonly string[];
10
10
  readonly exitCode: number | null;
11
+ /** A separately verified mission artifact; tool-history failures are not final-answer failures. */
12
+ readonly artifactEvidence?: boolean;
11
13
  }
12
14
 
13
15
  // eslint-disable-next-line no-control-regex
@@ -16,7 +18,7 @@ const FAILURE = /(?:\bcommand not found\b|\bnot recognized as (?:an internal or
16
18
  const SUCCESS = /(?:\b[1-9]\d*\s+(?:pass|passed)\b|\btests?:\s*[1-9]\d*\s+passed\b|\bbuild successful\b|\b(?:successfully|succeeded)\b|\bGOAL-COMPLETE\b)/i;
17
19
 
18
20
  /** Failure wins over success; an exit code of zero is not itself proof of the goal. */
19
- export function verdictForFinalPtyScreen({ screen, inputHistory, exitCode }: FinalPtyEvidence): PtyDriveVerdict {
21
+ export function verdictForFinalPtyScreen({ screen, inputHistory, exitCode, artifactEvidence = false }: FinalPtyEvidence): PtyDriveVerdict {
20
22
  if (exitCode !== null && exitCode !== 0) {
21
23
  return { kind: 'done-but-failed', reason: `child exit code ${exitCode}`, evidence: null };
22
24
  }
@@ -32,7 +34,14 @@ export function verdictForFinalPtyScreen({ screen, inputHistory, exitCode }: Fin
32
34
  }));
33
35
  const lines = screen.replace(ANSI, '').replace(/\r/g, '\n').split('\n')
34
36
  .map((line) => line.trim()).filter((line) => line && !inputs.has(line) && !echoedLiterals.has(line) && ![...inputs].some((input) => line.endsWith(`$ ${input}`) || line.endsWith(`> ${input}`)));
35
- const failure = lines.find((line) => FAILURE.test(line));
37
+ const toolNoMatch = (line: string) =>
38
+ /\b(?:rg|grep|diff)\b.*\bexit(?:ed)?\s*(?:with\s*)?(?:code\s*)?1\b|\bexit(?:ed)?\s*(?:with\s*)?(?:code\s*)?1\b.*\b(?:rg|grep|diff)\b/i.test(line)
39
+ && !/(?:\b(?:error|fatal|exception)\s*:|\b(?:build failed|tests? failed|failed to|permission denied|command not found)\b)/i.test(line);
40
+ // Codex renders tool status as «• Failed (exit 1) …». It is not the agent's final answer.
41
+ // For artifact-backed missions, consider only answer text following the last tool block.
42
+ const lastTool = artifactEvidence ? lines.reduce((index, line, i) => /^•\s+(?:Ran|Failed|Running|Worked)\b/i.test(line) ? i : index, -1) : -1;
43
+ const answer = lastTool < 0 ? lines : lines.slice(lastTool + 1).filter((line) => !/^[└│]/.test(line));
44
+ const failure = (artifactEvidence ? answer : lines).find((line) => FAILURE.test(line) && !toolNoMatch(line));
36
45
  if (failure) return { kind: 'done-but-failed', reason: failure, evidence: null };
37
46
  const evidence = lines.find((line) => SUCCESS.test(line) && !/\b0\s+(?:pass|passed)\b/i.test(line));
38
47
  if (evidence) return { kind: 'success', reason: 'final PTY screen has success evidence', evidence };
@@ -28,14 +28,17 @@ import { addItem, claimItem, decodeClaimHistoryEntry, devVersion, listChecklist,
28
28
  import * as features from '../release-loop/feature-store.js';
29
29
  import { evidencePlan, landedButYellow, type MergedChecklistPr } from '../release-loop/landed-but-yellow.js';
30
30
  import { formatSchedule, getSchedule, listSchedules, setSchedule } from '../release-loop/release-schedule.js';
31
+ import { placeCell, rebalance, seatMove, type PlacementPriority } from '../release-loop/placement.js';
31
32
  import { writeStdoutJson } from './stdout-json.js';
32
33
  import { CliUserError } from './cli-user-error.js';
33
34
  import { hqCliWriteAllowed, type HqDeps } from '../hq/hq.js';
34
35
  import { fileLeaseStore } from '../hq/lease.js';
36
+ import { isIsolatedLedgerWriteRoot } from '../hq/ledger-write-target.js';
35
37
  import { runUnattendedRelease, type UnattendedReleaseDeps } from '../../scripts/release-loop/unattended-release.js';
36
38
  import { cutReleaseBranch } from '../../scripts/release-loop/cut-branch.js';
37
39
  import { releaseReadiness } from '../../scripts/release-loop/release-readiness.js';
38
40
  import { runReleaseIfReady } from './release-run-if-ready.js';
41
+ import { landingFreezeMessage, LandingFrozenError, readLandingFreeze } from '../release-loop/landing-freeze.js';
39
42
 
40
43
  export const DEFAULT_PUBLIC_REPO = 'ElanvitalAI/elanous';
41
44
  const SEMVER = /^\d+\.\d+\.\d+(?:-(?:rc|alpha|beta)\.\d+)?$/;
@@ -459,7 +462,8 @@ async function jsonAction<T>(json: boolean | undefined, body: (log: (l: string)
459
462
  if (json) process.stdout.write(`${JSON.stringify({ ok: ok(r), ...(r as object) })}\n`);
460
463
  if (!ok(r) && !process.exitCode) process.exitCode = 1;
461
464
  } catch (e) {
462
- if (json) process.stdout.write(`${JSON.stringify({ ok: false, error: (e as Error).message })}\n`);
465
+ const frozen = e instanceof LandingFrozenError ? { reason: 'frozen' as const } : {};
466
+ if (json) process.stdout.write(`${JSON.stringify({ ok: false, ...frozen, error: (e as Error).message })}\n`);
463
467
  else console.error(`⛔ ${(e as Error).message}`);
464
468
  process.exitCode = 1;
465
469
  }
@@ -511,6 +515,46 @@ export interface LandedButYellowDeps {
511
515
  released?: typeof features.releasedVersion;
512
516
  }
513
517
 
518
+ const MERGED_PR_LIMIT = 400;
519
+
520
+ function mergedChecklistPrs(since: string, now: Date, run: Runner): { prs: MergedChecklistPr[]; truncated: boolean } {
521
+ const cwd = process.cwd();
522
+ const fetch = (lower: string, upper?: string, limit = MERGED_PR_LIMIT): MergedChecklistPr[] => {
523
+ const search = `merged:>=${lower}${upper ? ` merged:<${upper}` : ''}`;
524
+ const raw = must(run('gh', ['pr', 'list', '--state', 'merged', '--search', search, '--limit', String(limit), '--json', 'number,title,body,mergedAt'], cwd), '병합 PR 조회(gh)');
525
+ const page = JSON.parse(raw) as MergedChecklistPr[];
526
+ if (!Array.isArray(page)) throw new Error('병합 PR 조회(gh): 배열이 아니다');
527
+ return page;
528
+ };
529
+ const unique = new Map<number, MergedChecklistPr>();
530
+ let truncated = false;
531
+ const collect = (page: MergedChecklistPr[]) => {
532
+ for (const pr of page) if (!unique.has(pr.number)) unique.set(pr.number, pr);
533
+ };
534
+ const first = fetch(since);
535
+ if (first.length < MERGED_PR_LIMIT) return { prs: first, truncated: false };
536
+ const lower = Date.parse(`${since}T00:00:00Z`);
537
+ const upper = Math.max(lower + 1000, Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + 1));
538
+ const visit = (start: number, end: number): void => {
539
+ const from = new Date(start).toISOString();
540
+ const to = new Date(end).toISOString();
541
+ const page = fetch(from, to);
542
+ if (page.length < MERGED_PR_LIMIT) { collect(page); return; }
543
+ if (end - start <= 1000) {
544
+ // The second-precision search cannot be subdivided; probe one extra row to distinguish exactly 400 from a real truncation.
545
+ const probe = fetch(from, to, MERGED_PR_LIMIT + 1);
546
+ collect(probe.slice(0, MERGED_PR_LIMIT));
547
+ if (probe.length > MERGED_PR_LIMIT) truncated = true;
548
+ return;
549
+ }
550
+ const middle = start + Math.floor((end - start) / 2000) * 1000;
551
+ visit(start, middle);
552
+ visit(middle, end);
553
+ };
554
+ visit(lower, upper);
555
+ return { prs: [...unique.values()], truncated };
556
+ }
557
+
514
558
  function printLandedRows(rows: ReturnType<typeof landedButYellow>, items: ReturnType<typeof listChecklist>['items']): void {
515
559
  const updatedAtById = new Map(items.map((item) => [item.id, item.updatedAt]));
516
560
  const date = (iso: string) => {
@@ -544,6 +588,13 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
544
588
  const ledgerFenceDeps = (): HqDeps => getElanousConfigDirOverride() && !hqDeps.store && !getUserConfig().hq?.arbiter
545
589
  ? { ...hqDeps, store: fileLeaseStore(join(effectiveInstanceRoot(), 'hq', 'lease.json')), seenPath: join(effectiveInstanceRoot(), 'hq', 'seen-generation'), localPath: join(effectiveInstanceRoot(), 'hq', 'local.json') }
546
590
  : hqDeps;
591
+ const mayWriteLedger = (command: string, override: boolean, ledgerRoot: string) => {
592
+ if (isIsolatedLedgerWriteRoot(ledgerRoot)) {
593
+ try { debug.log('hq.fence', 'skipped-isolated', { root: ledgerRoot, command }); } catch { /* observation is fail-soft */ }
594
+ return true;
595
+ }
596
+ return hqCliWriteAllowed(command, override, ledgerFenceDeps());
597
+ };
547
598
  schedule.command('list').option('--json', '결과 JSON').action((o: { json?: boolean }) => {
548
599
  const rows = listSchedules(scheduleLedgerRoot());
549
600
  if (o.json) console.log(JSON.stringify(rows));
@@ -556,18 +607,45 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
556
607
  if (o.json) console.log(JSON.stringify(row)); else console.log(formatSchedule(row));
557
608
  });
558
609
  schedule.command('set').requiredOption('--version <v>', '갱신할 판')
559
- .option('--cut-at <iso>', '오프셋 포함 컷 시각').option('--land-by <iso>', '오프셋 포함 착지 마감').option('--json', '결과 JSON').option('--hq-override')
560
- .action((o: { version: string; cutAt?: string; landBy?: string; json?: boolean; hqOverride?: boolean }) => {
561
- if (o.cutAt === undefined && o.landBy === undefined) throw new CliUserError('갱신할 시각을 지정하라', '--cut-at <iso> 또는 --land-by <iso>');
562
- if (!hqCliWriteAllowed('release schedule set', Boolean(o.hqOverride), ledgerFenceDeps())) return;
563
- const row = setSchedule(o.version, { cutAt: o.cutAt, landBy: o.landBy }, process.env.ELANOUS_TRACK || 'cli', scheduleLedgerRoot());
610
+ .option('--cut-at <iso>', '오프셋 포함 컷 시각').option('--land-by <iso>', '오프셋 포함 착지 마감')
611
+ .option('--freeze-from <iso>', '동결 시작').option('--freeze-until <iso>', '동결 종료').option('--json', '결과 JSON').option('--hq-override')
612
+ .action((o: { version: string; cutAt?: string; landBy?: string; freezeFrom?: string; freezeUntil?: string; json?: boolean; hqOverride?: boolean }) => {
613
+ if (o.cutAt === undefined && o.landBy === undefined && o.freezeFrom === undefined && o.freezeUntil === undefined) throw new CliUserError('갱신할 시각을 지정하라', '--cut-at <iso> 또는 --land-by <iso>');
614
+ if (!mayWriteLedger('release schedule set', Boolean(o.hqOverride), scheduleLedgerRoot())) return;
615
+ const row = setSchedule(o.version, { cutAt: o.cutAt, landBy: o.landBy, freezeFrom: o.freezeFrom, freezeUntil: o.freezeUntil }, process.env.ELANOUS_TRACK || 'cli', scheduleLedgerRoot());
564
616
  if (o.json) console.log(JSON.stringify(row)); else console.log(formatSchedule(row));
565
617
  });
618
+ release.command('place <id>').description('칸의 우선순위·마감·용량으로 판 배치')
619
+ .option('--priority <priority>', 'P0|P1|P2').option('--dry-run', '이동 없이 배치 보기')
620
+ .action((id: string, opts: { priority?: string; dryRun?: boolean }) => {
621
+ const rows = listSchedules(scheduleLedgerRoot());
622
+ const found = rows.flatMap(({ version }) => listChecklist(version).items.filter((item) => item.id === id));
623
+ if (found.length !== 1) throw new CliUserError(found.length ? `여러 판의 같은 칸: ${id}` : `없는 칸: ${id}`, 'release checklist add 로 칸을 먼저 만든다');
624
+ const item = found[0]!;
625
+ if (!item.owner) throw new CliUserError(`담당 없는 칸: ${id}`);
626
+ const who = process.env.ELANOUS_TRACK;
627
+ if (who && who !== 'OP' && !ownerMatches(item.owner, parseOwner(who).seat)) throw new CliUserError('남의 칸 당기기 거부 — COO 에 요청');
628
+ if (!opts.dryRun && who !== 'OP') throw new CliUserError('판 배치는 COO 에 요청 — 자리는 이유와 함께 release checklist move 로 다음 판에만 이월');
629
+ if (!opts.dryRun && !mayWriteLedger('release place', false, scheduleLedgerRoot())) return;
630
+ const priority = opts.priority ?? item.priority;
631
+ if (!priority) throw new CliUserError(`우선순위가 없는 칸: ${id} — --priority P0|P1|P2 를 지정하라`);
632
+ const result = placeCell({ id, title: item.title, owner: item.owner, priority: priority as PlacementPriority, predecessors: item.predecessors ?? [], deadlineVersion: item.deadlineVersion }, { schedules: rows, dryRun: opts.dryRun, by: who ?? 'cli' });
633
+ console.log(`${opts.dryRun ? '· 드라이런' : '✅'} ${id} ${result.from ?? '-'} → ${result.version} · ${result.reason}${result.displaced.length ? ` · P2 이월 ${result.displaced.map((row) => row.id).join(', ')}` : ''}`);
634
+ });
635
+ release.command('rebalance').description('마감 2시간 전 미시작 칸을 다음 판으로')
636
+ .requiredOption('--version <v>', '대상 판').option('--dry-run', '이동 없이 보기')
637
+ .action((opts: { version: string; dryRun?: boolean }) => {
638
+ if (process.env.ELANOUS_TRACK !== 'OP' && !opts.dryRun) throw new CliUserError('판 이월은 COO 에 요청');
639
+ if (!opts.dryRun && !mayWriteLedger('release rebalance', false, scheduleLedgerRoot())) return;
640
+ const decisions = rebalance(opts.version, { schedules: listSchedules(scheduleLedgerRoot()), dryRun: opts.dryRun });
641
+ for (const row of decisions) console.log(`${opts.dryRun ? '· 드라이런' : '✅'} ${row.id} ${row.from} → ${row.version} · ${row.reason}`);
642
+ if (!decisions.length) console.log('이월할 미시작 칸 없음');
643
+ });
566
644
  const checklist = release.command('checklist').description('판별 확인표 조회·갱신')
567
645
  .option('--version <v>', '판 또는 별칭(기본: package.json 의 개발판에서 -dev.N 제거)')
568
646
  .option('--json', '결과 JSON')
569
647
  .option('--hq-override', '본부 임대 거부를 관측하며 수동 우회');
570
- const mayWrite = (cmd: Command) => hqCliWriteAllowed(`release checklist ${cmd.name()}`, Boolean(cmd.optsWithGlobals().hqOverride), ledgerFenceDeps());
648
+ const mayWrite = (cmd: Command) => mayWriteLedger(`release checklist ${cmd.name()}`, Boolean(cmd.optsWithGlobals().hqOverride), releaseLedgerRoot());
571
649
  const context = (cmd: Command) => {
572
650
  const opts = { ...(cmd.parent?.parent?.opts() as { version?: string; json?: boolean }), ...(cmd.parent?.opts() as { version?: string; json?: boolean }), ...(cmd.opts() as { version?: string; json?: boolean }) };
573
651
  const codenames = checklistCodenames();
@@ -607,11 +685,14 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
607
685
  return;
608
686
  }
609
687
  if (opts.applyEvidence && !opts.dryRun && !mayWrite(cmd)) return;
610
- const since = new Date((landedDeps.now ?? (() => new Date()))().getTime() - 7 * 86_400_000).toISOString().slice(0, 10);
611
- const raw = must((landedDeps.run ?? defaultRunner)('gh', ['pr', 'list', '--state', 'merged', '--search', `merged:>=${since}`, '--limit', '400', '--json', 'number,title,body,mergedAt'], process.cwd()), '병합 PR 조회(gh)');
612
- const prs = JSON.parse(raw) as MergedChecklistPr[];
613
- if (!Array.isArray(prs)) throw new Error('병합 PR 조회(gh): 배열이 아니다');
614
688
  const snapshots = opts.open ? openSnapshots : [(landedDeps.checklist ?? listChecklist)(context(cmd).version)];
689
+ const now = (landedDeps.now ?? (() => new Date()))();
690
+ const updated = snapshots.flatMap(({ items }) => items
691
+ .filter((item) => (item.status === 'yellow' || item.status === 'red') && (opts.owner === undefined || ownerMatches(item.owner, opts.owner)))
692
+ .map((item) => Date.parse(item.updatedAt)).filter((at) => Number.isFinite(at)));
693
+ const since = updated.length ? new Date(updated.reduce((earliest, at) => Math.min(earliest, at))).toISOString().slice(0, 10)
694
+ : new Date(now.getTime() - 7 * 86_400_000).toISOString().slice(0, 10);
695
+ const { prs, truncated } = mergedChecklistPrs(since, now, landedDeps.run ?? defaultRunner);
615
696
  const results: Array<{ version: string; rows?: ReturnType<typeof landedButYellow>; plan?: ReturnType<typeof evidencePlan> }> = [];
616
697
  let totalAdded = 0, totalSkipped = 0;
617
698
  for (const snapshot of snapshots) {
@@ -643,30 +724,32 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
643
724
  if (!json) console.log(`판 ${snapshots.length} · 붙임 합 ${totalAdded} · 건너뜀(언급만) 합 ${totalSkipped}`);
644
725
  else await writeStdoutJson(`${JSON.stringify(results)}\n`);
645
726
  } else if (json) await writeStdoutJson(`${JSON.stringify(opts.applyEvidence || opts.dryRun ? results[0]!.plan : results[0]!.rows)}\n`);
646
- if (prs.length >= 400) console.error('⚠ 병합 PR 400개 상한: 목록이 잘렸을 수 있다');
727
+ if (truncated) console.error('⚠ 병합 PR 400개 상한: 목록이 잘렸을 수 있다');
647
728
  } catch (error) {
648
729
  console.error(`⛔ ${error instanceof Error ? error.message : String(error)}`);
649
730
  process.exitCode = 1;
650
731
  }
651
732
  });
652
733
  withContext(checklist.command('add <id> <title>').description('칸 추가')).option('--owner <owner>', '담당').option('--kind <kind>', 'screen = 다섯 화면 짝 칸 · 근거에 짝: PWA … · 데스크톱 … · 폴드 … · 아이폰 … · 아이패드 … 한 줄')
734
+ .option('--priority <priority>', 'P0|P1|P2').option('--deadline-version <v>', '마감 판').option('--predecessor <id...>', '선행 칸 id')
653
735
  .option('--allow-duplicate-id', '다른 판에 같은 id 가 있어도 경고 후 추가')
654
- .action((id: string, title: string, opts: { owner?: string; kind?: string; allowDuplicateId?: boolean }, cmd: Command) => {
736
+ .action((id: string, title: string, opts: { owner?: string; kind?: string; priority?: string; deadlineVersion?: string; predecessor?: string[]; allowDuplicateId?: boolean }, cmd: Command) => {
655
737
  const { version, json } = context(cmd);
656
738
  if (!mayWrite(cmd)) return;
657
- const data = addItem(version, { id, title, ...(opts.owner !== undefined ? { owner: opts.owner } : {}), ...(opts.kind !== undefined ? { kind: opts.kind as ChecklistKind } : {}) }, { allowDuplicateId: opts.allowDuplicateId });
739
+ const data = addItem(version, { id, title, ...(opts.owner !== undefined ? { owner: opts.owner } : {}), ...(opts.kind !== undefined ? { kind: opts.kind as ChecklistKind } : {}), ...(opts.priority !== undefined ? { priority: opts.priority as PlacementPriority } : {}), ...(opts.deadlineVersion !== undefined ? { deadlineVersion: opts.deadlineVersion } : {}), ...(opts.predecessor !== undefined ? { predecessors: opts.predecessor } : {}) }, { allowDuplicateId: opts.allowDuplicateId });
658
740
  if (json) console.log(JSON.stringify(data)); else console.log(`✅ ${id} 추가`);
659
741
  });
660
742
  withContext(checklist.command('set <id>').description('칸 상태·근거·담당·처분 갱신'))
661
743
  .option('--status <status>', 'green|yellow|red|done').option('--evidence <evidence>', '근거').option('--owner <owner>', '담당')
662
744
  .option('--disposition <disposition>', 'move|known-issue|block').option('--kind <kind>', 'screen = 다섯 화면 짝 칸 · 근거에 짝: PWA … · 데스크톱 … · 폴드 … · 아이폰 … · 아이패드 … 한 줄')
663
- .action((id: string, opts: { status?: string; evidence?: string; owner?: string; disposition?: string; kind?: string }, cmd: Command) => {
745
+ .option('--priority <priority>', 'P0|P1|P2').option('--deadline-version <v>', '마감 판').option('--predecessor <id...>', '선행 칸 id')
746
+ .action((id: string, opts: { status?: string; evidence?: string; owner?: string; disposition?: string; kind?: string; priority?: string; deadlineVersion?: string; predecessor?: string[] }, cmd: Command) => {
664
747
  const { version, json } = context(cmd);
665
748
  if (opts.status !== undefined && !['green', 'yellow', 'red', 'done'].includes(opts.status)) throw new CliUserError(`잘못된 상태: ${opts.status}`, 'green|yellow|red|done');
666
749
  if (opts.disposition !== undefined && !['move', 'known-issue', 'block'].includes(opts.disposition)) throw new CliUserError(`잘못된 처분: ${opts.disposition}`, 'move|known-issue|block');
667
- if (opts.status === undefined && opts.evidence === undefined && opts.owner === undefined && opts.disposition === undefined && opts.kind === undefined) throw new CliUserError('갱신할 칸을 지정하라', '--status · --evidence · --owner · --disposition · --kind 중 하나');
750
+ if (opts.status === undefined && opts.evidence === undefined && opts.owner === undefined && opts.disposition === undefined && opts.kind === undefined && opts.priority === undefined && opts.deadlineVersion === undefined && opts.predecessor === undefined) throw new CliUserError('갱신할 칸을 지정하라', '--status · --evidence · --owner · --disposition · --kind · --priority · --deadline-version · --predecessor 중 하나');
668
751
  if (!mayWrite(cmd)) return;
669
- const data = setItem(version, id, { ...(opts.status !== undefined ? { status: opts.status as ChecklistStatus } : {}), ...(opts.evidence !== undefined ? { evidence: opts.evidence } : {}), ...(opts.owner !== undefined ? { owner: opts.owner } : {}), ...(opts.disposition !== undefined ? { disposition: opts.disposition as ChecklistDisposition } : {}), ...(opts.kind !== undefined ? { kind: opts.kind as ChecklistKind } : {}) }, process.env.ELANOUS_TRACK || 'cli');
752
+ const data = setItem(version, id, { ...(opts.status !== undefined ? { status: opts.status as ChecklistStatus } : {}), ...(opts.evidence !== undefined ? { evidence: opts.evidence } : {}), ...(opts.owner !== undefined ? { owner: opts.owner } : {}), ...(opts.disposition !== undefined ? { disposition: opts.disposition as ChecklistDisposition } : {}), ...(opts.kind !== undefined ? { kind: opts.kind as ChecklistKind } : {}), ...(opts.priority !== undefined ? { priority: opts.priority as PlacementPriority } : {}), ...(opts.deadlineVersion !== undefined ? { deadlineVersion: opts.deadlineVersion } : {}), ...(opts.predecessor !== undefined ? { predecessors: opts.predecessor } : {}) }, process.env.ELANOUS_TRACK || 'cli');
670
753
  const item = data.items.find((entry) => entry.id === id);
671
754
  const why = item?.kind === 'screen' && item.status === 'green' ? parityGap(item.evidence) : null;
672
755
  if (why) {
@@ -701,8 +784,15 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
701
784
  .action((id: string, opts: { from: string; to: string; reason?: string }, cmd: Command) => {
702
785
  const { codenames, json } = context(cmd);
703
786
  const from = checklistVersion(opts.from, codenames), to = checklistVersion(opts.to, codenames);
787
+ if (!opts.reason?.trim()) throw new CliUserError('이동 이유가 필요하다');
704
788
  if (!mayWrite(cmd)) return;
705
- features.move(id, from, to, actor(), undefined, undefined, opts.reason);
789
+ if (actor() === 'cli') {
790
+ // No seat identity (ELANOUS_TRACK unset): keep the plain move so seat sessions without the variable still work, but say so.
791
+ console.error('⚠ 자리 신원 없음(ELANOUS_TRACK 미설정) — 자리 규칙(자기 칸 · 다음 판 · 용량) 검사 없이 옮긴다');
792
+ debug.log('release.placement', 'anonymous-move', { id, from, to });
793
+ features.move(id, from, to, actor(), undefined, undefined, opts.reason);
794
+ } else if (actor() !== 'OP') seatMove(id, from, to, actor(), opts.reason, { schedules: listSchedules(scheduleLedgerRoot()) });
795
+ else features.move(id, from, to, actor(), undefined, undefined, opts.reason);
706
796
  if (json) console.log(JSON.stringify(listChecklist(to))); else console.log(`✅ ${id} ${from} → ${to}`);
707
797
  });
708
798
  withContext(checklist.command('retitle <id> <title>').description('칸 제목 수정')).action((id: string, title: string, _opts: unknown, cmd: Command) => {
@@ -813,14 +903,40 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
813
903
  .option('--cut-commit <sha>', 'origin/main HEAD 대신 컷으로 쓸 커밋')
814
904
  .option('--dry-run', '체크리스트·그래프 실행 없이 전체 입력 보기')
815
905
  .option('--if-ready', '준비되지 않았으면 사유를 출력하고 성공으로 건너뛴다')
906
+ .option('--force-freeze', '동결 중 게이트·발행 강행(관측 기록)')
816
907
  .option('--json', '결과 한 줄 JSON(stdout) · 사람 줄은 stderr')
817
- .action(async (o: { version: string; cutCommit?: string; dryRun?: boolean; ifReady?: boolean; json?: boolean }) => {
908
+ .action(async (o: { version: string; cutCommit?: string; dryRun?: boolean; ifReady?: boolean; forceFreeze?: boolean; json?: boolean }) => {
909
+ if (!o.dryRun) {
910
+ try {
911
+ const frozen = readLandingFreeze(releaseRunDeps.freezeRoot);
912
+ if (frozen) {
913
+ debug.log('release.run', o.forceFreeze ? 'freeze-forced' : 'frozen', { version: o.version, reason: frozen.reason, until: frozen.until });
914
+ if (!o.forceFreeze) {
915
+ // `--if-ready` (the scheduled path) defers quietly; a direct run fails — the release run is run again after `freeze off`.
916
+ if (o.ifReady) {
917
+ if (o.json) await writeStdoutJson(`${JSON.stringify({ ok: true, skipped: true, reason: 'frozen', detail: landingFreezeMessage(frozen) })}\n`);
918
+ else console.log(`· ${landingFreezeMessage(frozen)} · 릴리스 루프 연기`);
919
+ } else {
920
+ if (o.json) await writeStdoutJson(`${JSON.stringify({ ok: false, reason: 'frozen', error: landingFreezeMessage(frozen) })}\n`);
921
+ else console.error(`⛔ ${landingFreezeMessage(frozen)} · 릴리스 루프 거부(--force-freeze 로만 강행)`);
922
+ process.exitCode = 1;
923
+ }
924
+ return;
925
+ }
926
+ }
927
+ } catch (e) {
928
+ if (o.json) await writeStdoutJson(`${JSON.stringify({ ok: false, error: (e as Error).message })}\n`);
929
+ else console.error(`⛔ ${(e as Error).message}`);
930
+ process.exitCode = 1;
931
+ return;
932
+ }
933
+ }
818
934
  if (o.ifReady && !o.dryRun) {
819
935
  try {
820
936
  const outcome = await runReleaseIfReady(o.version, {
821
937
  ledgerRoot: releaseRunDeps.ledgerRoot,
822
938
  readiness: (version, options) => releaseReadiness(version, { ledgerRoot: releaseRunDeps.ledgerRoot, checklist: releaseRunDeps.checklist, ...options }),
823
- run: () => runUnattendedRelease({ version: o.version, cutCommit: o.cutCommit }, releaseRunDeps),
939
+ run: () => runUnattendedRelease({ version: o.version, cutCommit: o.cutCommit, forceFreeze: o.forceFreeze }, releaseRunDeps),
824
940
  });
825
941
  if (outcome.skipped) {
826
942
  if (o.json) await writeStdoutJson(`${JSON.stringify(outcome)}\n`);
@@ -833,6 +949,13 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
833
949
  if (o.json) await writeStdoutJson(`${JSON.stringify({ ok, ...result })}\n`);
834
950
  if (!ok && !process.exitCode) process.exitCode = 1;
835
951
  } catch (e) {
952
+ if (e instanceof LandingFrozenError) {
953
+ // A freeze switched on after the entry check still defers the scheduled run rather than failing it.
954
+ debug.log('release.run', 'frozen', { version: o.version, reason: e.freeze.reason, until: e.freeze.until, at: 'run-boundary' });
955
+ if (o.json) await writeStdoutJson(`${JSON.stringify({ ok: true, skipped: true, reason: 'frozen', detail: e.message })}\n`);
956
+ else console.log(`· ${e.message} · 릴리스 루프 연기`);
957
+ return;
958
+ }
836
959
  if (o.json) await writeStdoutJson(`${JSON.stringify({ ok: false, error: (e as Error).message })}\n`);
837
960
  else console.error(`⛔ ${(e as Error).message}`);
838
961
  process.exitCode = 1;
@@ -840,7 +963,7 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
840
963
  return;
841
964
  }
842
965
  await jsonAction(o.json, async (log) => {
843
- const result = await runUnattendedRelease({ version: o.version, dryRun: o.dryRun, cutCommit: o.cutCommit }, releaseRunDeps);
966
+ const result = await runUnattendedRelease({ version: o.version, dryRun: o.dryRun, cutCommit: o.cutCommit, forceFreeze: o.forceFreeze }, releaseRunDeps);
844
967
  log(`${result.dryRun ? '· 드라이런' : '▶ 릴리스 루프'} ${result.input.version} · 입력 ${JSON.stringify(result.input)}`);
845
968
  return result;
846
969
  }, (r) => r.dryRun || r.state?.status === 'done' || r.state?.status === 'awaiting-approval');
@@ -0,0 +1,91 @@
1
+ import type { Command } from 'commander';
2
+ import { readdirSync, readFileSync } from 'node:fs';
3
+ import { join, relative, resolve } from 'node:path';
4
+ import YAML from 'yaml';
5
+
6
+ export const RFC_STATUSES = ['unknown', 'proposed', 'adopted', 'completed', 'superseded', 'rejected'] as const;
7
+ export type RfcStatus = typeof RFC_STATUSES[number];
8
+ export interface RfcRow {
9
+ path: string;
10
+ status: RfcStatus;
11
+ owner: string | null;
12
+ card: string | null;
13
+ supersededBy: string | null;
14
+ handbook: string | null;
15
+ issue: string | null;
16
+ /** Legacy top-level `status:` / `owner:` values, verbatim: evidence for a human migration, never the lifecycle status. */
17
+ legacyStatus: string | null;
18
+ legacyOwner: string | null;
19
+ }
20
+
21
+ function rfcPaths(dir: string): string[] {
22
+ return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
23
+ const path = join(dir, entry.name);
24
+ if (entry.isDirectory()) return rfcPaths(path);
25
+ return entry.isFile() && /^RFC-.*\.md$/.test(entry.name) ? [path] : [];
26
+ });
27
+ }
28
+
29
+ function field(value: unknown): string | null {
30
+ return typeof value === 'string' && value.trim() && value.trim().toLowerCase() !== 'unknown' ? value.trim() : null;
31
+ }
32
+
33
+ /** Explicit lifecycle fields only: prose, old `status: rfc`, and file location do not establish adoption. */
34
+ export function readRfcRow(text: string, path: string): RfcRow {
35
+ const match = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(text);
36
+ let meta: Record<string, unknown> = {};
37
+ let issue: string | null = null;
38
+ let legacyStatus: string | null = null;
39
+ let legacyOwner: string | null = null;
40
+ if (!match) issue = 'lifecycle frontmatter missing';
41
+ else {
42
+ // Legacy RFC frontmatter can contain non-YAML prose. Parse only the new, top-level rfc_* declarations.
43
+ legacyStatus = /^status:\s*(.+)$/m.exec(match[1]!)?.[1]?.trim().replace(/^["']|["']$/g, '') || null;
44
+ legacyOwner = /^owner:\s*(.+)$/m.exec(match[1]!)?.[1]?.trim().replace(/^["']|["']$/g, '') || null;
45
+ const declarations = [...match[1]!.matchAll(/^(rfc_status|rfc_owner|rfc_card|rfc_superseded_by|rfc_handbook):\s*(.*)$/gm)];
46
+ for (const [, key, raw] of declarations) {
47
+ try { meta[key!] = YAML.parse(raw!); }
48
+ catch { issue = 'invalid lifecycle frontmatter'; }
49
+ }
50
+ }
51
+ const rawStatus = meta.rfc_status;
52
+ const status: RfcStatus = typeof rawStatus === 'string' && RFC_STATUSES.includes(rawStatus as RfcStatus)
53
+ ? rawStatus as RfcStatus : 'unknown';
54
+ if (rawStatus !== undefined && status === 'unknown' && rawStatus !== 'unknown') issue = 'invalid rfc_status';
55
+ return {
56
+ path, status, owner: field(meta.rfc_owner), card: field(meta.rfc_card),
57
+ supersededBy: field(meta.rfc_superseded_by), handbook: field(meta.rfc_handbook), issue, legacyStatus, legacyOwner,
58
+ };
59
+ }
60
+
61
+ export function scanRfcs(repoRoot: string): RfcRow[] {
62
+ const root = resolve(repoRoot);
63
+ return rfcPaths(join(root, 'docs')).map((file) => readRfcRow(readFileSync(file, 'utf8'), relative(root, file).replaceAll('\\', '/')))
64
+ .sort((a, b) => a.path.localeCompare(b.path, 'en'));
65
+ }
66
+
67
+ /** A missing declaration is not proof that a card does not exist; an adopted RFC with an explicit null is. */
68
+ export function adoptedWithoutCard(rows: readonly RfcRow[]): RfcRow[] {
69
+ return rows.filter((row) => row.status === 'adopted' && row.card === null);
70
+ }
71
+
72
+ export function runRfcStatus(options: { json?: boolean; missingCards?: boolean }, root = process.cwd(),
73
+ write: (text: string) => void = (text) => process.stdout.write(text)): void {
74
+ const all = scanRfcs(root);
75
+ const rows = options.missingCards ? adoptedWithoutCard(all) : all;
76
+ if (options.json) {
77
+ write(`${JSON.stringify({ total: all.length, adoptedWithoutCard: adoptedWithoutCard(all).length, rows }, null, 2)}\n`);
78
+ return;
79
+ }
80
+ write(`RFC ${all.length} · 채택·칸 없음 ${adoptedWithoutCard(all).length} · 표시 ${rows.length}\n`);
81
+ write('상태\t담당\t칸\t대체 문서\t핸드북\t옛 상태\t옛 담당\t경로\t메타 오류\n');
82
+ for (const row of rows) write(`${row.status}\t${row.owner ?? '모름'}\t${row.card ?? '모름'}\t${row.supersededBy ?? '모름'}\t${row.handbook ?? '모름'}\t${row.legacyStatus ?? '-'}\t${row.legacyOwner ?? '-'}\t${row.path}\t${row.issue ?? '-'}\n`);
83
+ }
84
+
85
+ export function registerRfcStatusCommand(docs: Command): void {
86
+ docs.command('rfc-status')
87
+ .description('전체 RFC 생애주기 표 (RFC frontmatter만 읽음)')
88
+ .option('--json', '기계가 읽을 JSON')
89
+ .option('--missing-cards', '채택됐으나 칸이 기록되지 않은 RFC만')
90
+ .action((options: { json?: boolean; missingCards?: boolean }) => runRfcStatus(options));
91
+ }
@@ -68,8 +68,8 @@ export function parseLinearWebhook(body: unknown, deliveryId: string, ledger?: E
68
68
  return isLinearProjectionEcho(event, ledger ?? new EventLedger()) ? null : event;
69
69
  }
70
70
 
71
- export async function fetchLinearIssues({ apiKey, teamKey, labelOrPrefix, since, fetch: fetchFn = fetch, ledger }: {
72
- apiKey: string; teamKey: string; labelOrPrefix?: string; since?: string; fetch?: typeof fetch; ledger?: EventLedger;
71
+ export async function fetchLinearIssues({ apiKey, teamKey, labelOrPrefix, excludeLabel, since, fetch: fetchFn = fetch, ledger }: {
72
+ apiKey: string; teamKey: string; labelOrPrefix?: string; excludeLabel?: string; since?: string; fetch?: typeof fetch; ledger?: EventLedger;
73
73
  }): Promise<ExternalTaskEvent[]> {
74
74
  const query = `query ConnectorIssues($teamKey: String!, $after: String) {
75
75
  issues(filter: { team: { key: { eq: $teamKey } } }, first: 100, after: $after) {
@@ -106,8 +106,10 @@ export async function fetchLinearIssues({ apiKey, teamKey, labelOrPrefix, since,
106
106
  const issue = asIssue(node);
107
107
  if (!issue || (since && issue.updatedAt < since) ||
108
108
  ['completed', 'canceled', 'duplicate'].includes((node.state as { type?: string } | undefined)?.type ?? '')) continue;
109
+ const labels = (node.labels as { nodes?: Array<{ name: string }> } | undefined)?.nodes;
110
+ if (excludeLabel && labels?.some(label => label.name.toLowerCase() === excludeLabel.toLowerCase())) continue;
109
111
  if (labelOrPrefix && !issue.title.startsWith(labelOrPrefix) &&
110
- !(node.labels as { nodes?: Array<{ name: string }> } | undefined)?.nodes?.some(label => label.name === labelOrPrefix)) continue;
112
+ !labels?.some(label => label.name === labelOrPrefix)) continue;
111
113
  const event: ExternalTaskEvent = { provider: 'linear', eventId: `${issue.id}:${issue.updatedAt}`, kind: 'updated',
112
114
  ref: issue.id, identifier: issue.identifier, title: issue.title, body: issue.description ?? '',
113
115
  url: issue.url ?? '', priority: priorityOf(issue.priority), occurredAt: issue.updatedAt };
@@ -1,32 +1,22 @@
1
- import { Database } from 'bun:sqlite';
2
- import { closeSync, constants, existsSync, fstatSync, lstatSync, mkdirSync, openSync } from 'node:fs';
1
+ import { closeSync, constants, fstatSync, lstatSync, mkdirSync, openSync } from 'node:fs';
3
2
  import { dlopen, FFIType } from 'bun:ffi';
4
3
  import { join } from 'node:path';
5
4
  import { listMemories, memoryRoot, saveMemory, type MemoryEntry } from '../memory.js';
6
- import { surfaceEventsDbPath } from '../domains/surface-events.js';
7
5
  import { DecisionLedger } from '../decisions/decision-ledger.js';
8
- import { listCoordEvents } from './coord-events.js';
9
- import { condenseContextWindowWithReport, type CondenseSkipped, type MemoryDeps, type MemoryItem, type MemorySource, type MemorySummary } from './long-term-memory.js';
6
+ import { condenseContextWindowWithReport, readCondenseEvents, type CondenseFunnel, type CondenseSkipped, type MemoryDeps, type MemoryItem, type MemorySource, type MemorySummary } from './long-term-memory.js';
10
7
 
11
8
  const summarize = async (source: MemorySource): Promise<MemorySummary> =>
12
9
  (await import('./nightly-condense.js')).summarize(source);
13
10
 
14
- /** Reuse the context-bus reader against a strictly read-only SQLite handle. */
15
- function readContextEvents(since: string) {
16
- const path = surfaceEventsDbPath();
17
- if (!existsSync(path)) return [];
18
- const db = new Database(path, { readonly: true, strict: true });
19
- try { return listCoordEvents({ since }, { db }); }
20
- finally { db.close(); }
21
- }
22
-
23
11
  export interface CondenseReport {
24
12
  since: string;
25
13
  until: string;
26
14
  cards: MemoryItem[];
27
15
  conflicts: number;
28
16
  skipped: CondenseSkipped;
17
+ funnel: CondenseFunnel;
29
18
  folded: number;
19
+ droppedHarnessChild: number;
30
20
  markdown: string;
31
21
  applied?: { written: number; retired: number; decisions: string[] };
32
22
  }
@@ -35,12 +25,17 @@ function mdCell(value: string): string {
35
25
  return value.replaceAll('|', '\\|').replaceAll('\n', ' ');
36
26
  }
37
27
 
38
- export function renderCondenseMarkdown(report: Pick<CondenseReport, 'since' | 'until' | 'cards' | 'conflicts' | 'skipped'>): string {
28
+ export function renderCondenseMarkdown(report: Pick<CondenseReport, 'since' | 'until' | 'cards' | 'conflicts' | 'skipped' | 'droppedHarnessChild' | 'funnel'>): string {
29
+ const seats = (counts: Record<string, number>) => ['OP', 'TC', 'MK', 'UX', ...Object.keys(counts).filter(seat => !['OP', 'TC', 'MK', 'UX'].includes(seat)).sort()]
30
+ .map(seat => `${seat} ${counts[seat] ?? 0}`).join(' · ');
31
+ const stage = (name: string, value: { total: number; bySeat: Record<string, number> }) =>
32
+ `${name} ${value.total} (${seats(value.bySeat)})`;
39
33
  const lines = [
40
34
  '# Long-term context memory candidates',
41
35
  '',
42
36
  `Window: ${report.since} → ${report.until} · Cards: ${report.cards.length} · Conflicts: ${report.conflicts}`,
43
- `Skipped: ${JSON.stringify(report.skipped)}`,
37
+ `Funnel: ${stage('read', report.funnel.read)} → ${stage('sources', report.funnel.sources)} → ${stage('summarized', report.funnel.summarized)} → ${stage('cards', report.funnel.cards)}`,
38
+ `Skipped: ${JSON.stringify(report.skipped)} · Dropped harness-child: ${report.droppedHarnessChild}`,
44
39
  '',
45
40
  ];
46
41
  for (const card of report.cards) {
@@ -180,15 +175,15 @@ export async function runContextCondense(options: {
180
175
  const now = options.now ?? new Date();
181
176
  const until = now.toISOString();
182
177
  const since = new Date(now.getTime() - options.hours * 3_600_000).toISOString();
183
- const { cards, skipped, folded } = await condenseContextWindowWithReport(since, until, [], {
178
+ const { cards, skipped, folded, droppedHarnessChild, funnel } = await condenseContextWindowWithReport(since, until, [], {
184
179
  summarize: deps.summarize,
185
- events: deps.events ?? readContextEvents,
180
+ events: deps.events ?? readCondenseEvents,
186
181
  decisions: deps.decisions ?? (() => new DecisionLedger(options.root ? { stateDir: options.root } : {}).list({ status: 'all' })
187
182
  .filter(decision => decision.raisedBy.agent !== 'context-condense')),
188
183
  }, now);
189
184
  const conflicts = cards.filter(card => card.conflict !== undefined).length;
190
- const report: CondenseReport = { since, until, cards, conflicts, skipped, folded,
191
- markdown: renderCondenseMarkdown({ since, until, cards, conflicts, skipped }) };
185
+ const report: CondenseReport = { since, until, cards, conflicts, skipped, folded, droppedHarnessChild, funnel,
186
+ markdown: renderCondenseMarkdown({ since, until, cards, conflicts, skipped, droppedHarnessChild, funnel }) };
192
187
  if (options.apply === true) report.applied = applyCondensedCards(cards, store ?? defaultStore(options.root));
193
188
  return report;
194
189
  }