@wrongstack/tools 0.296.4 → 0.298.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (258) hide show
  1. package/dist/_spawn-stream.d.ts +2 -0
  2. package/dist/_util.d.ts +29 -1
  3. package/dist/audit.js +128 -65
  4. package/dist/bash.js +20 -4
  5. package/dist/batch-tool-use.js +70 -7
  6. package/dist/browser/index.js +31 -1
  7. package/dist/builtin.js +883 -351
  8. package/dist/codebase-index/index.js +95 -16
  9. package/dist/codebase-index/project-server-protocol.d.ts +26 -0
  10. package/dist/codebase-index/project-server.js +76 -16
  11. package/dist/codebase-index/worker.js +36 -13
  12. package/dist/codebase-index/writer-search-helpers.d.ts +16 -0
  13. package/dist/codebase-index/writer.d.ts +13 -1
  14. package/dist/design.d.ts +1 -1
  15. package/dist/design.js +4 -3
  16. package/dist/diff.js +11 -1
  17. package/dist/e2e.js +9 -6
  18. package/dist/edit.js +9 -6
  19. package/dist/exec.js +562 -516
  20. package/dist/format.js +131 -65
  21. package/dist/git.js +22 -2
  22. package/dist/glob.js +11 -5
  23. package/dist/grep.js +160 -104
  24. package/dist/index.js +834 -290
  25. package/dist/install.js +135 -68
  26. package/dist/json.js +64 -18
  27. package/dist/kanban-board-inputs.d.ts +21 -4
  28. package/dist/kanban.js +20 -5
  29. package/dist/languages/index.js +143 -71
  30. package/dist/lint.js +128 -65
  31. package/dist/next-steps.d.ts +23 -0
  32. package/dist/next-steps.js +4 -0
  33. package/dist/outdated.js +101 -26
  34. package/dist/pack.js +883 -351
  35. package/dist/patch.js +11 -1
  36. package/dist/plan.js +32 -10
  37. package/dist/process-registry.d.ts +9 -0
  38. package/dist/process-registry.js +15 -0
  39. package/dist/ps-slash.js +15 -0
  40. package/dist/read.js +104 -23
  41. package/dist/replace.js +11 -3
  42. package/dist/scaffold.js +9 -6
  43. package/dist/session-kanban.js +21 -5
  44. package/dist/task.js +3 -1
  45. package/dist/test.js +128 -65
  46. package/dist/todo.js +3 -1
  47. package/dist/tool-tier.js +883 -351
  48. package/dist/tool-use.d.ts +1 -1
  49. package/dist/tool-use.js +28 -4
  50. package/dist/tree.d.ts +1 -0
  51. package/dist/tree.js +44 -8
  52. package/dist/typecheck.js +128 -65
  53. package/dist/write.js +9 -6
  54. package/package.json +5 -4
  55. package/dist/_concurrency.d.ts.map +0 -1
  56. package/dist/_danger-detect.d.ts.map +0 -1
  57. package/dist/_edit-match.d.ts.map +0 -1
  58. package/dist/_env.d.ts.map +0 -1
  59. package/dist/_fetch-guard.d.ts.map +0 -1
  60. package/dist/_output-spool.d.ts.map +0 -1
  61. package/dist/_redact-command.d.ts.map +0 -1
  62. package/dist/_regex.d.ts.map +0 -1
  63. package/dist/_session-shell.d.ts.map +0 -1
  64. package/dist/_shell-pick.d.ts.map +0 -1
  65. package/dist/_spawn-stream.d.ts.map +0 -1
  66. package/dist/_syntax-check.d.ts.map +0 -1
  67. package/dist/_util.d.ts.map +0 -1
  68. package/dist/_win32-resolve.d.ts.map +0 -1
  69. package/dist/audit.d.ts.map +0 -1
  70. package/dist/audit.js.map +0 -7
  71. package/dist/auto-proceed-loop-guard.d.ts.map +0 -1
  72. package/dist/auto-proceed-loop-guard.js.map +0 -7
  73. package/dist/bash-kill-guard.d.ts.map +0 -1
  74. package/dist/bash.d.ts.map +0 -1
  75. package/dist/bash.js.map +0 -7
  76. package/dist/batch-tool-use.d.ts.map +0 -1
  77. package/dist/batch-tool-use.js.map +0 -7
  78. package/dist/browser/artifacts.d.ts.map +0 -1
  79. package/dist/browser/index.d.ts.map +0 -1
  80. package/dist/browser/index.js.map +0 -7
  81. package/dist/browser/manager.d.ts.map +0 -1
  82. package/dist/browser/network-guard-proxy.d.ts.map +0 -1
  83. package/dist/browser/security.d.ts.map +0 -1
  84. package/dist/browser/tools.d.ts.map +0 -1
  85. package/dist/browser/types.d.ts.map +0 -1
  86. package/dist/builtin.d.ts.map +0 -1
  87. package/dist/builtin.js.map +0 -7
  88. package/dist/circuit-breaker.d.ts.map +0 -1
  89. package/dist/circuit-breaker.js.map +0 -7
  90. package/dist/codebase-index/background-indexer.d.ts.map +0 -1
  91. package/dist/codebase-index/bm25.d.ts.map +0 -1
  92. package/dist/codebase-index/circuit-breaker.d.ts.map +0 -1
  93. package/dist/codebase-index/codebase-index-tool.d.ts.map +0 -1
  94. package/dist/codebase-index/codebase-search-tool.d.ts.map +0 -1
  95. package/dist/codebase-index/codebase-stats-tool.d.ts.map +0 -1
  96. package/dist/codebase-index/dead-code-scan.d.ts.map +0 -1
  97. package/dist/codebase-index/generic-parser.d.ts.map +0 -1
  98. package/dist/codebase-index/gitignore.d.ts.map +0 -1
  99. package/dist/codebase-index/go-parser.d.ts.map +0 -1
  100. package/dist/codebase-index/index-service.d.ts.map +0 -1
  101. package/dist/codebase-index/index.d.ts.map +0 -1
  102. package/dist/codebase-index/index.js.map +0 -7
  103. package/dist/codebase-index/indexer.d.ts.map +0 -1
  104. package/dist/codebase-index/json-parser.d.ts.map +0 -1
  105. package/dist/codebase-index/languages.d.ts.map +0 -1
  106. package/dist/codebase-index/lsp-kind.d.ts.map +0 -1
  107. package/dist/codebase-index/parser-dispatch.d.ts.map +0 -1
  108. package/dist/codebase-index/project-server-cache.d.ts.map +0 -1
  109. package/dist/codebase-index/project-server-client.d.ts.map +0 -1
  110. package/dist/codebase-index/project-server-endpoint.d.ts.map +0 -1
  111. package/dist/codebase-index/project-server-protocol.d.ts.map +0 -1
  112. package/dist/codebase-index/project-server.d.ts.map +0 -1
  113. package/dist/codebase-index/project-server.js.map +0 -7
  114. package/dist/codebase-index/py-parser.d.ts.map +0 -1
  115. package/dist/codebase-index/refs-extractor.d.ts.map +0 -1
  116. package/dist/codebase-index/rs-parser.d.ts.map +0 -1
  117. package/dist/codebase-index/schema.d.ts.map +0 -1
  118. package/dist/codebase-index/spawn-gate.d.ts.map +0 -1
  119. package/dist/codebase-index/sqlite-runtime.d.ts.map +0 -1
  120. package/dist/codebase-index/ts-parser.d.ts.map +0 -1
  121. package/dist/codebase-index/worker-protocol.d.ts.map +0 -1
  122. package/dist/codebase-index/worker.d.ts.map +0 -1
  123. package/dist/codebase-index/worker.js.map +0 -7
  124. package/dist/codebase-index/writer-admin.d.ts.map +0 -1
  125. package/dist/codebase-index/writer-bulk-insert.d.ts.map +0 -1
  126. package/dist/codebase-index/writer-graph-helpers.d.ts.map +0 -1
  127. package/dist/codebase-index/writer-graph-reader.d.ts.map +0 -1
  128. package/dist/codebase-index/writer-helpers.d.ts.map +0 -1
  129. package/dist/codebase-index/writer-pragmas.d.ts.map +0 -1
  130. package/dist/codebase-index/writer-ref-mapper.d.ts.map +0 -1
  131. package/dist/codebase-index/writer-schema.d.ts.map +0 -1
  132. package/dist/codebase-index/writer-search-helpers.d.ts.map +0 -1
  133. package/dist/codebase-index/writer-store-pool.d.ts.map +0 -1
  134. package/dist/codebase-index/writer.d.ts.map +0 -1
  135. package/dist/codebase-index/yaml-parser.d.ts.map +0 -1
  136. package/dist/design.d.ts.map +0 -1
  137. package/dist/design.js.map +0 -7
  138. package/dist/diff.d.ts.map +0 -1
  139. package/dist/diff.js.map +0 -7
  140. package/dist/document.d.ts.map +0 -1
  141. package/dist/document.js.map +0 -7
  142. package/dist/e2e.d.ts.map +0 -1
  143. package/dist/e2e.js.map +0 -7
  144. package/dist/edit.d.ts.map +0 -1
  145. package/dist/edit.js.map +0 -7
  146. package/dist/exec-kill-guard.d.ts.map +0 -1
  147. package/dist/exec.d.ts.map +0 -1
  148. package/dist/exec.js.map +0 -7
  149. package/dist/fetch.d.ts.map +0 -1
  150. package/dist/fetch.js.map +0 -7
  151. package/dist/format.d.ts.map +0 -1
  152. package/dist/format.js.map +0 -7
  153. package/dist/git.d.ts.map +0 -1
  154. package/dist/git.js.map +0 -7
  155. package/dist/glob.d.ts.map +0 -1
  156. package/dist/glob.js.map +0 -7
  157. package/dist/grep.d.ts.map +0 -1
  158. package/dist/grep.js.map +0 -7
  159. package/dist/index.d.ts.map +0 -1
  160. package/dist/index.js.map +0 -7
  161. package/dist/install.d.ts.map +0 -1
  162. package/dist/install.js.map +0 -7
  163. package/dist/json.d.ts.map +0 -1
  164. package/dist/json.js.map +0 -7
  165. package/dist/kanban-board-inputs.d.ts.map +0 -1
  166. package/dist/kanban-decomposition-actions.d.ts.map +0 -1
  167. package/dist/kanban-detail-actions.d.ts.map +0 -1
  168. package/dist/kanban-evidence-bridge.d.ts.map +0 -1
  169. package/dist/kanban-presence.d.ts.map +0 -1
  170. package/dist/kanban-split-task-handler.d.ts.map +0 -1
  171. package/dist/kanban-task-inputs.d.ts.map +0 -1
  172. package/dist/kanban-tool-results.d.ts.map +0 -1
  173. package/dist/kanban-tool-schema.d.ts.map +0 -1
  174. package/dist/kanban-tool-types.d.ts.map +0 -1
  175. package/dist/kanban.d.ts.map +0 -1
  176. package/dist/kanban.js.map +0 -7
  177. package/dist/languages/detect.d.ts.map +0 -1
  178. package/dist/languages/diagnostics.d.ts.map +0 -1
  179. package/dist/languages/execute-tool.d.ts.map +0 -1
  180. package/dist/languages/execute.d.ts.map +0 -1
  181. package/dist/languages/index.d.ts.map +0 -1
  182. package/dist/languages/index.js.map +0 -7
  183. package/dist/languages/legacy-bridge.d.ts.map +0 -1
  184. package/dist/languages/package-tool.d.ts.map +0 -1
  185. package/dist/languages/plan.d.ts.map +0 -1
  186. package/dist/languages/profile-helpers.d.ts.map +0 -1
  187. package/dist/languages/profiles/additional.d.ts.map +0 -1
  188. package/dist/languages/profiles/primary.d.ts.map +0 -1
  189. package/dist/languages/registry.d.ts.map +0 -1
  190. package/dist/languages/tool.d.ts.map +0 -1
  191. package/dist/languages/types.d.ts.map +0 -1
  192. package/dist/lint.d.ts.map +0 -1
  193. package/dist/lint.js.map +0 -7
  194. package/dist/logs.d.ts.map +0 -1
  195. package/dist/logs.js.map +0 -7
  196. package/dist/memory.d.ts.map +0 -1
  197. package/dist/memory.js.map +0 -7
  198. package/dist/mode.d.ts.map +0 -1
  199. package/dist/mode.js.map +0 -7
  200. package/dist/next-steps.d.ts.map +0 -1
  201. package/dist/next-steps.js.map +0 -7
  202. package/dist/outdated.d.ts.map +0 -1
  203. package/dist/outdated.js.map +0 -7
  204. package/dist/pack.d.ts.map +0 -1
  205. package/dist/pack.js.map +0 -7
  206. package/dist/patch.d.ts.map +0 -1
  207. package/dist/patch.js.map +0 -7
  208. package/dist/plan.d.ts.map +0 -1
  209. package/dist/plan.js.map +0 -7
  210. package/dist/process-guardian.d.ts.map +0 -1
  211. package/dist/process-registry-persistent.d.ts.map +0 -1
  212. package/dist/process-registry.d.ts.map +0 -1
  213. package/dist/process-registry.js.map +0 -7
  214. package/dist/ps-slash.d.ts.map +0 -1
  215. package/dist/ps-slash.js.map +0 -7
  216. package/dist/read.d.ts.map +0 -1
  217. package/dist/read.js.map +0 -7
  218. package/dist/replace.d.ts.map +0 -1
  219. package/dist/replace.js.map +0 -7
  220. package/dist/scaffold.d.ts.map +0 -1
  221. package/dist/scaffold.js.map +0 -7
  222. package/dist/search.d.ts.map +0 -1
  223. package/dist/search.js.map +0 -7
  224. package/dist/session-kanban.d.ts.map +0 -1
  225. package/dist/session-kanban.js.map +0 -7
  226. package/dist/set-working-dir.d.ts.map +0 -1
  227. package/dist/skill.d.ts.map +0 -1
  228. package/dist/skill.js.map +0 -7
  229. package/dist/spawn-background.d.ts.map +0 -1
  230. package/dist/task.d.ts.map +0 -1
  231. package/dist/task.js.map +0 -7
  232. package/dist/test.d.ts.map +0 -1
  233. package/dist/test.js.map +0 -7
  234. package/dist/todo.d.ts.map +0 -1
  235. package/dist/todo.js.map +0 -7
  236. package/dist/tool-diff.d.ts.map +0 -1
  237. package/dist/tool-diff.js.map +0 -7
  238. package/dist/tool-help.d.ts.map +0 -1
  239. package/dist/tool-help.js.map +0 -7
  240. package/dist/tool-icon-map.d.ts.map +0 -1
  241. package/dist/tool-icons.d.ts.map +0 -1
  242. package/dist/tool-icons.js.map +0 -7
  243. package/dist/tool-search.d.ts.map +0 -1
  244. package/dist/tool-search.js.map +0 -7
  245. package/dist/tool-summary.d.ts.map +0 -1
  246. package/dist/tool-summary.js.map +0 -7
  247. package/dist/tool-tier.d.ts.map +0 -1
  248. package/dist/tool-tier.js.map +0 -7
  249. package/dist/tool-use.d.ts.map +0 -1
  250. package/dist/tool-use.js.map +0 -7
  251. package/dist/tree.d.ts.map +0 -1
  252. package/dist/tree.js.map +0 -7
  253. package/dist/typecheck.d.ts.map +0 -1
  254. package/dist/typecheck.js.map +0 -7
  255. package/dist/win32.d.ts.map +0 -1
  256. package/dist/win32.js.map +0 -7
  257. package/dist/write.d.ts.map +0 -1
  258. package/dist/write.js.map +0 -7
@@ -20,6 +20,8 @@ export interface SpawnStreamOptions {
20
20
  flushBytes?: number | undefined;
21
21
  /** Maximum chunks to buffer before applying backpressure to the child. Default 500. */
22
22
  maxQueueSize?: number | undefined;
23
+ /** Maximum UTF-8 bytes retained in the producer/consumer queue. Defaults to 1 MiB. */
24
+ maxQueueBytes?: number | undefined;
23
25
  }
24
26
  /**
25
27
  * Spawn a child process and yield `partial_output` progress events as
package/dist/_util.d.ts CHANGED
@@ -32,7 +32,35 @@ export declare function safeResolve(input: string, ctx: Context): string;
32
32
  * the caller named exactly one file.
33
33
  */
34
34
  export declare function assertRealInsideRoot(absPath: string, ctx: Context): Promise<void>;
35
- /** `safeResolve` + symlink realpath containment check. Async. */
35
+ /**
36
+ * Containment check that RETURNS the canonical path it validated (WS-048).
37
+ *
38
+ * The check resolves symlinks and then confirms the resolved target is inside
39
+ * an allowed root. Handing the caller back the *unresolved* path throws that
40
+ * work away: the caller opens a path whose components are still symlinks, so
41
+ * whatever the check proved about the target is no longer what the caller
42
+ * touches. Swapping an intermediate component between the check and the open
43
+ * redirects the operation, and nothing downstream re-validates.
44
+ *
45
+ * Returning the resolved path removes that gap for every component that
46
+ * existed at check time — those are now literal directories, not links that can
47
+ * be re-pointed. It is not a total TOCTOU cure (only fd-relative operations
48
+ * would be), but the window it closes is the one this function's own contract
49
+ * claims to have closed.
50
+ *
51
+ * For a path that does not exist yet — a `write` to a new file — the deepest
52
+ * existing ancestor is resolved and the not-yet-created tail is re-joined onto
53
+ * it, so the canonical form is still returned rather than the raw input.
54
+ */
55
+ export declare function resolveRealInsideRoot(absPath: string, ctx: Context): Promise<string>;
56
+ /**
57
+ * `safeResolve` + symlink realpath containment check, returning the CANONICAL
58
+ * path (WS-048).
59
+ *
60
+ * This used to return the unvalidated `abs` while validating `real`, so the
61
+ * header's promise of a "containment check" did not extend to the value the
62
+ * caller then opened.
63
+ */
36
64
  export declare function safeResolveReal(input: string, ctx: Context): Promise<string>;
37
65
  export declare function truncateMiddle(s: string, max: number): string;
38
66
  export declare function isBinaryBuffer(buf: Buffer): boolean;
package/dist/audit.js CHANGED
@@ -78,11 +78,12 @@ function createOutputSpool(opts) {
78
78
  return {
79
79
  write(text) {
80
80
  if (finalized || !text) return;
81
- totalBytes += Buffer.byteLength(text, "utf8");
81
+ const textBytes = Buffer.byteLength(text, "utf8");
82
+ totalBytes += textBytes;
82
83
  if (!stream && !failed) {
83
- if (headBytes + text.length <= threshold) {
84
+ if (headBytes + textBytes <= threshold) {
84
85
  head += text;
85
- headBytes += text.length;
86
+ headBytes += textBytes;
86
87
  return;
87
88
  }
88
89
  head += text;
@@ -92,7 +93,7 @@ function createOutputSpool(opts) {
92
93
  }
93
94
  if (stream) {
94
95
  if (stream.writableLength > SPOOL_WRITE_HWM_BYTES) {
95
- droppedBytes += Buffer.byteLength(text, "utf8");
96
+ droppedBytes += textBytes;
96
97
  return;
97
98
  }
98
99
  stream.write(text);
@@ -114,6 +115,52 @@ function createOutputSpool(opts) {
114
115
  };
115
116
  }
116
117
 
118
+ // src/_win32-resolve.ts
119
+ import * as fs from "node:fs";
120
+ import * as path2 from "node:path";
121
+ function resolveWin32Command(cmd) {
122
+ if (process.platform !== "win32") return cmd;
123
+ if (cmd.includes("/") || cmd.includes("\\") || path2.extname(cmd.replace(/\//g, "\\"))) {
124
+ return cmd;
125
+ }
126
+ const pathext = (process.env["PATHEXT"] ?? ".COM;.EXE;.BAT;.CMD;.VBS;.JS;.WS;.MSC").toLowerCase().split(";");
127
+ const pathDirs = (process.env["PATH"] ?? "").split(path2.delimiter);
128
+ for (const dir of pathDirs) {
129
+ const base = path2.join(dir, cmd);
130
+ for (const ext of pathext) {
131
+ const full = `${base}${ext}`;
132
+ try {
133
+ fs.accessSync(full, fs.constants.X_OK);
134
+ return full;
135
+ } catch {
136
+ }
137
+ }
138
+ }
139
+ return cmd;
140
+ }
141
+ var WIN32_SHELL_META = /[&|<>"\r\n\0]/;
142
+ function assertSafeWin32ShellArgs(args) {
143
+ for (const arg of args) {
144
+ if (typeof arg === "string" && WIN32_SHELL_META.test(arg)) {
145
+ throw new Error(
146
+ 'win32 cmd shim spawn: argument contains a shell metacharacter (one of & | < > ", or a newline) that could enable command injection through the .cmd/.bat wrapper - refusing to run. Offending argument: ' + JSON.stringify(arg)
147
+ );
148
+ }
149
+ }
150
+ }
151
+ function buildWin32CmdShimInvocation(command, args = []) {
152
+ assertSafeWin32ShellArgs([command, ...args]);
153
+ const line = ["call", quoteWin32CmdArg(command), ...args.map(quoteWin32CmdArg)].join(" ");
154
+ return {
155
+ command: process.env["COMSPEC"] ?? "cmd.exe",
156
+ args: ["/d", "/c", line],
157
+ windowsVerbatimArguments: true
158
+ };
159
+ }
160
+ function quoteWin32CmdArg(arg) {
161
+ return `"${arg}"`;
162
+ }
163
+
117
164
  // src/process-registry.ts
118
165
  import { spawn } from "node:child_process";
119
166
  import * as os from "node:os";
@@ -445,6 +492,7 @@ var ProcessRegistryImpl = class {
445
492
  }
446
493
  /** Get all tracked processes. */
447
494
  list() {
495
+ this._pruneAllStale();
448
496
  return Array.from(this.processes.values());
449
497
  }
450
498
  /** Get processes filtered by name (e.g. 'bash', 'exec'). */
@@ -475,6 +523,7 @@ var ProcessRegistryImpl = class {
475
523
  * Combined stats for observability — used by /ps and the TUI status bar.
476
524
  */
477
525
  stats() {
526
+ this._pruneAllStale();
478
527
  return {
479
528
  activeCount: this.activeCount,
480
529
  backgroundCount: this.activeBackgroundCount,
@@ -714,6 +763,19 @@ var ProcessRegistryImpl = class {
714
763
  this.processes.delete(pid);
715
764
  }
716
765
  }
766
+ /**
767
+ * Remove every stale entry, not just one PID. `list()`/`stats()` — the
768
+ * surfaces the TUI status bar and `/ps` poll — must prune too: a child
769
+ * whose 'close' event never fires (e.g. Windows grandchildren holding stdio
770
+ * open) would otherwise linger in the registry until someone looks up its
771
+ * exact PID, and PID reuse meanwhile makes `get()`/`kill()` target the
772
+ * wrong process. RAM-leak audit 2026-07-31, MEDIUM.
773
+ */
774
+ _pruneAllStale() {
775
+ for (const [pid, entry] of this.processes) {
776
+ if (this._isStaleEntry(entry)) this.processes.delete(pid);
777
+ }
778
+ }
717
779
  };
718
780
  var _registry;
719
781
  function getProcessRegistry() {
@@ -723,60 +785,17 @@ function getProcessRegistry() {
723
785
  return _registry;
724
786
  }
725
787
 
726
- // src/_win32-resolve.ts
727
- import * as fs from "node:fs";
728
- import * as path2 from "node:path";
729
- function resolveWin32Command(cmd) {
730
- if (process.platform !== "win32") return cmd;
731
- if (cmd.includes("/") || cmd.includes("\\") || path2.extname(cmd.replace(/\//g, "\\"))) {
732
- return cmd;
733
- }
734
- const pathext = (process.env["PATHEXT"] ?? ".COM;.EXE;.BAT;.CMD;.VBS;.JS;.WS;.MSC").toLowerCase().split(";");
735
- const pathDirs = (process.env["PATH"] ?? "").split(path2.delimiter);
736
- for (const dir of pathDirs) {
737
- const base = path2.join(dir, cmd);
738
- for (const ext of pathext) {
739
- const full = `${base}${ext}`;
740
- try {
741
- fs.accessSync(full, fs.constants.X_OK);
742
- return full;
743
- } catch {
744
- }
745
- }
746
- }
747
- return cmd;
748
- }
749
- var WIN32_SHELL_META = /[&|<>"\r\n\0]/;
750
- function assertSafeWin32ShellArgs(args) {
751
- for (const arg of args) {
752
- if (typeof arg === "string" && WIN32_SHELL_META.test(arg)) {
753
- throw new Error(
754
- 'win32 cmd shim spawn: argument contains a shell metacharacter (one of & | < > ", or a newline) that could enable command injection through the .cmd/.bat wrapper - refusing to run. Offending argument: ' + JSON.stringify(arg)
755
- );
756
- }
757
- }
758
- }
759
- function buildWin32CmdShimInvocation(command, args = []) {
760
- assertSafeWin32ShellArgs([command, ...args]);
761
- const line = ["call", quoteWin32CmdArg(command), ...args.map(quoteWin32CmdArg)].join(" ");
762
- return {
763
- command: process.env["COMSPEC"] ?? "cmd.exe",
764
- args: ["/d", "/c", line],
765
- windowsVerbatimArguments: true
766
- };
767
- }
768
- function quoteWin32CmdArg(arg) {
769
- return `"${arg}"`;
770
- }
771
-
772
788
  // src/_spawn-stream.ts
773
789
  var isWin = process.platform === "win32";
774
790
  async function* spawnStream(opts) {
775
791
  const max = opts.maxBytes ?? 2e5;
776
792
  const flushAt = opts.flushBytes ?? 4 * 1024;
777
793
  const maxQueue = opts.maxQueueSize ?? 500;
794
+ const maxQueueBytes = opts.maxQueueBytes ?? 1024 * 1024;
778
795
  let stdout = "";
779
796
  let stderr = "";
797
+ let stdoutRetainedBytes = 0;
798
+ let stderrRetainedBytes = 0;
780
799
  let pending = "";
781
800
  let error;
782
801
  const spool = createOutputSpool({ tool: opts.cmd, thresholdBytes: max });
@@ -818,6 +837,7 @@ async function* spawnStream(opts) {
818
837
  });
819
838
  }
820
839
  const queue = [];
840
+ let queuedBytes = 0;
821
841
  let waiter;
822
842
  let paused = false;
823
843
  const wake = () => {
@@ -828,7 +848,7 @@ async function* spawnStream(opts) {
828
848
  }
829
849
  };
830
850
  const resume = () => {
831
- if (paused && queue.length < maxQueue) {
851
+ if (paused && queue.length <= Math.floor(maxQueue / 2) && queuedBytes <= Math.floor(maxQueueBytes / 2)) {
832
852
  paused = false;
833
853
  child.stdout?.resume();
834
854
  child.stderr?.resume();
@@ -838,11 +858,16 @@ async function* spawnStream(opts) {
838
858
  const s = c.toString();
839
859
  stdoutBytes += c.byteLength;
840
860
  emitProcessOutput({ pid, stream: "stdout", chunk: c });
841
- if (stdout.length < max) stdout += s;
861
+ if (stdoutRetainedBytes < max) {
862
+ const retained = utf8Prefix(s, max - stdoutRetainedBytes);
863
+ stdout += retained;
864
+ stdoutRetainedBytes += Buffer.byteLength(retained, "utf8");
865
+ }
842
866
  spool.write(s);
843
- queue.push({ kind: "out", data: s });
867
+ queue.push({ kind: "out", data: s, bytes: c.byteLength });
868
+ queuedBytes += c.byteLength;
844
869
  wake();
845
- if (!paused && queue.length >= maxQueue) {
870
+ if (!paused && (queue.length >= maxQueue || queuedBytes >= maxQueueBytes)) {
846
871
  paused = true;
847
872
  child.stdout?.pause();
848
873
  child.stderr?.pause();
@@ -852,11 +877,16 @@ async function* spawnStream(opts) {
852
877
  const s = c.toString();
853
878
  stderrBytes += c.byteLength;
854
879
  emitProcessOutput({ pid, stream: "stderr", chunk: c });
855
- if (stderr.length < max) stderr += s;
880
+ if (stderrRetainedBytes < max) {
881
+ const retained = utf8Prefix(s, max - stderrRetainedBytes);
882
+ stderr += retained;
883
+ stderrRetainedBytes += Buffer.byteLength(retained, "utf8");
884
+ }
856
885
  spool.write(s);
857
- queue.push({ kind: "err", data: s });
886
+ queue.push({ kind: "err", data: s, bytes: c.byteLength });
887
+ queuedBytes += c.byteLength;
858
888
  wake();
859
- if (!paused && queue.length >= maxQueue) {
889
+ if (!paused && (queue.length >= maxQueue || queuedBytes >= maxQueueBytes)) {
860
890
  paused = true;
861
891
  child.stdout?.pause();
862
892
  child.stderr?.pause();
@@ -866,7 +896,9 @@ async function* spawnStream(opts) {
866
896
  child.stderr?.on("data", onErr);
867
897
  child.on("error", (e) => {
868
898
  error = e.message;
869
- queue.push({ kind: "error", data: e.message });
899
+ const bytes = Buffer.byteLength(e.message, "utf8");
900
+ queue.push({ kind: "error", data: e.message, bytes });
901
+ queuedBytes += bytes;
870
902
  wake();
871
903
  });
872
904
  const completeTelemetry = (code, signal, timedOut = false) => {
@@ -887,7 +919,13 @@ async function* spawnStream(opts) {
887
919
  if (typeof pid === "number") registry.unregister(pid);
888
920
  const exitCode2 = code ?? (signal ? 1 : 0);
889
921
  completeTelemetry(exitCode2, signal ?? void 0);
890
- queue.push({ kind: "close", data: "", code: exitCode2, ...signal ? { signal } : {} });
922
+ queue.push({
923
+ kind: "close",
924
+ data: "",
925
+ bytes: 0,
926
+ code: exitCode2,
927
+ ...signal ? { signal } : {}
928
+ });
891
929
  wake();
892
930
  });
893
931
  const onAbort = () => {
@@ -899,7 +937,7 @@ async function* spawnStream(opts) {
899
937
  } catch {
900
938
  }
901
939
  }
902
- queue.push({ kind: "close", data: "", code: 124 });
940
+ queue.push({ kind: "close", data: "", bytes: 0, code: 124 });
903
941
  completeTelemetry(124, "SIGKILL", true);
904
942
  wake();
905
943
  };
@@ -917,6 +955,7 @@ async function* spawnStream(opts) {
917
955
  });
918
956
  }
919
957
  const chunk = queue.shift();
958
+ queuedBytes -= chunk.bytes;
920
959
  resume();
921
960
  if (chunk.kind === "close") {
922
961
  if (!spawnFailed) exitCode = chunk.code ?? 0;
@@ -943,7 +982,7 @@ async function* spawnStream(opts) {
943
982
  stdout: spooled ? stdout + spoolNote(spooled) : stdout,
944
983
  stderr,
945
984
  exitCode,
946
- truncated: stdout.length >= max || stderr.length >= max,
985
+ truncated: stdoutBytes > stdoutRetainedBytes || stderrBytes > stderrRetainedBytes,
947
986
  error,
948
987
  spoolPath: spooled?.path,
949
988
  spoolBytes: spooled?.bytes
@@ -967,6 +1006,20 @@ async function* spawnStream(opts) {
967
1006
  }
968
1007
  }
969
1008
  }
1009
+ function utf8Prefix(text, maxBytes) {
1010
+ if (maxBytes <= 0) return "";
1011
+ if (Buffer.byteLength(text, "utf8") <= maxBytes) return text;
1012
+ let low = 0;
1013
+ let high = text.length;
1014
+ while (low < high) {
1015
+ const mid = Math.ceil((low + high) / 2);
1016
+ if (Buffer.byteLength(text.slice(0, mid), "utf8") <= maxBytes) low = mid;
1017
+ else high = mid - 1;
1018
+ }
1019
+ let end = low;
1020
+ if (end > 0 && /[\uD800-\uDBFF]/.test(text[end - 1])) end--;
1021
+ return text.slice(0, end);
1022
+ }
970
1023
 
971
1024
  // src/_util.ts
972
1025
  import * as path3 from "node:path";
@@ -2548,8 +2601,8 @@ async function scanDirectory(directory, depth, profiles, limits, state, extraIgn
2548
2601
  collectFileEvidence(directory, fullPath, entry.name, profiles, state);
2549
2602
  }
2550
2603
  }
2551
- function collectFileEvidence(directory, fullPath, basename4, profiles, state) {
2552
- const lower = basename4.toLowerCase();
2604
+ function collectFileEvidence(directory, fullPath, basename5, profiles, state) {
2605
+ const lower = basename5.toLowerCase();
2553
2606
  const extension = path4.extname(lower);
2554
2607
  for (const profile of profiles) {
2555
2608
  const detector = profile.detectors.find(
@@ -2560,7 +2613,7 @@ function collectFileEvidence(directory, fullPath, basename4, profiles, state) {
2560
2613
  candidate.evidence.push({
2561
2614
  kind: detector.kind,
2562
2615
  path: fullPath,
2563
- value: basename4,
2616
+ value: basename5,
2564
2617
  weight: detector.weight
2565
2618
  });
2566
2619
  if (detector.kind === "manifest" || detector.kind === "config") {
@@ -3179,10 +3232,11 @@ var MAX_ARGUMENTS = 128;
3179
3232
  var MAX_ARGUMENT_LENGTH = 4096;
3180
3233
  var PACKAGE_NAME_RE = /^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+(?:@[a-z0-9*+._~^<>=|-]+)?$/i;
3181
3234
  var COMPOSER_PACKAGE_RE = /^[a-z0-9_.-]+\/[a-z0-9_.-]+(?::[a-z0-9*+._~^<>=|-]+)?$/i;
3182
- var GO_MODULE_RE = /^(?:[a-z0-9.-]+\.)+[a-z0-9.-]+\/[a-z0-9._~+/@-]+$/i;
3235
+ var GO_MODULE_RE = /^(?:[a-z0-9-]+\.)+[a-z0-9-]+\/[a-z0-9._~+/@-]+$/i;
3183
3236
  var PYTHON_PACKAGE_RE = /^[a-z0-9._-]+(?:\[[a-z0-9._,-]+\])?==[a-z0-9*+._!~-]+$/i;
3184
3237
  async function planLanguageOperation(options) {
3185
3238
  validateOperationOptions(options.operation, options.operationOptions?.packages);
3239
+ validateFilterOption(options.operationOptions?.filter);
3186
3240
  const detection = await detectLanguageWorkspaces(options);
3187
3241
  const candidates = detection.workspaces.filter(
3188
3242
  (workspace) => workspace.capabilities.includes(options.operation)
@@ -3355,6 +3409,15 @@ function workspaceDepth(workspaceRoot, projectRoot) {
3355
3409
  const relative5 = path6.relative(path6.resolve(projectRoot), path6.resolve(workspaceRoot));
3356
3410
  return relative5 === "" ? 0 : relative5.split(path6.sep).length;
3357
3411
  }
3412
+ function validateFilterOption(filter) {
3413
+ if (filter === void 0) return;
3414
+ if (filter.startsWith("-") || filter.includes(" --")) {
3415
+ throw new Error(`Invalid test filter (parsed as a runner option): "${filter}"`);
3416
+ }
3417
+ if (filter.length > MAX_ARGUMENT_LENGTH || /[\r\n\0]/.test(filter)) {
3418
+ throw new Error("Invalid test filter (oversized or contains a control character)");
3419
+ }
3420
+ }
3358
3421
  function validateOperationOptions(operation, packages) {
3359
3422
  if (!operation.startsWith("package-") || !packages) return;
3360
3423
  for (const value of packages) {
package/dist/bash.js CHANGED
@@ -81,11 +81,12 @@ function createOutputSpool(opts) {
81
81
  return {
82
82
  write(text) {
83
83
  if (finalized || !text) return;
84
- totalBytes += Buffer.byteLength(text, "utf8");
84
+ const textBytes = Buffer.byteLength(text, "utf8");
85
+ totalBytes += textBytes;
85
86
  if (!stream && !failed) {
86
- if (headBytes + text.length <= threshold) {
87
+ if (headBytes + textBytes <= threshold) {
87
88
  head += text;
88
- headBytes += text.length;
89
+ headBytes += textBytes;
89
90
  return;
90
91
  }
91
92
  head += text;
@@ -95,7 +96,7 @@ function createOutputSpool(opts) {
95
96
  }
96
97
  if (stream) {
97
98
  if (stream.writableLength > SPOOL_WRITE_HWM_BYTES) {
98
- droppedBytes += Buffer.byteLength(text, "utf8");
99
+ droppedBytes += textBytes;
99
100
  return;
100
101
  }
101
102
  stream.write(text);
@@ -521,6 +522,7 @@ var ProcessRegistryImpl = class {
521
522
  }
522
523
  /** Get all tracked processes. */
523
524
  list() {
525
+ this._pruneAllStale();
524
526
  return Array.from(this.processes.values());
525
527
  }
526
528
  /** Get processes filtered by name (e.g. 'bash', 'exec'). */
@@ -551,6 +553,7 @@ var ProcessRegistryImpl = class {
551
553
  * Combined stats for observability — used by /ps and the TUI status bar.
552
554
  */
553
555
  stats() {
556
+ this._pruneAllStale();
554
557
  return {
555
558
  activeCount: this.activeCount,
556
559
  backgroundCount: this.activeBackgroundCount,
@@ -790,6 +793,19 @@ var ProcessRegistryImpl = class {
790
793
  this.processes.delete(pid);
791
794
  }
792
795
  }
796
+ /**
797
+ * Remove every stale entry, not just one PID. `list()`/`stats()` — the
798
+ * surfaces the TUI status bar and `/ps` poll — must prune too: a child
799
+ * whose 'close' event never fires (e.g. Windows grandchildren holding stdio
800
+ * open) would otherwise linger in the registry until someone looks up its
801
+ * exact PID, and PID reuse meanwhile makes `get()`/`kill()` target the
802
+ * wrong process. RAM-leak audit 2026-07-31, MEDIUM.
803
+ */
804
+ _pruneAllStale() {
805
+ for (const [pid, entry] of this.processes) {
806
+ if (this._isStaleEntry(entry)) this.processes.delete(pid);
807
+ }
808
+ }
793
809
  };
794
810
  var _registry;
795
811
  function getProcessRegistry() {
@@ -2,30 +2,59 @@
2
2
  import {
3
3
  GOVERNED_TOOL_EXECUTOR_META_KEY
4
4
  } from "@wrongstack/core/types";
5
+ var BATCHABLE_TOOL_NAMES = [
6
+ "read",
7
+ "glob",
8
+ "grep",
9
+ "tree",
10
+ "codebase-stats",
11
+ "codebase-search",
12
+ "language_info",
13
+ "tool_search",
14
+ "memory_for_file",
15
+ "memory_for_path",
16
+ "memory_search"
17
+ ];
18
+ var BATCHABLE_TOOLS = new Set(BATCHABLE_TOOL_NAMES);
19
+ var MAX_BATCH_CALLS = 8;
20
+ var MAX_PARALLEL_BATCH_CALLS = 3;
5
21
  var batchToolUseTool = {
6
22
  name: "batch_tool_use",
7
23
  category: "Meta",
8
- description: "Execute a batch of tool calls either sequentially or in parallel. Returns structured results for every call.",
9
- usageHint: "ADVANCED / POWER USER TOOL:\n\n- Useful when you have a clear list of independent operations to perform.\n- `parallel: true` (default) runs them concurrently for speed.\n- `stop_on_error: true` makes it fail fast on the first error.\nUse with care \u2014 batching many mutating operations can be risky. Prefer explicit sequential steps for important work.",
24
+ description: "Execute a small batch of independent, read-only discovery calls. Process, mutation, browser-action, MCP, delegation, and nested meta tools are not eligible.",
25
+ usageHint: `BOUNDED READ-ONLY BATCHING:
26
+
27
+ - Use only for a small set of independent reads or searches that are all needed now.
28
+ - Do not wrap normal sequential reasoning or every tool call in a batch.
29
+ - \`parallel: true\` (default) runs at most three eligible calls concurrently.
30
+ - \`stop_on_error: true\` makes it fail fast on the first error.
31
+ - Eligible tools: ${BATCHABLE_TOOL_NAMES.join(", ")}.`,
10
32
  permission: "confirm",
33
+ // Scheduling barrier: keep the outer meta-call out of ToolExecutor's
34
+ // non-mutating fan-out even though every eligible inner tool is read-only.
11
35
  mutating: true,
12
36
  timeoutMs: 12e4,
13
- capabilities: ["tool.mutate.any"],
37
+ capabilities: ["tool.meta"],
14
38
  icon: "meta",
15
39
  inputSchema: {
16
40
  type: "object",
17
41
  properties: {
18
42
  calls: {
19
43
  type: "array",
44
+ maxItems: MAX_BATCH_CALLS,
20
45
  items: {
21
46
  type: "object",
22
47
  properties: {
23
- tool: { type: "string" },
48
+ tool: {
49
+ type: "string",
50
+ enum: [...BATCHABLE_TOOL_NAMES],
51
+ description: "An explicitly batch-safe, read-only discovery tool."
52
+ },
24
53
  input: { type: "object" }
25
54
  },
26
55
  required: ["tool"]
27
56
  },
28
- description: "Array of tool calls to execute"
57
+ description: `Up to ${MAX_BATCH_CALLS} independent, batch-safe read calls.`
29
58
  },
30
59
  stop_on_error: {
31
60
  type: "boolean",
@@ -48,6 +77,9 @@ var batchToolUseTool = {
48
77
  stop_on_error: false
49
78
  };
50
79
  }
80
+ if (input.calls.length > MAX_BATCH_CALLS) {
81
+ throw new Error(`batch_tool_use: calls must contain at most ${MAX_BATCH_CALLS} items`);
82
+ }
51
83
  const governedExecute = ctx.meta[GOVERNED_TOOL_EXECUTOR_META_KEY];
52
84
  if (typeof governedExecute !== "function") {
53
85
  return {
@@ -67,8 +99,11 @@ var batchToolUseTool = {
67
99
  let succeeded = 0;
68
100
  let failed = 0;
69
101
  if (input.parallel !== false) {
70
- const promises = input.calls.map(async (call) => executeSingle(call, ctx, governedExecute));
71
- const allResults = await Promise.all(promises);
102
+ const allResults = await mapWithConcurrency(
103
+ input.calls,
104
+ MAX_PARALLEL_BATCH_CALLS,
105
+ (call) => executeSingle(call, ctx, governedExecute)
106
+ );
72
107
  results.push(...allResults);
73
108
  succeeded = allResults.filter((r) => r.success).length;
74
109
  failed = allResults.filter((r) => !r.success).length;
@@ -93,8 +128,28 @@ var batchToolUseTool = {
93
128
  };
94
129
  }
95
130
  };
131
+ async function mapWithConcurrency(items, limit, run) {
132
+ const results = new Array(items.length);
133
+ let nextIndex = 0;
134
+ const worker = async () => {
135
+ while (nextIndex < items.length) {
136
+ const index = nextIndex++;
137
+ results[index] = await run(items[index]);
138
+ }
139
+ };
140
+ await Promise.all(Array.from({ length: Math.min(limit, items.length) }, () => worker()));
141
+ return results;
142
+ }
96
143
  async function executeSingle(call, ctx, governedExecute) {
97
144
  const start = Date.now();
145
+ if (call.tool === batchToolUseTool.name) {
146
+ return {
147
+ tool: call.tool,
148
+ success: false,
149
+ error: "recursive batch_tool_use execution is not allowed",
150
+ executionMs: Date.now() - start
151
+ };
152
+ }
98
153
  const tool = ctx.tools.find((candidate) => candidate.name === call.tool);
99
154
  if (!tool) {
100
155
  return {
@@ -104,6 +159,14 @@ async function executeSingle(call, ctx, governedExecute) {
104
159
  executionMs: Date.now() - start
105
160
  };
106
161
  }
162
+ if (!BATCHABLE_TOOLS.has(tool.name) || tool.mutating) {
163
+ return {
164
+ tool: call.tool,
165
+ success: false,
166
+ error: `tool "${call.tool}" is not eligible for batch execution; call it directly. Allowed: ${BATCHABLE_TOOL_NAMES.join(", ")}`,
167
+ executionMs: Date.now() - start
168
+ };
169
+ }
107
170
  try {
108
171
  const result = await governedExecute(call.tool, call.input);
109
172
  return {