rovecode 0.4.0-beta.2 → 0.4.0

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 (428) hide show
  1. package/README.md +67 -69
  2. package/THIRD_PARTY_NOTICES.md +0 -44
  3. package/bin/rovecode.ts +21 -0
  4. package/package.json +16 -37
  5. package/src/account/keys.ts +97 -0
  6. package/src/account/login.ts +158 -0
  7. package/src/account/provision.ts +47 -0
  8. package/src/account/store.ts +63 -0
  9. package/src/acp/server.ts +373 -0
  10. package/src/cli/account-cmd.ts +116 -0
  11. package/src/cli/connect.ts +244 -0
  12. package/src/cli/context-cmd.ts +199 -0
  13. package/src/cli/dispatch.ts +109 -0
  14. package/src/cli/doctor.ts +324 -0
  15. package/src/cli/export.ts +278 -0
  16. package/src/cli/help.ts +240 -0
  17. package/src/cli/is-tui-invocation.ts +8 -0
  18. package/src/cli/main.ts +599 -0
  19. package/src/cli/market-cmd.ts +658 -0
  20. package/src/cli/mcp-market-cmd.ts +299 -0
  21. package/src/cli/output.ts +382 -0
  22. package/src/cli/repl.ts +172 -0
  23. package/src/cli/resume.ts +32 -0
  24. package/src/cli/run-limits.ts +78 -0
  25. package/src/cli/runtime.ts +792 -0
  26. package/src/cli/setup.ts +187 -0
  27. package/src/cli/update-cmd.ts +78 -0
  28. package/src/cli/workflow-cmd.ts +100 -0
  29. package/src/coding/checkpoints.ts +270 -0
  30. package/src/coding/diff.ts +136 -0
  31. package/src/coding/files.ts +339 -0
  32. package/src/coding/hashline.ts +319 -0
  33. package/src/coding/lsp.ts +406 -0
  34. package/src/coding/repomap-cache.ts +99 -0
  35. package/src/coding/repomap-files.ts +110 -0
  36. package/src/coding/repomap.ts +392 -0
  37. package/src/core/compaction.ts +399 -0
  38. package/src/core/config.ts +289 -0
  39. package/src/core/context-report.ts +228 -0
  40. package/src/core/context.ts +60 -0
  41. package/src/core/count-remote.ts +107 -0
  42. package/src/core/execpolicy-rules.ts +196 -0
  43. package/src/core/execpolicy.ts +385 -0
  44. package/src/core/executor.ts +397 -0
  45. package/src/core/guardrails.ts +400 -0
  46. package/src/core/hooks.ts +398 -0
  47. package/src/core/images.ts +230 -0
  48. package/src/core/intro.ts +236 -0
  49. package/src/core/loop.ts +621 -0
  50. package/src/core/modes.ts +372 -0
  51. package/src/core/orchestrator.ts +207 -0
  52. package/src/core/reflection.ts +165 -0
  53. package/src/core/sandbox-config.ts +167 -0
  54. package/src/core/session-images.ts +73 -0
  55. package/src/core/session.ts +398 -0
  56. package/src/core/settings.ts +98 -0
  57. package/src/core/stuck-detector.ts +273 -0
  58. package/src/core/tasks.ts +374 -0
  59. package/src/core/token-scale.ts +108 -0
  60. package/src/core/tool-output-budget.ts +166 -0
  61. package/src/core/tools.ts +288 -0
  62. package/src/core/types.ts +330 -0
  63. package/src/core/update-check.ts +171 -0
  64. package/src/core/update.ts +158 -0
  65. package/src/core/usage.ts +204 -0
  66. package/src/core/validate.ts +121 -0
  67. package/src/core/verify-gate.ts +159 -0
  68. package/src/core/verify.ts +237 -0
  69. package/src/core/voice.ts +158 -0
  70. package/src/core/win-job.ts +183 -0
  71. package/src/design/audit.ts +797 -0
  72. package/src/design/direction.ts +190 -0
  73. package/src/design/rules.ts +157 -0
  74. package/src/eval/bench.ts +150 -0
  75. package/src/eval/gauntlet-runner.ts +218 -0
  76. package/src/eval/gauntlet.ts +226 -0
  77. package/src/eval/grader.ts +186 -0
  78. package/src/eval/record.ts +202 -0
  79. package/src/eval/redact.ts +141 -0
  80. package/src/eval/replay.ts +147 -0
  81. package/src/eval/trajectory.ts +373 -0
  82. package/src/index.ts +17 -0
  83. package/src/market/catalogs/mcp-docs.json +111 -0
  84. package/src/market/catalogs/plugins.json +111 -0
  85. package/src/market/catalogs/skills.json +478 -0
  86. package/src/market/clone.ts +72 -0
  87. package/src/market/context-cost.ts +121 -0
  88. package/src/market/digest.ts +106 -0
  89. package/src/market/index.ts +22 -0
  90. package/src/market/install.ts +578 -0
  91. package/src/market/manifest.ts +187 -0
  92. package/src/market/prereq.ts +145 -0
  93. package/src/market/registry.ts +363 -0
  94. package/src/market/resolve.ts +111 -0
  95. package/src/market/types.ts +236 -0
  96. package/src/market/validate.ts +227 -0
  97. package/src/mcp/client.ts +431 -0
  98. package/src/mcp/config.ts +239 -0
  99. package/src/mcp/local-package.ts +211 -0
  100. package/src/mcp/market-catalog.ts +84 -0
  101. package/src/mcp/market-install.ts +289 -0
  102. package/src/mcp/market.ts +0 -0
  103. package/src/mcp/tools.ts +131 -0
  104. package/src/mcp/trust.ts +49 -0
  105. package/src/memory/blocks.ts +175 -0
  106. package/src/memory/recall.ts +355 -0
  107. package/src/memory/store.ts +105 -0
  108. package/src/memory/tools.ts +99 -0
  109. package/src/plugins/cli.ts +123 -0
  110. package/src/plugins/discover.ts +108 -0
  111. package/src/plugins/index.ts +50 -0
  112. package/src/plugins/init.ts +140 -0
  113. package/src/plugins/install.ts +184 -0
  114. package/src/plugins/load.ts +149 -0
  115. package/src/plugins/manifest.ts +106 -0
  116. package/src/plugins/state.ts +83 -0
  117. package/src/providers/auth.ts +293 -0
  118. package/src/providers/cache.ts +223 -0
  119. package/src/providers/catalog-local.ts +160 -0
  120. package/src/providers/catalog.ts +408 -0
  121. package/src/providers/middleware-context.ts +86 -0
  122. package/src/providers/middleware.ts +373 -0
  123. package/src/providers/profile-glm53.ts +111 -0
  124. package/src/providers/profile-sonnet5-persona.ts +65 -0
  125. package/src/providers/profile-sonnet5-voice.ts +23 -0
  126. package/src/providers/profiles.ts +156 -0
  127. package/src/providers/provider-config.ts +311 -0
  128. package/src/providers/registry.ts +302 -0
  129. package/src/providers/response-validation.ts +80 -0
  130. package/src/providers/retry.ts +234 -0
  131. package/src/providers/router.ts +294 -0
  132. package/src/providers/sse.ts +26 -0
  133. package/src/providers/stream-errors.ts +117 -0
  134. package/src/providers/stream.ts +569 -0
  135. package/src/providers/thinking.ts +189 -0
  136. package/src/providers/wire-messages.ts +129 -0
  137. package/src/sdk/client.ts +225 -0
  138. package/src/sdk/index.ts +3 -0
  139. package/src/server/dashboard.ts +144 -0
  140. package/src/server/http.ts +343 -0
  141. package/src/server/openapi.ts +246 -0
  142. package/src/sextant/card-hits.ts +102 -0
  143. package/src/sextant/card-keys.ts +55 -0
  144. package/src/sextant/context-source.ts +157 -0
  145. package/src/sextant/draw-agents.ts +273 -0
  146. package/src/sextant/draw-code.ts +388 -0
  147. package/src/sextant/draw-context.ts +222 -0
  148. package/src/sextant/draw-frame.ts +164 -0
  149. package/src/sextant/draw-market.ts +573 -0
  150. package/src/sextant/draw-messages.ts +386 -0
  151. package/src/sextant/draw-pet.ts +230 -0
  152. package/src/sextant/draw-plan.ts +159 -0
  153. package/src/sextant/draw-tabs.ts +85 -0
  154. package/src/sextant/draw-util.ts +65 -0
  155. package/src/sextant/engine.ts +230 -0
  156. package/src/sextant/frame-hits.ts +25 -0
  157. package/src/sextant/frame.ts +101 -0
  158. package/src/sextant/git-status.ts +197 -0
  159. package/src/sextant/grid.ts +59 -0
  160. package/src/sextant/input.ts +119 -0
  161. package/src/sextant/keys.ts +488 -0
  162. package/src/sextant/layout.ts +86 -0
  163. package/src/sextant/local-commands.ts +156 -0
  164. package/src/sextant/market-source.ts +287 -0
  165. package/src/sextant/mentions.ts +141 -0
  166. package/src/sextant/message-hits.ts +26 -0
  167. package/src/sextant/model.ts +387 -0
  168. package/src/sextant/overlays.ts +451 -0
  169. package/src/sextant/panel-hits.ts +38 -0
  170. package/src/sextant/pet.ts +399 -0
  171. package/src/sextant/screen.ts +324 -0
  172. package/src/sextant/scroll-hits.ts +66 -0
  173. package/src/sextant/scrollbar.ts +82 -0
  174. package/src/sextant/selection.ts +123 -0
  175. package/src/sextant/sextant-bridge.ts +174 -0
  176. package/src/sextant/sextant-cards.ts +142 -0
  177. package/src/sextant/sextant-diff-base.ts +63 -0
  178. package/src/sextant/sextant-files.ts +154 -0
  179. package/src/sextant/sextant-frame-loop.ts +314 -0
  180. package/src/sextant/sextant-renderer.ts +478 -0
  181. package/src/sextant/sextant-repo.ts +131 -0
  182. package/src/sextant/theme.ts +66 -0
  183. package/src/sextant/tool-rows.ts +189 -0
  184. package/src/sextant/types.ts +473 -0
  185. package/src/skills/index.ts +306 -0
  186. package/src/skills/tools.ts +69 -0
  187. package/src/skills/versioned.ts +227 -0
  188. package/src/telemetry/otel.ts +353 -0
  189. package/src/telemetry/otlp.ts +68 -0
  190. package/src/tools/ask-user.ts +156 -0
  191. package/src/tools/design.ts +151 -0
  192. package/src/tools/evalcell.ts +338 -0
  193. package/src/tools/html-text.ts +139 -0
  194. package/src/tools/provider.ts +149 -0
  195. package/src/tools/task.ts +216 -0
  196. package/src/tools/todo.ts +320 -0
  197. package/src/tools/webfetch.ts +331 -0
  198. package/src/tui/app.ts +608 -0
  199. package/src/tui/attach.ts +127 -0
  200. package/src/tui/checkpoints-cmd.ts +70 -0
  201. package/src/tui/clipboard-image.ts +81 -0
  202. package/src/tui/commands.ts +277 -0
  203. package/src/tui/cost.ts +108 -0
  204. package/src/tui/info-cmd.ts +144 -0
  205. package/src/tui/mcp-cmd.ts +128 -0
  206. package/src/tui/modes-cmd.ts +45 -0
  207. package/src/tui/overlays.ts +97 -0
  208. package/src/tui/pi-renderer.ts +424 -0
  209. package/src/tui/providers-cmd.ts +366 -0
  210. package/src/tui/renderer.ts +101 -0
  211. package/src/tui/replay-marker.ts +29 -0
  212. package/src/tui/session-cmd.ts +146 -0
  213. package/src/tui/sextant-attach.ts +68 -0
  214. package/src/tui/sextant-io.ts +184 -0
  215. package/src/tui/sextant-smoke.ts +110 -0
  216. package/src/tui/smoke.ts +72 -0
  217. package/src/tui/theme.ts +59 -0
  218. package/src/tui/todo-label.ts +7 -0
  219. package/src/workflow/engine.ts +266 -0
  220. package/tsconfig.json +30 -0
  221. package/vendor/pi-tui/LICENSE +21 -0
  222. package/vendor/pi-tui/PATCHES.md +12 -0
  223. package/vendor/pi-tui/PROVENANCE.md +12 -0
  224. package/vendor/pi-tui/README.upstream.md +854 -0
  225. package/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-console-mode.node +0 -0
  226. package/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-console-mode.node +0 -0
  227. package/vendor/pi-tui/src/alt-screen-search.ts +158 -0
  228. package/vendor/pi-tui/src/autocomplete.ts +827 -0
  229. package/vendor/pi-tui/src/components/alt-screen-flash.ts +52 -0
  230. package/vendor/pi-tui/src/components/box.ts +138 -0
  231. package/vendor/pi-tui/src/components/cancellable-loader.ts +41 -0
  232. package/vendor/pi-tui/src/components/editor.ts +2364 -0
  233. package/vendor/pi-tui/src/components/h-stack.ts +45 -0
  234. package/vendor/pi-tui/src/components/image.ts +128 -0
  235. package/vendor/pi-tui/src/components/input.ts +448 -0
  236. package/vendor/pi-tui/src/components/loader.ts +93 -0
  237. package/vendor/pi-tui/src/components/markdown.ts +1016 -0
  238. package/vendor/pi-tui/src/components/scroll-view.ts +217 -0
  239. package/vendor/pi-tui/src/components/select-list.ts +230 -0
  240. package/vendor/pi-tui/src/components/settings-list.ts +277 -0
  241. package/vendor/pi-tui/src/components/spacer.ts +29 -0
  242. package/vendor/pi-tui/src/components/stack.ts +155 -0
  243. package/vendor/pi-tui/src/components/text.ts +108 -0
  244. package/vendor/pi-tui/src/components/truncated-text.ts +66 -0
  245. package/vendor/pi-tui/src/components/v-stack.ts +34 -0
  246. package/vendor/pi-tui/src/editor-component.ts +75 -0
  247. package/vendor/pi-tui/src/fuzzy.ts +138 -0
  248. package/vendor/pi-tui/src/index.ts +149 -0
  249. package/vendor/pi-tui/src/keybindings.ts +321 -0
  250. package/vendor/pi-tui/src/keys.ts +1402 -0
  251. package/vendor/pi-tui/src/kill-ring.ts +47 -0
  252. package/vendor/pi-tui/src/latex.ts +1381 -0
  253. package/vendor/pi-tui/src/layout-node.ts +52 -0
  254. package/vendor/pi-tui/src/layout.ts +411 -0
  255. package/vendor/pi-tui/src/native-modifiers.ts +60 -0
  256. package/vendor/pi-tui/src/native-module-path.ts +32 -0
  257. package/vendor/pi-tui/src/stdin-buffer.ts +445 -0
  258. package/vendor/pi-tui/src/terminal-colors.ts +74 -0
  259. package/vendor/pi-tui/src/terminal-image.ts +701 -0
  260. package/vendor/pi-tui/src/terminal.ts +554 -0
  261. package/vendor/pi-tui/src/tui-alt-screen.ts +1379 -0
  262. package/vendor/pi-tui/src/tui-main-screen.ts +655 -0
  263. package/vendor/pi-tui/src/tui.ts +1264 -0
  264. package/vendor/pi-tui/src/undo-stack.ts +29 -0
  265. package/vendor/pi-tui/src/utils.ts +1327 -0
  266. package/vendor/pi-tui/src/word-navigation.ts +118 -0
  267. package/vendor/pi-tui/test/test-themes.ts +39 -0
  268. package/vendor/pi-tui/test/virtual-terminal.ts +219 -0
  269. package/CHANGELOG.md +0 -512
  270. package/bin/rovecode.js +0 -24
  271. package/dist/cli/app-dybnr56b.js +0 -2
  272. package/dist/cli/ask-user-p8hq4xgj.js +0 -2
  273. package/dist/cli/auth-login-ewpgw5sm.js +0 -2
  274. package/dist/cli/auth-m8p9grty.js +0 -2
  275. package/dist/cli/bench-xv3ypwev.js +0 -9
  276. package/dist/cli/catalog-737wb2s0.js +0 -2
  277. package/dist/cli/cli-arhg40m0.js +0 -2
  278. package/dist/cli/client-cf2pxx8q.js +0 -2
  279. package/dist/cli/commands-3p7e4xxs.js +0 -2
  280. package/dist/cli/connect-3q93d7cb.js +0 -2
  281. package/dist/cli/context-cmd-eqxmxhzq.js +0 -2
  282. package/dist/cli/context-report-hbw9zfes.js +0 -2
  283. package/dist/cli/count-remote-mby98cd0.js +0 -2
  284. package/dist/cli/design-122y0axd.js +0 -2
  285. package/dist/cli/dispatch-b4egzvvh.js +0 -2
  286. package/dist/cli/doctor-x4jkv72e.js +0 -3
  287. package/dist/cli/executor-ftvg6tsy.js +0 -2
  288. package/dist/cli/export-pdgdhkch.js +0 -2
  289. package/dist/cli/files-cez9a96p.js +0 -2
  290. package/dist/cli/gauntlet-r3xxaszc.js +0 -2
  291. package/dist/cli/gauntlet-runner-r515m7kk.js +0 -10
  292. package/dist/cli/gauntlet-wave3-bnkjk2v2.js +0 -5
  293. package/dist/cli/gauntlet-wave4-acs9s60q.js +0 -14
  294. package/dist/cli/hashline-ewg5hbe3.js +0 -2
  295. package/dist/cli/http-n0kehsk8.js +0 -5
  296. package/dist/cli/index-z5qt1s76.js +0 -2
  297. package/dist/cli/install-80mp63kx.js +0 -2
  298. package/dist/cli/loop-12twjcat.js +0 -2
  299. package/dist/cli/main-01pv9206.js +0 -4
  300. package/dist/cli/main-0jys2ccn.js +0 -3
  301. package/dist/cli/main-1ztz6fkj.js +0 -10
  302. package/dist/cli/main-23q7cmww.js +0 -9
  303. package/dist/cli/main-2rzbexn2.js +0 -3
  304. package/dist/cli/main-2wyax8k9.js +0 -9
  305. package/dist/cli/main-2yeveeve.js +0 -6
  306. package/dist/cli/main-2z3dek0b.js +0 -3
  307. package/dist/cli/main-2zgsknth.js +0 -3
  308. package/dist/cli/main-45ejth3a.js +0 -4
  309. package/dist/cli/main-45rn3trk.js +0 -22
  310. package/dist/cli/main-4p4e2w7x.js +0 -4
  311. package/dist/cli/main-4y0tnfpa.js +0 -16
  312. package/dist/cli/main-5py0rkmc.js +0 -4
  313. package/dist/cli/main-6dtqmbt6.js +0 -7
  314. package/dist/cli/main-6h9x282m.js +0 -4
  315. package/dist/cli/main-6vjeds42.js +0 -3
  316. package/dist/cli/main-78gq4bt9.js +0 -6
  317. package/dist/cli/main-7jd5vh3x.js +0 -4
  318. package/dist/cli/main-7kt6r53y.js +0 -4
  319. package/dist/cli/main-8c1tbazx.js +0 -58
  320. package/dist/cli/main-9a9rnh47.js +0 -19
  321. package/dist/cli/main-9ht36z12.js +0 -3
  322. package/dist/cli/main-a2yfvcy9.js +0 -7
  323. package/dist/cli/main-a3f51n0x.js +0 -5
  324. package/dist/cli/main-b8zq261k.js +0 -3
  325. package/dist/cli/main-bxtvnf6d.js +0 -13
  326. package/dist/cli/main-edxc3yzt.js +0 -4
  327. package/dist/cli/main-evgz4mp5.js +0 -21
  328. package/dist/cli/main-f33fc5je.js +0 -9
  329. package/dist/cli/main-fvnpq46y.js +0 -12
  330. package/dist/cli/main-gbbty4d4.js +0 -3
  331. package/dist/cli/main-gth53dnt.js +0 -25
  332. package/dist/cli/main-hqbz10aw.js +0 -9
  333. package/dist/cli/main-hrrvcfan.js +0 -38
  334. package/dist/cli/main-hzwtsb2m.js +0 -5
  335. package/dist/cli/main-j7ttv0sd.js +0 -34
  336. package/dist/cli/main-jak598k9.js +0 -5
  337. package/dist/cli/main-kba6zeyd.js +0 -6
  338. package/dist/cli/main-kwwsz6rq.js +0 -3
  339. package/dist/cli/main-m8vm17zq.js +0 -3
  340. package/dist/cli/main-mg4f96e1.js +0 -3
  341. package/dist/cli/main-mg9b20ac.js +0 -18
  342. package/dist/cli/main-mgb9ccnx.js +0 -3
  343. package/dist/cli/main-mjt2p7aj.js +0 -3
  344. package/dist/cli/main-n6qrdbmy.js +0 -3
  345. package/dist/cli/main-na7wse0x.js +0 -5
  346. package/dist/cli/main-nqveez48.js +0 -4
  347. package/dist/cli/main-ntqef02r.js +0 -10
  348. package/dist/cli/main-nvc3yjay.js +0 -136
  349. package/dist/cli/main-p0cfn6nr.js +0 -16
  350. package/dist/cli/main-qj2djy17.js +0 -19
  351. package/dist/cli/main-qsevpgsv.js +0 -3
  352. package/dist/cli/main-qvarybsp.js +0 -3
  353. package/dist/cli/main-rebtt91r.js +0 -5
  354. package/dist/cli/main-rpg7h8mb.js +0 -3
  355. package/dist/cli/main-rsy72qmw.js +0 -15
  356. package/dist/cli/main-rvetps99.js +0 -18
  357. package/dist/cli/main-s4bb0jav.js +0 -3
  358. package/dist/cli/main-s9v8k74e.js +0 -3
  359. package/dist/cli/main-tjvwmscs.js +0 -3
  360. package/dist/cli/main-tkgarpjj.js +0 -4
  361. package/dist/cli/main-v8y60bb2.js +0 -3
  362. package/dist/cli/main-vhrrq337.js +0 -3
  363. package/dist/cli/main-vp2dfb7s.js +0 -4
  364. package/dist/cli/main-vqbr22sz.js +0 -8
  365. package/dist/cli/main-vxnwe5xx.js +0 -18
  366. package/dist/cli/main-wgph00xf.js +0 -5
  367. package/dist/cli/main-wk2csfnj.js +0 -5
  368. package/dist/cli/main-wm997zjx.js +0 -3
  369. package/dist/cli/main-wpkyraxh.js +0 -3
  370. package/dist/cli/main-wqt32p5x.js +0 -4
  371. package/dist/cli/main-x9ct6y1a.js +0 -3
  372. package/dist/cli/main-xfekqh9m.js +0 -7
  373. package/dist/cli/main-xt9zc3n6.js +0 -7
  374. package/dist/cli/main-xx2z3zh5.js +0 -4
  375. package/dist/cli/main-y5c82rxr.js +0 -3
  376. package/dist/cli/main-yrjt2sqt.js +0 -14
  377. package/dist/cli/main-ys6zj3yr.js +0 -3
  378. package/dist/cli/main-ywbxshqc.js +0 -8
  379. package/dist/cli/main-z13755t8.js +0 -25
  380. package/dist/cli/main-zc7pyrbj.js +0 -4
  381. package/dist/cli/main.js +0 -279
  382. package/dist/cli/market-cmd-bm5xvn9f.js +0 -5
  383. package/dist/cli/mcp-login-bthtfpt7.js +0 -2
  384. package/dist/cli/mcp-market-cmd-mbeshfyd.js +0 -2
  385. package/dist/cli/notify-54v5z9dz.js +0 -2
  386. package/dist/cli/oauth-g5gme95c.js +0 -2
  387. package/dist/cli/output-satndjap.js +0 -16
  388. package/dist/cli/profiles-sfhpbq3m.js +0 -2
  389. package/dist/cli/provider-config-hv3xtdt4.js +0 -2
  390. package/dist/cli/provider-kwzq6g84.js +0 -2
  391. package/dist/cli/registry-fh0hdnyn.js +0 -2
  392. package/dist/cli/registry-y1y8e94r.js +0 -2
  393. package/dist/cli/repl-t4z03mqq.js +0 -11
  394. package/dist/cli/resume-fqt4chg8.js +0 -2
  395. package/dist/cli/run-flags-rysbag9t.js +0 -2
  396. package/dist/cli/runtime-j19fjbsa.js +0 -2
  397. package/dist/cli/sandbox-config-g4qxd7y5.js +0 -2
  398. package/dist/cli/server-r0b6bksk.js +0 -5
  399. package/dist/cli/session-arg-txmn5g4x.js +0 -2
  400. package/dist/cli/session-ed250d9j.js +0 -2
  401. package/dist/cli/sessions-cmd-adw7svfn.js +0 -7
  402. package/dist/cli/settings-y9rzcqx8.js +0 -2
  403. package/dist/cli/setup-jmbr11j0.js +0 -2
  404. package/dist/cli/sextant-smoke-tcth0vea.js +0 -5
  405. package/dist/cli/skills-cmd-zbdy99v6.js +0 -2
  406. package/dist/cli/smoke-1bg937kx.js +0 -8
  407. package/dist/cli/start-chat-p01cdks3.js +0 -12
  408. package/dist/cli/stream-4wmyaypz.js +0 -2
  409. package/dist/cli/task-eg4s093s.js +0 -2
  410. package/dist/cli/tasks-12v9rr9k.js +0 -2
  411. package/dist/cli/thinking-a5ngvqyh.js +0 -2
  412. package/dist/cli/todo-1wxpcecx.js +0 -2
  413. package/dist/cli/tools-2ftsya7w.js +0 -2
  414. package/dist/cli/tools-x1tj4fxm.js +0 -2
  415. package/dist/cli/trust-cmd-hccxehzb.js +0 -2
  416. package/dist/cli/update-check-ygt3vd7m.js +0 -2
  417. package/dist/cli/update-cmd-v23qhr8c.js +0 -2
  418. package/dist/cli/voice-g1gtck92.js +0 -2
  419. package/dist/cli/webfetch-0nnrjgb5.js +0 -2
  420. package/dist/cli/websearch-f0vr2p7d.js +0 -2
  421. package/dist/cli/workspace-9rq1w4ta.js +0 -2
  422. package/dist/lib/index.js +0 -62
  423. package/dist/lib/models-index.json +0 -1
  424. package/dist/lib/plugins.js +0 -6
  425. package/dist/lib/providers.js +0 -17
  426. package/dist/lib/public-api.js +0 -20
  427. package/dist/rovecode.exe +0 -4
  428. /package/{dist/cli → src/providers}/models-index.json +0 -0
@@ -0,0 +1,792 @@
1
+ /** Shared runtime construction for CLI surfaces (repl, run, tui): stores, tool
2
+ * registration, skills/memory indexes, provider resolution, RunConfig defaults.
3
+ * Extracted from repl.ts/main.ts so every surface builds the same agent. */
4
+
5
+ import type { AgentDefinition, ApprovalFn, Message, ModelRef, PermissionLevel, RunConfig, StreamFn, ThinkingEffort, TokenUsage, Tool } from "../core/types.ts";
6
+ import { parseEffort } from "../core/types.ts";
7
+ import { SessionStore } from "../core/session.ts";
8
+ import { ToolRegistry } from "../core/tools.ts";
9
+ import { SkillStore } from "../skills/index.ts";
10
+ import { createSkillTools, buildSkillsIndex } from "../skills/tools.ts";
11
+ import { BlockStore } from "../memory/blocks.ts";
12
+ import { memoryEditTool } from "../memory/tools.ts";
13
+ import type { ProviderConfig } from "../providers/stream.ts";
14
+ import { ProviderRegistry } from "../providers/registry.ts";
15
+ import { providerEditTool, providerListTool } from "../tools/provider.ts";
16
+ import { designAuditTool, designDirectionTool } from "../tools/design.ts";
17
+ import { designPromptSection } from "../design/rules.ts";
18
+ import { withToolCallParsing, toolPromptBlock } from "../providers/middleware.ts";
19
+ import { ModelCatalog } from "../providers/catalog.ts";
20
+ import { GLM_53_AGENT_CONTRACT, profileFor, profilePromptSection } from "../providers/profiles.ts";
21
+ import { loadProjectContext, type ProjectContext } from "../core/config.ts";
22
+ import { estimateTokens, type ContextChunk } from "../core/context.ts";
23
+ import { parseCompactionStrategy } from "../core/compaction.ts";
24
+ import { ToolGuard } from "../core/guardrails.ts";
25
+ import { HookRunner } from "../core/hooks.ts";
26
+ import { createReflectionHooks, reflectionEnabled } from "../core/reflection.ts";
27
+ import type { McpManager } from "../mcp/client.ts";
28
+ import { activatePlugins, discoverPlugins, loadState as loadPluginState, type DiscoveredPlugin, type LoadedPlugin } from "../plugins/index.ts";
29
+ import type { McpServerConfig } from "../mcp/config.ts";
30
+ import { trustedPredicate } from "../mcp/trust.ts";
31
+ import { positiveInt, positiveUsd, type RunLimits } from "./run-limits.ts";
32
+ import { costUsdTiered } from "../core/usage.ts";
33
+ import { ratesFor } from "../providers/catalog.ts";
34
+ import { contextBudgetFor } from "../core/context-report.ts";
35
+ import { tokenScaleFor } from "../core/token-scale.ts";
36
+ import { rovecodeHome } from "../providers/auth.ts";
37
+ import { readTool, editTool, writeTool, bashTool } from "../coding/hashline.ts";
38
+ import { globTool, grepTool, lsTool } from "../coding/files.ts";
39
+ import { withLspGate, lspGateNote, lspAvailabilityNote } from "../coding/lsp.ts";
40
+ import type { buildRepoMapChunk as BuildRepoMapChunkFn } from "../coding/repomap.ts";
41
+ import { anchorEntryId, Checkpoints, MUTATING_KINDS } from "../coding/checkpoints.ts";
42
+ import { createRouter, roleTableFromEnv, type Router } from "../providers/router.ts";
43
+ import { describeGiveUp, describeRetry, retryOptionsFromEnv, withRetry } from "../providers/retry.ts";
44
+ import { createEvalCellTool } from "../tools/evalcell.ts";
45
+ import { webFetchTool } from "../tools/webfetch.ts";
46
+ import { askUserTool, type AskFn } from "../tools/ask-user.ts";
47
+ import { execPolicyApprover } from "../core/execpolicy.ts";
48
+ import { recallTool } from "../memory/recall.ts";
49
+ import { configureExecutor, type SpawnRunner } from "../core/executor.ts";
50
+ import { loadSandboxConfig, unavailableRungError, type SandboxConfig } from "../core/sandbox-config.ts";
51
+ import { loadTodos, planReminder, todoTools } from "../tools/todo.ts";
52
+ import { noteVerifyCost, resolveForGate, runVerify, VERIFY_TIMEOUT_MS, type VerifyResolver } from "../core/verify-gate.ts";
53
+ import { SteeringQueue } from "../core/loop.ts";
54
+ import { TaskManager } from "../core/tasks.ts";
55
+ import { createTaskTool, createTaskStatusTool } from "../tools/task.ts";
56
+ import type { ChildContext, ChildRunnerDeps } from "../core/orchestrator.ts";
57
+ import { existsSync, mkdirSync } from "node:fs";
58
+ import { sep, join } from "node:path";
59
+ import { randomUUID } from "node:crypto";
60
+ import { noModelHint } from "../core/voice.ts";
61
+
62
+ /** upper bound on the per-model max_tokens buildDef derives from the catalog: enough for a long page or
63
+ * plan, not the 128K some models advertise — a runaway answer should stop before it costs that much */
64
+ export const MAX_OUTPUT_CAP = 32_768;
65
+
66
+ export interface RuntimeOptions {
67
+ cwd?: string;
68
+ sessionId?: string;
69
+ /** override the provider-derived stream (tests); null forces "no stream" */
70
+ stream?: StreamFn | null;
71
+ /** port #27 test seam: process runner behind the executor rung (probe AND
72
+ * commands); default Bun.spawn. Tests must never probe a real wsl.exe/docker. */
73
+ spawnRunner?: SpawnRunner;
74
+ /** test seam for the verify gate: which check a run that wrote files must pass. Default: core/verify.ts
75
+ * resolveVerify through verify-gate.ts resolveForGate (settings, then a manifest's unambiguous check script,
76
+ * never a guess). Returning null or no commands makes the gate report "not verified" instead of running. */
77
+ verifyResolver?: VerifyResolver;
78
+ /** port #27 test seam: platform the rung probe assumes; default process.platform */
79
+ platform?: NodeJS.Platform;
80
+ }
81
+
82
+ /** port #27: the rung this runtime asked the executor seam for, plus its probe. */
83
+ export interface SandboxState extends SandboxConfig {
84
+ /** settles once the rung is probed + installed behind getExecutor(); rejects
85
+ * with SandboxConfigError (one line). Await it before the first tool call —
86
+ * bootRuntime does; until then a non-direct rung is "desired, not yet met"
87
+ * and bashTool would get the seam's RungUnavailableError, never lazy direct. */
88
+ ready: Promise<void>;
89
+ }
90
+
91
+ export interface Runtime {
92
+ cwd: string;
93
+ sessionId: string;
94
+ store: SessionStore;
95
+ registry: ToolRegistry;
96
+ skillStore: SkillStore;
97
+ blockStore: BlockStore;
98
+ /** the live provider registry (providers/registry.ts): providers.json (user + project) + stored
99
+ * credentials + env, hot-reloaded on file change — `rovecode provider add`, `/provider …` in the
100
+ * TUI and the agent's provider_edit tool all land here and serve the next model call, no restart */
101
+ providers: ProviderRegistry;
102
+ /** the default provider as a stream.ts config — LIVE (re-resolved on every read); null when none */
103
+ provider: ProviderConfig | null;
104
+ /** the registry's dispatching stream (router → retry → middleware → per-provider adapter) or the
105
+ * opts.stream override; null only when opts.stream was explicitly null */
106
+ stream: StreamFn | null;
107
+ /** env ROVECODE_MODEL ?? providers.json `default` ?? the default provider's model ?? "" — LIVE */
108
+ defaultModel: string;
109
+ /** null when a run can start; else the one-line reason (no provider configured yet). Live: adding
110
+ * a provider through the CLI, the TUI or the agent flips it back to null without a restart.
111
+ * Always null when the runtime was built with an injected stream (tests, smoke). */
112
+ noProviderReason(): string | null;
113
+ /** interactive system prompt incl. skills index + memory index (indexes rebuilt per call). `cwdOverride`
114
+ * names another directory in the identity sentence — the live gauntlet points the model at its scratch
115
+ * workspace while everything else (indexes, profile override lookup) stays on the runtime's cwd */
116
+ systemPrompt(cwdOverride?: string): string;
117
+ buildDef(model: ModelRef, opts?: { cwd?: string }): AgentDefinition;
118
+ /** Build the repo map on the next turn of the event loop instead of inside the first buildDef. The TUI
119
+ * calls this right after its first paint: the build took 720–850 ms in this repository (2026-09-06),
120
+ * all of it on the first request's latency when it ran at submit time. A buildDef that arrives while
121
+ * the timer is still pending gets a definition WITHOUT the map — the map is not in that prompt, the
122
+ * request never waits — and every later run has it. Headless callers do not call this and keep the
123
+ * synchronous build: a one-shot `run` wants the map in its only prompt. Idempotent. */
124
+ warmRepoMap(): void;
125
+ /** `true`/`false` still mean auto/ask — every existing caller keeps working */
126
+ buildCfg(permission: PermissionLevel | boolean, approval?: ApprovalFn): RunConfig;
127
+ /** swap the session-scoped memory store — rebinds the memory tool AND the prompt (port #2 fix) */
128
+ setBlockStore(b: BlockStore): void;
129
+ /** tool-loop guardrails (port #4), one per runtime, thread into LoopDeps.guard */
130
+ guard: ToolGuard;
131
+ /** port #32: the open todo list, re-sent once per turn — thread into LoopDeps.planReminder */
132
+ planReminder: (history: readonly Message[]) => string | null;
133
+ /** How hard the model thinks before answering. Stamped onto every ModelRef buildDef hands out, so
134
+ * ONE setting reaches every surface (TUI, one-shot, serve, acp) without each threading a flag.
135
+ * A ref that already names an effort keeps it. */
136
+ effort: ThinkingEffort;
137
+ setEffort(e: ThinkingEffort): void;
138
+ /** the two ceilings on a run (cli/run-limits.ts): buildCfg reads them ahead of ROVECODE_MAX_TURNS /
139
+ * ROVECODE_MAX_SECONDS and the 60-turn default — `rovecode run` sets its flags and headless default here */
140
+ setRunLimits(limits: RunLimits): void;
141
+ /** MCP server manager (port #3); null when no servers were configured at boot AND none has been
142
+ * installed since — `reloadMcp` creates it on demand. */
143
+ mcp: McpManager | null;
144
+ /** Re-read the MCP files and bring the session in line with them, connecting anything new. What
145
+ * `market install mcp:<id>` calls so a fresh server is usable in the session that installed it
146
+ * rather than after a restart. Never throws: a server that will not connect comes back in `failed`
147
+ * and simply stays unavailable, exactly as at boot. */
148
+ reloadMcp(): Promise<{ added: string[]; removed: string[]; failed: { name: string; error: string }[]; skipped: string[] }>;
149
+ /** port #8 config snapshot (AGENTS.md/CLAUDE.md/… harvested cwd-upward ONCE
150
+ * at construction, for prompt-cache stability) incl. dropped/truncated
151
+ * source stubs for /status. Mid-session config edits are intentionally not
152
+ * picked up — restart rovecode (a new runtime) to refresh. */
153
+ projectContext: ProjectContext;
154
+ /** port #14: role→model router with fallback chains (env ROVECODE_MODEL_<ROLE>). */
155
+ router: Router;
156
+ /** port #14: fallback-advance notes accumulated since the last drain. */
157
+ drainRouterNotes(): string[];
158
+ /** live delivery of the same notes (retry "retrying in 4 s (2/4)", give-up, chain advance): buffered ones replay
159
+ * first, then each new note arrives as it happens — the TUI shows a notice while the backoff waits, cmdRun prints a
160
+ * stderr line. With a listener registered, drainRouterNotes has nothing left to drain. */
161
+ onRouterNote(fn: (note: string) => void): void;
162
+ /** port #11: shadow-git checkpoints for a session (lazy; null when git is absent
163
+ * or ROVECODE_NO_CHECKPOINTS=1). Snapshots land automatically after mutating tools. */
164
+ checkpointsFor(sessionId: string): Promise<Checkpoints | null>;
165
+ /** port #11: point checkpoint entryId capture at the ACTIVE session store after
166
+ * a TUI session switch (pairs with setBlockStore). */
167
+ setSessionStore(s: SessionStore): void;
168
+ /** port #27: executor rung selected by .rovecode/sandbox.json / ROVECODE_SANDBOX (+ probe) */
169
+ sandbox: SandboxState;
170
+ /** port #33: bind (or unbind with undefined) the interactive asker behind the ask_user
171
+ * tool — the TUI hands in its question overlay; headless surfaces (run/serve/acp) never
172
+ * call this, so the tool fails closed for them. setBlockStore idiom: registered once,
173
+ * dependency rebound late. */
174
+ setAskUser(fn: AskFn | undefined): void;
175
+ /** port #26: the ONE steering queue for this runtime's runs — hand it to agentLoop
176
+ * (in place of a fresh SteeringQueue) so background-task completion notes reach the
177
+ * parent's next turn. Surfaces with their own queue: rt.tasks.attach(queue). */
178
+ steering: SteeringQueue;
179
+ /** port #26: background subagents (bounded FIFO jobs over orchestrator runChild) */
180
+ tasks: TaskManager;
181
+ /** port #29: typed hook set — `.rovecode/hooks.{ts,js}` (+ `~/.rovecode`, ROVECODE_HOME) loaded at construction
182
+ * (background import; every run() waits for it, so no surface can race the load), session_open
183
+ * fired once loaded. Thread into LoopDeps.hooks; attach more sets programmatically via hooks.add()
184
+ * (port #39 OTel); surfaces call hooks.close() at teardown → session_close once. Load + runtime
185
+ * notes (import failure, wrong version, timeout, throw) land in hooks.warnings / onWarning(). */
186
+ hooks: HookRunner;
187
+ /** plugins (src/plugins, docs/plugins.md): discovered synchronously at construction — manifests and
188
+ * statuses only, no code run — so an ACTIVE plugin's skills, commands and MCP servers wire in with
189
+ * their file-based twins; the entry modules (tools + hooks) import in the background and `ready`
190
+ * joins them (bootRuntime awaits it, so no surface's first prompt can miss a plugin tool). A
191
+ * PROJECT plugin stays `untrusted` — nothing of it loads — until `rovecode plugin trust`. */
192
+ plugins: RuntimePlugins;
193
+ }
194
+
195
+ export interface RuntimePlugins {
196
+ /** every plugin found at construction, with its status (`rovecode plugin list` shows the same) */
197
+ found: readonly DiscoveredPlugin[];
198
+ /** settles when the active entry modules are imported and their tools/hooks attached */
199
+ ready: Promise<void>;
200
+ /** the plugins as activated — empty until `ready` */
201
+ readonly loaded: readonly LoadedPlugin[];
202
+ /** discovery + activation notes; a listener gets the buffered ones first (hooks.onWarning idiom) */
203
+ readonly warnings: readonly string[];
204
+ onWarning(fn: (note: string) => void): void;
205
+ }
206
+
207
+ // lazy module helpers — loaded on first use so boot pays nothing for features that are not configured
208
+ type OtelMod = typeof import("../telemetry/otel.ts");
209
+ let _otelMod: OtelMod | null = null;
210
+ function lazyOtel(): OtelMod | null {
211
+ if (!process.env.ROVECODE_OTEL_ENDPOINT) return null;
212
+ if (_otelMod === null) {
213
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
214
+ _otelMod = require("../telemetry/otel.ts") as OtelMod;
215
+ }
216
+ return _otelMod;
217
+ }
218
+
219
+ type McpClientMod = typeof import("../mcp/client.ts");
220
+ type McpToolsMod = typeof import("../mcp/tools.ts");
221
+ let _mcpMod: { client: McpClientMod; tools: McpToolsMod } | null = null;
222
+ function lazyMcp(): { client: McpClientMod; tools: McpToolsMod } {
223
+ if (_mcpMod === null) {
224
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
225
+ _mcpMod = {
226
+ client: require("../mcp/client.ts") as McpClientMod,
227
+ tools: require("../mcp/tools.ts") as McpToolsMod,
228
+ };
229
+ }
230
+ return _mcpMod;
231
+ }
232
+
233
+ export function createRuntime(opts: RuntimeOptions = {}): Runtime {
234
+ const cwd = opts.cwd ?? process.cwd();
235
+ // port #27: sandbox rung selection comes FIRST — a config error throws before any
236
+ // side effect (no sessions dir, no MCP children). The probe (wsl/docker trial
237
+ // spawn, 500ms cap) runs concurrently with the rest of construction; its verdict
238
+ // is `sandbox.ready`. The seam records the DESIRED rung synchronously (#10 G7),
239
+ // so an unmet wsl/docker desire is loud at bashTool, never a silent direct.
240
+ const sandboxCfg = loadSandboxConfig(cwd);
241
+ const ready = configureExecutor(sandboxCfg.rung, {
242
+ runner: opts.spawnRunner, dockerImage: sandboxCfg.dockerImage, platform: opts.platform,
243
+ }).then(() => undefined, (e: unknown) => { throw unavailableRungError(sandboxCfg, e); });
244
+ void ready.catch(() => {}); // verdict is read via bootRuntime / await — never an unhandled rejection
245
+ const sandbox: SandboxState = { ...sandboxCfg, ready };
246
+ const sessionsDir = join(cwd, ".rovecode", "sessions");
247
+ mkdirSync(sessionsDir, { recursive: true });
248
+ const sessionId = opts.sessionId ?? randomUUID();
249
+ const store = new SessionStore(sessionsDir, sessionId);
250
+
251
+ // port #11: shadow-git checkpoints — one repo per session under .rovecode/checkpoints/,
252
+ // snapshot after every SUCCESSFUL mutating tool call (kinds write/execute). Lazy
253
+ // per-session init; git absent or ROVECODE_NO_CHECKPOINTS=1 → silently off.
254
+ const cpBySession = new Map<string, Promise<Checkpoints | null>>();
255
+ const checkpointsFor = (sid: string): Promise<Checkpoints | null> => {
256
+ if (process.env.ROVECODE_NO_CHECKPOINTS === "1") return Promise.resolve(null);
257
+ let p = cpBySession.get(sid);
258
+ if (!p) { p = Checkpoints.init({ workspace: cwd, sessionId: sid }).then((c) => c, () => null); cpBySession.set(sid, p); }
259
+ return p;
260
+ };
261
+ let activeStore = store; // TUI session switches re-point it via setSessionStore
262
+ const withCheckpoint = (t: Tool): Tool => !MUTATING_KINDS.has(t.kind) ? t : {
263
+ ...t,
264
+ execute: async (a, c) => {
265
+ const out = await t.execute(a, c);
266
+ if (out.ok) {
267
+ const cp = await checkpointsFor(c.sessionId);
268
+ // conversation-restore anchor: last USER message (HIGH-2 — the tail entry is the
269
+ // assistant message that ISSUED this very tool call; branching there strands its
270
+ // tool_calls with no replies -> provider 400), when the active store IS this session
271
+ const entryId = activeStore.id === c.sessionId ? anchorEntryId(activeStore.messages()) : undefined;
272
+ await cp?.snapshot(t.schema.name, entryId).catch(() => {});
273
+ }
274
+ return out;
275
+ },
276
+ };
277
+
278
+ const registry = new ToolRegistry();
279
+ // plugins (src/plugins, docs/plugins.md): discovery runs no code — manifests, statuses, digests — so
280
+ // it can happen here, synchronously, and the ACTIVE plugins' declarative halves (skills dirs,
281
+ // command dirs, MCP servers) join their file-based twins below as if they had been in .rovecode/.
282
+ // The entry modules import after construction (see the activation block after hooks.open).
283
+ const pluginHome = rovecodeHome();
284
+ const pluginState = loadPluginState(pluginHome); // one trust store for project plugins AND project MCP files
285
+ const pluginsFound = discoverPlugins(cwd, { home: pluginHome, state: pluginState });
286
+ const pluginWarnings: string[] = [...pluginsFound.warnings];
287
+ const pluginListeners: ((note: string) => void)[] = [];
288
+ const pluginWarn = (note: string): void => { pluginWarnings.push(note); for (const l of pluginListeners) l(note); };
289
+ const activePlugins = pluginsFound.plugins.filter((p) => p.status === "active"); // untrusted/disabled/broken contribute NOTHING
290
+ // port #13: successful edits/writes get LSP diagnostics appended within a ≤2s
291
+ // settle window (typescript-language-server on PATH; absent → silently off).
292
+ const lspNote = (p: string): Promise<string> => lspGateNote(p, cwd);
293
+ // "absent → silently off" is right for the tool result and wrong for the person: say once, at boot, that the
294
+ // diagnostics loop is not running here (TypeScript projects only — coding/lsp.ts lspAvailabilityNote)
295
+ const lspGap = lspAvailabilityNote(cwd);
296
+ if (lspGap !== null) pluginWarn(lspGap);
297
+ registry.register(readTool, withCheckpoint(withLspGate(editTool, lspNote)), withCheckpoint(withLspGate(writeTool, lspNote)), withCheckpoint(bashTool));
298
+ registry.register(globTool, grepTool, lsTool); // port #22: bounded, gitignore-aware search/list (kind read → file.read auto-allow; non-mutating, no checkpoint)
299
+ registry.register(webFetchTool); // port #31: kind network → net.fetch, PROMPT by default (rule below); SSRF-guarded, bounded; no checkpoint
300
+ // a plugin's skills dir joins the store as one more root: a user plugin's as global, a project
301
+ // plugin's as project (the same precedence its own files would have had)
302
+ const skillStore = new SkillStore(cwd, { extraDirs: activePlugins.flatMap((p) => (p.skillsDir ? [{ dir: p.skillsDir, scope: p.scope === "project" ? "project" as const : "global" as const }] : [])) });
303
+ skillStore.scan();
304
+ registry.register(...createSkillTools(skillStore));
305
+ let blocks = new BlockStore(join(sessionsDir, sessionId, "memory"));
306
+ registry.register(memoryEditTool(blocks));
307
+ // port #18: persistent eval cell — registered ONLY when ROVECODE_EVAL_CELL=1
308
+ const evalCell = createEvalCellTool();
309
+ if (evalCell) registry.register(withCheckpoint(evalCell));
310
+ // port #17: cross-session recall (kind read → file.read gate; pure transcript search)
311
+ registry.register(recallTool(sessionsDir));
312
+ registry.register(...todoTools(sessionsDir)); // port #32: per-session todo list at <session>/todos.json (todo_write kind memory → memory.write allow; todo_read kind read)
313
+
314
+ /** LoopDeps.planReminder: while a plan is open, re-send it as the LAST thing in the request. Read
315
+ * from disk every turn, so the agent's own todo_write (and a /todos edit, and a second surface on
316
+ * the same session) are all reflected. Skipped when the model just wrote the list — it is already
317
+ * looking at that tool result, and a copy right under it teaches nothing. */
318
+ const planReminderFor = (history: readonly Message[]): string | null => {
319
+ const last = history.at(-1);
320
+ if (last?.parts.some((p) => p.kind === "tool_result" && p.output.startsWith("todos:"))) return null;
321
+ const dir = join(sessionsDir, store.id);
322
+ try { return planReminder(loadTodos(dir).items); } catch { return null; } // a missing/corrupt list never blocks a turn
323
+ };
324
+ // providers: ONE live registry per runtime (providers/registry.ts) — providers.json (user + project),
325
+ // stored credentials and env, re-read when a source file changes. provider_list is kind read (always
326
+ // allowed); provider_edit is kind custom → tool.provider_edit, PROMPT under the gated rules below
327
+ const providers = new ProviderRegistry(cwd);
328
+ registry.register(providerListTool(providers), providerEditTool(providers));
329
+ // design protocol (design/rules.ts): design_audit is kind read (free, never prompts -- checking your
330
+ // own work must cost nothing); design_direction is kind custom -> tool.design_direction, PROMPT under
331
+ // the gated rules, because it records the project's design identity and is asked once per project.
332
+ registry.register(designAuditTool(), designDirectionTool());
333
+ // port #33: ask_user on EVERY surface (kind read → auto-runs under gated/plan rules); only an
334
+ // interactive surface binds an asker via setAskUser — unbound, the tool fails closed
335
+ let askUser: AskFn | undefined;
336
+ registry.register(askUserTool(() => askUser));
337
+ const guard = new ToolGuard(); // port #4: loop signatures + duplicate-result stubs
338
+ // port #29: hooks v2 — the runner exists synchronously (createRuntime stays sync); open() imports
339
+ // .rovecode/hooks.{ts,js} (+ user scope) in the background and fires session_open; run() awaits it
340
+ const hooks = new HookRunner({ cwd, sessionId });
341
+ void hooks.open(cwd);
342
+
343
+ // plugin entry modules: imported in the background like the hook files, joined by bootRuntime through
344
+ // plugins.ready. Tools land on THIS registry (the same permission path as every built-in: kind → action)
345
+ // and are refused loudly when the name is taken — a plugin cannot replace `bash`. Hooks join the runner
346
+ // after the hook files (they miss session_open; pre_run is theirs). Activation failures are notes.
347
+ let loadedPlugins: LoadedPlugin[] = [];
348
+ const pluginsReady = activatePlugins(pluginsFound.plugins, { cwd, home: pluginHome }).then((a) => {
349
+ for (const w of a.warnings) pluginWarn(w);
350
+ const taken = new Set(registry.list().map((t) => t.schema.name));
351
+ for (const p of a.plugins) {
352
+ if (p.status !== "active") continue;
353
+ for (const t of p.tools) {
354
+ if (taken.has(t.schema.name)) { pluginWarn(`plugin ${p.name}: tool "${t.schema.name}" is already registered — refused (a plugin cannot replace a built-in or another plugin's tool)`); continue; }
355
+ taken.add(t.schema.name);
356
+ registry.register(t);
357
+ }
358
+ if (p.hooks) hooks.add(p.hooks, `plugin:${p.name}`);
359
+ }
360
+ loadedPlugins = a.plugins;
361
+ }, (e: unknown) => { pluginWarn(`plugins: activation failed — ${e instanceof Error ? e.message : String(e)}`); });
362
+ const plugins: RuntimePlugins = {
363
+ found: pluginsFound.plugins,
364
+ ready: pluginsReady,
365
+ get loaded() { return loadedPlugins; },
366
+ warnings: pluginWarnings,
367
+ onWarning(fn) { for (const w of pluginWarnings) fn(w); pluginListeners.push(fn); },
368
+ };
369
+
370
+ // port #3: MCP servers from .rovecode/mcp.json + harvested .mcp.json; two lazy tools only.
371
+ // connect() is fire-and-forget; tool executes await first-connect before dispatching.
372
+ // plugin MCP servers first, then the project's own files — .rovecode/mcp.json keeps the last word on a name
373
+ const mcpByName = new Map<string, McpServerConfig>();
374
+ for (const p of activePlugins) for (const c of p.mcp) {
375
+ if (mcpByName.has(c.name)) { pluginWarn(`plugin ${p.name}: MCP server "${c.name}" is also declared by another plugin — first kept`); continue; }
376
+ mcpByName.set(c.name, c);
377
+ }
378
+ // the user file (~/.rovecode/mcp.json — where `rovecode mcp add` writes) is the lowest of the three layers.
379
+ // Project files (.rovecode/mcp.json, .mcp.json) pass the same trust gate as project plugins (mcp/trust.ts):
380
+ // unapproved on this machine → nothing of theirs loads, one `mcp: …` note names the file and the command.
381
+ const mcpWarnings: string[] = [];
382
+ const hasMcpFiles = existsSync(join(pluginHome, "mcp.json"))
383
+ || existsSync(join(cwd, ".rovecode", "mcp.json"))
384
+ || existsSync(join(cwd, ".mcp.json"));
385
+ if (hasMcpFiles) {
386
+ for (const c of lazyMcp().client.loadMcpConfig(cwd, mcpWarnings, { home: pluginHome, trusted: trustedPredicate(pluginState) })) { if (mcpByName.has(c.name)) pluginWarn(`mcp.json server "${c.name}" overrides a plugin's entry of the same name`); mcpByName.set(c.name, c); }
387
+ }
388
+ for (const w of mcpWarnings) pluginWarn(`mcp: ${w}`);
389
+ // Reading the three files is its own function because it happens twice: once here, and again whenever
390
+ // something installs a server and wants it usable without a restart (reloadMcp below).
391
+ // `warn` is an OUT parameter rather than a swallowed local: a server that is skipped — an unset ${NAME},
392
+ // an unfilled <placeholder>, an untrusted project file — is the one thing the human most needs to hear
393
+ // after installing something, and dropping the reason here is what made a failed install look like a
394
+ // successful one that simply did nothing.
395
+ const readMcpConfigs = (warn: string[] = []): McpServerConfig[] => {
396
+ const byName = new Map<string, McpServerConfig>();
397
+ for (const p of activePlugins) for (const c of p.mcp) if (!byName.has(c.name)) byName.set(c.name, c);
398
+ const files = existsSync(join(pluginHome, "mcp.json"))
399
+ || existsSync(join(cwd, ".rovecode", "mcp.json"))
400
+ || existsSync(join(cwd, ".mcp.json"));
401
+ // the trust gate is re-read too: a project file approved since boot starts counting from now on,
402
+ // and one whose contents changed is untrusted again, exactly as it would be on a fresh start
403
+ if (files) for (const c of lazyMcp().client.loadMcpConfig(cwd, warn, { home: pluginHome, trusted: trustedPredicate(loadPluginState(pluginHome)) })) byName.set(c.name, c);
404
+ return [...byName.values()];
405
+ };
406
+
407
+ const mcpConfigs = [...mcpByName.values()];
408
+ let mcp: McpManager | null = null;
409
+ let mcpReady: Promise<void> = Promise.resolve();
410
+ /** register mcp_list/mcp_call once; they dispatch by server name, so a new server needs no new tool */
411
+ const registerMcpTools = (manager: McpManager): void => {
412
+ for (const t of lazyMcp().tools.createMcpTools(manager)) {
413
+ registry.register({ ...t, execute: async (a, c) => { await mcpReady; return t.execute(a, c); } });
414
+ }
415
+ };
416
+ if (mcpConfigs.length > 0) {
417
+ const mcpMod = lazyMcp();
418
+ const manager = new mcpMod.client.McpManager(mcpConfigs);
419
+ mcp = manager;
420
+ // The connect starts on the NEXT turn of the event loop, not here: runTui is synchronous from
421
+ // createRuntime through renderer.start(), which paints the first frame, and connect()'s first
422
+ // step is loading the MCP SDK (~200 ms of module evaluation). Kicked off inline that load ran on
423
+ // the first microtask — still ahead of the first paint. A zero timer puts it behind it. Nothing
424
+ // at boot awaits mcpReady; mcp_list/mcp_call do (registerMcpTools), so a tool call may wait —
425
+ // the terminal must not. Measured: two npx servers, createRuntime 254 ms → 30 ms.
426
+ // The result is not discarded: a server the loader accepted but that never answers (a command that does not
427
+ // exist, a package npx cannot resolve) is counted on the startup card as configured, and until it is named
428
+ // here the only sign of it was a tool call that found nothing. Same channel as the loader's own "skipped"
429
+ // lines (pluginWarn → the TUI's warn notes, stderr headless), same shape as reloadMcp's `failed`.
430
+ mcpReady = new Promise<void>((resolve) => {
431
+ setTimeout(() => {
432
+ manager.connect().then(
433
+ (r) => { for (const f of r.failed) pluginWarn(`mcp: server "${f.name}" did not connect — ${f.error}`); resolve(); },
434
+ () => resolve(),
435
+ );
436
+ }, 0);
437
+ });
438
+ registerMcpTools(manager);
439
+ }
440
+
441
+ /** Pick up mcp.json changes in a live session — what `market install mcp:<id>` calls so the answer is
442
+ * "ready" instead of "restart rovecode".
443
+ *
444
+ * Two cases, and the second is the one that matters most: when a session started with NO servers there
445
+ * is no manager and `mcp_list`/`mcp_call` were never registered, so installing your first server used
446
+ * to leave the model with no way to reach it at all. Here the manager is created and the two tools are
447
+ * registered at that moment. Servers already connected are left alone (see McpManager.sync). */
448
+ const reloadMcp = async (): Promise<{ added: string[]; removed: string[]; failed: { name: string; error: string }[]; skipped: string[] }> => {
449
+ const skipped: string[] = [];
450
+ const configs = readMcpConfigs(skipped);
451
+ if (mcp === null) {
452
+ if (configs.length === 0) return { added: [], removed: [], failed: [], skipped };
453
+ const manager = new (lazyMcp().client.McpManager)(configs);
454
+ mcp = manager;
455
+ registerMcpTools(manager);
456
+ const r = await manager.connect();
457
+ mcpReady = Promise.resolve();
458
+ return { added: configs.map((c) => c.name), removed: [], failed: r.failed, skipped };
459
+ }
460
+ const { added, removed } = await mcp.sync(configs);
461
+ const r = added.length > 0 ? await mcp.connect() : { failed: [] as { name: string; error: string }[] };
462
+ return { added, removed, failed: r.failed, skipped };
463
+ };
464
+
465
+ // boot-time view of the default provider — only the router's role table is pinned to it; every
466
+ // other reader goes through the LIVE getters on the returned Runtime (provider / defaultModel)
467
+ const bootDefault = providers.defaultRef();
468
+ // port #14: role router + fallback chains (env ROVECODE_MODEL_DEFAULT/SMOL/PLAN/COMMIT/TASK,
469
+ // comma-separated provider/model chains). The registry's dispatcher routes every candidate to
470
+ // ITS OWN provider's endpoint, so a cross-provider chain really fails over.
471
+ const routerNotes: string[] = [];
472
+ const routerListeners: ((note: string) => void)[] = [];
473
+ /** a note goes to every live listener at once (the TUI's notice, cmdRun's stderr line); with no listener it
474
+ * waits in the buffer for drainRouterNotes — so a retry is visible WHILE it waits, not after the run */
475
+ const pushRouterNote = (note: string): void => { if (routerListeners.length === 0) routerNotes.push(note); else for (const fn of routerListeners) fn(note); };
476
+ const fallbackRef: ModelRef = { provider: bootDefault?.provider ?? "mock", model: bootDefault?.model || "default" };
477
+ const router = createRouter({
478
+ roles: roleTableFromEnv(fallbackRef),
479
+ // MED-3: an explicitly configured default chain is the fallback pool even for models
480
+ // outside it (requested model prepended as primary); the synthesized single-model
481
+ // default (env unset) must NOT capture loose models — hence the env gate.
482
+ looseFallback: (process.env.ROVECODE_MODEL_DEFAULT ?? "").trim().length > 0,
483
+ onNote: (n) => pushRouterNote(
484
+ `router: ${n.chain} ${n.from.provider}/${n.from.model} → ${n.to ? `${n.to.provider}/${n.to.model}` : "chain exhausted"} (${n.reason})`),
485
+ });
486
+ // port #7: provider streams get the non-native tool-call parser (strict-gated passthrough
487
+ // for native turns); injected test streams stay untouched. Kill switch: ROVECODE_NO_TOOL_MIDDLEWARE=1
488
+ // port #14: the router wraps OUTERMOST (chain advance re-drives the whole turn).
489
+ // port #23: same-model retry sits INSIDE the router — backoff retries exhaust on candidate N
490
+ // before the chain advances (ROVECODE_RETRY_MAX / ROVECODE_RETRY_BASE_MS; providers/retry.ts header).
491
+ // The raw stream is the registry's DISPATCHER: it resolves model.provider on every call against the
492
+ // live snapshot, so the wrapped stream below never needs rebuilding when providers change. With
493
+ // nothing configured it yields a `config:` error turn (non-retryable) — surfaces consult
494
+ // noProviderReason() first and cmdRun keeps its mock fallback.
495
+ const rawStream = providers.stream();
496
+ const middlewared = process.env.ROVECODE_NO_TOOL_MIDDLEWARE !== "1" ? withToolCallParsing(rawStream) : rawStream;
497
+ // retries and give-ups surface as notes in the human's words (providers/retry.ts describeRetry/describeGiveUp):
498
+ // "anthropic: overloaded — retrying in 4 s (2/4)" — live to onRouterNote listeners, else buffered for drainRouterNotes
499
+ const stream = opts.stream !== undefined ? opts.stream : router.wrap(withRetry(middlewared, { ...retryOptionsFromEnv(), onRetry: (n) => pushRouterNote(describeRetry(n)), onGiveUp: (n) => pushRouterNote(describeGiveUp(n)) }));
500
+ const catalog = new ModelCatalog(); // offline models.dev snapshot (port #6)
501
+ // port #39: OTel span export rides the hook seam — attached ONLY when ROVECODE_OTEL_ENDPOINT is set (off:
502
+ // nothing constructed, no on_event tap → zero cost); export failures surface through hooks.warnings
503
+ const otelMod = lazyOtel();
504
+ if (otelMod) {
505
+ const otel = otelMod.otelOptionsFromEnv();
506
+ if (otel) hooks.add(otelMod.createOtelHooks({ ...otel, pricing: catalog, messages: () => activeStore.messages() }), "otel");
507
+ }
508
+
509
+ // port #8: harvest AGENTS.md / CLAUDE.md / .cursor / copilot instructions
510
+ // cwd-UPWARD (OMP ancestor-walk pattern) ONCE per runtime — a snapshot, like
511
+ // BlockStore, so the system prompt stays byte-stable for prompt caching
512
+ // (MED-4). It reaches the model as an ADR-007 "config" chunk (priority 70)
513
+ // via buildDef → assembleContext, never a second prompt-assembly path.
514
+ const projectContext = loadProjectContext(cwd);
515
+ const configText = `# Project context${projectContext.text}`;
516
+ const configChunk: ContextChunk | null = projectContext.text
517
+ ? { name: "config", text: configText, priority: 70, tokens: estimateTokens(configText) }
518
+ : null;
519
+
520
+ // port #12: repo-map fills the reserved ADR-007 chunk (priority 80, set by the
521
+ // module: system>files>repo-map>skills/config>history). Built LAZILY at the
522
+ // first buildDef() and memoized — createRuntime stays sync-cheap (`rovecode tools`,
523
+ // ACP session setup pay nothing) and per-file tags persist under
524
+ // .rovecode/cache/repomap.json, so warm launches skip extraction. Frozen after
525
+ // the first build, like config, for prompt-cache stability. ROVECODE_NO_REPOMAP=1
526
+ // disables; budget override via ROVECODE_REPOMAP_TOKENS (default 1024, aider's).
527
+ let extraChunksMemo: ContextChunk[] | null = null;
528
+ const extraChunks = (): ContextChunk[] => {
529
+ if (extraChunksMemo !== null) return extraChunksMemo;
530
+ let repoMapChunk: ContextChunk | null = null;
531
+ if (process.env.ROVECODE_NO_REPOMAP !== "1") {
532
+ const budget = Number(process.env.ROVECODE_REPOMAP_TOKENS ?? "") || 1024;
533
+ try {
534
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
535
+ const { buildRepoMapChunk } = require("../coding/repomap.ts") as { buildRepoMapChunk: typeof BuildRepoMapChunkFn };
536
+ repoMapChunk = buildRepoMapChunk(cwd, budget);
537
+ } catch { repoMapChunk = null; }
538
+ }
539
+ extraChunksMemo = [configChunk, repoMapChunk].filter((c): c is ContextChunk => c !== null);
540
+ return extraChunksMemo;
541
+ };
542
+ // warmRepoMap (Runtime interface): the map builds behind the TUI's first frame. While that timer is
543
+ // pending, buildDef hands out the cheap chunks only — "the map is not in this prompt", never "the
544
+ // request waits" — and does NOT memoize, so the timer's build is the one that freezes the map.
545
+ let warmPending = false;
546
+ const warmRepoMap = (): void => {
547
+ if (extraChunksMemo !== null || warmPending) return;
548
+ warmPending = true;
549
+ setTimeout(() => { warmPending = false; try { extraChunks(); } catch { /* the memo stays empty; the next buildDef builds synchronously */ } }, 0);
550
+ };
551
+ const chunksForDef = (): ContextChunk[] => extraChunksMemo ?? (warmPending ? [configChunk].filter((c): c is ContextChunk => c !== null) : extraChunks());
552
+
553
+ const systemPrompt = (cwdOverride?: string): string => {
554
+ const skillsIndex = buildSkillsIndex(skillStore);
555
+ const memoryIndex = blocks.renderForPrompt();
556
+ return `You are Rovecode, an interactive coding agent in ${cwdOverride ?? cwd}. Use read/edit/write/bash tools. Edits require line hashes from read output. Match the length of an answer to the task: a line for a lookup, the full thing for a plan, a design or a review — never pad, never truncate work that was asked for.${skillsIndex ? "\n\n# Skills\n" + skillsIndex : ""}${memoryIndex ? "\n\n# Memory\n" + memoryIndex : ""}`;
557
+ };
558
+
559
+ // ROVECODE_EFFORT is the boot default; /effort and --effort move it at runtime
560
+ // default "auto": no thinking field on the wire, the provider's own default stands (Claude 5: adaptive,
561
+ // high). The old default "off" sent an explicit `thinking: disabled` and switched off the reasoning the
562
+ // model does by itself — most of "we are not getting the model's real performance" (Berkay, 2026-09-04).
563
+ let effort: ThinkingEffort = parseEffort(process.env.ROVECODE_EFFORT) ?? "auto";
564
+
565
+ const buildDef = (model: ModelRef, opts: { cwd?: string } = {}): AgentDefinition => {
566
+ if (model.effort === undefined) model = { ...model, effort }; // one dial, every surface
567
+ activeModel = model; // port #26: children run the model of the run that started them
568
+ // models the catalog knows CANNOT do native tool calling get the senpi-format
569
+ // prompt block (port #7); unknown models attempt native first. Force: ROVECODE_TOOL_MIDDLEWARE=1
570
+ const info = catalog.lookup(model.provider, model.model);
571
+ // the answer's room comes from the catalog (models.dev maxOutput), capped: the old flat 4096 default
572
+ // truncated long outputs — a whole page of UI, a long plan — mid-sentence, and the model was blamed
573
+ if (model.maxTokens === undefined && info?.maxOutput) model = { ...model, maxTokens: Math.min(info.maxOutput, MAX_OUTPUT_CAP) };
574
+ // the catalog's word on a reasoning mode rides with the ref: thinking.ts sends no dial to a model listed without one
575
+ if (model.reasoning === undefined && info?.supportsReasoning !== undefined) model = { ...model, reasoning: info.supportsReasoning };
576
+ const nonNative = info?.supportsTools === false || process.env.ROVECODE_TOOL_MIDDLEWARE === "1";
577
+ // model profile (providers/profiles.ts): a per-family behavioral section rides AFTER the base prompt
578
+ // and its indexes and BEFORE the tool-calling block — one string for the whole run (prompt cache);
579
+ // .rovecode/profiles/<id>.md replaces the built-in text, ROVECODE_PROFILE=off drops it
580
+ const profile = profileFor(model);
581
+ // A model WITHOUT a profile gets the working agreement too (profile-glm53.ts names no model or vendor
582
+ // — it is the harness's contract: read before edit, verify before "done", parallel calls, scope).
583
+ // Until now only GLM received it and Claude/GPT got one sentence; that asymmetry cost quality.
584
+ const section = profile === null ? GLM_53_AGENT_CONTRACT : profilePromptSection(profile, cwd); // "" = an empty override file: no section, no separator
585
+ // design protocol (design/rules.ts): the ban list plus this project's recorded direction, read
586
+ // once per run start like the profile so the system prefix stays byte-stable (prompt cache).
587
+ // ROVECODE_DESIGN=off drops it for a run that has nothing to do with interfaces.
588
+ const design = process.env.ROVECODE_DESIGN === "off" ? "" : designPromptSection(opts.cwd ?? cwd);
589
+ const base = [systemPrompt(opts.cwd), section, design].filter((p) => p.length > 0).join("\n\n");
590
+ return {
591
+ name: "main", model, tools: ["*"],
592
+ systemPrompt: nonNative
593
+ ? `${base}\n\n# Tool calling\n${toolPromptBlock(registry.list().map((t) => t.schema))}`
594
+ : base,
595
+ ...(chunksForDef().length > 0 ? { contextChunks: chunksForDef() } : {}),
596
+ };
597
+ };
598
+ /** the workspace as a rule resource: every path resource is absolute (tools.ts describeResource),
599
+ * so `<cwd><sep>*` is "inside this repository" and nothing else — a sibling directory whose name
600
+ * merely STARTS with the cwd (…/repo-backup) does not match, because the separator is in the glob. */
601
+ const insideCwd = `${cwd.replace(/[\/]$/, "")}${sep}*`;
602
+
603
+ // run ceilings (cli/run-limits.ts): a surface's explicit limits, else the environment, else 60 turns and no
604
+ // clock — the TUI and serve/acp get the env knobs for free, `rovecode run` adds its flags + a 20-minute default
605
+ let runLimits: RunLimits = {};
606
+ const buildCfg = (permission: PermissionLevel | boolean, approval?: ApprovalFn): RunConfig => {
607
+ const level: PermissionLevel = permission === true ? "auto" : permission === false ? "ask" : permission;
608
+ const yolo = level === "auto";
609
+ const maxSeconds = runLimits.maxSeconds ?? positiveInt(process.env.ROVECODE_MAX_SECONDS);
610
+ const maxCostUsd = runLimits.maxCostUsd ?? positiveUsd(process.env.ROVECODE_MAX_COST);
611
+ // the same arithmetic /cost and the headless result use (tui/cost.ts, cli/output.ts): the catalog's price
612
+ // for the model that served the turn, tiered by the prompt the turn actually carried; no price → undefined
613
+ const priceUsd = (usage: TokenUsage, origin: ModelRef): number | undefined => {
614
+ const info = catalog.lookup(origin.provider, origin.model);
615
+ if (!info?.pricing) return undefined;
616
+ const n = { input: usage.input, output: usage.output, cacheRead: usage.cacheRead ?? 0, cacheWrite: usage.cacheWrite ?? 0 };
617
+ return costUsdTiered(n, ratesFor(info, n.input + n.cacheRead + n.cacheWrite));
618
+ };
619
+ // the finish check (core/loop.ts "done" exit): on by default, ROVECODE_FINISH_CHECK=0 is the escape hatch;
620
+ // the todo state is read from disk at the exit so a list the model wrote this run is what gets reported
621
+ const todoState = (): { open: number; total: number } | null => {
622
+ try {
623
+ const items = loadTodos(join(sessionsDir, activeStore.id)).items;
624
+ return items.length === 0 ? null : { open: items.filter((i) => i.status !== "completed").length, total: items.length };
625
+ } catch { return null; }
626
+ };
627
+ // the verify gate (core/verify-gate.ts): OFF unless ROVECODE_VERIFY=1. Measured 2026-09-06 (nimbus-6f): the
628
+ // projects actually edited with rovecode have no check command at all (23 of 23 edits), and where one exists it
629
+ // costs 3 s to 170 s — a gate that spends three minutes on a six-line CSS edit gets turned off and never comes
630
+ // back. The default lives in this ONE comparison so flipping it later is editing this line. The check is
631
+ // resolved per run (a key added to settings mid-session counts next run); ROVECODE_VERIFY_TIMEOUT=<seconds>
632
+ // bounds one command, default 120.
633
+ const verifyOn = process.env.ROVECODE_VERIFY === "1";
634
+ const verifyTimeoutMs = (positiveInt(process.env.ROVECODE_VERIFY_TIMEOUT) ?? VERIFY_TIMEOUT_MS / 1000) * 1000;
635
+ const verifyGate = (): RunConfig["verify"] => {
636
+ const resolution = (opts.verifyResolver ?? resolveForGate)(cwd);
637
+ return {
638
+ resolution, timeoutMs: verifyTimeoutMs,
639
+ run: async (signal) => { const o = await runVerify(resolution ?? { commands: [] }, cwd, { signal, timeoutMs: verifyTimeoutMs }); noteVerifyCost(cwd, o); return o; },
640
+ };
641
+ };
642
+ return (activeCfg = {
643
+ maxTurns: runLimits.maxTurns ?? positiveInt(process.env.ROVECODE_MAX_TURNS) ?? 60,
644
+ ...(verifyOn ? { verify: verifyGate() } : {}),
645
+ ...(maxSeconds !== undefined ? { maxSeconds } : {}),
646
+ ...(maxCostUsd !== undefined ? { maxCostUsd, priceUsd } : {}),
647
+ finishCheck: process.env.ROVECODE_FINISH_CHECK !== "0",
648
+ todoState,
649
+ // the history budget follows the model's window: a flat 200k spent a fifth of a 1M window and
650
+ // overflowed a 128k one. ROVECODE_CONTEXT_BUDGET overrides; an unknown window keeps the old default.
651
+ contextBudgetTokens: (() => {
652
+ const ref = activeModel ?? fallbackRef;
653
+ const cur = catalog.lookup(ref.provider, ref.model);
654
+ return contextBudgetFor({
655
+ ...(cur?.contextWindow !== undefined ? { window: cur.contextWindow } : {}),
656
+ ...(cur?.maxOutput !== undefined ? { maxOutput: cur.maxOutput } : {}),
657
+ ...(positiveInt(process.env.ROVECODE_CONTEXT_BUDGET) !== undefined ? { override: positiveInt(process.env.ROVECODE_CONTEXT_BUDGET) as number } : {}),
658
+ // our estimator is not this model's tokenizer, so a budget taken at face value compacts too
659
+ // late and the request that follows is rejected. charScale, not scale: what this budget is
660
+ // compared against is estimateTokens (chars/4) in loop.ts and compaction.ts, never countTokens.
661
+ scale: tokenScaleFor(ref).charScale,
662
+ });
663
+ })(),
664
+ compactionThreshold: 0.8,
665
+ compactionStrategy: parseCompactionStrategy(process.env.ROVECODE_COMPACTION) ?? "head-summarize", // port #25: ROVECODE_COMPACTION=head-summarize|keep-window|provider-native
666
+ parallelTools: true,
667
+ permissionRules: yolo
668
+ ? [{ action: "*", resource: "*", effect: "allow" }]
669
+ : [
670
+ { action: "file.read", resource: "*", effect: "allow" },
671
+ { action: "memory.write", resource: "*", effect: "allow" },
672
+ { action: "tool.skill_view", resource: "*", effect: "allow" },
673
+ { action: "tool.skills_list", resource: "*", effect: "allow" },
674
+ // mcp_list is kind:"read" → action "file.read"; the allow above already covers it
675
+ { action: "file.write", resource: "*", effect: "prompt" },
676
+ { action: "shell.exec", resource: "*", effect: "prompt" },
677
+ { action: "spawn", resource: "*", effect: "prompt" },
678
+ { action: "tool.mcp_call", resource: "*", effect: "prompt" },
679
+ // provider_edit (tools/provider.ts) rewrites providers.json / the default model: ask first.
680
+ // provider_list is kind read → covered by the file.read allow above
681
+ { action: "tool.provider_edit", resource: "*", effect: "prompt" },
682
+ // design_direction set writes .rovecode/design.json -- the once-per-project design identity,
683
+ // and that ONE card is the point: it is where the human sees what is recorded for them.
684
+ // `get` only reads that file, so it is allowed (last match wins): making the human approve
685
+ // the read costs an interruption before every UI task AND trains them to allow the card
686
+ // reflexively, which is the card that matters. The two modes are told apart by the tool's
687
+ // own resource() (tools/design.ts), not by the tool name.
688
+ { action: "tool.design_direction", resource: "*", effect: "prompt" },
689
+ { action: "tool.design_direction", resource: "get", effect: "allow" },
690
+ // port #31: resource = canonical host (lowercased, no trailing dot), so `allow
691
+ // net.fetch <host>` auto-runs THAT host only; web_fetch stops at a redirect to
692
+ // another host and reports it, so the new host gets its own decision here
693
+ { action: "net.fetch", resource: "*", effect: "prompt" },
694
+ // accept-edits: writing INSIDE the workspace stops asking. Placed last of the file.write
695
+ // rules because the last match wins (tools.ts evaluatePermissions) — a write outside the
696
+ // repo still hits the prompt rule above, and any deny rule a surface appends still wins.
697
+ ...(level === "accept-edits" ? [{ action: "file.write", resource: insideCwd, effect: "allow" as const }] : []),
698
+ ],
699
+ // port #9: execpolicy refines the PROMPT branch only (allow-listed argv →
700
+ // "once", forbidden → deny before any human); rules above stay the outer gate.
701
+ // The wrap is UNCONDITIONAL on gated configs (R2 #9 LOW-3): headless surfaces
702
+ // (run/serve pass no approver) get allow-list auto-run + forbidden hard-stop,
703
+ // and prompt-classified argv fails closed instead of "no approver connected".
704
+ // yolo stays approver-free — its allow-all rules never reach the prompt branch.
705
+ // port #29: the approval hook sits INSIDE the wrap, where the human would — a
706
+ // forbidden argv never reaches a hook, an allow-listed one never asks (hooks.ts).
707
+ approval: yolo ? undefined : execPolicyApprover(hooks.approver(approval)),
708
+ });
709
+ };
710
+
711
+ // port #26: background subagents. Children run through orchestrator runChild (the ONE
712
+ // agentLoop) with deps resolved at each start: the def/config of the run that STARTED
713
+ // the task (buildDef/buildCfg record them — every surface calls both right before its
714
+ // agentLoop, so a child inherits its parent's model and policy; deriveChildRules turns
715
+ // prompt→deny). ONE SteeringQueue per runtime: surfaces hand it to agentLoop and
716
+ // completion notes land in the parent's next turn (loop.ts:136). Children get the core
717
+ // coding/search/skill tools (no MCP/memory/eval-cell/checkpoints in v1) plus nested
718
+ // `task` (kind spawn, bound to THEIR depth + steering queue, so the depth cap governs
719
+ // nesting) and `task_status` (kind read: a child collects ITS children's results without
720
+ // a prompt nobody could answer — MED-2 split, tools/task.ts header).
721
+ let activeCfg: RunConfig | null = null;
722
+ let activeModel: ModelRef | null = null;
723
+ const steering = new SteeringQueue();
724
+ const childRegistry = (_def: AgentDefinition, _cwd: string, child?: ChildContext): ToolRegistry => {
725
+ const reg = new ToolRegistry();
726
+ reg.register(readTool, editTool, writeTool, bashTool, globTool, grepTool, lsTool, ...createSkillTools(skillStore), recallTool(sessionsDir));
727
+ if (child) reg.register(createTaskTool(tasks, { parentDepth: child.depth, notify: child.steering, caller: child.taskId, owner: child.signal }), createTaskStatusTool(tasks, { caller: child.taskId }));
728
+ return reg;
729
+ };
730
+ const tasks = new TaskManager({
731
+ deps: (): ChildRunnerDeps | null => stream ? {
732
+ defs: new Map([["main", buildDef(activeModel ?? fallbackRef)]]),
733
+ stream, registryFactory: childRegistry, rootDir: cwd, sessionsDir,
734
+ baseConfig: activeCfg ?? buildCfg(false),
735
+ hooks, // port #29: children run under the runtime's hooks (a veto cannot be dodged by delegation)
736
+ } : null,
737
+ });
738
+ tasks.attach(steering);
739
+ // port #28: built-in reflection set (core/reflection.ts) — a failed edit/write (or an LSP-diagnosed one) nudges the model once via steering, capped per run (ROVECODE_REFLECTION_MAX); ROVECODE_REFLECTION=0 disables.
740
+ // owns: the ACTIVE session's runs only — a task child (own store id, same hooks) must neither nudge nor sweep this queue (#26 MED-A)
741
+ if (reflectionEnabled()) hooks.add(createReflectionHooks({ steering, owns: (c) => c.sessionId === activeStore.id }), "reflection");
742
+ registry.register(createTaskTool(tasks, { parentDepth: 0 }), createTaskStatusTool(tasks)); // task: kind spawn → gated rules prompt once per start, yolo allows; task_status: kind read → allowed everywhere
743
+
744
+ return {
745
+ cwd, sessionId, store, registry, skillStore,
746
+ get blockStore() { return blocks; },
747
+ setBlockStore(b: BlockStore) { blocks = b; registry.register(memoryEditTool(b)); },
748
+ guard, planReminder: planReminderFor, get mcp() { return mcp; }, reloadMcp, projectContext, router,
749
+ get effort() { return effort; },
750
+ setEffort(e: ThinkingEffort) { effort = e; },
751
+ setRunLimits(l: RunLimits) { runLimits = l; },
752
+ drainRouterNotes: () => routerNotes.splice(0),
753
+ onRouterNote(fn) { for (const n of routerNotes.splice(0)) fn(n); routerListeners.push(fn); },
754
+ checkpointsFor,
755
+ setSessionStore(s: SessionStore) { activeStore = s; },
756
+ sandbox,
757
+ setAskUser(fn: AskFn | undefined) { askUser = fn; },
758
+ hooks,
759
+ plugins,
760
+ providers,
761
+ get provider() { return providers.defaultConfig(); },
762
+ stream,
763
+ get defaultModel() { return providers.defaultRef()?.model ?? process.env.ROVECODE_MODEL ?? ""; },
764
+ noProviderReason: () => (opts.stream === undefined && !providers.configured() ? NO_PROVIDER_HINT : null),
765
+ systemPrompt,
766
+ buildDef, buildCfg, warmRepoMap,
767
+ steering, tasks,
768
+ };
769
+ }
770
+
771
+ /** The one sentence every surface shows when nothing is configured (Runtime.noProviderReason). */
772
+ export const NO_PROVIDER_HINT = noModelHint("cli");
773
+
774
+ /** port #27: construct + await the sandbox probe — the boot path for every
775
+ * entrypoint that must fail CLEANLY at startup (run/repl/tui/acp/serve).
776
+ * createRuntime stays sync (its many callers/tests build synchronously); this
777
+ * is the one place the async verdict is joined. Throws SandboxConfigError
778
+ * (one actionable line) for a bad config or a configured rung the machine
779
+ * cannot provide; MCP children spawned during construction are reaped first,
780
+ * so a failed boot leaves no processes behind. */
781
+ export async function bootRuntime(opts: RuntimeOptions = {}): Promise<Runtime> {
782
+ const rt = createRuntime(opts);
783
+ try {
784
+ await rt.sandbox.ready;
785
+ await rt.hooks.ready; // port #29: hook files + session_open joined here too (notes recorded before the first prompt)
786
+ await rt.plugins.ready; // plugin entry modules imported, their tools and hooks attached — before any surface's first prompt
787
+ } catch (e) {
788
+ await rt.mcp?.close().catch(() => {});
789
+ throw e;
790
+ }
791
+ return rt;
792
+ }