@wrongstack/tools 0.297.0 → 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 (253) hide show
  1. package/dist/_spawn-stream.d.ts +2 -0
  2. package/dist/_util.d.ts +29 -1
  3. package/dist/audit.js +113 -65
  4. package/dist/bash.js +5 -4
  5. package/dist/batch-tool-use.js +70 -7
  6. package/dist/browser/index.js +31 -1
  7. package/dist/builtin.js +841 -351
  8. package/dist/codebase-index/index.js +68 -16
  9. package/dist/codebase-index/project-server-protocol.d.ts +26 -0
  10. package/dist/codebase-index/project-server.js +60 -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 +560 -529
  20. package/dist/format.js +116 -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 +792 -290
  25. package/dist/install.js +120 -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 +128 -71
  30. package/dist/lint.js +113 -65
  31. package/dist/outdated.js +86 -26
  32. package/dist/pack.js +841 -351
  33. package/dist/patch.js +11 -1
  34. package/dist/plan.js +32 -10
  35. package/dist/read.js +77 -23
  36. package/dist/replace.js +11 -3
  37. package/dist/scaffold.js +9 -6
  38. package/dist/session-kanban.js +21 -5
  39. package/dist/task.js +3 -1
  40. package/dist/test.js +113 -65
  41. package/dist/todo.js +3 -1
  42. package/dist/tool-tier.js +841 -351
  43. package/dist/tool-use.d.ts +1 -1
  44. package/dist/tool-use.js +28 -4
  45. package/dist/tree.d.ts +1 -0
  46. package/dist/tree.js +44 -8
  47. package/dist/typecheck.js +113 -65
  48. package/dist/write.js +9 -6
  49. package/package.json +5 -4
  50. package/dist/_concurrency.d.ts.map +0 -1
  51. package/dist/_danger-detect.d.ts.map +0 -1
  52. package/dist/_edit-match.d.ts.map +0 -1
  53. package/dist/_env.d.ts.map +0 -1
  54. package/dist/_fetch-guard.d.ts.map +0 -1
  55. package/dist/_output-spool.d.ts.map +0 -1
  56. package/dist/_redact-command.d.ts.map +0 -1
  57. package/dist/_regex.d.ts.map +0 -1
  58. package/dist/_session-shell.d.ts.map +0 -1
  59. package/dist/_shell-pick.d.ts.map +0 -1
  60. package/dist/_spawn-stream.d.ts.map +0 -1
  61. package/dist/_syntax-check.d.ts.map +0 -1
  62. package/dist/_util.d.ts.map +0 -1
  63. package/dist/_win32-resolve.d.ts.map +0 -1
  64. package/dist/audit.d.ts.map +0 -1
  65. package/dist/audit.js.map +0 -7
  66. package/dist/auto-proceed-loop-guard.d.ts.map +0 -1
  67. package/dist/auto-proceed-loop-guard.js.map +0 -7
  68. package/dist/bash-kill-guard.d.ts.map +0 -1
  69. package/dist/bash.d.ts.map +0 -1
  70. package/dist/bash.js.map +0 -7
  71. package/dist/batch-tool-use.d.ts.map +0 -1
  72. package/dist/batch-tool-use.js.map +0 -7
  73. package/dist/browser/artifacts.d.ts.map +0 -1
  74. package/dist/browser/index.d.ts.map +0 -1
  75. package/dist/browser/index.js.map +0 -7
  76. package/dist/browser/manager.d.ts.map +0 -1
  77. package/dist/browser/network-guard-proxy.d.ts.map +0 -1
  78. package/dist/browser/security.d.ts.map +0 -1
  79. package/dist/browser/tools.d.ts.map +0 -1
  80. package/dist/browser/types.d.ts.map +0 -1
  81. package/dist/builtin.d.ts.map +0 -1
  82. package/dist/builtin.js.map +0 -7
  83. package/dist/circuit-breaker.d.ts.map +0 -1
  84. package/dist/circuit-breaker.js.map +0 -7
  85. package/dist/codebase-index/background-indexer.d.ts.map +0 -1
  86. package/dist/codebase-index/bm25.d.ts.map +0 -1
  87. package/dist/codebase-index/circuit-breaker.d.ts.map +0 -1
  88. package/dist/codebase-index/codebase-index-tool.d.ts.map +0 -1
  89. package/dist/codebase-index/codebase-search-tool.d.ts.map +0 -1
  90. package/dist/codebase-index/codebase-stats-tool.d.ts.map +0 -1
  91. package/dist/codebase-index/dead-code-scan.d.ts.map +0 -1
  92. package/dist/codebase-index/generic-parser.d.ts.map +0 -1
  93. package/dist/codebase-index/gitignore.d.ts.map +0 -1
  94. package/dist/codebase-index/go-parser.d.ts.map +0 -1
  95. package/dist/codebase-index/index-service.d.ts.map +0 -1
  96. package/dist/codebase-index/index.d.ts.map +0 -1
  97. package/dist/codebase-index/index.js.map +0 -7
  98. package/dist/codebase-index/indexer.d.ts.map +0 -1
  99. package/dist/codebase-index/json-parser.d.ts.map +0 -1
  100. package/dist/codebase-index/languages.d.ts.map +0 -1
  101. package/dist/codebase-index/lsp-kind.d.ts.map +0 -1
  102. package/dist/codebase-index/parser-dispatch.d.ts.map +0 -1
  103. package/dist/codebase-index/project-server-cache.d.ts.map +0 -1
  104. package/dist/codebase-index/project-server-client.d.ts.map +0 -1
  105. package/dist/codebase-index/project-server-endpoint.d.ts.map +0 -1
  106. package/dist/codebase-index/project-server-protocol.d.ts.map +0 -1
  107. package/dist/codebase-index/project-server.d.ts.map +0 -1
  108. package/dist/codebase-index/project-server.js.map +0 -7
  109. package/dist/codebase-index/py-parser.d.ts.map +0 -1
  110. package/dist/codebase-index/refs-extractor.d.ts.map +0 -1
  111. package/dist/codebase-index/rs-parser.d.ts.map +0 -1
  112. package/dist/codebase-index/schema.d.ts.map +0 -1
  113. package/dist/codebase-index/spawn-gate.d.ts.map +0 -1
  114. package/dist/codebase-index/sqlite-runtime.d.ts.map +0 -1
  115. package/dist/codebase-index/ts-parser.d.ts.map +0 -1
  116. package/dist/codebase-index/worker-protocol.d.ts.map +0 -1
  117. package/dist/codebase-index/worker.d.ts.map +0 -1
  118. package/dist/codebase-index/worker.js.map +0 -7
  119. package/dist/codebase-index/writer-admin.d.ts.map +0 -1
  120. package/dist/codebase-index/writer-bulk-insert.d.ts.map +0 -1
  121. package/dist/codebase-index/writer-graph-helpers.d.ts.map +0 -1
  122. package/dist/codebase-index/writer-graph-reader.d.ts.map +0 -1
  123. package/dist/codebase-index/writer-helpers.d.ts.map +0 -1
  124. package/dist/codebase-index/writer-pragmas.d.ts.map +0 -1
  125. package/dist/codebase-index/writer-ref-mapper.d.ts.map +0 -1
  126. package/dist/codebase-index/writer-schema.d.ts.map +0 -1
  127. package/dist/codebase-index/writer-search-helpers.d.ts.map +0 -1
  128. package/dist/codebase-index/writer-store-pool.d.ts.map +0 -1
  129. package/dist/codebase-index/writer.d.ts.map +0 -1
  130. package/dist/codebase-index/yaml-parser.d.ts.map +0 -1
  131. package/dist/design.d.ts.map +0 -1
  132. package/dist/design.js.map +0 -7
  133. package/dist/diff.d.ts.map +0 -1
  134. package/dist/diff.js.map +0 -7
  135. package/dist/document.d.ts.map +0 -1
  136. package/dist/document.js.map +0 -7
  137. package/dist/e2e.d.ts.map +0 -1
  138. package/dist/e2e.js.map +0 -7
  139. package/dist/edit.d.ts.map +0 -1
  140. package/dist/edit.js.map +0 -7
  141. package/dist/exec-kill-guard.d.ts.map +0 -1
  142. package/dist/exec.d.ts.map +0 -1
  143. package/dist/exec.js.map +0 -7
  144. package/dist/fetch.d.ts.map +0 -1
  145. package/dist/fetch.js.map +0 -7
  146. package/dist/format.d.ts.map +0 -1
  147. package/dist/format.js.map +0 -7
  148. package/dist/git.d.ts.map +0 -1
  149. package/dist/git.js.map +0 -7
  150. package/dist/glob.d.ts.map +0 -1
  151. package/dist/glob.js.map +0 -7
  152. package/dist/grep.d.ts.map +0 -1
  153. package/dist/grep.js.map +0 -7
  154. package/dist/index.d.ts.map +0 -1
  155. package/dist/index.js.map +0 -7
  156. package/dist/install.d.ts.map +0 -1
  157. package/dist/install.js.map +0 -7
  158. package/dist/json.d.ts.map +0 -1
  159. package/dist/json.js.map +0 -7
  160. package/dist/kanban-board-inputs.d.ts.map +0 -1
  161. package/dist/kanban-decomposition-actions.d.ts.map +0 -1
  162. package/dist/kanban-detail-actions.d.ts.map +0 -1
  163. package/dist/kanban-evidence-bridge.d.ts.map +0 -1
  164. package/dist/kanban-presence.d.ts.map +0 -1
  165. package/dist/kanban-split-task-handler.d.ts.map +0 -1
  166. package/dist/kanban-task-inputs.d.ts.map +0 -1
  167. package/dist/kanban-tool-results.d.ts.map +0 -1
  168. package/dist/kanban-tool-schema.d.ts.map +0 -1
  169. package/dist/kanban-tool-types.d.ts.map +0 -1
  170. package/dist/kanban.d.ts.map +0 -1
  171. package/dist/kanban.js.map +0 -7
  172. package/dist/languages/detect.d.ts.map +0 -1
  173. package/dist/languages/diagnostics.d.ts.map +0 -1
  174. package/dist/languages/execute-tool.d.ts.map +0 -1
  175. package/dist/languages/execute.d.ts.map +0 -1
  176. package/dist/languages/index.d.ts.map +0 -1
  177. package/dist/languages/index.js.map +0 -7
  178. package/dist/languages/legacy-bridge.d.ts.map +0 -1
  179. package/dist/languages/package-tool.d.ts.map +0 -1
  180. package/dist/languages/plan.d.ts.map +0 -1
  181. package/dist/languages/profile-helpers.d.ts.map +0 -1
  182. package/dist/languages/profiles/additional.d.ts.map +0 -1
  183. package/dist/languages/profiles/primary.d.ts.map +0 -1
  184. package/dist/languages/registry.d.ts.map +0 -1
  185. package/dist/languages/tool.d.ts.map +0 -1
  186. package/dist/languages/types.d.ts.map +0 -1
  187. package/dist/lint.d.ts.map +0 -1
  188. package/dist/lint.js.map +0 -7
  189. package/dist/logs.d.ts.map +0 -1
  190. package/dist/logs.js.map +0 -7
  191. package/dist/memory.d.ts.map +0 -1
  192. package/dist/memory.js.map +0 -7
  193. package/dist/mode.d.ts.map +0 -1
  194. package/dist/mode.js.map +0 -7
  195. package/dist/next-steps.d.ts.map +0 -1
  196. package/dist/next-steps.js.map +0 -7
  197. package/dist/outdated.d.ts.map +0 -1
  198. package/dist/outdated.js.map +0 -7
  199. package/dist/pack.d.ts.map +0 -1
  200. package/dist/pack.js.map +0 -7
  201. package/dist/patch.d.ts.map +0 -1
  202. package/dist/patch.js.map +0 -7
  203. package/dist/plan.d.ts.map +0 -1
  204. package/dist/plan.js.map +0 -7
  205. package/dist/process-guardian.d.ts.map +0 -1
  206. package/dist/process-registry-persistent.d.ts.map +0 -1
  207. package/dist/process-registry.d.ts.map +0 -1
  208. package/dist/process-registry.js.map +0 -7
  209. package/dist/ps-slash.d.ts.map +0 -1
  210. package/dist/ps-slash.js.map +0 -7
  211. package/dist/read.d.ts.map +0 -1
  212. package/dist/read.js.map +0 -7
  213. package/dist/replace.d.ts.map +0 -1
  214. package/dist/replace.js.map +0 -7
  215. package/dist/scaffold.d.ts.map +0 -1
  216. package/dist/scaffold.js.map +0 -7
  217. package/dist/search.d.ts.map +0 -1
  218. package/dist/search.js.map +0 -7
  219. package/dist/session-kanban.d.ts.map +0 -1
  220. package/dist/session-kanban.js.map +0 -7
  221. package/dist/set-working-dir.d.ts.map +0 -1
  222. package/dist/skill.d.ts.map +0 -1
  223. package/dist/skill.js.map +0 -7
  224. package/dist/spawn-background.d.ts.map +0 -1
  225. package/dist/task.d.ts.map +0 -1
  226. package/dist/task.js.map +0 -7
  227. package/dist/test.d.ts.map +0 -1
  228. package/dist/test.js.map +0 -7
  229. package/dist/todo.d.ts.map +0 -1
  230. package/dist/todo.js.map +0 -7
  231. package/dist/tool-diff.d.ts.map +0 -1
  232. package/dist/tool-diff.js.map +0 -7
  233. package/dist/tool-help.d.ts.map +0 -1
  234. package/dist/tool-help.js.map +0 -7
  235. package/dist/tool-icon-map.d.ts.map +0 -1
  236. package/dist/tool-icons.d.ts.map +0 -1
  237. package/dist/tool-icons.js.map +0 -7
  238. package/dist/tool-search.d.ts.map +0 -1
  239. package/dist/tool-search.js.map +0 -7
  240. package/dist/tool-summary.d.ts.map +0 -1
  241. package/dist/tool-summary.js.map +0 -7
  242. package/dist/tool-tier.d.ts.map +0 -1
  243. package/dist/tool-tier.js.map +0 -7
  244. package/dist/tool-use.d.ts.map +0 -1
  245. package/dist/tool-use.js.map +0 -7
  246. package/dist/tree.d.ts.map +0 -1
  247. package/dist/tree.js.map +0 -7
  248. package/dist/typecheck.d.ts.map +0 -1
  249. package/dist/typecheck.js.map +0 -7
  250. package/dist/win32.d.ts.map +0 -1
  251. package/dist/win32.js.map +0 -7
  252. package/dist/write.d.ts.map +0 -1
  253. 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";
@@ -738,60 +785,17 @@ function getProcessRegistry() {
738
785
  return _registry;
739
786
  }
740
787
 
741
- // src/_win32-resolve.ts
742
- import * as fs from "node:fs";
743
- import * as path2 from "node:path";
744
- function resolveWin32Command(cmd) {
745
- if (process.platform !== "win32") return cmd;
746
- if (cmd.includes("/") || cmd.includes("\\") || path2.extname(cmd.replace(/\//g, "\\"))) {
747
- return cmd;
748
- }
749
- const pathext = (process.env["PATHEXT"] ?? ".COM;.EXE;.BAT;.CMD;.VBS;.JS;.WS;.MSC").toLowerCase().split(";");
750
- const pathDirs = (process.env["PATH"] ?? "").split(path2.delimiter);
751
- for (const dir of pathDirs) {
752
- const base = path2.join(dir, cmd);
753
- for (const ext of pathext) {
754
- const full = `${base}${ext}`;
755
- try {
756
- fs.accessSync(full, fs.constants.X_OK);
757
- return full;
758
- } catch {
759
- }
760
- }
761
- }
762
- return cmd;
763
- }
764
- var WIN32_SHELL_META = /[&|<>"\r\n\0]/;
765
- function assertSafeWin32ShellArgs(args) {
766
- for (const arg of args) {
767
- if (typeof arg === "string" && WIN32_SHELL_META.test(arg)) {
768
- throw new Error(
769
- '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)
770
- );
771
- }
772
- }
773
- }
774
- function buildWin32CmdShimInvocation(command, args = []) {
775
- assertSafeWin32ShellArgs([command, ...args]);
776
- const line = ["call", quoteWin32CmdArg(command), ...args.map(quoteWin32CmdArg)].join(" ");
777
- return {
778
- command: process.env["COMSPEC"] ?? "cmd.exe",
779
- args: ["/d", "/c", line],
780
- windowsVerbatimArguments: true
781
- };
782
- }
783
- function quoteWin32CmdArg(arg) {
784
- return `"${arg}"`;
785
- }
786
-
787
788
  // src/_spawn-stream.ts
788
789
  var isWin = process.platform === "win32";
789
790
  async function* spawnStream(opts) {
790
791
  const max = opts.maxBytes ?? 2e5;
791
792
  const flushAt = opts.flushBytes ?? 4 * 1024;
792
793
  const maxQueue = opts.maxQueueSize ?? 500;
794
+ const maxQueueBytes = opts.maxQueueBytes ?? 1024 * 1024;
793
795
  let stdout = "";
794
796
  let stderr = "";
797
+ let stdoutRetainedBytes = 0;
798
+ let stderrRetainedBytes = 0;
795
799
  let pending = "";
796
800
  let error;
797
801
  const spool = createOutputSpool({ tool: opts.cmd, thresholdBytes: max });
@@ -833,6 +837,7 @@ async function* spawnStream(opts) {
833
837
  });
834
838
  }
835
839
  const queue = [];
840
+ let queuedBytes = 0;
836
841
  let waiter;
837
842
  let paused = false;
838
843
  const wake = () => {
@@ -843,7 +848,7 @@ async function* spawnStream(opts) {
843
848
  }
844
849
  };
845
850
  const resume = () => {
846
- if (paused && queue.length < maxQueue) {
851
+ if (paused && queue.length <= Math.floor(maxQueue / 2) && queuedBytes <= Math.floor(maxQueueBytes / 2)) {
847
852
  paused = false;
848
853
  child.stdout?.resume();
849
854
  child.stderr?.resume();
@@ -853,11 +858,16 @@ async function* spawnStream(opts) {
853
858
  const s = c.toString();
854
859
  stdoutBytes += c.byteLength;
855
860
  emitProcessOutput({ pid, stream: "stdout", chunk: c });
856
- 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
+ }
857
866
  spool.write(s);
858
- queue.push({ kind: "out", data: s });
867
+ queue.push({ kind: "out", data: s, bytes: c.byteLength });
868
+ queuedBytes += c.byteLength;
859
869
  wake();
860
- if (!paused && queue.length >= maxQueue) {
870
+ if (!paused && (queue.length >= maxQueue || queuedBytes >= maxQueueBytes)) {
861
871
  paused = true;
862
872
  child.stdout?.pause();
863
873
  child.stderr?.pause();
@@ -867,11 +877,16 @@ async function* spawnStream(opts) {
867
877
  const s = c.toString();
868
878
  stderrBytes += c.byteLength;
869
879
  emitProcessOutput({ pid, stream: "stderr", chunk: c });
870
- 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
+ }
871
885
  spool.write(s);
872
- queue.push({ kind: "err", data: s });
886
+ queue.push({ kind: "err", data: s, bytes: c.byteLength });
887
+ queuedBytes += c.byteLength;
873
888
  wake();
874
- if (!paused && queue.length >= maxQueue) {
889
+ if (!paused && (queue.length >= maxQueue || queuedBytes >= maxQueueBytes)) {
875
890
  paused = true;
876
891
  child.stdout?.pause();
877
892
  child.stderr?.pause();
@@ -881,7 +896,9 @@ async function* spawnStream(opts) {
881
896
  child.stderr?.on("data", onErr);
882
897
  child.on("error", (e) => {
883
898
  error = e.message;
884
- 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;
885
902
  wake();
886
903
  });
887
904
  const completeTelemetry = (code, signal, timedOut = false) => {
@@ -902,7 +919,13 @@ async function* spawnStream(opts) {
902
919
  if (typeof pid === "number") registry.unregister(pid);
903
920
  const exitCode2 = code ?? (signal ? 1 : 0);
904
921
  completeTelemetry(exitCode2, signal ?? void 0);
905
- 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
+ });
906
929
  wake();
907
930
  });
908
931
  const onAbort = () => {
@@ -914,7 +937,7 @@ async function* spawnStream(opts) {
914
937
  } catch {
915
938
  }
916
939
  }
917
- queue.push({ kind: "close", data: "", code: 124 });
940
+ queue.push({ kind: "close", data: "", bytes: 0, code: 124 });
918
941
  completeTelemetry(124, "SIGKILL", true);
919
942
  wake();
920
943
  };
@@ -932,6 +955,7 @@ async function* spawnStream(opts) {
932
955
  });
933
956
  }
934
957
  const chunk = queue.shift();
958
+ queuedBytes -= chunk.bytes;
935
959
  resume();
936
960
  if (chunk.kind === "close") {
937
961
  if (!spawnFailed) exitCode = chunk.code ?? 0;
@@ -958,7 +982,7 @@ async function* spawnStream(opts) {
958
982
  stdout: spooled ? stdout + spoolNote(spooled) : stdout,
959
983
  stderr,
960
984
  exitCode,
961
- truncated: stdout.length >= max || stderr.length >= max,
985
+ truncated: stdoutBytes > stdoutRetainedBytes || stderrBytes > stderrRetainedBytes,
962
986
  error,
963
987
  spoolPath: spooled?.path,
964
988
  spoolBytes: spooled?.bytes
@@ -982,6 +1006,20 @@ async function* spawnStream(opts) {
982
1006
  }
983
1007
  }
984
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
+ }
985
1023
 
986
1024
  // src/_util.ts
987
1025
  import * as path3 from "node:path";
@@ -2563,8 +2601,8 @@ async function scanDirectory(directory, depth, profiles, limits, state, extraIgn
2563
2601
  collectFileEvidence(directory, fullPath, entry.name, profiles, state);
2564
2602
  }
2565
2603
  }
2566
- function collectFileEvidence(directory, fullPath, basename4, profiles, state) {
2567
- const lower = basename4.toLowerCase();
2604
+ function collectFileEvidence(directory, fullPath, basename5, profiles, state) {
2605
+ const lower = basename5.toLowerCase();
2568
2606
  const extension = path4.extname(lower);
2569
2607
  for (const profile of profiles) {
2570
2608
  const detector = profile.detectors.find(
@@ -2575,7 +2613,7 @@ function collectFileEvidence(directory, fullPath, basename4, profiles, state) {
2575
2613
  candidate.evidence.push({
2576
2614
  kind: detector.kind,
2577
2615
  path: fullPath,
2578
- value: basename4,
2616
+ value: basename5,
2579
2617
  weight: detector.weight
2580
2618
  });
2581
2619
  if (detector.kind === "manifest" || detector.kind === "config") {
@@ -3194,10 +3232,11 @@ var MAX_ARGUMENTS = 128;
3194
3232
  var MAX_ARGUMENT_LENGTH = 4096;
3195
3233
  var PACKAGE_NAME_RE = /^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+(?:@[a-z0-9*+._~^<>=|-]+)?$/i;
3196
3234
  var COMPOSER_PACKAGE_RE = /^[a-z0-9_.-]+\/[a-z0-9_.-]+(?::[a-z0-9*+._~^<>=|-]+)?$/i;
3197
- 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;
3198
3236
  var PYTHON_PACKAGE_RE = /^[a-z0-9._-]+(?:\[[a-z0-9._,-]+\])?==[a-z0-9*+._!~-]+$/i;
3199
3237
  async function planLanguageOperation(options) {
3200
3238
  validateOperationOptions(options.operation, options.operationOptions?.packages);
3239
+ validateFilterOption(options.operationOptions?.filter);
3201
3240
  const detection = await detectLanguageWorkspaces(options);
3202
3241
  const candidates = detection.workspaces.filter(
3203
3242
  (workspace) => workspace.capabilities.includes(options.operation)
@@ -3370,6 +3409,15 @@ function workspaceDepth(workspaceRoot, projectRoot) {
3370
3409
  const relative5 = path6.relative(path6.resolve(projectRoot), path6.resolve(workspaceRoot));
3371
3410
  return relative5 === "" ? 0 : relative5.split(path6.sep).length;
3372
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
+ }
3373
3421
  function validateOperationOptions(operation, packages) {
3374
3422
  if (!operation.startsWith("package-") || !packages) return;
3375
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);
@@ -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 {
@@ -6,8 +6,14 @@ import * as path from "node:path";
6
6
  import { atomicWrite, ulid } from "@wrongstack/core/utils";
7
7
  var ARTIFACT_RETENTION_MS = 7 * 24 * 60 * 60 * 1e3;
8
8
  var sweptRoots = /* @__PURE__ */ new Set();
9
+ var MAX_SWEPT_ROOTS = 128;
9
10
  function sweepOldArtifacts(root) {
10
11
  if (sweptRoots.has(root)) return;
12
+ while (sweptRoots.size >= MAX_SWEPT_ROOTS) {
13
+ const oldest = sweptRoots.values().next().value;
14
+ if (oldest === void 0) break;
15
+ sweptRoots.delete(oldest);
16
+ }
11
17
  sweptRoots.add(root);
12
18
  void (async () => {
13
19
  const cutoff = Date.now() - ARTIFACT_RETENTION_MS;
@@ -441,7 +447,7 @@ var BrowserSessionManager = class {
441
447
  constructor(options, launcher = defaultLauncher) {
442
448
  this.launcher = launcher;
443
449
  this.artifacts = new BrowserArtifactStore(options.artifactRoot);
444
- this.allowPrivateHosts = options.allowPrivateHosts ?? true;
450
+ this.allowPrivateHosts = options.allowPrivateHosts ?? false;
445
451
  this.allowedPrivateOrigins = options.allowedPrivateOrigins ?? [];
446
452
  this.networkProxy = new BrowserNetworkGuardProxy({
447
453
  allowPrivateHosts: this.allowPrivateHosts,
@@ -1017,6 +1023,9 @@ var browserScreenshotTool = {
1017
1023
  description: "Capture a page or element PNG and return sensitive artifact metadata with integrity hash.",
1018
1024
  usageHint: "browser_screenshot({ sessionId, fullPage?, selector? })",
1019
1025
  permission: "confirm",
1026
+ // WS-046: the element/expression acted on, not the sessionId — a trust
1027
+ // rule must distinguish clicking one control from another.
1028
+ subjectKey: "selector",
1020
1029
  mutating: true,
1021
1030
  riskTier: "safe",
1022
1031
  capabilities: [ToolCapabilities.NET_OUTBOUND],
@@ -1046,6 +1055,9 @@ function selectorTool(name, description, run) {
1046
1055
  description,
1047
1056
  usageHint: `${name}({ sessionId, selector })`,
1048
1057
  permission: "confirm",
1058
+ // WS-046: the element/expression acted on, not the sessionId — a trust
1059
+ // rule must distinguish clicking one control from another.
1060
+ subjectKey: "selector",
1049
1061
  mutating: true,
1050
1062
  riskTier: "standard",
1051
1063
  capabilities: [ToolCapabilities.NET_OUTBOUND],
@@ -1075,6 +1087,9 @@ var browserTypeTool = {
1075
1087
  description: "Fill an input in an owned browser session. Use secretEnv instead of text for credentials so values stay out of tool arguments and session audit.",
1076
1088
  usageHint: "browser_type({ sessionId, selector, text? | secretEnv? })",
1077
1089
  permission: "confirm",
1090
+ // WS-046: the element/expression acted on, not the sessionId — a trust
1091
+ // rule must distinguish clicking one control from another.
1092
+ subjectKey: "selector",
1078
1093
  mutating: true,
1079
1094
  riskTier: "standard",
1080
1095
  capabilities: [ToolCapabilities.NET_OUTBOUND],
@@ -1109,6 +1124,9 @@ var browserSelectTool = {
1109
1124
  description: "Select an option in an owned browser session.",
1110
1125
  usageHint: "browser_select({ sessionId, selector, value })",
1111
1126
  permission: "confirm",
1127
+ // WS-046: the element/expression acted on, not the sessionId — a trust
1128
+ // rule must distinguish clicking one control from another.
1129
+ subjectKey: "selector",
1112
1130
  mutating: true,
1113
1131
  riskTier: "standard",
1114
1132
  capabilities: [ToolCapabilities.NET_OUTBOUND],
@@ -1137,6 +1155,9 @@ var browserPressTool = {
1137
1155
  description: "Press a keyboard key in an owned browser session.",
1138
1156
  usageHint: "browser_press({ sessionId, key })",
1139
1157
  permission: "confirm",
1158
+ // WS-046: the element/expression acted on, not the sessionId — a trust
1159
+ // rule must distinguish clicking one control from another.
1160
+ subjectKey: "key",
1140
1161
  mutating: true,
1141
1162
  riskTier: "standard",
1142
1163
  capabilities: [ToolCapabilities.NET_OUTBOUND],
@@ -1155,6 +1176,9 @@ var browserDragTool = {
1155
1176
  description: "Drag one element to another in an owned browser session.",
1156
1177
  usageHint: "browser_drag({ sessionId, from, to })",
1157
1178
  permission: "confirm",
1179
+ // WS-046: the element/expression acted on, not the sessionId — a trust
1180
+ // rule must distinguish clicking one control from another.
1181
+ subjectKey: "from",
1158
1182
  mutating: true,
1159
1183
  riskTier: "standard",
1160
1184
  capabilities: [ToolCapabilities.NET_OUTBOUND],
@@ -1204,6 +1228,9 @@ var browserEvaluateTool = {
1204
1228
  description: "Evaluate bounded JavaScript in the page. Requires confirmation because page code is arbitrary.",
1205
1229
  usageHint: "browser_evaluate({ sessionId, expression })",
1206
1230
  permission: "confirm",
1231
+ // WS-046: the element/expression acted on, not the sessionId — a trust
1232
+ // rule must distinguish clicking one control from another.
1233
+ subjectKey: "expression",
1207
1234
  mutating: true,
1208
1235
  riskTier: "destructive",
1209
1236
  capabilities: [ToolCapabilities.NET_OUTBOUND, ToolCapabilities.TOOL_MUTATE_ANY],
@@ -1226,6 +1253,9 @@ var browserUploadTool = {
1226
1253
  description: "Upload project-local files through a file input in an owned browser session.",
1227
1254
  usageHint: "browser_upload({ sessionId, selector, files })",
1228
1255
  permission: "confirm",
1256
+ // WS-046: the element/expression acted on, not the sessionId — a trust
1257
+ // rule must distinguish clicking one control from another.
1258
+ subjectKey: "selector",
1229
1259
  mutating: true,
1230
1260
  riskTier: "destructive",
1231
1261
  capabilities: [ToolCapabilities.NET_OUTBOUND, ToolCapabilities.FS_READ],