elanous 0.2.19 → 0.2.20

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 (388) 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/{J5H7BvqgSI8DUjtRRcA_S → Bam1YziK2iH9bN4K0utGk}/_buildManifest.js +1 -1
  4. package/apps/pwa/out/_next/static/chunks/{4323.c5dfb6f503304775.js → 1268.0840ff91e083fcbd.js} +1 -1
  5. package/apps/pwa/out/_next/static/chunks/1307-77fad80cf54477c6.js +1 -0
  6. package/apps/pwa/out/_next/static/chunks/1612-f60c0c93ec773f81.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/1826-5d279da64bbbc2de.js +1 -0
  8. package/apps/pwa/out/_next/static/chunks/1844-9ec4b1d12f6e80eb.js +1 -0
  9. package/apps/pwa/out/_next/static/chunks/1866-e7a6999f25548b30.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/1948.15f356b15a625597.js +5 -0
  11. package/apps/pwa/out/_next/static/chunks/195-fb1d7690bf30bab4.js +1 -0
  12. package/apps/pwa/out/_next/static/chunks/1986-33e13d648a9df765.js +1 -0
  13. package/apps/pwa/out/_next/static/chunks/{2977-e38442e86289f1d3.js → 211-9c9090c59e2c5907.js} +1 -1
  14. package/apps/pwa/out/_next/static/chunks/2116-6429c188eb846ab8.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/2196-4700653d12a44b40.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/224-e7be8f762fe74986.js +1 -0
  17. package/apps/pwa/out/_next/static/chunks/2489-ff55a35b202c0f94.js +1 -0
  18. package/apps/pwa/out/_next/static/chunks/2537-6c289376caa88ca1.js +1 -0
  19. package/apps/pwa/out/_next/static/chunks/2549-89c518a4c8ed4d00.js +1 -0
  20. package/apps/pwa/out/_next/static/chunks/2699-325cc923ba7b8631.js +1 -0
  21. package/apps/pwa/out/_next/static/chunks/2957-96ffab3d521fb08f.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/311.e685738f92e6e2ec.js +1 -0
  23. package/apps/pwa/out/_next/static/chunks/3194-807b212fb8eae065.js +1 -0
  24. package/apps/pwa/out/_next/static/chunks/3324-d1d2979821dfa845.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/3558-bbb5ab553a0471d3.js +1 -0
  26. package/apps/pwa/out/_next/static/chunks/3857-995ea9f5ef33d71e.js +1 -0
  27. package/apps/pwa/out/_next/static/chunks/3916-3d02dd1fc257dd3c.js +1 -0
  28. package/apps/pwa/out/_next/static/chunks/4144-fad095b48a1862ee.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/4565-3d7e3fdeb43686a8.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/4795-f372490b178583fc.js +1 -0
  31. package/apps/pwa/out/_next/static/chunks/5360-be1f1d8f9fcc9755.js +1 -0
  32. package/apps/pwa/out/_next/static/chunks/5755-ea7ed88eb956f47b.js +1 -0
  33. package/apps/pwa/out/_next/static/chunks/5977-94ed6f78e9814b2b.js +1 -0
  34. package/apps/pwa/out/_next/static/chunks/{4847.56f5bc454fbcc827.js → 6110.1297d4093fdfbb0d.js} +2 -2
  35. package/apps/pwa/out/_next/static/chunks/6158.af2e93b7adff709b.js +2 -0
  36. package/apps/pwa/out/_next/static/chunks/6877-74399f69c7da1433.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/{1470.98a6ab61ccb4131c.js → 726.83532d69452ee24b.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/7290-f09c928a06c52e2d.js +1 -0
  39. package/apps/pwa/out/_next/static/chunks/{6062-73e68343a9814f6a.js → 7327-2d69fb321ba73389.js} +1 -1
  40. package/apps/pwa/out/_next/static/chunks/{8870-9614a213b990b10b.js → 7748-6e51bfe927019f5c.js} +3 -3
  41. package/apps/pwa/out/_next/static/chunks/7797-df5aa0381fdc078f.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/8060-cc008cc6fedfa818.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/8085-d065d0393c1760f5.js +1 -0
  44. package/apps/pwa/out/_next/static/chunks/8401-35e94e9091f5eb8d.js +1 -0
  45. package/apps/pwa/out/_next/static/chunks/8682-7061dd4fa7bd62ed.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/8695-846034ce44852c70.js +1 -0
  47. package/apps/pwa/out/_next/static/chunks/9070.067c106528ec1ae6.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/9103-60fbef33e66f0a46.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/9247-c1753a059e7bac08.js +1 -0
  50. package/apps/pwa/out/_next/static/chunks/{7647.2fcd30d894d5ee30.js → 935.4ddaa99fc1d1126c.js} +1 -1
  51. package/apps/pwa/out/_next/static/chunks/{ae787e4c-e5b1ef00b2dbceca.js → a9f2b54d-33c0689b70ed8cf3.js} +1 -1
  52. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-c1a96669158471ec.js +1 -0
  53. package/apps/pwa/out/_next/static/chunks/app/approvals/page-c855c24de7db74fe.js +1 -0
  54. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-85b316bb4235980b.js +1 -0
  55. package/apps/pwa/out/_next/static/chunks/app/board/page-9b2b26a33cb2cd42.js +1 -0
  56. package/apps/pwa/out/_next/static/chunks/app/botlab/page-c9955fde67b1bcb5.js +1 -0
  57. package/apps/pwa/out/_next/static/chunks/app/bots/page-7a852956c74015a3.js +1 -0
  58. package/apps/pwa/out/_next/static/chunks/app/ceo/page-c5a5c621be0e04ae.js +1 -0
  59. package/apps/pwa/out/_next/static/chunks/app/chat/page-4ba4abe096b6c2a8.js +1 -0
  60. package/apps/pwa/out/_next/static/chunks/app/consult/page-575d4a8b4f7becb2.js +1 -0
  61. package/apps/pwa/out/_next/static/chunks/app/control/page-f5d56ff5f45ad164.js +1 -0
  62. package/apps/pwa/out/_next/static/chunks/app/decisions/page-2260e9eef0357421.js +1 -0
  63. package/apps/pwa/out/_next/static/chunks/app/design-check/page-7c058069c5e7fa63.js +1 -0
  64. package/apps/pwa/out/_next/static/chunks/app/editor/page-dc5da0b8a2edb8a4.js +1 -0
  65. package/apps/pwa/out/_next/static/chunks/app/exec/page-466bf0ee16ad1432.js +1 -0
  66. package/apps/pwa/out/_next/static/chunks/app/field/page-a23bc71713bcc08f.js +1 -0
  67. package/apps/pwa/out/_next/static/chunks/app/healthz/route-c1a96669158471ec.js +1 -0
  68. package/apps/pwa/out/_next/static/chunks/app/inside/{page-5b29d9b06ceadade.js → page-ed4a6f799f005d09.js} +1 -1
  69. package/apps/pwa/out/_next/static/chunks/app/intake/page-9cd565005b93aca6.js +1 -0
  70. package/apps/pwa/out/_next/static/chunks/app/layout-55f0f40c6db95f15.js +1 -0
  71. package/apps/pwa/out/_next/static/chunks/app/live/page-474cb5888ea0e8f3.js +1 -0
  72. package/apps/pwa/out/_next/static/chunks/app/loops/page-35bfec2e631d3eae.js +1 -0
  73. package/apps/pwa/out/_next/static/chunks/app/market/page-9ca21f9ad3208a61.js +1 -0
  74. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-f9ccb1b1adbea14d.js +1 -0
  75. package/apps/pwa/out/_next/static/chunks/app/morning/page-ec102c9b8b691e37.js +1 -0
  76. package/apps/pwa/out/_next/static/chunks/app/not-found-a9c2cfa8dda922a5.js +1 -0
  77. package/apps/pwa/out/_next/static/chunks/app/observatory/page-6da35144878feb6d.js +1 -0
  78. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-f6055495e9ed7ea8.js +1 -0
  79. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-25204694fa9984a5.js +1 -0
  80. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-a60e035f151d84d3.js +1 -0
  81. package/apps/pwa/out/_next/static/chunks/app/outputs/page-189c587f36874538.js +1 -0
  82. package/apps/pwa/out/_next/static/chunks/app/page-c56634bde705a65d.js +1 -0
  83. package/apps/pwa/out/_next/static/chunks/app/reflection/page-db03ae3589a81c9e.js +1 -0
  84. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-4a6463b97f0ad94f.js +1 -0
  85. package/apps/pwa/out/_next/static/chunks/app/sessions/page-75bffecc9aa1004e.js +1 -0
  86. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-a8d2fe0c669e0de4.js +1 -0
  87. package/apps/pwa/out/_next/static/chunks/app/settings/page-31d5b875760bb62d.js +1 -0
  88. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-c26fa10aac6c21c4.js +1 -0
  89. package/apps/pwa/out/_next/static/chunks/app/setup/layout-c1a96669158471ec.js +1 -0
  90. package/apps/pwa/out/_next/static/chunks/app/setup/page-68793c6e10331d66.js +1 -0
  91. package/apps/pwa/out/_next/static/chunks/app/share/page-46d5201d2207a511.js +1 -0
  92. package/apps/pwa/out/_next/static/chunks/app/showroom/page-d81c07a7b4f618ae.js +1 -0
  93. package/apps/pwa/out/_next/static/chunks/app/tasks/page-be1c4e8697f22360.js +1 -0
  94. package/apps/pwa/out/_next/static/chunks/app/term/page-cf0a4b4abead71ea.js +1 -0
  95. package/apps/pwa/out/_next/static/chunks/app/today/page-a4717e2c4bf7a28f.js +1 -0
  96. package/apps/pwa/out/_next/static/chunks/app/trace/page-0fd895fab9b62c1b.js +1 -0
  97. package/apps/pwa/out/_next/static/chunks/app/vault/page-8b6c1454a8e8f306.js +1 -0
  98. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-c6e4364d9de8f717.js +1 -0
  99. package/apps/pwa/out/_next/static/chunks/app/workflows/page-0acc8e198f7b0dfb.js +1 -0
  100. package/apps/pwa/out/_next/static/chunks/app/workspace/page-34882c84f84021c5.js +1 -0
  101. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-0506a1e0e4bdb2f5.js +1 -0
  102. package/apps/pwa/out/_next/static/chunks/{8046cadf-211ca3c0e00f2b72.js → f6231284-45726550aba08fca.js} +1 -1
  103. package/apps/pwa/out/_next/static/chunks/{2ea96958.e8c7e112de50b067.js → f6b7c9ba.64a895b8b2fedbdc.js} +1 -1
  104. package/apps/pwa/out/_next/static/chunks/framework-5c169757551134ed.js +1 -0
  105. package/apps/pwa/out/_next/static/chunks/main-59fe95fed40c7a01.js +1 -0
  106. package/apps/pwa/out/_next/static/chunks/main-app-4ca74dbbee7057c6.js +1 -0
  107. package/apps/pwa/out/_next/static/chunks/pages/_app-13a0a886df4d97a1.js +1 -0
  108. package/apps/pwa/out/_next/static/chunks/pages/_error-6af8591cd3a3f038.js +1 -0
  109. package/apps/pwa/out/_next/static/chunks/webpack-c260571513bf43c2.js +1 -0
  110. package/apps/pwa/out/_next/static/css/c23d959d3ee15e57.css +1 -0
  111. package/apps/pwa/out/approvals/index.html +1 -1
  112. package/apps/pwa/out/approvals/index.txt +23 -23
  113. package/apps/pwa/out/autopilot/index.html +1 -1
  114. package/apps/pwa/out/autopilot/index.txt +23 -23
  115. package/apps/pwa/out/board/index.html +1 -1
  116. package/apps/pwa/out/board/index.txt +23 -23
  117. package/apps/pwa/out/botlab/index.html +1 -1
  118. package/apps/pwa/out/botlab/index.txt +22 -22
  119. package/apps/pwa/out/bots/index.html +1 -1
  120. package/apps/pwa/out/bots/index.txt +22 -22
  121. package/apps/pwa/out/ceo/index.html +1 -1
  122. package/apps/pwa/out/ceo/index.txt +23 -23
  123. package/apps/pwa/out/chat/index.html +1 -1
  124. package/apps/pwa/out/chat/index.txt +22 -22
  125. package/apps/pwa/out/consult/index.html +1 -1
  126. package/apps/pwa/out/consult/index.txt +23 -23
  127. package/apps/pwa/out/control/index.html +1 -1
  128. package/apps/pwa/out/control/index.txt +23 -23
  129. package/apps/pwa/out/decisions/index.html +1 -1
  130. package/apps/pwa/out/decisions/index.txt +22 -22
  131. package/apps/pwa/out/design-check/index.html +1 -1
  132. package/apps/pwa/out/design-check/index.txt +23 -23
  133. package/apps/pwa/out/editor/index.html +1 -1
  134. package/apps/pwa/out/editor/index.txt +23 -23
  135. package/apps/pwa/out/exec/index.html +1 -1
  136. package/apps/pwa/out/exec/index.txt +23 -23
  137. package/apps/pwa/out/field/index.html +1 -1
  138. package/apps/pwa/out/field/index.txt +23 -23
  139. package/apps/pwa/out/healthz +1 -1
  140. package/apps/pwa/out/index.html +1 -1
  141. package/apps/pwa/out/index.txt +22 -22
  142. package/apps/pwa/out/inside/index.html +1 -1
  143. package/apps/pwa/out/inside/index.txt +22 -22
  144. package/apps/pwa/out/intake/index.html +1 -1
  145. package/apps/pwa/out/intake/index.txt +23 -23
  146. package/apps/pwa/out/live/index.html +1 -1
  147. package/apps/pwa/out/live/index.txt +23 -23
  148. package/apps/pwa/out/loops/index.html +1 -1
  149. package/apps/pwa/out/loops/index.txt +22 -22
  150. package/apps/pwa/out/market/index.html +1 -1
  151. package/apps/pwa/out/market/index.txt +22 -22
  152. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  153. package/apps/pwa/out/missions/placeholder/index.txt +22 -22
  154. package/apps/pwa/out/morning/index.html +1 -1
  155. package/apps/pwa/out/morning/index.txt +23 -23
  156. package/apps/pwa/out/observatory/index.html +1 -1
  157. package/apps/pwa/out/observatory/index.txt +23 -23
  158. package/apps/pwa/out/ops/checklist/index.html +1 -1
  159. package/apps/pwa/out/ops/checklist/index.txt +22 -22
  160. package/apps/pwa/out/ops/release/index.html +1 -1
  161. package/apps/pwa/out/ops/release/index.txt +22 -22
  162. package/apps/pwa/out/ops/seats/index.html +1 -1
  163. package/apps/pwa/out/ops/seats/index.txt +22 -22
  164. package/apps/pwa/out/outputs/index.html +1 -1
  165. package/apps/pwa/out/outputs/index.txt +22 -22
  166. package/apps/pwa/out/reflection/index.html +1 -1
  167. package/apps/pwa/out/reflection/index.txt +23 -23
  168. package/apps/pwa/out/scheduler/index.html +1 -1
  169. package/apps/pwa/out/scheduler/index.txt +23 -23
  170. package/apps/pwa/out/sessions/index.html +1 -1
  171. package/apps/pwa/out/sessions/index.txt +22 -22
  172. package/apps/pwa/out/settings/devices/index.html +1 -1
  173. package/apps/pwa/out/settings/devices/index.txt +23 -23
  174. package/apps/pwa/out/settings/index.html +1 -1
  175. package/apps/pwa/out/settings/index.txt +23 -23
  176. package/apps/pwa/out/setup/done/index.html +1 -1
  177. package/apps/pwa/out/setup/done/index.txt +23 -23
  178. package/apps/pwa/out/setup/index.html +1 -1
  179. package/apps/pwa/out/setup/index.txt +23 -23
  180. package/apps/pwa/out/share/index.html +1 -1
  181. package/apps/pwa/out/share/index.txt +23 -23
  182. package/apps/pwa/out/showroom/index.html +1 -1
  183. package/apps/pwa/out/showroom/index.txt +22 -22
  184. package/apps/pwa/out/tasks/index.html +1 -1
  185. package/apps/pwa/out/tasks/index.txt +23 -23
  186. package/apps/pwa/out/term/index.html +1 -1
  187. package/apps/pwa/out/term/index.txt +23 -23
  188. package/apps/pwa/out/today/index.html +1 -1
  189. package/apps/pwa/out/today/index.txt +23 -23
  190. package/apps/pwa/out/trace/index.html +1 -1
  191. package/apps/pwa/out/trace/index.txt +23 -23
  192. package/apps/pwa/out/vault/index.html +1 -1
  193. package/apps/pwa/out/vault/index.txt +23 -23
  194. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  195. package/apps/pwa/out/workflows/chat-ui/index.txt +23 -23
  196. package/apps/pwa/out/workflows/index.html +1 -1
  197. package/apps/pwa/out/workflows/index.txt +23 -23
  198. package/apps/pwa/out/workspace/index.html +1 -1
  199. package/apps/pwa/out/workspace/index.txt +23 -23
  200. package/apps/pwa/out/worktrees/index.html +1 -1
  201. package/apps/pwa/out/worktrees/index.txt +23 -23
  202. package/graphs/demo/editor-recipes.yaml +22 -0
  203. package/graphs/retro/daily.yaml +25 -0
  204. package/graphs/retro/recipes.yaml +24 -0
  205. package/package.json +1 -1
  206. package/scripts/ask-marker-check.ts +3 -1
  207. package/scripts/land-global-checks.ts +89 -0
  208. package/scripts/lib/pch-regress-map.ts +2 -1
  209. package/scripts/release-loop/gate-node.ts +296 -25
  210. package/scripts/release-loop/gate-timing.ts +116 -7
  211. package/scripts/release-loop/preflight.ts +6 -4
  212. package/scripts/release-loop/publish-window.ts +141 -0
  213. package/scripts/release-loop/release-docs.ts +1 -4
  214. package/scripts/release-loop/release-universe.ts +38 -0
  215. package/scripts/release-loop/resume-release.ts +4 -1
  216. package/scripts/release-loop/unattended-release.ts +10 -2
  217. package/scripts/release-story/draft.ts +5 -1
  218. package/scripts/retro/shadow-compare.ts +71 -0
  219. package/scripts/rhythm/daily.ts +6 -1
  220. package/src/agent-mission/driver.ts +34 -6
  221. package/src/autopilot/build/llm-conflict-merge.ts +132 -7
  222. package/src/autopilot/build/merge-intent.ts +135 -7
  223. package/src/away/release-watch.ts +20 -3
  224. package/src/cli/config-drift.ts +513 -0
  225. package/src/cli/log-event-names.ts +4 -0
  226. package/src/cli/pr-cli.ts +20 -0
  227. package/src/cli/release-cli.ts +20 -5
  228. package/src/cli/retro-cli.ts +78 -0
  229. package/src/cli/tasks-cli.ts +94 -8
  230. package/src/dashboard/slash-runtime/dashboard-handlers.ts +5 -4
  231. package/src/dashboard/slash-runtime/help-from-registry.ts +57 -31
  232. package/src/domains/outbound-log-probe.ts +6 -1
  233. package/src/graph-runner/graph-run-api.ts +205 -0
  234. package/src/graph-runner/runner.ts +11 -1
  235. package/src/harness/harness-cli-command.ts +419 -52
  236. package/src/harness/harness-queue.ts +315 -25
  237. package/src/harness/harness-salvage-cli.ts +62 -0
  238. package/src/hq/fence-outcomes.ts +63 -0
  239. package/src/hq/fence-wrapper.ts +33 -0
  240. package/src/hq/hq.ts +11 -1
  241. package/src/index.ts +69 -6
  242. package/src/maturity/feature-maturity.ts +2 -2
  243. package/src/nexus/api/graph-kinds.ts +10 -4
  244. package/src/nexus/api/graphs-api.ts +178 -6
  245. package/src/nexus/api/http-server.ts +38 -3
  246. package/src/nexus/api/loop-edges.ts +4 -2
  247. package/src/nexus/api/ops-api.ts +22 -7
  248. package/src/nexus/api/ops-board-route.ts +48 -0
  249. package/src/onboarding/entry-hints.ts +5 -0
  250. package/src/onboarding.ts +24 -6
  251. package/src/release-loop/feature-store.ts +14 -11
  252. package/src/release-loop/gate-shards.ts +158 -0
  253. package/src/release-loop/release-schedule.ts +6 -2
  254. package/src/retro/daily.ts +372 -0
  255. package/src/seat-loop/op-decide.ts +173 -0
  256. package/src/seat-loop/seat-loop.ts +60 -3
  257. package/src/seat-loop/seat-report.ts +58 -3
  258. package/src/self-dev/ask-launch-flow.ts +5 -4
  259. package/src/self-dev/ask-launch-io.ts +49 -1
  260. package/src/self-dev/ask-path-candidates.ts +40 -2
  261. package/src/self-dev/draft-sweep.ts +371 -16
  262. package/src/self-dev/draft-triage-rules.ts +19 -4
  263. package/src/self-dev/pr-preview.ts +84 -16
  264. package/src/self-implement/blocked-draft-policy.ts +101 -0
  265. package/src/self-implement/execution-clarification.ts +11 -0
  266. package/src/self-implement/gate-env-retry.ts +83 -0
  267. package/src/self-implement/orchestrator.ts +218 -5
  268. package/src/self-implement/run-stop.ts +123 -0
  269. package/src/self-implement/run-ttl.ts +258 -0
  270. package/src/self-implement/salvage-retention.ts +123 -0
  271. package/src/self-implement/seams.ts +4 -0
  272. package/src/self-implement/self-implement-cli.ts +2 -0
  273. package/src/self-implement/sibling-resync.ts +223 -0
  274. package/src/self-implement/stop-autoheal.ts +204 -0
  275. package/src/task-agent/board.ts +1 -1
  276. package/src/task-agent/card-evidence.ts +99 -10
  277. package/src/task-agent/judge.ts +7 -0
  278. package/src/task-agent/live-moves.ts +120 -20
  279. package/src/task-agent/mission.ts +1 -1
  280. package/src/task-agent/shadow.ts +10 -1
  281. package/src/task-agent/task-hand.ts +7 -2
  282. package/src/task-orchestrator/surfaces/pod-command-job.ts +19 -5
  283. package/src/task-orchestrator/surfaces/pod-failure-reason.ts +4 -0
  284. package/src/task-orchestrator/surfaces/pod-install-slots.ts +89 -0
  285. package/src/task-orchestrator/surfaces/pod-lease.ts +93 -10
  286. package/src/task-orchestrator/surfaces/pod-pool.ts +147 -34
  287. package/src/task-orchestrator/surfaces/self-implement-pod.ts +19 -3
  288. package/src/telegram-commands.ts +41 -0
  289. package/src/testing/real-cli-spawn.ts +106 -0
  290. package/src/user-config.ts +56 -3
  291. package/src/version/packed-revision.json +1 -1
  292. package/apps/pwa/out/_next/static/chunks/1105-c473ed46272fba79.js +0 -1
  293. package/apps/pwa/out/_next/static/chunks/1157-cba0e8553d35a980.js +0 -1
  294. package/apps/pwa/out/_next/static/chunks/171-23a877ed11720de9.js +0 -1
  295. package/apps/pwa/out/_next/static/chunks/216-5acd01cbde4acda7.js +0 -1
  296. package/apps/pwa/out/_next/static/chunks/2330-c2f07246c8788af1.js +0 -1
  297. package/apps/pwa/out/_next/static/chunks/2336-e830c777eb6bce04.js +0 -1
  298. package/apps/pwa/out/_next/static/chunks/2375-e061bb24cd2b5cde.js +0 -1
  299. package/apps/pwa/out/_next/static/chunks/2415.ed6428ff334c7ce2.js +0 -1
  300. package/apps/pwa/out/_next/static/chunks/3094.67339318c7f72a3a.js +0 -2
  301. package/apps/pwa/out/_next/static/chunks/338-ceb956612dcfaa86.js +0 -1
  302. package/apps/pwa/out/_next/static/chunks/3395-835e4ceaa6335ba4.js +0 -1
  303. package/apps/pwa/out/_next/static/chunks/3403.4bc97b4066945a94.js +0 -5
  304. package/apps/pwa/out/_next/static/chunks/3441-be82e3595aef3a20.js +0 -1
  305. package/apps/pwa/out/_next/static/chunks/3689-89905039bf3e7257.js +0 -1
  306. package/apps/pwa/out/_next/static/chunks/371-4ad6af55dad02ced.js +0 -1
  307. package/apps/pwa/out/_next/static/chunks/3822-6f268bc26c6a84ac.js +0 -1
  308. package/apps/pwa/out/_next/static/chunks/4114-3e63117452378807.js +0 -1
  309. package/apps/pwa/out/_next/static/chunks/4457-d7df81c90f9d0268.js +0 -1
  310. package/apps/pwa/out/_next/static/chunks/4551-77dadae76f6cbace.js +0 -1
  311. package/apps/pwa/out/_next/static/chunks/4978.6fd93b974441e87f.js +0 -1
  312. package/apps/pwa/out/_next/static/chunks/5012-f746f6549b869f91.js +0 -1
  313. package/apps/pwa/out/_next/static/chunks/5609-012bb3557211f07e.js +0 -1
  314. package/apps/pwa/out/_next/static/chunks/5791-bb82743fa6299562.js +0 -1
  315. package/apps/pwa/out/_next/static/chunks/62-5bfed1e23790b745.js +0 -1
  316. package/apps/pwa/out/_next/static/chunks/6320-9bc8561fde416e07.js +0 -1
  317. package/apps/pwa/out/_next/static/chunks/649-ca96d1c5d09e02b8.js +0 -1
  318. package/apps/pwa/out/_next/static/chunks/6725-68ab7cc0b1faf00d.js +0 -1
  319. package/apps/pwa/out/_next/static/chunks/6726-85a7db59b288c53f.js +0 -1
  320. package/apps/pwa/out/_next/static/chunks/6735-33267c4630d0d963.js +0 -1
  321. package/apps/pwa/out/_next/static/chunks/6998-a654fe5c58271600.js +0 -1
  322. package/apps/pwa/out/_next/static/chunks/7633-141682015873beb6.js +0 -1
  323. package/apps/pwa/out/_next/static/chunks/7673-2eda1507d38dadb8.js +0 -1
  324. package/apps/pwa/out/_next/static/chunks/7990-e12efba616f46e03.js +0 -1
  325. package/apps/pwa/out/_next/static/chunks/8067-143791231c99bd02.js +0 -1
  326. package/apps/pwa/out/_next/static/chunks/8142-744998d65d7bb17e.js +0 -1
  327. package/apps/pwa/out/_next/static/chunks/8430-a10cfba8ef282d4e.js +0 -1
  328. package/apps/pwa/out/_next/static/chunks/920-f7dceb78f1c585ef.js +0 -1
  329. package/apps/pwa/out/_next/static/chunks/9454-12c2f30dea62291c.js +0 -1
  330. package/apps/pwa/out/_next/static/chunks/976-4a9b4ebdca7c739a.js +0 -1
  331. package/apps/pwa/out/_next/static/chunks/9989-15cdf568bafa9fc8.js +0 -1
  332. package/apps/pwa/out/_next/static/chunks/app/_not-found/page-859cea1570b6227a.js +0 -1
  333. package/apps/pwa/out/_next/static/chunks/app/approvals/page-deef5ee191830f14.js +0 -1
  334. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-b193da485f9a14b2.js +0 -1
  335. package/apps/pwa/out/_next/static/chunks/app/board/page-b6e1a79df0e214f4.js +0 -1
  336. package/apps/pwa/out/_next/static/chunks/app/botlab/page-e84df4d907f703c7.js +0 -1
  337. package/apps/pwa/out/_next/static/chunks/app/bots/page-a5fb9a5db89ae4ea.js +0 -1
  338. package/apps/pwa/out/_next/static/chunks/app/ceo/page-1499f481d7fc52d6.js +0 -1
  339. package/apps/pwa/out/_next/static/chunks/app/chat/page-c7cf753aeb648790.js +0 -1
  340. package/apps/pwa/out/_next/static/chunks/app/consult/page-4118469e514e10fd.js +0 -1
  341. package/apps/pwa/out/_next/static/chunks/app/control/page-e968f7630654332a.js +0 -1
  342. package/apps/pwa/out/_next/static/chunks/app/decisions/page-505915052f947022.js +0 -1
  343. package/apps/pwa/out/_next/static/chunks/app/design-check/page-c2f962fb4a3b06c6.js +0 -1
  344. package/apps/pwa/out/_next/static/chunks/app/editor/page-7b5ed0bd8a589ec4.js +0 -1
  345. package/apps/pwa/out/_next/static/chunks/app/exec/page-cf8193250dd42dd4.js +0 -1
  346. package/apps/pwa/out/_next/static/chunks/app/field/page-cf3f5dfa35dea8e9.js +0 -1
  347. package/apps/pwa/out/_next/static/chunks/app/healthz/route-859cea1570b6227a.js +0 -1
  348. package/apps/pwa/out/_next/static/chunks/app/intake/page-a15e7fcc283e9ca7.js +0 -1
  349. package/apps/pwa/out/_next/static/chunks/app/layout-d2915e7b70ff3d0e.js +0 -1
  350. package/apps/pwa/out/_next/static/chunks/app/live/page-b70cd005d9169e25.js +0 -1
  351. package/apps/pwa/out/_next/static/chunks/app/loops/page-2595f0eba29f57e1.js +0 -1
  352. package/apps/pwa/out/_next/static/chunks/app/market/page-c776322454156e36.js +0 -1
  353. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-77bbd17f4435aa10.js +0 -1
  354. package/apps/pwa/out/_next/static/chunks/app/morning/page-049689e611ecc936.js +0 -1
  355. package/apps/pwa/out/_next/static/chunks/app/not-found-85954a4fb09c44eb.js +0 -1
  356. package/apps/pwa/out/_next/static/chunks/app/observatory/page-e773e552e1068ee6.js +0 -1
  357. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-5633c4bfa7684b4a.js +0 -1
  358. package/apps/pwa/out/_next/static/chunks/app/ops/release/page-d829b92bd01b4994.js +0 -1
  359. package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-061c317fc771becf.js +0 -1
  360. package/apps/pwa/out/_next/static/chunks/app/outputs/page-3a45ece8865ed4b6.js +0 -1
  361. package/apps/pwa/out/_next/static/chunks/app/page-0be990618c0d56bd.js +0 -1
  362. package/apps/pwa/out/_next/static/chunks/app/reflection/page-bfea4865dc244b73.js +0 -1
  363. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-0d6ba8e337741484.js +0 -1
  364. package/apps/pwa/out/_next/static/chunks/app/sessions/page-76a88b55d816482e.js +0 -1
  365. package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-bba1542df59f61ce.js +0 -1
  366. package/apps/pwa/out/_next/static/chunks/app/settings/page-4185c95689bb372e.js +0 -1
  367. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-b699e1a8d5d34b8b.js +0 -1
  368. package/apps/pwa/out/_next/static/chunks/app/setup/layout-859cea1570b6227a.js +0 -1
  369. package/apps/pwa/out/_next/static/chunks/app/setup/page-c225e338c31bf274.js +0 -1
  370. package/apps/pwa/out/_next/static/chunks/app/share/page-449353f70507ca1f.js +0 -1
  371. package/apps/pwa/out/_next/static/chunks/app/showroom/page-90b889714102476d.js +0 -1
  372. package/apps/pwa/out/_next/static/chunks/app/tasks/page-f1ee7fbeb7f96ddc.js +0 -1
  373. package/apps/pwa/out/_next/static/chunks/app/term/page-d59a95e1434dfaa0.js +0 -1
  374. package/apps/pwa/out/_next/static/chunks/app/today/page-71491a23414d1a47.js +0 -1
  375. package/apps/pwa/out/_next/static/chunks/app/trace/page-de7112bdb65fa4fe.js +0 -1
  376. package/apps/pwa/out/_next/static/chunks/app/vault/page-feb4d0d88079660d.js +0 -1
  377. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-f17d887b40dd6b1d.js +0 -1
  378. package/apps/pwa/out/_next/static/chunks/app/workflows/page-8da77be2192bcd5d.js +0 -1
  379. package/apps/pwa/out/_next/static/chunks/app/workspace/page-72c3b9a5e9582b29.js +0 -1
  380. package/apps/pwa/out/_next/static/chunks/app/worktrees/page-c3afe4c8d0c48c39.js +0 -1
  381. package/apps/pwa/out/_next/static/chunks/framework-ea806bb57f2b8923.js +0 -1
  382. package/apps/pwa/out/_next/static/chunks/main-8d2302399c2c8f87.js +0 -1
  383. package/apps/pwa/out/_next/static/chunks/main-app-b9308f2d8a931531.js +0 -1
  384. package/apps/pwa/out/_next/static/chunks/pages/_app-b10928c60f0b8488.js +0 -1
  385. package/apps/pwa/out/_next/static/chunks/pages/_error-689039227d443fab.js +0 -1
  386. package/apps/pwa/out/_next/static/chunks/webpack-6f35f9d892fa630b.js +0 -1
  387. package/apps/pwa/out/_next/static/css/85f53c6f35d11382.css +0 -1
  388. /package/apps/pwa/out/_next/static/{J5H7BvqgSI8DUjtRRcA_S → Bam1YziK2iH9bN4K0utGk}/_ssgManifest.js +0 -0
@@ -0,0 +1,63 @@
1
+ // LOOPCHECK-FENCE (0.2.20): an hq-fence cron execution is one process, not a loop.
2
+ // Each wrapped run leaves its rc in a per-role streak file; the wrapper's owner alert
3
+ // raises a seat request only when the same role failed in consecutive runs, keyed per
4
+ // role so the seat request journal holds at most one `loopcheck:hq-fence:<role>` per day.
5
+ // No loops/ import here: hq.ts records through this module and loops/registry imports hq.ts.
6
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
7
+ import { dirname, join } from 'node:path';
8
+ import { Database } from 'bun:sqlite';
9
+ import { debug } from '../debug/log.js';
10
+
11
+ /** The role is the file name; `lastAt` is shown in the owner alert so TC sees when the streak last grew. */
12
+ export interface FenceOutcome { consecutiveFailures: number; lastRc: number; lastAt: string }
13
+
14
+ /** Two failures in a row: one rc=1 from a job that recovers on the next fire is not a fence fault. */
15
+ export const FENCE_FAILURE_STREAK = 2;
16
+
17
+ const safeRole = (role: string): string => role.replace(/[^\w.-]/g, '_') || 'unknown';
18
+
19
+ export function fenceOutcomePath(dir: string, role: string): string {
20
+ return join(dir, `${safeRole(role)}.json`);
21
+ }
22
+
23
+ /** Default directory next to the host-local lease view (`<config>/hq/fence-outcomes`). */
24
+ export function fenceOutcomeDir(localPath: string): string {
25
+ return join(dirname(localPath), 'fence-outcomes');
26
+ }
27
+
28
+ export function readFenceOutcome(dir: string, role: string): FenceOutcome | null {
29
+ try {
30
+ const parsed = JSON.parse(readFileSync(fenceOutcomePath(dir, role), 'utf8')) as Partial<FenceOutcome>;
31
+ return typeof parsed.consecutiveFailures === 'number' ? parsed as FenceOutcome : null;
32
+ } catch (error) {
33
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null;
34
+ debug.log('hq.fence-outcome', 'read-failed', { role, error: String(error) });
35
+ return null;
36
+ }
37
+ }
38
+
39
+ /** rc 0 resets the streak; any other rc extends it. A skipped (not-holder) run is not recorded.
40
+ * Overlapping runs of one role serialize the read-modify-write on an SQLite lock (released even on SIGKILL). */
41
+ export function recordFenceOutcome(dir: string, role: string, rc: number, now: Date = new Date()): FenceOutcome {
42
+ const path = fenceOutcomePath(dir, role);
43
+ mkdirSync(dirname(path), { recursive: true });
44
+ const lock = new Database(`${path}.lock.sqlite`);
45
+ try {
46
+ lock.exec('PRAGMA busy_timeout = 10000');
47
+ lock.exec('BEGIN EXCLUSIVE');
48
+ try {
49
+ const previous = readFenceOutcome(dir, role);
50
+ const outcome: FenceOutcome = { consecutiveFailures: rc === 0 ? 0 : (previous?.consecutiveFailures ?? 0) + 1, lastRc: rc, lastAt: now.toISOString() };
51
+ const tmp = `${path}.${process.pid}.tmp`;
52
+ writeFileSync(tmp, JSON.stringify(outcome));
53
+ renameSync(tmp, path);
54
+ lock.exec('COMMIT');
55
+ return outcome;
56
+ } catch (error) { lock.exec('ROLLBACK'); throw error; }
57
+ } finally { lock.close(); }
58
+ }
59
+
60
+ /** `hq-fence: fence failed (rc=1, role=seat-loop)` → `seat-loop`; other wrapper reasons have no role. */
61
+ export function fenceAlertRole(reason: string): string | null {
62
+ return /\brole=([\w.-]+)/.exec(reason)?.[1] ?? null;
63
+ }
@@ -3,6 +3,9 @@ import { chmodSync, constants, copyFileSync, lstatSync, mkdirSync, mkdtempSync,
3
3
  import { dirname, isAbsolute, join } from 'node:path';
4
4
  import { homedir } from 'node:os';
5
5
  import { createPatch } from 'diff';
6
+ import { debug } from '../debug/log.js';
7
+ import { notifyOwners } from '../loops/checker.js';
8
+ import { FENCE_FAILURE_STREAK, fenceAlertRole, readFenceOutcome } from './fence-outcomes.js';
6
9
 
7
10
  const quote = (value: string): string => `'${value.replaceAll("'", "'\"'\"'")}'`;
8
11
 
@@ -121,3 +124,33 @@ export function installHqFenceWrapper(configDir: string, body: string, yes: bool
121
124
  } finally { rmSync(tempDir, { recursive: true, force: true }); }
122
125
  return { path, preview, ...(backup ? { backup } : {}) };
123
126
  }
127
+
128
+ export interface FenceAlertDeps { root: string; outcomeDir: string; now?: Date }
129
+
130
+ /**
131
+ * The wrapper's owner alert. Returns the number of seat requests queued (0 or 1).
132
+ * - `fence failed (rc, role)`: queued only when that role's streak reached FENCE_FAILURE_STREAK
133
+ * (an unknown streak is not read as «first failure» — it is queued, still capped per role/day).
134
+ * - Any other wrapper failure (entry missing, bun missing, role missing): queued under `hq-fence:wrapper`.
135
+ */
136
+ export function hqFenceAlert(reason: string, deps: FenceAlertDeps): number {
137
+ const now = deps.now ?? new Date();
138
+ const fenceFailure = /fence failed \(rc=/.test(reason);
139
+ const role = fenceAlertRole(reason);
140
+ let streak: number | null = null;
141
+ let lastAt: string | undefined;
142
+ if (fenceFailure && role) {
143
+ const outcome = readFenceOutcome(deps.outcomeDir, role);
144
+ streak = outcome?.consecutiveFailures ?? null;
145
+ lastAt = outcome?.lastAt;
146
+ if (streak !== null && streak < FENCE_FAILURE_STREAK) {
147
+ debug.log('hq.fence-wrapper', 'alert-suppressed', { role, streak, threshold: FENCE_FAILURE_STREAK, reason });
148
+ return 0;
149
+ }
150
+ }
151
+ const id = `hq-fence:${fenceFailure && role ? role : 'wrapper'}`;
152
+ const text = streak !== null ? `${reason} · ${streak} consecutive runs (last ${lastAt ?? 'unknown'})` : reason;
153
+ const added = notifyOwners([{ id, owner: 'TC', enabled: true, registered: true, state: 'failing', reason: text }], { root: deps.root, now });
154
+ debug.log('hq.fence-wrapper', added ? 'alert-queued' : 'alert-deduplicated', { id, role, streak, reason });
155
+ return added;
156
+ }
package/src/hq/hq.ts CHANGED
@@ -10,6 +10,7 @@ import { debug } from '../debug/log.js';
10
10
  import { DEFAULT_NEXUS_HTTP_PORT } from '../nexus/default-port.js';
11
11
  import { getElanousConfigDir } from '../elanous-config-dir.js';
12
12
  import { getUserConfig, type HqConfig, type HqFenceRole } from '../user-config.js';
13
+ import { fenceOutcomeDir, recordFenceOutcome } from './fence-outcomes.js';
13
14
  import { promoteHq, proposeHqPromotion, writeOpSeatRequest } from './promote.js';
14
15
  import type { HqOpRequestWriter, PromoteResult } from './promote.js';
15
16
  import {
@@ -42,6 +43,8 @@ export interface HqDeps {
42
43
  /** Holder/standby views and the arbiter's check use this tailnet probe — never ssh (OP 10-04 09:04). */
43
44
  probe?: HostProbe;
44
45
  localPath?: string;
46
+ /** Per-role streak files of wrapped-run rc (LOOPCHECK-FENCE · default next to localPath). */
47
+ outcomeDir?: string;
45
48
  /** Host-local identity, deliberately outside the replicated config/state directory. */
46
49
  hostPath?: string;
47
50
  /** Seen-generation marker (default hq.seenGenerationFile · ~/.elanous-hq/seen-generation). */
@@ -391,5 +394,12 @@ export function hqFenceRun(role: HqFenceRole, command: string[], deps: HqDeps =
391
394
  const [cmd, ...args] = command;
392
395
  if (!cmd) throw new Error('hq fence: command required after --');
393
396
  const r = spawnSync(cmd, args, { stdio: 'inherit', env: { ...process.env, ELANOUS_HQ_GENERATION: String(decided.generation ?? '') } });
394
- return r.status ?? 1;
397
+ const rc = r.status ?? 1;
398
+ // LOOPCHECK-FENCE: the wrapper's alert reads this streak, so a lone failure is not a seat request.
399
+ try {
400
+ const dir = deps.outcomeDir ?? fenceOutcomeDir(deps.localPath ?? join(getElanousConfigDir(), 'hq', 'local.json'));
401
+ const outcome = recordFenceOutcome(dir, role, rc, new Date(deps.now?.() ?? Date.now()));
402
+ if (rc !== 0) debug.log('hq.fence', 'run-failed', { role, rc, consecutiveFailures: outcome.consecutiveFailures });
403
+ } catch (error) { try { debug.log('hq.fence', 'outcome-record-failed', { role, rc, error: String(error) }); } catch { /* fail-soft */ } }
404
+ return rc;
395
405
  }
package/src/index.ts CHANGED
@@ -7,6 +7,7 @@ import type { SelfDevRunParticipant, SelfDevRunState } from './self-dev/run-stor
7
7
  import { applyConfigDirFlagFromArgv } from './cli/config-dir-flag.js';
8
8
  import { LOGS_SINCE_OPTION, registerLogsCommands } from './cli/logs-cli.js';
9
9
  import { registerDocsCommands } from './cli/docs-cli.js';
10
+ import { registerRetroCommands } from './cli/retro-cli.js';
10
11
  import { registerA2ACommands } from './cli/a2a-cli.js';
11
12
  import { registerPluginCommands } from './cli/plugin-cli.js';
12
13
  import { registerMakeCommand } from './cli/make-cli.js';
@@ -219,13 +220,14 @@ program.enablePositionalOptions();
219
220
 
220
221
  program.command('preview <target>')
221
222
  .description('Isolated PR/branch-head TUI + PWA preview; publish the TUI-COMFORT friction table to a PR')
222
- .option('--scenario <id>', 'TUI-COMFORT scenario (default S2)', 'S2')
223
+ .option('--scenario <id>', 'TUI-COMFORT scenario IDs (comma-separated; default S2)', 'S2')
223
224
  .option('--flag <path>', 'Enable an existing config feature flag in main-based preview (repeatable)', (value: string, all: string[]) => [...all, value], [] as string[])
225
+ .option('--bundle-pr <number>', 'Merge PR head into isolated main preview (repeatable; no flags)', (value: string, all: number[]) => [...all, Number(value)], [] as number[])
224
226
  .option('--comment-pr <number>', 'PR to receive a branch/main preview report', (value: string) => Number(value))
225
- .action(async (target: string, opts: { scenario: string; flag: string[]; commentPr?: number }) => {
227
+ .action(async (target: string, opts: { scenario: string; flag: string[]; bundlePr: number[]; commentPr?: number }) => {
226
228
  try {
227
229
  const { runPrPreview } = await import('./self-dev/pr-preview.js');
228
- const result = await runPrPreview({ target, repoRoot: process.cwd(), scenario: opts.scenario, flags: opts.flag, ...(opts.commentPr === undefined ? {} : { commentPr: opts.commentPr }) });
230
+ const result = await runPrPreview({ target, repoRoot: process.cwd(), scenario: opts.scenario, flags: opts.flag, bundlePrs: opts.bundlePr, ...(opts.commentPr === undefined ? {} : { commentPr: opts.commentPr }) });
229
231
  console.log(`preview ${result.status} · ${result.head} · PR #${result.prNumber} · comment posted`);
230
232
  if (result.status !== 'passed') process.exitCode = 1;
231
233
  } catch (error) {
@@ -1059,10 +1061,12 @@ fenceWrapperCmd.command('install').description('래퍼 제안 또는 백업 후
1059
1061
  fenceWrapperCmd.command('alert <reason>').description('HQ 울타리 실패를 기존 루프 소유자 요청 원장에 적재')
1060
1062
  .action(async (reason: string) => {
1061
1063
  try {
1062
- const { notifyOwners } = await import('./loops/checker.js');
1064
+ // LOOPCHECK-FENCE: keyed per role (not per pid) and only after consecutive failures of that role.
1065
+ const { fenceOutcomeDir } = await import('./hq/fence-outcomes.js');
1066
+ const { hqFenceAlert } = await import('./hq/fence-wrapper.js');
1063
1067
  const { getElanousConfigDir } = await import('./elanous-config-dir.js');
1064
- notifyOwners([{ id: `hq-fence:${Date.now()}:${process.pid}`, owner: 'TC', enabled: true, registered: true,
1065
- state: 'failing', reason }], { root: getElanousConfigDir() });
1068
+ const root = getElanousConfigDir();
1069
+ hqFenceAlert(reason, { root, outcomeDir: fenceOutcomeDir(_joinPath(root, 'hq', 'local.json')) });
1066
1070
  } catch (error) { console.error(`hq fence-wrapper alert: ${error instanceof Error ? error.message : String(error)}`); process.exitCode = 1; }
1067
1071
  });
1068
1072
  hqCmd.command('fence-audit').description('운영 crontab 의 HQ 울타리 밖 쓰기 잡 목록과 감싼 줄 제안(읽기 전용)')
@@ -1905,6 +1909,7 @@ program
1905
1909
  });
1906
1910
 
1907
1911
  registerAutopilotCommands(program);
1912
+ registerRetroCommands(program);
1908
1913
 
1909
1914
  // ── memory ──
1910
1915
  const memCmd = program.command('memory').description('Persistent memories injected into every chat turn (user / feedback / project / reference)');
@@ -7679,6 +7684,11 @@ configCmd
7679
7684
  .description('Print all config or one dotted path, e.g. llm.provider (secrets redacted by default)')
7680
7685
  .option('--reveal', 'Print unredacted output and record the reveal')
7681
7686
  .action(async (path: string | undefined, opts: { reveal?: boolean }) => {
7687
+ const { resolveCurrentInstance } = await import('./instance/current.js');
7688
+ const instance = resolveCurrentInstance();
7689
+ if (instance.kind === 'test' && instance.layer === 'tree-derived') {
7690
+ console.error('⚠️ test 우주의 config를 읽습니다 — 운영 값은 --config-dir ~/.elanous');
7691
+ }
7682
7692
  const cfg = getUserConfig() as unknown as Record<string, unknown>;
7683
7693
  const value = path ? getConfigPath(cfg, path) : cfg;
7684
7694
  if (value === undefined) {
@@ -7773,6 +7783,59 @@ configCmd
7773
7783
  ui.info(`unset ${path}`);
7774
7784
  });
7775
7785
 
7786
+ // ── config drift / explain (CFG-STORE1 · 0.2.20 · 2026-10-08) — 읽기 전용 «유효 config ⊕ 출처» ──
7787
+ // 집(~/.elanous)과 파생 시험 우주(<트리>/.elanous-test) config 사본의 드리프트를 한 명령으로.
7788
+ // ⛔ 쓰기 0 · 못 읽음 ≠ 같음 · 비밀 값 가림. 계산 = src/cli/config-drift.ts.
7789
+ async function configDriftDiscover(o: { registry?: string | boolean; scanRoot?: string[]; treeScan?: boolean }) {
7790
+ const { discoverUniverses } = await import('./cli/config-drift.js');
7791
+ return discoverUniverses({
7792
+ ...(o.registry === false ? { registryPath: null } : typeof o.registry === 'string' ? { registryPath: o.registry } : {}),
7793
+ ...(o.treeScan === false ? { treeScanRoots: [] } : o.scanRoot?.length ? { treeScanRoots: o.scanRoot } : {}),
7794
+ });
7795
+ }
7796
+
7797
+ configCmd
7798
+ .command('drift')
7799
+ .description('읽기 전용 — 운영 config 와 파생 시험 우주(.elanous-test) config 사본이 다른 키 표 (CFG-STORE1)')
7800
+ .option('--key <key>', '이 키(또는 그 아래)만')
7801
+ .option('--json', 'JSON 출력')
7802
+ .option('--limit <n>', '표시할 키 수 (0=전부 · 기본 50)', '50')
7803
+ .option('--registry <path>', '인스턴스 레지스트리 경로 (기본 ~/.elanous/logs/instances.json)')
7804
+ .option('--scan-root <dir...>', '명시 트리 뿌리 (기본 = config-drift.ts defaultTreeScanRoots)')
7805
+ .option('--no-tree-scan', '트리 뿌리 훑기 끔 — 레지스트리만')
7806
+ .option('--no-registry', '레지스트리 끔 — 명시 트리 뿌리(자리·작업 트리)만')
7807
+ .option('--include-expected', 'sync-test 의 test-safe 변환(비밀 제거·발송 끔)과 _test 메타 키 차이도 드리프트로 센다')
7808
+ .action(async (o: { key?: string; json?: boolean; limit: string; includeExpected?: boolean; registry?: string | boolean; scanRoot?: string[]; treeScan?: boolean }) => {
7809
+ const { computeDrift, renderDrift } = await import('./cli/config-drift.js');
7810
+ const report = computeDrift(await configDriftDiscover(o), { key: o.key, includeExpected: o.includeExpected });
7811
+ debug.log('config.drift', 'computed', {
7812
+ key: o.key ?? null, rows: report.rows.length, prodStatus: report.prodStatus, ...report.universes,
7813
+ });
7814
+ debug.flush();
7815
+ console.log(o.json ? JSON.stringify(report, null, 2) : renderDrift(report, { limit: Number(o.limit) || 0 }));
7816
+ if (report.prodStatus !== 'ok') process.exit(1);
7817
+ });
7818
+
7819
+ configCmd
7820
+ .command('explain <key>')
7821
+ .description('읽기 전용 — 운영 값 ⊕ 출처 층 ⊕ 파생 시험 우주별 값 (CFG-STORE1)')
7822
+ .option('--json', 'JSON 출력')
7823
+ .option('--registry <path>', '인스턴스 레지스트리 경로')
7824
+ .option('--scan-root <dir...>', '명시 트리 뿌리')
7825
+ .option('--no-tree-scan', '트리 뿌리 훑기 끔 — 레지스트리만')
7826
+ .option('--no-registry', '레지스트리 끔 — 명시 트리 뿌리만')
7827
+ .action(async (key: string, o: { json?: boolean; registry?: string | boolean; scanRoot?: string[]; treeScan?: boolean }) => {
7828
+ const { computeExplain, renderExplain } = await import('./cli/config-drift.js');
7829
+ const { buildUserConfig } = await import('./user-config.js');
7830
+ const report = computeExplain(await configDriftDiscover(o), key, {
7831
+ codeDefaults: () => buildUserConfig('/nonexistent/elanous-config-drift/config.json'),
7832
+ });
7833
+ debug.log('config.explain', 'computed', { key, layer: report.prod.layer, ...report.universes });
7834
+ debug.flush();
7835
+ console.log(o.json ? JSON.stringify(report, null, 2) : renderExplain(report));
7836
+ if (report.prod.layer === 'prod-unreadable') process.exit(1);
7837
+ });
7838
+
7776
7839
  // ── config 격리 sync (ISO-1 · 2026-07-13) — 운영→테스트 물질화 동기화 ──
7777
7840
  // 격리 테스트 인스턴스는 운영 config 를 공유하지 않는다(overlay 은퇴 방향).
7778
7841
  configCmd
@@ -74,7 +74,7 @@ export const FEATURE_MATURITY = {
74
74
  loops: 'beta', buzz: 'beta', harness: 'stable', self: 'beta', factcheck: 'beta',
75
75
  provider: 'stable', 'provider:set': 'beta', 'provider:restore': 'beta',
76
76
  'provider:rotate': 'beta', 'provider:use': 'beta', 'status-bar': 'beta',
77
- schedule: 'beta', guardian: 'beta', intake: 'beta', logs: 'stable', ad: 'beta', docs: 'beta',
77
+ schedule: 'beta', guardian: 'beta', intake: 'beta', logs: 'stable', retro: 'beta', ad: 'beta', docs: 'beta',
78
78
  decisions: 'beta', directives: 'beta', claims: 'beta', lesson: 'beta', fleet: 'beta', ops: 'stable', outputs: 'beta', publish: 'beta',
79
79
  env: 'beta', 'agent-mission': 'beta', dev: 'beta', keys: 'beta', finance: 'beta',
80
80
  config: 'stable', tasks: 'stable', brief: 'beta', notify: 'beta', away: 'beta', labels: 'beta', msg: 'beta', ask: 'stable',
@@ -160,7 +160,7 @@ export const FEATURE_MATURITY = {
160
160
  ping: 'stable', provider: 'stable', sessions: 'stable', fork: 'stable', resume: 'stable',
161
161
  decisions: 'stable', work: 'stable', cancel: 'stable',
162
162
  skills: 'beta', skill: 'beta', digest: 'beta', intake: 'beta', ad: 'beta',
163
- taste: 'beta', missions: 'beta', attach: 'beta', detach: 'beta', now: 'beta', loops: 'beta', release: 'beta', away: 'beta', project: 'beta', wish: 'beta',
163
+ taste: 'beta', missions: 'beta', attach: 'beta', detach: 'beta', now: 'beta', loops: 'beta', runs: 'beta', release: 'beta', away: 'beta', project: 'beta', wish: 'beta',
164
164
  brain: 'tool', cc: 'tool', cdx: 'tool', gem: 'tool', local: 'tool',
165
165
  harness: 'ops',
166
166
  // Existing commands outside the CXO four-grade menu list are owner-only.
@@ -58,12 +58,18 @@ export async function handleGraphsValidatePost(req: Request, opts: MetaApiOpts):
58
58
  return jsonResponse({ error: 'bad_request', reason: 'graph (harness|workflow) and yaml (string) required' }, 400);
59
59
  }
60
60
  const { graph, yaml } = body as { graph: GraphKind; yaml: string };
61
+ const { ok, errors, ignoredKeys } = validateGraphYaml(graph, yaml);
62
+ debug.log('graph.kinds', 'validate', { graph, ok, errors, ignored: ignoredKeys });
63
+ return jsonResponse({ ok, errors, ignoredKeys }, ok ? 200 : 422);
64
+ }
65
+
66
+ /** The one validator behind `POST /v1/graphs/validate` and the graph save path. */
67
+ export function validateGraphYaml(graph: GraphKind, yaml: string): { ok: boolean; errors: readonly unknown[]; ignoredKeys: string[]; graphId?: string } {
61
68
  const result = graph === 'harness' ? parseGraphTemplateYaml(yaml) : parseWorkflowYaml(yaml);
62
- const errors = graph === 'harness' ? (result as ReturnType<typeof parseGraphTemplateYaml>).errors : (result as ReturnType<typeof parseWorkflowYaml>).issues;
69
+ const errors: readonly unknown[] = graph === 'harness' ? (result as ReturnType<typeof parseGraphTemplateYaml>).errors : (result as ReturnType<typeof parseWorkflowYaml>).issues;
63
70
  let input: unknown;
64
71
  try { input = parseYaml(yaml); } catch { /* The parser already reports the YAML error. */ }
65
72
  const ignoredKeys = ignoredPaths(input, graph === 'harness' ? harnessShape : workflowShape);
66
- const ok = errors.length === 0;
67
- debug.log('graph.kinds', 'validate', { graph, ok, errors, ignored: ignoredKeys });
68
- return jsonResponse({ ok, errors, ignoredKeys }, ok ? 200 : 422);
73
+ const graphId = graph === 'harness' ? (result as ReturnType<typeof parseGraphTemplateYaml>).template?.graphId : undefined;
74
+ return { ok: errors.length === 0, errors, ignoredKeys, ...(graphId === undefined ? {} : { graphId }) };
69
75
  }
@@ -1,15 +1,31 @@
1
- import { mkdtempSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
1
+ import { existsSync, mkdtempSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
2
2
  import { tmpdir } from 'node:os';
3
3
  import { join } from 'node:path';
4
4
  import { parse as parseYaml } from 'yaml';
5
5
  import { elanousStateRoot } from '../../autopilot/state-paths.js';
6
+ import { debug } from '../../debug/log.js';
6
7
  import { defaultGraphsDir, loadGraphTemplatesFrom } from '../../self-implement/graph-templates.js';
7
8
  import { loadGraphTemplates } from '../../self-implement/graph-yaml.js';
9
+ import { validateGraphYaml } from './graph-kinds.js';
8
10
  import { jsonResponse } from './json-response.js';
9
11
 
10
12
  /** Declared graph ids only. A user string is never joined onto a filesystem path. */
11
13
  const GRAPH_ID = /^[a-z0-9-]+$/;
12
14
 
15
+ /**
16
+ * Sidecar names a graph directory loads specially. `recipes.yaml` next to a graph is what
17
+ * the graph runner executes (`cmd:` recipes), so an HTTP write to `<mine>/recipes.yaml`
18
+ * would be a remote command path. These ids are never created, saved or reverted, and
19
+ * these files are never indexed as graphs.
20
+ */
21
+ export const RESERVED_GRAPH_IDS: ReadonlySet<string> = new Set(['recipes', 'overlay', 'overlays']);
22
+
23
+ function reserved(id: string): Response | null {
24
+ if (!RESERVED_GRAPH_IDS.has(id)) return null;
25
+ debug.log('nexus.graphs', 'rejected-reserved', { id });
26
+ return jsonResponse({ error: 'reserved-id', id }, 400);
27
+ }
28
+
13
29
  export interface GraphCatalogEntry {
14
30
  id: string;
15
31
  source: 'core' | 'mine';
@@ -45,6 +61,7 @@ function readDirYaml(dir: string): string[] {
45
61
  function indexByDeclaredId(dir: string): Map<string, { file: string; text: string }> {
46
62
  const found = new Map<string, { file: string; text: string }>();
47
63
  for (const file of readDirYaml(dir)) {
64
+ if (RESERVED_GRAPH_IDS.has(file.replace(/\.ya?ml$/, ''))) continue;
48
65
  let text: string;
49
66
  try { text = readFileSync(join(dir, file), 'utf8'); }
50
67
  catch { continue; }
@@ -125,6 +142,21 @@ function validateInTemp(text: string, expectedId: string): { ok: true } | { ok:
125
142
  }
126
143
  }
127
144
 
145
+ /** Same validator as POST /v1/graphs/validate. Invalid → 422; declared graph_id ≠ id → 400. Null = ok. */
146
+ function rejectGraphYaml(text: string, id: string, version?: string): Response | null {
147
+ const checked = validateGraphYaml('harness', text);
148
+ if (!checked.ok) {
149
+ debug.log('nexus.graphs', 'rejected-invalid', { id, ...(version ? { version } : {}), issues: checked.errors });
150
+ return jsonResponse({ error: 'invalid-graph', errors: checked.errors, ignoredKeys: checked.ignoredKeys }, 422);
151
+ }
152
+ if (checked.graphId !== id) {
153
+ const errors = [{ path: `${id}/graph_id`, message: 'graph_id 가 요청 id 와 다르다' }];
154
+ debug.log('nexus.graphs', 'rejected-invalid', { id, issues: errors });
155
+ return jsonResponse({ error: 'graph-id-mismatch', errors }, 400);
156
+ }
157
+ return null;
158
+ }
159
+
128
160
  /** Core graphs are read from the same YAML loader as the harness, never rewritten by this API. */
129
161
  export function handleGraphsGet(pathname: string, dir = defaultGraphsDir(), deps: GraphsApiDeps = {}): Response {
130
162
  const coreDir = deps.coreDir ?? dir;
@@ -168,6 +200,8 @@ export async function handleGraphsPut(pathname: string, req: Request, deps: Grap
168
200
  if (!match) return jsonResponse({ error: 'not-found' }, 404);
169
201
  const id = resolveId(match[1]!);
170
202
  if (!id) return jsonResponse({ error: 'bad_request', reason: 'invalid graph id' }, 400);
203
+ const reservedId = reserved(id);
204
+ if (reservedId) return reservedId;
171
205
  const coreDir = deps.coreDir ?? defaultGraphsDir();
172
206
  const mineDir = deps.mineDir ?? mineGraphsDir();
173
207
  const core = coreCatalog(coreDir);
@@ -178,12 +212,146 @@ export async function handleGraphsPut(pathname: string, req: Request, deps: Grap
178
212
  let body: { yaml?: unknown };
179
213
  try { body = await req.json() as { yaml?: unknown }; }
180
214
  catch { return jsonResponse({ error: 'bad_request', reason: 'json body required' }, 400); }
181
- if (typeof body.yaml !== 'string') return jsonResponse({ error: 'bad_request', reason: 'yaml string required' }, 400);
182
- const checked = validateInTemp(body.yaml, id);
183
- if (!checked.ok) return jsonResponse({ error: 'invalid-graph', errors: checked.errors }, 400);
215
+ if (typeof body?.yaml !== 'string') return jsonResponse({ error: 'bad_request', reason: 'yaml string required' }, 400);
216
+ const rejected = rejectGraphYaml(body.yaml, id);
217
+ if (rejected) return rejected;
184
218
  const existing = indexByDeclaredId(mineDir).get(id)!;
185
- writeFileSync(join(mineDir, existing.file), body.yaml);
186
- return jsonResponse({ id, source: 'mine', editable: true, saved: true });
219
+ // Writes go only to <mine>/<id>.yaml; a hand-placed file under another name is not edited over HTTP.
220
+ if (existing.file !== `${id}.yaml`) return jsonResponse({ error: 'file-name-mismatch', id, file: existing.file }, 409);
221
+ const { version, previous } = writeWithVersion(mineDir, id, existing, body.yaml);
222
+ debug.log('nexus.graphs', 'saved', { id, version, previous });
223
+ return jsonResponse({ id, source: 'mine', editable: true, saved: true, version, previous });
224
+ }
225
+
226
+ const VERSION_ID = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z(?:-\d{4})?$/;
227
+ export const GRAPH_VERSION_CAP = 20;
228
+
229
+ function versionsDir(mineDir: string, id: string): string {
230
+ return join(mineDir, '.versions', id);
231
+ }
232
+
233
+ /** Version ids are ISO timestamps (`:`/`.` → `-`), so name order is save order. */
234
+ export function listGraphVersions(mineDir: string, id: string): Array<{ version: string; savedAt: string; bytes: number }> {
235
+ const dir = versionsDir(mineDir, id);
236
+ // Only `.yaml` is ever written, read and reverted — a stray `.yml` is not a version.
237
+ return readDirYaml(dir)
238
+ .filter((file) => file.endsWith('.yaml'))
239
+ .map((file) => file.replace(/\.yaml$/, ''))
240
+ .filter((version) => VERSION_ID.test(version))
241
+ .sort()
242
+ .map((version) => {
243
+ const iso = /^(\d{4}-\d{2}-\d{2})T(\d{2})-(\d{2})-(\d{2})-(\d{3})Z/.exec(version);
244
+ let bytes = 0;
245
+ try { bytes = statSync(join(dir, `${version}.yaml`)).size; } catch { /* listed above; raced away */ }
246
+ return { version, savedAt: iso ? `${iso[1]}T${iso[2]}:${iso[3]}:${iso[4]}.${iso[5]}Z` : version, bytes };
247
+ });
248
+ }
249
+
250
+ function recordVersion(mineDir: string, id: string, text: string): string {
251
+ const dir = versionsDir(mineDir, id);
252
+ mkdirSync(dir, { recursive: true });
253
+ const base = new Date().toISOString().replace(/[:.]/g, '-');
254
+ let version = base;
255
+ for (let n = 1; existsSync(join(dir, `${version}.yaml`)); n += 1) version = `${base}-${String(n).padStart(4, '0')}`;
256
+ writeFileSync(join(dir, `${version}.yaml`), text);
257
+ const all = listGraphVersions(mineDir, id);
258
+ for (const old of all.slice(0, Math.max(0, all.length - GRAPH_VERSION_CAP))) {
259
+ rmSync(join(dir, `${old.version}.yaml`), { force: true });
260
+ }
261
+ return version;
262
+ }
263
+
264
+ /**
265
+ * Every saved state is a version. A graph with no history yet (e.g. a fresh clone)
266
+ * first records its current bytes, so `previous` always points at restorable bytes.
267
+ */
268
+ function writeWithVersion(mineDir: string, id: string, existing: { file: string; text: string }, text: string): { version: string; previous: string } {
269
+ const history = listGraphVersions(mineDir, id);
270
+ const previous = history.at(-1)?.version ?? recordVersion(mineDir, id, existing.text);
271
+ // Version first, then an atomic replace of the current file: a crash in between leaves
272
+ // the old current bytes intact (they are `previous`), never bytes that no version holds.
273
+ const version = recordVersion(mineDir, id, text);
274
+ const target = join(mineDir, `${id}.yaml`);
275
+ const temp = join(mineDir, `.${id}.yaml.${process.pid}.tmp`);
276
+ writeFileSync(temp, text);
277
+ renameSync(temp, target);
278
+ return { version, previous };
279
+ }
280
+
281
+ function mineOwnership(id: string, deps: GraphsApiDeps): { mineDir: string; existing: { file: string; text: string } } | Response {
282
+ const reservedId = reserved(id);
283
+ if (reservedId) return reservedId;
284
+ const coreDir = deps.coreDir ?? defaultGraphsDir();
285
+ const mineDir = deps.mineDir ?? mineGraphsDir();
286
+ const existing = indexByDeclaredId(mineDir).get(id);
287
+ if (!existing) {
288
+ const core = coreCatalog(coreDir);
289
+ if (core.ok && indexByDeclaredId(coreDir).has(id)) return jsonResponse({ error: 'core-read-only', id }, 403);
290
+ return jsonResponse({ error: 'not-found', id }, 404);
291
+ }
292
+ return { mineDir, existing };
293
+ }
294
+
295
+ /** GET /v1/graphs/<id>/versions — only «mine» graphs have history. */
296
+ export function handleGraphVersionsGet(pathname: string, deps: GraphsApiDeps = {}): Response {
297
+ const match = /^\/v1\/graphs\/([^/]+)\/versions$/.exec(pathname);
298
+ if (!match) return jsonResponse({ error: 'not-found' }, 404);
299
+ const id = resolveId(match[1]!);
300
+ if (!id) return jsonResponse({ error: 'bad_request', reason: 'invalid graph id' }, 400);
301
+ const owned = mineOwnership(id, deps);
302
+ if (owned instanceof Response) return owned;
303
+ return jsonResponse({ id, versions: listGraphVersions(owned.mineDir, id) });
304
+ }
305
+
306
+ /** POST /v1/graphs/<id>/revert {version} — restores that version; the restore is itself a new version. */
307
+ export async function handleGraphsRevert(pathname: string, req: Request, deps: GraphsApiDeps = {}): Promise<Response> {
308
+ const match = /^\/v1\/graphs\/([^/]+)\/revert$/.exec(pathname);
309
+ if (!match) return jsonResponse({ error: 'not-found' }, 404);
310
+ const id = resolveId(match[1]!);
311
+ if (!id) return jsonResponse({ error: 'bad_request', reason: 'invalid graph id' }, 400);
312
+ const owned = mineOwnership(id, deps);
313
+ if (owned instanceof Response) return owned;
314
+ if (owned.existing.file !== `${id}.yaml`) return jsonResponse({ error: 'file-name-mismatch', id, file: owned.existing.file }, 409);
315
+ let body: { version?: unknown };
316
+ try { body = await req.json() as { version?: unknown }; }
317
+ catch { return jsonResponse({ error: 'bad_request', reason: 'json body required' }, 400); }
318
+ if (typeof body?.version !== 'string' || !VERSION_ID.test(body.version)) {
319
+ return jsonResponse({ error: 'bad_request', reason: 'version string required' }, 400);
320
+ }
321
+ const target = body.version;
322
+ // Only a listed version id is ever joined onto a path.
323
+ if (!listGraphVersions(owned.mineDir, id).some((entry) => entry.version === target)) {
324
+ return jsonResponse({ error: 'version-not-found', id, version: target }, 404);
325
+ }
326
+ const text = readFileSync(join(versionsDir(owned.mineDir, id), `${target}.yaml`), 'utf8');
327
+ const rejected = rejectGraphYaml(text, id, target);
328
+ if (rejected) return rejected;
329
+ const { version, previous } = writeWithVersion(owned.mineDir, id, owned.existing, text);
330
+ debug.log('nexus.graphs', 'reverted', { id, version, previous, restoredFrom: target });
331
+ return jsonResponse({ id, source: 'mine', editable: true, reverted: true, version, previous, restoredFrom: target });
332
+ }
333
+
334
+ /** POST /v1/graphs {id, yaml} — a brand-new «mine» graph (blank canvas → save). Records version v1. */
335
+ export async function handleGraphsCreate(req: Request, deps: GraphsApiDeps = {}): Promise<Response> {
336
+ let body: { id?: unknown; yaml?: unknown };
337
+ try { body = await req.json() as { id?: unknown; yaml?: unknown }; }
338
+ catch { return jsonResponse({ error: 'bad_request', reason: 'json body required' }, 400); }
339
+ const id = typeof body?.id === 'string' ? resolveId(body.id) : null;
340
+ if (!id) return jsonResponse({ error: 'bad_request', reason: 'id must match [a-z0-9-]' }, 400);
341
+ const reservedId = reserved(id);
342
+ if (reservedId) return reservedId;
343
+ if (typeof body.yaml !== 'string') return jsonResponse({ error: 'bad_request', reason: 'yaml string required' }, 400);
344
+ const coreDir = deps.coreDir ?? defaultGraphsDir();
345
+ const mineDir = deps.mineDir ?? mineGraphsDir();
346
+ if (indexByDeclaredId(coreDir).has(id)) return jsonResponse({ error: 'core-read-only', id }, 403);
347
+ if (indexByDeclaredId(mineDir).has(id) || existsSync(join(mineDir, `${id}.yaml`))) return jsonResponse({ error: 'exists', id }, 409);
348
+ const rejected = rejectGraphYaml(body.yaml, id);
349
+ if (rejected) return rejected;
350
+ mkdirSync(mineDir, { recursive: true });
351
+ writeFileSync(join(mineDir, `${id}.yaml`), body.yaml, { flag: 'wx' });
352
+ const version = recordVersion(mineDir, id, body.yaml);
353
+ debug.log('nexus.graphs', 'saved', { id, version, created: true });
354
+ return jsonResponse({ id, source: 'mine', editable: true, saved: true, version, previous: null }, 201);
187
355
  }
188
356
 
189
357
  /** Copy core bytes into «my graphs», changing only the declared graph_id. */
@@ -198,6 +366,8 @@ export async function handleGraphsClone(pathname: string, req: Request, deps: Gr
198
366
  if (typeof body.newId !== 'string' || !GRAPH_ID.test(body.newId) || body.newId === id) {
199
367
  return jsonResponse({ error: 'bad_request', reason: 'newId must match [a-z0-9-]' }, 400);
200
368
  }
369
+ const reservedId = reserved(body.newId);
370
+ if (reservedId) return reservedId;
201
371
  const coreDir = deps.coreDir ?? defaultGraphsDir();
202
372
  const mineDir = deps.mineDir ?? mineGraphsDir();
203
373
  const source = indexByDeclaredId(coreDir).get(id) ?? indexByDeclaredId(mineDir).get(id);
@@ -215,7 +385,9 @@ export async function handleGraphsClone(pathname: string, req: Request, deps: Gr
215
385
  }
216
386
 
217
387
  export async function handleGraphsMutation(pathname: string, req: Request, deps: GraphsApiDeps = {}): Promise<Response | null> {
388
+ if (req.method === 'POST' && pathname === '/v1/graphs') return handleGraphsCreate(req, deps);
218
389
  if (req.method === 'PUT' && /^\/v1\/graphs\/[^/]+\/yaml$/.test(pathname)) return handleGraphsPut(pathname, req, deps);
219
390
  if (req.method === 'POST' && /^\/v1\/graphs\/[^/]+\/clone$/.test(pathname)) return handleGraphsClone(pathname, req, deps);
391
+ if (req.method === 'POST' && /^\/v1\/graphs\/[^/]+\/revert$/.test(pathname)) return handleGraphsRevert(pathname, req, deps);
220
392
  return null;
221
393
  }
@@ -24,6 +24,7 @@ import { createProject, listProjects } from '../../project/project-store.js';
24
24
  import { handleOutputsGet, type OutputsDeps } from './outputs-route.js';
25
25
  import { buildGridData, type GridDeps } from './grid.js';
26
26
  import { DECISIONS_PATH, handleDecisions, type DecisionsRouteDeps } from './decisions-route.js';
27
+ import { OPS_BOARD_PATH, handleOpsBoard, type OpsBoardDeps } from './ops-board-route.js';
27
28
  import { CARD_FOLLOWUP_PATH, createCardFollowupJobs, type CardFollowupJobs } from './card-followup-route.js';
28
29
  import { SKILL_EXEC_PATH, createSkillExecJobs, type SkillExecJobs } from './skill-exec-route.js';
29
30
  import { compareTokenConstTime } from '../../acp/transport/auth.js';
@@ -166,7 +167,8 @@ import { handleHarnessAskPost, handleHarnessAskStatusGet, handleHarnessRunEvents
166
167
  import { handleLoopEdgesGet } from './loop-edges.js';
167
168
  import { handleFabricPlans, type FabricPlansRouteOpts } from './fabric-plans.js';
168
169
  import { dispatchPersonaRoute } from './personas.js';
169
- import { handleMe } from './operator.js';
170
+ import { handleMe, operatorSignal } from './operator.js';
171
+ import { handleGraphRunRoute } from '../../graph-runner/graph-run-api.js';
170
172
  import { handleOpsApi } from './ops-api.js';
171
173
  import { handleRoleJudge } from './role-judge.js';
172
174
  import { handleAudioStt } from './audio-stt.js';
@@ -363,7 +365,7 @@ import {
363
365
  } from './workflows.js';
364
366
  import { handleTriggersSnapshot } from './triggers.js';
365
367
  import { handleWorkflowTemplatesList } from './workflow-templates.js';
366
- import { handleGraphsGet, handleGraphsMutation } from './graphs-api.js';
368
+ import { handleGraphVersionsGet, handleGraphsGet, handleGraphsMutation } from './graphs-api.js';
367
369
  import { handleGraphKindsGet, handleGraphsValidatePost } from './graph-kinds.js';
368
370
  import { syncInstalledPluginNodes } from '../../graph-kinds/installed-plugin-nodes.js';
369
371
  import { elanousStateRoot } from '../../autopilot/state-paths.js';
@@ -484,6 +486,7 @@ export interface NexusHttpServerOpts {
484
486
  /** Read-only grid measurement seam; omitted in production for live HQ and Pod pool reads. */
485
487
  grid?: GridDeps;
486
488
  decisions?: Pick<DecisionsRouteDeps, 'ledger'>;
489
+ opsBoard?: Pick<OpsBoardDeps, 'openStore'>;
487
490
  cardFollowup?: CardFollowupJobs;
488
491
  skillExec?: SkillExecJobs;
489
492
  execRequests?: ExecRequestRunner;
@@ -938,6 +941,20 @@ export async function routeRequest(
938
941
  }
939
942
  return pitch.handlePitchApi(req, opts.metaApi);
940
943
  }
944
+ // CGE-RUN — 편집기 «실행»은 운영자 전용이다: 사용자 그래프가 셸 노드를 부를 수 있으므로 같은 출처 무토큰 통과는 받지 않는다
945
+ // (운영자 프록시 헤더 · owner bearer 일치 · owner 가 발급한 짧은 임시 토큰만 — 명시 토큰은 언제나 필요).
946
+ if (/^\/v1\/graphs\/[^/]+\/(?:run|runs\/[^/]+)$/.test(pathname)) {
947
+ if (!opts.metaApi) return jsonResponse({ error: 'meta-api-not-wired' }, 503);
948
+ const signal = operatorSignal(req, opts.metaApi);
949
+ const credential = bearerCredential(req, opts.metaApi);
950
+ if (!signal.operator || (signal.operatorSource !== 'op-proxy' && credential !== 'bearer-match' && credential !== 'temp-token')) {
951
+ debug.log('graphs.run', 'refused', { path: pathname, source: signal.operatorSource });
952
+ return jsonResponse({ error: 'forbidden' }, 403);
953
+ }
954
+ const routed = handleGraphRunRoute(method, pathname);
955
+ if (routed) return routed;
956
+ return jsonResponse({ error: 'method-not-allowed' }, 405);
957
+ }
941
958
 
942
959
  // Default-deny for every `/v1/` path that is not on PUBLIC_ROUTES.
943
960
  // Reuses checkAuth (same-origin exemption + constant-time bearer).
@@ -1392,6 +1409,21 @@ export async function routeRequest(
1392
1409
  return (opts.cardFollowup ?? defaultCardFollowupJobs()).get(pathname.slice(CARD_FOLLOWUP_PATH.length + 1));
1393
1410
  }
1394
1411
 
1412
+ if (pathname === OPS_BOARD_PATH && (method === 'GET' || method === 'POST')) {
1413
+ const peerAddress = (server as BunServerLike & { requestIP?: (request: Request) => { address: string } | null }).requestIP?.(req)?.address;
1414
+ const host = req.headers.get('host') ?? new URL(req.url).host;
1415
+ const tailnetPeer = peerAddress !== undefined && (/^100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\./.test(peerAddress)
1416
+ || /^fd7a:115c:a1e0:/i.test(peerAddress));
1417
+ const localPeer = peerAddress === '127.0.0.1' || peerAddress === '::1';
1418
+ const tailnetHost = /^100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\.\d{1,3}\.\d{1,3}(?::\d+)?$/.test(host)
1419
+ || /^\[fd7a:115c:a1e0:[0-9a-f:]+\](?::\d+)?$/i.test(host) || /^[a-z0-9-]+(?:\.[a-z0-9-]+)*\.ts\.net(?::\d+)?$/i.test(host);
1420
+ const localHost = /^(?:localhost|127\.0\.0\.1)(?::\d+)?$/.test(host) || /^\[::1\](?::\d+)?$/.test(host);
1421
+ if ((!tailnetPeer && !localPeer) || (!tailnetHost && !localHost)) return jsonResponse({ error: 'forbidden' }, 403);
1422
+ return handleOpsBoard(req, {
1423
+ authorize: request => !!opts.metaApi && opts.metaApi.noAuth !== true && checkAuth(request, opts.metaApi),
1424
+ ...opts.opsBoard,
1425
+ });
1426
+ }
1395
1427
  if (method === 'GET' && pathname === DECISIONS_PATH) {
1396
1428
  return handleDecisions(req, {
1397
1429
  authorize: request => !!opts.metaApi && checkAuth(request, opts.metaApi),
@@ -1471,7 +1503,7 @@ export async function routeRequest(
1471
1503
  if (!opts.metaApi) return jsonResponse({ error: 'unauthorized' }, 401);
1472
1504
  return handlePluginsRemove(req, pluginRemove[1]!, opts.metaApi, opts.pluginStateRoot);
1473
1505
  }
1474
- if ((method === 'PUT' || method === 'POST') && pathname.startsWith('/v1/graphs/')) {
1506
+ if ((method === 'PUT' || method === 'POST') && (pathname.startsWith('/v1/graphs/') || pathname === '/v1/graphs')) {
1475
1507
  if (!opts.metaApi) return jsonResponse({ error: 'meta-api-not-wired' }, 503);
1476
1508
  if (!checkAuth(req, opts.metaApi)) return jsonResponse({ error: 'unauthorized' }, 401);
1477
1509
  const mutated = await handleGraphsMutation(pathname, req);
@@ -2785,6 +2817,9 @@ export async function routeRequest(
2785
2817
  if (method === 'GET' && (pathname === '/v1/graphs' || /^\/v1\/graphs\/[^/]+$/.test(pathname) || /^\/v1\/graphs\/[^/]+\/yaml$/.test(pathname))) {
2786
2818
  return handleGraphsGet(pathname);
2787
2819
  }
2820
+ if (method === 'GET' && /^\/v1\/graphs\/[^/]+\/versions$/.test(pathname)) {
2821
+ return handleGraphVersionsGet(pathname);
2822
+ }
2788
2823
  // Archon-port T2.3 (2026-05-08) — workflow GET surface.
2789
2824
  // (POST /validate · POST /:name/run · PUT/DELETE /:name landed
2790
2825
  // inside the mutation block above.)