@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
package/dist/pack.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);
@@ -126,6 +127,73 @@ var init_output_spool = __esm({
126
127
  }
127
128
  });
128
129
 
130
+ // src/_win32-resolve.ts
131
+ import * as fs from "node:fs";
132
+ import * as path2 from "node:path";
133
+ function resolveWin32Command(cmd) {
134
+ if (process.platform !== "win32") return cmd;
135
+ if (cmd.includes("/") || cmd.includes("\\") || path2.extname(cmd.replace(/\//g, "\\"))) {
136
+ return cmd;
137
+ }
138
+ const pathext = (process.env["PATHEXT"] ?? ".COM;.EXE;.BAT;.CMD;.VBS;.JS;.WS;.MSC").toLowerCase().split(";");
139
+ const pathDirs = (process.env["PATH"] ?? "").split(path2.delimiter);
140
+ for (const dir of pathDirs) {
141
+ const base = path2.join(dir, cmd);
142
+ for (const ext of pathext) {
143
+ const full = `${base}${ext}`;
144
+ try {
145
+ fs.accessSync(full, fs.constants.X_OK);
146
+ return full;
147
+ } catch {
148
+ }
149
+ }
150
+ }
151
+ return cmd;
152
+ }
153
+ function resolvePowerShell(cmd) {
154
+ if (process.platform !== "win32") return cmd;
155
+ const lower = cmd.toLowerCase();
156
+ if (lower !== "pwsh" && lower !== "powershell" && lower !== "pwsh.exe" && lower !== "powershell.exe") {
157
+ return resolveWin32Command(cmd);
158
+ }
159
+ const primary = lower.startsWith("pwsh") ? "pwsh.exe" : "powershell.exe";
160
+ const fallback = lower.startsWith("pwsh") ? "powershell.exe" : "pwsh.exe";
161
+ const resolved = resolveWin32Command(primary);
162
+ if (resolved !== primary) {
163
+ const fb = resolveWin32Command(fallback);
164
+ return fb === fallback ? cmd : fb;
165
+ }
166
+ return resolved;
167
+ }
168
+ function assertSafeWin32ShellArgs(args) {
169
+ for (const arg of args) {
170
+ if (typeof arg === "string" && WIN32_SHELL_META.test(arg)) {
171
+ throw new Error(
172
+ '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)
173
+ );
174
+ }
175
+ }
176
+ }
177
+ function buildWin32CmdShimInvocation(command, args = []) {
178
+ assertSafeWin32ShellArgs([command, ...args]);
179
+ const line = ["call", quoteWin32CmdArg(command), ...args.map(quoteWin32CmdArg)].join(" ");
180
+ return {
181
+ command: process.env["COMSPEC"] ?? "cmd.exe",
182
+ args: ["/d", "/c", line],
183
+ windowsVerbatimArguments: true
184
+ };
185
+ }
186
+ function quoteWin32CmdArg(arg) {
187
+ return `"${arg}"`;
188
+ }
189
+ var WIN32_SHELL_META;
190
+ var init_win32_resolve = __esm({
191
+ "src/_win32-resolve.ts"() {
192
+ "use strict";
193
+ WIN32_SHELL_META = /[&|<>"\r\n\0]/;
194
+ }
195
+ });
196
+
129
197
  // src/circuit-breaker.ts
130
198
  var DEFAULT_MAX_CONSECUTIVE_FAILURES, DEFAULT_SLOW_CALL_THRESHOLD_MS, DEFAULT_MAX_SLOW_CALLS, DEFAULT_WINDOW_MS, DEFAULT_MAX_CALLS_PER_WINDOW, DEFAULT_COOLDOWN_MS, CircuitBreaker;
131
199
  var init_circuit_breaker = __esm({
@@ -767,73 +835,6 @@ var init_process_registry = __esm({
767
835
  }
768
836
  });
769
837
 
770
- // src/_win32-resolve.ts
771
- import * as fs from "node:fs";
772
- import * as path2 from "node:path";
773
- function resolveWin32Command(cmd) {
774
- if (process.platform !== "win32") return cmd;
775
- if (cmd.includes("/") || cmd.includes("\\") || path2.extname(cmd.replace(/\//g, "\\"))) {
776
- return cmd;
777
- }
778
- const pathext = (process.env["PATHEXT"] ?? ".COM;.EXE;.BAT;.CMD;.VBS;.JS;.WS;.MSC").toLowerCase().split(";");
779
- const pathDirs = (process.env["PATH"] ?? "").split(path2.delimiter);
780
- for (const dir of pathDirs) {
781
- const base = path2.join(dir, cmd);
782
- for (const ext of pathext) {
783
- const full = `${base}${ext}`;
784
- try {
785
- fs.accessSync(full, fs.constants.X_OK);
786
- return full;
787
- } catch {
788
- }
789
- }
790
- }
791
- return cmd;
792
- }
793
- function resolvePowerShell(cmd) {
794
- if (process.platform !== "win32") return cmd;
795
- const lower = cmd.toLowerCase();
796
- if (lower !== "pwsh" && lower !== "powershell" && lower !== "pwsh.exe" && lower !== "powershell.exe") {
797
- return resolveWin32Command(cmd);
798
- }
799
- const primary = lower.startsWith("pwsh") ? "pwsh.exe" : "powershell.exe";
800
- const fallback = lower.startsWith("pwsh") ? "powershell.exe" : "pwsh.exe";
801
- const resolved = resolveWin32Command(primary);
802
- if (resolved !== primary) {
803
- const fb = resolveWin32Command(fallback);
804
- return fb === fallback ? cmd : fb;
805
- }
806
- return resolved;
807
- }
808
- function assertSafeWin32ShellArgs(args) {
809
- for (const arg of args) {
810
- if (typeof arg === "string" && WIN32_SHELL_META.test(arg)) {
811
- throw new Error(
812
- '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)
813
- );
814
- }
815
- }
816
- }
817
- function buildWin32CmdShimInvocation(command, args = []) {
818
- assertSafeWin32ShellArgs([command, ...args]);
819
- const line = ["call", quoteWin32CmdArg(command), ...args.map(quoteWin32CmdArg)].join(" ");
820
- return {
821
- command: process.env["COMSPEC"] ?? "cmd.exe",
822
- args: ["/d", "/c", line],
823
- windowsVerbatimArguments: true
824
- };
825
- }
826
- function quoteWin32CmdArg(arg) {
827
- return `"${arg}"`;
828
- }
829
- var WIN32_SHELL_META;
830
- var init_win32_resolve = __esm({
831
- "src/_win32-resolve.ts"() {
832
- "use strict";
833
- WIN32_SHELL_META = /[&|<>"\r\n\0]/;
834
- }
835
- });
836
-
837
838
  // src/_spawn-stream.ts
838
839
  import { spawn as spawn2 } from "node:child_process";
839
840
  import {
@@ -846,8 +847,11 @@ async function* spawnStream(opts) {
846
847
  const max = opts.maxBytes ?? 2e5;
847
848
  const flushAt = opts.flushBytes ?? 4 * 1024;
848
849
  const maxQueue = opts.maxQueueSize ?? 500;
850
+ const maxQueueBytes = opts.maxQueueBytes ?? 1024 * 1024;
849
851
  let stdout = "";
850
852
  let stderr = "";
853
+ let stdoutRetainedBytes = 0;
854
+ let stderrRetainedBytes = 0;
851
855
  let pending2 = "";
852
856
  let error;
853
857
  const spool = createOutputSpool({ tool: opts.cmd, thresholdBytes: max });
@@ -889,6 +893,7 @@ async function* spawnStream(opts) {
889
893
  });
890
894
  }
891
895
  const queue = [];
896
+ let queuedBytes = 0;
892
897
  let waiter;
893
898
  let paused = false;
894
899
  const wake = () => {
@@ -899,7 +904,7 @@ async function* spawnStream(opts) {
899
904
  }
900
905
  };
901
906
  const resume = () => {
902
- if (paused && queue.length < maxQueue) {
907
+ if (paused && queue.length <= Math.floor(maxQueue / 2) && queuedBytes <= Math.floor(maxQueueBytes / 2)) {
903
908
  paused = false;
904
909
  child.stdout?.resume();
905
910
  child.stderr?.resume();
@@ -909,11 +914,16 @@ async function* spawnStream(opts) {
909
914
  const s = c.toString();
910
915
  stdoutBytes += c.byteLength;
911
916
  emitProcessOutput({ pid, stream: "stdout", chunk: c });
912
- if (stdout.length < max) stdout += s;
917
+ if (stdoutRetainedBytes < max) {
918
+ const retained = utf8Prefix(s, max - stdoutRetainedBytes);
919
+ stdout += retained;
920
+ stdoutRetainedBytes += Buffer.byteLength(retained, "utf8");
921
+ }
913
922
  spool.write(s);
914
- queue.push({ kind: "out", data: s });
923
+ queue.push({ kind: "out", data: s, bytes: c.byteLength });
924
+ queuedBytes += c.byteLength;
915
925
  wake();
916
- if (!paused && queue.length >= maxQueue) {
926
+ if (!paused && (queue.length >= maxQueue || queuedBytes >= maxQueueBytes)) {
917
927
  paused = true;
918
928
  child.stdout?.pause();
919
929
  child.stderr?.pause();
@@ -923,11 +933,16 @@ async function* spawnStream(opts) {
923
933
  const s = c.toString();
924
934
  stderrBytes += c.byteLength;
925
935
  emitProcessOutput({ pid, stream: "stderr", chunk: c });
926
- if (stderr.length < max) stderr += s;
936
+ if (stderrRetainedBytes < max) {
937
+ const retained = utf8Prefix(s, max - stderrRetainedBytes);
938
+ stderr += retained;
939
+ stderrRetainedBytes += Buffer.byteLength(retained, "utf8");
940
+ }
927
941
  spool.write(s);
928
- queue.push({ kind: "err", data: s });
942
+ queue.push({ kind: "err", data: s, bytes: c.byteLength });
943
+ queuedBytes += c.byteLength;
929
944
  wake();
930
- if (!paused && queue.length >= maxQueue) {
945
+ if (!paused && (queue.length >= maxQueue || queuedBytes >= maxQueueBytes)) {
931
946
  paused = true;
932
947
  child.stdout?.pause();
933
948
  child.stderr?.pause();
@@ -937,7 +952,9 @@ async function* spawnStream(opts) {
937
952
  child.stderr?.on("data", onErr);
938
953
  child.on("error", (e) => {
939
954
  error = e.message;
940
- queue.push({ kind: "error", data: e.message });
955
+ const bytes = Buffer.byteLength(e.message, "utf8");
956
+ queue.push({ kind: "error", data: e.message, bytes });
957
+ queuedBytes += bytes;
941
958
  wake();
942
959
  });
943
960
  const completeTelemetry = (code, signal, timedOut = false) => {
@@ -958,7 +975,13 @@ async function* spawnStream(opts) {
958
975
  if (typeof pid === "number") registry.unregister(pid);
959
976
  const exitCode2 = code ?? (signal ? 1 : 0);
960
977
  completeTelemetry(exitCode2, signal ?? void 0);
961
- queue.push({ kind: "close", data: "", code: exitCode2, ...signal ? { signal } : {} });
978
+ queue.push({
979
+ kind: "close",
980
+ data: "",
981
+ bytes: 0,
982
+ code: exitCode2,
983
+ ...signal ? { signal } : {}
984
+ });
962
985
  wake();
963
986
  });
964
987
  const onAbort = () => {
@@ -970,7 +993,7 @@ async function* spawnStream(opts) {
970
993
  } catch {
971
994
  }
972
995
  }
973
- queue.push({ kind: "close", data: "", code: 124 });
996
+ queue.push({ kind: "close", data: "", bytes: 0, code: 124 });
974
997
  completeTelemetry(124, "SIGKILL", true);
975
998
  wake();
976
999
  };
@@ -988,6 +1011,7 @@ async function* spawnStream(opts) {
988
1011
  });
989
1012
  }
990
1013
  const chunk = queue.shift();
1014
+ queuedBytes -= chunk.bytes;
991
1015
  resume();
992
1016
  if (chunk.kind === "close") {
993
1017
  if (!spawnFailed) exitCode = chunk.code ?? 0;
@@ -1014,7 +1038,7 @@ async function* spawnStream(opts) {
1014
1038
  stdout: spooled ? stdout + spoolNote(spooled) : stdout,
1015
1039
  stderr,
1016
1040
  exitCode,
1017
- truncated: stdout.length >= max || stderr.length >= max,
1041
+ truncated: stdoutBytes > stdoutRetainedBytes || stderrBytes > stderrRetainedBytes,
1018
1042
  error,
1019
1043
  spoolPath: spooled?.path,
1020
1044
  spoolBytes: spooled?.bytes
@@ -1038,13 +1062,27 @@ async function* spawnStream(opts) {
1038
1062
  }
1039
1063
  }
1040
1064
  }
1065
+ function utf8Prefix(text, maxBytes) {
1066
+ if (maxBytes <= 0) return "";
1067
+ if (Buffer.byteLength(text, "utf8") <= maxBytes) return text;
1068
+ let low = 0;
1069
+ let high = text.length;
1070
+ while (low < high) {
1071
+ const mid = Math.ceil((low + high) / 2);
1072
+ if (Buffer.byteLength(text.slice(0, mid), "utf8") <= maxBytes) low = mid;
1073
+ else high = mid - 1;
1074
+ }
1075
+ let end = low;
1076
+ if (end > 0 && /[\uD800-\uDBFF]/.test(text[end - 1])) end--;
1077
+ return text.slice(0, end);
1078
+ }
1041
1079
  var isWin;
1042
1080
  var init_spawn_stream = __esm({
1043
1081
  "src/_spawn-stream.ts"() {
1044
1082
  "use strict";
1045
1083
  init_output_spool();
1046
- init_process_registry();
1047
1084
  init_win32_resolve();
1085
+ init_process_registry();
1048
1086
  isWin = process.platform === "win32";
1049
1087
  }
1050
1088
  });
@@ -1058,14 +1096,14 @@ function sha256hex(content) {
1058
1096
  return createHash("sha256").update(content, "utf8").digest("hex");
1059
1097
  }
1060
1098
  async function detectPackageManager(cwd) {
1061
- const { stat: stat17 } = await import("node:fs/promises");
1099
+ const { stat: stat18 } = await import("node:fs/promises");
1062
1100
  try {
1063
- await stat17(`${cwd}/pnpm-lock.yaml`);
1101
+ await stat18(`${cwd}/pnpm-lock.yaml`);
1064
1102
  return "pnpm";
1065
1103
  } catch {
1066
1104
  }
1067
1105
  try {
1068
- await stat17(`${cwd}/yarn.lock`);
1106
+ await stat18(`${cwd}/yarn.lock`);
1069
1107
  return "yarn";
1070
1108
  } catch {
1071
1109
  }
@@ -1093,11 +1131,15 @@ function safeResolve(input, ctx) {
1093
1131
  return ensureInsideRoot(resolvePath(input, ctx), ctx);
1094
1132
  }
1095
1133
  async function assertRealInsideRoot(absPath, ctx) {
1096
- if (ctx.allowOutsideProjectRoot) return;
1134
+ await resolveRealInsideRoot(absPath, ctx);
1135
+ }
1136
+ async function resolveRealInsideRoot(absPath, ctx) {
1137
+ if (ctx.allowOutsideProjectRoot) return absPath;
1097
1138
  const realRoots = await Promise.all(
1098
1139
  allowedRoots(ctx).map((r) => fsp2.realpath(r).catch(() => path3.resolve(r)))
1099
1140
  );
1100
1141
  let probe2 = absPath;
1142
+ const pendingTail = [];
1101
1143
  for (; ; ) {
1102
1144
  let real;
1103
1145
  try {
@@ -1105,13 +1147,16 @@ async function assertRealInsideRoot(absPath, ctx) {
1105
1147
  } catch (err) {
1106
1148
  if (err.code === "ENOENT") {
1107
1149
  const parent = path3.dirname(probe2);
1108
- if (parent === probe2) return;
1150
+ if (parent === probe2) return absPath;
1151
+ pendingTail.unshift(path3.basename(probe2));
1109
1152
  probe2 = parent;
1110
1153
  continue;
1111
1154
  }
1112
1155
  throw err;
1113
1156
  }
1114
- if (isInsideAny(real, realRoots)) return;
1157
+ if (isInsideAny(real, realRoots)) {
1158
+ return pendingTail.length > 0 ? path3.join(real, ...pendingTail) : real;
1159
+ }
1115
1160
  throw new Error(
1116
1161
  `Path "${absPath}" resolves through a symlink outside project root "${realRoots[0]}"`
1117
1162
  );
@@ -1119,8 +1164,7 @@ async function assertRealInsideRoot(absPath, ctx) {
1119
1164
  }
1120
1165
  async function safeResolveReal(input, ctx) {
1121
1166
  const abs = safeResolve(input, ctx);
1122
- await assertRealInsideRoot(abs, ctx);
1123
- return abs;
1167
+ return await resolveRealInsideRoot(abs, ctx);
1124
1168
  }
1125
1169
  function truncateMiddle(s, max) {
1126
1170
  if (Buffer.byteLength(s, "utf8") <= max) return s;
@@ -2684,8 +2728,8 @@ async function scanDirectory(directory, depth, profiles, limits, state, extraIgn
2684
2728
  collectFileEvidence(directory, fullPath, entry.name, profiles, state);
2685
2729
  }
2686
2730
  }
2687
- function collectFileEvidence(directory, fullPath, basename12, profiles, state) {
2688
- const lower = basename12.toLowerCase();
2731
+ function collectFileEvidence(directory, fullPath, basename13, profiles, state) {
2732
+ const lower = basename13.toLowerCase();
2689
2733
  const extension = path4.extname(lower);
2690
2734
  for (const profile of profiles) {
2691
2735
  const detector = profile.detectors.find(
@@ -2696,7 +2740,7 @@ function collectFileEvidence(directory, fullPath, basename12, profiles, state) {
2696
2740
  candidate.evidence.push({
2697
2741
  kind: detector.kind,
2698
2742
  path: fullPath,
2699
- value: basename12,
2743
+ value: basename13,
2700
2744
  weight: detector.weight
2701
2745
  });
2702
2746
  if (detector.kind === "manifest" || detector.kind === "config") {
@@ -2824,8 +2868,8 @@ function normalizeLimits(input) {
2824
2868
  async function canonicalDirectory(input) {
2825
2869
  const resolved = path4.resolve(input);
2826
2870
  const real = await fs2.realpath(resolved);
2827
- const stat17 = await fs2.stat(real);
2828
- if (!stat17.isDirectory()) throw new Error(`Project root is not a directory: ${input}`);
2871
+ const stat18 = await fs2.stat(real);
2872
+ if (!stat18.isDirectory()) throw new Error(`Project root is not a directory: ${input}`);
2829
2873
  return real;
2830
2874
  }
2831
2875
  async function canonicalInside(input, root, label) {
@@ -3381,6 +3425,7 @@ import * as fs3 from "node:fs/promises";
3381
3425
  import * as path6 from "node:path";
3382
3426
  async function planLanguageOperation(options) {
3383
3427
  validateOperationOptions(options.operation, options.operationOptions?.packages);
3428
+ validateFilterOption(options.operationOptions?.filter);
3384
3429
  const detection = await detectLanguageWorkspaces(options);
3385
3430
  const candidates = detection.workspaces.filter(
3386
3431
  (workspace) => workspace.capabilities.includes(options.operation)
@@ -3553,6 +3598,15 @@ function workspaceDepth(workspaceRoot, projectRoot) {
3553
3598
  const relative12 = path6.relative(path6.resolve(projectRoot), path6.resolve(workspaceRoot));
3554
3599
  return relative12 === "" ? 0 : relative12.split(path6.sep).length;
3555
3600
  }
3601
+ function validateFilterOption(filter) {
3602
+ if (filter === void 0) return;
3603
+ if (filter.startsWith("-") || filter.includes(" --")) {
3604
+ throw new Error(`Invalid test filter (parsed as a runner option): "${filter}"`);
3605
+ }
3606
+ if (filter.length > MAX_ARGUMENT_LENGTH || /[\r\n\0]/.test(filter)) {
3607
+ throw new Error("Invalid test filter (oversized or contains a control character)");
3608
+ }
3609
+ }
3556
3610
  function validateOperationOptions(operation, packages) {
3557
3611
  if (!operation.startsWith("package-") || !packages) return;
3558
3612
  for (const value of packages) {
@@ -3594,7 +3648,7 @@ var init_plan = __esm({
3594
3648
  MAX_ARGUMENT_LENGTH = 4096;
3595
3649
  PACKAGE_NAME_RE = /^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+(?:@[a-z0-9*+._~^<>=|-]+)?$/i;
3596
3650
  COMPOSER_PACKAGE_RE = /^[a-z0-9_.-]+\/[a-z0-9_.-]+(?::[a-z0-9*+._~^<>=|-]+)?$/i;
3597
- GO_MODULE_RE = /^(?:[a-z0-9.-]+\.)+[a-z0-9.-]+\/[a-z0-9._~+/@-]+$/i;
3651
+ GO_MODULE_RE = /^(?:[a-z0-9-]+\.)+[a-z0-9-]+\/[a-z0-9._~+/@-]+$/i;
3598
3652
  PYTHON_PACKAGE_RE = /^[a-z0-9._-]+(?:\[[a-z0-9._,-]+\])?==[a-z0-9*+._!~-]+$/i;
3599
3653
  }
3600
3654
  });
@@ -3714,8 +3768,8 @@ async function executeInternal(options, startedAt) {
3714
3768
  const target = options.plan.evidence.find((item) => item.kind === "target")?.path;
3715
3769
  if (!target) return unavailableResult(options, "Internal syntax plan has no target evidence.");
3716
3770
  const safeTarget = await assertContainedFile(target, options.projectRoot);
3717
- const stat17 = await fs4.stat(safeTarget);
3718
- if (stat17.size > MAX_INTERNAL_SOURCE_BYTES) {
3771
+ const stat18 = await fs4.stat(safeTarget);
3772
+ if (stat18.size > MAX_INTERNAL_SOURCE_BYTES) {
3719
3773
  return unavailableResult(
3720
3774
  options,
3721
3775
  `Internal syntax target exceeds ${MAX_INTERNAL_SOURCE_BYTES} bytes.`
@@ -4002,8 +4056,8 @@ async function snapshotPaths(paths) {
4002
4056
  const existing = [];
4003
4057
  for (const candidate of paths) {
4004
4058
  try {
4005
- const stat17 = await fs4.stat(candidate);
4006
- if (!stat17.isFile()) continue;
4059
+ const stat18 = await fs4.stat(candidate);
4060
+ if (!stat18.isFile()) continue;
4007
4061
  existing.push(candidate);
4008
4062
  } catch {
4009
4063
  }
@@ -4031,8 +4085,8 @@ async function snapshotSizes(paths) {
4031
4085
  const sizes = /* @__PURE__ */ new Map();
4032
4086
  for (const candidate of paths) {
4033
4087
  try {
4034
- const stat17 = await fs4.stat(candidate);
4035
- if (stat17.isFile()) sizes.set(candidate, stat17.size);
4088
+ const stat18 = await fs4.stat(candidate);
4089
+ if (stat18.isFile()) sizes.set(candidate, stat18.size);
4036
4090
  } catch {
4037
4091
  }
4038
4092
  }
@@ -4192,6 +4246,9 @@ var init_execute_tool = __esm({
4192
4246
  useInstead: ["language_info", "install", "audit", "outdated"]
4193
4247
  },
4194
4248
  permission: "confirm",
4249
+ // WS-046: gives permission decisions something to key on.
4250
+ // The action performed (build/test/lint); the cwd is where, not what.
4251
+ subjectKey: "action",
4195
4252
  mutating: true,
4196
4253
  riskTier: "standard",
4197
4254
  capabilities: ["shell.restricted", "fs.write"],
@@ -4424,6 +4481,9 @@ var init_package_tool = __esm({
4424
4481
  useInstead: ["language_info", "language", "install", "audit", "outdated"]
4425
4482
  },
4426
4483
  permission: "confirm",
4484
+ // WS-046: gives permission decisions something to key on.
4485
+ // The package operation performed — install/remove are not interchangeable.
4486
+ subjectKey: "operation",
4427
4487
  mutating: true,
4428
4488
  riskTier: "destructive",
4429
4489
  capabilities: ["shell.restricted", "fs.write", "net.outbound", "package.install"],
@@ -6475,9 +6535,9 @@ function parseSymbols6(opts) {
6475
6535
  function regexParse2(opts) {
6476
6536
  const { file, content, lang } = opts;
6477
6537
  const symbols = [];
6478
- const basename12 = path22.basename(file).toLowerCase();
6479
- const isPackageJson = basename12 === "package.json";
6480
- const isTsconfig = basename12 === "tsconfig.json" || basename12 === "tsconfig.build.json";
6538
+ const basename13 = path22.basename(file).toLowerCase();
6539
+ const isPackageJson = basename13 === "package.json";
6540
+ const isTsconfig = basename13 === "tsconfig.json" || basename13 === "tsconfig.build.json";
6481
6541
  const isJsonSchema = content.includes("$schema") || content.includes("$id") || content.includes("$ref");
6482
6542
  const isOpenApi = content.includes("openapi") || content.includes("swagger");
6483
6543
  const lines = content.split("\n");
@@ -8373,30 +8433,59 @@ ${hint}` : ""),
8373
8433
  import {
8374
8434
  GOVERNED_TOOL_EXECUTOR_META_KEY
8375
8435
  } from "@wrongstack/core/types";
8436
+ var BATCHABLE_TOOL_NAMES = [
8437
+ "read",
8438
+ "glob",
8439
+ "grep",
8440
+ "tree",
8441
+ "codebase-stats",
8442
+ "codebase-search",
8443
+ "language_info",
8444
+ "tool_search",
8445
+ "memory_for_file",
8446
+ "memory_for_path",
8447
+ "memory_search"
8448
+ ];
8449
+ var BATCHABLE_TOOLS = new Set(BATCHABLE_TOOL_NAMES);
8450
+ var MAX_BATCH_CALLS = 8;
8451
+ var MAX_PARALLEL_BATCH_CALLS = 3;
8376
8452
  var batchToolUseTool = {
8377
8453
  name: "batch_tool_use",
8378
8454
  category: "Meta",
8379
- description: "Execute a batch of tool calls either sequentially or in parallel. Returns structured results for every call.",
8380
- 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.",
8455
+ description: "Execute a small batch of independent, read-only discovery calls. Process, mutation, browser-action, MCP, delegation, and nested meta tools are not eligible.",
8456
+ usageHint: `BOUNDED READ-ONLY BATCHING:
8457
+
8458
+ - Use only for a small set of independent reads or searches that are all needed now.
8459
+ - Do not wrap normal sequential reasoning or every tool call in a batch.
8460
+ - \`parallel: true\` (default) runs at most three eligible calls concurrently.
8461
+ - \`stop_on_error: true\` makes it fail fast on the first error.
8462
+ - Eligible tools: ${BATCHABLE_TOOL_NAMES.join(", ")}.`,
8381
8463
  permission: "confirm",
8464
+ // Scheduling barrier: keep the outer meta-call out of ToolExecutor's
8465
+ // non-mutating fan-out even though every eligible inner tool is read-only.
8382
8466
  mutating: true,
8383
8467
  timeoutMs: 12e4,
8384
- capabilities: ["tool.mutate.any"],
8468
+ capabilities: ["tool.meta"],
8385
8469
  icon: "meta",
8386
8470
  inputSchema: {
8387
8471
  type: "object",
8388
8472
  properties: {
8389
8473
  calls: {
8390
8474
  type: "array",
8475
+ maxItems: MAX_BATCH_CALLS,
8391
8476
  items: {
8392
8477
  type: "object",
8393
8478
  properties: {
8394
- tool: { type: "string" },
8479
+ tool: {
8480
+ type: "string",
8481
+ enum: [...BATCHABLE_TOOL_NAMES],
8482
+ description: "An explicitly batch-safe, read-only discovery tool."
8483
+ },
8395
8484
  input: { type: "object" }
8396
8485
  },
8397
8486
  required: ["tool"]
8398
8487
  },
8399
- description: "Array of tool calls to execute"
8488
+ description: `Up to ${MAX_BATCH_CALLS} independent, batch-safe read calls.`
8400
8489
  },
8401
8490
  stop_on_error: {
8402
8491
  type: "boolean",
@@ -8419,6 +8508,9 @@ var batchToolUseTool = {
8419
8508
  stop_on_error: false
8420
8509
  };
8421
8510
  }
8511
+ if (input.calls.length > MAX_BATCH_CALLS) {
8512
+ throw new Error(`batch_tool_use: calls must contain at most ${MAX_BATCH_CALLS} items`);
8513
+ }
8422
8514
  const governedExecute = ctx.meta[GOVERNED_TOOL_EXECUTOR_META_KEY];
8423
8515
  if (typeof governedExecute !== "function") {
8424
8516
  return {
@@ -8438,8 +8530,11 @@ var batchToolUseTool = {
8438
8530
  let succeeded = 0;
8439
8531
  let failed = 0;
8440
8532
  if (input.parallel !== false) {
8441
- const promises = input.calls.map(async (call) => executeSingle(call, ctx, governedExecute));
8442
- const allResults = await Promise.all(promises);
8533
+ const allResults = await mapWithConcurrency(
8534
+ input.calls,
8535
+ MAX_PARALLEL_BATCH_CALLS,
8536
+ (call) => executeSingle(call, ctx, governedExecute)
8537
+ );
8443
8538
  results.push(...allResults);
8444
8539
  succeeded = allResults.filter((r) => r.success).length;
8445
8540
  failed = allResults.filter((r) => !r.success).length;
@@ -8464,8 +8559,28 @@ var batchToolUseTool = {
8464
8559
  };
8465
8560
  }
8466
8561
  };
8562
+ async function mapWithConcurrency(items, limit, run) {
8563
+ const results = new Array(items.length);
8564
+ let nextIndex = 0;
8565
+ const worker2 = async () => {
8566
+ while (nextIndex < items.length) {
8567
+ const index = nextIndex++;
8568
+ results[index] = await run(items[index]);
8569
+ }
8570
+ };
8571
+ await Promise.all(Array.from({ length: Math.min(limit, items.length) }, () => worker2()));
8572
+ return results;
8573
+ }
8467
8574
  async function executeSingle(call, ctx, governedExecute) {
8468
8575
  const start = Date.now();
8576
+ if (call.tool === batchToolUseTool.name) {
8577
+ return {
8578
+ tool: call.tool,
8579
+ success: false,
8580
+ error: "recursive batch_tool_use execution is not allowed",
8581
+ executionMs: Date.now() - start
8582
+ };
8583
+ }
8469
8584
  const tool = ctx.tools.find((candidate) => candidate.name === call.tool);
8470
8585
  if (!tool) {
8471
8586
  return {
@@ -8475,6 +8590,14 @@ async function executeSingle(call, ctx, governedExecute) {
8475
8590
  executionMs: Date.now() - start
8476
8591
  };
8477
8592
  }
8593
+ if (!BATCHABLE_TOOLS.has(tool.name) || tool.mutating) {
8594
+ return {
8595
+ tool: call.tool,
8596
+ success: false,
8597
+ error: `tool "${call.tool}" is not eligible for batch execution; call it directly. Allowed: ${BATCHABLE_TOOL_NAMES.join(", ")}`,
8598
+ executionMs: Date.now() - start
8599
+ };
8600
+ }
8478
8601
  try {
8479
8602
  const result = await governedExecute(call.tool, call.input);
8480
8603
  return {
@@ -8511,8 +8634,14 @@ import * as path10 from "node:path";
8511
8634
  import { atomicWrite, ulid } from "@wrongstack/core/utils";
8512
8635
  var ARTIFACT_RETENTION_MS = 7 * 24 * 60 * 60 * 1e3;
8513
8636
  var sweptRoots = /* @__PURE__ */ new Set();
8637
+ var MAX_SWEPT_ROOTS = 128;
8514
8638
  function sweepOldArtifacts(root) {
8515
8639
  if (sweptRoots.has(root)) return;
8640
+ while (sweptRoots.size >= MAX_SWEPT_ROOTS) {
8641
+ const oldest = sweptRoots.values().next().value;
8642
+ if (oldest === void 0) break;
8643
+ sweptRoots.delete(oldest);
8644
+ }
8516
8645
  sweptRoots.add(root);
8517
8646
  void (async () => {
8518
8647
  const cutoff = Date.now() - ARTIFACT_RETENTION_MS;
@@ -8535,8 +8664,8 @@ function sweepOldArtifacts(root) {
8535
8664
  for (const name of names) {
8536
8665
  const target = path10.join(dir, name);
8537
8666
  try {
8538
- const stat17 = await fs7.stat(target);
8539
- if (stat17.isFile() && stat17.mtimeMs < cutoff) {
8667
+ const stat18 = await fs7.stat(target);
8668
+ if (stat18.isFile() && stat18.mtimeMs < cutoff) {
8540
8669
  await fs7.rm(target, { force: true });
8541
8670
  removed++;
8542
8671
  }
@@ -8564,14 +8693,14 @@ var BrowserArtifactStore = class {
8564
8693
  }
8565
8694
  async record(id, sessionId, kind, target, mimeType) {
8566
8695
  await fs7.chmod(target, 384).catch(() => void 0);
8567
- const [stat17, sha256] = await Promise.all([fs7.stat(target), hashFile(target)]);
8696
+ const [stat18, sha256] = await Promise.all([fs7.stat(target), hashFile(target)]);
8568
8697
  const artifact = {
8569
8698
  id,
8570
8699
  kind,
8571
8700
  sensitivity: "sensitive",
8572
8701
  path: target,
8573
8702
  mimeType,
8574
- sizeBytes: stat17.size,
8703
+ sizeBytes: stat18.size,
8575
8704
  sha256,
8576
8705
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
8577
8706
  };
@@ -8941,7 +9070,7 @@ var BrowserSessionManager = class {
8941
9070
  constructor(options, launcher = defaultLauncher) {
8942
9071
  this.launcher = launcher;
8943
9072
  this.artifacts = new BrowserArtifactStore(options.artifactRoot);
8944
- this.allowPrivateHosts = options.allowPrivateHosts ?? true;
9073
+ this.allowPrivateHosts = options.allowPrivateHosts ?? false;
8945
9074
  this.allowedPrivateOrigins = options.allowedPrivateOrigins ?? [];
8946
9075
  this.networkProxy = new BrowserNetworkGuardProxy({
8947
9076
  allowPrivateHosts: this.allowPrivateHosts,
@@ -9164,8 +9293,8 @@ var BrowserSessionManager = class {
9164
9293
  if (realRelative.startsWith("..") || path11.isAbsolute(realRelative)) {
9165
9294
  throw new Error("browser: upload files must not escape the project root through a symlink");
9166
9295
  }
9167
- const stat17 = await fs8.stat(realFile);
9168
- if (!stat17.isFile()) throw new Error(`browser: upload target is not a file: ${file}`);
9296
+ const stat18 = await fs8.stat(realFile);
9297
+ if (!stat18.isFile()) throw new Error(`browser: upload target is not a file: ${file}`);
9169
9298
  resolved.push(realFile);
9170
9299
  }
9171
9300
  await this.runPageOperation(
@@ -9514,6 +9643,9 @@ var browserScreenshotTool = {
9514
9643
  description: "Capture a page or element PNG and return sensitive artifact metadata with integrity hash.",
9515
9644
  usageHint: "browser_screenshot({ sessionId, fullPage?, selector? })",
9516
9645
  permission: "confirm",
9646
+ // WS-046: the element/expression acted on, not the sessionId — a trust
9647
+ // rule must distinguish clicking one control from another.
9648
+ subjectKey: "selector",
9517
9649
  mutating: true,
9518
9650
  riskTier: "safe",
9519
9651
  capabilities: [ToolCapabilities.NET_OUTBOUND],
@@ -9543,6 +9675,9 @@ function selectorTool(name, description, run) {
9543
9675
  description,
9544
9676
  usageHint: `${name}({ sessionId, selector })`,
9545
9677
  permission: "confirm",
9678
+ // WS-046: the element/expression acted on, not the sessionId — a trust
9679
+ // rule must distinguish clicking one control from another.
9680
+ subjectKey: "selector",
9546
9681
  mutating: true,
9547
9682
  riskTier: "standard",
9548
9683
  capabilities: [ToolCapabilities.NET_OUTBOUND],
@@ -9572,6 +9707,9 @@ var browserTypeTool = {
9572
9707
  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.",
9573
9708
  usageHint: "browser_type({ sessionId, selector, text? | secretEnv? })",
9574
9709
  permission: "confirm",
9710
+ // WS-046: the element/expression acted on, not the sessionId — a trust
9711
+ // rule must distinguish clicking one control from another.
9712
+ subjectKey: "selector",
9575
9713
  mutating: true,
9576
9714
  riskTier: "standard",
9577
9715
  capabilities: [ToolCapabilities.NET_OUTBOUND],
@@ -9606,6 +9744,9 @@ var browserSelectTool = {
9606
9744
  description: "Select an option in an owned browser session.",
9607
9745
  usageHint: "browser_select({ sessionId, selector, value })",
9608
9746
  permission: "confirm",
9747
+ // WS-046: the element/expression acted on, not the sessionId — a trust
9748
+ // rule must distinguish clicking one control from another.
9749
+ subjectKey: "selector",
9609
9750
  mutating: true,
9610
9751
  riskTier: "standard",
9611
9752
  capabilities: [ToolCapabilities.NET_OUTBOUND],
@@ -9634,6 +9775,9 @@ var browserPressTool = {
9634
9775
  description: "Press a keyboard key in an owned browser session.",
9635
9776
  usageHint: "browser_press({ sessionId, key })",
9636
9777
  permission: "confirm",
9778
+ // WS-046: the element/expression acted on, not the sessionId — a trust
9779
+ // rule must distinguish clicking one control from another.
9780
+ subjectKey: "key",
9637
9781
  mutating: true,
9638
9782
  riskTier: "standard",
9639
9783
  capabilities: [ToolCapabilities.NET_OUTBOUND],
@@ -9652,6 +9796,9 @@ var browserDragTool = {
9652
9796
  description: "Drag one element to another in an owned browser session.",
9653
9797
  usageHint: "browser_drag({ sessionId, from, to })",
9654
9798
  permission: "confirm",
9799
+ // WS-046: the element/expression acted on, not the sessionId — a trust
9800
+ // rule must distinguish clicking one control from another.
9801
+ subjectKey: "from",
9655
9802
  mutating: true,
9656
9803
  riskTier: "standard",
9657
9804
  capabilities: [ToolCapabilities.NET_OUTBOUND],
@@ -9701,6 +9848,9 @@ var browserEvaluateTool = {
9701
9848
  description: "Evaluate bounded JavaScript in the page. Requires confirmation because page code is arbitrary.",
9702
9849
  usageHint: "browser_evaluate({ sessionId, expression })",
9703
9850
  permission: "confirm",
9851
+ // WS-046: the element/expression acted on, not the sessionId — a trust
9852
+ // rule must distinguish clicking one control from another.
9853
+ subjectKey: "expression",
9704
9854
  mutating: true,
9705
9855
  riskTier: "destructive",
9706
9856
  capabilities: [ToolCapabilities.NET_OUTBOUND, ToolCapabilities.TOOL_MUTATE_ANY],
@@ -9723,6 +9873,9 @@ var browserUploadTool = {
9723
9873
  description: "Upload project-local files through a file input in an owned browser session.",
9724
9874
  usageHint: "browser_upload({ sessionId, selector, files })",
9725
9875
  permission: "confirm",
9876
+ // WS-046: the element/expression acted on, not the sessionId — a trust
9877
+ // rule must distinguish clicking one control from another.
9878
+ subjectKey: "selector",
9726
9879
  mutating: true,
9727
9880
  riskTier: "destructive",
9728
9881
  capabilities: [ToolCapabilities.NET_OUTBOUND, ToolCapabilities.FS_READ],
@@ -10658,6 +10811,7 @@ var REFS_INDEX_SQL = [
10658
10811
  var SYMBOLS_FTS_SQL = "CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5(text, tokenize = 'unicode61')";
10659
10812
 
10660
10813
  // src/codebase-index/writer-search-helpers.ts
10814
+ var SEARCH_CANDIDATE_SCAN_CAP = 5e3;
10661
10815
  function normalizeSearchLimit(limit) {
10662
10816
  return typeof limit === "number" && Number.isFinite(limit) ? Math.max(0, Math.trunc(limit)) : void 0;
10663
10817
  }
@@ -10687,8 +10841,8 @@ function buildWriterSearchWhere(query, filter) {
10687
10841
  }
10688
10842
  if (query.trim()) {
10689
10843
  const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
10690
- conditions.push(`(${tokens.map(() => "text LIKE ?").join(" OR ")})`);
10691
- for (const token of tokens) values.push(`%${token}%`);
10844
+ conditions.push(`(${tokens.map(() => "text LIKE ? ESCAPE '\\'").join(" OR ")})`);
10845
+ for (const token of tokens) values.push(`%${escapeLike(token)}%`);
10692
10846
  }
10693
10847
  return { where: conditions.length ? `WHERE ${conditions.join(" AND ")}` : "", values };
10694
10848
  }
@@ -10797,6 +10951,7 @@ var StorePool = class {
10797
10951
 
10798
10952
  // src/codebase-index/writer.ts
10799
10953
  var DB_FILE2 = "index.db";
10954
+ var MAX_STATEMENT_CACHE = 128;
10800
10955
  var IndexStore = class _IndexStore {
10801
10956
  db;
10802
10957
  /** Absolute path to this project's index directory. */
@@ -10834,12 +10989,31 @@ var IndexStore = class _IndexStore {
10834
10989
  // Dirty on open so the first getOrBuildBm25() rebuilds against current rows;
10835
10990
  // an empty or pre-existing corpus makes a stale IDF table meaningless.
10836
10991
  bm25Dirty = true;
10837
- /** Prepare-once helper: compile `sql` on first use, reuse thereafter. */
10992
+ /**
10993
+ * Prepare-once helper: compile `sql` on first use, reuse thereafter.
10994
+ *
10995
+ * Bounded LRU rather than an open Map. The cache is keyed by SQL TEXT, and
10996
+ * the fallback search builder emits one `text LIKE ?` clause per query token
10997
+ * — so the SQL varies with the token count and a stream of differently-sized
10998
+ * queries grew the cache without limit. Sage's store already bounds its
10999
+ * equivalent at 128 (WS-096).
11000
+ *
11001
+ * Re-inserting on a hit keeps the hot fixed-SQL statements (upsertFile,
11002
+ * insertRefs, …) at the young end, so a burst of one-off search SQL evicts
11003
+ * itself rather than the reindex hot path.
11004
+ */
10838
11005
  stmt(sql) {
10839
- let s = this.stmtCache.get(sql);
10840
- if (s === void 0) {
10841
- s = this.db.prepare(sql);
10842
- this.stmtCache.set(sql, s);
11006
+ const cached = this.stmtCache.get(sql);
11007
+ if (cached !== void 0) {
11008
+ this.stmtCache.delete(sql);
11009
+ this.stmtCache.set(sql, cached);
11010
+ return cached;
11011
+ }
11012
+ const s = this.db.prepare(sql);
11013
+ this.stmtCache.set(sql, s);
11014
+ if (this.stmtCache.size > MAX_STATEMENT_CACHE) {
11015
+ const oldest = this.stmtCache.keys().next();
11016
+ if (!oldest.done) this.stmtCache.delete(oldest.value);
10843
11017
  }
10844
11018
  return s;
10845
11019
  }
@@ -11112,7 +11286,7 @@ var IndexStore = class _IndexStore {
11112
11286
  const { where, values } = built;
11113
11287
  const limit = normalizeSearchLimit(opts?.limit);
11114
11288
  const limitSql = limit !== void 0 ? " LIMIT ?" : "";
11115
- const sql = `SELECT id, lang, kind, name, file, line, col, signature, doc_comment, text FROM symbols ${where}${limitSql}`;
11289
+ const sql = `SELECT id, lang, kind, name, file, line, col, signature, doc_comment FROM symbols ${where}${limitSql}`;
11116
11290
  const binds = limit !== void 0 ? [...values, limit] : values;
11117
11291
  const rows = this.stmt(sql).all(
11118
11292
  ...binds
@@ -11241,11 +11415,13 @@ var IndexStore = class _IndexStore {
11241
11415
  /** Legacy ranked path: LIKE candidates + in-process BM25 + JS snippets. */
11242
11416
  searchRankedFallback(query, filter, limit) {
11243
11417
  if (!query.trim()) {
11244
- const total = this.countSearch(query, filter);
11245
- if (total === 0) return { results: [], total: 0 };
11246
- return { results: this.search(query, filter, { limit }), total };
11418
+ const total2 = this.countSearch(query, filter);
11419
+ if (total2 === 0) return { results: [], total: 0 };
11420
+ return { results: this.search(query, filter, { limit }), total: total2 };
11247
11421
  }
11248
- const candidates = this.search(query, filter);
11422
+ const total = this.countSearch(query, filter);
11423
+ if (total === 0) return { results: [], total: 0 };
11424
+ const candidates = this.search(query, filter, { limit: SEARCH_CANDIDATE_SCAN_CAP });
11249
11425
  if (candidates.length === 0) return { results: [], total: 0 };
11250
11426
  const candidateById = new Map(candidates.map((c) => [c.id, c]));
11251
11427
  const bm25 = this.getOrBuildBm25();
@@ -11271,7 +11447,7 @@ var IndexStore = class _IndexStore {
11271
11447
  const c = expectDefined2(candidateById.get(id));
11272
11448
  return { ...c, score, snippet: bm25.extractSnippet(id, qTokens) };
11273
11449
  });
11274
- return { results, total: candidates.length };
11450
+ return { results, total };
11275
11451
  }
11276
11452
  getAllIndexable() {
11277
11453
  return getAllIndexableWithStatement((sql) => this.stmt(sql));
@@ -11690,12 +11866,12 @@ var buildIdCache;
11690
11866
  function projectIndexServerBuildId(entrypoint) {
11691
11867
  const file = entrypoint instanceof URL || entrypoint.startsWith("file:") ? fileURLToPath(entrypoint) : path16.resolve(entrypoint);
11692
11868
  try {
11693
- const stat17 = fs11.statSync(file);
11694
- if (buildIdCache?.file === file && buildIdCache.mtimeMs === stat17.mtimeMs && buildIdCache.size === stat17.size) {
11869
+ const stat18 = fs11.statSync(file);
11870
+ if (buildIdCache?.file === file && buildIdCache.mtimeMs === stat18.mtimeMs && buildIdCache.size === stat18.size) {
11695
11871
  return buildIdCache.buildId;
11696
11872
  }
11697
11873
  const buildId = createHash4("sha256").update(fs11.readFileSync(file)).digest("hex").slice(0, 24);
11698
- buildIdCache = { file, mtimeMs: stat17.mtimeMs, size: stat17.size, buildId };
11874
+ buildIdCache = { file, mtimeMs: stat18.mtimeMs, size: stat18.size, buildId };
11699
11875
  return buildId;
11700
11876
  } catch {
11701
11877
  return `unreadable:${path16.basename(file)}`;
@@ -11865,6 +12041,8 @@ var ProjectServerConnection = class {
11865
12041
  connectResolve = null;
11866
12042
  connectReject = null;
11867
12043
  nextId = 1;
12044
+ /** WS-027: read from the owner-only metadata file — see currentAuthToken. */
12045
+ authToken;
11868
12046
  pending = /* @__PURE__ */ new Map();
11869
12047
  transition(status, options = {}) {
11870
12048
  const previous = connectionStates.get(this.endpoint);
@@ -12001,6 +12179,31 @@ var ProjectServerConnection = class {
12001
12179
  this.transition("offline");
12002
12180
  maybeStopHeartbeatLoop();
12003
12181
  }
12182
+ /**
12183
+ * WS-027: the per-process token, read from the daemon's owner-only metadata
12184
+ * file — never from the `hello` frame, which the daemon sends to every
12185
+ * socket that connects (the mistake WS-028 found in the SAGE daemon).
12186
+ *
12187
+ * Read lazily and re-read while unknown: the daemon starts listening before
12188
+ * it writes metadata (endpoint ownership has to be won first), and a
12189
+ * respawned daemon mints a new token, so a cached wrong one would be sticky.
12190
+ */
12191
+ currentAuthToken() {
12192
+ if (this.authToken === void 0) {
12193
+ try {
12194
+ const raw = fs12.readFileSync(
12195
+ projectIndexServerMetadataPath(this.projectRoot, this.indexDir),
12196
+ "utf8"
12197
+ );
12198
+ const parsed = JSON.parse(raw);
12199
+ if (typeof parsed.authToken === "string" && parsed.authToken.length > 0) {
12200
+ this.authToken = parsed.authToken;
12201
+ }
12202
+ } catch {
12203
+ }
12204
+ }
12205
+ return this.authToken;
12206
+ }
12004
12207
  request(message, options) {
12005
12208
  const socket = this.socket;
12006
12209
  if (!socket || socket.destroyed) {
@@ -12044,7 +12247,7 @@ var ProjectServerConnection = class {
12044
12247
  return;
12045
12248
  }
12046
12249
  }
12047
- this.write({ ...message, id });
12250
+ this.write({ ...message, id, authToken: this.currentAuthToken() });
12048
12251
  });
12049
12252
  }
12050
12253
  async ensureConnected(spawnIfMissing) {
@@ -12091,6 +12294,7 @@ var ProjectServerConnection = class {
12091
12294
  connectOnce() {
12092
12295
  this.socket?.destroy();
12093
12296
  this.socket = null;
12297
+ this.authToken = void 0;
12094
12298
  this.info = null;
12095
12299
  this.activity = null;
12096
12300
  this.health = null;
@@ -12700,9 +12904,9 @@ async function runIndexerWithStore(store, opts) {
12700
12904
  const statReadParse = await Promise.allSettled(
12701
12905
  batchFiles.map(
12702
12906
  async (file) => {
12703
- let stat17;
12907
+ let stat18;
12704
12908
  try {
12705
- stat17 = await fs17.stat(file, statOpts);
12909
+ stat18 = await fs17.stat(file, statOpts);
12706
12910
  } catch (e) {
12707
12911
  if (isAbortError(e)) throw e;
12708
12912
  return {
@@ -12714,21 +12918,21 @@ async function runIndexerWithStore(store, opts) {
12714
12918
  missing: isMissingPathError(e)
12715
12919
  };
12716
12920
  }
12717
- if (!stat17.isFile()) return { file, stat: stat17, lang: "", parsed: null };
12921
+ if (!stat18.isFile()) return { file, stat: stat18, lang: "", parsed: null };
12718
12922
  const lang = detectLang(file);
12719
- if (!lang) return { file, stat: stat17, lang: "", parsed: null };
12720
- if (stat17.size > MAX_INDEX_FILE_BYTES) {
12923
+ if (!lang) return { file, stat: stat18, lang: "", parsed: null };
12924
+ if (stat18.size > MAX_INDEX_FILE_BYTES) {
12721
12925
  return {
12722
12926
  file,
12723
- stat: stat17,
12927
+ stat: stat18,
12724
12928
  lang,
12725
12929
  parsed: null,
12726
- error: `file too large (${stat17.size} bytes; max ${MAX_INDEX_FILE_BYTES})`
12930
+ error: `file too large (${stat18.size} bytes; max ${MAX_INDEX_FILE_BYTES})`
12727
12931
  };
12728
12932
  }
12729
12933
  const meta = existingMeta.get(file);
12730
- if (!force && meta && meta.mtimeMs === Math.floor(stat17.mtimeMs)) {
12731
- return { file, stat: stat17, lang, parsed: null, skippedMeta: meta };
12934
+ if (!force && meta && meta.mtimeMs === Math.floor(stat18.mtimeMs)) {
12935
+ return { file, stat: stat18, lang, parsed: null, skippedMeta: meta };
12732
12936
  }
12733
12937
  let content;
12734
12938
  try {
@@ -12737,7 +12941,7 @@ async function runIndexerWithStore(store, opts) {
12737
12941
  if (isAbortError(e)) throw e;
12738
12942
  return {
12739
12943
  file,
12740
- stat: stat17,
12944
+ stat: stat18,
12741
12945
  lang,
12742
12946
  parsed: null,
12743
12947
  error: `read error: ${e instanceof Error ? e.message : String(e)}`
@@ -12749,13 +12953,13 @@ async function runIndexerWithStore(store, opts) {
12749
12953
  } catch (e) {
12750
12954
  return {
12751
12955
  file,
12752
- stat: stat17,
12956
+ stat: stat18,
12753
12957
  lang,
12754
12958
  parsed: null,
12755
12959
  error: `parse error: ${e instanceof Error ? e.message : String(e)}`
12756
12960
  };
12757
12961
  }
12758
- return { file, stat: stat17, lang, parsed, content };
12962
+ return { file, stat: stat18, lang, parsed, content };
12759
12963
  }
12760
12964
  )
12761
12965
  );
@@ -12776,7 +12980,7 @@ async function runIndexerWithStore(store, opts) {
12776
12980
  errors.push(`${file}: ${result.error}`);
12777
12981
  continue;
12778
12982
  }
12779
- const { stat: stat17, lang, parsed } = result;
12983
+ const { stat: stat18, lang, parsed } = result;
12780
12984
  if (result.skippedMeta) {
12781
12985
  langStats[lang] = (langStats[lang] ?? 0) + result.skippedMeta.symbolCount;
12782
12986
  symbolsIndexed += result.skippedMeta.symbolCount;
@@ -12788,7 +12992,7 @@ async function runIndexerWithStore(store, opts) {
12788
12992
  store.upsertFile({
12789
12993
  file,
12790
12994
  lang,
12791
- mtimeMs: Math.floor(stat17.mtimeMs),
12995
+ mtimeMs: Math.floor(stat18.mtimeMs),
12792
12996
  symbolCount: 0,
12793
12997
  lastIndexed: Date.now()
12794
12998
  });
@@ -12800,7 +13004,7 @@ async function runIndexerWithStore(store, opts) {
12800
13004
  store.replaceEmptyFile({
12801
13005
  file,
12802
13006
  lang,
12803
- mtimeMs: Math.floor(stat17.mtimeMs),
13007
+ mtimeMs: Math.floor(stat18.mtimeMs),
12804
13008
  symbolCount: 0,
12805
13009
  lastIndexed: Date.now()
12806
13010
  });
@@ -12812,7 +13016,7 @@ async function runIndexerWithStore(store, opts) {
12812
13016
  lang,
12813
13017
  symbols: parsed.symbols,
12814
13018
  refs: parsed.refs ?? [],
12815
- mtimeMs: Math.floor(stat17.mtimeMs),
13019
+ mtimeMs: Math.floor(stat18.mtimeMs),
12816
13020
  symbolCount: parsed.symbols.length
12817
13021
  });
12818
13022
  deleteForFiles.push(file);
@@ -13865,8 +14069,9 @@ function runDeadCodeScan(projectRoot, opts = {}) {
13865
14069
  const content = fs19.readFileSync(epFile, "utf8");
13866
14070
  const strippedContent = content.replace(/\/\*[\s\S]*?\*\//g, (m) => " ".repeat(m.length)).replace(/\/\/[^\n]*/g, (m) => " ".repeat(m.length));
13867
14071
  const reExportRe = /export\s+(?:(?:type\s+)?\{[\s\S]*?\}\s+from|\*\s+as\s+\w+\s+from|\*\s+from)\s+['"]([^'"]+)['"]/g;
13868
- let match;
13869
- while ((match = reExportRe.exec(strippedContent)) !== null) {
14072
+ for (; ; ) {
14073
+ const match = reExportRe.exec(strippedContent);
14074
+ if (match === null) break;
13870
14075
  const moduleSpec = match[1];
13871
14076
  const resolvedFiles = resolveModulePath(epFile, moduleSpec, indexedFiles);
13872
14077
  for (const rf of resolvedFiles) {
@@ -14072,6 +14277,9 @@ var designTool = {
14072
14277
  description: 'Browse, load, customize, and enforce curated frontend/mobile UI design kits. Use BEFORE writing UI code to commit to one coherent, modern, responsive, dark/light, accessible design. Actions: "list" (menu), "use" (load+pin a kit for a stack), "foundations" (baseline), "set" (override kit colors/tokens), "materialize" (write the tokens to a real theme file \u2014 CSS @theme/OKLCH or native), "verify" (scan UI files for off-palette colors).',
14073
14278
  usageHint: 'Flow: `design {action:"use", kit:"minimal-clarity", stack:"web"}` \u2192 optionally `design {action:"set", set:{primary:"oklch(62% 0.2 25)"}}` \u2192 `design {action:"materialize"}` to write tokens to disk \u2192 implement against them \u2192 `design {action:"verify"}`.',
14074
14279
  permission: "confirm",
14280
+ // WS-046: gives permission decisions something to key on.
14281
+ // The action performed; `out` is optional so it cannot be the subject.
14282
+ subjectKey: "action",
14075
14283
  mutating: true,
14076
14284
  capabilities: ["fs.write"],
14077
14285
  timeoutMs: 15e3,
@@ -14252,9 +14460,7 @@ Run \`design {action:"materialize"}\` to write the tuned tokens to your theme fi
14252
14460
  const abs = path25.join(absParent, path25.basename(absResolved));
14253
14461
  const rel = path25.relative(root, abs);
14254
14462
  if (rel.startsWith("..") || path25.isAbsolute(rel)) {
14255
- throw new Error(
14256
- `design: materialize path "${result.path}" would escape the project root`
14257
- );
14463
+ throw new Error(`design: materialize path "${result.path}" would escape the project root`);
14258
14464
  }
14259
14465
  let exists = false;
14260
14466
  try {
@@ -14329,6 +14535,7 @@ import { statSync as statSync3 } from "node:fs";
14329
14535
  import * as fs21 from "node:fs/promises";
14330
14536
  import * as path26 from "node:path";
14331
14537
  import { buildChildEnv as buildChildEnv3 } from "@wrongstack/core/utils";
14538
+ var MAX_FILE_DUMP_BYTES = 5 * 1024 * 1024;
14332
14539
  var diffTool = {
14333
14540
  name: "diff",
14334
14541
  category: "Filesystem",
@@ -14336,6 +14543,7 @@ var diffTool = {
14336
14543
  usageHint: 'USE FOR CODE REVIEW AND CHANGE INSPECTION:\n\n- `files` + no `a`/`b` \u2192 show file content with line numbers (NOT a unified diff; no +/- prefixes).\n- `a` and/or `b` \u2192 git-style commit/branch diff (unified format, real +/- prefixes).\n- `staged: true` \u2192 only show staged changes.\n- `mode` can be "unified", "stat", or "side-by-side" (only affects the git-diff path).\n\nNOTE: For a true file-vs-file unified diff, supply `a` and `b` so the tool delegates to `git diff`. The `files`-only path is a line-numbered dump, not a diff.\n\nThis tool has important safety guards against flag injection (see previous security findings).',
14337
14544
  permission: "auto",
14338
14545
  mutating: false,
14546
+ maxOutputBytes: 262144,
14339
14547
  capabilities: ["fs.read"],
14340
14548
  icon: "diff",
14341
14549
  timeoutMs: 1e4,
@@ -14411,8 +14619,8 @@ function findGitDir(cwd) {
14411
14619
  let dir = cwd;
14412
14620
  for (let i = 0; i < 20; i++) {
14413
14621
  try {
14414
- const stat17 = statSync3(path26.join(dir, ".git"));
14415
- if (stat17.isDirectory()) return dir;
14622
+ const stat18 = statSync3(path26.join(dir, ".git"));
14623
+ if (stat18.isDirectory()) return dir;
14416
14624
  } catch {
14417
14625
  }
14418
14626
  const parent = path26.dirname(dir);
@@ -14454,10 +14662,18 @@ async function fileDiff(input, ctx, _signal) {
14454
14662
  };
14455
14663
  }
14456
14664
  const results = [];
14665
+ let truncated = false;
14457
14666
  for (const file of files) {
14458
14667
  const absPath = safeResolve(file, ctx);
14459
- const stat17 = await fs21.stat(absPath).catch(() => null);
14460
- if (!stat17?.isFile()) continue;
14668
+ const stat18 = await fs21.stat(absPath).catch(() => null);
14669
+ if (!stat18?.isFile()) continue;
14670
+ if (stat18.size > MAX_FILE_DUMP_BYTES) {
14671
+ truncated = true;
14672
+ results.push(
14673
+ `--- ${file} (skipped: ${stat18.size} bytes exceeds the ${MAX_FILE_DUMP_BYTES} limit; use the read tool with offset/limit) ---`
14674
+ );
14675
+ continue;
14676
+ }
14461
14677
  const content = await fs21.readFile(absPath, "utf8");
14462
14678
  const lines = content.split(/\r?\n/);
14463
14679
  results.push(formatWithLineNumbers(file, lines));
@@ -14465,7 +14681,7 @@ async function fileDiff(input, ctx, _signal) {
14465
14681
  return {
14466
14682
  diff: results.join("\n\n"),
14467
14683
  files,
14468
- truncated: false,
14684
+ truncated,
14469
14685
  mode: input.mode ?? "unified"
14470
14686
  };
14471
14687
  }
@@ -14558,8 +14774,8 @@ async function resolveFiles(filesInput, cwd) {
14558
14774
  for (const f of files) {
14559
14775
  const absPath = f.trim().startsWith("/") ? f.trim() : `${cwd}/${f.trim()}`;
14560
14776
  try {
14561
- const stat17 = await fs22.stat(absPath);
14562
- if (stat17.isFile()) resolved.push(absPath);
14777
+ const stat18 = await fs22.stat(absPath);
14778
+ if (stat18.isFile()) resolved.push(absPath);
14563
14779
  } catch {
14564
14780
  }
14565
14781
  }
@@ -15412,7 +15628,7 @@ var editTool = {
15412
15628
  });
15413
15629
  }
15414
15630
  const absPath = await safeResolveReal(input.path, ctx);
15415
- const stat17 = await fs23.stat(absPath).catch((err) => {
15631
+ const stat18 = await fs23.stat(absPath).catch((err) => {
15416
15632
  if (err.code === "ENOENT") {
15417
15633
  throw new ToolValidationError({
15418
15634
  message: `edit: file "${input.path}" does not exist. Use \`write\` instead.`,
@@ -15422,7 +15638,7 @@ var editTool = {
15422
15638
  }
15423
15639
  throw err;
15424
15640
  });
15425
- if (!stat17.isFile()) {
15641
+ if (!stat18.isFile()) {
15426
15642
  throw new ToolValidationError({
15427
15643
  message: `edit: "${input.path}" is not a regular file`,
15428
15644
  field: "path"
@@ -15452,7 +15668,7 @@ var editTool = {
15452
15668
  });
15453
15669
  }
15454
15670
  }
15455
- if (autoRead && updated.mtimeMs > stat17.mtimeMs + mtimeTolerance) {
15671
+ if (autoRead && updated.mtimeMs > stat18.mtimeMs + mtimeTolerance) {
15456
15672
  throw new ToolValidationError({
15457
15673
  message: `edit: file "${input.path}" changed while being auto-read. Retry the edit.`,
15458
15674
  field: "path",
@@ -15575,10 +15791,6 @@ import {
15575
15791
  emitProcessStarted as emitProcessStarted3
15576
15792
  } from "@wrongstack/core/observability";
15577
15793
  import { toErrorMessage as toErrorMessage3 } from "@wrongstack/core/utils/error";
15578
- init_output_spool();
15579
- init_util();
15580
- init_process_registry();
15581
- init_win32_resolve();
15582
15794
 
15583
15795
  // src/_danger-detect.ts
15584
15796
  var argHas = (args, value) => args.includes(value);
@@ -15861,6 +16073,11 @@ function levelRank(level) {
15861
16073
  }
15862
16074
  }
15863
16075
 
16076
+ // src/exec.ts
16077
+ init_output_spool();
16078
+ init_util();
16079
+ init_win32_resolve();
16080
+
15864
16081
  // src/exec-kill-guard.ts
15865
16082
  import * as os8 from "node:os";
15866
16083
  import * as path29 from "node:path";
@@ -16097,6 +16314,7 @@ async function checkKillTarget(target) {
16097
16314
  }
16098
16315
 
16099
16316
  // src/exec.ts
16317
+ init_process_registry();
16100
16318
  var isWin4 = process.platform === "win32";
16101
16319
  var DEFAULT_ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
16102
16320
  // JS / TS toolchain
@@ -16733,7 +16951,17 @@ var BLOCKED_ARG_PATTERNS = {
16733
16951
  bun: [],
16734
16952
  docker: [],
16735
16953
  // find -exec/-ok/-execdir execute arbitrary commands
16736
- find: [/^-exec$/, /^-exec;$/, /^-ok$/, /^-ok;$/, /^-execdir$/, /^-execdir;$/, /^-exec=/, /^-ok=/, /^-execdir=/],
16954
+ find: [
16955
+ /^-exec$/,
16956
+ /^-exec;$/,
16957
+ /^-ok$/,
16958
+ /^-ok;$/,
16959
+ /^-execdir$/,
16960
+ /^-execdir;$/,
16961
+ /^-exec=/,
16962
+ /^-ok=/,
16963
+ /^-execdir=/
16964
+ ],
16737
16965
  // rm -rf / is catastrophic — block absolute paths, home, dot-dirs,
16738
16966
  // and glob patterns that could expand to dangerous targets.
16739
16967
  // `rm -rf ./src/*` expands to project files; `rm -rf ../../` escapes upward;
@@ -16804,6 +17032,14 @@ var execTool = {
16804
17032
  useInstead: ["bash"]
16805
17033
  },
16806
17034
  permission: "confirm",
17035
+ // WS-046: without this, every exec call collapsed onto the bare tool name.
17036
+ // Three consequences followed: "always allow" stored a rule that could never
17037
+ // match, pressing "no" once blocked ALL exec for the session, and the
17038
+ // permission cache keyed one decision for every command. `subjectKey:
17039
+ // 'command'` renders the FULL invocation (`command` + `args`) — using the
17040
+ // program alone would make `exec git status` and `exec git push --force` the
17041
+ // same subject, so trusting one would silently authorize the other.
17042
+ subjectKey: "command",
16807
17043
  mutating: true,
16808
17044
  riskTier: "standard",
16809
17045
  timeoutMs: DEFAULT_TIMEOUT_MS3,
@@ -17020,7 +17256,14 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
17020
17256
  const pid = child.pid;
17021
17257
  if (typeof pid === "number") {
17022
17258
  const fullCommand = `${cmd} ${args.join(" ")}`;
17023
- registry.register({ pid, name: "exec", command: redactCommand(fullCommand), startedAt: Date.now(), sessionId, child });
17259
+ registry.register({
17260
+ pid,
17261
+ name: "exec",
17262
+ command: redactCommand(fullCommand),
17263
+ startedAt: Date.now(),
17264
+ sessionId,
17265
+ child
17266
+ });
17024
17267
  }
17025
17268
  const timer = setTimeout(() => {
17026
17269
  killed = true;
@@ -17460,6 +17703,9 @@ var formatTool = {
17460
17703
  description: "Format source files according to project style (Biome). Can also run in check-only mode.",
17461
17704
  usageHint: "RUN REGULARLY:\n\n- Use on changed files before committing.\n- `check: true` verifies formatting without making changes (useful in CI-like flows).\nThis project has very consistent formatting expectations. Always ensure your changes are formatted.",
17462
17705
  permission: "confirm",
17706
+ // WS-046: gives permission decisions something to key on.
17707
+ // The files being rewritten are the subject; the fixer is how, not what.
17708
+ subjectKey: "files",
17463
17709
  mutating: true,
17464
17710
  capabilities: ["fs.write", "shell.restricted"],
17465
17711
  icon: "code",
@@ -17564,13 +17810,13 @@ var formatTool = {
17564
17810
  }
17565
17811
  };
17566
17812
  async function detectFixer(cwd) {
17567
- const { stat: stat17 } = await import("node:fs/promises");
17813
+ const { stat: stat18 } = await import("node:fs/promises");
17568
17814
  try {
17569
- await stat17(`${cwd}/biome.json`);
17815
+ await stat18(`${cwd}/biome.json`);
17570
17816
  return "biome";
17571
17817
  } catch {
17572
17818
  try {
17573
- await stat17(`${cwd}/.prettierrc`);
17819
+ await stat18(`${cwd}/.prettierrc`);
17574
17820
  return "prettier";
17575
17821
  } catch {
17576
17822
  return "biome";
@@ -17597,6 +17843,10 @@ var gitTool = {
17597
17843
  // Conservative: any of these may mutate. The non-mutating commands
17598
17844
  // (status/log/diff/branch/fetch) are still gated on `permission: 'confirm'`
17599
17845
  // and `MUTATING_SUBCOMMANDS` is consulted at runtime for per-call checks.
17846
+ // WS-046: gives permission decisions something to key on.
17847
+ // The git subcommand (status/commit/push) is what a trust rule needs to
17848
+ // distinguish; `git status` and `git push --force` must not share a subject.
17849
+ subjectKey: "command",
17600
17850
  mutating: true,
17601
17851
  capabilities: ["fs.write", "shell.restricted"],
17602
17852
  timeoutMs: TIMEOUT_MS2,
@@ -17665,6 +17915,8 @@ var gitTool = {
17665
17915
  truncated: false
17666
17916
  };
17667
17917
  }
17918
+ const branchGuard = validateBranchInput(input);
17919
+ if (branchGuard) return branchGuard;
17668
17920
  if (input.command === "worktree") {
17669
17921
  const guard = validateWorktreeInput(input, ctx.projectRoot);
17670
17922
  if (guard) return guard;
@@ -17713,6 +17965,18 @@ var gitTool = {
17713
17965
  return result;
17714
17966
  }
17715
17967
  };
17968
+ function validateBranchInput(input) {
17969
+ const branch = input.branch;
17970
+ if (branch === void 0) return null;
17971
+ if (!branch.startsWith("-") && !branch.includes(" --")) return null;
17972
+ return {
17973
+ command: input.command,
17974
+ stdout: "",
17975
+ stderr: `unsafe branch name (parsed as a git option): ${branch}`,
17976
+ exitCode: 1,
17977
+ truncated: false
17978
+ };
17979
+ }
17716
17980
  function validateWorktreeInput(input, projectRoot) {
17717
17981
  const reject = (stderr) => ({
17718
17982
  command: "worktree",
@@ -17721,7 +17985,6 @@ function validateWorktreeInput(input, projectRoot) {
17721
17985
  exitCode: 1,
17722
17986
  truncated: false
17723
17987
  });
17724
- if (input.branch?.startsWith("-")) return reject(`unsafe branch name: ${input.branch}`);
17725
17988
  if (input.worktreePath?.startsWith("-")) {
17726
17989
  return reject(`unsafe worktree path: ${input.worktreePath}`);
17727
17990
  }
@@ -17739,8 +18002,8 @@ function findGitDir2(cwd, projectRoot) {
17739
18002
  let dir = cwd;
17740
18003
  for (let i = 0; i < 20; i++) {
17741
18004
  try {
17742
- const stat17 = statSync4(`${dir}/.git`);
17743
- if (stat17.isDirectory() || stat17.isFile()) return dir;
18005
+ const stat18 = statSync4(`${dir}/.git`);
18006
+ if (stat18.isDirectory() || stat18.isFile()) return dir;
17744
18007
  } catch {
17745
18008
  }
17746
18009
  if (dir === root) break;
@@ -17775,7 +18038,10 @@ function buildArgs(input) {
17775
18038
  ...files.length ? ["--", ...files] : []
17776
18039
  ];
17777
18040
  case "branch":
17778
- return input.branch ? ["branch", ...input.branch.startsWith("-") || input.branch.includes(" --") ? [] : [input.branch]] : ["branch"];
18041
+ return input.branch ? [
18042
+ "branch",
18043
+ ...input.branch.startsWith("-") || input.branch.includes(" --") ? [] : [input.branch]
18044
+ ] : ["branch"];
17779
18045
  case "checkout":
17780
18046
  return [
17781
18047
  "checkout",
@@ -17868,7 +18134,7 @@ import * as path30 from "node:path";
17868
18134
  import { compileGlob as compileGlob2, DEFAULT_WALK_IGNORE_DIRS as DEFAULT_WALK_IGNORE_DIRS2 } from "@wrongstack/core/utils";
17869
18135
 
17870
18136
  // src/_concurrency.ts
17871
- async function mapWithConcurrency(items, limit, fn) {
18137
+ async function mapWithConcurrency2(items, limit, fn) {
17872
18138
  if (items.length === 0) return [];
17873
18139
  const effectiveLimit = Math.max(1, Math.min(limit | 0, items.length));
17874
18140
  const results = new Array(items.length);
@@ -18003,10 +18269,10 @@ var globTool = {
18003
18269
  }
18004
18270
  if (truncated) return;
18005
18271
  }
18006
- await mapWithConcurrency(matchedFiles, WALK_CONCURRENCY, pushResult);
18272
+ await mapWithConcurrency2(matchedFiles, WALK_CONCURRENCY, pushResult);
18007
18273
  if (truncated) return;
18008
18274
  const remainingSubdirs = truncated ? [] : subdirs;
18009
- await mapWithConcurrency(remainingSubdirs, WALK_CONCURRENCY, ({ full, rel }) => walk(full, rel));
18275
+ await mapWithConcurrency2(remainingSubdirs, WALK_CONCURRENCY, ({ full, rel }) => walk(full, rel));
18010
18276
  };
18011
18277
  await walk(base, "");
18012
18278
  results.sort((a, b) => b.mtime - a.mtime);
@@ -18015,12 +18281,16 @@ var globTool = {
18015
18281
  };
18016
18282
 
18017
18283
  // src/grep.ts
18018
- import { expectDefined as expectDefined7 } from "@wrongstack/core/utils";
18019
18284
  import { spawn as spawn11 } from "node:child_process";
18020
18285
  import * as fs25 from "node:fs/promises";
18021
18286
  import * as path31 from "node:path";
18022
18287
  import { ToolValidationError as ToolValidationError4 } from "@wrongstack/core/types";
18023
- import { buildChildEnv as buildChildEnv5, compileGlob as compileGlob3, DEFAULT_WALK_IGNORE_DIRS as DEFAULT_WALK_IGNORE_DIRS3 } from "@wrongstack/core/utils";
18288
+ import {
18289
+ buildChildEnv as buildChildEnv5,
18290
+ compileGlob as compileGlob3,
18291
+ DEFAULT_WALK_IGNORE_DIRS as DEFAULT_WALK_IGNORE_DIRS3,
18292
+ expectDefined as expectDefined7
18293
+ } from "@wrongstack/core/utils";
18024
18294
 
18025
18295
  // src/_regex.ts
18026
18296
  var MAX_PATTERN_LEN = 256;
@@ -18073,6 +18343,8 @@ var DEFAULT_IGNORE3 = DEFAULT_WALK_IGNORE_DIRS3;
18073
18343
  var NATIVE_SCAN_CONCURRENCY = 32;
18074
18344
  var NATIVE_READ_CHUNK_BYTES = 64 * 1024;
18075
18345
  var NATIVE_MAX_FILE_BYTES = 1e6;
18346
+ var RG_MAX_QUEUE_CHUNKS = 128;
18347
+ var RG_MAX_QUEUE_CHARS = 8 * 1024 * 1024;
18076
18348
  var grepTool = {
18077
18349
  name: "grep",
18078
18350
  category: "Search",
@@ -18200,9 +18472,19 @@ async function* runRgStream(input, base, mode, limit, signal) {
18200
18472
  const FLUSH_AT = 16;
18201
18473
  const MAX_BUF_BYTES = 1e6;
18202
18474
  let bufOverflow = false;
18203
- const child = spawn11("rg", args, { signal, env: buildChildEnv5(), stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
18475
+ const child = spawn11("rg", args, {
18476
+ signal,
18477
+ env: buildChildEnv5(),
18478
+ // rg diagnostics are not part of the tool result. Ignoring stderr avoids
18479
+ // an unread pipe filling up and pinning the child when rg reports many
18480
+ // filesystem errors.
18481
+ stdio: ["ignore", "pipe", "ignore"],
18482
+ windowsHide: true
18483
+ });
18204
18484
  const queue = [];
18485
+ let queuedChars = 0;
18205
18486
  let waiter;
18487
+ let paused = false;
18206
18488
  const wake = () => {
18207
18489
  if (waiter) {
18208
18490
  const w = waiter;
@@ -18210,93 +18492,133 @@ async function* runRgStream(input, base, mode, limit, signal) {
18210
18492
  w();
18211
18493
  }
18212
18494
  };
18213
- child.stdout?.on("data", (c) => {
18214
- queue.push({ kind: "out", data: c.toString() });
18495
+ const pauseIfFlooded = () => {
18496
+ if (paused || queue.length < RG_MAX_QUEUE_CHUNKS && queuedChars < RG_MAX_QUEUE_CHARS)
18497
+ return;
18498
+ paused = true;
18499
+ child.stdout?.pause();
18500
+ };
18501
+ const resumeIfDrained = () => {
18502
+ if (!paused || queue.length >= RG_MAX_QUEUE_CHUNKS || queuedChars >= RG_MAX_QUEUE_CHARS)
18503
+ return;
18504
+ paused = false;
18505
+ child.stdout?.resume();
18506
+ };
18507
+ const onStdoutData = (c) => {
18508
+ const data = c.toString();
18509
+ queue.push({ kind: "out", data });
18510
+ queuedChars += data.length;
18215
18511
  wake();
18216
- });
18217
- child.on("error", (e) => {
18512
+ pauseIfFlooded();
18513
+ };
18514
+ const onError = (e) => {
18218
18515
  queue.push({ kind: "error", data: e.message });
18219
18516
  wake();
18220
- });
18221
- child.on("close", () => {
18517
+ };
18518
+ const onClose = () => {
18222
18519
  queue.push({ kind: "close", data: "" });
18223
18520
  wake();
18224
- });
18521
+ };
18522
+ child.stdout?.on("data", onStdoutData);
18523
+ child.on("error", onError);
18524
+ child.on("close", onClose);
18225
18525
  let pendingBatch = [];
18226
18526
  let errored = false;
18227
- for (; ; ) {
18228
- while (queue.length === 0) {
18229
- await new Promise((r) => {
18230
- waiter = r;
18231
- });
18232
- }
18233
- const c = expectDefined7(queue.shift());
18234
- if (c.kind === "error") {
18235
- errored = true;
18236
- continue;
18237
- }
18238
- if (c.kind === "close") break;
18239
- buf += c.data;
18240
- if (buf.length > MAX_BUF_BYTES && !bufOverflow) {
18241
- bufOverflow = true;
18242
- buf = buf.slice(-MAX_BUF_BYTES);
18243
- try {
18244
- child.kill("SIGTERM");
18245
- } catch {
18527
+ let closed = false;
18528
+ try {
18529
+ for (; ; ) {
18530
+ while (queue.length === 0) {
18531
+ await new Promise((r) => {
18532
+ waiter = r;
18533
+ });
18246
18534
  }
18247
- }
18248
- const idx = buf.lastIndexOf("\n");
18249
- if (idx === -1) continue;
18250
- const ready = buf.slice(0, idx);
18251
- buf = buf.slice(idx + 1);
18252
- for (const line of ready.split("\n")) {
18253
- if (!line) continue;
18254
- totalLines++;
18255
- if (mode === "count") totalCount += parseRgCountLine(line);
18256
- if (matches.length < limit) {
18257
- matches.push(line);
18258
- pendingBatch.push(line);
18259
- batchSinceFlush++;
18535
+ const c = expectDefined7(queue.shift());
18536
+ queuedChars = Math.max(0, queuedChars - c.data.length);
18537
+ resumeIfDrained();
18538
+ if (c.kind === "error") {
18539
+ errored = true;
18540
+ continue;
18541
+ }
18542
+ if (c.kind === "close") {
18543
+ closed = true;
18544
+ break;
18545
+ }
18546
+ buf += c.data;
18547
+ if (buf.length > MAX_BUF_BYTES && !bufOverflow) {
18548
+ bufOverflow = true;
18549
+ buf = buf.slice(-MAX_BUF_BYTES);
18550
+ try {
18551
+ child.kill("SIGTERM");
18552
+ } catch {
18553
+ }
18554
+ }
18555
+ const idx = buf.lastIndexOf("\n");
18556
+ if (idx === -1) continue;
18557
+ const ready = buf.slice(0, idx);
18558
+ buf = buf.slice(idx + 1);
18559
+ for (const line of ready.split("\n")) {
18560
+ if (!line) continue;
18561
+ totalLines++;
18562
+ if (mode === "count") totalCount += parseRgCountLine(line);
18563
+ if (matches.length < limit) {
18564
+ matches.push(line);
18565
+ pendingBatch.push(line);
18566
+ batchSinceFlush++;
18567
+ }
18568
+ }
18569
+ if (batchSinceFlush >= FLUSH_AT) {
18570
+ yield {
18571
+ type: "partial_output",
18572
+ text: pendingBatch.join("\n"),
18573
+ data: { matches_so_far: matches.length }
18574
+ };
18575
+ pendingBatch = [];
18576
+ batchSinceFlush = 0;
18577
+ }
18578
+ }
18579
+ if (buf.trim()) {
18580
+ for (const line of buf.split("\n")) {
18581
+ if (!line) continue;
18582
+ totalLines++;
18583
+ if (mode === "count") totalCount += parseRgCountLine(line);
18584
+ if (matches.length < limit) {
18585
+ matches.push(line);
18586
+ pendingBatch.push(line);
18587
+ }
18260
18588
  }
18261
18589
  }
18262
- if (batchSinceFlush >= FLUSH_AT) {
18590
+ if (pendingBatch.length > 0) {
18263
18591
  yield {
18264
18592
  type: "partial_output",
18265
18593
  text: pendingBatch.join("\n"),
18266
18594
  data: { matches_so_far: matches.length }
18267
18595
  };
18268
- pendingBatch = [];
18269
- batchSinceFlush = 0;
18270
- }
18271
- }
18272
- if (buf.trim()) {
18273
- for (const line of buf.split("\n")) {
18274
- if (!line) continue;
18275
- totalLines++;
18276
- if (mode === "count") totalCount += parseRgCountLine(line);
18277
- if (matches.length < limit) {
18278
- matches.push(line);
18279
- pendingBatch.push(line);
18280
- }
18281
18596
  }
18282
- }
18283
- if (pendingBatch.length > 0) {
18597
+ if (errored) throw new Error("rg: spawn error");
18284
18598
  yield {
18285
- type: "partial_output",
18286
- text: pendingBatch.join("\n"),
18287
- data: { matches_so_far: matches.length }
18599
+ type: "final",
18600
+ output: {
18601
+ matches,
18602
+ count: mode === "count" ? totalCount : totalLines,
18603
+ truncated: totalLines > limit || bufOverflow,
18604
+ used: "rg"
18605
+ }
18288
18606
  };
18289
- }
18290
- if (errored) throw new Error("rg: spawn error");
18291
- yield {
18292
- type: "final",
18293
- output: {
18294
- matches,
18295
- count: mode === "count" ? totalCount : totalLines,
18296
- truncated: totalLines > limit || bufOverflow,
18297
- used: "rg"
18607
+ } finally {
18608
+ child.stdout?.off("data", onStdoutData);
18609
+ child.off("error", onError);
18610
+ child.off("close", onClose);
18611
+ child.stdout?.destroy();
18612
+ queue.length = 0;
18613
+ queuedChars = 0;
18614
+ waiter = void 0;
18615
+ if (!closed && child.exitCode === null) {
18616
+ try {
18617
+ child.kill("SIGTERM");
18618
+ } catch {
18619
+ }
18298
18620
  }
18299
- };
18621
+ }
18300
18622
  }
18301
18623
  function parseRgCountLine(line) {
18302
18624
  const idx = line.lastIndexOf(":");
@@ -18326,8 +18648,8 @@ async function runNative(input, base, mode, limit, signal) {
18326
18648
  if (globRe && !globRe.test(name) && !globRe.test(full)) return;
18327
18649
  if (globRe) globRe.lastIndex = 0;
18328
18650
  try {
18329
- const stat17 = await fs25.stat(full);
18330
- if (!stat17.isFile() || stat17.size > maxBytes || stopped || signal.aborted) return;
18651
+ const stat18 = await fs25.stat(full);
18652
+ if (!stat18.isFile() || stat18.size > maxBytes || stopped || signal.aborted) return;
18331
18653
  const file = await fs25.open(full, "r");
18332
18654
  try {
18333
18655
  let bytesReadTotal = 0;
@@ -18427,8 +18749,8 @@ async function runNative(input, base, mode, limit, signal) {
18427
18749
  files.push({ full, name: e.name });
18428
18750
  }
18429
18751
  }
18430
- await mapWithConcurrency(files, NATIVE_SCAN_CONCURRENCY, ({ full, name }) => scanFile(full, name));
18431
- await mapWithConcurrency(
18752
+ await mapWithConcurrency2(files, NATIVE_SCAN_CONCURRENCY, ({ full, name }) => scanFile(full, name));
18753
+ await mapWithConcurrency2(
18432
18754
  subdirs,
18433
18755
  Math.min(16, NATIVE_SCAN_CONCURRENCY),
18434
18756
  ({ full, rel }) => walk(full, rel)
@@ -18447,7 +18769,7 @@ async function runNative(input, base, mode, limit, signal) {
18447
18769
  init_spawn_stream();
18448
18770
  init_util();
18449
18771
  init_legacy_bridge();
18450
- import { join as join24 } from "node:path";
18772
+ import { join as join25 } from "node:path";
18451
18773
  import {
18452
18774
  detectEcosystem as detectPackageEcosystem,
18453
18775
  recordPackageAction
@@ -18458,6 +18780,10 @@ var installTool = {
18458
18780
  description: "Install, update or manage packages using the detected package manager (pnpm/npm/yarn). Strongly preferred over raw shell commands for dependency management because it is structured and safer.",
18459
18781
  usageHint: "ALWAYS USE THIS INSTEAD OF BASH FOR PACKAGE WORK:\n\n- Empty `packages` \u2192 normal `install` (respects lockfile).\n- Provide names \u2192 adds/updates specific packages.\n- `dry_run: true` for safe preview.\n- Set `save` appropriately.\nThis tool has proper capability declaration and is heavily recommended in the security posture of the project.",
18460
18782
  permission: "confirm",
18783
+ // WS-046: gives permission decisions something to key on.
18784
+ // What gets installed is the subject — installing a package runs its
18785
+ // lifecycle scripts, so `express` and `evil-pkg` must not be interchangeable.
18786
+ subjectKey: "packages",
18461
18787
  mutating: true,
18462
18788
  riskTier: "standard",
18463
18789
  icon: "package",
@@ -18627,10 +18953,10 @@ function resolveManifestPath(cwd, pkgManager) {
18627
18953
  case "pnpm":
18628
18954
  case "yarn":
18629
18955
  case "npm":
18630
- return join24(cwd, "package.json");
18956
+ return join25(cwd, "package.json");
18631
18957
  /* v8 ignore next 2 -- pkgManager is always pnpm/yarn/npm; the default is defensive. */
18632
18958
  default:
18633
- return join24(cwd, "package.json");
18959
+ return join25(cwd, "package.json");
18634
18960
  }
18635
18961
  }
18636
18962
 
@@ -18638,6 +18964,24 @@ function resolveManifestPath(cwd, pkgManager) {
18638
18964
  init_util();
18639
18965
  import * as fs26 from "node:fs/promises";
18640
18966
  import { deepMerge as deepMergeCore } from "@wrongstack/core/utils";
18967
+ var MAX_JSON_FILE_BYTES = 16 * 1024 * 1024;
18968
+ var MAX_JSON_FILE_BYTES_HUMAN = "16 MiB";
18969
+ var JsonFileTooLargeError = class extends Error {
18970
+ constructor(filePath, size) {
18971
+ super(
18972
+ `json: "${filePath}" is ${size} bytes \u2014 exceeds the ${MAX_JSON_FILE_BYTES_HUMAN} file-size limit. Extract the relevant portion into a smaller file first, or use a shell tool to query it directly.`
18973
+ );
18974
+ this.name = "JsonFileTooLargeError";
18975
+ }
18976
+ };
18977
+ async function readJsonFileBounded(filePath, ctx) {
18978
+ const resolved = await safeResolveReal(filePath, ctx);
18979
+ const stat18 = await fs26.stat(resolved);
18980
+ if (stat18.size > MAX_JSON_FILE_BYTES) {
18981
+ throw new JsonFileTooLargeError(filePath, stat18.size);
18982
+ }
18983
+ return fs26.readFile(resolved, "utf8");
18984
+ }
18641
18985
  var jsonTool = {
18642
18986
  name: "json",
18643
18987
  category: "Data",
@@ -18645,6 +18989,7 @@ var jsonTool = {
18645
18989
  usageHint: 'VERY USEFUL FOR DATA INSPECTION:\n\n- `action: "parse"` (default): read/pretty-print/convert JSON, JSON5, or YAML from `file` or `data`.\n- `action: "query"`: JMESPath-like query (`a.b[0].c`, `items[*].name`, filters, functions).\n- `action: "validate"`: validate data against a JSON Schema (`schema` param).\n- `action: "transform"`: chain multiple JMESPath transforms (`transforms` param).\n- `action: "merge"`: deep merge `base` and `patch` objects (`conflictResolution` param).\nPrefer this over raw `read` + manual parsing when dealing with configuration or data files.',
18646
18990
  permission: "auto",
18647
18991
  mutating: false,
18992
+ maxOutputBytes: 262144,
18648
18993
  timeoutMs: 5e3,
18649
18994
  capabilities: ["fs.read"],
18650
18995
  icon: "json",
@@ -18711,9 +19056,15 @@ async function executeParse(input, ctx) {
18711
19056
  let raw;
18712
19057
  if (input.file) {
18713
19058
  try {
18714
- raw = await fs26.readFile(await safeResolveReal(input.file, ctx), "utf8");
18715
- } catch {
18716
- return { data: null, formatted: "", type: "unknown", action: "parse", error: "Could not read file" };
19059
+ raw = await readJsonFileBounded(input.file, ctx);
19060
+ } catch (error) {
19061
+ return {
19062
+ data: null,
19063
+ formatted: "",
19064
+ type: "unknown",
19065
+ action: "parse",
19066
+ error: error instanceof JsonFileTooLargeError ? error.message : "Could not read file"
19067
+ };
18717
19068
  }
18718
19069
  } else if (input.data) {
18719
19070
  raw = input.data;
@@ -18769,10 +19120,16 @@ async function executeQuery(input, ctx) {
18769
19120
  let parsed;
18770
19121
  if (input.file) {
18771
19122
  try {
18772
- const raw = await fs26.readFile(await safeResolveReal(input.file, ctx), "utf8");
19123
+ const raw = await readJsonFileBounded(input.file, ctx);
18773
19124
  parsed = JSON.parse(raw);
18774
- } catch {
18775
- return { data: null, formatted: "", type: "unknown", action: "query", error: "Could not read/parse file" };
19125
+ } catch (error) {
19126
+ return {
19127
+ data: null,
19128
+ formatted: "",
19129
+ type: "unknown",
19130
+ action: "query",
19131
+ error: error instanceof JsonFileTooLargeError ? error.message : "Could not read/parse file"
19132
+ };
18776
19133
  }
18777
19134
  } else if (input.data) {
18778
19135
  try {
@@ -18811,10 +19168,16 @@ async function executeValidate(input, ctx) {
18811
19168
  let parsed;
18812
19169
  if (input.file) {
18813
19170
  try {
18814
- const raw = await fs26.readFile(await safeResolveReal(input.file, ctx), "utf8");
19171
+ const raw = await readJsonFileBounded(input.file, ctx);
18815
19172
  parsed = JSON.parse(raw);
18816
- } catch {
18817
- return { data: null, formatted: "", type: "unknown", action: "validate", error: "Could not read/parse file" };
19173
+ } catch (error) {
19174
+ return {
19175
+ data: null,
19176
+ formatted: "",
19177
+ type: "unknown",
19178
+ action: "validate",
19179
+ error: error instanceof JsonFileTooLargeError ? error.message : "Could not read/parse file"
19180
+ };
18818
19181
  }
18819
19182
  } else if (input.data) {
18820
19183
  try {
@@ -18853,10 +19216,16 @@ async function executeTransform(input, ctx) {
18853
19216
  let parsed;
18854
19217
  if (input.file) {
18855
19218
  try {
18856
- const raw = await fs26.readFile(await safeResolveReal(input.file, ctx), "utf8");
19219
+ const raw = await readJsonFileBounded(input.file, ctx);
18857
19220
  parsed = JSON.parse(raw);
18858
- } catch {
18859
- return { data: null, formatted: "", type: "unknown", action: "transform", error: "Could not read/parse file" };
19221
+ } catch (error) {
19222
+ return {
19223
+ data: null,
19224
+ formatted: "",
19225
+ type: "unknown",
19226
+ action: "transform",
19227
+ error: error instanceof JsonFileTooLargeError ? error.message : "Could not read/parse file"
19228
+ };
18860
19229
  }
18861
19230
  } else if (input.data) {
18862
19231
  try {
@@ -19460,6 +19829,10 @@ async function handleKanbanDetailAction(projectRoot, input) {
19460
19829
  }
19461
19830
 
19462
19831
  // src/kanban-board-inputs.ts
19832
+ function agentSettableGate(enforcement) {
19833
+ if (enforcement === void 0 || enforcement === "off") return {};
19834
+ return { completionGate: { enforcement } };
19835
+ }
19463
19836
  function boardCreateInput(input, title) {
19464
19837
  return {
19465
19838
  title,
@@ -19472,7 +19845,7 @@ function boardCreateInput(input, title) {
19472
19845
  decomposition: input.atomicityDecomposition ?? "propose"
19473
19846
  }
19474
19847
  } : {},
19475
- ...input.gateEnforcement !== void 0 ? { completionGate: { enforcement: input.gateEnforcement } } : {}
19848
+ ...agentSettableGate(input.gateEnforcement)
19476
19849
  };
19477
19850
  }
19478
19851
  function boardUpdatePatch(input) {
@@ -19486,7 +19859,7 @@ function boardUpdatePatch(input) {
19486
19859
  decomposition: input.atomicityDecomposition ?? "propose"
19487
19860
  }
19488
19861
  } : {},
19489
- ...input.gateEnforcement !== void 0 ? { completionGate: { enforcement: input.gateEnforcement } } : {}
19862
+ ...agentSettableGate(input.gateEnforcement)
19490
19863
  };
19491
19864
  }
19492
19865
  function duplicateBoardOptions(input) {
@@ -19501,6 +19874,7 @@ function duplicateBoardOptions(input) {
19501
19874
 
19502
19875
  // src/kanban-task-inputs.ts
19503
19876
  import { randomUUID } from "node:crypto";
19877
+ import { clampSubagentCapabilities } from "@wrongstack/core/security";
19504
19878
  function taskInput(input) {
19505
19879
  const assignment = hasAssignmentInput(input) ? assignmentForTaskCreate(input) : void 0;
19506
19880
  return {
@@ -19598,6 +19972,10 @@ function taskPatch(input) {
19598
19972
  ...input.actualHours !== void 0 ? { actualHours: input.actualHours } : {}
19599
19973
  };
19600
19974
  }
19975
+ function clampRequestedCapabilities(requested) {
19976
+ if (requested === void 0) return void 0;
19977
+ return clampSubagentCapabilities(requested).granted;
19978
+ }
19601
19979
  function assignmentInput(input) {
19602
19980
  return {
19603
19981
  agentId: input.agentId,
@@ -19608,7 +19986,7 @@ function assignmentInput(input) {
19608
19986
  fallbackProfile: input.fallbackProfile,
19609
19987
  fallbackModels: input.fallbackModels,
19610
19988
  tools: input.tools,
19611
- allowedCapabilities: input.allowedCapabilities,
19989
+ allowedCapabilities: clampRequestedCapabilities(input.allowedCapabilities),
19612
19990
  assignee: input.assignee,
19613
19991
  leaseId: input.leaseId,
19614
19992
  claimedAt: input.claimedAt,
@@ -19635,7 +20013,7 @@ function assignmentForTaskCreate(input) {
19635
20013
  ...input.fallbackProfile !== void 0 ? { fallbackProfile: input.fallbackProfile } : {},
19636
20014
  ...input.fallbackModels !== void 0 ? { fallbackModels: input.fallbackModels } : {},
19637
20015
  ...input.tools !== void 0 ? { tools: input.tools } : {},
19638
- ...input.allowedCapabilities !== void 0 ? { allowedCapabilities: input.allowedCapabilities } : {},
20016
+ ...input.allowedCapabilities !== void 0 ? { allowedCapabilities: clampRequestedCapabilities(input.allowedCapabilities) } : {},
19639
20017
  ...input.leaseId !== void 0 ? { leaseId: input.leaseId } : {},
19640
20018
  ...input.claimedAt !== void 0 ? { claimedAt: input.claimedAt } : {},
19641
20019
  ...input.heartbeatAt !== void 0 ? { heartbeatAt: input.heartbeatAt } : {},
@@ -19846,7 +20224,13 @@ var KANBAN_INPUT_SCHEMA = {
19846
20224
  moveTasksToColumnId: { type: "string" },
19847
20225
  atomicityMode: { type: "string", enum: ["off", "assess", "enforce"] },
19848
20226
  atomicityDecomposition: { type: "string", enum: ["auto", "propose"] },
19849
- gateEnforcement: { type: "string", enum: ["strict", "soft", "off"] },
20227
+ gateEnforcement: {
20228
+ type: "string",
20229
+ // WS-023: `'off'` is deliberately absent. The agent whose work this gate
20230
+ // checks must not be able to switch it off; it may only tighten. Turning
20231
+ // a gate off stays a human decision, made through board config.
20232
+ enum: ["strict", "soft"]
20233
+ },
19850
20234
  subtasks: {
19851
20235
  type: "array",
19852
20236
  minItems: 2,
@@ -19953,7 +20337,9 @@ async function ensureSessionKanbanBoard(projectRoot, sessionId) {
19953
20337
  description: "Live session work: todos, tasks, and plan items.",
19954
20338
  tags: sessionBoardTags(sessionId),
19955
20339
  columns: SESSION_KANBAN_COLUMNS,
19956
- generatedBy: `session-kanban:${sessionId}`
20340
+ generatedBy: `session-kanban:${sessionId}`,
20341
+ kind: "session_mirror",
20342
+ retention: { mode: "archive_after_ttl", ttlMs: 7 * 24 * 60 * 60 * 1e3 }
19957
20343
  });
19958
20344
  }
19959
20345
  if (!sameColumns(board.columns) || !board.tags?.includes(SESSION_BOARD_TAG)) {
@@ -20987,11 +21373,11 @@ var lintTool = {
20987
21373
  }
20988
21374
  };
20989
21375
  async function detectLinter(cwd) {
20990
- const { stat: stat17 } = await import("node:fs/promises");
21376
+ const { stat: stat18 } = await import("node:fs/promises");
20991
21377
  const checks = ["biome.json", ".eslintrc.json", "tslint.json", ".eslintrc.js", "tsconfig.json"];
20992
21378
  for (const f of checks) {
20993
21379
  try {
20994
- await stat17(`${cwd}/${f}`);
21380
+ await stat18(`${cwd}/${f}`);
20995
21381
  if (f.includes("biome")) return "biome";
20996
21382
  if (f.includes("eslint")) return "eslint";
20997
21383
  if (f.includes("tslint")) return "tslint";
@@ -21235,6 +21621,9 @@ var outdatedTool = {
21235
21621
  // tool.confirm_needed flow on every invocation. M-1 originally
21236
21622
  // fixed four sibling tools (mcp_control, shellcheck, shellcheck (scan mode),
21237
21623
  // search) but missed this one; applying the same contract here.
21624
+ // WS-046: gives permission decisions something to key on.
21625
+ // The working directory scanned.
21626
+ subjectKey: "cwd",
21238
21627
  mutating: true,
21239
21628
  // Capability is outbound network — the tool only hits the package
21240
21629
  // registry over HTTP, never touches the filesystem or runs shell.
@@ -21416,6 +21805,10 @@ var patchTool = {
21416
21805
  useInstead: ["edit"]
21417
21806
  },
21418
21807
  permission: "confirm",
21808
+ // WS-046: gives permission decisions something to key on.
21809
+ // The target directory. The patch body itself is unstable (every diff
21810
+ // differs), so it could never re-match its own stored rule.
21811
+ subjectKey: "directory",
21419
21812
  mutating: true,
21420
21813
  capabilities: ["fs.write"],
21421
21814
  icon: "edit",
@@ -21482,8 +21875,8 @@ var patchTool = {
21482
21875
  const before = beforeContents.get(target) ?? null;
21483
21876
  const after = await readTextForTracking(target);
21484
21877
  if (after === null || after === before) continue;
21485
- const stat17 = await fs27.stat(target).catch(() => null);
21486
- if (stat17) ctx.recordRead?.(target, stat17.mtimeMs, "write", sha256hex(after));
21878
+ const stat18 = await fs27.stat(target).catch(() => null);
21879
+ if (stat18) ctx.recordRead?.(target, stat18.mtimeMs, "write", sha256hex(after));
21487
21880
  ctx.session?.recordFileChange?.({
21488
21881
  path: target,
21489
21882
  action: before === null ? "created" : "modified",
@@ -21508,8 +21901,8 @@ var patchTool = {
21508
21901
  var MAX_TRACKING_BYTES = 5 * 1024 * 1024;
21509
21902
  async function readTextForTracking(absPath) {
21510
21903
  try {
21511
- const stat17 = await fs27.stat(absPath);
21512
- if (!stat17.isFile() || stat17.size > MAX_TRACKING_BYTES) return null;
21904
+ const stat18 = await fs27.stat(absPath);
21905
+ if (!stat18.isFile() || stat18.size > MAX_TRACKING_BYTES) return null;
21513
21906
  const buf = await fs27.readFile(absPath);
21514
21907
  if (buf.includes(0)) return null;
21515
21908
  return buf.toString("utf8");
@@ -21539,7 +21932,13 @@ function runPatch(args, cwd, signal) {
21539
21932
  let stdout = "";
21540
21933
  let stderr = "";
21541
21934
  const env = { ...buildChildEnv8(), LANG: "C", LC_ALL: "C" };
21542
- const child = spawn14("patch", args, { cwd, signal, env, stdio: ["pipe", "pipe", "pipe"], windowsHide: true });
21935
+ const child = spawn14("patch", args, {
21936
+ cwd,
21937
+ signal,
21938
+ env,
21939
+ stdio: ["pipe", "pipe", "pipe"],
21940
+ windowsHide: true
21941
+ });
21543
21942
  child.stdout?.on("data", (c) => {
21544
21943
  stdout += c.toString();
21545
21944
  });
@@ -21560,6 +21959,7 @@ function extractPatchedFiles(output) {
21560
21959
  }
21561
21960
 
21562
21961
  // src/plan.ts
21962
+ import { randomUUID as randomUUID2 } from "node:crypto";
21563
21963
  import {
21564
21964
  addPlanItem,
21565
21965
  clearPlan,
@@ -21567,20 +21967,20 @@ import {
21567
21967
  formatPlan,
21568
21968
  getPlanTemplate,
21569
21969
  mutatePlan as mutatePlan2,
21970
+ mutateTasks as mutateTasks2,
21570
21971
  removePlanItem,
21571
21972
  setPlanItemStatus
21572
21973
  } from "@wrongstack/core/storage";
21573
- import {
21574
- mutateTasks as mutateTasks2
21575
- } from "@wrongstack/core/storage";
21576
21974
  import { formatTaskList } from "@wrongstack/core/utils";
21577
- import { randomUUID as randomUUID2 } from "node:crypto";
21578
21975
  var planTool = {
21579
21976
  name: "plan",
21580
21977
  category: "Session",
21581
21978
  description: 'Manage a session-persistent strategic plan. The plan is written to disk and survives conversation resumptions within the same session, but is isolated to this session \u2014 other sessions have their own separate plans. Unlike todos (which are per-turn and lost on restart), a plan tracks high-level progress across multiple turns. Use this to outline big-picture work, then promote concrete items into the todo list when ready to execute. By default plans are isolated to this session; use `scope: "project"` to store the plan in a shared project-level file visible to all sessions.',
21582
21979
  usageHint: 'RECOMMENDED FOR COMPLEX, MULTI-PHASE WORK:\n\n- Start by creating a high-level plan with `action: "add"` or using templates (`template_use`).\n- Use `promote` to turn a plan item into actionable todos.\n- Use `taskify` to convert a plan item into a structured task (with type/priority/deps).\n- Keep plans at the "why and what" level, and todos at the "how and next step" level.\n- Common templates: "new-feature", "bug-fix", "refactor", "release", "security-audit".\n\nThis tool is excellent for maintaining long-term direction across many turns within a session. Plans survive resume but are not shared across separate sessions.\nUse `scope: "project"` to use a shared project-level plan file.',
21583
21980
  permission: "confirm",
21981
+ // WS-046: gives permission decisions something to key on.
21982
+ // The action performed; plan has no single file or path subject.
21983
+ subjectKey: "action",
21584
21984
  mutating: true,
21585
21985
  capabilities: ["fs.write"],
21586
21986
  icon: "plan",
@@ -21637,7 +22037,10 @@ var planTool = {
21637
22037
  let planPath;
21638
22038
  if (input.scope === "project") {
21639
22039
  if (typeof sessionPlanPath === "string") {
21640
- const lastSep = Math.max(sessionPlanPath.lastIndexOf("/"), sessionPlanPath.lastIndexOf("\\"));
22040
+ const lastSep = Math.max(
22041
+ sessionPlanPath.lastIndexOf("/"),
22042
+ sessionPlanPath.lastIndexOf("\\")
22043
+ );
21641
22044
  planPath = lastSep >= 0 ? sessionPlanPath.slice(0, lastSep + 1) + "backlog.plan.json" : "backlog.plan.json";
21642
22045
  }
21643
22046
  } else {
@@ -21674,7 +22077,11 @@ var planTool = {
21674
22077
  case "start":
21675
22078
  case "done": {
21676
22079
  if (!input.target) {
21677
- early = mkResult(p, false, `${input.action} requires \`target\` (id|index|substring).`);
22080
+ early = mkResult(
22081
+ p,
22082
+ false,
22083
+ `${input.action} requires \`target\` (id|index|substring).`
22084
+ );
21678
22085
  return p;
21679
22086
  }
21680
22087
  const next = setPlanItemStatus(
@@ -21702,7 +22109,11 @@ var planTool = {
21702
22109
  }
21703
22110
  case "promote": {
21704
22111
  if (!input.target) {
21705
- early = mkResult(p, false, `${input.action} requires \`target\` (id|index|substring).`);
22112
+ early = mkResult(
22113
+ p,
22114
+ false,
22115
+ `${input.action} requires \`target\` (id|index|substring).`
22116
+ );
21706
22117
  return p;
21707
22118
  }
21708
22119
  const derived = deriveTodosFromPlanItem(p, input.target, input.subtasks);
@@ -21745,7 +22156,11 @@ var planTool = {
21745
22156
  return clearPlan(p);
21746
22157
  case "taskify": {
21747
22158
  if (!input.target) {
21748
- early = mkResult(p, false, "taskify requires `target` (plan item id|index|substring).");
22159
+ early = mkResult(
22160
+ p,
22161
+ false,
22162
+ "taskify requires `target` (plan item id|index|substring)."
22163
+ );
21749
22164
  return p;
21750
22165
  }
21751
22166
  let itemIdx = -1;
@@ -21818,7 +22233,11 @@ var planTool = {
21818
22233
  ${formatTaskList(taskFile.tasks)}`
21819
22234
  );
21820
22235
  } catch (err) {
21821
- return mkResult(plan, false, `taskify: task not saved \u2014 ${err instanceof Error ? err.message : String(err)}`);
22236
+ return mkResult(
22237
+ plan,
22238
+ false,
22239
+ `taskify: task not saved \u2014 ${err instanceof Error ? err.message : String(err)}`
22240
+ );
21822
22241
  }
21823
22242
  }
21824
22243
  return mkResult(plan, true, `Plan ${input.action} ok.`);
@@ -21895,9 +22314,9 @@ var readTool = {
21895
22314
  }
21896
22315
  const absPath = await safeResolveReal(input.path, ctx);
21897
22316
  const shouldIncludeSymbols = input.includeSymbols === true || input.includeSymbols !== false && ctx.meta[ADVANCED_MODE_META_KEY] === true;
21898
- let stat17;
22317
+ let stat18;
21899
22318
  try {
21900
- stat17 = await fs28.stat(absPath);
22319
+ stat18 = await fs28.stat(absPath);
21901
22320
  } catch (err) {
21902
22321
  const code = err.code;
21903
22322
  if (code === "ENOENT") {
@@ -21916,7 +22335,7 @@ var readTool = {
21916
22335
  cause: err
21917
22336
  });
21918
22337
  }
21919
- if (!stat17.isFile()) {
22338
+ if (!stat18.isFile()) {
21920
22339
  throw new FsError({
21921
22340
  message: `read: "${input.path}" is not a regular file`,
21922
22341
  code: "FS_READ_FAILED",
@@ -21924,23 +22343,23 @@ var readTool = {
21924
22343
  context: { reason: "not-a-regular-file" }
21925
22344
  });
21926
22345
  }
21927
- if (stat17.size > MAX_BYTES2) {
22346
+ if (stat18.size > MAX_BYTES2) {
21928
22347
  throw new FsError({
21929
- message: `read: file too large (${stat17.size} bytes, limit ${MAX_BYTES2})`,
22348
+ message: `read: file too large (${stat18.size} bytes, limit ${MAX_BYTES2})`,
21930
22349
  code: "FS_READ_FAILED",
21931
22350
  path: absPath,
21932
- context: { size: stat17.size, limit: MAX_BYTES2, reason: "too-large" }
22351
+ context: { size: stat18.size, limit: MAX_BYTES2, reason: "too-large" }
21933
22352
  });
21934
22353
  }
21935
22354
  const offset = Math.max(1, input.offset ?? 1);
21936
22355
  const limit = Math.max(0, Math.min(input.limit ?? 2e3, 5e3));
21937
22356
  const prior = getReadRangeRecord(ctx, absPath);
21938
22357
  const requestedEnd = prior ? Math.min(offset + limit - 1, prior.totalLines) : offset + limit - 1;
21939
- if (input.mode !== "summary" && limit > 0 && prior && coversRange(prior, stat17.mtimeMs, offset, requestedEnd)) {
21940
- ctx.recordRead(absPath, stat17.mtimeMs);
22358
+ if (input.mode !== "summary" && limit > 0 && prior && coversRange(prior, stat18.mtimeMs, offset, requestedEnd)) {
22359
+ ctx.recordRead(absPath, stat18.mtimeMs);
21941
22360
  const symResult2 = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
21942
22361
  return {
21943
- text: `[unchanged since previous read: "${input.path}" mtime=${Math.round(stat17.mtimeMs)}; requested lines ${offset}-${requestedEnd} were already shown. Use offset/limit for a new range if needed.]`,
22362
+ text: `[unchanged since previous read: "${input.path}" mtime=${Math.round(stat18.mtimeMs)}; requested lines ${offset}-${requestedEnd} were already shown. Use offset/limit for a new range if needed.]`,
21944
22363
  total_lines: prior.totalLines,
21945
22364
  encoding: "utf8",
21946
22365
  truncated: requestedEnd < prior.totalLines,
@@ -21958,11 +22377,11 @@ var readTool = {
21958
22377
  const allLines = text.split(/\r\n|\r|\n/);
21959
22378
  const total = allLines.length;
21960
22379
  if (input.mode === "summary") {
21961
- ctx.recordRead(absPath, stat17.mtimeMs, "user", contentHash);
21962
- rememberReadRange(ctx, absPath, stat17.mtimeMs, total, 1, Math.min(total, 200));
22380
+ ctx.recordRead(absPath, stat18.mtimeMs, "user", contentHash);
22381
+ rememberReadRange(ctx, absPath, stat18.mtimeMs, total, 1, Math.min(total, 200));
21963
22382
  const symResult2 = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
21964
22383
  return {
21965
- text: summarizeFile(input.path, stat17.size, allLines),
22384
+ text: summarizeFile(input.path, stat18.size, allLines),
21966
22385
  total_lines: total,
21967
22386
  encoding: "utf8",
21968
22387
  truncated: total > 200,
@@ -21974,8 +22393,8 @@ var readTool = {
21974
22393
  };
21975
22394
  }
21976
22395
  if (limit === 0) {
21977
- ctx.recordRead(absPath, stat17.mtimeMs, "user", contentHash);
21978
- rememberReadRange(ctx, absPath, stat17.mtimeMs, total, 1, 0);
22396
+ ctx.recordRead(absPath, stat18.mtimeMs, "user", contentHash);
22397
+ rememberReadRange(ctx, absPath, stat18.mtimeMs, total, 1, 0);
21979
22398
  const symResult2 = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
21980
22399
  return {
21981
22400
  text: "",
@@ -21987,8 +22406,8 @@ var readTool = {
21987
22406
  };
21988
22407
  }
21989
22408
  if (offset > total) {
21990
- ctx.recordRead(absPath, stat17.mtimeMs, "user", contentHash);
21991
- rememberReadRange(ctx, absPath, stat17.mtimeMs, total, total + 1, total + 1);
22409
+ ctx.recordRead(absPath, stat18.mtimeMs, "user", contentHash);
22410
+ rememberReadRange(ctx, absPath, stat18.mtimeMs, total, total + 1, total + 1);
21992
22411
  const symResult2 = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
21993
22412
  return {
21994
22413
  text: `[offset ${offset} is past end of file "${input.path}" \u2014 file has ${total} line(s). Do not retry this offset.]`,
@@ -22003,8 +22422,8 @@ var readTool = {
22003
22422
  const truncated = offset - 1 + slice.length < total;
22004
22423
  const width = String(offset + slice.length - 1).length;
22005
22424
  const numbered = slice.map((line, i) => `${String(offset + i).padStart(width, " ")}\u2192${line}`).join("\n");
22006
- ctx.recordRead(absPath, stat17.mtimeMs, "user", contentHash);
22007
- rememberReadRange(ctx, absPath, stat17.mtimeMs, total, offset, offset + slice.length - 1);
22425
+ ctx.recordRead(absPath, stat18.mtimeMs, "user", contentHash);
22426
+ rememberReadRange(ctx, absPath, stat18.mtimeMs, total, offset, offset + slice.length - 1);
22008
22427
  const symResult = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
22009
22428
  return {
22010
22429
  text: numbered,
@@ -22110,20 +22529,20 @@ ${interesting.join("\n")}` : "symbols/imports: (none detected)"
22110
22529
  }
22111
22530
 
22112
22531
  // src/replace.ts
22113
- import { expectDefined as expectDefined8 } from "@wrongstack/core/utils";
22114
22532
  import { spawn as spawn15 } from "node:child_process";
22115
22533
  import * as fs29 from "node:fs/promises";
22116
22534
  import * as path33 from "node:path";
22535
+ import { ToolValidationError as ToolValidationError6 } from "@wrongstack/core/types";
22117
22536
  import {
22118
22537
  atomicWrite as atomicWrite3,
22119
22538
  buildChildEnv as buildChildEnv9,
22120
22539
  compileGlob as compileGlob4,
22121
22540
  detectNewlineStyle as detectNewlineStyle2,
22541
+ expectDefined as expectDefined8,
22122
22542
  normalizeToLf as normalizeToLf2,
22123
22543
  toStyle as toStyle2,
22124
22544
  unifiedDiff as unifiedDiff2
22125
22545
  } from "@wrongstack/core/utils";
22126
- import { ToolValidationError as ToolValidationError6 } from "@wrongstack/core/types";
22127
22546
  init_util();
22128
22547
  var DEFAULT_IGNORE4 = ["node_modules", ".git", "dist", "build", ".next", "coverage"];
22129
22548
  var replaceTool = {
@@ -22132,6 +22551,10 @@ var replaceTool = {
22132
22551
  description: "Perform a search-and-replace across multiple files using a regex pattern. This is a powerful bulk transformation tool. Dry-run is ON by default \u2014 set `dry_run: false` to apply changes.",
22133
22552
  usageHint: "DANGEROUS IF USED CARELESSLY \u2014 review the diff output carefully.\n\nRecommended workflow:\n1. Run without `dry_run: false` first to see exactly what would change (dry-run is the default).\n2. Review the diff output, then re-run with `dry_run: false` to apply.\n3. Use a specific enough `pattern` (and `glob` / `files`) to avoid accidental broad changes.\n4. `replace_all` controls whether only the first match per file or all matches are replaced.\nThis tool is excellent for large-scale refactors (renaming, import updates, etc.) but must be used with caution.",
22134
22553
  permission: "confirm",
22554
+ // WS-046: gives permission decisions something to key on.
22555
+ // The file scope being rewritten, not the pattern: a trust rule should say
22556
+ // where a bulk replace may run.
22557
+ subjectKey: "files",
22135
22558
  mutating: true,
22136
22559
  capabilities: ["fs.write"],
22137
22560
  icon: "edit",
@@ -22204,8 +22627,8 @@ var replaceTool = {
22204
22627
  }
22205
22628
  const rel = path33.relative(realRoot, realPath);
22206
22629
  if (rel.startsWith("..") || path33.isAbsolute(rel)) continue;
22207
- const stat17 = await fs29.stat(realPath).catch(() => null);
22208
- if (!stat17?.isFile()) continue;
22630
+ const stat18 = await fs29.stat(realPath).catch(() => null);
22631
+ if (!stat18?.isFile()) continue;
22209
22632
  let content;
22210
22633
  try {
22211
22634
  const buf = await fs29.readFile(realPath);
@@ -22230,7 +22653,7 @@ var replaceTool = {
22230
22653
  totalReplacements += count;
22231
22654
  if (!dryRun) {
22232
22655
  const newContent = toStyle2(newContentLf, style);
22233
- await atomicWrite3(realPath, newContent, { mode: stat17.mode & 511 });
22656
+ await atomicWrite3(realPath, newContent, { mode: stat18.mode & 511 });
22234
22657
  const written = await fs29.stat(realPath).catch(() => null);
22235
22658
  if (written) {
22236
22659
  ctx.recordRead?.(realPath, written.mtimeMs, "write", sha256hex(newContent));
@@ -22270,8 +22693,8 @@ async function resolveFiles2(filesInput, ctx, extraGlob) {
22270
22693
  const resolved = [];
22271
22694
  for (const p of parts) {
22272
22695
  const absPath = safeResolve(p, ctx);
22273
- const stat17 = await fs29.stat(absPath).catch(() => null);
22274
- if (stat17?.isFile()) {
22696
+ const stat18 = await fs29.stat(absPath).catch(() => null);
22697
+ if (stat18?.isFile()) {
22275
22698
  resolved.push(absPath);
22276
22699
  }
22277
22700
  }
@@ -22291,7 +22714,11 @@ async function globFiles(pattern, base, extraGlob) {
22291
22714
  function checkRg() {
22292
22715
  return new Promise((resolve16) => {
22293
22716
  try {
22294
- const p = spawn15("rg", ["--version"], { env: buildChildEnv9(), stdio: "ignore", windowsHide: true });
22717
+ const p = spawn15("rg", ["--version"], {
22718
+ env: buildChildEnv9(),
22719
+ stdio: "ignore",
22720
+ windowsHide: true
22721
+ });
22295
22722
  p.on("error", () => resolve16(false));
22296
22723
  p.on("close", (code) => resolve16(code === 0));
22297
22724
  } catch {
@@ -22342,8 +22769,8 @@ async function globNative(pattern, base, extraGlob) {
22342
22769
  if (DEFAULT_IGNORE4.includes(e.name)) continue;
22343
22770
  const full = path33.join(dir, e.name);
22344
22771
  try {
22345
- const stat17 = await fs29.lstat(full);
22346
- if (stat17.isSymbolicLink()) continue;
22772
+ const stat18 = await fs29.lstat(full);
22773
+ if (stat18.isSymbolicLink()) continue;
22347
22774
  } catch {
22348
22775
  continue;
22349
22776
  }
@@ -22467,6 +22894,7 @@ var scaffoldTool = {
22467
22894
  usageHint: "PREFERRED FOR SCAFFOLDING:\n\n- Use built-in templates when they match your needs (e.g. react-component, npm-package).\n- Supports `dry_run` so you can preview exactly what will be created.\n- Has the powerful `fs.write.outside-project` capability \u2014 review paths carefully.\nMuch cleaner and safer than manually writing multiple files.",
22468
22895
  permission: "confirm",
22469
22896
  mutating: true,
22897
+ subjectKey: "name",
22470
22898
  capabilities: ["fs.write.outside-project", "fs.write"],
22471
22899
  icon: "scaffold",
22472
22900
  timeoutMs: 3e4,
@@ -22541,15 +22969,17 @@ async function handleBuiltIn(name, templateFiles, cwd, ctx, dryRun, vars) {
22541
22969
  output: dryRun ? `Would create ${filesCreated} files: ${files.join(", ")}` : `Created ${filesCreated} files: ${files.join(", ")}`
22542
22970
  };
22543
22971
  }
22972
+ function escapeRegexLiteral(value) {
22973
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
22974
+ }
22544
22975
  function substituteVars(content, name, vars) {
22976
+ const kebab = name.toLowerCase().replace(/\s+/g, "-");
22977
+ const pascal = name.replace(/(?:^|[-_\s]+)([a-z])/g, (_, c) => c.toUpperCase());
22545
22978
  let result = content;
22546
- result = result.replace(/\{\{name\}\}/g, name.toLowerCase().replace(/\s+/g, "-"));
22547
- result = result.replace(
22548
- /\{\{Name\}\}/g,
22549
- name.replace(/(?:^|[-_\s]+)([a-z])/g, (_, c) => c.toUpperCase())
22550
- );
22979
+ result = result.replace(/\{\{name\}\}/g, () => kebab);
22980
+ result = result.replace(/\{\{Name\}\}/g, () => pascal);
22551
22981
  for (const [k, v] of Object.entries(vars)) {
22552
- result = result.replace(new RegExp(`\\{\\{${k}\\}\\}`, "g"), v);
22982
+ result = result.replace(new RegExp(`\\{\\{${escapeRegexLiteral(k)}\\}\\}`, "g"), () => v);
22553
22983
  }
22554
22984
  return result;
22555
22985
  }
@@ -23485,11 +23915,11 @@ var testTool = {
23485
23915
  }
23486
23916
  };
23487
23917
  async function detectRunner(cwd) {
23488
- const { stat: stat17 } = await import("node:fs/promises");
23918
+ const { stat: stat18 } = await import("node:fs/promises");
23489
23919
  const candidates = ["vitest.config.ts", "jest.config.js", ".mocharc.json"];
23490
23920
  for (const f of candidates) {
23491
23921
  try {
23492
- await stat17(path35.join(cwd, f));
23922
+ await stat18(path35.join(cwd, f));
23493
23923
  if (f.includes("vitest")) return "vitest";
23494
23924
  if (f.includes("jest")) return "jest";
23495
23925
  if (f.includes("mocha")) return "mocha";
@@ -23886,12 +24316,19 @@ var toolSearchTool = {
23886
24316
  };
23887
24317
 
23888
24318
  // src/tool-use.ts
24319
+ import {
24320
+ GOVERNED_TOOL_EXECUTOR_META_KEY as GOVERNED_TOOL_EXECUTOR_META_KEY2
24321
+ } from "@wrongstack/core/types";
23889
24322
  var toolUseTool = {
23890
24323
  name: "tool_use",
23891
24324
  category: "Meta",
23892
24325
  description: "Directly execute any registered tool by its exact name, bypassing normal discovery. This is a powerful meta-tool intended for cases where the agent has a clear plan and knows precisely which tool to invoke.",
23893
24326
  usageHint: "ADVANCED META TOOL \u2014 USE WITH CARE:\n\n- Only use when you are certain of the exact tool name and its expected input shape.\n- Prefer using the normal tool calling mechanism when possible.\n- Very useful in batch-tool-use or when orchestrating complex workflows programmatically.\n- The call still goes through full permission checks and capability validation.",
23894
24327
  permission: "confirm",
24328
+ // WS-046: gives permission decisions something to key on.
24329
+ // The tool being invoked through this indirection — the whole point of a
24330
+ // permission decision here is WHICH tool is being reached.
24331
+ subjectKey: "tool",
23895
24332
  mutating: true,
23896
24333
  timeoutMs: 6e4,
23897
24334
  capabilities: ["tool.mutate.any"],
@@ -23910,7 +24347,7 @@ var toolUseTool = {
23910
24347
  },
23911
24348
  required: ["tool"]
23912
24349
  },
23913
- async execute(input, ctx, opts) {
24350
+ async execute(input, ctx) {
23914
24351
  const start = Date.now();
23915
24352
  if (!input?.tool) {
23916
24353
  return {
@@ -23929,6 +24366,14 @@ var toolUseTool = {
23929
24366
  executionMs: Date.now() - start
23930
24367
  };
23931
24368
  }
24369
+ if (tool.name === toolUseTool.name) {
24370
+ return {
24371
+ tool: input.tool,
24372
+ success: false,
24373
+ error: "tool_use: recursive meta-tool execution is not allowed",
24374
+ executionMs: Date.now() - start
24375
+ };
24376
+ }
23932
24377
  if (tool.permission === "deny") {
23933
24378
  return {
23934
24379
  tool: input.tool,
@@ -23937,12 +24382,21 @@ var toolUseTool = {
23937
24382
  executionMs: Date.now() - start
23938
24383
  };
23939
24384
  }
24385
+ const governedExecute = ctx.meta[GOVERNED_TOOL_EXECUTOR_META_KEY2];
24386
+ if (typeof governedExecute !== "function") {
24387
+ return {
24388
+ tool: input.tool,
24389
+ success: false,
24390
+ error: "tool_use: governed nested execution is unavailable; call the tool directly",
24391
+ executionMs: Date.now() - start
24392
+ };
24393
+ }
23940
24394
  try {
23941
- const result = await tool.execute(input.input, ctx, opts);
24395
+ const result = await governedExecute(input.tool, input.input ?? {});
23942
24396
  return {
23943
24397
  tool: input.tool,
23944
- success: true,
23945
- result,
24398
+ success: result.success,
24399
+ ...result.success ? { result: result.result } : { error: result.error ?? "nested tool failed" },
23946
24400
  executionMs: Date.now() - start
23947
24401
  };
23948
24402
  } catch (e) {
@@ -23958,10 +24412,12 @@ var toolUseTool = {
23958
24412
 
23959
24413
  // src/tree.ts
23960
24414
  init_util();
23961
- import { DEFAULT_WALK_IGNORE_DIRS as DEFAULT_WALK_IGNORE_DIRS4, expectDefined as expectDefined10 } from "@wrongstack/core/utils";
23962
24415
  import * as fs32 from "node:fs/promises";
23963
24416
  import * as path36 from "node:path";
24417
+ import { DEFAULT_WALK_IGNORE_DIRS as DEFAULT_WALK_IGNORE_DIRS4, expectDefined as expectDefined10 } from "@wrongstack/core/utils";
23964
24418
  var DEFAULT_IGNORE5 = [...DEFAULT_WALK_IGNORE_DIRS4, ".wrongstack", ".ssh", ".gnupg", ".aws"];
24419
+ var DEFAULT_MAX_ENTRIES2 = 5e3;
24420
+ var MAX_TREE_OUTPUT_BYTES = 256 * 1024;
23965
24421
  var treeTool = {
23966
24422
  name: "tree",
23967
24423
  category: "Filesystem",
@@ -24005,6 +24461,12 @@ var treeTool = {
24005
24461
  show_hidden: {
24006
24462
  type: "boolean",
24007
24463
  description: "Show hidden files starting with . (default: false)"
24464
+ },
24465
+ max_entries: {
24466
+ type: "integer",
24467
+ description: `Maximum entries retained in the rendered tree (default ${DEFAULT_MAX_ENTRIES2}).`,
24468
+ minimum: 1,
24469
+ maximum: 5e4
24008
24470
  }
24009
24471
  }
24010
24472
  },
@@ -24018,7 +24480,7 @@ var treeTool = {
24018
24480
  if (!final) throw new Error("tree: stream ended without final event");
24019
24481
  return final;
24020
24482
  },
24021
- async *executeStream(input, ctx) {
24483
+ async *executeStream(input, ctx, opts) {
24022
24484
  const basePath = input.path ? safeResolve(input.path, ctx) : ctx.cwd;
24023
24485
  const maxDepth = input.depth ?? 3;
24024
24486
  const showFiles = input.show_files ?? true;
@@ -24026,8 +24488,17 @@ var treeTool = {
24026
24488
  const showHidden = input.show_hidden ?? false;
24027
24489
  const exclude = /* @__PURE__ */ new Set([...DEFAULT_IGNORE5, ...input.exclude ?? []]);
24028
24490
  const filterGlob = input.glob;
24491
+ const maxEntries = input.max_entries ?? DEFAULT_MAX_ENTRIES2;
24492
+ const signal = opts?.signal ?? ctx.signal ?? new AbortController().signal;
24029
24493
  const lines = [basePath];
24030
24494
  const totals = { totalFiles: { value: 0 }, totalDirs: { value: 0 } };
24495
+ const retention = {
24496
+ entries: 0,
24497
+ outputBytes: Buffer.byteLength(basePath, "utf8"),
24498
+ maxEntries,
24499
+ maxOutputBytes: MAX_TREE_OUTPUT_BYTES,
24500
+ truncated: false
24501
+ };
24031
24502
  const queue = [];
24032
24503
  const FLUSH_EVERY = 200;
24033
24504
  let lastEmittedTotal = 0;
@@ -24054,12 +24525,19 @@ var treeTool = {
24054
24525
  isLast: true,
24055
24526
  totalFiles: totals.totalFiles,
24056
24527
  totalDirs: totals.totalDirs,
24057
- onProgress: tickProgress
24528
+ onProgress: tickProgress,
24529
+ retention,
24530
+ signal
24058
24531
  });
24059
24532
  let walkDone = false;
24060
- walkPromise.finally(() => {
24061
- walkDone = true;
24062
- });
24533
+ void walkPromise.then(
24534
+ () => {
24535
+ walkDone = true;
24536
+ },
24537
+ () => {
24538
+ walkDone = true;
24539
+ }
24540
+ );
24063
24541
  while (!walkDone || queue.length > 0) {
24064
24542
  if (queue.length > 0) {
24065
24543
  yield expectDefined10(queue.shift());
@@ -24082,13 +24560,15 @@ var treeTool = {
24082
24560
  tree: lines.join("\n"),
24083
24561
  total_files: totals.totalFiles.value,
24084
24562
  total_dirs: totals.totalDirs.value,
24085
- truncated: false,
24563
+ truncated: retention.truncated,
24086
24564
  path: basePath
24087
24565
  }
24088
24566
  };
24089
24567
  }
24090
24568
  };
24091
24569
  async function walkDir(dir, depth, opts) {
24570
+ opts.signal.throwIfAborted();
24571
+ if (opts.retention.truncated) return;
24092
24572
  const entries = await fs32.readdir(dir, { withFileTypes: true }).catch(() => []);
24093
24573
  const filtered = entries.filter((e) => {
24094
24574
  if (!opts.showHidden && e.name.startsWith(".")) return false;
@@ -24108,6 +24588,8 @@ async function walkDir(dir, depth, opts) {
24108
24588
  return a.name.localeCompare(b.name);
24109
24589
  });
24110
24590
  for (let i = 0; i < items.length; i++) {
24591
+ opts.signal.throwIfAborted();
24592
+ if (opts.retention.truncated) return;
24111
24593
  const entry = items[i];
24112
24594
  if (!entry) continue;
24113
24595
  const isLast = i === items.length - 1;
@@ -24116,7 +24598,15 @@ async function walkDir(dir, depth, opts) {
24116
24598
  const displayName = entry.name + (entry.isDirectory() ? "/" : "");
24117
24599
  if (!opts.showDirs && entry.isDirectory()) continue;
24118
24600
  if (!opts.showFiles && entry.isFile()) continue;
24119
- opts.lines.push(opts.prefix + branch + displayName);
24601
+ const line = opts.prefix + branch + displayName;
24602
+ const lineBytes = Buffer.byteLength(line, "utf8") + 1;
24603
+ if (opts.retention.entries >= opts.retention.maxEntries || opts.retention.outputBytes + lineBytes > opts.retention.maxOutputBytes) {
24604
+ opts.retention.truncated = true;
24605
+ return;
24606
+ }
24607
+ opts.lines.push(line);
24608
+ opts.retention.entries++;
24609
+ opts.retention.outputBytes += lineBytes;
24120
24610
  if (entry.isDirectory() && (opts.maxDepth === 0 || depth < opts.maxDepth)) {
24121
24611
  const childPrefix = opts.prefix + connector;
24122
24612
  await walkDir(path36.join(dir, entry.name), depth + 1, {
@@ -24231,11 +24721,11 @@ var typecheckTool = {
24231
24721
  }
24232
24722
  };
24233
24723
  async function findTsConfig(cwd) {
24234
- const { stat: stat17 } = await import("node:fs/promises");
24724
+ const { stat: stat18 } = await import("node:fs/promises");
24235
24725
  const candidates = ["tsconfig.json", "tsconfig.base.json"];
24236
24726
  for (const f of candidates) {
24237
24727
  try {
24238
- const s = await stat17(path37.join(cwd, f));
24728
+ const s = await stat18(path37.join(cwd, f));
24239
24729
  if (s.isFile()) return path37.join(cwd, f);
24240
24730
  } catch {
24241
24731
  }
@@ -24310,12 +24800,12 @@ async function prepareWrite(input, ctx) {
24310
24800
  let existed = false;
24311
24801
  let prev = "";
24312
24802
  try {
24313
- const stat17 = await fs33.stat(absPath);
24314
- existed = stat17.isFile();
24803
+ const stat18 = await fs33.stat(absPath);
24804
+ existed = stat18.isFile();
24315
24805
  if (existed) {
24316
24806
  if (!ctx.hasRead(absPath)) {
24317
24807
  prev = await fs33.readFile(absPath, "utf8");
24318
- ctx.recordRead(absPath, stat17.mtimeMs, "write", sha256hex(prev));
24808
+ ctx.recordRead(absPath, stat18.mtimeMs, "write", sha256hex(prev));
24319
24809
  } else {
24320
24810
  prev = await fs33.readFile(absPath, "utf8");
24321
24811
  }
@@ -24332,8 +24822,8 @@ async function finishWrite(input, ctx, prepared, signal) {
24332
24822
  await atomicWrite5(prepared.absPath, input.content);
24333
24823
  const diff = prepared.existed ? unifiedDiff3(prepared.prev, input.content, { fromFile: input.path, toFile: input.path }) : `+++ ${input.path}
24334
24824
  + (new file, ${input.content.split("\n").length} lines)`;
24335
- const stat17 = await fs33.stat(prepared.absPath);
24336
- ctx.recordRead(prepared.absPath, stat17.mtimeMs, "write", sha256hex(input.content));
24825
+ const stat18 = await fs33.stat(prepared.absPath);
24826
+ ctx.recordRead(prepared.absPath, stat18.mtimeMs, "write", sha256hex(input.content));
24337
24827
  ctx.session.recordFileChange({
24338
24828
  path: prepared.absPath,
24339
24829
  action: prepared.existed ? "modified" : "created",