elanous 0.2.23 → 0.2.24

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 (347) hide show
  1. package/apps/pwa/out/404/index.html +1 -1
  2. package/apps/pwa/out/404.html +1 -1
  3. package/apps/pwa/out/_next/static/chunks/{272-b23d2e92161d11fd.js → 272-0eed0fb0aab716b6.js} +1 -1
  4. package/apps/pwa/out/_next/static/chunks/{3961-fbef064c213b2b18.js → 3961-aaae04ed1d32622f.js} +1 -1
  5. package/apps/pwa/out/_next/static/chunks/app/approvals/{page-d5341ac96203a983.js → page-3b9fc39362473ae7.js} +1 -1
  6. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-f2b62c19ca68fef8.js +1 -0
  7. package/apps/pwa/out/_next/static/chunks/app/board/{page-55c631c032dcc47e.js → page-d7602d489edf5f37.js} +1 -1
  8. package/apps/pwa/out/_next/static/chunks/app/botlab/{page-ad3bd8084048bee4.js → page-24282bf9f90031a5.js} +1 -1
  9. package/apps/pwa/out/_next/static/chunks/app/bots/page-8b7c5ab6b5bc8d64.js +1 -0
  10. package/apps/pwa/out/_next/static/chunks/app/ceo/{page-5c5d0e370f495ef6.js → page-4eb84d454f8c3415.js} +1 -1
  11. package/apps/pwa/out/_next/static/chunks/app/chat/{page-c325e21543cd48a8.js → page-b2e494bdad57fcb5.js} +1 -1
  12. package/apps/pwa/out/_next/static/chunks/app/consult/{page-6168a7c43e6ef8f5.js → page-c0ab229673a8b0eb.js} +1 -1
  13. package/apps/pwa/out/_next/static/chunks/app/control/page-3e2b45f3c36dbac4.js +1 -0
  14. package/apps/pwa/out/_next/static/chunks/app/decisions/page-c22aa511b28743ad.js +1 -0
  15. package/apps/pwa/out/_next/static/chunks/app/design-check/page-26345242e4049f55.js +1 -0
  16. package/apps/pwa/out/_next/static/chunks/app/editor/{page-25bb425afdb27541.js → page-bf88c0f558d8ef54.js} +1 -1
  17. package/apps/pwa/out/_next/static/chunks/app/exec/{page-32d72fdef1afe11a.js → page-52fec523e5b5d3ad.js} +1 -1
  18. package/apps/pwa/out/_next/static/chunks/app/field/page-67723dfdcde92b31.js +1 -0
  19. package/apps/pwa/out/_next/static/chunks/app/inside/{page-2bd9ce9c020d02d4.js → page-22725e2570f1126b.js} +1 -1
  20. package/apps/pwa/out/_next/static/chunks/app/intake/{page-a34096245e63b9f2.js → page-1891335950d68b70.js} +1 -1
  21. package/apps/pwa/out/_next/static/chunks/app/layout-3267af1ebdc73cf5.js +1 -0
  22. package/apps/pwa/out/_next/static/chunks/app/live/{page-4cc66d22ca4ec502.js → page-c314b75c11234998.js} +1 -1
  23. package/apps/pwa/out/_next/static/chunks/app/live-run/{page-83b14c729fcea837.js → page-9f9c85415f61782b.js} +1 -1
  24. package/apps/pwa/out/_next/static/chunks/app/loops/page-c1d4276059085587.js +1 -0
  25. package/apps/pwa/out/_next/static/chunks/app/market/page-833aab587f26ad4c.js +1 -0
  26. package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-ccdb83ed40c8d724.js → page-8526c17ae38e6a50.js} +1 -1
  27. package/apps/pwa/out/_next/static/chunks/app/morning/{page-7ad3de8fa06fdea9.js → page-f998637a8402115c.js} +1 -1
  28. package/apps/pwa/out/_next/static/chunks/app/not-found-b7bb53828b17b335.js +1 -0
  29. package/apps/pwa/out/_next/static/chunks/app/observatory/page-1b1850de0257e46d.js +1 -0
  30. package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-786111f30b6f0969.js → page-6f23159fedb41f67.js} +1 -1
  31. package/apps/pwa/out/_next/static/chunks/app/ops/release/{page-48fb31810693c357.js → page-1d809a87139dd024.js} +1 -1
  32. package/apps/pwa/out/_next/static/chunks/app/ops/seats/{page-4e17cce5e2ed0fee.js → page-da9e570234d481fd.js} +1 -1
  33. package/apps/pwa/out/_next/static/chunks/app/outputs/{page-cb154acadb025414.js → page-b92caddb5c715f30.js} +1 -1
  34. package/apps/pwa/out/_next/static/chunks/app/page-8ce3a38e210b810a.js +1 -0
  35. package/apps/pwa/out/_next/static/chunks/app/reflection/{page-c2dd2b19c93f90ab.js → page-57ffeeb6019440fd.js} +1 -1
  36. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-abbb417bf514630b.js +1 -0
  37. package/apps/pwa/out/_next/static/chunks/app/sessions/{page-d3c007c8a0ddd7ee.js → page-2c2b3f29cec1efa4.js} +1 -1
  38. package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-7837ce2aacb97b04.js → page-9369279f72fc7b51.js} +1 -1
  39. package/apps/pwa/out/_next/static/chunks/app/settings/page-64861f14039499c3.js +1 -0
  40. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-ddc4c93efd8ab94b.js +1 -0
  41. package/apps/pwa/out/_next/static/chunks/app/setup/page-901a1391b81053bc.js +1 -0
  42. package/apps/pwa/out/_next/static/chunks/app/share/page-68d9bbaac9841a58.js +1 -0
  43. package/apps/pwa/out/_next/static/chunks/app/showroom/{page-119be9a0444d2482.js → page-b5c0c996f329371e.js} +1 -1
  44. package/apps/pwa/out/_next/static/chunks/app/tasks/{page-fa806c018833e332.js → page-89881ccbcf728978.js} +1 -1
  45. package/apps/pwa/out/_next/static/chunks/app/term/page-0e87a803b3d7165e.js +1 -0
  46. package/apps/pwa/out/_next/static/chunks/app/today/{page-d431fbdea6ad93dd.js → page-58d4e5cb30ef4871.js} +1 -1
  47. package/apps/pwa/out/_next/static/chunks/app/trace/page-02ce4c3b5f39e4d2.js +1 -0
  48. package/apps/pwa/out/_next/static/chunks/app/vault/page-e620d1bb8eadcec2.js +1 -0
  49. package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/{page-847c3de6ca8a4416.js → page-acf868c64d77f950.js} +1 -1
  50. package/apps/pwa/out/_next/static/chunks/app/workflows/page-54fa605fa6dba457.js +1 -0
  51. package/apps/pwa/out/_next/static/chunks/app/workspace/{page-74e69ba2c8e970d4.js → page-bc79f0833533fa84.js} +1 -1
  52. package/apps/pwa/out/_next/static/chunks/app/worktrees/{page-70038f41ae7cf931.js → page-d5c4398eea03d8f3.js} +1 -1
  53. package/apps/pwa/out/_next/static/css/72becfd42807b50d.css +1 -0
  54. package/apps/pwa/out/approvals/index.html +1 -1
  55. package/apps/pwa/out/approvals/index.txt +14 -14
  56. package/apps/pwa/out/autopilot/index.html +1 -1
  57. package/apps/pwa/out/autopilot/index.txt +14 -14
  58. package/apps/pwa/out/board/index.html +1 -1
  59. package/apps/pwa/out/board/index.txt +14 -14
  60. package/apps/pwa/out/botlab/index.html +1 -1
  61. package/apps/pwa/out/botlab/index.txt +14 -14
  62. package/apps/pwa/out/bots/index.html +1 -1
  63. package/apps/pwa/out/bots/index.txt +14 -14
  64. package/apps/pwa/out/ceo/index.html +1 -1
  65. package/apps/pwa/out/ceo/index.txt +14 -14
  66. package/apps/pwa/out/chat/index.html +1 -1
  67. package/apps/pwa/out/chat/index.txt +14 -14
  68. package/apps/pwa/out/consult/index.html +1 -1
  69. package/apps/pwa/out/consult/index.txt +14 -14
  70. package/apps/pwa/out/control/index.html +1 -1
  71. package/apps/pwa/out/control/index.txt +14 -14
  72. package/apps/pwa/out/decisions/index.html +1 -1
  73. package/apps/pwa/out/decisions/index.txt +14 -14
  74. package/apps/pwa/out/design-check/index.html +1 -1
  75. package/apps/pwa/out/design-check/index.txt +14 -14
  76. package/apps/pwa/out/editor/index.html +1 -1
  77. package/apps/pwa/out/editor/index.txt +14 -14
  78. package/apps/pwa/out/exec/index.html +1 -1
  79. package/apps/pwa/out/exec/index.txt +14 -14
  80. package/apps/pwa/out/field/index.html +1 -1
  81. package/apps/pwa/out/field/index.txt +14 -14
  82. package/apps/pwa/out/healthz +1 -1
  83. package/apps/pwa/out/index.html +1 -1
  84. package/apps/pwa/out/index.txt +14 -14
  85. package/apps/pwa/out/inside/index.html +1 -1
  86. package/apps/pwa/out/inside/index.txt +14 -14
  87. package/apps/pwa/out/intake/index.html +1 -1
  88. package/apps/pwa/out/intake/index.txt +14 -14
  89. package/apps/pwa/out/live/index.html +1 -1
  90. package/apps/pwa/out/live/index.txt +14 -14
  91. package/apps/pwa/out/live-run/index.html +1 -1
  92. package/apps/pwa/out/live-run/index.txt +14 -14
  93. package/apps/pwa/out/loops/index.html +1 -1
  94. package/apps/pwa/out/loops/index.txt +14 -14
  95. package/apps/pwa/out/market/index.html +1 -1
  96. package/apps/pwa/out/market/index.txt +14 -14
  97. package/apps/pwa/out/missions/placeholder/index.html +1 -1
  98. package/apps/pwa/out/missions/placeholder/index.txt +14 -14
  99. package/apps/pwa/out/morning/index.html +1 -1
  100. package/apps/pwa/out/morning/index.txt +14 -14
  101. package/apps/pwa/out/observatory/index.html +1 -1
  102. package/apps/pwa/out/observatory/index.txt +14 -14
  103. package/apps/pwa/out/ops/checklist/index.html +1 -1
  104. package/apps/pwa/out/ops/checklist/index.txt +14 -14
  105. package/apps/pwa/out/ops/release/index.html +1 -1
  106. package/apps/pwa/out/ops/release/index.txt +14 -14
  107. package/apps/pwa/out/ops/seats/index.html +1 -1
  108. package/apps/pwa/out/ops/seats/index.txt +14 -14
  109. package/apps/pwa/out/outputs/index.html +1 -1
  110. package/apps/pwa/out/outputs/index.txt +14 -14
  111. package/apps/pwa/out/reflection/index.html +1 -1
  112. package/apps/pwa/out/reflection/index.txt +15 -15
  113. package/apps/pwa/out/scheduler/index.html +1 -1
  114. package/apps/pwa/out/scheduler/index.txt +14 -14
  115. package/apps/pwa/out/sessions/index.html +1 -1
  116. package/apps/pwa/out/sessions/index.txt +14 -14
  117. package/apps/pwa/out/settings/devices/index.html +1 -1
  118. package/apps/pwa/out/settings/devices/index.txt +14 -14
  119. package/apps/pwa/out/settings/index.html +1 -1
  120. package/apps/pwa/out/settings/index.txt +14 -14
  121. package/apps/pwa/out/setup/done/index.html +1 -1
  122. package/apps/pwa/out/setup/done/index.txt +14 -14
  123. package/apps/pwa/out/setup/index.html +1 -1
  124. package/apps/pwa/out/setup/index.txt +14 -14
  125. package/apps/pwa/out/share/index.html +1 -1
  126. package/apps/pwa/out/share/index.txt +14 -14
  127. package/apps/pwa/out/showroom/index.html +1 -1
  128. package/apps/pwa/out/showroom/index.txt +14 -14
  129. package/apps/pwa/out/tasks/index.html +1 -1
  130. package/apps/pwa/out/tasks/index.txt +14 -14
  131. package/apps/pwa/out/term/index.html +1 -1
  132. package/apps/pwa/out/term/index.txt +14 -14
  133. package/apps/pwa/out/today/index.html +1 -1
  134. package/apps/pwa/out/today/index.txt +14 -14
  135. package/apps/pwa/out/trace/index.html +1 -1
  136. package/apps/pwa/out/trace/index.txt +14 -14
  137. package/apps/pwa/out/vault/index.html +1 -1
  138. package/apps/pwa/out/vault/index.txt +14 -14
  139. package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
  140. package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
  141. package/apps/pwa/out/workflows/index.html +1 -1
  142. package/apps/pwa/out/workflows/index.txt +14 -14
  143. package/apps/pwa/out/workspace/index.html +1 -1
  144. package/apps/pwa/out/workspace/index.txt +14 -14
  145. package/apps/pwa/out/worktrees/index.html +1 -1
  146. package/apps/pwa/out/worktrees/index.txt +14 -14
  147. package/catalog/models/openrouter/deepseek-v4-pro-0813.yaml +39 -0
  148. package/catalog/models/openrouter/deepseek-v4.1-flash.yaml +38 -0
  149. package/catalog/models/openrouter/glm-5.3-flash.yaml +35 -0
  150. package/catalog/providers/openrouter.yaml +2 -0
  151. package/graphs/implement-loop.yaml +31 -0
  152. package/graphs/ops/harness-gc.yaml +17 -0
  153. package/graphs/ops/recipes.yaml +3 -0
  154. package/graphs/overlays/README.md +1 -1
  155. package/graphs/overlays/stop-heal-ladder.yaml +105 -0
  156. package/package.json +1 -1
  157. package/scripts/add-api-key.sh +1 -1
  158. package/scripts/ci-typecheck-changed.ts +71 -14
  159. package/scripts/fleet/bedrock-pod-secret.ts +122 -0
  160. package/scripts/fleet/node-setup-linux.sh +97 -0
  161. package/scripts/fleet/node-setup.sh +11 -0
  162. package/scripts/fleet/zai-pod-secret.ts +86 -0
  163. package/scripts/land-global-checks.ts +3 -0
  164. package/scripts/measure-log-diet-2.ts +213 -0
  165. package/scripts/measure-tsc-affected-roots.ts +289 -0
  166. package/scripts/release-loop/gate-executor.ts +28 -0
  167. package/scripts/release-loop/gate-node.ts +262 -26
  168. package/scripts/release-loop/resume-release.ts +9 -3
  169. package/scripts/release-loop/resume.ts +41 -0
  170. package/scripts/release-loop/unattended-release.ts +22 -7
  171. package/scripts/tsc-baseline-cache.ts +147 -0
  172. package/scripts/usage-rollup.ts +58 -6
  173. package/src/agent/run-context.ts +10 -0
  174. package/src/agent-mission/driver.ts +18 -3
  175. package/src/agent-mission/resource-ladder.ts +8 -3
  176. package/src/agent-mission/review-loop.ts +8 -2
  177. package/src/agent-substrate/acp-implementer.ts +139 -0
  178. package/src/agent-substrate/pr-reviewer.ts +31 -4
  179. package/src/agent-substrate/self-review-cli.ts +48 -9
  180. package/src/autopilot/mission-critique.ts +18 -5
  181. package/src/autopilot/mission-feasibility-negotiate.ts +11 -10
  182. package/src/autopilot/mission-trace.ts +12 -1
  183. package/src/autopilot/system-lookback-run.ts +16 -10
  184. package/src/bench/bench.ts +183 -0
  185. package/src/budget/fetchers/grok.ts +15 -5
  186. package/src/budget/llm-cost.ts +37 -5
  187. package/src/budget/types.ts +8 -2
  188. package/src/budget/unified-usage.ts +88 -6
  189. package/src/cli/bench-cli.ts +34 -0
  190. package/src/cli/guardian-cli.ts +26 -1
  191. package/src/cli/image-cli.ts +101 -0
  192. package/src/cli/log-event-names.ts +4 -0
  193. package/src/cli/pr-cli.ts +62 -5
  194. package/src/cli/pr-land-overlap-evidence.ts +160 -12
  195. package/src/cli/release-cli.ts +18 -9
  196. package/src/cli/rfc-status.ts +52 -2
  197. package/src/cli/self-update.ts +104 -0
  198. package/src/cli/tasks-cli.ts +109 -4
  199. package/src/cli/usage-cli.ts +91 -8
  200. package/src/core-turn/run-goal-loop.ts +5 -4
  201. package/src/dashboard/ad-run-setup-bindings.ts +279 -0
  202. package/src/dashboard/index.ts +18 -276
  203. package/src/dashboard/stale-install-check.ts +11 -0
  204. package/src/debug/log-diet.ts +38 -7
  205. package/src/debug/log.ts +19 -1
  206. package/src/domains/release-tool.ts +39 -0
  207. package/src/error-report/report.ts +21 -4
  208. package/src/github/pr-labels.ts +1 -0
  209. package/src/graph-runner/graph-cli.ts +42 -1
  210. package/src/graph-runner/runner.ts +167 -6
  211. package/src/grok/usage.ts +23 -2
  212. package/src/grounding/ref-index.ts +253 -0
  213. package/src/harness/dispatch-backpressure.ts +10 -2
  214. package/src/harness/harness-clean.ts +199 -78
  215. package/src/harness/harness-cli-command.ts +122 -7
  216. package/src/harness/harness-gc.ts +331 -0
  217. package/src/harness/harness-stop.ts +4 -2
  218. package/src/harness/harness-substrate-default.ts +142 -7
  219. package/src/harness/launch-registry.ts +6 -0
  220. package/src/harness/worktree-branch-prefix.ts +9 -0
  221. package/src/hq/fence.ts +187 -0
  222. package/src/hq/seats.ts +38 -22
  223. package/src/images/image-ledger.ts +179 -0
  224. package/src/index.ts +93 -11
  225. package/src/instance/child-scope.ts +32 -1
  226. package/src/instance/dev-project.ts +133 -0
  227. package/src/instance/resolve.ts +24 -0
  228. package/src/instance/spawn-elanous.ts +45 -0
  229. package/src/intake-plane/ref-prefetch.ts +110 -0
  230. package/src/intelligence-map/model-catalog.ts +72 -0
  231. package/src/llm/bedrock-eventstream.ts +215 -0
  232. package/src/llm/bedrock-provider.ts +114 -41
  233. package/src/llm/bedrock.ts +174 -28
  234. package/src/llm/openrouter-byok-cost.ts +96 -0
  235. package/src/llm/openrouter-deepseek.ts +151 -0
  236. package/src/llm/openrouter-upstream-pins.config.ts +25 -0
  237. package/src/llm/zai-coding-plan.ts +700 -0
  238. package/src/llm.ts +87 -16
  239. package/src/loops/checker.ts +148 -9
  240. package/src/loops/orchestrator/resync-shadow.ts +34 -7
  241. package/src/maturity/feature-maturity.ts +2 -2
  242. package/src/model-tier/llm-tier-map.ts +9 -5
  243. package/src/models/prompts.ts +2 -1
  244. package/src/mss/logging/log-store.ts +58 -11
  245. package/src/nexus/api/missions.ts +73 -2
  246. package/src/nexus/api/ops-api.ts +18 -0
  247. package/src/nexus/index.ts +7 -75
  248. package/src/nexus/tools-kind.ts +78 -0
  249. package/src/oauth/codex-account-rotation.ts +39 -2
  250. package/src/oauth/codex-account-store.ts +323 -24
  251. package/src/oauth/fallback-chain.ts +53 -15
  252. package/src/onboarding/first-run.ts +37 -18
  253. package/src/onboarding/web-first.ts +20 -2
  254. package/src/ops/machine-heartbeat.ts +185 -0
  255. package/src/ops/overview-cache.ts +175 -0
  256. package/src/ops/overview.ts +8 -0
  257. package/src/prompt-cache/openai.ts +3 -0
  258. package/src/prompt-cache/types.ts +5 -0
  259. package/src/prompt-enhance/enhance.ts +23 -0
  260. package/src/release-loop/placement.ts +16 -1
  261. package/src/requirements/requirement-ledger.ts +536 -0
  262. package/src/retro/daily.ts +5 -5
  263. package/src/seat-loop/seat-loop.ts +32 -10
  264. package/src/self-dev/dev-cli.ts +44 -10
  265. package/src/self-dev/dev-pipeline.ts +7 -2
  266. package/src/self-dev/draft-residue.ts +14 -2
  267. package/src/self-dev/draft-sweep.ts +103 -0
  268. package/src/self-dev/graph-journey-route.ts +6 -0
  269. package/src/self-dev/orchestrate-cli.ts +25 -3
  270. package/src/self-dev/orchestrate.ts +41 -6
  271. package/src/self-dev/run-supervisor.ts +188 -15
  272. package/src/self-dev/run-types.ts +13 -0
  273. package/src/self-dev/supervisor-verdict-edges.ts +1 -0
  274. package/src/self-implement/budget-gate.ts +53 -13
  275. package/src/self-implement/child-context-composition.ts +139 -0
  276. package/src/self-implement/child-llm-preference.ts +13 -5
  277. package/src/self-implement/claude-acp-implement.ts +67 -0
  278. package/src/self-implement/flex-signals.ts +59 -0
  279. package/src/self-implement/frozen-merges.ts +8 -3
  280. package/src/self-implement/goal-author-decompose.ts +10 -2
  281. package/src/self-implement/goal-author.ts +24 -9
  282. package/src/self-implement/graph-overlay-condition.ts +28 -0
  283. package/src/self-implement/graph-overlay-yaml.ts +41 -7
  284. package/src/self-implement/graph-templates.ts +33 -1
  285. package/src/self-implement/graph-yaml.ts +56 -0
  286. package/src/self-implement/heal-intake.ts +5 -1
  287. package/src/self-implement/orchestrator.ts +144 -15
  288. package/src/self-implement/running-runs.ts +75 -5
  289. package/src/self-implement/seams.ts +47 -1
  290. package/src/self-implement/self-implement-cli.ts +9 -4
  291. package/src/self-implement/stop-autoheal.ts +69 -2
  292. package/src/session-runtime/retry-policy.ts +2 -1
  293. package/src/steward/launch.ts +5 -4
  294. package/src/steward/triage-budget.ts +62 -0
  295. package/src/steward/triage.ts +83 -22
  296. package/src/task-agent/actions.ts +62 -5
  297. package/src/task-agent/completion-evidence.ts +5 -0
  298. package/src/task-agent/cover.ts +101 -3
  299. package/src/task-agent/judge.ts +16 -0
  300. package/src/task-agent/land-order-ledger.ts +95 -0
  301. package/src/task-agent/live-move-history.ts +87 -1
  302. package/src/task-agent/live-moves.ts +350 -20
  303. package/src/task-agent/rejudge-on-head.ts +75 -20
  304. package/src/task-agent/review-prior-fix.ts +146 -0
  305. package/src/task-agent/run-adopt.ts +136 -0
  306. package/src/task-agent/shadow.ts +53 -9
  307. package/src/task-agent/task-hand.ts +54 -7
  308. package/src/task-orchestrator/surfaces/pod-account-broker.ts +38 -5
  309. package/src/task-orchestrator/surfaces/pod-bedrock-child.ts +74 -0
  310. package/src/task-orchestrator/surfaces/pod-child-default.ts +50 -0
  311. package/src/task-orchestrator/surfaces/pod-child-providers.ts +14 -1
  312. package/src/task-orchestrator/surfaces/pod-failure-reason.ts +79 -6
  313. package/src/task-orchestrator/surfaces/pod-lease.ts +17 -5
  314. package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +11 -5
  315. package/src/task-orchestrator/surfaces/pod-pool.ts +20 -4
  316. package/src/task-orchestrator/surfaces/pod-zai-child.ts +101 -0
  317. package/src/task-orchestrator/surfaces/self-implement-pod.ts +213 -23
  318. package/src/task-orchestrator/surfaces/self-implement.ts +19 -1
  319. package/src/typecheck-ratchet.ts +1 -0
  320. package/src/user-config.ts +51 -11
  321. package/src/version/packed-revision.json +1 -1
  322. package/src/workflow-runtime/triggers/registry.ts +59 -0
  323. package/src/workflow-runtime/triggers/webhook-source.ts +35 -5
  324. package/apps/pwa/out/_next/static/chunks/app/autopilot/page-0160ab83c8685a12.js +0 -1
  325. package/apps/pwa/out/_next/static/chunks/app/bots/page-e6784759a2531031.js +0 -1
  326. package/apps/pwa/out/_next/static/chunks/app/control/page-31417d3604154a6d.js +0 -1
  327. package/apps/pwa/out/_next/static/chunks/app/decisions/page-e9606dc0bdec9305.js +0 -1
  328. package/apps/pwa/out/_next/static/chunks/app/design-check/page-a648bc464c9dc754.js +0 -1
  329. package/apps/pwa/out/_next/static/chunks/app/field/page-d27b794c8ba76bbb.js +0 -1
  330. package/apps/pwa/out/_next/static/chunks/app/layout-22170f4f00a15496.js +0 -1
  331. package/apps/pwa/out/_next/static/chunks/app/loops/page-6e2781c523dad761.js +0 -1
  332. package/apps/pwa/out/_next/static/chunks/app/market/page-26dd415c8f13f341.js +0 -1
  333. package/apps/pwa/out/_next/static/chunks/app/not-found-015c02c4fec4023e.js +0 -1
  334. package/apps/pwa/out/_next/static/chunks/app/observatory/page-6b0ae5b5c42f6d2f.js +0 -1
  335. package/apps/pwa/out/_next/static/chunks/app/page-16258e956ea93fc2.js +0 -1
  336. package/apps/pwa/out/_next/static/chunks/app/scheduler/page-85f35dd1dd96ae87.js +0 -1
  337. package/apps/pwa/out/_next/static/chunks/app/settings/page-7350ee93f4fe1033.js +0 -1
  338. package/apps/pwa/out/_next/static/chunks/app/setup/done/page-00723458575f2b19.js +0 -1
  339. package/apps/pwa/out/_next/static/chunks/app/setup/page-745d9b6c015076f8.js +0 -1
  340. package/apps/pwa/out/_next/static/chunks/app/share/page-736d33c7414a8a1a.js +0 -1
  341. package/apps/pwa/out/_next/static/chunks/app/term/page-c782041b5f5199ba.js +0 -1
  342. package/apps/pwa/out/_next/static/chunks/app/trace/page-0c056abff82b9891.js +0 -1
  343. package/apps/pwa/out/_next/static/chunks/app/vault/page-242d668e4fbf72c4.js +0 -1
  344. package/apps/pwa/out/_next/static/chunks/app/workflows/page-81dd0d8ae923ed72.js +0 -1
  345. package/apps/pwa/out/_next/static/css/5619d59e87eddf66.css +0 -1
  346. /package/apps/pwa/out/_next/static/{rfkMm4movqkqMebBrCXiR → Q-7iofoxOEv1WX18S35of}/_buildManifest.js +0 -0
  347. /package/apps/pwa/out/_next/static/{rfkMm4movqkqMebBrCXiR → Q-7iofoxOEv1WX18S35of}/_ssgManifest.js +0 -0
@@ -7,14 +7,21 @@
7
7
  * - 무엇을: `--overlap-evidence <file>` 의 JSON 을 «믿지 않고» 잰다.
8
8
  * ① 재기반 — 원격 base 의 «지금» 끝(`git ls-remote`)이 고정 머리의 조상인가(`git merge-base --is-ancestor`).
9
9
  * ② 재게이트 — 증거의 게이트 기록이 «그 머리»를 «그 base 끝» 위에서 통과했다고 말하는가(머리·base 끝이 ① 과 맞물린다).
10
+ * ②' 재게이트 뒤 base 가 앞섰다(OVERLAP-EVIDENCE-BASE-WINDOW) — `gate.baseCommit ≠ 지금 끝`이면 «창»을 잰다:
11
+ * ⓐ `gate.baseCommit` 이 고정 머리의 조상 ⊕ 지금 끝의 조상이고 ⓑ 창 `gate.baseCommit..지금 끝`의 커밋이 «하나도»
12
+ * PR 의 바뀐 파일(머리 vs merge-base)을 안 건드리면 통과(관측 reason `base-advanced-disjoint`).
13
+ * 겹치면 `base-stale-overlap`(REJUDGE 가 재게이트를 청한다) · 못 재면 `base-window-unmeasured` · ⓐ 가 아니면 종전 사유.
10
14
  * ③ 리뷰 — 증거가 가리키는 `review-results/<card>-<pr>-<head12>.json`(TA 가 받는 `self review --json` stdout)이
11
- * 같은 PR·같은 머리에 대해 `reviewed:true` · `verdict:pass` · must-fix 0 인가.
15
+ * 같은 PR·같은 머리에 대해 `reviewed:true` · `verdict ∈ {pass, warn}` · must-fix 가 «잰 수» 0 인가
16
+ * (자동 병합 canAuto · TA-LAND-WARN-MUSTFIX0 과 같은 규칙 — must-fix 를 못 셌으면 거부).
12
17
  * ⓐ 이미 조상(TC #26019 실측) — 겹친 최근 착지 커밋이 «전부» 고정 머리의 조상이면 그 겹침은 이미 그 머리의 게이트·리뷰에
13
18
  * 반영됐다 ⇒ ①② 없이 ③ 만으로 통과(관측 reason `already-ancestor`). 하나라도 조상이 아니면 ①②③ 전부.
19
+ * ⓐ' 증거 파일이 없을 때(`verifyAncestorAutoPass`) — 같은 ⓐ 를 표준 자리 `review-results/*-<pr>-<head12>.json` 만으로 잰다
20
+ * (관측 reason `already-ancestor-auto`). 조상이 아닌 착지가 하나라도 있거나 리뷰 결과가 없으면 거부.
14
21
  * 하나라도 못 재거나 어긋나면 거부 — 호출부는 종전 거부를 그대로 낸다(fail-closed).
15
22
  * - 관측: `pr.land` · `overlap-evidence` (decision · reason · 잰 값).
16
23
  */
17
- import { basename, dirname } from 'node:path';
24
+ import { basename, dirname, join } from 'node:path';
18
25
  import { debug } from '../debug/log.js';
19
26
  import type { CmdRunner } from '../autopilot/pr-manager.js';
20
27
  import { parseReviewResultOutput } from '../task-agent/review-result-capture.js';
@@ -33,6 +40,7 @@ interface OverlapEvidenceFile {
33
40
  export type OverlapEvidenceVerdict =
34
41
  | { ok: true; mode: 'already-ancestor'; reviewResult: string }
35
42
  | { ok: true; mode: 'rebased'; baseTip: string; reviewResult: string }
43
+ | { ok: true; mode: 'base-advanced-disjoint'; baseTip: string; gateBase: string; windowCommits: number; reviewResult: string }
36
44
  | { ok: false; reason: string };
37
45
 
38
46
  export interface VerifyOverlapEvidenceInput {
@@ -48,9 +56,14 @@ export interface VerifyOverlapEvidenceInput {
48
56
  overlappingLandings?: readonly string[];
49
57
  }
50
58
 
51
- type Checked = { ok: false; reason: string; baseTip?: string } | (OverlapEvidenceVerdict & { ok: true; baseTip?: string });
59
+ type Checked = { ok: false; reason: string; baseTip?: string; gateBase?: string; windowCommits?: number }
60
+ | (OverlapEvidenceVerdict & { ok: true; baseTip?: string });
52
61
 
53
- function checkReview(input: VerifyOverlapEvidenceInput, e: Partial<OverlapEvidenceFile>, head: string, baseTip?: string): { ok: false; reason: string; baseTip?: string } | { ok: true; reviewResult: string } {
62
+ /** 리뷰가 받는 판정 — 자동 병합(canAuto)·TA 착지(TA-LAND-WARN-MUSTFIX0)와 같은 집합. */
63
+ const ACCEPTED_REVIEW_VERDICTS: ReadonlySet<string> = new Set(['pass', 'warn']);
64
+ const COMMIT_MARK = '\u0001';
65
+
66
+ function checkReview(input: Pick<VerifyOverlapEvidenceInput, 'pr' | 'readFile'>, e: Partial<OverlapEvidenceFile>, head: string, baseTip?: string): { ok: false; reason: string; baseTip?: string } | { ok: true; reviewResult: string } {
54
67
  if (typeof e.reviewResult !== 'string' || !e.reviewResult) return { ok: false, reason: 'review-missing', baseTip };
55
68
  // TA 가 받는 자리(`<state>/review-results/<card>-<pr>-<head12>.json` · reviewResultPath)만 — 아무 디렉터리의 같은 이름은 안 받는다.
56
69
  if (basename(dirname(e.reviewResult)) !== 'review-results'
@@ -65,11 +78,67 @@ function checkReview(input: VerifyOverlapEvidenceInput, e: Partial<OverlapEviden
65
78
  if (!review.head || review.head.toLowerCase() !== head) return { ok: false, reason: 'review-head-mismatch', baseTip };
66
79
  if (review.error) return { ok: false, reason: 'review-error', baseTip };
67
80
  if (review.reviewed !== true) return { ok: false, reason: 'review-not-reviewed', baseTip };
68
- if (review.verdict !== 'pass') return { ok: false, reason: `review-verdict-${review.verdict ?? 'null'}`, baseTip };
81
+ if (review.verdict === null || !ACCEPTED_REVIEW_VERDICTS.has(review.verdict)) return { ok: false, reason: `review-verdict-${review.verdict ?? 'null'}`, baseTip };
69
82
  if (review.mustFix !== 0) return { ok: false, reason: review.mustFix === null ? 'review-must-fix-unknown' : `review-must-fix-${review.mustFix}`, baseTip };
70
83
  return { ok: true, reviewResult: e.reviewResult };
71
84
  }
72
85
 
86
+ type WindowResult = { ok: true; windowCommits: number } | { ok: false; reason: string; windowCommits?: number };
87
+
88
+ /**
89
+ * ②' — 재게이트 뒤 앞선 base 의 창 `gateBase..baseTip` 이 PR 의 바뀐 파일과 «서로소»인가. 읽기 전용 git 만 쓴다
90
+ * (끝 커밋이 로컬에 없을 때만 ref 를 안 쓰는 fetch). `staleReason` 은 ⓐ(조상 아님)일 때 그대로 내는 종전 사유다.
91
+ */
92
+ function checkBaseWindow(input: VerifyOverlapEvidenceInput, head: string, baseTip: string, gateBase: string, staleReason: string): WindowResult {
93
+ const git = (args: string[]) => input.run('git', args, { cwd: input.cwd });
94
+ const short = (sha: string) => sha.slice(0, 12);
95
+ if (!git(['merge-base', '--is-ancestor', gateBase, head]).ok) {
96
+ return { ok: false, reason: `${staleReason} (gate base is not an ancestor of head)` };
97
+ }
98
+ if (!git(['cat-file', '-e', `${baseTip}^{commit}`]).ok) {
99
+ // ref 를 안 바꾸는 fetch — FETCH_HEAD 도 원격 추적 ref 도 안 쓴다.
100
+ git(['fetch', '--quiet', '--no-write-fetch-head', '--refmap=', input.remote, `refs/heads/${input.branch}`]);
101
+ if (!git(['cat-file', '-e', `${baseTip}^{commit}`]).ok) {
102
+ return { ok: false, reason: `base-window-unmeasured: ${input.remote}/${input.branch} ${short(baseTip)} not available locally · ${staleReason}` };
103
+ }
104
+ }
105
+ if (!git(['merge-base', '--is-ancestor', gateBase, baseTip]).ok) {
106
+ return { ok: false, reason: `${staleReason} (gate base is not an ancestor of ${input.remote}/${input.branch})` };
107
+ }
108
+ const mergeBase = git(['merge-base', baseTip, head]);
109
+ const forkPoint = mergeBase.ok ? mergeBase.out.trim() : '';
110
+ if (!SHA40.test(forkPoint)) return { ok: false, reason: `base-window-unmeasured: merge-base ${short(baseTip)} ${short(head)} failed · ${staleReason}` };
111
+ const diff = git(['diff', '--name-only', '--no-renames', forkPoint, head]);
112
+ if (!diff.ok) return { ok: false, reason: `base-window-unmeasured: diff --name-only ${short(forkPoint)} ${short(head)} failed · ${staleReason}` };
113
+ const prFiles = new Set(diff.out.split('\n').map((line) => line.trim()).filter(Boolean));
114
+ if (prFiles.size === 0) return { ok: false, reason: `base-window-unmeasured: PR changed files are empty · ${staleReason}` };
115
+ // 병합 커밋은 첫 부모 대비 diff(-m --first-parent) — base 에 «들어온» 변경만, 빠짐없이.
116
+ const log = git(['log', '--first-parent', '-m', '--no-renames', '--name-only', `--format=${COMMIT_MARK}%H`, `${gateBase}..${baseTip}`]);
117
+ if (!log.ok) return { ok: false, reason: `base-window-unmeasured: log ${short(gateBase)}..${short(baseTip)} failed · ${staleReason}` };
118
+ const touched = new Map<string, string[]>();
119
+ let current = '';
120
+ for (const raw of log.out.split('\n')) {
121
+ const line = raw.trim();
122
+ if (!line) continue;
123
+ if (line.startsWith(COMMIT_MARK)) { current = line.slice(COMMIT_MARK.length); if (!touched.has(current)) touched.set(current, []); continue; }
124
+ if (current && prFiles.has(line)) touched.get(current)!.push(line);
125
+ }
126
+ const windowCommits = touched.size;
127
+ if (windowCommits === 0 || [...touched.keys()].some((sha) => !SHA40.test(sha))) {
128
+ return { ok: false, reason: `base-window-unmeasured: window ${short(gateBase)}..${short(baseTip)} listed no commits · ${staleReason}` };
129
+ }
130
+ const overlapping = [...touched.entries()].filter(([, files]) => files.length > 0);
131
+ if (overlapping.length > 0) {
132
+ const files = [...new Set(overlapping.flatMap(([, f]) => f))];
133
+ return {
134
+ ok: false,
135
+ windowCommits,
136
+ reason: `base-stale-overlap: gate ${short(gateBase)} ≠ ${input.remote}/${input.branch} ${short(baseTip)} · ${overlapping.length}/${windowCommits} window commits touch PR files (${overlapping.map(([sha]) => short(sha)).slice(0, 5).join(', ')}): ${files.slice(0, 10).join(', ')}`,
137
+ };
138
+ }
139
+ return { ok: true, windowCommits };
140
+ }
141
+
73
142
  function check(input: VerifyOverlapEvidenceInput): Checked {
74
143
  const head = input.expectedHead.toLowerCase();
75
144
  let raw: unknown;
@@ -94,15 +163,32 @@ function check(input: VerifyOverlapEvidenceInput): Checked {
94
163
  const baseTip = remote.ok ? remote.out.trim().split(/\s+/)[0] ?? '' : '';
95
164
  if (!SHA40.test(baseTip)) return { ok: false, reason: `rebase-unmeasured: ls-remote ${input.remote} ${input.branch} failed` };
96
165
  const ancestor = input.run('git', ['merge-base', '--is-ancestor', baseTip, head], { cwd: input.cwd });
97
- if (!ancestor.ok) return { ok: false, reason: `not-rebased: ${input.remote}/${input.branch} ${baseTip.slice(0, 12)} is not an ancestor of ${head.slice(0, 12)}`, baseTip };
166
+ const gate = e.gate;
167
+ // ②' 의 입구 — 게이트 기록이 «그 머리»를 통과했고 base 만 지금 끝과 다르다.
168
+ const gateBase = gate && typeof gate === 'object' && gate.passed === true
169
+ && typeof gate.head === 'string' && gate.head.toLowerCase() === head
170
+ && typeof gate.baseCommit === 'string' && SHA40.test(gate.baseCommit) && gate.baseCommit.toLowerCase() !== baseTip.toLowerCase()
171
+ ? gate.baseCommit.toLowerCase() : undefined;
172
+ const advanced = (staleReason: string): Checked => {
173
+ const window = checkBaseWindow(input, head, baseTip, gateBase!, staleReason);
174
+ if (!window.ok) return { ok: false, reason: window.reason, baseTip, gateBase, windowCommits: window.windowCommits };
175
+ const review = checkReview(input, e, head, baseTip);
176
+ return review.ok
177
+ ? { ok: true, mode: 'base-advanced-disjoint', baseTip, gateBase: gateBase!, windowCommits: window.windowCommits, reviewResult: review.reviewResult }
178
+ : { ...review, gateBase, windowCommits: window.windowCommits };
179
+ };
180
+ if (!ancestor.ok) {
181
+ const notRebased = `not-rebased: ${input.remote}/${input.branch} ${baseTip.slice(0, 12)} is not an ancestor of ${head.slice(0, 12)}`;
182
+ return gateBase ? advanced(notRebased) : { ok: false, reason: notRebased, baseTip };
183
+ }
98
184
 
99
185
  // ② 재게이트 — 그 머리를, ① 의 그 base 끝 위에서.
100
- const gate = e.gate;
101
186
  if (!gate || typeof gate !== 'object') return { ok: false, reason: 'gate-missing', baseTip };
102
187
  if (gate.passed !== true) return { ok: false, reason: 'gate-not-passed', baseTip };
103
188
  if (typeof gate.head !== 'string' || gate.head.toLowerCase() !== head) return { ok: false, reason: 'gate-head-mismatch', baseTip };
104
189
  if (typeof gate.baseCommit !== 'string' || gate.baseCommit.toLowerCase() !== baseTip.toLowerCase()) {
105
- return { ok: false, reason: `gate-base-stale: gate ${String(gate.baseCommit).slice(0, 12)} ≠ ${input.remote}/${input.branch} ${baseTip.slice(0, 12)}`, baseTip };
190
+ const stale = `gate-base-stale: gate ${String(gate.baseCommit).slice(0, 12)} ≠ ${input.remote}/${input.branch} ${baseTip.slice(0, 12)}`;
191
+ return gateBase ? advanced(stale) : { ok: false, reason: stale, baseTip };
106
192
  }
107
193
 
108
194
  // ③ 리뷰 — 그 PR · 그 머리의 pass ⊕ must-fix 0.
@@ -110,22 +196,84 @@ function check(input: VerifyOverlapEvidenceInput): Checked {
110
196
  return review.ok ? { ok: true, mode: 'rebased', baseTip, reviewResult: review.reviewResult } : review;
111
197
  }
112
198
 
199
+ export type AncestorAutoVerdict =
200
+ | { ok: true; mode: 'already-ancestor-auto'; reviewResult: string }
201
+ | { ok: false; reason: string };
202
+
203
+ export interface VerifyAncestorAutoInput {
204
+ pr: number;
205
+ expectedHead: string;
206
+ cwd: string;
207
+ run: CmdRunner;
208
+ readFile: (path: string) => string;
209
+ /** 표준 자리 `<state>/review-results` — 이 안의 `*-<pr>-<head12>.json` 만 본다. */
210
+ reviewResultsDir: string;
211
+ listDir: (dir: string) => readonly string[];
212
+ /** 겹침 관문이 찾은 «겹친» 최근 착지 커밋들. */
213
+ overlappingLandings: readonly string[];
214
+ }
215
+
216
+ function checkAncestorAuto(input: VerifyAncestorAutoInput): AncestorAutoVerdict {
217
+ const head = input.expectedHead.toLowerCase();
218
+ const landings = input.overlappingLandings;
219
+ if (landings.length === 0) return { ok: false, reason: 'ancestor-auto-no-overlapping-landings' };
220
+ const notAncestor = landings.find((sha) => !(/^[0-9a-f]{7,40}$/i.test(sha)
221
+ && input.run('git', ['merge-base', '--is-ancestor', sha, head], { cwd: input.cwd }).ok));
222
+ if (notAncestor !== undefined) return { ok: false, reason: `ancestor-auto-not-ancestor: ${notAncestor.slice(0, 12)} is not an ancestor of ${head.slice(0, 12)}` };
223
+ let names: readonly string[];
224
+ try { names = input.listDir(input.reviewResultsDir); } catch { return { ok: false, reason: 'review-missing' }; }
225
+ const suffix = new RegExp(`-${input.pr}-${head.slice(0, 12)}\\.json$`, 'i');
226
+ // 같은 <pr>-<head12> 리뷰가 여럿이면 «전부» 통과해야 한다 — 하나라도 거부면 거부(fail-closed · 처음 통과한 하나로 승인하지 않는다).
227
+ let accepted: string | undefined;
228
+ for (const name of [...names].filter((n) => suffix.test(n)).sort()) {
229
+ const review = checkReview(input, { reviewResult: join(input.reviewResultsDir, name) }, head);
230
+ if (!review.ok) return { ok: false, reason: review.reason };
231
+ accepted = review.reviewResult;
232
+ }
233
+ return accepted === undefined ? { ok: false, reason: 'review-missing' } : { ok: true, mode: 'already-ancestor-auto', reviewResult: accepted };
234
+ }
235
+
236
+ /**
237
+ * 증거 파일이 없을 때의 already-ancestor 자동 판정 — 겹친 착지가 «전부» 고정 머리의 조상이고 표준 자리의 리뷰 결과가
238
+ * pass|warn ⊕ must-fix 0 이면 통과. 던지지 않는다 — 어떤 실패도 거부(fail-closed)이고 관측 한 줄을 남긴다.
239
+ */
240
+ export function verifyAncestorAutoPass(input: VerifyAncestorAutoInput): AncestorAutoVerdict {
241
+ let verdict: AncestorAutoVerdict;
242
+ try { verdict = checkAncestorAuto(input); } catch (error) { verdict = { ok: false, reason: `verify-threw: ${String(error).slice(0, 200)}` }; }
243
+ debug.log('pr.land', 'overlap-evidence', {
244
+ decision: verdict.ok ? 'accepted' : 'rejected',
245
+ reason: verdict.ok ? 'already-ancestor-auto' : verdict.reason,
246
+ overlappingLandings: input.overlappingLandings,
247
+ pr: input.pr,
248
+ head: input.expectedHead,
249
+ evidencePath: null,
250
+ reviewResultsDir: input.reviewResultsDir,
251
+ });
252
+ return verdict;
253
+ }
254
+
113
255
  /** 던지지 않는다 — 어떤 실패도 거부(fail-closed)이고 관측 한 줄을 남긴다. */
114
256
  export function verifyOverlapEvidence(input: VerifyOverlapEvidenceInput): OverlapEvidenceVerdict {
115
257
  let verdict: Checked;
116
258
  try { verdict = check(input); } catch (error) { verdict = { ok: false, reason: `verify-threw: ${String(error).slice(0, 200)}` }; }
117
259
  debug.log('pr.land', 'overlap-evidence', {
118
260
  decision: verdict.ok ? 'accepted' : 'rejected',
119
- reason: verdict.ok ? (verdict.mode === 'already-ancestor' ? 'already-ancestor' : 'rebased+gated+reviewed') : verdict.reason,
261
+ reason: verdict.ok
262
+ ? (verdict.mode === 'already-ancestor' ? 'already-ancestor' : verdict.mode === 'base-advanced-disjoint' ? 'base-advanced-disjoint' : 'rebased+gated+reviewed')
263
+ : verdict.reason,
120
264
  overlappingLandings: input.overlappingLandings ?? [],
121
265
  pr: input.pr,
122
266
  head: input.expectedHead,
123
267
  base: `${input.remote}/${input.branch}`,
124
268
  baseTip: verdict.baseTip ?? null,
125
269
  evidencePath: input.evidencePath,
270
+ gateBase: (verdict.ok ? (verdict.mode === 'base-advanced-disjoint' ? verdict.gateBase : undefined) : verdict.gateBase) ?? null,
271
+ windowCommits: (verdict.ok ? (verdict.mode === 'base-advanced-disjoint' ? verdict.windowCommits : undefined) : verdict.windowCommits) ?? null,
126
272
  });
127
273
  if (!verdict.ok) return { ok: false, reason: verdict.reason };
128
- return verdict.mode === 'already-ancestor'
129
- ? { ok: true, mode: 'already-ancestor', reviewResult: verdict.reviewResult }
130
- : { ok: true, mode: 'rebased', baseTip: verdict.baseTip!, reviewResult: verdict.reviewResult };
274
+ if (verdict.mode === 'already-ancestor') return { ok: true, mode: 'already-ancestor', reviewResult: verdict.reviewResult };
275
+ if (verdict.mode === 'base-advanced-disjoint') {
276
+ return { ok: true, mode: 'base-advanced-disjoint', baseTip: verdict.baseTip!, gateBase: verdict.gateBase, windowCommits: verdict.windowCommits, reviewResult: verdict.reviewResult };
277
+ }
278
+ return { ok: true, mode: 'rebased', baseTip: verdict.baseTip!, reviewResult: verdict.reviewResult };
131
279
  }
@@ -5,8 +5,8 @@
5
5
  // elanous release tag --version <x.y.z> --source <commit> [--yes]
6
6
  // elanous release verify [--version <x.y.z>]
7
7
  // elanous release notes --from <ref> [--to <ref>]
8
- // elanous release run --version <x.y.z> [--prerelease rc] [--main-cut | --cut-commit <sha>] [--publish-at <iso>] [--dry-run] [--json] [--if-ready]
9
- // elanous release resume --run <runId> --from <node> [--waive <node> --reason <text>] [--accepted-regressions <json>] [--json]
8
+ // elanous release run --version <x.y.z> [--prerelease rc] [--main-cut | --cut-commit <sha>] [--publish-at <iso>] [--gate-executor <pod|host> [--gate-host <ssh-host>]] [--dry-run] [--json] [--if-ready]
9
+ // elanous release resume --run <runId> --from <node> [--waive <node> --reason <text>] [--accepted-regressions <json>] [--gate-executor <pod|host> [--gate-host <ssh-host>]] [--json]
10
10
  // elanous release auto-start [--window-minutes <n>] [--apply] [--json]
11
11
  // elanous release rehearsal tick [--apply] [--checkout <dir>] [--json]
12
12
  // elanous release preflight --version <x.y.z> [--publish-at <iso>] [--json] (컷 30분 전 사전 점검 · 읽기 전용 · ⛔ 있으면 exit 1)
@@ -989,7 +989,7 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
989
989
  .action(async (o: { version: string; base: string; pick: string[]; append?: boolean; dryRun?: boolean; json?: boolean }) => {
990
990
  await jsonAction(o.json, async (log) => cutReleaseBranch({ ...o, log }), () => true);
991
991
  });
992
- const runAction = async (o: { version: string; cutCommit?: string; mainCut?: boolean; prerelease?: string; dryRun?: boolean; ifReady?: boolean; publishAt?: string; forceFreeze?: boolean; json?: boolean }, logRun = console.log): Promise<boolean | AutoStartDeferral | undefined> => {
992
+ const runAction = async (o: { version: string; cutCommit?: string; mainCut?: boolean; prerelease?: string; dryRun?: boolean; ifReady?: boolean; publishAt?: string; forceFreeze?: boolean; gateExecutor?: string; gateHost?: string; json?: boolean }, logRun = console.log): Promise<boolean | AutoStartDeferral | undefined> => {
993
993
  // Only the rc rehearsal channel is operated for now (alpha/beta stay valid version shapes, not run options).
994
994
  if (o.prerelease !== undefined && o.prerelease !== 'rc') {
995
995
  const message = `--prerelease supports rc only: ${o.prerelease}`;
@@ -1038,7 +1038,7 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
1038
1038
  const outcome = await runReleaseIfReady(o.version, {
1039
1039
  ledgerRoot: releaseRunDeps.ledgerRoot,
1040
1040
  readiness: (version, options) => releaseReadiness(version, { ledgerRoot: releaseRunDeps.ledgerRoot, checklist: releaseRunDeps.checklist, ...options }),
1041
- run: () => runUnattendedRelease({ version: o.version, cutCommit: o.cutCommit, mainCut: o.mainCut, prerelease, forceFreeze: o.forceFreeze, publishAt: o.publishAt }, releaseRunDeps),
1041
+ run: () => runUnattendedRelease({ version: o.version, cutCommit: o.cutCommit, mainCut: o.mainCut, prerelease, forceFreeze: o.forceFreeze, publishAt: o.publishAt, gateExecutor: o.gateExecutor, gateHost: o.gateHost }, releaseRunDeps),
1042
1042
  });
1043
1043
  if (outcome.skipped) {
1044
1044
  if (o.json) await writeStdoutJson(`${JSON.stringify(outcome)}\n`);
@@ -1066,7 +1066,7 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
1066
1066
  }
1067
1067
  }
1068
1068
  await jsonAction(o.json, async (log) => {
1069
- const result = await runUnattendedRelease({ version: o.version, dryRun: o.dryRun, cutCommit: o.cutCommit, mainCut: o.mainCut, prerelease, forceFreeze: o.forceFreeze, publishAt: o.publishAt }, releaseRunDeps);
1069
+ const result = await runUnattendedRelease({ version: o.version, dryRun: o.dryRun, cutCommit: o.cutCommit, mainCut: o.mainCut, prerelease, forceFreeze: o.forceFreeze, publishAt: o.publishAt, gateExecutor: o.gateExecutor, gateHost: o.gateHost }, releaseRunDeps);
1070
1070
  log(`${result.dryRun ? '· 드라이런' : '▶ 릴리스 루프'} ${result.input.version} · 입력 ${JSON.stringify(result.input)}`);
1071
1071
  return result;
1072
1072
  }, (r) => r.dryRun || r.state?.status === 'done' || r.state?.status === 'awaiting-approval');
@@ -1081,8 +1081,10 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
1081
1081
  .option('--if-ready', '준비되지 않았으면 사유를 출력하고 성공으로 건너뛴다')
1082
1082
  .option('--publish-at <iso>', '승인·선행 노드 통과 뒤 지정 시각까지 발행 대기(오프셋 포함 ISO 시각)')
1083
1083
  .option('--force-freeze', '동결 중 게이트·발행 강행(관측 기록) — 가지 컷은 동결이 필요 없다 · 동결은 비상 스위치로 남는다')
1084
+ .option('--gate-executor <pod|host>', 'gate 노드 실행기(기본 pod) — host 는 Pod 대신 SSH 호스트의 깨끗한 클론에서 같은 판정(면제 아님)')
1085
+ .option('--gate-host <ssh-host>', '--gate-executor host 의 SSH 호스트(기본 release.loop.gateRemote)')
1084
1086
  .option('--json', '결과 한 줄 JSON(stdout) · 사람 줄은 stderr')
1085
- .action(async (o: { version: string; cutCommit?: string; mainCut?: boolean; prerelease?: string; dryRun?: boolean; ifReady?: boolean; publishAt?: string; forceFreeze?: boolean; json?: boolean }) => { await runAction(o); });
1087
+ .action(async (o: { version: string; cutCommit?: string; mainCut?: boolean; prerelease?: string; dryRun?: boolean; ifReady?: boolean; publishAt?: string; forceFreeze?: boolean; gateExecutor?: string; gateHost?: string; json?: boolean }) => { await runAction(o); });
1086
1088
  release.command('preflight')
1087
1089
  .description('컷 30분 전 사전 점검(읽기 전용) — 체크리스트 게이트 모의 · 발행 뒤 칸 · run --dry-run 입력 · 동결 · 일정/게이트 실측. ⛔ 있으면 exit 1')
1088
1090
  .requiredOption('--version <v>', '점검할 판(x.y.z)')
@@ -1128,8 +1130,10 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
1128
1130
  .option('--waive <node>', '저장된 실패 노드 한 개를 면제(--from 은 그 노드 그대로)')
1129
1131
  .option('--reason <text>', '면제 사유 — 공개 알려진 문제에 들어갈 사용자 영향 설명')
1130
1132
  .option('--accepted-regressions <json>', '알려진 문제 추가 입력 JSON 배열 [{"id":"…","note":"…"}]')
1133
+ .option('--gate-executor <pod|host>', '다시 도는 gate 의 실행기 전환(--from gate 이전만) — 런 입력에 기록 · 면제 아님')
1134
+ .option('--gate-host <ssh-host>', '--gate-executor host 의 SSH 호스트(기본 저장된 런 입력 gateRemote)')
1131
1135
  .option('--json', '결과 한 줄 JSON(stdout)')
1132
- .action(async (o: { run: string; from: string; partial?: boolean; waive?: string; reason?: string; acceptedRegressions?: string; json?: boolean }) => {
1136
+ .action(async (o: { run: string; from: string; partial?: boolean; waive?: string; reason?: string; acceptedRegressions?: string; gateExecutor?: string; gateHost?: string; json?: boolean }) => {
1133
1137
  await jsonAction(o.json, async (log) => {
1134
1138
  let acceptedRegressions: AcceptedRegression[] | undefined;
1135
1139
  if (o.acceptedRegressions !== undefined) {
@@ -1143,12 +1147,17 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
1143
1147
  const result = await resumeReleaseRun({ runId: o.run, from: o.from, partial: o.partial,
1144
1148
  ...(o.waive === undefined ? {} : { waive: o.waive }),
1145
1149
  ...(o.reason === undefined ? {} : { reason: o.reason }),
1146
- ...(o.acceptedRegressions === undefined ? {} : { acceptedRegressions }) });
1150
+ ...(o.acceptedRegressions === undefined ? {} : { acceptedRegressions }),
1151
+ ...(o.gateExecutor === undefined ? {} : { gateExecutor: o.gateExecutor }),
1152
+ ...(o.gateHost === undefined ? {} : { gateHost: o.gateHost }) });
1153
+ const override = result.state.inputOverrides?.at(-1);
1154
+ if (o.gateExecutor !== undefined && override) log(`⇄ gate 실행기 ${override.set.gateExecutor}${override.set.gateRemote ? ` @ ${override.set.gateRemote}` : ''} (런 입력에 기록 · 면제 아님)`);
1147
1155
  if (result.partial.requested) log(result.partial.applied
1148
1156
  ? `◐ 부분 재검: ${result.partial.files}파일 · 나머지는 앞 게이트(${result.partial.priorCommit.slice(0, 12)}) 결과를 잇는다`
1149
1157
  : `● 전체 게이트로: ${result.partial.reason}`);
1150
1158
  log(`${result.tip.changed ? `↻ ${result.tip.branch} ${result.tip.from.slice(0, 12)} → ${result.tip.to.slice(0, 12)} (백업 ${result.tip.backup})` : `· ${result.tip.branch} 끝 그대로 ${result.tip.to.slice(0, 12)}`} · ${o.from} 부터 → ${result.state.status}`);
1151
- return { tip: result.tip, partial: result.partial, status: result.state.status, runId: result.state.runId };
1159
+ return { tip: result.tip, partial: result.partial, status: result.state.status, runId: result.state.runId,
1160
+ ...(o.gateExecutor !== undefined && result.state.inputOverrides?.length ? { gateExecutor: result.state.inputOverrides.at(-1) } : {}) };
1152
1161
  }, (r) => r.status === 'done' || r.status === 'awaiting-approval');
1153
1162
  });
1154
1163
  release.command('rehearsal').description('판 일정에서 컷 전 가벼운 RC 두 단계').command('tick')
@@ -1,6 +1,6 @@
1
1
  import type { Command } from 'commander';
2
2
  import { readdirSync, readFileSync } from 'node:fs';
3
- import { join, relative, resolve } from 'node:path';
3
+ import { dirname, join, relative, resolve } from 'node:path';
4
4
  import YAML from 'yaml';
5
5
 
6
6
  export const RFC_STATUSES = ['unknown', 'proposed', 'adopted', 'completed', 'superseded', 'rejected'] as const;
@@ -69,6 +69,49 @@ export function adoptedWithoutCard(rows: readonly RfcRow[]): RfcRow[] {
69
69
  return rows.filter((row) => row.status === 'adopted' && row.card === null);
70
70
  }
71
71
 
72
+ const RFC_HUB_FILE = 'INDEX-loop-agents-and-harness-rfcs.md';
73
+
74
+ function markdownLinks(markdown: string): string[] {
75
+ return [...markdown.matchAll(/\]\(([^)\s]+\.md)(?:#[^)]*)?\)/g)].map((match) => match[1]!);
76
+ }
77
+
78
+ function hasHubConnectionReference(text: string, documentPath: string, root: string): boolean {
79
+ const firstSection = text.search(/^##\s/m);
80
+ const header = firstSection < 0 ? text : text.slice(0, firstSection);
81
+ const hubPath = resolve(root, 'docs', RFC_HUB_FILE);
82
+ return header.split(/\r?\n/).some((line) => {
83
+ if (!line.includes('🔗') || !line.includes('연결 참조')) return false;
84
+ return markdownLinks(line).some((target) =>
85
+ resolve(dirname(resolve(root, documentPath)), target) === hubPath);
86
+ });
87
+ }
88
+
89
+ /** 허브를 못 읽으면 null — «0» 과 «못 셌음» 은 다른 값이다. */
90
+ function hubConnectionCounts(root: string, rows: readonly RfcRow[]): { unconnected: number; unlisted: number } | null {
91
+ const hubPath = resolve(root, 'docs', RFC_HUB_FILE);
92
+ let hub: string;
93
+ try { hub = readFileSync(hubPath, 'utf8'); }
94
+ catch { return null; }
95
+
96
+ const hubDirectory = dirname(hubPath);
97
+ const listedPaths = new Set(markdownLinks(hub)
98
+ .filter((target) => /^RFC-.*\.md$/.test(target.split(/[?#]/, 1)[0]!))
99
+ .map((target) => relative(root, resolve(hubDirectory, target)).replaceAll('\\', '/')));
100
+ const rowsByPath = new Map(rows.map((row) => [row.path, row]));
101
+ const unconnected = [...listedPaths].filter((path) => {
102
+ const row = rowsByPath.get(path);
103
+ if (!row) return false;
104
+ return !hasHubConnectionReference(readFileSync(resolve(root, row.path), 'utf8'), row.path, root);
105
+ }).length;
106
+ const unlisted = rows.filter((row) => {
107
+ if (listedPaths.has(row.path)) return false;
108
+ const text = readFileSync(resolve(root, row.path), 'utf8');
109
+ const title = /^#\s+(.+)$/m.exec(text)?.[1] ?? '';
110
+ return /루프|하니스|그래프/.test(title);
111
+ }).length;
112
+ return { unconnected, unlisted };
113
+ }
114
+
72
115
  export function runRfcStatus(options: { json?: boolean; missingCards?: boolean }, root = process.cwd(),
73
116
  write: (text: string) => void = (text) => process.stdout.write(text)): void {
74
117
  const all = scanRfcs(root);
@@ -77,9 +120,13 @@ export function runRfcStatus(options: { json?: boolean; missingCards?: boolean }
77
120
  write(`${JSON.stringify({ total: all.length, adoptedWithoutCard: adoptedWithoutCard(all).length, rows }, null, 2)}\n`);
78
121
  return;
79
122
  }
123
+ const hubCounts = hubConnectionCounts(root, all);
80
124
  write(`RFC ${all.length} · 채택·칸 없음 ${adoptedWithoutCard(all).length} · 표시 ${rows.length}\n`);
81
125
  write('상태\t담당\t칸\t대체 문서\t핸드북\t옛 상태\t옛 담당\t경로\t메타 오류\n');
82
126
  for (const row of rows) write(`${row.status}\t${row.owner ?? '모름'}\t${row.card ?? '모름'}\t${row.supersededBy ?? '모름'}\t${row.handbook ?? '모름'}\t${row.legacyStatus ?? '-'}\t${row.legacyOwner ?? '-'}\t${row.path}\t${row.issue ?? '-'}\n`);
127
+ write(hubCounts
128
+ ? `허브 연결: 되돌림 없음 ${hubCounts.unconnected} · 미등재 ${hubCounts.unlisted}\n`
129
+ : `허브 연결: 못 셈 — docs/${RFC_HUB_FILE} 를 못 읽었다\n`);
83
130
  }
84
131
 
85
132
  export function registerRfcStatusCommand(docs: Command): void {
@@ -87,5 +134,8 @@ export function registerRfcStatusCommand(docs: Command): void {
87
134
  .description('전체 RFC 생애주기 표 (RFC frontmatter만 읽음)')
88
135
  .option('--json', '기계가 읽을 JSON')
89
136
  .option('--missing-cards', '채택됐으나 칸이 기록되지 않은 RFC만')
90
- .action((options: { json?: boolean; missingCards?: boolean }) => runRfcStatus(options));
137
+ .action((options: { json?: boolean; missingCards?: boolean }): void => {
138
+ // registerDocsCommands in docs-cli.ts calls registerRfcStatusCommand; this action calls runRfcStatus and its hub check.
139
+ runRfcStatus(options);
140
+ });
91
141
  }
@@ -428,6 +428,80 @@ export interface VersionPruneOutcome {
428
428
  skipped?: string;
429
429
  }
430
430
 
431
+ export interface RailSafeProcessListing {
432
+ status: number | null;
433
+ stdout: string;
434
+ stderr: string;
435
+ error?: Error;
436
+ }
437
+
438
+ export interface AssessRailSafeInput {
439
+ entries: ReadonlyArray<{ name: string; mtimeMs: number }>;
440
+ current: string;
441
+ daemonSha: string;
442
+ keep: number;
443
+ versionsDir: string;
444
+ listProcesses?: (pattern: string) => RailSafeProcessListing;
445
+ readProcessCommand?: (pid: string) => string | null;
446
+ isReleaseRunActive?: () => boolean;
447
+ }
448
+
449
+ /** Shadow probes must never stall the install: a hung pgrep/ps reads as «no process» (status null). */
450
+ const RAIL_SAFE_PROBE_TIMEOUT_MS = 5_000;
451
+
452
+ function pgrepProcesses(pattern: string): RailSafeProcessListing {
453
+ const result = spawnSync('pgrep', ['-f', pattern], { encoding: 'utf8', maxBuffer: SPAWN_MAX_BUFFER, timeout: RAIL_SAFE_PROBE_TIMEOUT_MS });
454
+ return { status: result.status, stdout: result.stdout ?? '', stderr: result.error?.message ?? result.stderr ?? '', ...(result.error ? { error: result.error } : {}) };
455
+ }
456
+
457
+ function processCommand(pid: string): string | null {
458
+ const result = spawnSync('ps', ['-p', pid, '-o', 'command='], { encoding: 'utf8', maxBuffer: SPAWN_MAX_BUFFER, timeout: RAIL_SAFE_PROBE_TIMEOUT_MS });
459
+ return result.status === 0 ? (result.stdout ?? '').trim() || null : null;
460
+ }
461
+
462
+ function listedProcessCommands(listing: RailSafeProcessListing, readCommand: (pid: string) => string | null): string[] {
463
+ if (listing.status !== 0) return [];
464
+ return listing.stdout.split(/\r?\n/).flatMap((line) => {
465
+ const entry = line.trim();
466
+ if (!entry) return [];
467
+ if (!/^\d+$/.test(entry)) return [entry];
468
+ const command = readCommand(entry);
469
+ return command ? [command] : [];
470
+ });
471
+ }
472
+
473
+ function pgrepPathPattern(path: string): string {
474
+ return path.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
475
+ }
476
+
477
+ /** Shadow-only observation: determine whether a prune plan targets an argv-held version or a release run is active. */
478
+ export function assessRailSafe(input: AssessRailSafeInput): {
479
+ wouldBlock: boolean;
480
+ reasons: Array<'held-version' | 'release-run-active'>;
481
+ heldVersions: string[];
482
+ releaseRunActive: boolean;
483
+ } {
484
+ const plan = planVersionPrune(input.entries, input.current, input.daemonSha, input.keep);
485
+ const listProcesses = input.listProcesses ?? pgrepProcesses;
486
+ const readCommand = input.readProcessCommand ?? processCommand;
487
+ const heldVersions = plan.removed.filter((name) => {
488
+ const path = join(input.versionsDir, name);
489
+ try {
490
+ return listedProcessCommands(listProcesses(pgrepPathPattern(path)), readCommand).some((argv) => argv.includes(path));
491
+ } catch { return false; }
492
+ });
493
+ let releaseRunActive = false;
494
+ try {
495
+ releaseRunActive = input.isReleaseRunActive?.() ?? listedProcessCommands(
496
+ listProcesses('release[[:space:]]+(run|resume)'), readCommand,
497
+ ).some((argv) => /release\s+(?:run|resume)/.test(argv));
498
+ } catch { releaseRunActive = false; }
499
+ const reasons: Array<'held-version' | 'release-run-active'> = [];
500
+ if (heldVersions.length) reasons.push('held-version');
501
+ if (releaseRunActive) reasons.push('release-run-active');
502
+ return { wouldBlock: reasons.length > 0, reasons, heldVersions, releaseRunActive };
503
+ }
504
+
431
505
  /**
432
506
  * 야간 자동 설치는 하룻밤에 한 판(약 641MB · 2026-09-24 실측)을 쌓는다. 설치기는 옛 판을 안 지운다.
433
507
  * 남기는 것: `current` 가 가리키는 판 · 지금 데몬이 도는 판(판정의 `from` sha) · 최근 `keep` 판(롤백용).
@@ -664,6 +738,12 @@ export interface ReleaseUpdateDeps {
664
738
  listPruneProcesses?: ProcessListing;
665
739
  /** Per-process cwd/executable probe; when absent the host's lsof is used. */
666
740
  scanPruneProcess?: ProcessScan;
741
+ /** Shadow-only rail-safe process listing override. */
742
+ listRailSafeProcesses?: (pattern: string) => RailSafeProcessListing;
743
+ /** Resolves pgrep's PID-only output to a command line; defaults to `ps -p <pid> -o command=`. */
744
+ readRailSafeProcessCommand?: (pid: string) => string | null;
745
+ /** Shadow-only release-run detector override. */
746
+ isReleaseRunActive?: () => boolean;
667
747
  /** 릴리스 기준 URL 주입 — 환경변수와 설치 출처보다 우선한다. */
668
748
  releaseBase?: string;
669
749
  }
@@ -740,10 +820,14 @@ export async function runReleaseUpdate(options: ReleaseUpdateOptions = {}, deps:
740
820
  }
741
821
  let previous = '';
742
822
  let previousBuild = '';
823
+ let previousCommit = '';
743
824
  let replacedDev: { from: string; to: string } | null = null;
744
825
  try {
745
826
  const metadata: unknown = JSON.parse(readFileSync(join(prefix, 'install.json'), 'utf8'));
746
827
  previousBuild = installedBuild(metadata);
828
+ if (typeof metadata === 'object' && metadata !== null && typeof (metadata as { commit?: unknown }).commit === 'string') {
829
+ previousCommit = (metadata as { commit: string }).commit;
830
+ }
747
831
  if (typeof metadata === 'object' && metadata !== null && (metadata as { channel?: unknown }).channel === 'dev') {
748
832
  const fromDir = typeof (metadata as { versionDir?: unknown }).versionDir === 'string' ? (metadata as { versionDir: string }).versionDir : '';
749
833
  replacedDev = { from: fromDir, to: '' };
@@ -757,6 +841,26 @@ export async function runReleaseUpdate(options: ReleaseUpdateOptions = {}, deps:
757
841
  const result = spawnSync(command, args, { cwd, encoding: 'utf8', input, maxBuffer: SPAWN_MAX_BUFFER, env: { ...process.env, ...env, PATH: childPath() } });
758
842
  return { status: result.status, stderr: result.error?.message ?? result.stderr ?? '' };
759
843
  });
844
+ let railSafeEntries: Array<{ name: string; mtimeMs: number }> = [];
845
+ try {
846
+ railSafeEntries = readdirSync(join(prefix, 'versions'), { withFileTypes: true })
847
+ .filter((entry) => entry.isDirectory())
848
+ .map((entry) => ({ name: entry.name, mtimeMs: statSync(join(prefix, 'versions', entry.name)).mtimeMs }));
849
+ } catch { /* shadow observation cannot change the install decision */ }
850
+ let railSafe: ReturnType<typeof assessRailSafe> = { wouldBlock: false, reasons: [], heldVersions: [], releaseRunActive: false };
851
+ try {
852
+ railSafe = assessRailSafe({
853
+ entries: railSafeEntries,
854
+ current: previous,
855
+ daemonSha: previousCommit,
856
+ keep: options.keep ?? 3,
857
+ versionsDir: join(prefix, 'versions'),
858
+ ...(deps.listRailSafeProcesses ? { listProcesses: deps.listRailSafeProcesses } : {}),
859
+ ...(deps.readRailSafeProcessCommand ? { readProcessCommand: deps.readRailSafeProcessCommand } : {}),
860
+ ...(deps.isReleaseRunActive ? { isReleaseRunActive: deps.isReleaseRunActive } : {}),
861
+ });
862
+ } catch { /* shadow observation cannot change the install decision */ }
863
+ try { debug.log('self-update.rail-safe', 'shadow', railSafe); } catch { /* shadow observation never changes update decisions */ }
760
864
  try {
761
865
  const install = run('bash', ['-s', '--', '--no-modify-path', '--prefix', prefix], prefix, installer, envLiteral({ ELANOUS_VERSION: version ?? '', ELANOUS_INSTALL_SOURCE: '', ELANOUS_RELEASE_BASE: base }));
762
866
  if (install.status !== 0) return fail(1, `설치 실패: ${install.stderr.trim() || install.status}`);