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
@@ -63,7 +63,8 @@ export function getModelFamily(modelId: string | undefined): ModelFamily {
63
63
  // OpenRouter gateway ids only (`openrouter/<vendor>/<model>`). Bare vendor ids such as
64
64
  // `qwen/qwen3-coder-30b` stay 'other' on purpose: LM Studio publishes native ids in that
65
65
  // same shape (see getModelTier's local pattern), and this family must not reach local runs.
66
- if (m.startsWith('openrouter/') && (m.includes('glm') || m.includes('kimi') || m.includes('qwen'))) {
66
+ // OR-DEEPSEEK-TUNE (2026-10-11) — `openrouter/deepseek/*` 도 같은 군(종전 'other' → 도구 규율 null).
67
+ if (m.startsWith('openrouter/') && (m.includes('glm') || m.includes('kimi') || m.includes('qwen') || m.startsWith('openrouter/deepseek/'))) {
67
68
  return 'open-weight';
68
69
  }
69
70
  return 'other';
@@ -101,13 +101,27 @@ export const STORE_SAFETY_MAX = 100_000;
101
101
  export const LOG_STORE_READONLY_BUSY_TIMEOUT_MS = 2_000;
102
102
  const LOG_STORE_WRITE_BUSY_TIMEOUT_MS = 5_000;
103
103
  const LOG_STORE_OPEN_RETRY_DELAYS_MS = [100, 200, 400, 800, 1600] as const;
104
+ const LOG_STORE_BUSY_RETRY_MAX_ATTEMPTS = 3;
105
+ const LOG_STORE_BUSY_RETRY_BUFFER_MAX_ROWS = 2_000;
106
+ type LogStoreInsertRow = { rec: LogRecord; surface: string };
107
+ type LogStoreRetryBatch = { rows: LogStoreInsertRow[]; attempts: number; preserveRows: boolean };
104
108
  const sqliteOpenWait = new Int32Array(new SharedArrayBuffer(4));
105
109
  let pendingBusyOpenLosses: Array<{ path: string; loss: { store: 'logs'; op: 'open'; waitedMs: number } }> = [];
106
-
107
110
  function sqliteBusy(error: unknown): boolean {
108
111
  return /SQLITE_BUSY|SQLITE_LOCKED|database is locked|database is busy/i.test(String(error));
109
112
  }
110
113
 
114
+ function emitInsertBusyLost(data: Record<string, unknown>): void {
115
+ try { process.stderr.write(`[store.sqlite] busy-lost ${JSON.stringify(data)}\n`); } catch { /* diagnostics must not block callers */ }
116
+ try { debug.log('store.sqlite', 'busy-lost', data); } catch { /* diagnostics must not block callers */ }
117
+ }
118
+
119
+ function topCategories(rows: readonly LogStoreInsertRow[]): string[] {
120
+ const counts = new Map<string, number>();
121
+ for (const { rec } of rows) counts.set(rec.category, (counts.get(rec.category) ?? 0) + 1);
122
+ return [...counts].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, 3).map(([category]) => category);
123
+ }
124
+
111
125
  export class LogCursorNotFoundError extends Error {
112
126
  constructor(public readonly beforeId: number) {
113
127
  super(`page cursor row ${beforeId} not found in this store`);
@@ -196,6 +210,7 @@ export class LogStore {
196
210
  readonly instance: string;
197
211
  /** read-only open (LF7-b 연합 조회) — 스키마 생성/마이그레이션/insert 불가. */
198
212
  readonly readonly: boolean;
213
+ private busyRetryBuffer: LogStoreRetryBatch[] = [];
199
214
 
200
215
  /** 타 인스턴스 스토어의 연합 조회용 read-only open — "연합은 read-only 로만"
201
216
  * 불변식의 구조적 집행(마이그레이션 포함 어떤 write 도 발생 불가). */
@@ -296,10 +311,12 @@ export class LogStore {
296
311
  })();
297
312
  }
298
313
 
299
- /** 배치 insert(단일 트랜잭션). 실패는 throw — fail-soft 는 호출측(StoreSink)
300
- * 책임(ops-log 의 recordOpsEvent/Safe 분업 동형). */
314
+ /** 배치 insert(단일 트랜잭션). SQLITE_BUSY 는 bounded in-memory buffer 후 throw — fail-soft 는 호출측(StoreSink) 책임. */
301
315
  insertBatch(rows: Array<{ rec: LogRecord; surface: string }>, opts: { preserveRows?: boolean } = {}): void {
302
316
  if (rows.length === 0) return;
317
+ const retrying = this.busyRetryBuffer;
318
+ const current: LogStoreRetryBatch = { rows, attempts: 0, preserveRows: opts.preserveRows === true };
319
+ const batches = [...retrying, current];
303
320
  const stmt = this.db.prepare(
304
321
  `INSERT INTO logs (ts, ts_ms, level, instance, host_id, surface, category, event, session_id, trace_id, data)
305
322
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
@@ -307,8 +324,8 @@ export class LogStore {
307
324
  const decisionRow = this.db.prepare(`SELECT id FROM logs WHERE instance = ? AND category = 'loop.steward' AND event = 'decision'
308
325
  AND json_valid(data) AND json_extract(data, '$.runId') = ? ORDER BY id DESC LIMIT 1`);
309
326
  const updateDecision = this.db.prepare(`UPDATE logs SET ts = ?, ts_ms = ?, level = ?, surface = ?, data = ? WHERE id = ?`);
310
- const insertAll = this.db.transaction((batch: Array<{ rec: LogRecord; surface: string }>) => {
311
- for (const { rec, surface } of batch) {
327
+ const insertAll = this.db.transaction((batch: Array<{ row: LogStoreInsertRow; preserveRows: boolean }>) => {
328
+ for (const { row: { rec, surface }, preserveRows } of batch) {
312
329
  const tsMs = Date.parse(rec.ts);
313
330
  const timestamp = Number.isFinite(tsMs) ? tsMs : Date.now();
314
331
  const level = deriveLogLevel(rec);
@@ -316,7 +333,7 @@ export class LogStore {
316
333
  const runId = rec.category === 'loop.steward' && rec.event === 'decision' &&
317
334
  rec.data && typeof rec.data === 'object' && !Array.isArray(rec.data)
318
335
  ? (rec.data as Record<string, unknown>).runId : undefined;
319
- const existing = !opts.preserveRows && typeof runId === 'string' && runId
336
+ const existing = !preserveRows && typeof runId === 'string' && runId
320
337
  ? decisionRow.get(this.instance, runId) as { id: number } | undefined : undefined;
321
338
  if (existing) {
322
339
  updateDecision.run(rec.ts, timestamp, level, surface, data, existing.id);
@@ -340,14 +357,43 @@ export class LogStore {
340
357
  );
341
358
  }
342
359
  });
360
+ const allRows = batches.flatMap((batch) => batch.rows.map((row) => ({ row, preserveRows: batch.preserveRows })));
343
361
  const startedAt = Date.now();
344
362
  try {
345
- insertAll(rows);
363
+ insertAll(allRows);
364
+ this.busyRetryBuffer = [];
365
+ for (const batch of retrying) {
366
+ try { debug.log('store.sqlite', 'busy-recovered', { rows: batch.rows.length, attempts: batch.attempts + 1 }); }
367
+ catch { /* diagnostics must not affect writes */ }
368
+ }
346
369
  } catch (error) {
347
- if (sqliteBusy(error)) {
348
- const loss = { store: 'logs', op: 'insertBatch', waitedMs: Date.now() - startedAt };
349
- try { process.stderr.write(`[store.sqlite] busy-lost ${JSON.stringify(loss)}\n`); } catch { /* diagnostics must not block callers */ }
350
- debug.log('store.sqlite', 'busy-lost', loss);
370
+ if (!sqliteBusy(error)) throw error;
371
+ emitInsertBusyLost({
372
+ store: 'logs', op: 'insertBatch', waitedMs: Date.now() - startedAt,
373
+ rows: allRows.length, categories: topCategories(allRows.map(({ row }) => row)),
374
+ });
375
+ const retained: LogStoreRetryBatch[] = [];
376
+ for (const batch of batches) {
377
+ const attempts = batch.attempts + 1;
378
+ if (attempts >= LOG_STORE_BUSY_RETRY_MAX_ATTEMPTS) {
379
+ emitInsertBusyLost({ store: 'logs', op: 'insertBatch', reason: 'retry-exhausted', droppedRows: batch.rows.length });
380
+ } else {
381
+ retained.push({ ...batch, attempts });
382
+ }
383
+ }
384
+ let bufferedRows = retained.reduce((count, batch) => count + batch.rows.length, 0);
385
+ let droppedRows = 0;
386
+ while (bufferedRows > LOG_STORE_BUSY_RETRY_BUFFER_MAX_ROWS && retained.length > 0) {
387
+ const oldest = retained[0]!;
388
+ const drop = Math.min(bufferedRows - LOG_STORE_BUSY_RETRY_BUFFER_MAX_ROWS, oldest.rows.length);
389
+ oldest.rows.splice(0, drop);
390
+ bufferedRows -= drop;
391
+ droppedRows += drop;
392
+ if (oldest.rows.length === 0) retained.shift();
393
+ }
394
+ this.busyRetryBuffer = retained;
395
+ if (droppedRows > 0) {
396
+ emitInsertBusyLost({ store: 'logs', op: 'insertBatch', reason: 'retry-buffer-full', droppedRows });
351
397
  }
352
398
  throw error;
353
399
  }
@@ -835,6 +881,7 @@ export class StoreSink implements LogSink {
835
881
  const batch = this.buf;
836
882
  this.buf = [];
837
883
  try {
884
+ // insertBatch retries SQLITE_BUSY batches on the next write; this sink remains fail-soft on final failure.
838
885
  this.store.insertBatch(batch);
839
886
  } catch {
840
887
  /* fail-soft — 파일 트레일이 진실원. 배치는 버린다(재시도 루프 금지 —
@@ -35,6 +35,18 @@ import {
35
35
  type MissionStatus,
36
36
  } from '../../task-orchestrator/mission.js';
37
37
  import type { Task } from '../../task-orchestrator/types.js';
38
+ import {
39
+ openSchedulesDb,
40
+ listSchedules,
41
+ scheduleHealth,
42
+ } from '../../domains/schedule-registry.js';
43
+ import {
44
+ cronRowsToDerived,
45
+ rollupDerived,
46
+ taskStatusToDerived,
47
+ type DerivedJob,
48
+ type MissionRollup,
49
+ } from '../../autopilot/mission-trace.js';
38
50
 
39
51
  import { checkAuth, type MetaApiOpts } from './meta-api.js';
40
52
  import { jsonResponse } from './http-server.js';
@@ -68,12 +80,63 @@ export interface MissionCardWire {
68
80
  * Mission. 자율 Mission 이면 실행모델·apm 상태·계보 키를 담아 통합 Missions
69
81
  * 표면이 autopilot 여부와 lineage 를 한눈에 렌더한다. */
70
82
  autopilot: MissionAutopilot | null;
83
+ /** 파생 작업 롤업(태스크 + 크론). ⛔ **결손 수리(MISSIONS-STALE-MARK · 2026-10-11)**:
84
+ * 이 칸이 없어 PWA 미션 카드의 `RollupChips` 는 항상 `undefined` 를 받아
85
+ * «아무것도» 안 그렸다 — 그래서 «파생 0»과 «파생 미측정»을 화면이 «구분 못 했다».
86
+ * 카드는 목록 엔드포인트만 보므로(미션별 `traceAutopilotMission` 을 안 부름)
87
+ * 여기서 «이미 로드한 태스크» ⊕ «요청당 한 번 읽은 schedule_registry」로
88
+ * 계산한다. 자율행동(surface_events)·자식 미션은 상세 트레이스
89
+ * (`ops_status action=mission`)의 몫으로 남는다(목록에서 N× 무거운 조인 금지).
90
+ * 매핑은 `rollupDerived`·`taskStatusToDerived`·`cronRowsToDerived` — «단일 소유»라
91
+ * 트레이스와 갈림이 없다. `total: 0` = «파생이 정말 0». */
92
+ derived: MissionRollup;
71
93
  createdAt: number;
72
94
  updatedAt: number;
73
95
  closedAt: number | null;
74
96
  }
75
97
 
76
- export function summarise(mission: Mission, tasks: readonly Task[]): MissionCardWire {
98
+ /** 태스크 행 → 파생물(순수). 트레이스의 `fetchTaskDerived` 와 같은 상태 매핑
99
+ * (`taskStatusToDerived`)을 쓴다 — «두 가지 상태 어휘»를 만들지 않는다. */
100
+ export function taskRowsToDerived(tasks: readonly Task[]): DerivedJob[] {
101
+ return tasks.map((t) => ({
102
+ kind: 'task',
103
+ id: t.id,
104
+ name: t.title,
105
+ status: taskStatusToDerived(t.status),
106
+ }));
107
+ }
108
+
109
+ /** 요청당 «한 번» schedule_registry 를 읽어 apmId 별 크론 파생물을 나눠 담는다.
110
+ * ⛔ 미션별로 DB 를 열면 목록 N건 = N× 열기 — 그래서 한 번 읽고 in-process 분배.
111
+ * fail-soft: 레지스트리가 없으면 빈 Map(derived 는 태스크만 집계). */
112
+ export function loadCronDerivedByMission(): Map<string, DerivedJob[]> {
113
+ const out = new Map<string, DerivedJob[]>();
114
+ try {
115
+ const db = openSchedulesDb();
116
+ try {
117
+ const rows = listSchedules(db);
118
+ const staleIds = new Set(scheduleHealth(rows).stale.map((s) => s.id));
119
+ const rowsByMission = new Map<string, typeof rows>();
120
+ for (const row of rows) {
121
+ const apmId = row.autopilot_id;
122
+ if (!apmId) continue;
123
+ const bucket = rowsByMission.get(apmId);
124
+ if (bucket) bucket.push(row);
125
+ else rowsByMission.set(apmId, [row]);
126
+ }
127
+ for (const [apmId, missionRows] of rowsByMission) {
128
+ out.set(apmId, cronRowsToDerived(missionRows, staleIds));
129
+ }
130
+ } finally { db.close(); }
131
+ } catch { /* fail-soft */ }
132
+ return out;
133
+ }
134
+
135
+ export function summarise(
136
+ mission: Mission,
137
+ tasks: readonly Task[],
138
+ crons: readonly DerivedJob[] = [],
139
+ ): MissionCardWire {
77
140
  const taskStatusCounts: Record<string, number> = {};
78
141
  const surfaceKindCounts: Record<string, number> = {};
79
142
  for (const t of tasks) {
@@ -92,6 +155,7 @@ export function summarise(mission: Mission, tasks: readonly Task[]): MissionCard
92
155
  taskStatusCounts,
93
156
  surfaceKindCounts,
94
157
  autopilot: mission.autopilot ?? null,
158
+ derived: rollupDerived([...taskRowsToDerived(tasks), ...crons]),
95
159
  createdAt: mission.createdAt,
96
160
  updatedAt: mission.updatedAt,
97
161
  closedAt: mission.closedAt ?? null,
@@ -115,8 +179,15 @@ export function handleMissionsList(
115
179
  const store = new TaskStore();
116
180
  try {
117
181
  const missions = store.listMissions(filter);
182
+ // 크론 레지스트리는 «요청당 한 번»만 읽고 apmId 로 분배(미션별 DB 열기 금지).
183
+ const cronsByMission = loadCronDerivedByMission();
118
184
  const cards = missions.map((m) =>
119
- summarise(m, store.listTasksForMission(m.id)),
185
+ summarise(
186
+ m,
187
+ store.listTasksForMission(m.id),
188
+ // schedule_registry.autopilot_id 는 계보 키(apmId)다 — TOX 미션 id 와 다를 수 있다.
189
+ cronsByMission.get(m.autopilot?.apmId ?? m.id) ?? cronsByMission.get(m.id) ?? [],
190
+ ),
120
191
  );
121
192
  return jsonResponse({
122
193
  total: cards.length,
@@ -9,6 +9,8 @@ import { jsonResponse } from './json-response.js';
9
9
  import { operatorSignal } from './operator.js';
10
10
  import { createSeatsCache, todayKst } from './ops-seats.js';
11
11
  import { liveSeatsSources } from './ops-seats-sources.js';
12
+ import { createOverviewCache } from '../../ops/overview-cache.js';
13
+ import { parseSince } from '../../ops/overview-collect.js';
12
14
  import type { MetaApiOpts } from './meta-api.js';
13
15
 
14
16
  const VERSION = /^\d+\.\d+\.\d+(?:-(?:rc|alpha|beta)\.\d+)?$/;
@@ -17,6 +19,11 @@ const DATE = /^\d{4}-\d{2}-\d{2}$/;
17
19
  let seatsCache: ReturnType<typeof createSeatsCache> | null = null;
18
20
  /** Tests swap the sources; production builds them lazily on first use. */
19
21
  export function setSeatsCacheForTest(cache: ReturnType<typeof createSeatsCache> | null): void { seatsCache = cache; }
22
+ let overviewCache: ReturnType<typeof createOverviewCache> | null = null;
23
+ export function setOverviewCacheForTest(cache: ReturnType<typeof createOverviewCache> | null): void {
24
+ overviewCache?.stop();
25
+ overviewCache = cache;
26
+ }
20
27
  const LOG_PATH = /^\/v1\/ops\/release\/runs\/([^/]+)\/nodes\/([^/]+)\/log$/;
21
28
 
22
29
  interface RunNode { nodeId: string; ok?: unknown; output?: unknown; startedAt?: unknown; endedAt?: unknown }
@@ -190,6 +197,17 @@ export function handleOpsApi(req: Request, metaApi: MetaApiOpts | undefined): Re
190
197
  response.headers.set('cache-control', 'no-store');
191
198
  return response;
192
199
  };
200
+ if (pathname === '/v1/ops/overview') {
201
+ let since: ReturnType<typeof parseSince>;
202
+ try { since = parseSince(url.searchParams.get('since') ?? undefined); }
203
+ catch { return jsonResponse({ error: 'invalid-since' }, 400); }
204
+ const version = url.searchParams.get('version');
205
+ if (version !== null && !VERSION.test(version)) return jsonResponse({ error: 'invalid-version' }, 400);
206
+ overviewCache ??= createOverviewCache();
207
+ return overviewCache.get({ sinceMs: since.ms, sinceLabel: since.label,
208
+ ...(version ? { version } : {}), ...(url.searchParams.has('owner') ? { owner: url.searchParams.get('owner')! } : {}) })
209
+ .then((overview) => served(jsonResponse(overview)), () => jsonResponse({ error: 'overview-unavailable' }, 500));
210
+ }
193
211
  if (pathname === '/v1/ops/seats') {
194
212
  const date = url.searchParams.get('date') ?? todayKst();
195
213
  if (!DATE.test(date) || Number.isNaN(Date.parse(`${date}T00:00:00+09:00`))) return jsonResponse({ error: 'invalid-date' }, 400);
@@ -4021,78 +4021,10 @@ function shouldRegisterWebterm(): boolean {
4021
4021
  // `boot/register-daemon.ts` and re-exported above (line 32) so existing
4022
4022
  // test imports (`from '../src/nexus/index.js'`) keep working.
4023
4023
 
4024
- /** Resolve the active daemon tool surface kind for runNexus.
4025
- *
4026
- * Resolution order (parity with `deriveChildEnv` for daemon tab spawn —
4027
- * the same switch fed into a daemon child env should govern the in-
4028
- * process path too):
4029
- *
4030
- * 1. CLI flag (`opts.tools`) ← 1회 boot
4031
- * 2. `ELANOUS_TOOLS` env ← shell session
4032
- * 3. UserConfig switch `global.tools` (PWA Settings 토글이 여기) ← 영구
4033
- * 4. switch default = `'webterm'` (`builtins/global.ts:12`) ← fallback
4034
- *
4035
- * Pre-fix the in-process branch fell back to `'none'` while
4036
- * `deriveChildEnv` fell back to the switch default `'webterm'`. That
4037
- * meant a clean-machine `elanous nexus` (no flag/env) gave the in-process
4038
- * LLM call an empty tool catalog while a separately-launched
4039
- * `elanous serve` daemon honored the switch default's full surface —
4040
- * surprising divergence the user had no way to discover.
4041
- *
4042
- * `'all'` is in the switch enum but has no `toolSurface()` impl yet;
4043
- * fall back to `'webterm'` (the most-capable wired surface). */
4044
- const daemonToolSurfaceKinds: Record<DaemonToolSurfaceKind, true> = {
4045
- none: true,
4046
- readonly: true,
4047
- chat: true,
4048
- webterm: true,
4049
- };
4050
-
4051
- function resolveKnownToolsKind(raw: string): DaemonToolSurfaceKind | undefined {
4052
- return Object.hasOwn(daemonToolSurfaceKinds, raw)
4053
- ? raw as DaemonToolSurfaceKind
4054
- : undefined;
4055
- }
4056
-
4057
- export function resolveToolsKind(opts: { tools?: string }): DaemonToolSurfaceKind {
4058
- let raw: string | undefined = opts.tools;
4059
- if (!raw) raw = process.env.ELANOUS_TOOLS?.trim();
4060
- if (!raw) {
4061
- try {
4062
- const cfg = readUserConfig();
4063
- const v = readSwitchValue(cfg, 'global.tools');
4064
- if (typeof v === 'string') raw = v;
4065
- } catch { /* swallow — boot must not fail on a malformed config */ }
4066
- }
4067
- if (!raw) raw = 'webterm';
4068
- return resolveKnownToolsKind(raw) ?? 'webterm';
4069
- }
4070
-
4071
- /** Resolve the `:agent` (통합 모드 webterm dock) tool surface kind.
4072
- *
4073
- * Distinct from {@link resolveToolsKind} because the integrated
4074
- * webterm-dock chat is **defined by** its PTY surface — WebTerminal*
4075
- * tools (Snapshot · Input · Screenshot) are what make `:agent`
4076
- * meaningful next to a live terminal. If the user has narrowed their
4077
- * user-config (`global.tools chat` / `readonly`) to keep the
4078
- * standalone `/chat` lighter, that preference must NOT bleed into the
4079
- * integrated dock — otherwise the `:agent` LLM loses access to the
4080
- * very terminal it's supposed to drive.
4081
- *
4082
- * Resolution order:
4083
- * 1. CLI flag (`opts.tools`) ← explicit operator intent wins
4084
- * 2. **`'webterm'`** (hardcoded) ← env + global.tools intentionally
4085
- * skipped vs. resolveToolsKind
4086
- *
4087
- * CLI flag is honored so a developer debugging tool selection
4088
- * (`elanous nexus run --tools readonly`) still sees a readonly `:agent`;
4089
- * but a clean-machine boot or a config-narrowed boot always gets
4090
- * webterm for the integrated dock. */
4091
- export function resolveAgentTurnToolsKind(opts: { tools?: string }): DaemonToolSurfaceKind {
4092
- const raw = opts.tools?.trim();
4093
- if (!raw) return 'webterm';
4094
- // 'all' (or any unrecognized string) → webterm fallback, same as
4095
- // resolveToolsKind so the two resolvers stay consistent for valid
4096
- // CLI input.
4097
- return resolveKnownToolsKind(raw) ?? 'webterm';
4098
- }
4024
+ // C-2a (same pattern as `shouldRegisterDaemon` above): the tool-surface
4025
+ // resolution block (`daemonToolSurfaceKinds` · `resolveKnownToolsKind` ·
4026
+ // `resolveToolsKind` · `resolveAgentTurnToolsKind`) moved to
4027
+ // `nexus/tools-kind.ts` and re-exported here so existing test imports
4028
+ // (`from '../src/nexus/index.js'`) keep working.
4029
+ import { resolveAgentTurnToolsKind, resolveToolsKind } from './tools-kind.js';
4030
+ export { resolveToolsKind, resolveAgentTurnToolsKind } from './tools-kind.js';
@@ -0,0 +1,78 @@
1
+ import { readUserConfig, readSwitchValue } from './config/user-config.js';
2
+ import type { DaemonToolSurfaceKind } from '../boot/daemon-tools/types.js';
3
+
4
+ /** Resolve the active daemon tool surface kind for runNexus.
5
+ *
6
+ * Resolution order (parity with `deriveChildEnv` for daemon tab spawn —
7
+ * the same switch fed into a daemon child env should govern the in-
8
+ * process path too):
9
+ *
10
+ * 1. CLI flag (`opts.tools`) ← 1회 boot
11
+ * 2. `ELANOUS_TOOLS` env ← shell session
12
+ * 3. UserConfig switch `global.tools` (PWA Settings 토글이 여기) ← 영구
13
+ * 4. switch default = `'webterm'` (`builtins/global.ts:12`) ← fallback
14
+ *
15
+ * Pre-fix the in-process branch fell back to `'none'` while
16
+ * `deriveChildEnv` fell back to the switch default `'webterm'`. That
17
+ * meant a clean-machine `elanous nexus` (no flag/env) gave the in-process
18
+ * LLM call an empty tool catalog while a separately-launched
19
+ * `elanous serve` daemon honored the switch default's full surface —
20
+ * surprising divergence the user had no way to discover.
21
+ *
22
+ * `'all'` is in the switch enum but has no `toolSurface()` impl yet;
23
+ * fall back to `'webterm'` (the most-capable wired surface). */
24
+ const daemonToolSurfaceKinds: Record<DaemonToolSurfaceKind, true> = {
25
+ none: true,
26
+ readonly: true,
27
+ chat: true,
28
+ webterm: true,
29
+ };
30
+
31
+ function resolveKnownToolsKind(raw: string): DaemonToolSurfaceKind | undefined {
32
+ return Object.hasOwn(daemonToolSurfaceKinds, raw)
33
+ ? raw as DaemonToolSurfaceKind
34
+ : undefined;
35
+ }
36
+
37
+ export function resolveToolsKind(opts: { tools?: string }): DaemonToolSurfaceKind {
38
+ let raw: string | undefined = opts.tools;
39
+ if (!raw) raw = process.env.ELANOUS_TOOLS?.trim();
40
+ if (!raw) {
41
+ try {
42
+ const cfg = readUserConfig();
43
+ const v = readSwitchValue(cfg, 'global.tools');
44
+ if (typeof v === 'string') raw = v;
45
+ } catch { /* swallow — boot must not fail on a malformed config */ }
46
+ }
47
+ if (!raw) raw = 'webterm';
48
+ return resolveKnownToolsKind(raw) ?? 'webterm';
49
+ }
50
+
51
+ /** Resolve the `:agent` (통합 모드 webterm dock) tool surface kind.
52
+ *
53
+ * Distinct from {@link resolveToolsKind} because the integrated
54
+ * webterm-dock chat is **defined by** its PTY surface — WebTerminal*
55
+ * tools (Snapshot · Input · Screenshot) are what make `:agent`
56
+ * meaningful next to a live terminal. If the user has narrowed their
57
+ * user-config (`global.tools chat` / `readonly`) to keep the
58
+ * standalone `/chat` lighter, that preference must NOT bleed into the
59
+ * integrated dock — otherwise the `:agent` LLM loses access to the
60
+ * very terminal it's supposed to drive.
61
+ *
62
+ * Resolution order:
63
+ * 1. CLI flag (`opts.tools`) ← explicit operator intent wins
64
+ * 2. **`'webterm'`** (hardcoded) ← env + global.tools intentionally
65
+ * skipped vs. resolveToolsKind
66
+ *
67
+ * CLI flag is honored so a developer debugging tool selection
68
+ * (`elanous nexus run --tools readonly`) still sees a readonly `:agent`;
69
+ * but a clean-machine boot or a config-narrowed boot always gets
70
+ * webterm for the integrated dock. */
71
+ export function resolveAgentTurnToolsKind(opts: { tools?: string }): DaemonToolSurfaceKind {
72
+ const raw = opts.tools?.trim();
73
+ if (!raw) return 'webterm';
74
+ // 'all' (or any unrecognized string) → webterm fallback, same as
75
+ // resolveToolsKind so the two resolvers stay consistent for valid
76
+ // CLI input.
77
+ return resolveKnownToolsKind(raw) ?? 'webterm';
78
+ }
@@ -29,6 +29,7 @@ export interface RotationCandidate {
29
29
  /** 선불 크레딧 잔액·보유(모르면 없다). 크레딧 정책에서 «어느 계정 크레딧으로» 갈지 고른다. */
30
30
  readonly creditBalance?: number;
31
31
  readonly hasCredits?: boolean;
32
+ readonly resetCreditAvailability?: 'available' | 'unavailable' | 'unknown';
32
33
  }
33
34
 
34
35
  /** ⛔ 비공개 — 밖에서 이름으로 부를 소비처가 없다(리뷰 must-fix: dead export 금지). */
@@ -69,6 +70,10 @@ interface RotationDecision {
69
70
  }[];
70
71
  /** 넘겼을 때만 있다. */
71
72
  readonly to?: RotationCandidate;
73
+ /** 다른 계정의 리셋권이 이 판정을 연 경우의 관측용 이름. 소비 대상은 소비기가 별도로 고른다. */
74
+ readonly resetCreditAccount?: string;
75
+ /** SUBSCRIPTION-EXHAUST-REROUTE — 크레딧 예비분에 닿아 `no-candidate` 로 넘겼을 때만 있다(잔액은 «알려진» 값). */
76
+ readonly creditReserveHit?: { readonly balance: number; readonly reserve: number };
72
77
  }
73
78
 
74
79
  /** ⛔ 비공개 — 호출자는 객체 리터럴로 준다(구조적 타이핑). */
@@ -104,6 +109,9 @@ interface RotationInput {
104
109
  readonly currentCreditBalance?: number;
105
110
  readonly currentHasCredits?: boolean;
106
111
  readonly creditPace?: { active: boolean };
112
+ /** SUBSCRIPTION-EXHAUST-REROUTE (`llm.codexCreditReserve`) — 크레딧 잔액이 이 값 «이하»면 크레딧을 쓰지 않는다.
113
+ * ⛔ 없으면 종전(잔액 > 0 이면 쓴다). 잔액을 «모르면» 이 값으로 막지 않는다(fail-open). */
114
+ readonly creditReserve?: number;
107
115
  }
108
116
 
109
117
  /**
@@ -215,6 +223,10 @@ export function decideCodexRotation(input: RotationInput): RotationDecision {
215
223
  const to = usable[0];
216
224
  if (!to) {
217
225
  if (input.resetCreditAvailability === 'available') return { reason: 'reset-credit-available', candidateCount, accountThresholds };
226
+ const resetCreditAccount = otherCandidates
227
+ .filter((candidate) => candidate.home.trim() && candidate.resetCreditAvailability === 'available')
228
+ .sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)[0]?.name;
229
+ if (resetCreditAccount) return { reason: 'reset-credit-available', candidateCount, accountThresholds, resetCreditAccount };
218
230
  if (input.creditPace?.active) {
219
231
  const self = input.candidates.find((candidate) => candidate.name === input.current.name);
220
232
  const eligible = (name: string, balance: number | undefined, hasCredits: boolean | undefined, home: string | undefined): boolean =>
@@ -235,18 +247,27 @@ export function decideCodexRotation(input: RotationInput): RotationDecision {
235
247
  // 한 계정 크레딧이 바닥나면 남은 크레딧을 두고 폴백(grok)으로 갔다. ⇒ 크레딧이 «더 많이» 남은 계정으로 옮긴다.
236
248
  // 흔들림 방지: 지금 계정 크레딧이 없거나(보유 false · 잔액 ≤0) 다른 계정이 20% 넘게 많을 때만.
237
249
  if (input.creditsAllowed === true) {
250
+ // SUBSCRIPTION-EXHAUST-REROUTE — 예비분이 있으면 «예비분 초과»만 쓸 크레딧이다(없으면 종전 > 0).
251
+ const reserve = typeof input.creditReserve === 'number' && Number.isFinite(input.creditReserve) && input.creditReserve >= 0
252
+ ? input.creditReserve : undefined;
253
+ const floor = reserve ?? 0;
238
254
  const richest = otherCandidates
239
- .filter((c) => c.home.length > 0 && c.hasCredits !== false && typeof c.creditBalance === 'number' && c.creditBalance > 0)
255
+ .filter((c) => c.home.length > 0 && c.hasCredits !== false && typeof c.creditBalance === 'number' && c.creditBalance > floor)
240
256
  .sort((a, b) => (b.creditBalance! - a.creditBalance!) || (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))[0];
241
257
  // 지금 계정 신호를 귀속할 수 없을 때(기본 계정 `source=default`)는 후보 목록의 같은 이름 줄을 쓴다 —
242
258
  // 후보는 같은 홈 해석으로 신호를 읽었다(09-28 운영: default 가 «사용=? · 찼나=모름»이라 잔액을 못 봐 머물렀다).
243
259
  const self = input.candidates.find((c) => c.name === input.current.name);
244
260
  const cur = input.currentCreditBalance ?? self?.creditBalance;
245
261
  const curHas = input.currentHasCredits ?? self?.hasCredits;
246
- const currentDry = curHas === false || (typeof cur === 'number' && cur <= 0);
262
+ const currentDry = curHas === false || (typeof cur === 'number' && cur <= floor);
247
263
  if (richest && (currentDry || (typeof cur === 'number' && richest.creditBalance! > cur * 1.2))) {
248
264
  return { reason: 'rotated', candidateCount, accountThresholds, to: richest };
249
265
  }
266
+ // 🩸 10-11: 크레딧이 0 이 돼도 여기 머물러 codex 로 계속 쐈다. 잔액이 «알려져 있고» 예비분 이하이며
267
+ // 예비분을 넘는 다른 계정도 없으면 «갈 곳 없음» — 폴백 체인이 다음 칸을 정한다. 모르면 종전대로 머문다.
268
+ if (reserve !== undefined && typeof cur === 'number' && Number.isFinite(cur) && cur <= reserve) {
269
+ return { reason: 'no-candidate', candidateCount, accountThresholds, creditReserveHit: { balance: cur, reserve } };
270
+ }
250
271
  return { reason: 'credits-allowed', candidateCount, accountThresholds };
251
272
  }
252
273
  return { reason: 'no-candidate', candidateCount, accountThresholds };
@@ -255,6 +276,21 @@ export function decideCodexRotation(input: RotationInput): RotationDecision {
255
276
  return { reason: 'rotated', candidateCount, accountThresholds, to };
256
277
  }
257
278
 
279
+ /** SUBSCRIPTION-EXHAUST-REROUTE — 크레딧 예비분 기본(`llm.codexCreditReserve` 가 없을 때 · 근거는 codex-account-store 주석).
280
+ * 여기 두는 이유: Pod 계획(pod-account-broker)이 store 를 끌어오지 않고 같은 값을 쓴다. */
281
+ export const CODEX_CREDIT_RESERVE_DEFAULT = 2000;
282
+
283
+ /** CODEX-CREDITS-STEP — 크레딧 잔액이 «확인된» 후보(잔액 > 예비분 · 보유 ≠ false · 홈을 안다)를 잔액 많은 순으로.
284
+ * 잔액을 모르면 넣지 않는다(돈 · fail-closed). 회전 수집기와 Pod 계획이 «같은 자»를 쓴다. */
285
+ export function creditCandidatesByBalance(candidates: readonly RotationCandidate[], reserve = 0): RotationCandidate[] {
286
+ // SUBSCRIPTION-EXHAUST-REROUTE — 잔액이 예비분(`llm.codexCreditReserve`) «초과»인 계정만(0 = 종전 > 0).
287
+ const floor = Number.isFinite(reserve) && reserve >= 0 ? reserve : 0;
288
+ return candidates
289
+ .filter((c) => c.home.length > 0 && c.hasCredits !== false
290
+ && typeof c.creditBalance === 'number' && Number.isFinite(c.creditBalance) && c.creditBalance > floor)
291
+ .sort((a, b) => (b.creditBalance! - a.creditBalance!) || (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
292
+ }
293
+
258
294
  /**
259
295
  * ⛔⭐⭐⭐⭐⭐ **회전한 계정을 «자식 env»로 내려보낼 덧칠** (2026-08-07 · 크레딧 유출 차단).
260
296
  *
@@ -285,6 +321,7 @@ export function observeRotation(decision: RotationDecision, from: string): void
285
321
  from,
286
322
  to: decision.to?.name,
287
323
  reason: decision.reason,
324
+ ...(decision.resetCreditAccount ? { resetCreditAccount: decision.resetCreditAccount } : {}),
288
325
  candidateCount: decision.candidateCount ?? 'unknown',
289
326
  ...(decision.reason === 'disabled' && decision.disabledProvenance
290
327
  ? { disabledProvenance: decision.disabledProvenance }