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,78 @@
1
+ import { existsSync } from 'node:fs';
2
+ import type { Command } from 'commander';
3
+ import { LogStore, logsDbPath } from '../mss/logging/log-store.js';
4
+
5
+ type RetroFact = { store: string; at: string; category: string; event: string; kind: 'draft-birth' | 'draft-death' | 'stop-reason' | 'ta-outcome'; data: unknown };
6
+
7
+ const FACT_EVENTS = new Map<string, RetroFact['kind']>([
8
+ ['rework-blocked-draft-pr', 'draft-birth'],
9
+ ['lineage-supersede', 'draft-death'],
10
+ ['resolved', 'stop-reason'],
11
+ ['decision', 'stop-reason'],
12
+ ['action', 'ta-outcome'],
13
+ ['task-agent.action', 'ta-outcome'],
14
+ ]);
15
+
16
+ export function parseRetroSince(value: string, now = Date.now()): number {
17
+ const match = /^([1-9]\d*)([smhd])$/.exec(value);
18
+ if (!match) throw new Error('--since requires a positive duration (e.g. 24h)');
19
+ const duration = Number(match[1]) * ({ s: 1000, m: 60000, h: 3600000, d: 86400000 }[match[2]!] ?? 0);
20
+ if (!Number.isSafeInteger(duration) || !Number.isSafeInteger(now - duration)) throw new Error('--since duration is out of range');
21
+ return now - duration;
22
+ }
23
+
24
+ export function collectRetroFacts(sinceMs: number, targets: readonly { name: string; dbPath: string }[] = [{ name: 'current', dbPath: logsDbPath() }]): { facts: RetroFact[]; unavailable: string[] } {
25
+ const facts: RetroFact[] = [];
26
+ const unavailable: string[] = [];
27
+ const untilMs = Date.now();
28
+ for (const target of targets) {
29
+ if (!existsSync(target.dbPath)) { unavailable.push(target.name); continue; }
30
+ try {
31
+ const store = LogStore.openReadOnly(target.dbPath);
32
+ try {
33
+ let beforeId: number | undefined;
34
+ for (;;) {
35
+ const rows = store.query({ sinceMs, beforeId, limit: 1000 });
36
+ for (const row of rows) {
37
+ if (Date.parse(row.ts) > untilMs) continue;
38
+ const kind = FACT_EVENTS.get(row.event);
39
+ if (!kind || (row.event === 'resolved' && row.category !== 'self-dev.supervisor') || ((row.event === 'task-agent.action' || row.event === 'action') && row.category !== 'task-agent')) continue;
40
+ let data: unknown;
41
+ try { data = row.data === null ? null : JSON.parse(row.data); } catch { data = row.data; }
42
+ const record = data && typeof data === 'object' && !Array.isArray(data) ? data as Record<string, unknown> : {};
43
+ if (row.event === 'rework-blocked-draft-pr' && (row.category !== 'self-implement' || !Number.isInteger(record.number) || record.skipped || record.error)) continue;
44
+ if (row.event === 'lineage-supersede' && (row.category !== 'self-implement' || !Array.isArray(record.closed) || record.closed.length === 0)) continue;
45
+ if (row.event === 'resolved' && typeof record.stopReason !== 'string') continue;
46
+ if (row.event === 'decision' && (row.category !== 'self-dev.supervisor' || record.action !== 'stop' || typeof record.stopReason !== 'string')) continue;
47
+ if ((row.event === 'task-agent.action' || row.event === 'action') && (row.category !== 'task-agent' || typeof record.result !== 'string' || !record.result.trim())) continue;
48
+ facts.push({ store: target.name, at: row.ts, category: row.category, event: row.event, kind, data });
49
+ }
50
+ if (rows.length < 1000) break;
51
+ beforeId = rows.at(-1)!.id;
52
+ }
53
+ } finally { store.close(); }
54
+ } catch { unavailable.push(target.name); }
55
+ }
56
+ facts.sort((a, b) => b.at.localeCompare(a.at));
57
+ return { facts, unavailable };
58
+ }
59
+
60
+ export function registerRetroCommands(program: Command): void {
61
+ program.command('retro').description('Read-only retrospective facts from existing logs')
62
+ .command('facts').description('Draft births/deaths, stop reasons and task-agent outcomes')
63
+ .option('--since <duration>', 'Lookback duration', '24h')
64
+ .option('--json', 'JSON output')
65
+ .action((opts: { since: string; json?: boolean }) => {
66
+ try {
67
+ const sinceMs = parseRetroSince(opts.since);
68
+ const result = { since: new Date(sinceMs).toISOString(), ...collectRetroFacts(sinceMs) };
69
+ console.log(opts.json ? JSON.stringify(result) : [
70
+ `retro facts since ${result.since} · ${result.facts.length} facts · unavailable: ${result.unavailable.join(', ') || 'none'}`,
71
+ ...result.facts.map((fact) => `${fact.at} ${fact.kind} ${fact.category}.${fact.event} ${JSON.stringify(fact.data)}`),
72
+ ].join('\n'));
73
+ } catch (error) {
74
+ console.error(`retro facts: ${error instanceof Error ? error.message : String(error)}`);
75
+ process.exitCode = 2;
76
+ }
77
+ });
78
+ }
@@ -1,4 +1,6 @@
1
- import { readFileSync } from 'node:fs';
1
+ import { closeSync, mkdirSync, openSync, readFileSync, readSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { loadRunLedger as loadRunLedgerSync } from '../self-implement/run-ledger.js';
2
4
  import { join, resolve } from 'node:path';
3
5
  import { InvalidArgumentError, Option, type Command } from 'commander';
4
6
  import { debug } from '../debug/log.js';
@@ -8,7 +10,7 @@ import { checklistDevVersion } from '../release-loop/checklist.js';
8
10
  import { cardsBoard, countBoardTasks, INTERNAL_PACK_PROJECT, mergeBoards, releasePackBoard, type BoardNode } from '../task-agent/board.js';
9
11
  import type { Task } from '../task-orchestrator/types.js';
10
12
  import { cardPrNumber, COMPLETION_KINDS, handTask, nextMoveFor, readTaskAgentState, readTaskCard, taskAgentStatePath as taskStatePathDefault, shellQuote, TASK_CARD_PREFIX, TASK_SEATS, type CompletionKind, type TaskCard, type TaskLaunchContext, type TaskLaunchReceipt, type TaskLauncher, type TaskSeat } from '../task-agent/task-hand.js';
11
- import { refreshCardRunBinding, type CardRunEvidenceDeps } from '../task-agent/card-evidence.js';
13
+ import { readCardSalvageBranch, refreshCardRunBinding, type CardRunEvidenceDeps } from '../task-agent/card-evidence.js';
12
14
  import { advanceMission, handMission, MissionAdvanceError, openMissionIds, recordPieceRef, type AdvanceMissionResult, type MissionDecompose, type PieceEvidenceReader } from '../task-agent/mission.js';
13
15
 
14
16
  type TaskRow = Pick<Task, 'id' | 'title' | 'priority' | 'status' | 'createdAt' | 'approval' | 'generatedBy'>;
@@ -82,14 +84,89 @@ export function taskLauncherArgv(configDir: string, args: readonly string[]): st
82
84
  return [ELANOUS_CLI_ENTRY, '--config-dir', configDir, ...args];
83
85
  }
84
86
 
85
- export async function defaultTaskLauncher(args: string[], cwd?: string, context?: TaskLaunchContext, deps: { spawn?: DetachedSpawn } = {}): Promise<void | TaskLaunchReceipt> {
87
+ /** STOP-RECORD launch-failed: 발사 직후 이 창 안에 자식이 비0 으로 끝나면 «발사 실패»로 본다. */
88
+ export const LAUNCH_EARLY_EXIT_WINDOW_MS = 3_000;
89
+
90
+ export interface DefaultTaskLauncherDeps {
91
+ spawn?: DetachedSpawn;
92
+ /** 조기 종료 감시 창(ms) — context(런 id)가 있을 때만 · 0 이면 감시하지 않는다(옛 동작). */
93
+ earlyExitWindowMs?: number;
94
+ /** 자식 stderr 를 받을 디렉토리(시험 주입). 기본 = `<tmpdir>/elanous-task-launch`. */
95
+ stderrDir?: string;
96
+ /** stop 줄 기록기(시험 주입). 기본 = 런 원장. */
97
+ writeStop?: import('../self-implement/run-ledger.js').RunLedgerWriter;
98
+ /** 이미 stop 이 있나(시험 주입). 기본 = 이 우주 런 원장 ⊕ 이 프로세스 기록. */
99
+ hasStop?: (runId: string) => boolean;
100
+ }
101
+
102
+ /** stderr 파일의 첫 비어 있지 않은 줄(최대 4KB 만 읽는다 — 원 로그 꼬리를 싣지 않는다). */
103
+ function firstStderrLine(path: string): string {
104
+ try {
105
+ const fd = openSync(path, 'r');
106
+ try {
107
+ const buf = Buffer.alloc(4096);
108
+ const n = readSync(fd, buf, 0, buf.length, 0);
109
+ return buf.subarray(0, n).toString('utf8').split(/\r?\n/).map((line) => line.trim()).find((line) => line.length > 0) ?? '';
110
+ } finally { closeSync(fd); }
111
+ } catch { return ''; }
112
+ }
113
+
114
+ export async function defaultTaskLauncher(args: string[], cwd?: string, context?: TaskLaunchContext, deps: DefaultTaskLauncherDeps = {}): Promise<void | TaskLaunchReceipt> {
86
115
  const spawn = deps.spawn ?? ((await import('node:child_process')).spawn as unknown as DetachedSpawn);
87
116
  const { getElanousConfigDirOverride } = await import('../elanous-config-dir.js');
88
117
  const { prodInstanceRoot } = await import('../instance/resolve.js');
89
118
  const configDir = getElanousConfigDirOverride() ?? prodInstanceRoot();
90
- // TA-CARD-RUN-LINK: 런 id 를 자식 환경에 얹는다 — `harness say` 의 런(호스트 orchestrate)이 그 id 를 상속한다.
91
- await spawnDetachedConfirmed(spawn, process.execPath, taskLauncherArgv(configDir, args), SPAWN_CONFIRM_TIMEOUT_MS, cwd, context?.env);
92
- return context ? { runId: context.runId } : undefined;
119
+ const argv = taskLauncherArgv(configDir, args);
120
+ const window = context ? deps.earlyExitWindowMs ?? LAUNCH_EARLY_EXIT_WINDOW_MS : 0;
121
+ if (!context || window <= 0) {
122
+ // TA-CARD-RUN-LINK: 런 id 를 자식 환경에 얹는다 — `harness say` 의 런(호스트 orchestrate)이 그 id 를 상속한다.
123
+ await spawnDetachedConfirmed(spawn, process.execPath, argv, SPAWN_CONFIRM_TIMEOUT_MS, cwd, context?.env);
124
+ return context ? { runId: context.runId } : undefined;
125
+ }
126
+ // STOP-RECORD launch-failed: stdio 를 버리면 자식이 exit 2 로 죽어도 흔적이 0 이고 카드는 «launched» 로 남는다
127
+ // (ORCH-LIVE-1008). stderr 만 파일로 받고(파이프는 부모가 먼저 끝나면 자식을 EPIPE 로 죽인다), 짧은 창 안의 종료를 본다.
128
+ const stderrDir = deps.stderrDir ?? join(tmpdir(), 'elanous-task-launch');
129
+ mkdirSync(stderrDir, { recursive: true });
130
+ const stderrPath = join(stderrDir, `${context.runId}.stderr`);
131
+ // 'w' — 같은 runId 재발사가 지난 시도의 stderr 를 이번 cause 로 읽지 않게(리뷰 must-fix).
132
+ const fd = openSync(stderrPath, 'w');
133
+ let exited: Promise<{ code: number | null; signal: string | null }> | undefined;
134
+ const watched: DetachedSpawn = (command, spawnArgs, options) => {
135
+ const child = (spawn as unknown as (c: string, a: string[], o: unknown) => ReturnType<DetachedSpawn>)(command, spawnArgs, { ...options, stdio: ['ignore', 'ignore', fd] });
136
+ const emitter = child as unknown as { once?: (event: 'exit', listener: (code: number | null, signal: string | null) => void) => unknown };
137
+ exited = new Promise((done) => { emitter.once?.('exit', (code, signal) => done({ code, signal })); });
138
+ return child;
139
+ };
140
+ try {
141
+ await spawnDetachedConfirmed(watched, process.execPath, argv, SPAWN_CONFIRM_TIMEOUT_MS, cwd, context.env);
142
+ } finally {
143
+ try { closeSync(fd); } catch { /* the child keeps its own copy */ }
144
+ }
145
+ let timer: ReturnType<typeof setTimeout> | undefined;
146
+ const early = exited
147
+ ? await Promise.race([exited, new Promise<undefined>((done) => { timer = setTimeout(() => done(undefined), window); })])
148
+ : undefined;
149
+ if (timer) clearTimeout(timer);
150
+ if (early && early.code !== 0) {
151
+ const first = firstStderrLine(stderrPath);
152
+ const cause = `child rc=${early.code ?? early.signal ?? 'unknown'}${first ? `: ${first}` : ''}`;
153
+ const { boundRunStopCause, recordRunStop, runStopRecorded, RUN_STOP_EVENT } = await import('../self-implement/run-stop.js');
154
+ const hasStop = deps.hasStop ?? ((id: string) => {
155
+ if (runStopRecorded(id)) return true;
156
+ try { return (loadRunLedgerSync(id) ?? []).some((entry) => entry.event === RUN_STOP_EVENT); } catch { return false; }
157
+ });
158
+ // 자식이 같은 runId 로 이미 stop 을 썼으면 더하지 않는다(런마다 한 줄 · 리뷰 must-fix).
159
+ if (hasStop(context.runId)) throw new Error(`발사 직후 자식이 종료했다 — ${boundRunStopCause(cause)}`);
160
+ const entry = recordRunStop({
161
+ runId: context.runId, site: 'launch-failed', class: 'launch-failed',
162
+ cause,
163
+ evidenceRef: stderrPath,
164
+ nextMove: 'Fix the launch argv/config from the stderr evidence, then re-hand the card',
165
+ }, ...(deps.writeStop ? [deps.writeStop] : []));
166
+ // handTask 가 이 예외로 카드를 `launch-failed` 로 적는다(«launched» 로 남지 않게).
167
+ throw new Error(`발사 직후 자식이 종료했다 — ${String(entry.data.cause)}`);
168
+ }
169
+ return { runId: context.runId };
93
170
  }
94
171
 
95
172
  /** `spawn` 의 필요한 면만(시험 주입). */
@@ -523,8 +600,17 @@ export function registerTasksCommands(program: Command, deps: TasksCliDeps = {})
523
600
  }
524
601
  return;
525
602
  }
526
- if (opts.json) out(JSON.stringify({ card, nextMove: nextMoveFor(card) }));
527
- else for (const line of taskCardLines(card)) out(line);
603
+ // DRAFT-NOT-ARCHIVE: 멈춘 런이 draft PR 대신 남긴 수확 가지(원장 `salvaged`). 못 읽었으면 «못 읽음»(⛔ 없음 아님).
604
+ let salvage: Awaited<ReturnType<typeof readCardSalvageBranch>> = { salvageBranch: null };
605
+ try { salvage = await readCardSalvageBranch(card, deps.cardRunEvidence ?? {}); }
606
+ catch (error) { salvage = { salvageBranch: null, readErrors: [error instanceof Error ? error.message : String(error)] }; }
607
+ const salvageUnread = !salvage.salvageBranch && !!salvage.readErrors?.length;
608
+ if (opts.json) out(JSON.stringify({ card, nextMove: nextMoveFor(card), salvageBranch: salvage.salvageBranch, ...(salvageUnread ? { salvageBranchUnread: true } : {}) }));
609
+ else {
610
+ for (const line of taskCardLines(card)) out(line);
611
+ if (salvage.salvageBranch) out(`수확 가지: ${salvage.salvageBranch} (draft PR 대신)`);
612
+ else if (salvageUnread) out('수확 가지: 못 읽음(원장 읽기 실패)');
613
+ }
528
614
  return;
529
615
  }
530
616
  try {
@@ -1505,15 +1505,16 @@ export function buildDashboardSlashRegistry(nowDeps?: ContextNowDeps, decideLedg
1505
1505
 
1506
1506
  registry.register(['help', '?'], (_args, ctx) => {
1507
1507
  const { cols, rows } = termSize();
1508
+ // The transient modal paints only height - 2 content rows and does not scroll.
1509
+ // The builder adds columns to fit that budget; only when it still overflows, use the scrollable chat log.
1510
+ const minHeight = Math.min(6, Math.max(3, rows - 2));
1511
+ const modalContentRows = Math.max(minHeight, Math.min(rows - 4, Math.floor(rows * 0.7))) - 2;
1508
1512
  const lines = buildEssentialHelpLines({
1509
1513
  names: registry.names(),
1510
1514
  descriptions: SLASH_COMMANDS,
1511
1515
  width: Math.max(1, Math.floor(cols * 0.7) - 2),
1516
+ maxRows: modalContentRows,
1512
1517
  });
1513
- // The transient modal paints only height - 2 content rows and does not scroll.
1514
- // When the catalog outgrows it, use the scrollable chat log instead of hiding keys.
1515
- const minHeight = Math.min(6, Math.max(3, rows - 2));
1516
- const modalContentRows = Math.max(minHeight, Math.min(rows - 4, Math.floor(rows * 0.7))) - 2;
1517
1518
  if (ctx.showHelpModal && lines.length <= modalContentRows) {
1518
1519
  ctx.showHelpModal({ title: 'Dashboard help', lines });
1519
1520
  } else {
@@ -43,11 +43,15 @@ export function buildEssentialHelpLines({
43
43
  descriptions,
44
44
  width,
45
45
  audience = readTuiSlashAudience(),
46
+ maxRows,
46
47
  }: {
47
48
  names: readonly string[];
48
49
  descriptions: readonly Pick<SlashCommand, 'name' | 'aliases' | 'description'>[];
49
50
  width: number;
50
51
  audience?: { role: Role; showBeta: boolean };
52
+ /** The modal's row budget. When the readable layout overflows it, add columns (shorter descriptions)
53
+ * before the caller has to fall back to the chat log — TUI-HELP-OVERLAY (0.2.19 tui node «help-closes»). */
54
+ maxRows?: number;
51
55
  }): string[] {
52
56
  const registered = new Set(names);
53
57
  const entries = descriptions
@@ -67,41 +71,63 @@ export function buildEssentialHelpLines({
67
71
  // in each cell. Never trade a missing label for a shorter modal.
68
72
  const widestLabel = Math.max(0, ...entries.map(({ label }) => visibleWidth(label)));
69
73
  const readableCellWidth = widestLabel + 2 + 28 + 1;
70
- const columns = entries.length >= 40 && width >= 100
74
+ const baseColumns = entries.length >= 40 && width >= 100
71
75
  ? Math.max(1, Math.floor(width / (readableCellWidth + 1)))
72
76
  : 1;
73
- const columnWidth = Math.floor(width / columns);
74
- // Task order (TUI-SLASH-DECIDE-NOW C): group head line, then that group's commands by name.
75
- // Empty groups print no head; unmapped commands land in «그 밖», so none disappears.
76
- const commandLines: string[] = [];
77
- for (const group of HELP_GROUP_ORDER) {
78
- const members = entries.filter(({ name }) => helpGroupFor(name) === group);
79
- if (members.length === 0) continue;
80
- commandLines.push(fitLine(helpGroupHeading(group), width));
81
- for (let start = 0; start < members.length; start += columns) {
82
- const cells = members.slice(start, start + columns).map(({ label, description }) => {
83
- const cellWidth = columnWidth - (columns > 1 ? 1 : 0);
84
- return visibleWidth(`${label} `) > cellWidth ? '' : fitLine(`${label} ${description}`, cellWidth);
85
- });
86
- commandLines.push(cells.map((cell, index) =>
87
- index === cells.length - 1 ? cell : cell + ' '.repeat(columnWidth - visibleWidth(cell)),
88
- ).join('').trimEnd());
89
- }
77
+ // Narrowest cell that still shows the whole label and a few description characters.
78
+ const minCellWidth = widestLabel + 2 + 10 + 1;
79
+ let lines = layout(baseColumns);
80
+ for (let columns = baseColumns + 1; maxRows !== undefined && lines.length > maxRows
81
+ && Math.floor(width / columns) - 1 >= minCellWidth; columns++) {
82
+ lines = layout(columns);
90
83
  }
91
- const keyItems = ESSENTIAL_KEYS.map(([key, description]) => `${key} ${description}`);
92
- const keyLines: string[] = [];
93
- if (entries.length >= 40 && width >= 100) {
94
- let keyRow = '';
95
- for (const item of keyItems) {
96
- if (keyRow && visibleWidth(keyRow) + 2 + visibleWidth(item) > width) {
97
- keyLines.push(keyRow);
98
- keyRow = '';
84
+ return lines;
85
+
86
+ function layout(columns: number): string[] {
87
+ const columnWidth = Math.floor(width / columns);
88
+ // Task order (TUI-SLASH-DECIDE-NOW C): group head line, then that group's commands by name.
89
+ // Empty groups print no head; unmapped commands land in «그 밖», so none disappears.
90
+ const commandLines: string[] = [];
91
+ const cellWidth = columnWidth - (columns > 1 ? 1 : 0);
92
+ const cellOf = ({ label, description }: { label: string; description: string }) =>
93
+ visibleWidth(`${label} `) > cellWidth ? '' : fitLine(`${label} ${description}`, cellWidth);
94
+ const pushRow = (cells: string[]) => commandLines.push(cells.map((cell, index) =>
95
+ index === cells.length - 1 ? cell : cell + ' '.repeat(columnWidth - visibleWidth(cell)),
96
+ ).join('').trimEnd());
97
+ if (columns > 1) {
98
+ // TUI-HELP-OVERLAY · 2026-10-07 — in columns the group head is a cell and groups flow on: a head line plus a
99
+ // half-empty last row per group pushed the catalog past the modal's rows, so /help fell back to the chat log
100
+ // and Esc no longer closed it (0.2.19 release tui node «help-closes»).
101
+ const cells: string[] = [];
102
+ for (const group of HELP_GROUP_ORDER) {
103
+ const members = entries.filter(({ name }) => helpGroupFor(name) === group);
104
+ if (members.length === 0) continue;
105
+ cells.push(fitLine(helpGroupHeading(group), cellWidth), ...members.map(cellOf));
106
+ }
107
+ for (let start = 0; start < cells.length; start += columns) pushRow(cells.slice(start, start + columns));
108
+ } else {
109
+ for (const group of HELP_GROUP_ORDER) {
110
+ const members = entries.filter(({ name }) => helpGroupFor(name) === group);
111
+ if (members.length === 0) continue;
112
+ commandLines.push(fitLine(helpGroupHeading(group), width));
113
+ for (const member of members) pushRow([cellOf(member)]);
114
+ }
115
+ }
116
+ const keyItems = ESSENTIAL_KEYS.map(([key, description]) => `${key} ${description}`);
117
+ const keyLines: string[] = [];
118
+ if (entries.length >= 40 && width >= 100) {
119
+ let keyRow = '';
120
+ for (const item of keyItems) {
121
+ if (keyRow && visibleWidth(keyRow) + 2 + visibleWidth(item) > width) {
122
+ keyLines.push(keyRow);
123
+ keyRow = '';
124
+ }
125
+ keyRow += (keyRow ? ' ' : '') + fitLine(item, width);
99
126
  }
100
- keyRow += (keyRow ? ' ' : '') + fitLine(item, width);
127
+ if (keyRow) keyLines.push(keyRow);
128
+ } else {
129
+ keyLines.push(...keyItems.map((item) => fitLine(item, width)));
101
130
  }
102
- if (keyRow) keyLines.push(keyRow);
103
- } else {
104
- keyLines.push(...keyItems.map((item) => fitLine(item, width)));
131
+ return [fitLine('Commands', width), ...commandLines, '', fitLine('Keys', width), ...keyLines];
105
132
  }
106
- return [fitLine('Commands', width), ...commandLines, '', fitLine('Keys', width), ...keyLines];
107
133
  }
@@ -2,7 +2,12 @@ import { inQuietHours, kstMinutes, sendOutbound, setInProcessOutbound } from './
2
2
  import { setUserConfigOverlay } from '../user-config.js';
3
3
 
4
4
  // Focused subprocess fixture: isolated state/config roots and a fake curl on PATH.
5
- setUserConfigOverlay(cfg => ({ ...cfg, telegram: { ...cfg.telegram, botToken: '123456:private-secret', homeChannel: 98765 } }));
5
+ // Hermetic: the shared `.elanous-test` config may carry a routing table another test left behind (a declared
6
+ // `channels` table is authoritative and never falls back to the main home), so the fixture owns every routing key.
7
+ setUserConfigOverlay(cfg => ({ ...cfg, telegram: {
8
+ ...cfg.telegram, channels: undefined, reportChannel: undefined, testChannel: undefined,
9
+ botToken: '123456:private-secret', homeChannel: 98765,
10
+ } }));
6
11
  process.env.SEND_VIA_ELANOUS = '0';
7
12
  // Quiet hours (00:00~06:30 KST) defer instead of sending, so a gate run at night saw no `sent` row.
8
13
  // Shift this fixture's clock back to 23:59 KST of the previous day; the shift stays inside this process.
@@ -0,0 +1,205 @@
1
+ // CGE-RUN — 편집기에서 «내 그래프»를 실행하고 노드별 상태를 읽는다.
2
+ // 실행은 `elanous graph run` 과 같은 범용 실행기(runGraph)다. 하니스(LLM) 런은 골 종류로만 그래프를 고르므로 여기서 다루지 않는다.
3
+ // 그래프와 레시피는 런마다 작업 폴더로 «복사»해서 돌린다 — 실행 중에 편집기가 파일을 바꿔도 이 런은 시작한 판으로 끝난다.
4
+ // ⛔ 레시피(실제로 도는 명령)는 «저장소»의 편집기 레시피(`graphs/demo/editor-recipes.yaml`)만 쓴다 — «mine» 폴더는 HTTP 로 쓸 수 있으므로
5
+ // 거기 레시피를 읽으면 «편집한 그래프 실행»이 «편집한 명령 실행»이 된다(TC 10-07 19:31). 그래프가 부르는 레시피가 그 목록에 없으면 시작 전에 거부한다.
6
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
7
+ import { randomBytes } from 'node:crypto';
8
+ import { join } from 'node:path';
9
+ import { parse as parseYaml } from 'yaml';
10
+ import { elanousStateRoot } from '../autopilot/state-paths.js';
11
+ import { debug } from '../debug/log.js';
12
+ import { effectiveInstanceRoot } from '../instance/resolve.js';
13
+ import { jsonResponse } from '../nexus/api/json-response.js';
14
+ import { defaultGraphsDir } from '../self-implement/graph-templates.js';
15
+ import { runGraph, type GraphRunState } from './runner.js';
16
+
17
+ const GRAPH_ID = /^[a-z0-9-]+$/;
18
+ const RUN_ID = /^ed-[a-z0-9]+-[a-f0-9]{6}$/;
19
+ /** 실행기가 받는 끝 노드 이름(runner.ts — `unsupported terminal node`). 미리 말해 준다. */
20
+ const RUNNABLE_TERMINALS = new Set(['done', 'failed']);
21
+
22
+ export interface GraphRunApiDeps {
23
+ mineDir?: string;
24
+ /** 편집기 실행이 쓰는 레시피 파일(저장소 것만) — 기본 `graphs/demo/editor-recipes.yaml`. */
25
+ recipesFile?: string;
26
+ root?: string;
27
+ run?: typeof runGraph;
28
+ now?: () => number;
29
+ }
30
+
31
+ /** 시작 전에 죽은 런(레시피 없음 등)은 원장 파일이 안 생긴다 — 그 이유를 여기 잠깐 들고 있다가 상태 조회에 돌려준다. */
32
+ const startFailures = new Map<string, { error: string; at: string }>();
33
+
34
+ const mineDirOf = (deps: GraphRunApiDeps) => deps.mineDir ?? join(elanousStateRoot(), 'graphs');
35
+ const rootOf = (deps: GraphRunApiDeps) => deps.root ?? effectiveInstanceRoot();
36
+
37
+ function decodeId(raw: string, pattern: RegExp): string | null {
38
+ try {
39
+ const id = decodeURIComponent(raw);
40
+ return pattern.test(id) ? id : null;
41
+ } catch { return null; }
42
+ }
43
+
44
+ /** 파일 이름이 아니라 선언된 graph_id 로 찾는다(graphs-api 와 같은 규칙). */
45
+ function findMine(dir: string, id: string): { file: string; doc: Record<string, unknown> } | null {
46
+ let names: string[];
47
+ try { names = readdirSync(dir).filter((name) => name.endsWith('.yaml') || name.endsWith('.yml')).sort(); }
48
+ catch { return null; }
49
+ for (const name of names) {
50
+ if (name === 'recipes.yaml') continue;
51
+ try {
52
+ const doc: unknown = parseYaml(readFileSync(join(dir, name), 'utf8'));
53
+ if (doc && typeof doc === 'object' && !Array.isArray(doc) && (doc as Record<string, unknown>).graph_id === id) {
54
+ return { file: join(dir, name), doc: doc as Record<string, unknown> };
55
+ }
56
+ } catch { continue; }
57
+ }
58
+ return null;
59
+ }
60
+
61
+ /** 되돌이(순환) 위의 노드가 max_visits 1 이면 두 번째 방문에서 예산 초과로 멈춘다 — 시작 전에 말한다. */
62
+ function loopVisitIssues(doc: Record<string, unknown>): string[] {
63
+ const next = new Map<string, Set<string>>();
64
+ for (const edge of Array.isArray(doc.edges) ? doc.edges : []) {
65
+ if (!edge || typeof edge !== 'object') continue;
66
+ const { from, to, map } = edge as Record<string, unknown>;
67
+ if (typeof from !== 'string') continue;
68
+ const targets = [...(typeof to === 'string' ? [to] : []),
69
+ ...(map && typeof map === 'object' ? Object.values(map as Record<string, unknown>).filter((v): v is string => typeof v === 'string') : [])];
70
+ for (const target of targets) (next.get(from) ?? next.set(from, new Set()).get(from)!).add(target);
71
+ }
72
+ const reaches = (start: string, goal: string): boolean => {
73
+ const seen = new Set<string>();
74
+ const stack = [...(next.get(start) ?? [])];
75
+ while (stack.length) {
76
+ const id = stack.pop()!;
77
+ if (id === goal) return true;
78
+ if (seen.has(id)) continue;
79
+ seen.add(id);
80
+ stack.push(...(next.get(id) ?? []));
81
+ }
82
+ return false;
83
+ };
84
+ const issues: string[] = [];
85
+ for (const node of Array.isArray(doc.nodes) ? doc.nodes : []) {
86
+ if (!node || typeof node !== 'object') continue;
87
+ const { node_id: id, max_visits: visits } = node as Record<string, unknown>;
88
+ if (typeof id === 'string' && (typeof visits !== 'number' || visits < 2) && reaches(id, id)) {
89
+ issues.push(`노드 '${id}' 는 되돌이 위에 있는데 max_visits 가 ${typeof visits === 'number' ? visits : '없음'} 이다 — 두 번째 방문에서 멈춘다 · 2 이상으로`);
90
+ }
91
+ }
92
+ return issues;
93
+ }
94
+
95
+ /** 실행기가 받지 않을 모양을 시작 «전»에 사람 말로. 빈 배열이면 돌릴 수 있다. */
96
+ export function runnableIssues(doc: Record<string, unknown>, allowedRecipes: ReadonlySet<string>): string[] {
97
+ const issues: string[] = [];
98
+ for (const node of Array.isArray(doc.nodes) ? doc.nodes : []) {
99
+ if (!node || typeof node !== 'object') continue;
100
+ const { node_id: nodeId, recipe } = node as Record<string, unknown>;
101
+ if (typeof recipe !== 'string' || recipe === 'none') continue;
102
+ if (recipe.startsWith('approval:')) { issues.push(`노드 '${String(nodeId)}' 의 승인 레시피 '${recipe}' 는 편집기 데모 실행에서 쓸 수 없다`); continue; }
103
+ const prefixed = /^cmd:(.*)$/.exec(recipe);
104
+ if (prefixed && !allowedRecipes.has(prefixed[1]!)) {
105
+ issues.push(`노드 '${String(nodeId)}' 의 레시피 '${recipe}' 는 편집기 실행 목록에 없다 — 쓸 수 있는 것: ${[...allowedRecipes].map((r) => `cmd:${r}`).join(' · ')}`);
106
+ }
107
+ }
108
+ // grow 는 매핑 안 된 결과에서 LLM 제안기를 부른다 — 데모 실행 범위 밖(하니스 LLM 런은 0.2.21).
109
+ if (doc.grow !== undefined) issues.push('grow(그래프 자라기)는 편집기 데모 실행에서 쓸 수 없다 — grow 줄을 지우고 저장하라');
110
+ issues.push(...loopVisitIssues(doc));
111
+ const terminals = Array.isArray(doc.terminal_nodes) ? doc.terminal_nodes.filter((t): t is string => typeof t === 'string') : [];
112
+ if (!terminals.length) issues.push('끝 노드가 없다 — done 노드를 잇고 끝으로 지정하라');
113
+ for (const t of terminals) if (!RUNNABLE_TERMINALS.has(t)) issues.push(`끝 노드 '${t}' 는 실행할 수 없다 — 끝 노드 이름은 done 또는 failed 여야 한다`);
114
+ return issues;
115
+ }
116
+
117
+ export interface GraphRunNodeView { nodeId: string; ok: boolean; executed: boolean; startedAt?: string; endedAt?: string; error?: string }
118
+ export interface GraphRunView {
119
+ graphId: string; runId: string; status: GraphRunState['status'] | 'starting';
120
+ startedAt?: string; finishedAt?: string; path: string[];
121
+ nodes: GraphRunNodeView[]; currentNode?: { nodeId: string; startedAt: string }; error?: string;
122
+ }
123
+
124
+ export function graphRunView(state: GraphRunState): GraphRunView {
125
+ return {
126
+ graphId: state.graphId, runId: state.runId, status: state.status,
127
+ ...(state.startedAt ? { startedAt: state.startedAt } : {}), ...(state.finishedAt ? { finishedAt: state.finishedAt } : {}),
128
+ path: state.path,
129
+ nodes: state.nodes.map((node) => ({
130
+ nodeId: node.nodeId, ok: node.ok, executed: node.executed,
131
+ ...(node.startedAt ? { startedAt: node.startedAt } : {}), ...(node.endedAt ? { endedAt: node.endedAt } : {}),
132
+ ...(node.error ? { error: node.error.slice(0, 300) } : {}),
133
+ })),
134
+ ...(state.currentNode ? { currentNode: state.currentNode } : {}),
135
+ };
136
+ }
137
+
138
+ /** POST /v1/graphs/<id>/run — 내 그래프만 실행한다(핵심 그래프는 복제해서 돌린다). 응답은 기다리지 않는다(202 ⊕ runId). */
139
+ export function handleGraphRunStart(rawId: string, deps: GraphRunApiDeps = {}): Response {
140
+ const id = decodeId(rawId, GRAPH_ID);
141
+ if (!id) return jsonResponse({ error: 'bad_request', reason: 'invalid graph id' }, 400);
142
+ const mine = findMine(mineDirOf(deps), id);
143
+ if (!mine) return jsonResponse({ error: 'not-found', id, reason: '내 그래프에만 실행이 있다 — 핵심 그래프는 복제해서 돌린다' }, 404);
144
+ const recipesFile = deps.recipesFile ?? join(defaultGraphsDir(), 'demo', 'editor-recipes.yaml');
145
+ let recipeText: string;
146
+ let allowed: Set<string>;
147
+ try {
148
+ recipeText = readFileSync(recipesFile, 'utf8');
149
+ const parsed: unknown = parseYaml(recipeText);
150
+ allowed = new Set(parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? Object.keys(parsed) : []);
151
+ } catch {
152
+ return jsonResponse({ error: 'recipes-unavailable' }, 503);
153
+ }
154
+ const issues = runnableIssues(mine.doc, allowed);
155
+ if (issues.length) return jsonResponse({ error: 'not-runnable', id, issues }, 422);
156
+
157
+ const now = deps.now?.() ?? Date.now();
158
+ const runId = `ed-${now.toString(36)}-${randomBytes(3).toString('hex')}`;
159
+ const root = rootOf(deps);
160
+ const work = join(root, 'graph-run-work', id, runId);
161
+ try {
162
+ mkdirSync(work, { recursive: true });
163
+ copyFileSync(mine.file, join(work, 'graph.yaml'));
164
+ writeFileSync(join(work, 'recipes.yaml'), recipeText);
165
+ } catch (error) {
166
+ return jsonResponse({ error: 'run-workspace-failed', reason: error instanceof Error ? error.message.slice(0, 200) : 'unknown' }, 500);
167
+ }
168
+ debug.log('graphs.run', 'started', { id, runId });
169
+ void (deps.run ?? runGraph)(join(work, 'graph.yaml'), { runId, input: { source: 'editor' }, deps: { root } })
170
+ .then((state) => debug.log('graphs.run', 'finished', { id, runId, status: state.status, executed: state.executed }))
171
+ .catch((error: unknown) => {
172
+ const message = error instanceof Error ? error.message : String(error);
173
+ startFailures.set(runId, { error: message.slice(0, 300), at: new Date().toISOString() });
174
+ if (startFailures.size > 50) startFailures.delete(startFailures.keys().next().value!);
175
+ debug.log('graphs.run', 'failed', { id, runId, error: message.slice(0, 200) }, { level: 'warn' });
176
+ });
177
+ return jsonResponse({ id, runId, demo: true }, 202);
178
+ }
179
+
180
+ /** GET /v1/graphs/<id>/runs/<runId> — 원장 그대로(노드별 ok·시각 · 지금 노드). 원장이 아직 없으면 starting, 시작 전에 죽었으면 failed ⊕ 이유. */
181
+ export function handleGraphRunGet(rawId: string, rawRunId: string, deps: GraphRunApiDeps = {}): Response {
182
+ const id = decodeId(rawId, GRAPH_ID);
183
+ const runId = decodeId(rawRunId, RUN_ID);
184
+ if (!id || !runId) return jsonResponse({ error: 'bad_request', reason: 'invalid id' }, 400);
185
+ const file = join(rootOf(deps), 'graph-runs', id, `${runId}.json`);
186
+ let state: GraphRunState | null = null;
187
+ try { state = JSON.parse(readFileSync(file, 'utf8')) as GraphRunState; } catch { state = null; }
188
+ if (state && state.graphId === id && state.runId === runId) return jsonResponse(graphRunView(state));
189
+ const failed = startFailures.get(runId);
190
+ if (failed) return jsonResponse({ graphId: id, runId, status: 'failed', path: [], nodes: [], error: failed.error } satisfies GraphRunView);
191
+ if (!existsSync(join(rootOf(deps), 'graph-run-work', id, runId))) return jsonResponse({ error: 'not-found', id, runId }, 404);
192
+ return jsonResponse({ graphId: id, runId, status: 'starting', path: [], nodes: [] } satisfies GraphRunView);
193
+ }
194
+
195
+ const RUN_START = /^\/v1\/graphs\/([^/]+)\/run$/;
196
+ const RUN_GET = /^\/v1\/graphs\/([^/]+)\/runs\/([^/]+)$/;
197
+
198
+ /** http-server 가 인증 «뒤»에 부른다. 이 두 길이 아니면 null. */
199
+ export function handleGraphRunRoute(method: string, pathname: string, deps: GraphRunApiDeps = {}): Response | null {
200
+ const start = RUN_START.exec(pathname);
201
+ if (start && method === 'POST') return handleGraphRunStart(start[1]!, deps);
202
+ const get = RUN_GET.exec(pathname);
203
+ if (get && method === 'GET') return handleGraphRunGet(get[1]!, get[2]!, deps);
204
+ return null;
205
+ }
@@ -66,6 +66,12 @@ export interface GraphRunOptions {
66
66
  resumeGraphId?: string;
67
67
  fromNodeId?: string;
68
68
  useCurrentGraph?: boolean;
69
+ /**
70
+ * RELEASE-LEDGER-UNIVERSE — stamp every cmd: child with this run's ledger root (`ELANOUS_STATE_DIR`).
71
+ * Without it a child launched from a non-leader tree (`bun scripts/...` with cwd = that tree) re-resolves
72
+ * its own universe and lands in the tree-derived test universe while the run ledger is prod.
73
+ */
74
+ pinChildUniverse?: boolean;
69
75
  deps?: { root?: string; runBash?: BashRun; log?: (event: string, data: Record<string, unknown>) => void; processStartMs?: (pid: number) => number | null; growthProposer?: GrowthProposer; growthLLM?: (prompt: string) => Promise<string>; classifyGrowthRecipe?: (node: GraphNodeSpec, resolved: { command?: string; approval?: string }) => GrowthRecipeEffect; growthDecision?: GrowthDecisionDeps & { list?: (filters: { status: 'all' }) => DecisionEntry[] } };
70
76
  }
71
77
 
@@ -714,7 +720,11 @@ export async function runGraph(path: string, options: GraphRunOptions = {}): Pro
714
720
  // state stamp so a cmd: child resolves its default config in the same universe.
715
721
  // The ignored config-dir environment variable must not appear to propagate the flag.
716
722
  delete env.ELANOUS_CONFIG_DIR;
717
- if (getElanousConfigDirOverride()) {
723
+ if (options.pinChildUniverse) {
724
+ // The run's own ledger root is the parent universe — the child writes where the run is recorded.
725
+ env.ELANOUS_STATE_DIR = root;
726
+ debug.log('graph-runner', 'child-universe-pinned', { graphId, runId, node: current, configDir: getElanousConfigDirOverride() ?? null, stateDir: env.ELANOUS_STATE_DIR, why: 'pinChildUniverse' });
727
+ } else if (getElanousConfigDirOverride()) {
718
728
  env.ELANOUS_STATE_DIR = effectiveInstanceRoot();
719
729
  debug.log('graph-runner', 'child-universe-pinned', { graphId, runId, node: current, configDir: getElanousConfigDirOverride(), stateDir: env.ELANOUS_STATE_DIR });
720
730
  }