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.
- package/README.md +67 -69
- package/THIRD_PARTY_NOTICES.md +0 -44
- package/bin/rovecode.ts +21 -0
- package/package.json +16 -37
- package/src/account/keys.ts +97 -0
- package/src/account/login.ts +158 -0
- package/src/account/provision.ts +47 -0
- package/src/account/store.ts +63 -0
- package/src/acp/server.ts +373 -0
- package/src/cli/account-cmd.ts +116 -0
- package/src/cli/connect.ts +244 -0
- package/src/cli/context-cmd.ts +199 -0
- package/src/cli/dispatch.ts +109 -0
- package/src/cli/doctor.ts +324 -0
- package/src/cli/export.ts +278 -0
- package/src/cli/help.ts +240 -0
- package/src/cli/is-tui-invocation.ts +8 -0
- package/src/cli/main.ts +599 -0
- package/src/cli/market-cmd.ts +658 -0
- package/src/cli/mcp-market-cmd.ts +299 -0
- package/src/cli/output.ts +382 -0
- package/src/cli/repl.ts +172 -0
- package/src/cli/resume.ts +32 -0
- package/src/cli/run-limits.ts +78 -0
- package/src/cli/runtime.ts +792 -0
- package/src/cli/setup.ts +187 -0
- package/src/cli/update-cmd.ts +78 -0
- package/src/cli/workflow-cmd.ts +100 -0
- package/src/coding/checkpoints.ts +270 -0
- package/src/coding/diff.ts +136 -0
- package/src/coding/files.ts +339 -0
- package/src/coding/hashline.ts +319 -0
- package/src/coding/lsp.ts +406 -0
- package/src/coding/repomap-cache.ts +99 -0
- package/src/coding/repomap-files.ts +110 -0
- package/src/coding/repomap.ts +392 -0
- package/src/core/compaction.ts +399 -0
- package/src/core/config.ts +289 -0
- package/src/core/context-report.ts +228 -0
- package/src/core/context.ts +60 -0
- package/src/core/count-remote.ts +107 -0
- package/src/core/execpolicy-rules.ts +196 -0
- package/src/core/execpolicy.ts +385 -0
- package/src/core/executor.ts +397 -0
- package/src/core/guardrails.ts +400 -0
- package/src/core/hooks.ts +398 -0
- package/src/core/images.ts +230 -0
- package/src/core/intro.ts +236 -0
- package/src/core/loop.ts +621 -0
- package/src/core/modes.ts +372 -0
- package/src/core/orchestrator.ts +207 -0
- package/src/core/reflection.ts +165 -0
- package/src/core/sandbox-config.ts +167 -0
- package/src/core/session-images.ts +73 -0
- package/src/core/session.ts +398 -0
- package/src/core/settings.ts +98 -0
- package/src/core/stuck-detector.ts +273 -0
- package/src/core/tasks.ts +374 -0
- package/src/core/token-scale.ts +108 -0
- package/src/core/tool-output-budget.ts +166 -0
- package/src/core/tools.ts +288 -0
- package/src/core/types.ts +330 -0
- package/src/core/update-check.ts +171 -0
- package/src/core/update.ts +158 -0
- package/src/core/usage.ts +204 -0
- package/src/core/validate.ts +121 -0
- package/src/core/verify-gate.ts +159 -0
- package/src/core/verify.ts +237 -0
- package/src/core/voice.ts +158 -0
- package/src/core/win-job.ts +183 -0
- package/src/design/audit.ts +797 -0
- package/src/design/direction.ts +190 -0
- package/src/design/rules.ts +157 -0
- package/src/eval/bench.ts +150 -0
- package/src/eval/gauntlet-runner.ts +218 -0
- package/src/eval/gauntlet.ts +226 -0
- package/src/eval/grader.ts +186 -0
- package/src/eval/record.ts +202 -0
- package/src/eval/redact.ts +141 -0
- package/src/eval/replay.ts +147 -0
- package/src/eval/trajectory.ts +373 -0
- package/src/index.ts +17 -0
- package/src/market/catalogs/mcp-docs.json +111 -0
- package/src/market/catalogs/plugins.json +111 -0
- package/src/market/catalogs/skills.json +478 -0
- package/src/market/clone.ts +72 -0
- package/src/market/context-cost.ts +121 -0
- package/src/market/digest.ts +106 -0
- package/src/market/index.ts +22 -0
- package/src/market/install.ts +578 -0
- package/src/market/manifest.ts +187 -0
- package/src/market/prereq.ts +145 -0
- package/src/market/registry.ts +363 -0
- package/src/market/resolve.ts +111 -0
- package/src/market/types.ts +236 -0
- package/src/market/validate.ts +227 -0
- package/src/mcp/client.ts +431 -0
- package/src/mcp/config.ts +239 -0
- package/src/mcp/local-package.ts +211 -0
- package/src/mcp/market-catalog.ts +84 -0
- package/src/mcp/market-install.ts +289 -0
- package/src/mcp/market.ts +0 -0
- package/src/mcp/tools.ts +131 -0
- package/src/mcp/trust.ts +49 -0
- package/src/memory/blocks.ts +175 -0
- package/src/memory/recall.ts +355 -0
- package/src/memory/store.ts +105 -0
- package/src/memory/tools.ts +99 -0
- package/src/plugins/cli.ts +123 -0
- package/src/plugins/discover.ts +108 -0
- package/src/plugins/index.ts +50 -0
- package/src/plugins/init.ts +140 -0
- package/src/plugins/install.ts +184 -0
- package/src/plugins/load.ts +149 -0
- package/src/plugins/manifest.ts +106 -0
- package/src/plugins/state.ts +83 -0
- package/src/providers/auth.ts +293 -0
- package/src/providers/cache.ts +223 -0
- package/src/providers/catalog-local.ts +160 -0
- package/src/providers/catalog.ts +408 -0
- package/src/providers/middleware-context.ts +86 -0
- package/src/providers/middleware.ts +373 -0
- package/src/providers/profile-glm53.ts +111 -0
- package/src/providers/profile-sonnet5-persona.ts +65 -0
- package/src/providers/profile-sonnet5-voice.ts +23 -0
- package/src/providers/profiles.ts +156 -0
- package/src/providers/provider-config.ts +311 -0
- package/src/providers/registry.ts +302 -0
- package/src/providers/response-validation.ts +80 -0
- package/src/providers/retry.ts +234 -0
- package/src/providers/router.ts +294 -0
- package/src/providers/sse.ts +26 -0
- package/src/providers/stream-errors.ts +117 -0
- package/src/providers/stream.ts +569 -0
- package/src/providers/thinking.ts +189 -0
- package/src/providers/wire-messages.ts +129 -0
- package/src/sdk/client.ts +225 -0
- package/src/sdk/index.ts +3 -0
- package/src/server/dashboard.ts +144 -0
- package/src/server/http.ts +343 -0
- package/src/server/openapi.ts +246 -0
- package/src/sextant/card-hits.ts +102 -0
- package/src/sextant/card-keys.ts +55 -0
- package/src/sextant/context-source.ts +157 -0
- package/src/sextant/draw-agents.ts +273 -0
- package/src/sextant/draw-code.ts +388 -0
- package/src/sextant/draw-context.ts +222 -0
- package/src/sextant/draw-frame.ts +164 -0
- package/src/sextant/draw-market.ts +573 -0
- package/src/sextant/draw-messages.ts +386 -0
- package/src/sextant/draw-pet.ts +230 -0
- package/src/sextant/draw-plan.ts +159 -0
- package/src/sextant/draw-tabs.ts +85 -0
- package/src/sextant/draw-util.ts +65 -0
- package/src/sextant/engine.ts +230 -0
- package/src/sextant/frame-hits.ts +25 -0
- package/src/sextant/frame.ts +101 -0
- package/src/sextant/git-status.ts +197 -0
- package/src/sextant/grid.ts +59 -0
- package/src/sextant/input.ts +119 -0
- package/src/sextant/keys.ts +488 -0
- package/src/sextant/layout.ts +86 -0
- package/src/sextant/local-commands.ts +156 -0
- package/src/sextant/market-source.ts +287 -0
- package/src/sextant/mentions.ts +141 -0
- package/src/sextant/message-hits.ts +26 -0
- package/src/sextant/model.ts +387 -0
- package/src/sextant/overlays.ts +451 -0
- package/src/sextant/panel-hits.ts +38 -0
- package/src/sextant/pet.ts +399 -0
- package/src/sextant/screen.ts +324 -0
- package/src/sextant/scroll-hits.ts +66 -0
- package/src/sextant/scrollbar.ts +82 -0
- package/src/sextant/selection.ts +123 -0
- package/src/sextant/sextant-bridge.ts +174 -0
- package/src/sextant/sextant-cards.ts +142 -0
- package/src/sextant/sextant-diff-base.ts +63 -0
- package/src/sextant/sextant-files.ts +154 -0
- package/src/sextant/sextant-frame-loop.ts +314 -0
- package/src/sextant/sextant-renderer.ts +478 -0
- package/src/sextant/sextant-repo.ts +131 -0
- package/src/sextant/theme.ts +66 -0
- package/src/sextant/tool-rows.ts +189 -0
- package/src/sextant/types.ts +473 -0
- package/src/skills/index.ts +306 -0
- package/src/skills/tools.ts +69 -0
- package/src/skills/versioned.ts +227 -0
- package/src/telemetry/otel.ts +353 -0
- package/src/telemetry/otlp.ts +68 -0
- package/src/tools/ask-user.ts +156 -0
- package/src/tools/design.ts +151 -0
- package/src/tools/evalcell.ts +338 -0
- package/src/tools/html-text.ts +139 -0
- package/src/tools/provider.ts +149 -0
- package/src/tools/task.ts +216 -0
- package/src/tools/todo.ts +320 -0
- package/src/tools/webfetch.ts +331 -0
- package/src/tui/app.ts +608 -0
- package/src/tui/attach.ts +127 -0
- package/src/tui/checkpoints-cmd.ts +70 -0
- package/src/tui/clipboard-image.ts +81 -0
- package/src/tui/commands.ts +277 -0
- package/src/tui/cost.ts +108 -0
- package/src/tui/info-cmd.ts +144 -0
- package/src/tui/mcp-cmd.ts +128 -0
- package/src/tui/modes-cmd.ts +45 -0
- package/src/tui/overlays.ts +97 -0
- package/src/tui/pi-renderer.ts +424 -0
- package/src/tui/providers-cmd.ts +366 -0
- package/src/tui/renderer.ts +101 -0
- package/src/tui/replay-marker.ts +29 -0
- package/src/tui/session-cmd.ts +146 -0
- package/src/tui/sextant-attach.ts +68 -0
- package/src/tui/sextant-io.ts +184 -0
- package/src/tui/sextant-smoke.ts +110 -0
- package/src/tui/smoke.ts +72 -0
- package/src/tui/theme.ts +59 -0
- package/src/tui/todo-label.ts +7 -0
- package/src/workflow/engine.ts +266 -0
- package/tsconfig.json +30 -0
- package/vendor/pi-tui/LICENSE +21 -0
- package/vendor/pi-tui/PATCHES.md +12 -0
- package/vendor/pi-tui/PROVENANCE.md +12 -0
- package/vendor/pi-tui/README.upstream.md +854 -0
- package/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-console-mode.node +0 -0
- package/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-console-mode.node +0 -0
- package/vendor/pi-tui/src/alt-screen-search.ts +158 -0
- package/vendor/pi-tui/src/autocomplete.ts +827 -0
- package/vendor/pi-tui/src/components/alt-screen-flash.ts +52 -0
- package/vendor/pi-tui/src/components/box.ts +138 -0
- package/vendor/pi-tui/src/components/cancellable-loader.ts +41 -0
- package/vendor/pi-tui/src/components/editor.ts +2364 -0
- package/vendor/pi-tui/src/components/h-stack.ts +45 -0
- package/vendor/pi-tui/src/components/image.ts +128 -0
- package/vendor/pi-tui/src/components/input.ts +448 -0
- package/vendor/pi-tui/src/components/loader.ts +93 -0
- package/vendor/pi-tui/src/components/markdown.ts +1016 -0
- package/vendor/pi-tui/src/components/scroll-view.ts +217 -0
- package/vendor/pi-tui/src/components/select-list.ts +230 -0
- package/vendor/pi-tui/src/components/settings-list.ts +277 -0
- package/vendor/pi-tui/src/components/spacer.ts +29 -0
- package/vendor/pi-tui/src/components/stack.ts +155 -0
- package/vendor/pi-tui/src/components/text.ts +108 -0
- package/vendor/pi-tui/src/components/truncated-text.ts +66 -0
- package/vendor/pi-tui/src/components/v-stack.ts +34 -0
- package/vendor/pi-tui/src/editor-component.ts +75 -0
- package/vendor/pi-tui/src/fuzzy.ts +138 -0
- package/vendor/pi-tui/src/index.ts +149 -0
- package/vendor/pi-tui/src/keybindings.ts +321 -0
- package/vendor/pi-tui/src/keys.ts +1402 -0
- package/vendor/pi-tui/src/kill-ring.ts +47 -0
- package/vendor/pi-tui/src/latex.ts +1381 -0
- package/vendor/pi-tui/src/layout-node.ts +52 -0
- package/vendor/pi-tui/src/layout.ts +411 -0
- package/vendor/pi-tui/src/native-modifiers.ts +60 -0
- package/vendor/pi-tui/src/native-module-path.ts +32 -0
- package/vendor/pi-tui/src/stdin-buffer.ts +445 -0
- package/vendor/pi-tui/src/terminal-colors.ts +74 -0
- package/vendor/pi-tui/src/terminal-image.ts +701 -0
- package/vendor/pi-tui/src/terminal.ts +554 -0
- package/vendor/pi-tui/src/tui-alt-screen.ts +1379 -0
- package/vendor/pi-tui/src/tui-main-screen.ts +655 -0
- package/vendor/pi-tui/src/tui.ts +1264 -0
- package/vendor/pi-tui/src/undo-stack.ts +29 -0
- package/vendor/pi-tui/src/utils.ts +1327 -0
- package/vendor/pi-tui/src/word-navigation.ts +118 -0
- package/vendor/pi-tui/test/test-themes.ts +39 -0
- package/vendor/pi-tui/test/virtual-terminal.ts +219 -0
- package/CHANGELOG.md +0 -512
- package/bin/rovecode.js +0 -24
- package/dist/cli/app-dybnr56b.js +0 -2
- package/dist/cli/ask-user-p8hq4xgj.js +0 -2
- package/dist/cli/auth-login-ewpgw5sm.js +0 -2
- package/dist/cli/auth-m8p9grty.js +0 -2
- package/dist/cli/bench-xv3ypwev.js +0 -9
- package/dist/cli/catalog-737wb2s0.js +0 -2
- package/dist/cli/cli-arhg40m0.js +0 -2
- package/dist/cli/client-cf2pxx8q.js +0 -2
- package/dist/cli/commands-3p7e4xxs.js +0 -2
- package/dist/cli/connect-3q93d7cb.js +0 -2
- package/dist/cli/context-cmd-eqxmxhzq.js +0 -2
- package/dist/cli/context-report-hbw9zfes.js +0 -2
- package/dist/cli/count-remote-mby98cd0.js +0 -2
- package/dist/cli/design-122y0axd.js +0 -2
- package/dist/cli/dispatch-b4egzvvh.js +0 -2
- package/dist/cli/doctor-x4jkv72e.js +0 -3
- package/dist/cli/executor-ftvg6tsy.js +0 -2
- package/dist/cli/export-pdgdhkch.js +0 -2
- package/dist/cli/files-cez9a96p.js +0 -2
- package/dist/cli/gauntlet-r3xxaszc.js +0 -2
- package/dist/cli/gauntlet-runner-r515m7kk.js +0 -10
- package/dist/cli/gauntlet-wave3-bnkjk2v2.js +0 -5
- package/dist/cli/gauntlet-wave4-acs9s60q.js +0 -14
- package/dist/cli/hashline-ewg5hbe3.js +0 -2
- package/dist/cli/http-n0kehsk8.js +0 -5
- package/dist/cli/index-z5qt1s76.js +0 -2
- package/dist/cli/install-80mp63kx.js +0 -2
- package/dist/cli/loop-12twjcat.js +0 -2
- package/dist/cli/main-01pv9206.js +0 -4
- package/dist/cli/main-0jys2ccn.js +0 -3
- package/dist/cli/main-1ztz6fkj.js +0 -10
- package/dist/cli/main-23q7cmww.js +0 -9
- package/dist/cli/main-2rzbexn2.js +0 -3
- package/dist/cli/main-2wyax8k9.js +0 -9
- package/dist/cli/main-2yeveeve.js +0 -6
- package/dist/cli/main-2z3dek0b.js +0 -3
- package/dist/cli/main-2zgsknth.js +0 -3
- package/dist/cli/main-45ejth3a.js +0 -4
- package/dist/cli/main-45rn3trk.js +0 -22
- package/dist/cli/main-4p4e2w7x.js +0 -4
- package/dist/cli/main-4y0tnfpa.js +0 -16
- package/dist/cli/main-5py0rkmc.js +0 -4
- package/dist/cli/main-6dtqmbt6.js +0 -7
- package/dist/cli/main-6h9x282m.js +0 -4
- package/dist/cli/main-6vjeds42.js +0 -3
- package/dist/cli/main-78gq4bt9.js +0 -6
- package/dist/cli/main-7jd5vh3x.js +0 -4
- package/dist/cli/main-7kt6r53y.js +0 -4
- package/dist/cli/main-8c1tbazx.js +0 -58
- package/dist/cli/main-9a9rnh47.js +0 -19
- package/dist/cli/main-9ht36z12.js +0 -3
- package/dist/cli/main-a2yfvcy9.js +0 -7
- package/dist/cli/main-a3f51n0x.js +0 -5
- package/dist/cli/main-b8zq261k.js +0 -3
- package/dist/cli/main-bxtvnf6d.js +0 -13
- package/dist/cli/main-edxc3yzt.js +0 -4
- package/dist/cli/main-evgz4mp5.js +0 -21
- package/dist/cli/main-f33fc5je.js +0 -9
- package/dist/cli/main-fvnpq46y.js +0 -12
- package/dist/cli/main-gbbty4d4.js +0 -3
- package/dist/cli/main-gth53dnt.js +0 -25
- package/dist/cli/main-hqbz10aw.js +0 -9
- package/dist/cli/main-hrrvcfan.js +0 -38
- package/dist/cli/main-hzwtsb2m.js +0 -5
- package/dist/cli/main-j7ttv0sd.js +0 -34
- package/dist/cli/main-jak598k9.js +0 -5
- package/dist/cli/main-kba6zeyd.js +0 -6
- package/dist/cli/main-kwwsz6rq.js +0 -3
- package/dist/cli/main-m8vm17zq.js +0 -3
- package/dist/cli/main-mg4f96e1.js +0 -3
- package/dist/cli/main-mg9b20ac.js +0 -18
- package/dist/cli/main-mgb9ccnx.js +0 -3
- package/dist/cli/main-mjt2p7aj.js +0 -3
- package/dist/cli/main-n6qrdbmy.js +0 -3
- package/dist/cli/main-na7wse0x.js +0 -5
- package/dist/cli/main-nqveez48.js +0 -4
- package/dist/cli/main-ntqef02r.js +0 -10
- package/dist/cli/main-nvc3yjay.js +0 -136
- package/dist/cli/main-p0cfn6nr.js +0 -16
- package/dist/cli/main-qj2djy17.js +0 -19
- package/dist/cli/main-qsevpgsv.js +0 -3
- package/dist/cli/main-qvarybsp.js +0 -3
- package/dist/cli/main-rebtt91r.js +0 -5
- package/dist/cli/main-rpg7h8mb.js +0 -3
- package/dist/cli/main-rsy72qmw.js +0 -15
- package/dist/cli/main-rvetps99.js +0 -18
- package/dist/cli/main-s4bb0jav.js +0 -3
- package/dist/cli/main-s9v8k74e.js +0 -3
- package/dist/cli/main-tjvwmscs.js +0 -3
- package/dist/cli/main-tkgarpjj.js +0 -4
- package/dist/cli/main-v8y60bb2.js +0 -3
- package/dist/cli/main-vhrrq337.js +0 -3
- package/dist/cli/main-vp2dfb7s.js +0 -4
- package/dist/cli/main-vqbr22sz.js +0 -8
- package/dist/cli/main-vxnwe5xx.js +0 -18
- package/dist/cli/main-wgph00xf.js +0 -5
- package/dist/cli/main-wk2csfnj.js +0 -5
- package/dist/cli/main-wm997zjx.js +0 -3
- package/dist/cli/main-wpkyraxh.js +0 -3
- package/dist/cli/main-wqt32p5x.js +0 -4
- package/dist/cli/main-x9ct6y1a.js +0 -3
- package/dist/cli/main-xfekqh9m.js +0 -7
- package/dist/cli/main-xt9zc3n6.js +0 -7
- package/dist/cli/main-xx2z3zh5.js +0 -4
- package/dist/cli/main-y5c82rxr.js +0 -3
- package/dist/cli/main-yrjt2sqt.js +0 -14
- package/dist/cli/main-ys6zj3yr.js +0 -3
- package/dist/cli/main-ywbxshqc.js +0 -8
- package/dist/cli/main-z13755t8.js +0 -25
- package/dist/cli/main-zc7pyrbj.js +0 -4
- package/dist/cli/main.js +0 -279
- package/dist/cli/market-cmd-bm5xvn9f.js +0 -5
- package/dist/cli/mcp-login-bthtfpt7.js +0 -2
- package/dist/cli/mcp-market-cmd-mbeshfyd.js +0 -2
- package/dist/cli/notify-54v5z9dz.js +0 -2
- package/dist/cli/oauth-g5gme95c.js +0 -2
- package/dist/cli/output-satndjap.js +0 -16
- package/dist/cli/profiles-sfhpbq3m.js +0 -2
- package/dist/cli/provider-config-hv3xtdt4.js +0 -2
- package/dist/cli/provider-kwzq6g84.js +0 -2
- package/dist/cli/registry-fh0hdnyn.js +0 -2
- package/dist/cli/registry-y1y8e94r.js +0 -2
- package/dist/cli/repl-t4z03mqq.js +0 -11
- package/dist/cli/resume-fqt4chg8.js +0 -2
- package/dist/cli/run-flags-rysbag9t.js +0 -2
- package/dist/cli/runtime-j19fjbsa.js +0 -2
- package/dist/cli/sandbox-config-g4qxd7y5.js +0 -2
- package/dist/cli/server-r0b6bksk.js +0 -5
- package/dist/cli/session-arg-txmn5g4x.js +0 -2
- package/dist/cli/session-ed250d9j.js +0 -2
- package/dist/cli/sessions-cmd-adw7svfn.js +0 -7
- package/dist/cli/settings-y9rzcqx8.js +0 -2
- package/dist/cli/setup-jmbr11j0.js +0 -2
- package/dist/cli/sextant-smoke-tcth0vea.js +0 -5
- package/dist/cli/skills-cmd-zbdy99v6.js +0 -2
- package/dist/cli/smoke-1bg937kx.js +0 -8
- package/dist/cli/start-chat-p01cdks3.js +0 -12
- package/dist/cli/stream-4wmyaypz.js +0 -2
- package/dist/cli/task-eg4s093s.js +0 -2
- package/dist/cli/tasks-12v9rr9k.js +0 -2
- package/dist/cli/thinking-a5ngvqyh.js +0 -2
- package/dist/cli/todo-1wxpcecx.js +0 -2
- package/dist/cli/tools-2ftsya7w.js +0 -2
- package/dist/cli/tools-x1tj4fxm.js +0 -2
- package/dist/cli/trust-cmd-hccxehzb.js +0 -2
- package/dist/cli/update-check-ygt3vd7m.js +0 -2
- package/dist/cli/update-cmd-v23qhr8c.js +0 -2
- package/dist/cli/voice-g1gtck92.js +0 -2
- package/dist/cli/webfetch-0nnrjgb5.js +0 -2
- package/dist/cli/websearch-f0vr2p7d.js +0 -2
- package/dist/cli/workspace-9rq1w4ta.js +0 -2
- package/dist/lib/index.js +0 -62
- package/dist/lib/models-index.json +0 -1
- package/dist/lib/plugins.js +0 -6
- package/dist/lib/providers.js +0 -17
- package/dist/lib/public-api.js +0 -20
- package/dist/rovecode.exe +0 -4
- /package/{dist/cli → src/providers}/models-index.json +0 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/** What check this project runs before the agent's work counts as done — decided without ever guessing.
|
|
2
|
+
*
|
|
3
|
+
* Berkay (2026-09-06): the CLI should govern its own work and catch its own mistakes before saying done. The
|
|
4
|
+
* loop's gate (core/verify-gate.ts, nimbus-99) runs a command after the last edit; THIS module decides which one,
|
|
5
|
+
* and it is the half that can do damage: a gate that runs the wrong command once — `npm test` that deploys,
|
|
6
|
+
* wipes a database, or takes twenty minutes — is turned off forever. Two measurements shaped it (nimbus-6f, same
|
|
7
|
+
* night): in Berkay's real projects (plain HTML/CSS/JS, no package.json) there is nothing to run at all, so
|
|
8
|
+
* "none" is the MAIN path and its text is what a person reads; and in a TypeScript project a type-check caught
|
|
9
|
+
* the same three constructed mistakes as the full suite in a tenth of the time (10 s against 170 s here). So:
|
|
10
|
+
*
|
|
11
|
+
* 1. Configuration first. `verify` in .rovecode/settings.json (project) or ~/.rovecode/settings.json (user),
|
|
12
|
+
* a command string or a list, project winning like every other key. This is the one source that needs
|
|
13
|
+
* no inference, and the ONLY way a test suite becomes the gate. `false` turns the gate off on purpose and
|
|
14
|
+
* stops the inference below as well.
|
|
15
|
+
* 2. Inference second, aimed at the FAST check, and only where both the name and the shape are recognised:
|
|
16
|
+
* - package.json `typecheck` whose body is tsc; `lint` whose body is eslint, biome or oxlint. Name alone is
|
|
17
|
+
* not enough (a `lint` script may do anything); the body has to be a runner known to run unattended and end.
|
|
18
|
+
* - package.json `check`, only when neither of those was taken, and only when its body compiles or lints
|
|
19
|
+
* without running tests: a `check` that runs the suite is the thorough gate, and the thorough gate is
|
|
20
|
+
* configured, never inferred — the refusal names it and how to opt in.
|
|
21
|
+
* - Cargo.toml → `cargo check`; go.mod → `go vet ./...`; a ruff-configured Python project → `ruff check .`.
|
|
22
|
+
* - A `test` script, `cargo test`, `pytest`, Makefile targets: never inferred, always listed with the reason.
|
|
23
|
+
* 3. Everything seen and NOT inferred goes into `refused`, in words specific enough that a person reads the
|
|
24
|
+
* line and either agrees or sets the key. "Nothing" is a good answer; "could not infer" is not.
|
|
25
|
+
*
|
|
26
|
+
* The seam for the runner is `commands: string[]` it does not interpret, so per-file targeting ("tsc --noEmit"
|
|
27
|
+
* plus the tests that import the changed files) can be added here later without touching anything downstream.
|
|
28
|
+
* Pure apart from reading the files named above; never throws; never runs anything. */
|
|
29
|
+
|
|
30
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
31
|
+
import { join } from "node:path";
|
|
32
|
+
import { loadSettings, settingsPath } from "./settings.ts";
|
|
33
|
+
|
|
34
|
+
export interface VerifyPlan {
|
|
35
|
+
/** where the commands came from; "none" = the gate has nothing to run */
|
|
36
|
+
source: "settings" | "inferred" | "none";
|
|
37
|
+
/** the shell commands the gate runs, in order, uninterpreted; empty when source is "none" */
|
|
38
|
+
commands: string[];
|
|
39
|
+
/** one sentence: the file and key or script the answer came from, or why there is none */
|
|
40
|
+
reason: string;
|
|
41
|
+
/** what was seen and deliberately not inferred, each with its reason — the argument for trusting the gate */
|
|
42
|
+
refused: string[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface VerifyDeps {
|
|
46
|
+
exists?: (p: string) => boolean;
|
|
47
|
+
read?: (p: string) => string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** words in a `check` script's body that mean it is not a check */
|
|
51
|
+
const DANGEROUS = /\b(deploy|publish|release|push|migrate|drop|rm\s+-rf|docker|kubectl|terraform|curl|wget|ssh|scp|rsync)\b/i;
|
|
52
|
+
/** a body that keeps running until killed — never a gate */
|
|
53
|
+
const WATCH = /(^|\s)(--watch(=\S*)?|-w|--watchAll)(\s|$)|\bnodemon\b|\bvitest\s*$/;
|
|
54
|
+
/** a body that runs a test suite — the thorough gate, configured only */
|
|
55
|
+
const RUNS_TESTS = /\b(bun test|vitest|jest|mocha|node --test|playwright test|cypress run|ava|tap|uvu)\b/;
|
|
56
|
+
/** runners whose bare invocation runs once, unattended, and exits */
|
|
57
|
+
const TYPECHECK_RUNNER = /^(bunx |npx |pnpm exec |yarn )?(tsc|vue-tsc|svelte-check)(\s|$)/;
|
|
58
|
+
const LINT_RUNNER = /^(bunx |npx |pnpm exec |yarn )?(eslint|biome (check|lint)|oxlint)(\s|$)/;
|
|
59
|
+
|
|
60
|
+
/** the package manager the lockfile says this project uses; npm when none says */
|
|
61
|
+
function packageRunner(cwd: string, exists: (p: string) => boolean): { run: string; from: string } {
|
|
62
|
+
if (exists(join(cwd, "bun.lock")) || exists(join(cwd, "bun.lockb"))) return { run: "bun run", from: "bun.lock" };
|
|
63
|
+
if (exists(join(cwd, "pnpm-lock.yaml"))) return { run: "pnpm run", from: "pnpm-lock.yaml" };
|
|
64
|
+
if (exists(join(cwd, "yarn.lock"))) return { run: "yarn", from: "yarn.lock" };
|
|
65
|
+
if (exists(join(cwd, "package-lock.json"))) return { run: "npm run", from: "package-lock.json" };
|
|
66
|
+
return { run: "npm run", from: "no lockfile (npm assumed)" };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const NONE_REASON = "nothing configured and nothing fast and safe to infer — set verify in .rovecode/settings.json (a command, or a list) to turn the gate on";
|
|
70
|
+
export const NOTHING_HERE = "no package.json, Cargo.toml, go.mod or pyproject.toml here, so there is no compiler, linter or test runner to run; for a plain HTML/CSS/JS site that is the normal answer, and the gate stays off";
|
|
71
|
+
|
|
72
|
+
/** Resolve this project's verify command(s). */
|
|
73
|
+
export function resolveVerify(cwd: string, deps: VerifyDeps = {}): VerifyPlan {
|
|
74
|
+
const exists = deps.exists ?? existsSync;
|
|
75
|
+
const read = deps.read ?? ((p: string) => readFileSync(p, "utf8"));
|
|
76
|
+
|
|
77
|
+
// 1. configuration
|
|
78
|
+
const settings = loadSettings(cwd);
|
|
79
|
+
if (settings.verify === false) {
|
|
80
|
+
return { source: "none", commands: [], reason: "settings.json verify: false — the gate is off by choice, nothing is inferred", refused: [] };
|
|
81
|
+
}
|
|
82
|
+
if (settings.verify !== undefined) {
|
|
83
|
+
const commands = Array.isArray(settings.verify) ? settings.verify : [settings.verify];
|
|
84
|
+
const projectFile = settingsPath("project", cwd);
|
|
85
|
+
const where = existsSync(projectFile) && hasVerifyKey(projectFile, read) ? projectFile : settingsPath("user", cwd);
|
|
86
|
+
return { source: "settings", commands, reason: `${where} verify`, refused: [] };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// 2. inference
|
|
90
|
+
const commands: string[] = [];
|
|
91
|
+
const from: string[] = [];
|
|
92
|
+
const refused: string[] = [];
|
|
93
|
+
|
|
94
|
+
const pkgPath = join(cwd, "package.json");
|
|
95
|
+
if (exists(pkgPath)) {
|
|
96
|
+
let scripts: Record<string, string> = {};
|
|
97
|
+
let ok = true;
|
|
98
|
+
try {
|
|
99
|
+
const raw = JSON.parse(read(pkgPath)) as { scripts?: unknown };
|
|
100
|
+
const s = raw.scripts;
|
|
101
|
+
if (s !== undefined && (typeof s !== "object" || s === null || Array.isArray(s))) { refused.push('package.json: "scripts" is not an object — nothing inferred from it'); ok = false; }
|
|
102
|
+
else if (s) for (const [k, v] of Object.entries(s as Record<string, unknown>)) if (typeof v === "string") scripts[k] = v;
|
|
103
|
+
} catch { refused.push("package.json is not valid JSON — nothing inferred from it"); ok = false; scripts = {}; }
|
|
104
|
+
if (ok) {
|
|
105
|
+
const pm = packageRunner(cwd, exists);
|
|
106
|
+
const body = (name: string): string => scripts[name]!.trim();
|
|
107
|
+
const optIn = (name: string) => `set \`"verify": "${pm.run} ${name}"\` in .rovecode/settings.json`;
|
|
108
|
+
|
|
109
|
+
// the fast checks: a recognised runner, or a refusal that quotes the body
|
|
110
|
+
const take = (name: string, runner: RegExp, what: string, known: string): void => {
|
|
111
|
+
if (scripts[name] === undefined) return;
|
|
112
|
+
const b = body(name);
|
|
113
|
+
if (WATCH.test(b)) { refused.push(`package.json script "${name}" runs \`${b}\`: a watch mode never exits, so it cannot gate a run`); return; }
|
|
114
|
+
if (!runner.test(b)) { refused.push(`package.json script "${name}" runs \`${b}\`: not a ${what} this gate knows runs unattended and ends (${known}) — ${optIn(name)} to run it anyway`); return; }
|
|
115
|
+
commands.push(`${pm.run} ${name}`); from.push(`package.json script "${name}" (${pm.from})`);
|
|
116
|
+
};
|
|
117
|
+
take("typecheck", TYPECHECK_RUNNER, "type-checker", "tsc, vue-tsc, svelte-check");
|
|
118
|
+
take("lint", LINT_RUNNER, "linter", "eslint, biome check, oxlint");
|
|
119
|
+
|
|
120
|
+
// `check`: the convention means "verify, change nothing" — taken as the fallback fast check when it only
|
|
121
|
+
// compiles or lints; refused, with the cost named, when it runs the suite or does anything a check must not
|
|
122
|
+
if (scripts.check !== undefined) {
|
|
123
|
+
const b = body("check");
|
|
124
|
+
const bad = DANGEROUS.exec(b);
|
|
125
|
+
const tests = RUNS_TESTS.exec(b);
|
|
126
|
+
if (bad) refused.push(`package.json script "check" runs \`${b}\`: it names \`${bad[1]}\`, which a check must not do — ${optIn("check")} if that is really what you want the gate to run`);
|
|
127
|
+
else if (WATCH.test(b)) refused.push(`package.json script "check" runs \`${b}\`: a watch mode never exits, so it cannot gate a run`);
|
|
128
|
+
else if (tests) refused.push(`package.json script "check" runs \`${b}\`: it runs the test suite (\`${tests[1]}\`), which is minutes here rather than seconds; the thorough gate is configured, never inferred — ${optIn("check")} if that cost is right for this project`);
|
|
129
|
+
else if (commands.length > 0) refused.push(`package.json script "check" runs \`${b}\`: a faster script already covers it (${from.map((f) => f.split(" (")[0]).join(", ")}) — ${optIn("check")} to run it instead`);
|
|
130
|
+
else { commands.push(`${pm.run} check`); from.push(`package.json script "check" (${pm.from})`); }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// `test`: never inferred — the name promises nothing about the body, and a suite is the slow gate
|
|
134
|
+
if (scripts.test !== undefined) {
|
|
135
|
+
const b = body("test");
|
|
136
|
+
refused.push(`package.json script "test" runs \`${b}\`: a test suite is never inferred, only configured (the fast gate is a type-check; a suite can take minutes and may need services) — ${optIn("test")} if it is quick enough here`);
|
|
137
|
+
}
|
|
138
|
+
if (commands.length === 0 && refused.length === 0) refused.push('package.json has no "typecheck", "lint", "check" or "test" script — name one, or set verify in .rovecode/settings.json');
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (exists(join(cwd, "Cargo.toml"))) {
|
|
143
|
+
commands.push("cargo check"); from.push("Cargo.toml");
|
|
144
|
+
refused.push("`cargo test` is not inferred: it runs the crate's tests, which may be slow or need services — set verify to run it");
|
|
145
|
+
}
|
|
146
|
+
if (exists(join(cwd, "go.mod"))) {
|
|
147
|
+
commands.push("go vet ./..."); from.push("go.mod");
|
|
148
|
+
refused.push("`go test ./...` is not inferred: it runs the module's tests — set verify to run it");
|
|
149
|
+
}
|
|
150
|
+
const pyproject = join(cwd, "pyproject.toml");
|
|
151
|
+
if (exists(pyproject) || exists(join(cwd, "ruff.toml"))) {
|
|
152
|
+
const ruffToml = exists(join(cwd, "ruff.toml"));
|
|
153
|
+
let ruff = ruffToml;
|
|
154
|
+
if (!ruff && exists(pyproject)) { try { ruff = /^\[tool\.ruff(\.|\])/m.test(read(pyproject)); } catch { /* unreadable: no ruff */ } }
|
|
155
|
+
if (ruff) { commands.push("ruff check ."); from.push(ruffToml ? "ruff.toml" : "pyproject.toml [tool.ruff]"); }
|
|
156
|
+
else refused.push("pyproject.toml without a [tool.ruff] section: no linter is configured that this gate knows — set verify (for example `ruff check .` or `pytest -q`) to run one");
|
|
157
|
+
refused.push("`pytest` is not inferred: it runs the project's tests — set verify to run it");
|
|
158
|
+
}
|
|
159
|
+
if (exists(join(cwd, "Makefile")) || exists(join(cwd, "makefile"))) {
|
|
160
|
+
refused.push("a Makefile is here, but make targets are never inferred (a target can do anything) — set verify to `make check` or the target you mean");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (commands.length > 0) {
|
|
164
|
+
return { source: "inferred", commands, reason: `inferred from ${from.join(", ")}`, refused };
|
|
165
|
+
}
|
|
166
|
+
if (refused.length === 0) return { source: "none", commands: [], reason: NOTHING_HERE, refused: [] };
|
|
167
|
+
return { source: "none", commands: [], reason: NONE_REASON, refused };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function hasVerifyKey(path: string, read: (p: string) => string): boolean {
|
|
171
|
+
try { const raw = JSON.parse(read(path)) as Record<string, unknown>; return raw !== null && typeof raw === "object" && "verify" in raw; } catch { return false; }
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** the commands as one label — the SAME words in the doctor row and in the loop's `verify` RunEvent ("running
|
|
175
|
+
* bun run check && bun test"), so a person sees one name for one thing */
|
|
176
|
+
export function verifyLabel(plan: VerifyPlan): string {
|
|
177
|
+
return plan.commands.join(" && ");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** one line for `rovecode doctor` and the settings docs */
|
|
181
|
+
export function describeVerify(plan: VerifyPlan): string {
|
|
182
|
+
if (plan.source === "none") return `none — ${plan.reason}`;
|
|
183
|
+
return `${verifyLabel(plan)} — ${plan.reason}`;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** what the gate does NOT see, said wherever the gate is described (doctor, README): the loop counts edits made
|
|
187
|
+
* through `edit` and `write`; a run that changed files only through `bash` has no counted write, so it is not
|
|
188
|
+
* verified. Owned here so the doctor row and the docs cannot drift apart. */
|
|
189
|
+
export const VERIFY_BLIND_SPOT = "runs after edits made with `edit` or `write`; a run that changed files only through `bash` is not counted, so it is not verified";
|
|
190
|
+
|
|
191
|
+
// ---- cost: a check's duration is the fact that makes the doctor row honest ("bun run check, 170 s last time").
|
|
192
|
+
// The resolver never runs anything, so it cannot measure; the gate can, and records here after each run. Project-local
|
|
193
|
+
// because the same command costs different amounts in different repos; one small file, one entry per command.
|
|
194
|
+
|
|
195
|
+
export const VERIFY_TIMING_FILE = join(".rovecode", "verify-timing.json");
|
|
196
|
+
type Timings = Record<string, { ms: number; at: string }>;
|
|
197
|
+
|
|
198
|
+
function readTimings(cwd: string): Timings {
|
|
199
|
+
try {
|
|
200
|
+
const raw = JSON.parse(readFileSync(join(cwd, VERIFY_TIMING_FILE), "utf8")) as unknown;
|
|
201
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return {};
|
|
202
|
+
const out: Timings = {};
|
|
203
|
+
for (const [k, v] of Object.entries(raw as Record<string, unknown>)) {
|
|
204
|
+
if (v && typeof v === "object" && typeof (v as { ms?: unknown }).ms === "number" && typeof (v as { at?: unknown }).at === "string") out[k] = v as { ms: number; at: string };
|
|
205
|
+
}
|
|
206
|
+
return out;
|
|
207
|
+
} catch { return {}; }
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** for the gate: remember how long `command` took in this project. Never throws (a timing is not worth a crash). */
|
|
211
|
+
export function recordVerifyTiming(cwd: string, command: string, ms: number, now: Date = new Date()): void {
|
|
212
|
+
try {
|
|
213
|
+
const t = readTimings(cwd);
|
|
214
|
+
t[command] = { ms: Math.max(0, Math.round(ms)), at: now.toISOString() };
|
|
215
|
+
mkdirSync(join(cwd, ".rovecode"), { recursive: true });
|
|
216
|
+
writeFileSync(join(cwd, VERIFY_TIMING_FILE), JSON.stringify(t, null, 2) + "\n");
|
|
217
|
+
} catch { /* a timing is not worth a crash */ }
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** the last measured duration of `command` here, or undefined when it has not run yet */
|
|
221
|
+
export function lastVerifyTiming(cwd: string, command: string): number | undefined {
|
|
222
|
+
return readTimings(cwd)[command]?.ms;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** "3 s" / "170 s" / "2 min 50 s" — the cost as a person says it */
|
|
226
|
+
export function formatDuration(ms: number): string {
|
|
227
|
+
const s = Math.round(ms / 1000);
|
|
228
|
+
if (s < 1) return "under a second";
|
|
229
|
+
if (s < 120) return `${s} s`;
|
|
230
|
+
const m = Math.floor(s / 60), r = s % 60;
|
|
231
|
+
return r === 0 ? `${m} min` : `${m} min ${r} s`;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** the label with each command's last cost when known: "bun run typecheck (9 s) && bun test (not run yet)" */
|
|
235
|
+
export function verifyLabelWithCost(cwd: string, plan: VerifyPlan): string {
|
|
236
|
+
return plan.commands.map((c) => { const ms = lastVerifyTiming(cwd, c); return ms === undefined ? `${c} (not run here yet)` : `${c} (${formatDuration(ms)} last time)`; }).join(" && ");
|
|
237
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/** rovecode's voice — the one place user-facing wording lives: mode labels, the no-model recipe, the
|
|
2
|
+
* welcome card, the panels' empty-state hints. First person, short plain sentences, one idea per
|
|
3
|
+
* line, no emoji; the ◆ glyph marks rovecode's own lines; a problem always ends with the next step.
|
|
4
|
+
* Pure strings and string builders: every surface (CLI, REPL, TUI, server, ACP) reads from here so a
|
|
5
|
+
* wording change lands everywhere at once. */
|
|
6
|
+
|
|
7
|
+
/** the permission modes as the screen names them; the flags/commands keep their names (--yolo, /yolo, ROVECODE_YOLO) */
|
|
8
|
+
export const MODE_ASK = "ask first";
|
|
9
|
+
export const MODE_AUTO = "auto (never asks)";
|
|
10
|
+
export const MODE_ACCEPT = "accept edits";
|
|
11
|
+
|
|
12
|
+
/** "ask first" | "auto (never asks)" — the status/notes label for the yolo flag */
|
|
13
|
+
export function modeLabel(yolo: boolean): string { return yolo ? MODE_AUTO : MODE_ASK; }
|
|
14
|
+
/** compact form for footers and status bars: "ask first" | "auto" */
|
|
15
|
+
export function modeLabelShort(yolo: boolean): string { return yolo ? "auto" : MODE_ASK; }
|
|
16
|
+
/** what each mode means, in one sentence */
|
|
17
|
+
export function modeMeaning(yolo: boolean): string {
|
|
18
|
+
return yolo ? "I won't stop to ask before writes or shell commands" : "I ask before I write or run anything";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** the marker every problem message ends with */
|
|
22
|
+
export const NEXT = "→ next:";
|
|
23
|
+
export const next = (text: string): string => `${NEXT} ${text}`;
|
|
24
|
+
|
|
25
|
+
/** The no-model recipe — three lines, `rovecode setup` first. `where` picks the commands that exist
|
|
26
|
+
* on that surface; the first line is stable ("no provider configured") for callers that match it. */
|
|
27
|
+
export function noModelHint(where: "cli" | "tui" = "cli"): string {
|
|
28
|
+
const setup = where === "tui" ? "/setup" : "rovecode setup";
|
|
29
|
+
const alt = where === "tui"
|
|
30
|
+
? "/provider add <id> <baseUrl> · /provider key <id> <secret>"
|
|
31
|
+
: "rovecode provider add <id> <baseUrl>, then rovecode auth set <id>";
|
|
32
|
+
return [
|
|
33
|
+
"no provider configured — no model to think with yet.",
|
|
34
|
+
` ${next(setup)} (pick a provider, paste the key hidden, one test call)`,
|
|
35
|
+
` or: ${alt} · or set ROVECODE_BASE_URL + ROVECODE_API_KEY`,
|
|
36
|
+
].join("\n");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** `rovecode run` with nothing configured answers from a scripted mock; this is what it says. */
|
|
40
|
+
export const MOCK_PROVIDER_TEXT =
|
|
41
|
+
`Rovecode mock provider: no model is connected, so this is a canned reply. ${next("rovecode setup")} (or rovecode provider add <id> <baseUrl> + rovecode auth set <id>, or set ROVECODE_BASE_URL and ROVECODE_API_KEY)`;
|
|
42
|
+
|
|
43
|
+
export interface WelcomeOpts {
|
|
44
|
+
/** the session's provider/model when one is connected; null = nothing configured */
|
|
45
|
+
connected: { provider: string; model: string } | null;
|
|
46
|
+
cwd: string;
|
|
47
|
+
yolo: boolean;
|
|
48
|
+
/** plan/act; act is the default and is not mentioned */
|
|
49
|
+
mode?: "plan" | "act";
|
|
50
|
+
/** the running version, so "which rovecode is this" never needs a second command */
|
|
51
|
+
version?: string;
|
|
52
|
+
/** what this session actually loaded. Zeroes are omitted rather than printed: a line reading
|
|
53
|
+
* "0 skills · 0 plugins · 0 MCP servers" is three facts nobody asked for, while "19 skills · 2 MCP
|
|
54
|
+
* servers" is the answer to "did my install take". */
|
|
55
|
+
loaded?: { skills?: number; plugins?: number; mcp?: number };
|
|
56
|
+
/** the one-line update notice, when there is one; see core/update-check.ts */
|
|
57
|
+
update?: string | null;
|
|
58
|
+
/** columns available; under BANNER_COLS the wordmark is dropped rather than wrapped (default 80) */
|
|
59
|
+
width?: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** ROVECODE in half-block letters, two rows. Berkay picked this over a boxed header and a plain block
|
|
63
|
+
* (2026-09-06). It is 31 columns wide, so it fits a narrow split, and it only appears when the card
|
|
64
|
+
* knows the version — a banner with nothing to say under it is decoration. */
|
|
65
|
+
const WORDMARK = ["█▀█ █▀█ █ █ █▀▀ █▀▀ █▀█ █▀▄ █▀▀", "█▀▄ █▄█ ▀▄▀ █▄▄ █▄▄ █▄█ █▄▀ █▄▄"] as const;
|
|
66
|
+
const BANNER_COLS = 46;
|
|
67
|
+
|
|
68
|
+
/** "19 skills · 3 plugins · 2 MCP servers", counting only what is there. Null when nothing loaded,
|
|
69
|
+
* because the absence of the line says that better than the line saying zero three times. */
|
|
70
|
+
export function loadedLine(loaded: WelcomeOpts["loaded"]): string | null {
|
|
71
|
+
if (!loaded) return null;
|
|
72
|
+
const plural = (n: number, one: string) => `${n} ${one}${n === 1 ? "" : "s"}`;
|
|
73
|
+
const bits: string[] = [];
|
|
74
|
+
if (loaded.skills) bits.push(plural(loaded.skills, "skill"));
|
|
75
|
+
if (loaded.plugins) bits.push(plural(loaded.plugins, "plugin"));
|
|
76
|
+
if (loaded.mcp) bits.push(plural(loaded.mcp, "MCP server"));
|
|
77
|
+
return bits.length > 0 ? bits.join(" · ") : null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The card a fresh session opens with (the messages panel's first note). */
|
|
81
|
+
export function welcomeCard(o: WelcomeOpts): string {
|
|
82
|
+
const where = `${o.cwd} · ${modeLabelShort(o.yolo)}${o.mode === "plan" ? " · plan mode (read-only)" : ""}`;
|
|
83
|
+
const version = o.version ? ` ${o.version}` : "";
|
|
84
|
+
// the two optional footers, in the order a reader wants them: what this session has, then what it
|
|
85
|
+
// could have. Both drop out entirely when there is nothing to say — see loadedLine and updateLine.
|
|
86
|
+
const tail = [loadedLine(o.loaded), where, o.update ?? null].filter((l): l is string => l !== null);
|
|
87
|
+
// The banner form: the wordmark, then one line per fact. It needs a version (there is nothing to put
|
|
88
|
+
// beside the mark otherwise) and room (under BANNER_COLS the mark would wrap into noise), and it falls
|
|
89
|
+
// back to exactly the prose card below — which is what a resumed or narrow session still sees.
|
|
90
|
+
if (o.version !== undefined && (o.width ?? 80) >= BANNER_COLS) {
|
|
91
|
+
const model = o.connected === null ? null
|
|
92
|
+
: o.connected.model.length > 0 ? `${o.connected.provider}/${o.connected.model}` : o.connected.provider;
|
|
93
|
+
const loaded = loadedLine(o.loaded);
|
|
94
|
+
return [
|
|
95
|
+
` ${WORDMARK[0]}`,
|
|
96
|
+
` ${WORDMARK[1]} ${o.version}`,
|
|
97
|
+
"",
|
|
98
|
+
model === null ? " no model connected yet — /setup fixes that in about a minute"
|
|
99
|
+
: ` ${[model, loaded].filter((s) => s !== null).join(" · ")}`,
|
|
100
|
+
` ${where}`,
|
|
101
|
+
...(model === null ? [] : [` I read first, then ${o.yolo ? "work without asking" : "ask before I write or run anything"}. /help lists commands by topic.`]),
|
|
102
|
+
...(o.update ? [` ${o.update}`] : []),
|
|
103
|
+
].join("\n");
|
|
104
|
+
}
|
|
105
|
+
if (o.connected === null) {
|
|
106
|
+
return [
|
|
107
|
+
`◆ rovecode${version} here. No model connected yet, so I can't think.`,
|
|
108
|
+
"/setup fixes that in about a minute.",
|
|
109
|
+
...tail,
|
|
110
|
+
].join("\n");
|
|
111
|
+
}
|
|
112
|
+
const model = o.connected.model.length > 0 ? `${o.connected.provider}/${o.connected.model}` : o.connected.provider;
|
|
113
|
+
return [
|
|
114
|
+
`◆ rovecode${version} here. Connected to ${model}.`,
|
|
115
|
+
`Tell me what you want done; I read first, then ${o.yolo ? "work without asking" : "ask before I write or run anything"}.`,
|
|
116
|
+
"/help lists commands by topic.",
|
|
117
|
+
...tail,
|
|
118
|
+
].join("\n");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** one-liner for a resumed session (its transcript is the card) */
|
|
122
|
+
export function resumedLine(id: string, cwd: string, yolo: boolean): string {
|
|
123
|
+
return `◆ back in session ${id.slice(0, 8)} · ${cwd} · ${modeLabelShort(yolo)}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** the /effort note. Says what the level costs, because that is the part that surprises people:
|
|
127
|
+
* thinking tokens are billed as output and they arrive before any answer does. */
|
|
128
|
+
export function effortNote(level: string, receives?: string): string {
|
|
129
|
+
const head = level === "off" ? "thinking: off — I answer straight away."
|
|
130
|
+
: level === "auto" ? "thinking: auto — the model decides how much to reason; I send no dial."
|
|
131
|
+
: `thinking: ${level} — I reason before answering. It costs output tokens and delays the first word.`;
|
|
132
|
+
// the second line is the truth on the wire (providers/thinking.ts thinkingLine): the level is one word,
|
|
133
|
+
// what this model's endpoint actually receives for it is another, and a model without a reasoning mode
|
|
134
|
+
// receives nothing at all — the person should not have to guess which
|
|
135
|
+
return receives ? `${head}\n ${receives}` : head;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** the /yolo toggle's note */
|
|
139
|
+
export function modeSwitchNote(yolo: boolean): string {
|
|
140
|
+
return `mode: ${modeLabel(yolo)} — ${modeMeaning(yolo)}`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** the /accept-edits toggle's note. Says the boundary out loud: this is not auto mode, and the two
|
|
144
|
+
* things it does NOT cover are the two that can leave the repository. */
|
|
145
|
+
export function acceptEditsNote(on: boolean): string {
|
|
146
|
+
return on
|
|
147
|
+
? `mode: ${MODE_ACCEPT} — I write inside this folder without asking. Shell commands, subagents, network and any write outside it still ask, and deny rules still hold.`
|
|
148
|
+
: `mode: ${MODE_ASK} — I ask before every write again.`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ---------- panel empty states (one dim line each; a second line only when the panel has room) ----------
|
|
152
|
+
|
|
153
|
+
export const EMPTY = {
|
|
154
|
+
code: "nothing open — when I read a file it shows here",
|
|
155
|
+
files: ["no files yet", "they appear as I read them"] as const,
|
|
156
|
+
plan: ["no plan yet", "my steps show up here"] as const,
|
|
157
|
+
usage: "0 so far",
|
|
158
|
+
} as const;
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/** Windows process-TREE kill via a kernel Job Object (port #21 HIGH-1).
|
|
2
|
+
*
|
|
3
|
+
* `taskkill /T` walks LIVE parent links. msys2 (Git bash) implements a
|
|
4
|
+
* Cygwin→Cygwin exec by handing the pid to the new image and exiting the
|
|
5
|
+
* forked stub, so the external command actually running inside a compound
|
|
6
|
+
* (`a; sleep N; b`), nested (`bash -c '…'`) or backgrounded (`x & y & wait`)
|
|
7
|
+
* shape has a DEAD Win32 parent and the walk never reaches it. Measured
|
|
8
|
+
* through bunRunner on the reference box with an msys `sleep` child: 3/3
|
|
9
|
+
* leaks + 3/3 runner hangs per shape (the orphan kept stdout open); only a
|
|
10
|
+
* bare `sleep N`, exec'd without a fork, died. A native child (bun.exe) stays
|
|
11
|
+
* reachable, which is why the shipped test passed. A Job Object records
|
|
12
|
+
* membership at CreateProcess time and inherits it down the tree, so
|
|
13
|
+
* TerminateJobObject kills every descendant regardless of parent links.
|
|
14
|
+
*
|
|
15
|
+
* Per-spawn lifecycle (executor.ts bunRunner):
|
|
16
|
+
* createWinJob() → assign(launcherPid) right after Bun.spawn
|
|
17
|
+
* abort → terminate() TerminateJobObject, then close
|
|
18
|
+
* settle → release() a run that completed on its own:
|
|
19
|
+
* drop the close-kill limit, then close — a child the command
|
|
20
|
+
* deliberately left behind (`server > log 2>&1 &`) survives, as
|
|
21
|
+
* it does on POSIX and did before this port. While the command is
|
|
22
|
+
* in flight the limit stays armed, so an rovecode crash takes the
|
|
23
|
+
* tree with it (the OS closes the handle).
|
|
24
|
+
* assign() also sweeps the launcher's already-visible descendants by a
|
|
25
|
+
* Toolhelp32 parent-chain walk: a child forked in the spawn→assign window is
|
|
26
|
+
* not a member by inheritance (under load that window can span a scheduler
|
|
27
|
+
* quantum). Residual: a Cygwin stub that already exec'd AND exited inside
|
|
28
|
+
* that window leaves its child unreachable by any parent walk. The sweep
|
|
29
|
+
* costs one process snapshot (~15ms with ~300 processes on the loaded
|
|
30
|
+
* reference box).
|
|
31
|
+
* Fail-safe: non-win32, no bun:ffi, or any kernel32 call failing → null; the
|
|
32
|
+
* caller keeps `taskkill /T /F` alone and reports treeKill "taskkill-only".
|
|
33
|
+
* Nested jobs (Windows 8+) let this work when rovecode itself already runs
|
|
34
|
+
* inside a job (terminals and IDEs do that): the launcher is then in both. */
|
|
35
|
+
|
|
36
|
+
import { dlopen, FFIType, ptr } from "bun:ffi";
|
|
37
|
+
|
|
38
|
+
const JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x2000;
|
|
39
|
+
const JobObjectExtendedLimitInformation = 9;
|
|
40
|
+
/** sizeof(JOBOBJECT_EXTENDED_LIMIT_INFORMATION) on x64; LimitFlags sits at
|
|
41
|
+
* BasicLimitInformation offset 16 (after two LARGE_INTEGERs). */
|
|
42
|
+
const EXTENDED_LIMIT_SIZE = 144;
|
|
43
|
+
const LIMIT_FLAGS_OFFSET = 16;
|
|
44
|
+
const PROCESS_SET_QUOTA_AND_TERMINATE = 0x0101;
|
|
45
|
+
const TH32CS_SNAPPROCESS = 0x2;
|
|
46
|
+
/** sizeof(PROCESSENTRY32W) on x64; th32ProcessID at 8, th32ParentProcessID at 32. */
|
|
47
|
+
const PE32_SIZE = 568;
|
|
48
|
+
const PE32_PID = 8;
|
|
49
|
+
const PE32_PPID = 32;
|
|
50
|
+
|
|
51
|
+
type Handle = number;
|
|
52
|
+
|
|
53
|
+
interface Kernel32 {
|
|
54
|
+
CreateJobObjectW(attrs: null, name: null): Handle | null;
|
|
55
|
+
SetInformationJobObject(job: Handle, cls: number, info: number, len: number): number;
|
|
56
|
+
AssignProcessToJobObject(job: Handle, proc: Handle): number;
|
|
57
|
+
TerminateJobObject(job: Handle, exitCode: number): number;
|
|
58
|
+
OpenProcess(access: number, inherit: number, pid: number): Handle | null;
|
|
59
|
+
CloseHandle(h: Handle): number;
|
|
60
|
+
CreateToolhelp32Snapshot(flags: number, pid: number): Handle | null;
|
|
61
|
+
Process32FirstW(snap: Handle, entry: number): number;
|
|
62
|
+
Process32NextW(snap: Handle, entry: number): number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let library: { symbols: unknown } | null | undefined; // undefined = not tried yet
|
|
66
|
+
let override: boolean | null = null;
|
|
67
|
+
|
|
68
|
+
function kernel32(): Kernel32 | null {
|
|
69
|
+
if (library === undefined) {
|
|
70
|
+
library = null;
|
|
71
|
+
if (process.platform === "win32") {
|
|
72
|
+
try {
|
|
73
|
+
library = dlopen("kernel32.dll", {
|
|
74
|
+
CreateJobObjectW: { args: [FFIType.ptr, FFIType.ptr], returns: FFIType.ptr },
|
|
75
|
+
SetInformationJobObject: { args: [FFIType.ptr, FFIType.i32, FFIType.ptr, FFIType.u32], returns: FFIType.i32 },
|
|
76
|
+
AssignProcessToJobObject: { args: [FFIType.ptr, FFIType.ptr], returns: FFIType.i32 },
|
|
77
|
+
TerminateJobObject: { args: [FFIType.ptr, FFIType.u32], returns: FFIType.i32 },
|
|
78
|
+
OpenProcess: { args: [FFIType.u32, FFIType.i32, FFIType.u32], returns: FFIType.ptr },
|
|
79
|
+
CloseHandle: { args: [FFIType.ptr], returns: FFIType.i32 },
|
|
80
|
+
CreateToolhelp32Snapshot: { args: [FFIType.u32, FFIType.u32], returns: FFIType.ptr },
|
|
81
|
+
Process32FirstW: { args: [FFIType.ptr, FFIType.ptr], returns: FFIType.i32 },
|
|
82
|
+
Process32NextW: { args: [FFIType.ptr, FFIType.ptr], returns: FFIType.i32 },
|
|
83
|
+
});
|
|
84
|
+
} catch { library = null; }
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return library ? (library.symbols as Kernel32) : null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** A kernel HANDLE is a small positive value: NULL arrives as null and
|
|
91
|
+
* INVALID_HANDLE_VALUE (-1) fits this range in no representation. */
|
|
92
|
+
const isHandle = (h: unknown): h is Handle => typeof h === "number" && h > 0 && h <= 0xffff_ffff;
|
|
93
|
+
|
|
94
|
+
/** Whether the real Job Object path is usable here (status surfaces, tests). */
|
|
95
|
+
export function winJobsAvailable(): boolean { return override ?? kernel32() !== null; }
|
|
96
|
+
|
|
97
|
+
/** Test seam: `false` forces the fail-safe path (the box-without-kernel32
|
|
98
|
+
* shape — callers fall back to taskkill-only); `null` restores detection. */
|
|
99
|
+
export function overrideWinJobs(available: boolean | null): void { override = available; }
|
|
100
|
+
|
|
101
|
+
export interface WinJob {
|
|
102
|
+
/** Put `pid` and its currently visible descendants in the job. false →
|
|
103
|
+
* nothing is a member (caller must fall back); the job is closed. */
|
|
104
|
+
assign(pid: number): boolean;
|
|
105
|
+
/** Abort: kill every member now, then close. Idempotent. */
|
|
106
|
+
terminate(): void;
|
|
107
|
+
/** Normal settle: clear the close-kill limit, then close — members that
|
|
108
|
+
* outlived the command keep running. No-op after terminate(). */
|
|
109
|
+
release(): void;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** One job per spawn, armed with KILL_ON_JOB_CLOSE. null = unavailable. */
|
|
113
|
+
export function createWinJob(): WinJob | null {
|
|
114
|
+
if (override === false) return null;
|
|
115
|
+
const k = kernel32();
|
|
116
|
+
if (!k) return null;
|
|
117
|
+
const job = k.CreateJobObjectW(null, null);
|
|
118
|
+
if (!isHandle(job)) return null;
|
|
119
|
+
const limits = new Uint8Array(EXTENDED_LIMIT_SIZE);
|
|
120
|
+
new DataView(limits.buffer).setUint32(LIMIT_FLAGS_OFFSET, JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, true);
|
|
121
|
+
if (!k.SetInformationJobObject(job, JobObjectExtendedLimitInformation, ptr(limits), EXTENDED_LIMIT_SIZE)) {
|
|
122
|
+
k.CloseHandle(job);
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
let handle: Handle | null = job;
|
|
126
|
+
const close = (): void => { if (handle !== null) { k.CloseHandle(handle); handle = null; } };
|
|
127
|
+
const assignOne = (pid: number): boolean => {
|
|
128
|
+
if (handle === null) return false;
|
|
129
|
+
const p = k.OpenProcess(PROCESS_SET_QUOTA_AND_TERMINATE, 0, pid);
|
|
130
|
+
if (!isHandle(p)) return false;
|
|
131
|
+
try { return k.AssignProcessToJobObject(handle, p) !== 0; } finally { k.CloseHandle(p); }
|
|
132
|
+
};
|
|
133
|
+
return {
|
|
134
|
+
assign(pid) {
|
|
135
|
+
if (!assignOne(pid)) { close(); return false; }
|
|
136
|
+
// belt-and-braces for the spawn→assign window; already-member or
|
|
137
|
+
// already-exited descendants simply fail to assign, which is fine
|
|
138
|
+
for (const d of descendants(k, pid)) assignOne(d);
|
|
139
|
+
return true;
|
|
140
|
+
},
|
|
141
|
+
terminate() {
|
|
142
|
+
if (handle === null) return;
|
|
143
|
+
k.TerminateJobObject(handle, 1);
|
|
144
|
+
close();
|
|
145
|
+
},
|
|
146
|
+
release() {
|
|
147
|
+
if (handle === null) return;
|
|
148
|
+
k.SetInformationJobObject(handle, JobObjectExtendedLimitInformation, ptr(new Uint8Array(EXTENDED_LIMIT_SIZE)), EXTENDED_LIMIT_SIZE);
|
|
149
|
+
close();
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Live descendants of `root` by parent-pid chain (one Toolhelp32 snapshot).
|
|
155
|
+
* pids recycle, so a stale parent link could form a cycle: visited-set walk. */
|
|
156
|
+
function descendants(k: Kernel32, root: number): number[] {
|
|
157
|
+
const snap = k.CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
|
|
158
|
+
if (!isHandle(snap)) return [];
|
|
159
|
+
const children = new Map<number, number[]>();
|
|
160
|
+
try {
|
|
161
|
+
const entry = new Uint8Array(PE32_SIZE);
|
|
162
|
+
const view = new DataView(entry.buffer);
|
|
163
|
+
view.setUint32(0, PE32_SIZE, true); // dwSize must be set before Process32FirstW
|
|
164
|
+
if (!k.Process32FirstW(snap, ptr(entry))) return [];
|
|
165
|
+
do {
|
|
166
|
+
const pid = view.getUint32(PE32_PID, true);
|
|
167
|
+
const ppid = view.getUint32(PE32_PPID, true);
|
|
168
|
+
const list = children.get(ppid);
|
|
169
|
+
if (list) list.push(pid); else children.set(ppid, [pid]);
|
|
170
|
+
} while (k.Process32NextW(snap, ptr(entry)));
|
|
171
|
+
} finally {
|
|
172
|
+
k.CloseHandle(snap);
|
|
173
|
+
}
|
|
174
|
+
const seen = new Set<number>([root]);
|
|
175
|
+
const out: number[] = [];
|
|
176
|
+
const stack = [root];
|
|
177
|
+
while (stack.length > 0) {
|
|
178
|
+
for (const c of children.get(stack.pop()!) ?? []) {
|
|
179
|
+
if (!seen.has(c)) { seen.add(c); out.push(c); stack.push(c); }
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return out;
|
|
183
|
+
}
|