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
@@ -12,6 +12,7 @@ import { runGraph, type GraphRunState } from '../../src/graph-runner/runner.js';
12
12
  import { releaseRunUniverse } from './release-universe.js';
13
13
  import { baseVersion, isStableVersion, nextPrereleaseVersion, type PrereleaseKind } from './release-version.js';
14
14
  import { publishAtError } from './publish-at.js';
15
+ import { checkGateExecutorChoice, type GateExecutor } from './gate-executor.js';
15
16
 
16
17
  const VERSION = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)$/;
17
18
  const GRAPH = join(import.meta.dir, '../../graphs/release/release-loop.yaml');
@@ -39,7 +40,10 @@ export interface ReleaseRunInput extends ReleaseLoopConfig {
39
40
  version: string;
40
41
  previousVersion: string;
41
42
  cutCommit?: string;
42
- gatePodPool: string;
43
+ /** Required for the pod executor (the default); a host-executor run may have none. */
44
+ gatePodPool?: string;
45
+ /** GATE-HOST-EXECUTOR — absent = pod (today's input, byte-identical); `host` sweeps on `gateRemote`. */
46
+ gateExecutor?: GateExecutor;
43
47
  forceFreeze?: boolean;
44
48
  publishAt?: string;
45
49
  /** RELEASE-BRANCH: version-release cuts release/<v> from main HEAD and bumps only the branch. */
@@ -104,22 +108,32 @@ export function releaseLoopConfig(configPath = getElanousConfigDirOverride() ? j
104
108
  return raw.release?.loop ?? {};
105
109
  }
106
110
 
107
- export function buildReleaseRunInput(version: string, deps: UnattendedReleaseDeps = {}, cutCommit?: string): ReleaseRunInput {
111
+ /** GATE-HOST-EXECUTOR — the operator's explicit executor choice (`--gate-executor` / `--gate-host`); absent = pod. */
112
+ export interface GateExecutorRequest { executor?: string; host?: string }
113
+
114
+ export function buildReleaseRunInput(version: string, deps: UnattendedReleaseDeps = {}, cutCommit?: string, gate: GateExecutorRequest = {}): ReleaseRunInput {
108
115
  const config = deps.config ?? releaseLoopConfig(deps.configPath);
116
+ if (gate.executor === undefined && gate.host !== undefined) throw new Error('--gate-host requires --gate-executor host');
117
+ const choice = gate.executor === undefined ? undefined
118
+ : checkGateExecutorChoice(gate.executor, gate.executor === 'host' ? gate.host ?? config.gateRemote : gate.host);
109
119
  const pool = config.gatePodPool;
110
- if (typeof pool !== 'string' || !pool.trim()) throw new Error('release.loop.gatePodPool is required before graph execution');
120
+ const hasPool = typeof pool === 'string' && !!pool.trim();
121
+ // The pod executor (the default) stays fail-closed on a missing pool; an explicit host executor does not need one.
122
+ if (choice?.executor !== 'host' && !hasPool) throw new Error('release.loop.gatePodPool is required before graph execution');
111
123
  const previousVersion = latestPublishedPreviousVersion(baseVersion(version), deps.ledgerRoot);
112
124
  return {
113
125
  ...config,
114
126
  version, previousVersion,
115
127
  ...(cutCommit !== undefined ? { cutCommit } : {}),
116
- gatePodPool: pool.trim(),
128
+ ...(hasPool ? { gatePodPool: pool.trim() } : {}),
129
+ ...(choice ? { gateExecutor: choice.executor } : {}),
130
+ ...(choice?.host ? { gateRemote: choice.host } : {}),
117
131
  };
118
132
  }
119
133
 
120
134
  /** Fail closed at the entry boundary, before any graph node can change a release. */
121
135
  export async function runUnattendedRelease(
122
- opts: { version: string; dryRun?: boolean; cutCommit?: string; forceFreeze?: boolean; mainCut?: boolean; prerelease?: PrereleaseKind; publishAt?: string },
136
+ opts: { version: string; dryRun?: boolean; cutCommit?: string; forceFreeze?: boolean; mainCut?: boolean; prerelease?: PrereleaseKind; publishAt?: string; gateExecutor?: string; gateHost?: string },
123
137
  deps: UnattendedReleaseDeps = {},
124
138
  ): Promise<{ input: ReleaseRunInput; dryRun: boolean; state?: GraphRunState }> {
125
139
  // RELEASE-BRANCH (10-06): the default cut is a release branch, so main keeps landing and the run no longer needs a
@@ -135,7 +149,7 @@ export async function runUnattendedRelease(
135
149
  }
136
150
  }
137
151
  const version = resolveRunVersion(opts.version, opts.prerelease, deps);
138
- const input = buildReleaseRunInput(version, deps, opts.cutCommit);
152
+ const input = buildReleaseRunInput(version, deps, opts.cutCommit, { executor: opts.gateExecutor, host: opts.gateHost });
139
153
  if (opts.publishAt !== undefined) {
140
154
  const invalid = publishAtError(opts.publishAt);
141
155
  if (invalid) throw new Error(invalid);
@@ -143,7 +157,8 @@ export async function runUnattendedRelease(
143
157
  }
144
158
  if (opts.forceFreeze) input.forceFreeze = true;
145
159
  if (branchCut) input.branchCut = true;
146
- debug.log('release.run', 'input', { version, requested: opts.version, branchCut, prerelease: opts.prerelease ?? null, dryRun: opts.dryRun === true });
160
+ debug.log('release.run', 'input', { version, requested: opts.version, branchCut, prerelease: opts.prerelease ?? null, dryRun: opts.dryRun === true,
161
+ ...(input.gateExecutor ? { gateExecutor: input.gateExecutor, gateHost: input.gateExecutor === 'host' ? input.gateRemote ?? null : null } : {}) });
147
162
  if (opts.dryRun) return { input, dryRun: true };
148
163
  if (opts.prerelease) {
149
164
  // A rehearsal neither judges nor carries the stable version's checklist cells.
@@ -0,0 +1,147 @@
1
+ /**
2
+ * 승격 착지의 base 쪽 tsc 진단을 «main 커밋별로 한 번만» 재고 재사용하는 캐시.
3
+ * 키 = base sha ⊕ tsc 명령 ⊕ 설정 파일(base 커밋에서 git show 로 읽은 내용) 해시 ⊕ tsc 판.
4
+ * 값 = rebase 전 정규화 진단(워크트리 절대 경로는 «<ROOT>» 자리표로 저장).
5
+ * ⛔ 캐시 실패는 호출자에게 던지지 않는다 — 읽기·쓰기 모두 miss 로 떨어진다.
6
+ */
7
+ import { createHash, randomBytes } from 'node:crypto';
8
+ import { execFileSync } from 'node:child_process';
9
+ import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, utimesSync, writeFileSync } from 'node:fs';
10
+ import { join } from 'node:path';
11
+ import { elanousStateRoot } from '../src/autopilot/state-paths.js';
12
+ import type { TypecheckError } from '../src/typecheck-ratchet.js';
13
+
14
+ export const ROOT_PLACEHOLDER = '<ROOT>';
15
+ export const BASELINE_CACHE_MAX_ENTRIES = 50;
16
+ const CACHE_FORMAT = 1;
17
+
18
+ /** 키에 들어가는 «기준 커밋의 설정 파일» — 이 중 하나라도 달라지면 같은 sha 라도 다른 진단이다. */
19
+ export const BASELINE_CACHE_CONFIG_FILES = ['tsconfig.gate.json', 'apps/pwa/tsconfig.json', 'tsconfig.pwa-env.json', 'tsconfig.json', 'bun.lock'] as const;
20
+
21
+ export interface BaselineCacheOptions {
22
+ /** 캐시 디렉터리. 생략하면 `<상태 디렉터리>/cache/tsc-baseline`. */
23
+ dir?: string;
24
+ env?: NodeJS.ProcessEnv;
25
+ /** git 실행 심 — 출력 바이트를 돌려주고 실패하면 던진다. */
26
+ git?: (args: string[], cwd: string) => Buffer;
27
+ /** tsc 판 조회 심. */
28
+ tscVersion?: (cwd: string) => string;
29
+ maxEntries?: number;
30
+ }
31
+
32
+ export type BaselineCacheLookup =
33
+ | { result: 'hit'; diagnostics: TypecheckError[] }
34
+ | { result: 'miss' }
35
+ | { result: 'corrupt' };
36
+
37
+ export function baselineCacheEnabled(env: NodeJS.ProcessEnv = process.env): boolean {
38
+ return env.TSC_BASELINE_CACHE?.trim() !== '0';
39
+ }
40
+
41
+ export function defaultBaselineCacheDir(): string {
42
+ return join(elanousStateRoot(), 'cache', 'tsc-baseline');
43
+ }
44
+
45
+ function defaultGit(args: string[], cwd: string): Buffer {
46
+ const env = { ...process.env };
47
+ delete env.GIT_DIR;
48
+ delete env.GIT_WORK_TREE;
49
+ delete env.GIT_COMMON_DIR;
50
+ return execFileSync('git', args, { cwd, env, stdio: ['ignore', 'pipe', 'pipe'], maxBuffer: 64 * 1024 * 1024 });
51
+ }
52
+
53
+ function defaultTscVersion(cwd: string): string {
54
+ try {
55
+ const parsed = JSON.parse(readFileSync(join(cwd, 'node_modules', 'typescript', 'package.json'), 'utf8')) as { version?: unknown };
56
+ return typeof parsed.version === 'string' ? parsed.version : 'unknown';
57
+ } catch {
58
+ return 'unknown';
59
+ }
60
+ }
61
+
62
+ /** 키를 못 만들면(base 를 sha 로 풀 수 없음 등) undefined — 호출자는 캐시 없이 잰다. */
63
+ export function computeBaselineCacheKey(base: string, command: string, cwd: string, options: BaselineCacheOptions = {}): string | undefined {
64
+ try {
65
+ const git = options.git ?? defaultGit;
66
+ const sha = git(['rev-parse', '--verify', `${base}^{commit}`], cwd).toString('utf8').trim();
67
+ if (!/^[0-9a-f]{40,64}$/.test(sha)) return undefined;
68
+ const files: Record<string, string> = {};
69
+ for (const file of BASELINE_CACHE_CONFIG_FILES) {
70
+ try {
71
+ files[file] = createHash('sha256').update(git(['show', `${sha}:${file}`], cwd)).digest('hex');
72
+ } catch {
73
+ files[file] = 'absent';
74
+ }
75
+ }
76
+ const tsc = (options.tscVersion ?? defaultTscVersion)(cwd);
77
+ return createHash('sha256').update(JSON.stringify({ format: CACHE_FORMAT, sha, command, files, tsc })).digest('hex');
78
+ } catch {
79
+ return undefined;
80
+ }
81
+ }
82
+
83
+ function worktreeRoots(worktree: string): string[] {
84
+ const real = (() => { try { return realpathSync(worktree); } catch { return worktree; } })();
85
+ return [...new Set([worktree, real])].sort((left, right) => right.length - left.length);
86
+ }
87
+
88
+ /** rebase 전 진단의 워크트리 절대 경로를 «<ROOT>» 로 바꾼다(긴 경로 먼저). */
89
+ export function toPlaceholderPaths(diagnostics: readonly TypecheckError[], worktree: string): TypecheckError[] {
90
+ const roots = worktreeRoots(worktree);
91
+ const swap = (text: string): string => roots.reduce((acc, root) => acc.split(root).join(ROOT_PLACEHOLDER), text);
92
+ return diagnostics.map((diagnostic) => ({ ...diagnostic, line: swap(diagnostic.line), file: swap(diagnostic.file) }));
93
+ }
94
+
95
+ function entryFile(dir: string, key: string): string {
96
+ return join(dir, `${key}.json`);
97
+ }
98
+
99
+ function isStoredDiagnostic(value: unknown): value is TypecheckError {
100
+ if (!value || typeof value !== 'object') return false;
101
+ const entry = value as Record<string, unknown>;
102
+ return typeof entry.file === 'string' && typeof entry.line === 'string' && (entry.code === undefined || typeof entry.code === 'string');
103
+ }
104
+
105
+ export function lookupBaselineCache(key: string, options: BaselineCacheOptions = {}): BaselineCacheLookup {
106
+ try {
107
+ const file = entryFile(options.dir ?? defaultBaselineCacheDir(), key);
108
+ if (!existsSync(file)) return { result: 'miss' };
109
+ const parsed = JSON.parse(readFileSync(file, 'utf8')) as { format?: unknown; key?: unknown; diagnostics?: unknown };
110
+ if (parsed.format !== CACHE_FORMAT || parsed.key !== key || !Array.isArray(parsed.diagnostics) || !parsed.diagnostics.every(isStoredDiagnostic)) {
111
+ return { result: 'corrupt' };
112
+ }
113
+ try { const now = new Date(); utimesSync(file, now, now); } catch { /* 보존 순서 갱신 실패는 무해하다 */ }
114
+ return { result: 'hit', diagnostics: parsed.diagnostics };
115
+ } catch {
116
+ return { result: 'corrupt' };
117
+ }
118
+ }
119
+
120
+ function pruneBaselineCache(dir: string, keep: number): void {
121
+ const entries = readdirSync(dir)
122
+ .filter((name) => name.endsWith('.json'))
123
+ .map((name) => ({ name, mtimeMs: statSync(join(dir, name)).mtimeMs }))
124
+ .sort((left, right) => right.mtimeMs - left.mtimeMs || left.name.localeCompare(right.name));
125
+ for (const stale of entries.slice(keep)) rmSync(join(dir, stale.name), { force: true });
126
+ }
127
+
128
+ /** `<ROOT>` 자리표가 든 진단을 원자 쓰기(임시→rename)로 저장한다. 성공 여부만 돌려주고 던지지 않는다. */
129
+ export function storeBaselineCache(key: string, command: string, diagnostics: readonly TypecheckError[], options: BaselineCacheOptions = {}): boolean {
130
+ try {
131
+ const dir = options.dir ?? defaultBaselineCacheDir();
132
+ mkdirSync(dir, { recursive: true });
133
+ const file = entryFile(dir, key);
134
+ const temporary = `${file}.${process.pid}.${randomBytes(4).toString('hex')}.tmp`;
135
+ try {
136
+ writeFileSync(temporary, JSON.stringify({ format: CACHE_FORMAT, key, command, storedAt: new Date().toISOString(), diagnostics }));
137
+ renameSync(temporary, file);
138
+ } catch (error) {
139
+ rmSync(temporary, { force: true });
140
+ throw error;
141
+ }
142
+ try { pruneBaselineCache(dir, options.maxEntries ?? BASELINE_CACHE_MAX_ENTRIES); } catch { /* 정리 실패는 저장 성공을 뒤집지 않는다 */ }
143
+ return true;
144
+ } catch {
145
+ return false;
146
+ }
147
+ }
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bun
2
2
  // 실행 칸(Pod 등)이 사라지기 «전»에 자기 llm.usage 를 한 줄로 요약해 stdout 에 — 슈퍼바이저가 호스트로 가져간다.
3
3
  // RFC-fleet-supervisor-substrates-benchmark-and-token-accounting §A2·F2 · 2026-09-25.
4
- // bun scripts/usage-rollup.ts [--since 6h]
4
+ // bun scripts/usage-rollup.ts [--since 6h] [--partial [--seq N]]
5
5
  // 출력: ELANOUS_USAGE_ROLLUP {"substrate","runId","armId","truncated","rows":[{site,provider,model,calls,inputTokens,outputTokens,cacheReadInputTokens,cacheCreationInputTokens,usdKnown,unknownCostCalls}],"total":{…}}
6
6
  // ⛔ 의존성 없음(설치본 `elanous` 만 부른다) — clone 에 node_modules 가 없어도 돈다.
7
7
  // ⛔ 비용을 모르는 호출(cost.kind≠known)은 0 으로 합치지 않고 따로 센다.
@@ -9,18 +9,50 @@
9
9
  // 호스트(맥)에서 돌리면 테스트 우주·워크트리·다른 트랙의 런까지 섞인다(📏 09-25: 운영만 16회 vs 전체 124회). 호스트 집계용이 아니다.
10
10
  import { spawnSync } from 'node:child_process';
11
11
 
12
- export interface UsageRow { site: string; provider: string; model: string; calls: number; inputTokens: number; outputTokens: number; cacheReadInputTokens: number; cacheCreationInputTokens: number; cacheCreationUnmeasuredCalls: number; usdKnown: number; unknownCostCalls: number; includedCalls: number; apiEquivalentUsd: number }
12
+ export interface UsageRow { site: string; provider: string; model: string; calls: number; inputTokens: number; outputTokens: number; cacheReadInputTokens: number; cacheCreationInputTokens: number; cacheCreationUnmeasuredCalls: number; usdKnown: number; unknownCostCalls: number; includedCalls: number; apiEquivalentUsd: number;
13
+ /** OR-BYOK-COST — BYOK 호출 수 ⊕ 그중 상류 청구액 합(알려진 것만) ⊕ 상류를 모르는 호출 수. BYOK 를 본 행에만 있다(종전 행 바이트 불변). */
14
+ byokCalls?: number; byokUpstreamUsd?: number; byokUpstreamUnknownCalls?: number }
15
+
16
+ type UsageData = { site?: string; provider?: string; billingProvider?: string; model?: string; inputTokens?: number; outputTokens?: number; cacheReadInputTokens?: number; cacheCreationInputTokens?: number; cost?: { kind?: string; usd?: number }; podRollupKey?: string; partial?: boolean; partialSeq?: number; partialAt?: string };
17
+
18
+ /** 같은 podRollupKey 두 행 중 어느 쪽이 더 완전한가 — 최종 > 부분 · 부분끼리는 seq 큰 쪽 · 같으면 at 늦은 쪽. 완전히 같으면 앞 행을 둔다. */
19
+ function rollupRowBeats(next: UsageData, kept: UsageData): boolean {
20
+ const nextFinal = next.partial !== true;
21
+ const keptFinal = kept.partial !== true;
22
+ if (nextFinal !== keptFinal) return nextFinal;
23
+ if (nextFinal) return false;
24
+ const nextSeq = Number(next.partialSeq ?? 0);
25
+ const keptSeq = Number(kept.partialSeq ?? 0);
26
+ if (nextSeq !== keptSeq) return nextSeq > keptSeq;
27
+ return String(next.partialAt ?? '') > String(kept.partialAt ?? '');
28
+ }
13
29
 
14
30
  export function rollup(lines: readonly string[]): { rows: UsageRow[]; truncated: boolean } {
15
31
  const by = new Map<string, UsageRow>();
32
+ // ⭐ MF-3c0d253e — 같은 Pod 사용량의 재방출(부분 ⊕ 최종 · abort ⊕ resume)은 podRollupKey 로 한 번만 센다(이중계상 방지).
33
+ // 들어온 순서가 아니라 «더 완전한 행»을 남긴다(최종 > 부분 · seq · at — rollupRowBeats).
34
+ // 키가 없는 행(기존 경로 전부)은 종전과 동작·바이트 그대로다.
16
35
  let truncated = false;
36
+ const entries: UsageData[] = [];
37
+ const keptIndexByPodRollupKey = new Map<string, number>();
17
38
  for (const line of lines) {
18
39
  if (/limitReached=true|truncated/.test(line) && !line.startsWith('{')) truncated = true;
19
40
  if (!line.startsWith('{')) continue;
20
41
  let r: { event?: string; data?: Record<string, unknown> };
21
42
  try { r = JSON.parse(line); } catch { continue; }
22
43
  if (r.event !== 'llm-usage' || !r.data) continue;
23
- const d = r.data as { site?: string; provider?: string; billingProvider?: string; model?: string; inputTokens?: number; outputTokens?: number; cacheReadInputTokens?: number; cacheCreationInputTokens?: number; cost?: { kind?: string; usd?: number } };
44
+ const d = r.data as UsageData;
45
+ if (typeof d.podRollupKey === 'string') {
46
+ const keptIndex = keptIndexByPodRollupKey.get(d.podRollupKey);
47
+ if (keptIndex !== undefined) {
48
+ if (rollupRowBeats(d, entries[keptIndex]!)) entries[keptIndex] = d;
49
+ continue;
50
+ }
51
+ keptIndexByPodRollupKey.set(d.podRollupKey, entries.length);
52
+ }
53
+ entries.push(d);
54
+ }
55
+ for (const d of entries) {
24
56
  const provider = d.billingProvider ?? d.provider ?? '?'; // ⭐ 과금 주체 우선(B10)
25
57
  const key = `${d.site ?? '?'}|${provider}|${d.model ?? '?'}`;
26
58
  const row = by.get(key) ?? { site: d.site ?? '?', provider, model: d.model ?? '?', calls: 0, inputTokens: 0, outputTokens: 0, cacheReadInputTokens: 0, cacheCreationInputTokens: 0, cacheCreationUnmeasuredCalls: 0, usdKnown: 0, unknownCostCalls: 0, includedCalls: 0, apiEquivalentUsd: 0 };
@@ -38,14 +70,34 @@ export function rollup(lines: readonly string[]): { rows: UsageRow[]; truncated:
38
70
  // ⭐ `actual`(provider 보고 청구액 · C7)은 알려진 비용이다.
39
71
  else if ((d.cost?.kind === 'known' || d.cost?.kind === 'actual') && typeof d.cost.usd === 'number') row.usdKnown += d.cost.usd;
40
72
  else { row.unknownCostCalls++; if (d.cost?.kind === 'partial' && typeof d.cost.usd === 'number') row.usdKnown += d.cost.usd; }
73
+ // OR-BYOK-COST — BYOK 비용은 위에서 이미 usdKnown(actual)·unknownCostCalls(partial)로 갈렸다. 여기선 출처만 따로 나른다.
74
+ const c = d.cost as { byok?: unknown; upstreamUsd?: unknown } | undefined;
75
+ if (c?.byok === true) {
76
+ row.byokCalls = (row.byokCalls ?? 0) + 1;
77
+ if (typeof c.upstreamUsd === 'number' && Number.isFinite(c.upstreamUsd)) row.byokUpstreamUsd = (row.byokUpstreamUsd ?? 0) + c.upstreamUsd;
78
+ else row.byokUpstreamUnknownCalls = (row.byokUpstreamUnknownCalls ?? 0) + 1;
79
+ }
41
80
  by.set(key, row);
42
81
  }
43
82
  return { rows: [...by.values()], truncated };
44
83
  }
45
84
 
85
+ /** METER-POD-ROLLUP-GAP — 인자 해석: `--partial`(⊕ `--seq N`)면 부분 롤업 한 줄로 바꾼다. 전체판(기본)은 불변. */
86
+ export function parseRollupArgs(argv: readonly string[]): { since: string; partial: boolean; seq: number } {
87
+ const sinceAt = argv.indexOf('--since');
88
+ const seqAt = argv.indexOf('--seq');
89
+ const seqRaw = seqAt >= 0 ? Number(argv[seqAt + 1] ?? 1) : 1;
90
+ return { since: sinceAt >= 0 ? argv[sinceAt + 1] ?? '6h' : '6h', partial: argv.includes('--partial'), seq: Number.isInteger(seqRaw) && seqRaw > 0 ? seqRaw : 1 };
91
+ }
92
+
93
+ /** 한 줄 표현 — 전체판 줄은 기존 바이트와 동일, 부분판은 partial:true ⊕ seq ⊕ at 만 얹는다. */
94
+ export function renderRollupLine(payload: Record<string, unknown>, opts: { partial: boolean; seq: number }): string {
95
+ if (opts.partial) return `ELANOUS_USAGE_ROLLUP_PARTIAL ${JSON.stringify({ ...payload, partial: true, seq: opts.seq, at: new Date().toISOString() })}`;
96
+ return `ELANOUS_USAGE_ROLLUP ${JSON.stringify(payload)}`;
97
+ }
98
+
46
99
  if (import.meta.main) {
47
- const i = process.argv.indexOf('--since');
48
- const since = i > 0 ? process.argv[i + 1] ?? '6h' : '6h';
100
+ const { since, partial, seq } = parseRollupArgs(process.argv);
49
101
  const r = spawnSync('elanous', ['logs', '--all', '--include-test', '--event', 'llm-usage', '--since', since, '--limit', '50000', '--json', '--json-data'], { encoding: 'utf8', timeout: 120_000, maxBuffer: 256 * 1024 * 1024 });
50
102
  const { rows, truncated } = rollup(`${r.stdout ?? ''}\n${r.stderr ?? ''}`.split('\n'));
51
103
  const total = rows.reduce((t, x) => ({ calls: t.calls + x.calls, inputTokens: t.inputTokens + x.inputTokens, outputTokens: t.outputTokens + x.outputTokens, usdKnown: t.usdKnown + x.usdKnown, unknownCostCalls: t.unknownCostCalls + x.unknownCostCalls }), { calls: 0, inputTokens: 0, outputTokens: 0, usdKnown: 0, unknownCostCalls: 0 });
@@ -54,5 +106,5 @@ if (import.meta.main) {
54
106
  const elanousVersion = v.status === 0 ? (v.stdout ?? '').trim().split('\n').pop() ?? null : null;
55
107
  // ⭐ 런 출처 칸(🅣 RFC run-origin 이름 그대로) — 호스트 재방출이 그대로 받는다.
56
108
  const origin = { podName: process.env.ELANOUS_POD_NAME ?? null, nodeName: process.env.ELANOUS_NODE_NAME ?? null, hostId: process.env.ELANOUS_HOST_ID ?? null, imageCommit: process.env.ELANOUS_IMAGE_COMMIT ?? null };
57
- console.log(`ELANOUS_USAGE_ROLLUP ${JSON.stringify({ substrate: process.env.ELANOUS_SUBSTRATE ?? null, elanousVersion, ...origin, runId: process.env.ELANOUS_RUN_ID ?? null, armId: process.env.ELANOUS_ARM_ID ?? null, measured: r.status === 0, truncated, rows, total })}`);
109
+ console.log(renderRollupLine({ substrate: process.env.ELANOUS_SUBSTRATE ?? null, elanousVersion, ...origin, runId: process.env.ELANOUS_RUN_ID ?? null, armId: process.env.ELANOUS_ARM_ID ?? null, measured: r.status === 0, truncated, rows, total }, { partial, seq }));
58
110
  }
@@ -85,6 +85,14 @@ function childBedrockEnv(provider: string): Record<string, string> {
85
85
  return provider === 'bedrock' ? bedrockChildRelayEnv() : {};
86
86
  }
87
87
 
88
+ /** ZAI-POD-CHILD — glm(Z.ai GLM Coding Plan) 자격은 «env»(`ZAI_API_KEY` · Pod 에선 secretKeyRef)에 산다 — 키 캐시 파일이 없는 Pod 에선
89
+ * replace env 라 릴레이가 없으면 자식이 직결을 못 탄다. 카탈로그 envKey(`ZHIPU_API_KEY`)와 다른 이름이라 따로 넘긴다(값은 싣기만 · 어디에도 안 찍는다). */
90
+ function childZaiEnv(provider: string): Record<string, string> {
91
+ if (provider !== 'glm') return {};
92
+ const key = process.env.ZAI_API_KEY?.trim();
93
+ return key ? { ZAI_API_KEY: key } : {};
94
+ }
95
+
88
96
  /** The family classifier is model-id based, so preserve local-provider identity
89
97
  * across the replace-env child boundary without changing non-local model IDs. */
90
98
  function childRelayModel(provider: string, model: string): string {
@@ -110,6 +118,7 @@ export function childLlmSelectionEnv(selection?: ChildLlmSelection): Record<stri
110
118
  ...childProviderKeyEnv(provider),
111
119
  ...childLocalEndpointEnv(provider),
112
120
  ...childBedrockEnv(provider),
121
+ ...childZaiEnv(provider),
113
122
  };
114
123
  }
115
124
  // ⛔⭐ 상속 갈래도 local 이면 «주소»를 같이 넘긴다(BACKLOG B12 · 2026-09-25).
@@ -122,5 +131,6 @@ export function childLlmSelectionEnv(selection?: ChildLlmSelection): Record<stri
122
131
  ...(inheritedProvider ? childLocalEndpointEnv(inheritedProvider) : {}),
123
132
  // 상속 갈래도 bedrock 이면 AWS_* 를 같이 넘긴다(명시 갈래와 같은 규칙 — replace env 라 자동 상속이 없다).
124
133
  ...(inheritedProvider ? childBedrockEnv(inheritedProvider) : {}),
134
+ ...(inheritedProvider ? childZaiEnv(inheritedProvider) : {}),
125
135
  };
126
136
  }
@@ -174,6 +174,8 @@ export interface AgentBackend {
174
174
  args: string[];
175
175
  /** 구독 모드 위해 스크럽할 env 키(API 과금 회피). */
176
176
  scrubEnv?: string[];
177
+ /** 이 실행에만 덧씌울 env(스크럽 뒤 · 바깥 값을 «덮는다»). CODEX-CREDITS-STEP 재시도가 크레딧 계정의 `CODEX_HOME` 을 싣는다. */
178
+ env?: Readonly<Record<string, string>>;
177
179
  /** 초기 신뢰/권한 프롬프트 처리 — 스크린 보고 필요 시 키 입력. 처리했으면 true. */
178
180
  handleTrust?(screen: string, write: (s: string) => void): boolean;
179
181
  }
@@ -341,6 +343,7 @@ export function resolveBackendSpawn(
341
343
  forcedEnv.push(key);
342
344
  }
343
345
  }
346
+ for (const [key, value] of Object.entries(backend.env ?? {})) env[key] = value;
344
347
  let nestedEnvRemovedCount = 0;
345
348
  for (const key of NESTED_AGENT_ENV_BLOCKLIST) {
346
349
  if (key in env) {
@@ -464,6 +467,10 @@ export function resolveDefaultBackend(
464
467
  debug.log('agent-mission.backend', 'fallback-switch', { from: 'codex', to: decision.backend, reason: 'codex-exhausted' });
465
468
  return decision.backend === 'claude' ? claudeBackend : grokBackend;
466
469
  }
470
+ if (decision.action === 'codex-credits') {
471
+ // CODEX-CREDITS-STEP — 체인 마지막 칸: codex 로 «돌아와» 선불 크레딧으로 계속(판정 쪽이 이미 관측을 남겼다).
472
+ debug.log('agent-mission.backend', 'fallback-codex-credits', { from: 'codex', to: 'codex', reason: 'chain-last-step' });
473
+ }
467
474
  } catch (err) {
468
475
  debug.log('agent-mission.backend', 'fallback-failed', { message: (err as Error)?.message }, { level: 'warn' });
469
476
  }
@@ -522,10 +529,17 @@ export function decideRuntimeFallback(
522
529
  failure: unknown,
523
530
  currentStep: FallbackStep,
524
531
  state: RuntimeFallbackState,
525
- decide: (input: { currentStep: FallbackStep; currentCredentialRateLimited: true }) => { action: string; backend?: string },
532
+ decide: (input: { currentStep: FallbackStep; currentCredentialRateLimited: true }) => { action: string; backend?: string; to?: { readonly name: string; readonly home: string } },
526
533
  ): AgentBackend | null {
527
534
  if (classifyAuthError(failure).errorKind !== 'rate-limited' || state.descents >= state.maxDescents) return null;
528
535
  const decision = decide({ currentStep, currentCredentialRateLimited: true });
536
+ // CODEX-CREDITS-STEP — grok 등이 한도로 죽고 체인의 다음 칸이 `codex-credits` 면 codex 로 돌아온다(크레딧 · 한 번만).
537
+ if (decision.action === 'codex-credits') {
538
+ if (state.attemptedSteps.has('codex-credits')) return null;
539
+ // 판정이 고른 크레딧 계정(잔액 많은 순)의 홈으로 자식 codex 를 띄운다 — 없으면 호스트 CODEX_HOME 그대로.
540
+ const home = decision.to?.home?.trim();
541
+ return home ? { ...codexBackend, env: { CODEX_HOME: home } } : codexBackend;
542
+ }
529
543
  if (decision.action !== 'switch-backend') return null;
530
544
  const nextStep = decision.backend === 'grok' ? 'grok' : decision.backend === 'claude' ? 'claude-pty' : null;
531
545
  if (!nextStep || state.attemptedSteps.has(nextStep)) return null;
@@ -642,7 +656,7 @@ export interface AgentMissionDeps {
642
656
  /** Inject controller responses while keeping the production control loop. */
643
657
  controlStream?: StreamLLMFn;
644
658
  resolvePtyWebAddress?: typeof resolvePtyWebAddress;
645
- resolveRunFallback?: (input: { currentStep: FallbackStep; currentCredentialRateLimited: true }) => { action: string; backend?: string };
659
+ resolveRunFallback?: (input: { currentStep: FallbackStep; currentCredentialRateLimited: true }) => { action: string; backend?: string; to?: { readonly name: string; readonly home: string } };
646
660
  /** 재귀 재시도 사이에만 전달되는 런 로컬 폴백 진행 상태. */
647
661
  runtimeFallback?: RuntimeFallbackContext;
648
662
  reemitPtyUsage?: typeof reemitPtyUsage;
@@ -2310,7 +2324,8 @@ async function runAgentMissionBody(spec: AgentMissionSpec, deps: AgentMissionDep
2310
2324
  });
2311
2325
  if (nextBackend && !deps.isMissionAborted?.()) {
2312
2326
  decisionEvent({ step: 'recover', text: `Switching ${backend.name} to ${nextBackend.name}`, detail: { blocked: control.termination.message, action: `Retry with ${nextBackend.name}` } });
2313
- const nextStep: FallbackStep = nextBackend.name === 'grok' ? 'grok' : nextBackend.name === 'claude' ? 'claude-pty' : 'codex-rotate';
2327
+ // runtime 이 codex 로 «돌아오는» 길은 `codex-credits` 칸뿐이다(decideRuntimeFallback) — 그 칸을 시도한 것으로 적는다.
2328
+ const nextStep: FallbackStep = nextBackend.name === 'grok' ? 'grok' : nextBackend.name === 'claude' ? 'claude-pty' : 'codex-credits';
2314
2329
  stopLive();
2315
2330
  if (h.pid) await (deps.terminateProcessTree ?? terminateMissionProcessTree)(h.pid);
2316
2331
  try { h.kill(); } catch { /* noop */ }
@@ -2,7 +2,8 @@ import { readFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { elanousStateRoot } from '../autopilot/state-paths.js';
4
4
  import { debug } from '../debug/log.js';
5
- import { streamLLM } from '../llm.js';
5
+ import * as llm from '../llm.js';
6
+ import { resolveRoleLlm } from '../user-config.js';
6
7
  import { emitDecision, type DecisionEvent } from '../live/detail-switch.js';
7
8
  import { OFFICIAL_INDEX_KEYS } from '../market/official-keys.js';
8
9
  import { verifyIndex, type MarketplaceIndex } from '../market/signed-index.js';
@@ -80,10 +81,14 @@ function parseNeeds(raw: string, vocabulary: readonly string[]): string[] {
80
81
  }
81
82
 
82
83
  async function inferNeeds(mission: string, vocabulary: readonly string[]): Promise<string> {
83
- return streamLLM([
84
+ const resolved = resolveRoleLlm('classify');
85
+ const provider = llm.PROVIDERS[resolved.provider];
86
+ if (!provider) throw new Error(`Unknown classify provider: ${resolved.provider}`);
87
+ debug.log('llm.role-pin', 'resolved', { site: 'resource-ladder', role: 'classify', model: resolved.model, provider: resolved.provider, source: resolved.source });
88
+ return llm.streamLLM([
84
89
  { role: 'system', content: `Identify at most five concrete tool/service/skill capabilities required for this mission. Reply ONLY with JSON {"needs":["capability"]}. No credentials, tokens, URLs, or mission text in the output. If a relevant label appears in this verified official marketplace vocabulary, use that exact label; otherwise use a concrete capability. If uncertain, return an empty list.\nVerified labels: ${JSON.stringify(vocabulary)}` },
85
90
  { role: 'user', content: mission },
86
- ], () => {}, { maxTokens: 200, temperature: 0 });
91
+ ], () => {}, { model: resolved.model, provider, usageRole: 'classify', maxTokens: 200, temperature: 0 });
87
92
  }
88
93
 
89
94
  async function readOfficialIndex(): Promise<{ marketplaceBytes: Uint8Array; signatureText: string }> {
@@ -11,6 +11,7 @@ import { runGitCommand } from '../git-fs/runner.js';
11
11
  import * as llm from '../llm.js';
12
12
  import type { LLMMessage } from '../llm.js';
13
13
  import { debug } from '../debug/log.js';
14
+ import { resolveRoleLlm } from '../user-config.js';
14
15
  import { releasePathHold, releasePathHoldShouldPost, releasePathHoldComment, releasePathHoldCommentsArgs, releasePrFilePaths, RELEASE_PATH_LABEL } from '../self-dev/release-path-guard.js';
15
16
  import { runAgentMission, resolveBackend, type AgentMissionResult, type EvidenceMode } from './driver.js';
16
17
  import { judgeWithAcp, prepareJudgeDiff } from './acp-judge.js';
@@ -439,8 +440,13 @@ asks: reinforce 일 때 codex 가 반영할 **구체 지적 목록**(각 한 줄
439
440
  JSON 만: {"verdict":"...","asks":["..."],"reason":"..."}`;
440
441
  const messages: LLMMessage[] = [{ role: 'system', content: sys }, { role: 'user', content: body.slice(0, 6000) }];
441
442
  let raw = '';
442
- try { raw = await llm.streamLLM(messages, () => {}, { maxTokens: 700, temperature: 0.1 }); }
443
- catch (e) { return { verdict: 'ambiguous', asks: [], reason: `분류 오류: ${describeThrownValue(e)}`, classificationSource: 'llm-call-failure' }; }
443
+ try {
444
+ const resolved = resolveRoleLlm('classify');
445
+ const provider = llm.PROVIDERS[resolved.provider];
446
+ if (!provider) throw new Error(`Unknown classify provider: ${resolved.provider}`);
447
+ debug.log('llm.role-pin', 'resolved', { site: 'classify-review', role: 'classify', model: resolved.model, provider: resolved.provider, source: resolved.source });
448
+ raw = await llm.streamLLM(messages, () => {}, { model: resolved.model, provider, usageRole: 'classify', maxTokens: 700, temperature: 0.1 });
449
+ } catch (e) { return { verdict: 'ambiguous', asks: [], reason: `분류 오류: ${describeThrownValue(e)}`, classificationSource: 'llm-call-failure' }; }
444
450
  const jm = raw.match(/\{[\s\S]*\}/);
445
451
  if (!jm) return { verdict: 'ambiguous', asks: [], reason: 'JSON 파싱 실패', classificationSource: 'llm' };
446
452
  try {
@@ -0,0 +1,139 @@
1
+ // ── ACP Claude Code 를 «구현» 실행기로 (CLAUDE-IMPL-LANE · `--child-llm-provider claude-acp`) ──
2
+ //
3
+ // acp-reviewer.ts 의 짝이다. 리뷰어는 read-only 라 읽기 툴만 허가하지만, 구현 실행기는 런 워크트리 안에서
4
+ // 쓰기·실행을 허가해야 한다. 그래서 approver 가 «워크트리 밖 경로 쓰기»를 구조적으로 거부한다.
5
+ // 자식은 claude 구독 ACP 어댑터(`claude-agent-acp`)이며 env 는 subscriptionReviewerSpawn 의 과금 스크럽
6
+ // (ANTHROPIC_API_KEY 등 제거)을 그대로 쓴다 — 지우지 않으면 구독이 아니라 API 로 과금된다.
7
+ import { isAbsolute, relative, resolve } from 'node:path';
8
+ import { AcpAgent, type AcpAgentOpts } from '../acp/client.js';
9
+ import { debug } from '../debug/log.js';
10
+ import { subscriptionReviewerSpawn } from '../self-dev/review-provider-fallback.js';
11
+ import type { SubscriptionReviewerSpec } from '../user-config.js';
12
+ import { isReadOnlyReviewTool } from './acp-reviewer.js';
13
+
14
+ const WRITE_TOOL_KINDS = new Set(['edit', 'delete', 'move']);
15
+ const PATH_INPUT_KEYS = ['file_path', 'filePath', 'path', 'notebook_path', 'destination', 'source'] as const;
16
+
17
+ export interface ImplementerPermissionRequest {
18
+ title: string;
19
+ kind?: string;
20
+ rawInput?: unknown;
21
+ }
22
+
23
+ export interface ImplementerPermissionDecision {
24
+ tool: string;
25
+ path: string | null;
26
+ decision: 'allow' | 'deny';
27
+ }
28
+
29
+ function requestedPaths(rawInput: unknown): string[] {
30
+ if (!rawInput || typeof rawInput !== 'object') return [];
31
+ const input = rawInput as Record<string, unknown>;
32
+ return PATH_INPUT_KEYS.flatMap((key) => (typeof input[key] === 'string' && input[key] ? [input[key] as string] : []));
33
+ }
34
+
35
+ function insideWorktree(worktree: string, target: string): boolean {
36
+ const rel = relative(resolve(worktree), resolve(worktree, target));
37
+ return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel));
38
+ }
39
+
40
+ /** 허가 판정(순수 함수). 쓰기 계열(edit·delete·move)은 모든 경로가 워크트리 안일 때만 허가하고,
41
+ * 실행(execute)은 cwd 가 워크트리라 허가한다. 읽기 계열은 허가, 그 외는 경로가 있고 전부 안쪽일 때만 허가(보수적). */
42
+ export function decideImplementerPermission(req: ImplementerPermissionRequest, worktree: string): ImplementerPermissionDecision {
43
+ const paths = requestedPaths(req.rawInput);
44
+ const outside = paths.find((p) => !insideWorktree(worktree, p));
45
+ const tool = req.title.slice(0, 80);
46
+ const shown = outside ?? paths[0] ?? null;
47
+ const verdict = (allow: boolean): ImplementerPermissionDecision => ({ tool, path: shown, decision: allow ? 'allow' : 'deny' });
48
+ if (req.kind && WRITE_TOOL_KINDS.has(req.kind)) return verdict(paths.length > 0 && outside === undefined);
49
+ if (req.kind === 'execute') return verdict(true);
50
+ if (isReadOnlyReviewTool(req.kind, req.title)) return verdict(true);
51
+ return verdict(paths.length > 0 && outside === undefined);
52
+ }
53
+
54
+ export interface AcpImplementerOpts {
55
+ /** 런 워크트리 — ACP 자식의 cwd 이자 쓰기 허용 경계. */
56
+ worktree: string;
57
+ /** 자식 env 의 원천(기본 process.env). 과금 키는 subscriptionReviewerSpawn 이 제거한다. */
58
+ env?: NodeJS.ProcessEnv;
59
+ /** cct 실행기가 `teamclaude env --no-mitm` 에서 읽은 프록시 주소. */
60
+ proxyBaseUrl?: string;
61
+ spec?: SubscriptionReviewerSpec;
62
+ /** 모델 별칭 — 지정 시 session/set_model 로 고정. */
63
+ model?: string;
64
+ /** 이어갈 ACP 세션 id — 주면 newSession 대신 loadSession 으로 이어간다. */
65
+ resumeSessionId?: string;
66
+ /** 구현 1턴 최대 대기(ms). 초과 시 cancel → throw. 기본 1_800_000. */
67
+ timeoutMs?: number;
68
+ /** spawn → initialize → session 준비까지의 단일 기한(ms). 기본 60_000. */
69
+ handshakeTimeoutMs?: number;
70
+ /** 테스트용 ACP 에이전트 생성기. */
71
+ createAgent?: (opts: AcpAgentOpts) => AcpAgent;
72
+ }
73
+
74
+ export interface AcpImplementerResult {
75
+ text: string;
76
+ sessionId: string;
77
+ allowed: number;
78
+ denied: number;
79
+ }
80
+
81
+ const OBS = 'agent-substrate.acp-implementer';
82
+
83
+ export function makeAcpImplementer(opts: AcpImplementerOpts): (prompt: string) => Promise<AcpImplementerResult> {
84
+ return async (prompt) => {
85
+ const spawn = subscriptionReviewerSpawn(opts.spec ?? { provider: 'claude-acp' }, opts.env ?? process.env, opts.proxyBaseUrl);
86
+ const timeoutMs = opts.timeoutMs ?? 1_800_000;
87
+ const handshakeTimeoutMs = opts.handshakeTimeoutMs ?? 60_000;
88
+ let allowed = 0;
89
+ let denied = 0;
90
+ const agentOpts: AcpAgentOpts = {
91
+ backendId: 'claude',
92
+ backendSpec: spawn.backendSpec,
93
+ env: spawn.env,
94
+ cwd: opts.worktree,
95
+ log: (message) => debug.log(OBS, 'agent-log', { message }),
96
+ permissionApprover: async (req) => {
97
+ const verdict = decideImplementerPermission(req, opts.worktree);
98
+ if (verdict.decision === 'allow') allowed++; else denied++;
99
+ debug.log(OBS, 'permission', { ...verdict });
100
+ return verdict.decision === 'allow';
101
+ },
102
+ };
103
+ const agent = opts.createAgent ? opts.createAgent(agentOpts) : new AcpAgent(agentOpts);
104
+ let handshakeTimer: ReturnType<typeof setTimeout> | undefined;
105
+ const withinHandshake = <T>(step: Promise<T>): Promise<T> => Promise.race([step, new Promise<never>((_, reject) => {
106
+ handshakeTimer = setTimeout(() => reject(new Error(`ACP handshake timeout (${handshakeTimeoutMs}ms)`)), handshakeTimeoutMs);
107
+ })]).finally(() => { if (handshakeTimer) clearTimeout(handshakeTimer); });
108
+ let timer: ReturnType<typeof setTimeout> | undefined;
109
+ let timedOut = false;
110
+ let text = '';
111
+ try {
112
+ await withinHandshake(agent.start());
113
+ let sid: string;
114
+ if (opts.resumeSessionId) {
115
+ await withinHandshake(agent.loadSession({ sessionId: opts.resumeSessionId, cwd: opts.worktree }));
116
+ sid = opts.resumeSessionId;
117
+ } else {
118
+ sid = await withinHandshake(agent.newSession());
119
+ }
120
+ if (opts.model) await withinHandshake(agent.selectSessionModel(sid, opts.model));
121
+ debug.log(OBS, 'start', { cwd: opts.worktree, model: opts.model ?? '(default)', resumed: Boolean(opts.resumeSessionId) });
122
+ timer = setTimeout(() => { timedOut = true; agent.cancel(sid).catch(() => { /* noop */ }); }, timeoutMs);
123
+ await agent.prompt(sid, [{ type: 'text', text: prompt }], (u) => {
124
+ if (u.sessionUpdate === 'agent_message_chunk' && u.content.type === 'text' && u.content.text) text += u.content.text;
125
+ });
126
+ if (timedOut) throw new Error(`ACP 구현 타임아웃(${timeoutMs}ms)`);
127
+ debug.log(OBS, 'done', { chars: text.length, allowed, denied });
128
+ return { text, sessionId: sid, allowed, denied };
129
+ } catch (e) {
130
+ debug.log(OBS, 'error', { message: (e as Error).message, allowed, denied }, { level: 'error' });
131
+ throw e;
132
+ } finally {
133
+ if (timer) clearTimeout(timer);
134
+ try { await agent.stop(); } catch (e) {
135
+ debug.log(OBS, 'stop-fail', { message: (e as Error).message }, { level: 'error' });
136
+ }
137
+ }
138
+ };
139
+ }