rovecode 0.4.0-beta.3 → 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 (432) hide show
  1. package/README.md +57 -72
  2. package/THIRD_PARTY_NOTICES.md +0 -44
  3. package/bin/rovecode.ts +21 -0
  4. package/package.json +16 -38
  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 -527
  270. package/bin/rovecode.js +0 -24
  271. package/dist/cli/app-j6gn14w3.js +0 -2
  272. package/dist/cli/ask-user-cwstt8fz.js +0 -2
  273. package/dist/cli/auth-login-9bbp9915.js +0 -2
  274. package/dist/cli/auth-m8p9grty.js +0 -2
  275. package/dist/cli/bench-16zqdms5.js +0 -9
  276. package/dist/cli/catalog-1xchffa4.js +0 -2
  277. package/dist/cli/cli-1n1zb64f.js +0 -2
  278. package/dist/cli/client-2t9gjkck.js +0 -2
  279. package/dist/cli/commands-exafvm2b.js +0 -2
  280. package/dist/cli/connect-6zde0kn3.js +0 -2
  281. package/dist/cli/context-cmd-5t43wgqt.js +0 -2
  282. package/dist/cli/context-report-kt01pw8y.js +0 -2
  283. package/dist/cli/count-remote-ap7x3vh6.js +0 -2
  284. package/dist/cli/design-ne5zszyh.js +0 -2
  285. package/dist/cli/dispatch-2r5myxye.js +0 -2
  286. package/dist/cli/doctor-ws4fh4tn.js +0 -3
  287. package/dist/cli/executor-bdrjn634.js +0 -2
  288. package/dist/cli/export-1mxb9g5p.js +0 -2
  289. package/dist/cli/files-g104xghh.js +0 -2
  290. package/dist/cli/gauntlet-07xrjpj7.js +0 -2
  291. package/dist/cli/gauntlet-runner-xvy64436.js +0 -10
  292. package/dist/cli/gauntlet-wave3-jm91yt5w.js +0 -5
  293. package/dist/cli/gauntlet-wave4-r13py7p1.js +0 -14
  294. package/dist/cli/hashline-znvrat11.js +0 -2
  295. package/dist/cli/http-xafw6fsh.js +0 -143
  296. package/dist/cli/index-1sgjm25y.js +0 -2
  297. package/dist/cli/init-g2m0tn4m.js +0 -51
  298. package/dist/cli/install-avaqjjqq.js +0 -2
  299. package/dist/cli/loop-mmpfft01.js +0 -2
  300. package/dist/cli/main-0904f6ps.js +0 -5
  301. package/dist/cli/main-0ab9fc26.js +0 -9
  302. package/dist/cli/main-0jys2ccn.js +0 -3
  303. package/dist/cli/main-0mtcdbs7.js +0 -3
  304. package/dist/cli/main-0z1w2zsg.js +0 -3
  305. package/dist/cli/main-1dchs7xv.js +0 -18
  306. package/dist/cli/main-1ereejm1.js +0 -3
  307. package/dist/cli/main-1k1kw6b5.js +0 -3
  308. package/dist/cli/main-27y4sm2k.js +0 -38
  309. package/dist/cli/main-2wwjex5j.js +0 -58
  310. package/dist/cli/main-2yeveeve.js +0 -6
  311. package/dist/cli/main-2yfck9b5.js +0 -3
  312. package/dist/cli/main-2zmzgkwh.js +0 -3
  313. package/dist/cli/main-351pz3z7.js +0 -7
  314. package/dist/cli/main-3gjqfh7a.js +0 -6
  315. package/dist/cli/main-3nf3kgve.js +0 -3
  316. package/dist/cli/main-3pjrb2hd.js +0 -3
  317. package/dist/cli/main-3rxcvgna.js +0 -19
  318. package/dist/cli/main-4b3jgy66.js +0 -19
  319. package/dist/cli/main-4wndhjdc.js +0 -7
  320. package/dist/cli/main-4xcmvxnk.js +0 -3
  321. package/dist/cli/main-5tbz0wbz.js +0 -4
  322. package/dist/cli/main-5ywnwthm.js +0 -3
  323. package/dist/cli/main-6b62vkz0.js +0 -14
  324. package/dist/cli/main-6dnk69vp.js +0 -3
  325. package/dist/cli/main-6genrmhs.js +0 -136
  326. package/dist/cli/main-73g7eff4.js +0 -15
  327. package/dist/cli/main-7c5thhjd.js +0 -5
  328. package/dist/cli/main-7rn6bqje.js +0 -3
  329. package/dist/cli/main-80haw7qk.js +0 -4
  330. package/dist/cli/main-875s60s2.js +0 -4
  331. package/dist/cli/main-8kjxbpw4.js +0 -8
  332. package/dist/cli/main-90ds1z4e.js +0 -10
  333. package/dist/cli/main-9etavkew.js +0 -3
  334. package/dist/cli/main-a9njrkk1.js +0 -3
  335. package/dist/cli/main-aecrjq2d.js +0 -12
  336. package/dist/cli/main-ck9asesq.js +0 -9
  337. package/dist/cli/main-cta9racd.js +0 -4
  338. package/dist/cli/main-ddv7j2ag.js +0 -3
  339. package/dist/cli/main-dfreez27.js +0 -10
  340. package/dist/cli/main-f7rw7des.js +0 -3
  341. package/dist/cli/main-ggcn7rd7.js +0 -5
  342. package/dist/cli/main-gzkmycnv.js +0 -3
  343. package/dist/cli/main-hq51jg8v.js +0 -18
  344. package/dist/cli/main-jft389w9.js +0 -8
  345. package/dist/cli/main-k1eqkg83.js +0 -3
  346. package/dist/cli/main-k2y8a2aw.js +0 -9
  347. package/dist/cli/main-kcpbykxz.js +0 -4
  348. package/dist/cli/main-kd488vje.js +0 -22
  349. package/dist/cli/main-kh32yvgk.js +0 -5
  350. package/dist/cli/main-kqxnqjnv.js +0 -25
  351. package/dist/cli/main-kyn0xnsg.js +0 -3
  352. package/dist/cli/main-m1kk6fp5.js +0 -21
  353. package/dist/cli/main-mv40pcr2.js +0 -4
  354. package/dist/cli/main-n0t3973w.js +0 -3
  355. package/dist/cli/main-nqveez48.js +0 -4
  356. package/dist/cli/main-pknhvrmj.js +0 -3
  357. package/dist/cli/main-pn1w7a7j.js +0 -3
  358. package/dist/cli/main-prxxs70n.js +0 -4
  359. package/dist/cli/main-q3vsesf9.js +0 -3
  360. package/dist/cli/main-qsevpgsv.js +0 -3
  361. package/dist/cli/main-rdgdw24b.js +0 -25
  362. package/dist/cli/main-rfth4tbm.js +0 -16
  363. package/dist/cli/main-rg0wn0xf.js +0 -5
  364. package/dist/cli/main-sdmxhtv8.js +0 -4
  365. package/dist/cli/main-skbp13js.js +0 -18
  366. package/dist/cli/main-t4xnd213.js +0 -7
  367. package/dist/cli/main-vqak588n.js +0 -4
  368. package/dist/cli/main-w2n1303f.js +0 -9
  369. package/dist/cli/main-wbrdspr2.js +0 -5
  370. package/dist/cli/main-wsrg79c1.js +0 -7
  371. package/dist/cli/main-x4r0fne4.js +0 -5
  372. package/dist/cli/main-xea2f3tn.js +0 -6
  373. package/dist/cli/main-xg704a3c.js +0 -3
  374. package/dist/cli/main-xvnrabfp.js +0 -16
  375. package/dist/cli/main-xy53xf0r.js +0 -4
  376. package/dist/cli/main-y1fqy60y.js +0 -3
  377. package/dist/cli/main-yn8cd281.js +0 -34
  378. package/dist/cli/main-yr0ksc0h.js +0 -4
  379. package/dist/cli/main-z2ex2vyf.js +0 -4
  380. package/dist/cli/main-z3aayzvq.js +0 -3
  381. package/dist/cli/main-zaqh35jg.js +0 -3
  382. package/dist/cli/main-zc2e8e46.js +0 -4
  383. package/dist/cli/main-zzrfw6cf.js +0 -13
  384. package/dist/cli/main.js +0 -280
  385. package/dist/cli/market-cmd-e14kmx9n.js +0 -5
  386. package/dist/cli/mcp-login-wq7ktdek.js +0 -2
  387. package/dist/cli/mcp-market-cmd-9mg3jecy.js +0 -2
  388. package/dist/cli/notify-b7qc0cjb.js +0 -2
  389. package/dist/cli/oauth-z8whcgfx.js +0 -2
  390. package/dist/cli/output-b3ewj3ps.js +0 -16
  391. package/dist/cli/profiles-6mr5he5e.js +0 -2
  392. package/dist/cli/provider-config-g7j42q8x.js +0 -2
  393. package/dist/cli/provider-jr1y8vvm.js +0 -2
  394. package/dist/cli/registry-s8yk86g0.js +0 -2
  395. package/dist/cli/registry-t6p8d4mn.js +0 -2
  396. package/dist/cli/repl-bajwe1mh.js +0 -11
  397. package/dist/cli/resume-rwn9nz7y.js +0 -2
  398. package/dist/cli/run-flags-nah7ndpt.js +0 -2
  399. package/dist/cli/runtime-n7gafzhb.js +0 -2
  400. package/dist/cli/sandbox-config-emdy18x4.js +0 -2
  401. package/dist/cli/server-b0nvs2bn.js +0 -5
  402. package/dist/cli/session-arg-y75wd4kj.js +0 -2
  403. package/dist/cli/session-j62evmjq.js +0 -2
  404. package/dist/cli/sessions-cmd-tsnwz0ns.js +0 -7
  405. package/dist/cli/settings-df10wfez.js +0 -2
  406. package/dist/cli/setup-jzvv72fg.js +0 -2
  407. package/dist/cli/sextant-smoke-37m81ke6.js +0 -5
  408. package/dist/cli/skills-cmd-gjxnxnhx.js +0 -2
  409. package/dist/cli/smoke-p7748apt.js +0 -8
  410. package/dist/cli/start-chat-s4st3mm0.js +0 -12
  411. package/dist/cli/stream-gmeyewds.js +0 -2
  412. package/dist/cli/task-gh0kkp3n.js +0 -2
  413. package/dist/cli/tasks-z1kfpe8e.js +0 -2
  414. package/dist/cli/thinking-0eqkrz6t.js +0 -2
  415. package/dist/cli/todo-5brcrt9m.js +0 -2
  416. package/dist/cli/tools-7pzm0vj9.js +0 -2
  417. package/dist/cli/tools-s635p6s8.js +0 -2
  418. package/dist/cli/trust-cmd-cjav8zgm.js +0 -2
  419. package/dist/cli/update-check-pt31bm2f.js +0 -2
  420. package/dist/cli/update-cmd-tk131s9t.js +0 -2
  421. package/dist/cli/voice-56nabd8d.js +0 -2
  422. package/dist/cli/webfetch-xd8q596m.js +0 -2
  423. package/dist/cli/websearch-5hkf98k1.js +0 -2
  424. package/dist/cli/workflow-cmd-cy3cvzjp.js +0 -4
  425. package/dist/cli/workspace-q10g5z3e.js +0 -2
  426. package/dist/lib/index.js +0 -62
  427. package/dist/lib/models-index.json +0 -1
  428. package/dist/lib/plugins.js +0 -55
  429. package/dist/lib/providers.js +0 -17
  430. package/dist/lib/public-api.js +0 -20
  431. package/dist/lib/sdk.js +0 -360
  432. /package/{dist/cli → src/providers}/models-index.json +0 -0
@@ -0,0 +1,108 @@
1
+ /** How wrong our token estimate is for a given model, measured rather than assumed.
2
+ *
3
+ * rovecode budgets with two estimators and neither is the tokenizer that decides: `estimateTokens`
4
+ * (chars/4) drives compaction, `countTokens` (o200k, OpenAI's tokenizer) drives the reports. For an
5
+ * OpenAI model o200k is the truth by construction. For everyone else it is a stand-in, and the size of
6
+ * the error is not a detail — compaction fires on the estimate, so an estimate that reads low compacts
7
+ * too late and the provider rejects the request that follows.
8
+ *
9
+ * The numbers below were measured with `bun scripts/measure-tokenizer.ts`, which sends real samples to
10
+ * Anthropic's own `/v1/messages/count_tokens` and divides. Rerun it to check them; that is the point of
11
+ * keeping the script. Measured 2026-09-05 over twenty samples — the system prompt, the tool schemas, a
12
+ * TypeScript file, English prose, Turkish prose, a JSON tool result, and fourteen market documentation
13
+ * bodies:
14
+ *
15
+ * claude-opus-5, claude-sonnet-5 chars/4 1.33–1.81× o200k 1.43–1.79×
16
+ * claude-haiku-4-5 chars/4 1.00–1.46× o200k 1.08–1.29×
17
+ *
18
+ * The market documents were added after nimbus-24, independently measuring 19 skill bodies, found
19
+ * ratios above the ceiling the first six samples produced. They were right, and the reason matters:
20
+ * markdown documentation — headings, bullets, fenced code and tables in one file — tokenizes worse
21
+ * than either prose or code alone, and it is precisely what lands in a window when a model opens a
22
+ * skill. Their worst case (mcp-builder, 1.792) reproduces here at 1.791. A sample set that omits the
23
+ * commonest content is not conservative; it is wrong in the expensive direction.
24
+ *
25
+ * Two things that table settles. The Claude 5 models share one tokenizer — Opus 5 and Sonnet 5 returned
26
+ * identical counts on every sample — so the factor belongs to a generation, not to a model name.
27
+ * And 4.5 is a different, older one: applying the 5-generation figure to Haiku would over-correct by two
28
+ * fifths, which wastes window rather than overflowing it, but is still a wrong number.
29
+ *
30
+ * Each factor is the **maximum** ratio across the samples, not the mean. The two errors are not
31
+ * symmetric: a budget that reads high merely leaves some window unused, while one that reads low sends
32
+ * a request the provider refuses and loses the turn. Rounded up to two decimals, and rounded up rather
33
+ * than to nearest, for the same reason.
34
+ *
35
+ * A model nobody has measured gets 1 and says so. A guessed multiplier is worse than none: it moves the
36
+ * budget by an amount whose provenance no one can explain, and hides the fact that the number is unknown. */
37
+
38
+ export interface TokenScale {
39
+ /** multiply an o200k estimate (`countTokens`) by this to approximate what the provider will count;
40
+ * 1 when unmeasured */
41
+ scale: number;
42
+ /** the same for the OTHER estimator, `estimateTokens` (chars/4), which is what compaction and context
43
+ * assembly measure with. It is a different approximation with a different error, so it needs its own
44
+ * number: on Claude 4.5 the o200k figure is 1.29x while chars/4 needs 1.46x, and using the first to
45
+ * size a budget the second is compared against under-corrects by an eighth — which is the direction
46
+ * that overflows the window. Measured in the same run, from the same samples. */
47
+ charScale: number;
48
+ /** true when the number came from a measurement rather than from the default */
49
+ measured: boolean;
50
+ /** one line for the reader: where the number is from, or that there is none */
51
+ note: string;
52
+ }
53
+
54
+ const UNMEASURED: TokenScale = {
55
+ scale: 1,
56
+ charScale: 1,
57
+ measured: false,
58
+ note: "no measurement for this model — the estimate is used as-is; bun scripts/measure-tokenizer.ts measures it",
59
+ };
60
+
61
+ /** o200k is OpenAI's own tokenizer, so `scale` is exactly 1 — but chars/4 is nobody's tokenizer, and
62
+ * the budget is compared against chars/4. Left at 1 because it has not been measured against an
63
+ * OpenAI model, and an unmeasured number is what this file refuses to invent. */
64
+ const EXACT: TokenScale = {
65
+ scale: 1,
66
+ charScale: 1,
67
+ measured: true,
68
+ note: "o200k is this vendor's own tokenizer — the estimate is exact",
69
+ };
70
+
71
+ interface Row { provider: string; match: RegExp; scale: number; charScale: number; note: string }
72
+
73
+ const MEASURED: Row[] = [
74
+ {
75
+ provider: "anthropic",
76
+ // the 5 generation: opus-5, sonnet-5, fable-5.x, and the dated snapshots of each
77
+ match: /(opus-5|sonnet-5|fable-5|mythos-5)/,
78
+ scale: 1.80,
79
+ charScale: 1.82,
80
+ note: "measured 2026-09-05 against /v1/messages/count_tokens over 20 samples — o200k reads up to 1.79× low on Claude 5",
81
+ },
82
+ {
83
+ provider: "anthropic",
84
+ match: /(haiku-4-5|opus-4-5|sonnet-4-6)/,
85
+ scale: 1.29,
86
+ charScale: 1.46,
87
+ note: "measured 2026-09-05 against /v1/messages/count_tokens over 20 samples — o200k reads up to 1.29× low on Claude 4.5/4.6",
88
+ },
89
+ ];
90
+
91
+ /** OpenAI's own models are counted with OpenAI's own tokenizer; there is nothing to correct. */
92
+ const EXACT_PROVIDERS = new Set(["openai"]);
93
+
94
+ /** The scale for a model, by provider and model id. Unknown models get 1 with a note saying so —
95
+ * never a factor borrowed from a neighbouring model, which would be a guess dressed as a measurement. */
96
+ export function tokenScaleFor(ref: { provider: string; model: string }): TokenScale {
97
+ const provider = ref.provider.toLowerCase();
98
+ const model = ref.model.toLowerCase();
99
+ if (EXACT_PROVIDERS.has(provider)) return EXACT;
100
+ const row = MEASURED.find((r) => r.provider === provider && r.match.test(model));
101
+ return row ? { scale: row.scale, charScale: row.charScale, measured: true, note: row.note } : UNMEASURED;
102
+ }
103
+
104
+ /** An estimate corrected towards what the provider will count. Rounds up: a token of slack costs
105
+ * nothing, a token of shortfall is a rejected request. */
106
+ export function scaleEstimate(tokens: number, ref: { provider: string; model: string }): number {
107
+ return Math.ceil(tokens * tokenScaleFor(ref).scale);
108
+ }
@@ -0,0 +1,166 @@
1
+ /** Unified tool-output budget policy (P0-3; research: docs/research/harness-architecture-research.md G3).
2
+ *
3
+ * Before this module every tool invented its own ceiling — bash truncates at 10k chars
4
+ * (coding/hashline.ts), MCP at OUTPUT_MAX (mcp/client.ts), webfetch at MAX_BYTES, evalcell at
5
+ * 64 KiB, and `read` (2000 lines of unbounded width — one minified line is unbounded output)
6
+ * had none. The strategy differed too: hard clips lose the tail, and the tail is where the
7
+ * error usually is (test summaries, stack traces, "n lines omitted" footers).
8
+ *
9
+ * One policy now backs them all:
10
+ * - at/under the cap the output passes BYTE-VERBATIM — a tool result the policy did nothing
11
+ * to is byte-identical, so nothing downstream (reflection, guardrails, transcripts) sees
12
+ * a difference;
13
+ * - over the cap the MIDDLE goes: head + tail are kept (the tail carries the diagnosis, the
14
+ * head carries the orientation), joined by a marker that names what was removed — chars
15
+ * and their estimate in the loop's own unit (estimateTokens, chars/4) — and how to see the
16
+ * omitted span (a narrower range/query; the read tool's offset/limit);
17
+ * - the cut is UTF-8/grapheme safe: it never splits a surrogate pair, a combining-mark
18
+ * sequence, a ZWJ emoji chain or a flag pair. A boundary that lands inside one backs off
19
+ * to the last whole grapheme — the model is never handed half an emoji.
20
+ *
21
+ * Where it runs: the loop applies it ONCE per batch to the results it persists and re-sends
22
+ * (core/loop.ts, after dispatchBatch settles). Tool-side caps stay — a tighter tool cap wins
23
+ * by construction (its output is already under the ceiling). The live `tool_execution_end`
24
+ * event carries the RAW output (surface fidelity; the human can handle a wall of text, the
25
+ * context window cannot) — an accepted, documented divergence.
26
+ *
27
+ * Sizing: DEFAULT_OUTPUT_CAP is a ceiling, not a target — it exists for the pathological
28
+ * output (a 2 MB log cat'd whole), not to shave normal ones. 32 KiB ≈ 8k tokens ≈ the room
29
+ * a whole small file takes; beyond it the middle of a tool result is almost never what the
30
+ * next turn needs. */
31
+
32
+ import { estimateTokens } from "./context.ts";
33
+
34
+ export interface OutputBudgetOptions {
35
+ /** ceiling for a tool result in UTF-16 code units (string.length), default DEFAULT_OUTPUT_CAP */
36
+ defaultCap?: number;
37
+ /** per-tool ceilings; unlisted tools get defaultCap. A tool's own tighter cap wins by
38
+ * construction (its output never reaches the ceiling). */
39
+ perTool?: Record<string, number>;
40
+ /** share of the kept budget spent on the head; the tail gets the rest (0..1, default 0.7) */
41
+ headShare?: number;
42
+ }
43
+
44
+ export interface TruncatedOutput {
45
+ text: string;
46
+ truncated: boolean;
47
+ originalChars: number;
48
+ /** chars of payload kept (head + tail, marker excluded) */
49
+ keptChars: number;
50
+ /** removed payload, estimated with the loop's own unit (estimateTokens) */
51
+ estTokensCut: number;
52
+ }
53
+
54
+ export const DEFAULT_OUTPUT_CAP = 32_768;
55
+ export const DEFAULT_HEAD_SHARE = 0.7;
56
+ /** the marker opens with this — tests and the idempotence note key on it */
57
+ export const TRUNCATION_MARK = "[…output budget:";
58
+
59
+ const ZWJ = "‍";
60
+ const isCombining = (ch: string): boolean => /\p{M}/u.test(ch);
61
+ const isRI = (ch: string): boolean => /\p{Regional_Indicator}/u.test(ch);
62
+
63
+ /** Whole-grapheme slice by UTF-16 budget: take code points until `units` are spent, then back
64
+ * the boundary off so it never lands inside a combining sequence, a ZWJ chain, or a flag pair. */
65
+ function takeUnits(cps: string[], from: number, dir: 1 | -1, units: number): number {
66
+ let i = from;
67
+ let spent = 0;
68
+ while (i >= 0 && i < cps.length) {
69
+ const w = cps[i]!.length; // 1 or 2 UTF-16 units — a code point is never split
70
+ if (spent + w > units) break;
71
+ spent += w;
72
+ i += dir;
73
+ }
74
+ // back off: not after a ZWJ, not before/after a combining mark run, not inside a flag pair
75
+ if (dir === 1) {
76
+ let end = i; // exclusive
77
+ while (end > from && (cps[end - 1] === ZWJ || (end < cps.length && isCombining(cps[end]!)))) end--;
78
+ // flag pairs: an odd run of regional indicators before the cut means the last one is unpaired
79
+ if (end > from && end < cps.length && isRI(cps[end - 1]!) && isRI(cps[end]!)) {
80
+ let run = 0;
81
+ for (let j = end - 1; j >= 0 && isRI(cps[j]!); j--) run++;
82
+ if (run % 2 === 1) end--;
83
+ }
84
+ return end;
85
+ }
86
+ let start = i + 1; // inclusive
87
+ while (start <= from && (cps[start] === ZWJ || isCombining(cps[start]!))) start++;
88
+ if (start > 0 && start < cps.length && isRI(cps[start - 1]!) && isRI(cps[start]!)) {
89
+ let run = 0;
90
+ for (let j = start; j < cps.length && isRI(cps[j]!); j++) run++;
91
+ if (run % 2 === 1) start++;
92
+ }
93
+ return start;
94
+ }
95
+
96
+ function marker(removedChars: number, originalChars: number): string {
97
+ const est = estimateTokens("x".repeat(removedChars));
98
+ return `\n\n${TRUNCATION_MARK} removed ${removedChars} of ${originalChars} chars (~${est} tokens) from the middle — re-run the tool with a narrower range/query (read: offset/limit) to see any part in full…]\n\n`;
99
+ }
100
+
101
+ /** The pure core: one output, one cap. Two-pass because the marker names the cut and is itself
102
+ * bounded by the cap — the second pass sizes the cut against the real marker length. */
103
+ export function applyBudget(output: string, cap: number, headShare: number = DEFAULT_HEAD_SHARE): TruncatedOutput {
104
+ const originalChars = output.length;
105
+ const verbatim: TruncatedOutput = { text: output, truncated: false, originalChars, keptChars: originalChars, estTokensCut: 0 };
106
+ if (originalChars <= cap) return verbatim;
107
+ if (cap < 32) {
108
+ // degenerate: no room for a useful marker — hard-clip with an ellipsis, still bounded
109
+ const text = output.slice(0, Math.max(0, cap - 1)) + "…";
110
+ return { text, truncated: true, originalChars, keptChars: text.length, estTokensCut: estimateTokens("x".repeat(originalChars - text.length)) };
111
+ }
112
+ const cps = Array.from(output);
113
+ // pass 1 with an estimated marker, pass 2 with the real one (digit-count stable)
114
+ let mk = marker(0, originalChars);
115
+ let headEnd = 0, tailStart = cps.length;
116
+ for (let pass = 0; pass < 2; pass++) {
117
+ const room = Math.max(0, cap - mk.length);
118
+ const headUnits = Math.floor(room * Math.min(1, Math.max(0, headShare)));
119
+ headEnd = takeUnits(cps, 0, 1, headUnits);
120
+ tailStart = takeUnits(cps, cps.length - 1, -1, room - unitsOf(cps, 0, headEnd));
121
+ if (tailStart <= headEnd) tailStart = headEnd; // pathological: marker ≈ cap → no tail
122
+ const removedUnits = unitsOf(cps, headEnd, tailStart);
123
+ mk = marker(removedUnits, originalChars);
124
+ }
125
+ const head = cps.slice(0, headEnd).join("");
126
+ const tail = cps.slice(tailStart).join("");
127
+ const text = head + mk + tail;
128
+ const removed = originalChars - (head.length + tail.length);
129
+ return {
130
+ text,
131
+ truncated: true,
132
+ originalChars,
133
+ keptChars: head.length + tail.length,
134
+ estTokensCut: estimateTokens("x".repeat(removed)),
135
+ };
136
+ }
137
+
138
+ function unitsOf(cps: string[], from: number, to: number): number {
139
+ let n = 0;
140
+ for (let i = from; i < to; i++) n += cps[i]!.length;
141
+ return n;
142
+ }
143
+
144
+ /** The first `units` UTF-16 units of text, cut at a whole-grapheme boundary (compaction.ts's prune
145
+ * stub head reuses it — a stub prefix must never open with half an emoji either). */
146
+ export function safeHead(text: string, units: number): string {
147
+ const cps = Array.from(text);
148
+ return cps.slice(0, takeUnits(cps, 0, 1, units)).join("");
149
+ }
150
+
151
+ export interface ToolOutputBudgetPolicy {
152
+ capFor(tool: string): number;
153
+ apply(tool: string, output: string): TruncatedOutput;
154
+ }
155
+
156
+ /** The policy the loop holds for a run. `undefined` options = the defaults; the policy is a pure
157
+ * function of its options — no I/O, no clock, no state. */
158
+ export function createOutputBudget(opts: OutputBudgetOptions = {}): ToolOutputBudgetPolicy {
159
+ const defaultCap = opts.defaultCap ?? DEFAULT_OUTPUT_CAP;
160
+ const headShare = opts.headShare ?? DEFAULT_HEAD_SHARE;
161
+ const capFor = (tool: string): number => opts.perTool?.[tool] ?? defaultCap;
162
+ return {
163
+ capFor,
164
+ apply: (tool, output) => applyBudget(output, capFor(tool), headShare),
165
+ };
166
+ }
@@ -0,0 +1,288 @@
1
+ /** Tool pipeline: validate → revise (extension hooks) → policy → [pre_tool hook] → approve (the
2
+ * ApprovalFn chain: execpolicy → [approval hook] → human, composed in cli/runtime.ts buildCfg) →
3
+ * execute → [post_tool hook] (ADR-005; hooks v2 = port #29).
4
+ * Tool calls are recorded BEFORE execution; truncated responses fail calls unexecuted. */
5
+
6
+ import type {
7
+ Tool, ToolContext, ToolOutput, PermissionRule, PermissionDecision,
8
+ ApprovalRequest, ToolCallPart, RunEvent,
9
+ } from "./types.ts";
10
+ import type { ToolGuard } from "./guardrails.ts";
11
+ import { formatIssues, validateArgs } from "./validate.ts";
12
+ import { cloneForHook, type HookCtx, type HookRunner } from "./hooks.ts";
13
+ import { isAbsolute, join } from "node:path";
14
+
15
+ export interface ExtensionHooks {
16
+ /** May revise args; returns revised args (omp revision gate). */
17
+ reviseToolArgs?: (tool: string, args: unknown) => Promise<unknown>;
18
+ onToolResult?: (tool: string, args: unknown, out: ToolOutput) => Promise<void>;
19
+ /** port #29 typed hook set (core/hooks.ts; a HookRunner satisfies this seam): pre_tool / post_tool
20
+ * ride dispatch at the seams below, timeout-bounded + isolated by the runner; the approval hook
21
+ * rides the ApprovalFn chain instead (HookRunner.approver — after execpolicy, before the human); the
22
+ * loop taps the run-level hooks (pre_run / compaction / post_run / on_event) through observer(). */
23
+ run?: HookRunner["run"];
24
+ observer?: HookRunner["observer"];
25
+ }
26
+
27
+ /** Output of a tool_call that never executed because the run aborted (port
28
+ * #21): a queued sibling in an aborted batch, or an approval answered after
29
+ * the abort. Same text loop.ts synthesizes for calls a batch never delivered
30
+ * (opencode session/processor.ts:587; codex normalize.rs:51-67). */
31
+ export const ABORTED_TOOL_RESULT = "Tool execution aborted";
32
+ /** port #29: the small ctx hooks receive — cwd, session, run; no registry/store handles */
33
+ const hookCtx = (c: ToolContext): HookCtx =>
34
+ ({ cwd: c.cwd, sessionId: c.sessionId, ...(c.runId !== undefined ? { runId: c.runId } : {}) });
35
+
36
+ /** Deny-by-default wildcard rules, last match wins (opencode permission.ts:126). */
37
+ export function evaluatePermissions(rules: PermissionRule[], action: string, resource: string): PermissionDecision {
38
+ let decision: PermissionDecision = { effect: "deny", reason: `no rule allows ${action}` };
39
+ for (const r of rules) {
40
+ if (matchesGlob(r.action, action) && matchesGlob(r.resource, resource)) {
41
+ decision = r.effect === "allow" ? { effect: "allow" }
42
+ : r.effect === "deny" ? { effect: "deny", reason: `denied by rule ${r.action} ${r.resource}` }
43
+ : { effect: "prompt", prompt: `permission required for ${action} ${resource}` };
44
+ }
45
+ }
46
+ return decision;
47
+ }
48
+
49
+ function matchesGlob(pattern: string, value: string): boolean {
50
+ if (pattern === "*") return true;
51
+ const rx = new RegExp("^" + pattern.split("*").map(escapeRx).join(".*") + "$");
52
+ return rx.test(value);
53
+ }
54
+ function escapeRx(s: string): string { return s.replace(/[.+^${}()|[\]\\]/g, "\\$&"); }
55
+
56
+ export class ToolRegistry {
57
+ private tools = new Map<string, Tool>();
58
+ private approvalCache = new Map<string, "once" | "always">();
59
+
60
+ register(...tools: Tool[]): void { for (const t of tools) this.tools.set(t.schema.name, t); }
61
+ list(): Tool[] { return [...this.tools.values()]; }
62
+
63
+ /** ADR-005 ladder. Emits events; never lets tool exceptions escape as control flow. */
64
+ async dispatch(
65
+ call: ToolCallPart,
66
+ ctx: ToolContext,
67
+ hooks: ExtensionHooks | undefined,
68
+ rules: PermissionRule[],
69
+ approve: ((req: ApprovalRequest) => Promise<"once" | "always" | "deny">) | undefined,
70
+ emit: (e: RunEvent) => void,
71
+ guard?: ToolGuard,
72
+ ): Promise<ToolOutput> {
73
+ const t0 = Date.now();
74
+ const tool = this.tools.get(call.tool);
75
+ if (!tool) {
76
+ emit({ type: "tool_call_failed", callId: call.id, reason: "not_found", detail: `unknown tool ${call.tool}` });
77
+ return { ok: false, output: `Error: unknown tool '${call.tool}'` };
78
+ }
79
+
80
+ // 1. extension revision
81
+ let args = call.args;
82
+ if (hooks?.reviseToolArgs) args = await hooks.reviseToolArgs(call.tool, args);
83
+
84
+ // 1a. validate against the tool's OWN published schema (ADR-005's first step; core/validate.ts).
85
+ // After revision, so a hook that repairs args is judged on what it produced; before the guard and
86
+ // policy, because a call that cannot execute should not consume a loop-guard slot, an approval
87
+ // card, or the human's attention. The failure shape matches the others here: a tool_call_failed
88
+ // event and an ok:false result the model reads and corrects on the next turn.
89
+ const issues = validateArgs(tool.schema.args, args);
90
+ if (issues.length > 0) {
91
+ const detail = formatIssues(call.tool, issues);
92
+ emit({ type: "tool_call_failed", callId: call.id, reason: "invalid_args", detail });
93
+ return { ok: false, output: detail };
94
+ }
95
+
96
+ // 1b. loop guard (port #4, hermes): stub repeated identical calls BEFORE the user is
97
+ // prompted for them; warn notes ride along on the result
98
+ let warnNote: string | undefined;
99
+ if (guard) {
100
+ const verdict = guard.checkCall(call.tool, args);
101
+ if (verdict.action === "stub") {
102
+ const note = verdict.note ?? "call blocked by loop guard: identical call repeated too often";
103
+ emit({ type: "tool_execution_start", callId: call.id, tool: call.tool, args });
104
+ emit({ type: "tool_execution_end", callId: call.id, ok: false, output: note, durationMs: 0 });
105
+ return { ok: false, output: note };
106
+ }
107
+ if (verdict.action === "warn") warnNote = verdict.note;
108
+ }
109
+
110
+ // 2. policy (deny-default)
111
+ const resource = describeResource(tool, args, ctx.cwd);
112
+ const decision = evaluatePermissions(rules, actionFor(tool), resource);
113
+ if (decision.effect === "deny") {
114
+ emit({ type: "tool_call_failed", callId: call.id, reason: "permission_denied", detail: decision.reason });
115
+ return { ok: false, output: `Permission denied: ${decision.reason}` };
116
+ }
117
+
118
+ // 2b. pre_tool hook (port #29) — AFTER policy: the rule deny above is never un-denied and hooks
119
+ // never see rule-rejected calls; a hook deny applies in every mode incl. yolo (the user's own
120
+ // stricter layer) and takes the policy-deny failure shape with the hook's reason. Hooks see a COPY
121
+ // of the args (hooks.ts cloneForHook): mutating it cannot re-aim what policy just evaluated
122
+ const veto = await hooks?.run?.("pre_tool", hookCtx(ctx), { id: call.id, tool: call.tool, args: cloneForHook(args) });
123
+ if (veto) {
124
+ emit({ type: "tool_call_failed", callId: call.id, reason: "permission_denied", detail: veto.deny });
125
+ return { ok: false, output: `Permission denied by hook: ${veto.deny}` };
126
+ }
127
+
128
+ // 3. approval — always resolved against the REVISED args (omp wrapper.ts:205-247). The approver
129
+ // IS the chain: execpolicy refinement → approval hook → human (cli/runtime.ts buildCfg), so a
130
+ // hook is consulted only where the human would be — never ahead of a forbidden-argv hard stop
131
+ if (decision.effect === "prompt") {
132
+ // "always" is remembered by what the RULES are about — action + resource — not by the whole
133
+ // argument blob. Keyed on the args, an "always" on `write {path, content}` never matched again:
134
+ // the next write to the same file carries different content, so the cache missed and the card
135
+ // came back. The pair below is the same identity evaluatePermissions just decided on, so
136
+ // "always" now means what the card says: this action, on this file / this command / this host.
137
+ const key = approvalKey(actionFor(tool), resource, tool.schema.name, args);
138
+ const cached = this.approvalCache.get(key);
139
+ if (!cached) {
140
+ if (!approve) {
141
+ emit({ type: "tool_call_failed", callId: call.id, reason: "permission_denied", detail: "approval required but no approver connected" });
142
+ return { ok: false, output: "Permission denied: approval required, no approver available" };
143
+ }
144
+ const verdict = await approve({ tool: call.tool, args, revisedArgs: args, reason: decision.prompt });
145
+ if (verdict === "deny") {
146
+ emit({ type: "tool_call_failed", callId: call.id, reason: "permission_denied", detail: "user denied" });
147
+ return { ok: false, output: "Permission denied by user" };
148
+ }
149
+ // "once" means once: only "always" verdicts persist across calls
150
+ if (verdict === "always") this.approvalCache.set(key, verdict);
151
+ }
152
+ }
153
+
154
+ // 3b. abort re-check (port #21 LOW-1): the approver may answer long after
155
+ // the run aborted — the loop has already synthesized ABORTED for this call
156
+ // and returned, so executing now would run the tool detached from any run.
157
+ // Nothing above touches the workspace; this is the last gate before the
158
+ // tool's side effect. (An "always" verdict is still cached: it is the
159
+ // user's decision about the tool+args, not about this run.)
160
+ if (ctx.signal.aborted) return { ok: false, output: ABORTED_TOOL_RESULT };
161
+
162
+ // 4. execute with typed error capture; ctx.onUpdate is wired here so a
163
+ // tool's progress notes (MCP onprogress, LSP/checkpoint updates) become
164
+ // real tool_execution_update events for ALL tools (port #3 LOW-6)
165
+ emit({ type: "tool_execution_start", callId: call.id, tool: call.tool, args });
166
+ let out: ToolOutput;
167
+ try {
168
+ out = await tool.execute(args, { ...ctx, onUpdate: (note) => emit({ type: "tool_execution_update", callId: call.id, note }) });
169
+ } catch (e) {
170
+ out = { ok: false, output: `Error: ${e instanceof Error ? e.message : String(e)}` };
171
+ }
172
+ // 4b. loop guard result pass: byte-identical duplicate results become stubs.
173
+ // out.ok is threaded through so FAILED results are never stubbed (hermes
174
+ // keeps errors verbatim) even when the text dodges the string sniff.
175
+ if (guard) {
176
+ const r = guard.checkResult(call.tool, args, out.output, out.ok);
177
+ if (r.deduped) out = { ...out, output: r.output };
178
+ }
179
+ if (warnNote) out = { ...out, output: `${out.output}\n\n[loop-guard] ${warnNote}` };
180
+ // port #29: post_tool may annotate/replace what the model will see (growth-bounded in the runner);
181
+ // tool_execution_end below carries the final text, like the guard's stub/warn rewrites above. The
182
+ // hook gets a copy of the result: only its RETURNED {output} counts (an assignment dodges no bound)
183
+ const ann = await hooks?.run?.("post_tool", hookCtx(ctx), { id: call.id, tool: call.tool, args: cloneForHook(args) }, { ...out });
184
+ if (ann?.output !== undefined) out = { ...out, output: ann.output };
185
+ emit({ type: "tool_execution_end", callId: call.id, ok: out.ok, output: out.output, durationMs: Date.now() - t0 });
186
+ if (hooks?.onToolResult) await hooks.onToolResult(call.tool, args, out).catch(() => {});
187
+ return out;
188
+ }
189
+
190
+ /** Batch executor: concurrent for parallel-safe tools, sequential otherwise (pi executionMode). */
191
+ async dispatchBatch(
192
+ calls: ToolCallPart[],
193
+ ctx: ToolContext,
194
+ hooks: ExtensionHooks | undefined,
195
+ rules: PermissionRule[],
196
+ approve: ((req: ApprovalRequest) => Promise<"once" | "always" | "deny">) | undefined,
197
+ emit: (e: RunEvent) => void,
198
+ parallelEnabled: boolean,
199
+ guard?: ToolGuard,
200
+ ): Promise<Map<string, ToolOutput>> {
201
+ const results = new Map<string, ToolOutput>();
202
+ const run = async (c: ToolCallPart) => { results.set(c.id, await this.dispatch(c, ctx, hooks, rules, approve, emit, guard)); };
203
+ if (!parallelEnabled || calls.some((c) => this.tools.get(c.tool)?.sequential !== false)) {
204
+ for (const c of calls) {
205
+ // port #21 MED-2: an abort that landed during the previous call must not
206
+ // START the next one — nor prompt for it. It gets the aborted synthesis
207
+ // HERE: the loop's batch-finally only synthesizes for a batch that never
208
+ // settled, and a killed bash settles well inside the loop's grace.
209
+ if (ctx.signal.aborted) { results.set(c.id, { ok: false, output: ABORTED_TOOL_RESULT }); continue; }
210
+ await run(c);
211
+ }
212
+ } else {
213
+ await Promise.all(calls.map(run));
214
+ }
215
+ return results;
216
+ }
217
+ }
218
+
219
+ function actionFor(tool: Tool): string {
220
+ switch (tool.kind) {
221
+ case "read": return "file.read";
222
+ case "write": return "file.write";
223
+ case "execute": return "shell.exec";
224
+ case "spawn": return "spawn";
225
+ case "memory": return "memory.write";
226
+ case "network": return "net.fetch"; // port #31: outbound requests; resource = URL host
227
+ default: return `tool.${tool.schema.name}`;
228
+ }
229
+ }
230
+
231
+ /** Resource for policy rules. An args key is only honored when the tool's
232
+ * DECLARED schema has that property: args are not schema-validated before
233
+ * policy, so a smuggled key ({query, path:"/tmp/x"} on recall, whose schema
234
+ * has no `path`) must not re-aim a tool-targeted deny rule at another
235
+ * resource. Policy runs pre-execute, so per-tool arg-stripping can't repair
236
+ * this — the gate belongs here.
237
+ * For a path-declared tool the resource is the path the tool will actually
238
+ * touch: a missing/empty `path` defaults to ctx.cwd and a relative one
239
+ * resolves against it (the tools' own resolvePath rule), so neither omitting
240
+ * nor relativizing the arg can dodge a path-targeted rule (port #22 MED-4).
241
+ * Consequently path resources are ALWAYS absolute, so a permission rule's
242
+ * pattern must match the full absolute path (matchesGlob anchors it): an
243
+ * absolute path/glob or `*` — a cwd-relative pattern such as
244
+ * `deny file.write ".env*"` can never match via dispatch.
245
+ * Command resources and the tool-name fallback are untouched. */
246
+ function describeResource(tool: Tool, args: unknown, cwd: string): string {
247
+ // a tool that declares its own mode wins: a path/command/url says WHAT is touched, but a tool
248
+ // whose modes differ in what they may do has to be able to say WHICH mode (types.ts Tool.resource)
249
+ if (typeof tool.resource === "function") return tool.resource(args);
250
+ const props = tool.schema.args["properties"];
251
+ const declared = (key: string): boolean =>
252
+ typeof props === "object" && props !== null && key in (props as Record<string, unknown>);
253
+ const a = args && typeof args === "object" ? (args as Record<string, unknown>) : undefined;
254
+ if (declared("path")) {
255
+ const p = a?.["path"];
256
+ if (p === undefined || p === null || p === "") return cwd;
257
+ return isAbsolute(String(p)) ? String(p) : join(cwd, String(p));
258
+ }
259
+ if (a && declared("command") && "command" in a) return String(a["command"]);
260
+ if (a && declared("url") && "url" in a) return hostOf(String(a["url"]));
261
+ return tool.schema.name;
262
+ }
263
+
264
+ /** Policy resource for url-declared tools (port #31): the URL's hostname — no
265
+ * scheme/port/credentials/path — so `allow net.fetch docs.example.com` and
266
+ * `deny net.fetch *` read naturally. The host is CANONICAL (lowercased, trailing
267
+ * dot stripped — the form the tool's own SSRF guard checks), so `evil.com.`
268
+ * cannot dodge a `deny net.fetch evil.com` rule. The decision covers this host
269
+ * only: web_fetch stops at a redirect to a different host and reports the
270
+ * target URL, so that host comes back through this gate as its own call.
271
+ * Unparseable URLs keep the raw string (the tool rejects them anyway), so `*`
272
+ * rules still see a stable resource. */
273
+ function hostOf(url: string): string {
274
+ try { const h = new URL(url).hostname; return (h.endsWith(".") ? h.slice(0, -1) : h).toLowerCase() || url; } catch { return url; }
275
+ }
276
+
277
+ /** The identity an "always" verdict is remembered under.
278
+ *
279
+ * Where the card names a real target — a path, a shell command, a host — that pair IS the decision
280
+ * the human made ("always allow writing THIS file"), and it is the same identity the rules evaluate.
281
+ * Where the tool declares none of those, describeResource falls back to the tool NAME, and widening
282
+ * to it would turn "always" on one `mcp_call` into "always" on every MCP call. So those keep the
283
+ * exact-arguments key they always had: the narrow reading is the safe one when the card cannot say
284
+ * what the decision is about. */
285
+ function approvalKey(action: string, resource: string, toolName: string, args: unknown): string {
286
+ const base = action + "|" + resource;
287
+ return resource === toolName ? base + "|" + JSON.stringify(args) : base;
288
+ }