rovecode 0.3.2

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 (334) hide show
  1. package/LICENSE +662 -0
  2. package/README.md +737 -0
  3. package/THIRD_PARTY_NOTICES.md +268 -0
  4. package/bin/rovecode.ts +21 -0
  5. package/package.json +56 -0
  6. package/src/acp/server.ts +374 -0
  7. package/src/cli/auth-login.ts +122 -0
  8. package/src/cli/connect.ts +244 -0
  9. package/src/cli/context-cmd.ts +199 -0
  10. package/src/cli/dispatch.ts +82 -0
  11. package/src/cli/doctor.ts +362 -0
  12. package/src/cli/export.ts +276 -0
  13. package/src/cli/help.ts +293 -0
  14. package/src/cli/is-tui-invocation.ts +8 -0
  15. package/src/cli/main.ts +583 -0
  16. package/src/cli/market-cmd.ts +658 -0
  17. package/src/cli/mcp-login.ts +141 -0
  18. package/src/cli/mcp-market-cmd.ts +302 -0
  19. package/src/cli/output.ts +382 -0
  20. package/src/cli/repl.ts +250 -0
  21. package/src/cli/repomap-root.ts +14 -0
  22. package/src/cli/resume.ts +57 -0
  23. package/src/cli/run-flags.ts +43 -0
  24. package/src/cli/run-limits.ts +78 -0
  25. package/src/cli/runtime.ts +931 -0
  26. package/src/cli/session-arg.ts +30 -0
  27. package/src/cli/sessions-cmd.ts +145 -0
  28. package/src/cli/setup.ts +153 -0
  29. package/src/cli/skills-cmd.ts +194 -0
  30. package/src/cli/start-chat.ts +65 -0
  31. package/src/cli/trust-cmd.ts +52 -0
  32. package/src/coding/bash.ts +148 -0
  33. package/src/coding/checkpoints.ts +327 -0
  34. package/src/coding/diff.ts +138 -0
  35. package/src/coding/files.ts +341 -0
  36. package/src/coding/hashline.ts +274 -0
  37. package/src/coding/lsp-gate.ts +254 -0
  38. package/src/coding/lsp-servers.ts +147 -0
  39. package/src/coding/lsp.ts +283 -0
  40. package/src/coding/repomap-cache.ts +99 -0
  41. package/src/coding/repomap-files.ts +192 -0
  42. package/src/coding/repomap.ts +481 -0
  43. package/src/core/agents.ts +255 -0
  44. package/src/core/compaction.ts +259 -0
  45. package/src/core/config.ts +289 -0
  46. package/src/core/context-report.ts +228 -0
  47. package/src/core/context.ts +60 -0
  48. package/src/core/count-remote.ts +107 -0
  49. package/src/core/execpolicy-rules.ts +196 -0
  50. package/src/core/execpolicy.ts +385 -0
  51. package/src/core/executor.ts +454 -0
  52. package/src/core/guardrails.ts +400 -0
  53. package/src/core/hooks.ts +411 -0
  54. package/src/core/images.ts +230 -0
  55. package/src/core/intro.ts +266 -0
  56. package/src/core/loop.ts +567 -0
  57. package/src/core/modes.ts +372 -0
  58. package/src/core/orchestrator.ts +245 -0
  59. package/src/core/proc-group.ts +48 -0
  60. package/src/core/project-trust.ts +98 -0
  61. package/src/core/reflection.ts +165 -0
  62. package/src/core/sandbox-config.ts +186 -0
  63. package/src/core/session-id.ts +24 -0
  64. package/src/core/session-images.ts +73 -0
  65. package/src/core/session-ops.ts +183 -0
  66. package/src/core/session-text.ts +29 -0
  67. package/src/core/session.ts +469 -0
  68. package/src/core/settings.ts +170 -0
  69. package/src/core/tasks.ts +646 -0
  70. package/src/core/token-scale.ts +108 -0
  71. package/src/core/tools.ts +309 -0
  72. package/src/core/trust.ts +104 -0
  73. package/src/core/types.ts +330 -0
  74. package/src/core/update-check.ts +171 -0
  75. package/src/core/usage.ts +204 -0
  76. package/src/core/validate.ts +121 -0
  77. package/src/core/verify-gate.ts +159 -0
  78. package/src/core/verify.ts +236 -0
  79. package/src/core/voice.ts +158 -0
  80. package/src/core/win-job.ts +183 -0
  81. package/src/core/workspace.ts +184 -0
  82. package/src/design/audit.ts +797 -0
  83. package/src/design/direction.ts +190 -0
  84. package/src/design/rules.ts +157 -0
  85. package/src/eval/bench.ts +150 -0
  86. package/src/eval/gauntlet-runner.ts +215 -0
  87. package/src/eval/gauntlet-support.ts +84 -0
  88. package/src/eval/gauntlet-wave3.ts +269 -0
  89. package/src/eval/gauntlet-wave4.ts +217 -0
  90. package/src/eval/gauntlet.ts +253 -0
  91. package/src/index.ts +17 -0
  92. package/src/lanes/agy.ts +95 -0
  93. package/src/lanes/approval.ts +24 -0
  94. package/src/lanes/claude.ts +129 -0
  95. package/src/lanes/codex.ts +127 -0
  96. package/src/lanes/events.ts +130 -0
  97. package/src/lanes/job.ts +142 -0
  98. package/src/lanes/opencode.ts +122 -0
  99. package/src/lanes/process.ts +184 -0
  100. package/src/lanes/progress.ts +183 -0
  101. package/src/lanes/registry.ts +178 -0
  102. package/src/lanes/runner.ts +124 -0
  103. package/src/lanes/types.ts +112 -0
  104. package/src/market/catalogs/mcp-docs.json +111 -0
  105. package/src/market/catalogs/plugins.json +111 -0
  106. package/src/market/catalogs/skills.json +478 -0
  107. package/src/market/clone.ts +72 -0
  108. package/src/market/context-cost.ts +121 -0
  109. package/src/market/digest.ts +106 -0
  110. package/src/market/index.ts +22 -0
  111. package/src/market/install.ts +578 -0
  112. package/src/market/manifest.ts +187 -0
  113. package/src/market/prereq.ts +145 -0
  114. package/src/market/registry.ts +363 -0
  115. package/src/market/resolve.ts +111 -0
  116. package/src/market/types.ts +236 -0
  117. package/src/market/validate.ts +227 -0
  118. package/src/mcp/client.ts +449 -0
  119. package/src/mcp/config.ts +252 -0
  120. package/src/mcp/local-package.ts +211 -0
  121. package/src/mcp/market-catalog.ts +84 -0
  122. package/src/mcp/market-install.ts +289 -0
  123. package/src/mcp/market.ts +362 -0
  124. package/src/mcp/oauth.ts +251 -0
  125. package/src/mcp/prompts-resources.ts +249 -0
  126. package/src/mcp/shared.ts +149 -0
  127. package/src/mcp/status.ts +67 -0
  128. package/src/mcp/tools.ts +275 -0
  129. package/src/mcp/transport.ts +122 -0
  130. package/src/mcp/trust.ts +25 -0
  131. package/src/memory/blocks.ts +278 -0
  132. package/src/memory/recall.ts +355 -0
  133. package/src/memory/scope.ts +182 -0
  134. package/src/memory/store.ts +105 -0
  135. package/src/memory/tools.ts +99 -0
  136. package/src/plugins/cli.ts +119 -0
  137. package/src/plugins/discover.ts +108 -0
  138. package/src/plugins/index.ts +50 -0
  139. package/src/plugins/install.ts +184 -0
  140. package/src/plugins/load.ts +124 -0
  141. package/src/plugins/manifest.ts +92 -0
  142. package/src/plugins/state.ts +83 -0
  143. package/src/providers/auth.ts +408 -0
  144. package/src/providers/cache.ts +223 -0
  145. package/src/providers/catalog-local.ts +160 -0
  146. package/src/providers/catalog.ts +421 -0
  147. package/src/providers/middleware-context.ts +86 -0
  148. package/src/providers/middleware.ts +373 -0
  149. package/src/providers/model-list.ts +23 -0
  150. package/src/providers/models-index.json +1 -0
  151. package/src/providers/oauth/common.ts +105 -0
  152. package/src/providers/oauth/device-code.ts +107 -0
  153. package/src/providers/oauth/github-copilot.ts +146 -0
  154. package/src/providers/oauth/loopback.ts +158 -0
  155. package/src/providers/oauth/openai.ts +163 -0
  156. package/src/providers/oauth/openrouter.ts +89 -0
  157. package/src/providers/oauth/pkce.ts +45 -0
  158. package/src/providers/oauth/registry.ts +39 -0
  159. package/src/providers/oauth/seam.ts +89 -0
  160. package/src/providers/profile-glm53.ts +111 -0
  161. package/src/providers/profile-sonnet5-persona.ts +65 -0
  162. package/src/providers/profile-sonnet5-voice.ts +23 -0
  163. package/src/providers/profiles.ts +156 -0
  164. package/src/providers/provider-config.ts +311 -0
  165. package/src/providers/registry.ts +333 -0
  166. package/src/providers/responses.ts +209 -0
  167. package/src/providers/retry.ts +234 -0
  168. package/src/providers/router.ts +294 -0
  169. package/src/providers/sse.ts +26 -0
  170. package/src/providers/stream-errors.ts +117 -0
  171. package/src/providers/stream.ts +566 -0
  172. package/src/providers/thinking.ts +189 -0
  173. package/src/providers/wire-messages.ts +129 -0
  174. package/src/providers/wire-responses.ts +79 -0
  175. package/src/providers/wire-select.ts +53 -0
  176. package/src/server/http.ts +291 -0
  177. package/src/server/openapi.ts +246 -0
  178. package/src/sextant/card-hits.ts +102 -0
  179. package/src/sextant/card-keys.ts +55 -0
  180. package/src/sextant/context-source.ts +157 -0
  181. package/src/sextant/crew-cards.ts +350 -0
  182. package/src/sextant/draw-agents.ts +273 -0
  183. package/src/sextant/draw-code.ts +388 -0
  184. package/src/sextant/draw-context.ts +222 -0
  185. package/src/sextant/draw-frame.ts +164 -0
  186. package/src/sextant/draw-market.ts +573 -0
  187. package/src/sextant/draw-messages.ts +386 -0
  188. package/src/sextant/draw-pet.ts +230 -0
  189. package/src/sextant/draw-plan.ts +187 -0
  190. package/src/sextant/draw-tabs.ts +85 -0
  191. package/src/sextant/draw-util.ts +65 -0
  192. package/src/sextant/draw-wizard.ts +378 -0
  193. package/src/sextant/engine.ts +230 -0
  194. package/src/sextant/frame-hits.ts +25 -0
  195. package/src/sextant/frame.ts +101 -0
  196. package/src/sextant/git-status.ts +197 -0
  197. package/src/sextant/grid.ts +59 -0
  198. package/src/sextant/input.ts +119 -0
  199. package/src/sextant/keys.ts +521 -0
  200. package/src/sextant/layout.ts +86 -0
  201. package/src/sextant/local-commands.ts +169 -0
  202. package/src/sextant/market-source.ts +287 -0
  203. package/src/sextant/mentions.ts +200 -0
  204. package/src/sextant/message-hits.ts +26 -0
  205. package/src/sextant/model.ts +387 -0
  206. package/src/sextant/overlays.ts +456 -0
  207. package/src/sextant/panel-hits.ts +38 -0
  208. package/src/sextant/pet.ts +399 -0
  209. package/src/sextant/screen.ts +324 -0
  210. package/src/sextant/scroll-hits.ts +66 -0
  211. package/src/sextant/scrollbar.ts +82 -0
  212. package/src/sextant/sextant-bridge.ts +174 -0
  213. package/src/sextant/sextant-cards.ts +142 -0
  214. package/src/sextant/sextant-diff-base.ts +63 -0
  215. package/src/sextant/sextant-files.ts +154 -0
  216. package/src/sextant/sextant-frame-loop.ts +335 -0
  217. package/src/sextant/sextant-renderer.ts +574 -0
  218. package/src/sextant/sextant-repo.ts +140 -0
  219. package/src/sextant/theme.ts +66 -0
  220. package/src/sextant/tool-rows.ts +189 -0
  221. package/src/sextant/types.ts +493 -0
  222. package/src/skills/index.ts +387 -0
  223. package/src/skills/pack.ts +220 -0
  224. package/src/skills/spec.ts +162 -0
  225. package/src/skills/tools.ts +69 -0
  226. package/src/skills/versioned.ts +227 -0
  227. package/src/telemetry/otel-export.ts +122 -0
  228. package/src/telemetry/otel-lanes.ts +89 -0
  229. package/src/telemetry/otel-logs.ts +131 -0
  230. package/src/telemetry/otel-metrics.ts +136 -0
  231. package/src/telemetry/otel.ts +397 -0
  232. package/src/telemetry/otlp.ts +76 -0
  233. package/src/tools/ask-user.ts +156 -0
  234. package/src/tools/bash-bg.ts +94 -0
  235. package/src/tools/bash-jobs.ts +237 -0
  236. package/src/tools/design.ts +151 -0
  237. package/src/tools/evalcell.ts +338 -0
  238. package/src/tools/html-text.ts +139 -0
  239. package/src/tools/provider.ts +149 -0
  240. package/src/tools/task.ts +250 -0
  241. package/src/tools/todo.ts +320 -0
  242. package/src/tools/webfetch.ts +332 -0
  243. package/src/tools/websearch.ts +359 -0
  244. package/src/tui/agents-cmd.ts +41 -0
  245. package/src/tui/app.ts +749 -0
  246. package/src/tui/attach.ts +127 -0
  247. package/src/tui/boot-notes.ts +41 -0
  248. package/src/tui/builtin-prompts.ts +59 -0
  249. package/src/tui/checkpoints-cmd.ts +70 -0
  250. package/src/tui/clipboard-image.ts +81 -0
  251. package/src/tui/clipboard.ts +78 -0
  252. package/src/tui/commands.ts +283 -0
  253. package/src/tui/config-view.ts +53 -0
  254. package/src/tui/context-cmds.ts +282 -0
  255. package/src/tui/cost.ts +108 -0
  256. package/src/tui/crash-guard.ts +173 -0
  257. package/src/tui/focus-terminal.ts +34 -0
  258. package/src/tui/git-cmds.ts +273 -0
  259. package/src/tui/git-plain.ts +58 -0
  260. package/src/tui/info-cmd.ts +150 -0
  261. package/src/tui/input-plain.ts +76 -0
  262. package/src/tui/mcp-cmd.ts +128 -0
  263. package/src/tui/memory-note.ts +77 -0
  264. package/src/tui/modes-cmd.ts +45 -0
  265. package/src/tui/notify-seq.ts +100 -0
  266. package/src/tui/notify.ts +318 -0
  267. package/src/tui/overlays.ts +97 -0
  268. package/src/tui/pi-renderer.ts +428 -0
  269. package/src/tui/providers-cmd.ts +377 -0
  270. package/src/tui/reasoning-view.ts +56 -0
  271. package/src/tui/renderer.ts +128 -0
  272. package/src/tui/replay-marker.ts +29 -0
  273. package/src/tui/session-cmd.ts +148 -0
  274. package/src/tui/session-manage.ts +95 -0
  275. package/src/tui/sextant-attach.ts +102 -0
  276. package/src/tui/sextant-io.ts +202 -0
  277. package/src/tui/sextant-smoke.ts +110 -0
  278. package/src/tui/shell-cmd.ts +158 -0
  279. package/src/tui/smoke.ts +72 -0
  280. package/src/tui/staged-terminal.ts +50 -0
  281. package/src/tui/startup.ts +12 -0
  282. package/src/tui/theme.ts +59 -0
  283. package/src/tui/todo-label.ts +7 -0
  284. package/src/tui/trust-card.ts +107 -0
  285. package/src/tui/tui-commands.ts +87 -0
  286. package/tsconfig.json +30 -0
  287. package/vendor/pi-tui/LICENSE +21 -0
  288. package/vendor/pi-tui/PATCHES.md +12 -0
  289. package/vendor/pi-tui/PROVENANCE.md +12 -0
  290. package/vendor/pi-tui/README.upstream.md +854 -0
  291. package/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-console-mode.node +0 -0
  292. package/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-console-mode.node +0 -0
  293. package/vendor/pi-tui/src/alt-screen-search.ts +158 -0
  294. package/vendor/pi-tui/src/autocomplete.ts +827 -0
  295. package/vendor/pi-tui/src/components/alt-screen-flash.ts +52 -0
  296. package/vendor/pi-tui/src/components/box.ts +138 -0
  297. package/vendor/pi-tui/src/components/cancellable-loader.ts +41 -0
  298. package/vendor/pi-tui/src/components/editor.ts +2364 -0
  299. package/vendor/pi-tui/src/components/h-stack.ts +45 -0
  300. package/vendor/pi-tui/src/components/image.ts +128 -0
  301. package/vendor/pi-tui/src/components/input.ts +448 -0
  302. package/vendor/pi-tui/src/components/loader.ts +93 -0
  303. package/vendor/pi-tui/src/components/markdown.ts +1016 -0
  304. package/vendor/pi-tui/src/components/scroll-view.ts +217 -0
  305. package/vendor/pi-tui/src/components/select-list.ts +230 -0
  306. package/vendor/pi-tui/src/components/settings-list.ts +277 -0
  307. package/vendor/pi-tui/src/components/spacer.ts +29 -0
  308. package/vendor/pi-tui/src/components/stack.ts +155 -0
  309. package/vendor/pi-tui/src/components/text.ts +108 -0
  310. package/vendor/pi-tui/src/components/truncated-text.ts +66 -0
  311. package/vendor/pi-tui/src/components/v-stack.ts +34 -0
  312. package/vendor/pi-tui/src/editor-component.ts +75 -0
  313. package/vendor/pi-tui/src/fuzzy.ts +138 -0
  314. package/vendor/pi-tui/src/index.ts +149 -0
  315. package/vendor/pi-tui/src/keybindings.ts +321 -0
  316. package/vendor/pi-tui/src/keys.ts +1402 -0
  317. package/vendor/pi-tui/src/kill-ring.ts +47 -0
  318. package/vendor/pi-tui/src/latex.ts +1381 -0
  319. package/vendor/pi-tui/src/layout-node.ts +52 -0
  320. package/vendor/pi-tui/src/layout.ts +411 -0
  321. package/vendor/pi-tui/src/native-modifiers.ts +60 -0
  322. package/vendor/pi-tui/src/native-module-path.ts +32 -0
  323. package/vendor/pi-tui/src/stdin-buffer.ts +445 -0
  324. package/vendor/pi-tui/src/terminal-colors.ts +74 -0
  325. package/vendor/pi-tui/src/terminal-image.ts +701 -0
  326. package/vendor/pi-tui/src/terminal.ts +554 -0
  327. package/vendor/pi-tui/src/tui-alt-screen.ts +1379 -0
  328. package/vendor/pi-tui/src/tui-main-screen.ts +655 -0
  329. package/vendor/pi-tui/src/tui.ts +1264 -0
  330. package/vendor/pi-tui/src/undo-stack.ts +29 -0
  331. package/vendor/pi-tui/src/utils.ts +1327 -0
  332. package/vendor/pi-tui/src/word-navigation.ts +118 -0
  333. package/vendor/pi-tui/test/test-themes.ts +39 -0
  334. package/vendor/pi-tui/test/virtual-terminal.ts +219 -0
@@ -0,0 +1,94 @@
1
+ /** The three tools that read and stop background shell jobs (port #55): `bash_list`, `bash_output`,
2
+ * `bash_kill`. The job manager itself is tools/bash-jobs.ts; starting a job is `bash` with a flag.
3
+ *
4
+ * WHY THREE READ/WRITE TOOLS AND NOT ONE. A model that starts a four-minute build has to be able to
5
+ * ask three different questions — what is still running, what has it printed since I last looked, and
6
+ * stop it — and folding them into one tool with an `action` argument means every call carries an
7
+ * argument that could be wrong. The cost is three schema entries; the benefit is that each call says
8
+ * what it does.
9
+ *
10
+ * `bash_output` returns only what is NEW since the previous read, so a model polling a build sees the
11
+ * next hundred lines rather than the same first hundred again. That is the single most important
12
+ * property here: a tool that re-sends its whole buffer every time turns a long build into a context
13
+ * leak, and the model cannot tell the repeat from real progress. When the ring dropped output the
14
+ * reader had not yet seen, the read SAYS how much it lost — a gap a model is told about can be worked
15
+ * around; a silent one is read as the command having printed nothing. */
16
+
17
+ import type { Tool, ToolOutput } from "../core/types.ts";
18
+ import { jobManager, type JobInfo } from "./bash-jobs.ts";
19
+
20
+ const NO_MANAGER = "background jobs are not available on this surface";
21
+
22
+ function describe(j: JobInfo, now: number): string {
23
+ const secs = Math.round(((j.finishedAt ?? now) - j.startedAt) / 1000);
24
+ const code = j.exitCode !== undefined ? ` exit=${j.exitCode}` : "";
25
+ const pid = j.pid !== undefined ? ` pid=${j.pid}` : "";
26
+ const drop = j.dropped > 0 ? ` (${j.dropped} chars dropped from the buffer)` : "";
27
+ return `${j.id} ${j.status}${code}${pid} ${secs}s — ${j.command}${drop}`;
28
+ }
29
+
30
+ export const bashListTool: Tool = {
31
+ schema: {
32
+ name: "bash_list",
33
+ description:
34
+ "List this session's background shell jobs (started with `bash … run_in_background: true`): id, status, exit code, pid, elapsed and the command. " +
35
+ "A finished job stays listed until its output has been read at least once, so nothing you started disappears unseen.",
36
+ args: { type: "object", properties: {} },
37
+ },
38
+ kind: "read",
39
+ execute(_args, _ctx): Promise<ToolOutput> {
40
+ const m = jobManager();
41
+ if (!m) return Promise.resolve({ ok: false, output: NO_MANAGER });
42
+ const jobs = m.list();
43
+ if (jobs.length === 0) return Promise.resolve({ ok: true, output: "no background jobs in this session" });
44
+ const now = Date.now();
45
+ return Promise.resolve({ ok: true, output: jobs.map((j) => describe(j, now)).join("\n"), data: jobs });
46
+ },
47
+ };
48
+
49
+ export const bashOutputTool: Tool = {
50
+ schema: {
51
+ name: "bash_output",
52
+ description:
53
+ "Read what a background job has printed SINCE YOUR LAST READ of it (not the whole buffer — repeated reads show progress, never the same lines twice). " +
54
+ "Says `more output remains` when the job printed more than one read returns, and says how many characters were dropped if the job outran its buffer. " +
55
+ "Do not poll in a tight loop: a finished job also posts one note into this conversation by itself.",
56
+ args: { type: "object", properties: { id: { type: "string", description: "job id from bash_list (e.g. b1)" } }, required: ["id"] },
57
+ },
58
+ kind: "read",
59
+ execute(args, _ctx): Promise<ToolOutput> {
60
+ const m = jobManager();
61
+ if (!m) return Promise.resolve({ ok: false, output: NO_MANAGER });
62
+ const id = String((args as { id: string }).id ?? "").trim();
63
+ if (id === "") return Promise.resolve({ ok: false, output: "bash_output needs an id (bash_list shows them)" });
64
+ const r = m.read(id);
65
+ if (!r.ok) return Promise.resolve({ ok: false, output: r.reason });
66
+ const head = describe(r.info, Date.now());
67
+ const lost = r.lost > 0 ? `\n[${r.lost} characters were dropped from this job's buffer before you read them]` : "";
68
+ const more = r.more ? "\n[more output remains — read again]" : "";
69
+ const body = r.text === "" ? "(nothing new)" : r.text;
70
+ return Promise.resolve({ ok: true, output: `${head}${lost}\n${body}${more}`, data: r.info });
71
+ },
72
+ };
73
+
74
+ export const bashKillTool: Tool = {
75
+ schema: {
76
+ name: "bash_kill",
77
+ description:
78
+ "Stop a background job and everything it started (the process TREE, not just the shell — a killed `npm run dev` must not leave its port bound). " +
79
+ "Killing an already-finished job is not an error. Its output stays readable afterwards.",
80
+ args: { type: "object", properties: { id: { type: "string", description: "job id from bash_list (e.g. b1)" } }, required: ["id"] },
81
+ },
82
+ kind: "execute",
83
+ execute(args, _ctx): Promise<ToolOutput> {
84
+ const m = jobManager();
85
+ if (!m) return Promise.resolve({ ok: false, output: NO_MANAGER });
86
+ const id = String((args as { id: string }).id ?? "").trim();
87
+ if (id === "") return Promise.resolve({ ok: false, output: "bash_kill needs an id (bash_list shows them)" });
88
+ const r = m.kill(id);
89
+ if (!r.ok) return Promise.resolve({ ok: false, output: r.reason ?? `could not kill "${id}"` });
90
+ return Promise.resolve({ ok: true, output: `${id} ${r.info?.status ?? "killed"} — its output is still readable with bash_output ${id}`, data: r.info });
91
+ },
92
+ };
93
+
94
+ export const bashJobTools: readonly Tool[] = [bashListTool, bashOutputTool, bashKillTool];
@@ -0,0 +1,237 @@
1
+ /** Background shell jobs (port #55). `bash … run_in_background: true` starts a job here instead of
2
+ * awaiting it; `bash_list` / `bash_output` / `bash_kill` (tools/bash-bg.ts) read and stop them.
3
+ *
4
+ * WHAT A JOB IS NOT. It is not an agent. TaskManager (core/tasks.ts) stays the one agent-job registry
5
+ * and nothing in this file runs a loop. A job is one shell command whose output the run can come back
6
+ * to — a dev server, a long build, a test suite that takes four minutes — and the reason it exists is
7
+ * that the alternative is worse in both directions: a foreground `bash` either blocks the whole run
8
+ * until the command finishes, or gets killed by a timeout and loses whatever it had printed.
9
+ *
10
+ * SAME DOOR, SAME LOCKS. Every job runs through the SAME executor seam a foreground `bash` uses, so
11
+ * the configured rung shapes its argv and `kill` is the rung's own tree kill (Windows Job Object +
12
+ * taskkill sweep; SIGTERM to the process group on POSIX, #67 — a background `npm run dev` that forks
13
+ * is exactly the case where killing only the launcher leaves the port bound). Policy is upstream:
14
+ * the permission rules, execpolicy, hooks and the human approval all run before bashTool.execute, so
15
+ * starting a job in the background is gated exactly like running it in the foreground. It is the same
16
+ * tool with a flag, deliberately (ADR-005).
17
+ *
18
+ * BOUNDS, because an unbounded buffer is a memory leak with a feature name:
19
+ * MAX_JOBS running at once; the next start is refused AS DATA, never thrown
20
+ * RING_CHARS decoded output kept per job; the oldest is dropped and the drop is COUNTED, and a
21
+ * read reports how many UNREAD characters it lost rather than silently skipping them
22
+ * MAX_READ one read returns at most this much and says `more` when output remains
23
+ * KEEP_FINISHED finished jobs stay listed until a read has seen their final output; after that only
24
+ * the newest few are kept, buffers trimmed to a tail
25
+ *
26
+ * A finished job posts ONE note through the mechanisms that already exist: a push into the run's
27
+ * SteeringQueue (the loop drains it before the next model call, like a task note) and one listener
28
+ * emission for the TUI's transcript. It does not interrupt a turn and it does not poll.
29
+ *
30
+ * Pattern source (Apache-2.0, PATTERN LEVEL ONLY, no code copied): google-gemini/gemini-cli @ 0bd1d43
31
+ * — `is_background` as a flag on the ONE shell tool, list rendering pid/status/command/exit code, a
32
+ * bounded read that refuses a pid outside the session's own history, and a timed-out result that names
33
+ * the timeout and keeps its partial output. Departures: a per-job cursor (each read returns only NEW
34
+ * output) instead of a tail snapshot, an in-memory ring instead of a log file, a hard concurrency
35
+ * bound, and the kill is the executor's tree-kill path rather than a bare signal. */
36
+
37
+ import { getExecutor, type SpawnObserver } from "../core/executor.ts";
38
+ import type { SteeringQueue } from "../core/loop.ts";
39
+
40
+ export const MAX_JOBS = 4;
41
+ export const RING_CHARS = 200_000;
42
+ export const MAX_READ = 10_000;
43
+ export const KEEP_FINISHED = 8;
44
+
45
+ export type JobStatus = "running" | "exited" | "killed" | "failed";
46
+
47
+ export interface JobInfo {
48
+ id: string;
49
+ command: string;
50
+ status: JobStatus;
51
+ startedAt: number;
52
+ finishedAt?: number;
53
+ /** the launcher's pid, once the process exists */
54
+ pid?: number;
55
+ /** the shell's exit code (exited/failed) */
56
+ exitCode?: number;
57
+ /** characters dropped from the ring because the job outran the buffer */
58
+ dropped: number;
59
+ /** true once a read has seen the job's final output — only then may it be reaped */
60
+ drained: boolean;
61
+ }
62
+
63
+ interface JobRecord {
64
+ info: JobInfo;
65
+ buf: string;
66
+ /** how far a reader has consumed `buf`; each read returns only what is new */
67
+ cursor: number;
68
+ ac: AbortController;
69
+ done: Promise<void>;
70
+ }
71
+
72
+ export interface JobManagerOptions {
73
+ /** where a finish note lands — the same queue the loop drains before its next model call */
74
+ notify?: SteeringQueue;
75
+ /** the run's signal: its abort kills every job, so background work never outlives the session */
76
+ owner?: AbortSignal;
77
+ }
78
+
79
+ export class BashJobManager {
80
+ private readonly jobs = new Map<string, JobRecord>();
81
+ private readonly listeners = new Set<(j: JobInfo) => void>();
82
+ private seq = 0;
83
+ private unbind?: () => void;
84
+
85
+ constructor(private readonly opts: JobManagerOptions = {}) {
86
+ const owner = opts.owner;
87
+ if (owner) {
88
+ const onAbort = (): void => { this.killAll(); };
89
+ owner.addEventListener("abort", onAbort, { once: true });
90
+ this.unbind = () => owner.removeEventListener("abort", onAbort);
91
+ }
92
+ }
93
+
94
+ /** Status-transition listener (the TUI's transcript note). Returns the unsubscribe. */
95
+ subscribe(fn: (j: JobInfo) => void): () => void {
96
+ this.listeners.add(fn);
97
+ return () => this.listeners.delete(fn);
98
+ }
99
+
100
+ list(): JobInfo[] { return [...this.jobs.values()].map((j) => ({ ...j.info })); }
101
+
102
+ status(id: string): JobInfo | undefined {
103
+ const j = this.jobs.get(id);
104
+ return j ? { ...j.info } : undefined;
105
+ }
106
+
107
+ private get running(): number {
108
+ let n = 0;
109
+ for (const j of this.jobs.values()) if (j.info.status === "running") n++;
110
+ return n;
111
+ }
112
+
113
+ /** Start a job. Refusals are DATA — a full pool is a normal condition, not an exception. */
114
+ start(command: string, cwd: string): { ok: true; info: JobInfo } | { ok: false; reason: string } {
115
+ if (this.running >= MAX_JOBS) {
116
+ return { ok: false, reason: `${MAX_JOBS} background jobs already running (bash_list shows them; bash_kill frees a slot) — run this one in the foreground or wait` };
117
+ }
118
+ const id = `b${++this.seq}`;
119
+ const ac = new AbortController();
120
+ const rec: JobRecord = {
121
+ info: { id, command, status: "running", startedAt: Date.now(), dropped: 0, drained: false },
122
+ buf: "", cursor: 0, ac,
123
+ done: Promise.resolve(),
124
+ };
125
+ const dec = new TextDecoder();
126
+ const observe: SpawnObserver = {
127
+ onSpawn: (pid) => { rec.info.pid = pid; },
128
+ onChunk: (_stream, bytes) => { this.append(rec, dec.decode(bytes, { stream: true })); },
129
+ };
130
+ this.jobs.set(id, rec);
131
+ rec.done = getExecutor().run(command, cwd, ac.signal, observe).then(
132
+ (r) => {
133
+ // A killed job's non-zero exit is the kill, not the command's verdict — say which happened.
134
+ if (rec.info.status === "killed") { this.finish(rec); return; }
135
+ rec.info.status = r.code === 0 ? "exited" : "failed";
136
+ rec.info.exitCode = r.code;
137
+ // The observer streamed the bytes; the result's assembled text is the SAME output a foreground
138
+ // run would have returned, so appending it would double every line. Only the tail of a run that
139
+ // produced nothing through the taps (a rung that does not observe) is taken from the result.
140
+ if (rec.buf === "" && r.text !== "") this.append(rec, r.text);
141
+ this.finish(rec);
142
+ },
143
+ (e: unknown) => {
144
+ rec.info.status = "failed";
145
+ this.append(rec, `\n[job runner threw: ${e instanceof Error ? e.message : String(e)}]`);
146
+ this.finish(rec);
147
+ },
148
+ );
149
+ return { ok: true, info: { ...rec.info } };
150
+ }
151
+
152
+ private append(rec: JobRecord, text: string): void {
153
+ if (text === "") return;
154
+ rec.buf += text;
155
+ if (rec.buf.length > RING_CHARS) {
156
+ const cut = rec.buf.length - RING_CHARS;
157
+ rec.buf = rec.buf.slice(cut);
158
+ rec.info.dropped += cut;
159
+ // the cursor moves with the window; what a reader has not read yet and lost is reported at read time
160
+ rec.cursor = Math.max(0, rec.cursor - cut);
161
+ }
162
+ }
163
+
164
+ private finish(rec: JobRecord): void {
165
+ rec.info.finishedAt = Date.now();
166
+ const info: JobInfo = { ...rec.info };
167
+ for (const l of this.listeners) l(info);
168
+ // ONE note, through the queue the loop already drains — never an interrupt, never a poll
169
+ const how = rec.info.status === "killed" ? "killed" : `exit=${rec.info.exitCode}`;
170
+ this.opts.notify?.push(`background job ${rec.info.id} finished (${how}): ${clip(rec.info.command, 60)} — bash_output ${rec.info.id} reads what it printed`);
171
+ this.reap();
172
+ }
173
+
174
+ /** Read only what is NEW since the last read. `lost` is unread output the ring dropped: a reader that
175
+ * is told "you missed 40k characters" can act on it; one that is silently handed a gap cannot. */
176
+ read(id: string): { ok: false; reason: string } | { ok: true; info: JobInfo; text: string; more: boolean; lost: number } {
177
+ const rec = this.jobs.get(id);
178
+ if (!rec) return { ok: false, reason: `no background job "${id}" in this session (bash_list shows them; a finished job is dropped after its output has been read)` };
179
+ const lostBefore = rec.info.dropped;
180
+ const fresh = rec.buf.slice(rec.cursor);
181
+ const text = fresh.slice(0, MAX_READ);
182
+ rec.cursor += text.length;
183
+ const more = rec.cursor < rec.buf.length;
184
+ if (!more && rec.info.status !== "running") { rec.info.drained = true; this.reap(); }
185
+ return { ok: true, info: { ...rec.info }, text, more, lost: lostBefore };
186
+ }
187
+
188
+ /** Kill a job through the executor's own abort path — the tree, not just the launcher. */
189
+ kill(id: string): { ok: boolean; reason?: string; info?: JobInfo } {
190
+ const rec = this.jobs.get(id);
191
+ if (!rec) return { ok: false, reason: `no background job "${id}" in this session` };
192
+ if (rec.info.status !== "running") return { ok: true, info: { ...rec.info } };
193
+ rec.info.status = "killed";
194
+ rec.ac.abort();
195
+ return { ok: true, info: { ...rec.info } };
196
+ }
197
+
198
+ killAll(): number {
199
+ let n = 0;
200
+ for (const j of this.jobs.values()) if (j.info.status === "running") { this.kill(j.info.id); n++; }
201
+ return n;
202
+ }
203
+
204
+ /** Wait for every job to settle (surface shutdown / tests). */
205
+ async drain(): Promise<void> { await Promise.allSettled([...this.jobs.values()].map((j) => j.done)); }
206
+
207
+ dispose(): void { this.killAll(); this.unbind?.(); this.listeners.clear(); }
208
+
209
+ /** Drop drained finished jobs beyond KEEP_FINISHED, oldest first. A finished job whose output nobody
210
+ * has read is NEVER dropped: that output is the only record the command ever ran. */
211
+ private reap(): void {
212
+ const done = [...this.jobs.values()].filter((j) => j.info.status !== "running" && j.info.drained);
213
+ if (done.length <= KEEP_FINISHED) return;
214
+ done.sort((a, b) => (a.info.finishedAt ?? 0) - (b.info.finishedAt ?? 0));
215
+ for (const j of done.slice(0, done.length - KEEP_FINISHED)) this.jobs.delete(j.info.id);
216
+ }
217
+ }
218
+
219
+ const clip = (s: string, n: number): string => (s.length > n ? s.slice(0, n - 1) + "…" : s);
220
+
221
+ // ---------- the one manager a runtime installs ----------
222
+ //
223
+ // bashTool has to reach the manager without every call site threading it, and a runtime must be able to
224
+ // exist without one (the `tools` listing, the gauntlet, a unit test): so `run_in_background` with no
225
+ // manager installed is a refusal that names the surface, never a silent foreground run. A background
226
+ // request that quietly ran in the foreground would block the run it was asked not to block.
227
+
228
+ let installed: BashJobManager | null = null;
229
+
230
+ export function installJobManager(m: BashJobManager | null): void { installed = m; }
231
+ export function hasJobManager(): boolean { return installed !== null; }
232
+ export function jobManager(): BashJobManager | null { return installed; }
233
+
234
+ export function startBashJob(command: string, cwd: string): { ok: true; info: JobInfo } | { ok: false; reason: string } {
235
+ if (!installed) return { ok: false, reason: "background jobs are not available on this surface — run the command in the foreground (drop run_in_background)" };
236
+ return installed.start(command, cwd);
237
+ }
@@ -0,0 +1,151 @@
1
+ /** design_audit / design_direction — the agent's half of the design protocol (design/rules.ts).
2
+ *
3
+ * Split like provider_list/provider_edit (and task/task_status) for the same reason: reading must
4
+ * never cost a prompt, writing must be visible.
5
+ * - design_audit is kind "read" -> policy action file.read, auto-allowed under the gated rules and
6
+ * in plan mode. Checking your own work has to be free, or it will not happen.
7
+ * - design_direction is kind "custom" -> action tool.design_direction, PROMPT under the gated rules
8
+ * (cli/runtime.ts buildCfg), denied in plan mode by the blanket tool.* rule, allowed under yolo.
9
+ * It writes the project's design identity, and the approval card is where the human sees exactly
10
+ * what is being recorded on their behalf. It is asked ONCE per project, which is the whole design
11
+ * of the feature -- so the one prompt is the point, not friction.
12
+ *
13
+ * design_direction deliberately cannot invent a direction: `set` records what the human chose. The
14
+ * model is expected to have asked first (the prompt section says so). Nothing here can enforce that
15
+ * -- what it can do is keep the record honest and put the write in front of a human once. */
16
+
17
+ import { isAbsolute, relative, resolve } from "node:path";
18
+ import type { Tool, ToolOutput } from "../core/types.ts";
19
+ import { auditFiles, auditProject, formatFindings, type Finding } from "../design/audit.ts";
20
+ import {
21
+ designPath, loadDirection, parseDirection, renderDirection, saveDirection,
22
+ type DesignDirection,
23
+ } from "../design/direction.ts";
24
+
25
+ /** Resolve a caller-supplied path against the run's cwd and refuse to leave it: an audit is about
26
+ * THIS project's files, and a read tool that wanders is a read tool nobody can reason about. */
27
+ function insideCwd(cwd: string, p: string): string | null {
28
+ const abs = isAbsolute(p) ? resolve(p) : resolve(cwd, p);
29
+ const rel = relative(resolve(cwd), abs);
30
+ return rel === "" || rel.startsWith("..") || isAbsolute(rel) ? null : abs;
31
+ }
32
+
33
+ export function designAuditTool(): Tool {
34
+ return {
35
+ schema: {
36
+ name: "design_audit",
37
+ description:
38
+ "Check interface code for the patterns that arrive when nobody decided (a webfont loaded without a choice, amber doing accent duty, the three-up feature grid, a centred full-viewport hero, the violet-to-blue gradient, nothing but square corners) and for drift from the direction this project recorded in .rovecode/design.json. Run it on the SOURCE TREE, not on a fetched page: a built page measures the framework's output, not the design. Pass the page files you touched along with the components they import, because density and centring are scored per page and 'no rounded corner anywhere' is scored per project. It reports evidence, not verdicts: each finding says what it counted, so you can fix it or explain why it is wrong here. Never prompts, never writes.",
39
+ args: {
40
+ type: "object",
41
+ properties: {
42
+ files: { type: "array", items: { type: "string" }, description: "paths inside the project to audit (html/css/jsx/tsx/vue/svelte)" },
43
+ source: { type: "string", description: "audit this markup/CSS directly instead of reading files" },
44
+ ignore: { type: "array", items: { type: "string" }, description: "rule ids to skip (the full table is in docs/design.md): cliche-font, font-deviation, font-named-not-loaded, cliche-accent-amber, accent-deviation, template-grid, template-icons, reflex-hero, cliche-gradient, off-palette, decoration-density, rule-line-density, everything-centered, all-square" },
45
+ },
46
+ },
47
+ },
48
+ kind: "read",
49
+ sequential: false,
50
+ async execute(args, ctx): Promise<ToolOutput> {
51
+ const a = (args && typeof args === "object" ? args : {}) as { files?: string[]; source?: string; ignore?: string[] };
52
+ const direction = loadDirection(ctx.cwd);
53
+ const ignore = a.ignore ?? [];
54
+ const findings: Finding[] = [];
55
+
56
+ // `source` is one pathless page: auditProject treats a lone file as a page so pasted markup still
57
+ // gets the page-scoped density and centring checks (auditSource alone is file-scope by design).
58
+ if (typeof a.source === "string" && a.source.length > 0) {
59
+ findings.push(...auditProject([{ path: "source", text: a.source }], { direction, ignore }));
60
+ }
61
+ const paths = a.files ?? [];
62
+ if (paths.length > 0) {
63
+ const resolved: string[] = [];
64
+ const outside: string[] = [];
65
+ for (const p of paths) {
66
+ const abs = insideCwd(ctx.cwd, p);
67
+ if (abs === null) outside.push(p); else resolved.push(abs);
68
+ }
69
+ if (outside.length > 0) {
70
+ return { ok: false, output: `design_audit only reads files inside the project. Outside: ${outside.join(", ")}` };
71
+ }
72
+ findings.push(...auditFiles(resolved, { direction, ignore }).map((f) => ({
73
+ ...f, ...(f.file !== undefined ? { file: relative(resolve(ctx.cwd), f.file) || f.file } : {}),
74
+ })));
75
+ }
76
+ if (paths.length === 0 && (a.source === undefined || a.source.length === 0)) {
77
+ return { ok: false, output: "design_audit needs `files` (paths inside the project) or `source` (markup/CSS text)." };
78
+ }
79
+ return { ok: true, output: formatFindings(findings, direction), data: { findings, direction } };
80
+ },
81
+ };
82
+ }
83
+
84
+ export function designDirectionTool(): Tool {
85
+ return {
86
+ schema: {
87
+ name: "design_direction",
88
+ description:
89
+ "Read or record this project's design direction (.rovecode/design.json). `get` returns the recorded direction, or says none is recorded. `set` records the direction THE HUMAN CHOSE after you proposed three — do not call it with a direction you picked yourself. The one exception is a headless run where nothing can ask a human: then build ONE direction and record it with provisional:true and the other two in `alternatives`, so the next interactive session asks before more UI is built. Recording it is what makes the choice a once-per-project question and lets design_audit check later screens for consistency.",
90
+ args: {
91
+ type: "object",
92
+ properties: {
93
+ action: { type: "string", enum: ["get", "set"] },
94
+ name: { type: "string", description: "short name of the chosen direction, as presented to the human" },
95
+ rationale: { type: "string", description: "one line: why it suits this product" },
96
+ palette: { type: "object", description: "chosen colours, e.g. {\"ink\":\"#0b1a2e\",\"accent\":\"#c2410c\"}" },
97
+ typeface: { type: "object", description: "{\"display\":\"...\",\"text\":\"...\"}" },
98
+ corners: { type: "string", enum: ["sharp", "soft", "round"] },
99
+ layout: { type: "string", enum: ["centered", "left", "asymmetric", "grid"] },
100
+ density: { type: "string", enum: ["tight", "regular", "airy"] },
101
+ sectionOrder: { type: "array", items: { type: "string" }, description: "the page's blocks in order, e.g. [\"hero\",\"proof\",\"pricing\",\"faq\"]" },
102
+ heroPattern: { type: "string", description: "the hero pattern by name, e.g. \"command-first\" or \"product-forward\"" },
103
+ typeScale: { type: "array", items: { type: "number" }, description: "the chosen type steps, e.g. [14,16,20,28,44]" },
104
+ motion: { type: "string", description: "the motion rule in the human's words, e.g. \"one entrance per section, no loops\"" },
105
+ copyRegister: { type: "string", description: "how the copy reads, e.g. \"technical, names and numbers, no marketing verbs\"" },
106
+ notes: { type: "string", description: "what the audit cannot infer: imagery, what to avoid here" },
107
+ provisional: { type: "boolean", description: "true ONLY when no human could be asked (a headless run): you built to one direction to get unblocked. Never true when a human answered." },
108
+ alternatives: { type: "array", items: { type: "string" }, description: "with provisional: the names of the directions you did NOT build, so the next interactive session can offer them" },
109
+ },
110
+ required: ["action"],
111
+ },
112
+ },
113
+ kind: "custom",
114
+ sequential: true,
115
+ // `get` reads .rovecode/design.json and writes nothing; `set` records the project's design
116
+ // identity. Only the second is worth a human's attention, so the two get their own policy
117
+ // resource (types.ts Tool.resource) instead of sharing one rule under the tool name. Anything
118
+ // that is not the literal "get" is treated as the write: an unknown action must not read as
119
+ // the safer of the two.
120
+ resource(args: unknown): string {
121
+ const a = args && typeof args === "object" ? (args as Record<string, unknown>) : undefined;
122
+ return a?.["action"] === "get" ? "get" : "set";
123
+ },
124
+ async execute(args, ctx): Promise<ToolOutput> {
125
+ const a = (args && typeof args === "object" ? args : {}) as Record<string, unknown>;
126
+ if (a["action"] === "get") {
127
+ const d = loadDirection(ctx.cwd);
128
+ return d === null
129
+ ? { ok: true, output: `No design direction recorded for this project (${designPath(ctx.cwd)} does not exist). Propose three distinct directions and let the human choose before writing UI. If nothing here can ask a human, build ONE and record it with provisional: true.` }
130
+ : { ok: true, output: renderDirection(d), data: d };
131
+ }
132
+ if (a["action"] !== "set") return { ok: false, output: "design_direction: action must be \"get\" or \"set\"." };
133
+
134
+ const parsed: DesignDirection | null = parseDirection(a);
135
+ if (parsed === null) {
136
+ return { ok: false, output: "design_direction set needs at least `name` — the short name of the direction the human chose." };
137
+ }
138
+ // Who chose is DERIVED, never taken from the call: `provisional` means the agent picked, its
139
+ // absence means a human did. Trusting the model with both fields lets one contradict the other,
140
+ // and the whole point of the field is that it can be believed. Stamped on every write, so a
141
+ // record always says who — absent is only ever a file written before the field existed.
142
+ parsed.chosenBy = parsed.provisional === true ? "agent" : "human";
143
+ const path = saveDirection(ctx.cwd, parsed);
144
+ if (parsed.provisional === true) {
145
+ const alts = parsed.alternatives === undefined ? "" : ` The alternatives (${parsed.alternatives.join(", ")}) are recorded with it.`;
146
+ return { ok: true, output: `Recorded "${parsed.name}" in ${path} as PROVISIONAL — you chose it, not the human.${alts} Build to it and say so in your final summary, naming what you did not build. The next interactive session asks before more UI is written.`, data: parsed };
147
+ }
148
+ return { ok: true, output: `Recorded "${parsed.name}" in ${path}. Later UI in this project is built to it, and design_audit checks against it.`, data: parsed };
149
+ },
150
+ };
151
+ }