@wrongstack/tools 0.296.4 → 0.298.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (258) hide show
  1. package/dist/_spawn-stream.d.ts +2 -0
  2. package/dist/_util.d.ts +29 -1
  3. package/dist/audit.js +128 -65
  4. package/dist/bash.js +20 -4
  5. package/dist/batch-tool-use.js +70 -7
  6. package/dist/browser/index.js +31 -1
  7. package/dist/builtin.js +883 -351
  8. package/dist/codebase-index/index.js +95 -16
  9. package/dist/codebase-index/project-server-protocol.d.ts +26 -0
  10. package/dist/codebase-index/project-server.js +76 -16
  11. package/dist/codebase-index/worker.js +36 -13
  12. package/dist/codebase-index/writer-search-helpers.d.ts +16 -0
  13. package/dist/codebase-index/writer.d.ts +13 -1
  14. package/dist/design.d.ts +1 -1
  15. package/dist/design.js +4 -3
  16. package/dist/diff.js +11 -1
  17. package/dist/e2e.js +9 -6
  18. package/dist/edit.js +9 -6
  19. package/dist/exec.js +562 -516
  20. package/dist/format.js +131 -65
  21. package/dist/git.js +22 -2
  22. package/dist/glob.js +11 -5
  23. package/dist/grep.js +160 -104
  24. package/dist/index.js +834 -290
  25. package/dist/install.js +135 -68
  26. package/dist/json.js +64 -18
  27. package/dist/kanban-board-inputs.d.ts +21 -4
  28. package/dist/kanban.js +20 -5
  29. package/dist/languages/index.js +143 -71
  30. package/dist/lint.js +128 -65
  31. package/dist/next-steps.d.ts +23 -0
  32. package/dist/next-steps.js +4 -0
  33. package/dist/outdated.js +101 -26
  34. package/dist/pack.js +883 -351
  35. package/dist/patch.js +11 -1
  36. package/dist/plan.js +32 -10
  37. package/dist/process-registry.d.ts +9 -0
  38. package/dist/process-registry.js +15 -0
  39. package/dist/ps-slash.js +15 -0
  40. package/dist/read.js +104 -23
  41. package/dist/replace.js +11 -3
  42. package/dist/scaffold.js +9 -6
  43. package/dist/session-kanban.js +21 -5
  44. package/dist/task.js +3 -1
  45. package/dist/test.js +128 -65
  46. package/dist/todo.js +3 -1
  47. package/dist/tool-tier.js +883 -351
  48. package/dist/tool-use.d.ts +1 -1
  49. package/dist/tool-use.js +28 -4
  50. package/dist/tree.d.ts +1 -0
  51. package/dist/tree.js +44 -8
  52. package/dist/typecheck.js +128 -65
  53. package/dist/write.js +9 -6
  54. package/package.json +5 -4
  55. package/dist/_concurrency.d.ts.map +0 -1
  56. package/dist/_danger-detect.d.ts.map +0 -1
  57. package/dist/_edit-match.d.ts.map +0 -1
  58. package/dist/_env.d.ts.map +0 -1
  59. package/dist/_fetch-guard.d.ts.map +0 -1
  60. package/dist/_output-spool.d.ts.map +0 -1
  61. package/dist/_redact-command.d.ts.map +0 -1
  62. package/dist/_regex.d.ts.map +0 -1
  63. package/dist/_session-shell.d.ts.map +0 -1
  64. package/dist/_shell-pick.d.ts.map +0 -1
  65. package/dist/_spawn-stream.d.ts.map +0 -1
  66. package/dist/_syntax-check.d.ts.map +0 -1
  67. package/dist/_util.d.ts.map +0 -1
  68. package/dist/_win32-resolve.d.ts.map +0 -1
  69. package/dist/audit.d.ts.map +0 -1
  70. package/dist/audit.js.map +0 -7
  71. package/dist/auto-proceed-loop-guard.d.ts.map +0 -1
  72. package/dist/auto-proceed-loop-guard.js.map +0 -7
  73. package/dist/bash-kill-guard.d.ts.map +0 -1
  74. package/dist/bash.d.ts.map +0 -1
  75. package/dist/bash.js.map +0 -7
  76. package/dist/batch-tool-use.d.ts.map +0 -1
  77. package/dist/batch-tool-use.js.map +0 -7
  78. package/dist/browser/artifacts.d.ts.map +0 -1
  79. package/dist/browser/index.d.ts.map +0 -1
  80. package/dist/browser/index.js.map +0 -7
  81. package/dist/browser/manager.d.ts.map +0 -1
  82. package/dist/browser/network-guard-proxy.d.ts.map +0 -1
  83. package/dist/browser/security.d.ts.map +0 -1
  84. package/dist/browser/tools.d.ts.map +0 -1
  85. package/dist/browser/types.d.ts.map +0 -1
  86. package/dist/builtin.d.ts.map +0 -1
  87. package/dist/builtin.js.map +0 -7
  88. package/dist/circuit-breaker.d.ts.map +0 -1
  89. package/dist/circuit-breaker.js.map +0 -7
  90. package/dist/codebase-index/background-indexer.d.ts.map +0 -1
  91. package/dist/codebase-index/bm25.d.ts.map +0 -1
  92. package/dist/codebase-index/circuit-breaker.d.ts.map +0 -1
  93. package/dist/codebase-index/codebase-index-tool.d.ts.map +0 -1
  94. package/dist/codebase-index/codebase-search-tool.d.ts.map +0 -1
  95. package/dist/codebase-index/codebase-stats-tool.d.ts.map +0 -1
  96. package/dist/codebase-index/dead-code-scan.d.ts.map +0 -1
  97. package/dist/codebase-index/generic-parser.d.ts.map +0 -1
  98. package/dist/codebase-index/gitignore.d.ts.map +0 -1
  99. package/dist/codebase-index/go-parser.d.ts.map +0 -1
  100. package/dist/codebase-index/index-service.d.ts.map +0 -1
  101. package/dist/codebase-index/index.d.ts.map +0 -1
  102. package/dist/codebase-index/index.js.map +0 -7
  103. package/dist/codebase-index/indexer.d.ts.map +0 -1
  104. package/dist/codebase-index/json-parser.d.ts.map +0 -1
  105. package/dist/codebase-index/languages.d.ts.map +0 -1
  106. package/dist/codebase-index/lsp-kind.d.ts.map +0 -1
  107. package/dist/codebase-index/parser-dispatch.d.ts.map +0 -1
  108. package/dist/codebase-index/project-server-cache.d.ts.map +0 -1
  109. package/dist/codebase-index/project-server-client.d.ts.map +0 -1
  110. package/dist/codebase-index/project-server-endpoint.d.ts.map +0 -1
  111. package/dist/codebase-index/project-server-protocol.d.ts.map +0 -1
  112. package/dist/codebase-index/project-server.d.ts.map +0 -1
  113. package/dist/codebase-index/project-server.js.map +0 -7
  114. package/dist/codebase-index/py-parser.d.ts.map +0 -1
  115. package/dist/codebase-index/refs-extractor.d.ts.map +0 -1
  116. package/dist/codebase-index/rs-parser.d.ts.map +0 -1
  117. package/dist/codebase-index/schema.d.ts.map +0 -1
  118. package/dist/codebase-index/spawn-gate.d.ts.map +0 -1
  119. package/dist/codebase-index/sqlite-runtime.d.ts.map +0 -1
  120. package/dist/codebase-index/ts-parser.d.ts.map +0 -1
  121. package/dist/codebase-index/worker-protocol.d.ts.map +0 -1
  122. package/dist/codebase-index/worker.d.ts.map +0 -1
  123. package/dist/codebase-index/worker.js.map +0 -7
  124. package/dist/codebase-index/writer-admin.d.ts.map +0 -1
  125. package/dist/codebase-index/writer-bulk-insert.d.ts.map +0 -1
  126. package/dist/codebase-index/writer-graph-helpers.d.ts.map +0 -1
  127. package/dist/codebase-index/writer-graph-reader.d.ts.map +0 -1
  128. package/dist/codebase-index/writer-helpers.d.ts.map +0 -1
  129. package/dist/codebase-index/writer-pragmas.d.ts.map +0 -1
  130. package/dist/codebase-index/writer-ref-mapper.d.ts.map +0 -1
  131. package/dist/codebase-index/writer-schema.d.ts.map +0 -1
  132. package/dist/codebase-index/writer-search-helpers.d.ts.map +0 -1
  133. package/dist/codebase-index/writer-store-pool.d.ts.map +0 -1
  134. package/dist/codebase-index/writer.d.ts.map +0 -1
  135. package/dist/codebase-index/yaml-parser.d.ts.map +0 -1
  136. package/dist/design.d.ts.map +0 -1
  137. package/dist/design.js.map +0 -7
  138. package/dist/diff.d.ts.map +0 -1
  139. package/dist/diff.js.map +0 -7
  140. package/dist/document.d.ts.map +0 -1
  141. package/dist/document.js.map +0 -7
  142. package/dist/e2e.d.ts.map +0 -1
  143. package/dist/e2e.js.map +0 -7
  144. package/dist/edit.d.ts.map +0 -1
  145. package/dist/edit.js.map +0 -7
  146. package/dist/exec-kill-guard.d.ts.map +0 -1
  147. package/dist/exec.d.ts.map +0 -1
  148. package/dist/exec.js.map +0 -7
  149. package/dist/fetch.d.ts.map +0 -1
  150. package/dist/fetch.js.map +0 -7
  151. package/dist/format.d.ts.map +0 -1
  152. package/dist/format.js.map +0 -7
  153. package/dist/git.d.ts.map +0 -1
  154. package/dist/git.js.map +0 -7
  155. package/dist/glob.d.ts.map +0 -1
  156. package/dist/glob.js.map +0 -7
  157. package/dist/grep.d.ts.map +0 -1
  158. package/dist/grep.js.map +0 -7
  159. package/dist/index.d.ts.map +0 -1
  160. package/dist/index.js.map +0 -7
  161. package/dist/install.d.ts.map +0 -1
  162. package/dist/install.js.map +0 -7
  163. package/dist/json.d.ts.map +0 -1
  164. package/dist/json.js.map +0 -7
  165. package/dist/kanban-board-inputs.d.ts.map +0 -1
  166. package/dist/kanban-decomposition-actions.d.ts.map +0 -1
  167. package/dist/kanban-detail-actions.d.ts.map +0 -1
  168. package/dist/kanban-evidence-bridge.d.ts.map +0 -1
  169. package/dist/kanban-presence.d.ts.map +0 -1
  170. package/dist/kanban-split-task-handler.d.ts.map +0 -1
  171. package/dist/kanban-task-inputs.d.ts.map +0 -1
  172. package/dist/kanban-tool-results.d.ts.map +0 -1
  173. package/dist/kanban-tool-schema.d.ts.map +0 -1
  174. package/dist/kanban-tool-types.d.ts.map +0 -1
  175. package/dist/kanban.d.ts.map +0 -1
  176. package/dist/kanban.js.map +0 -7
  177. package/dist/languages/detect.d.ts.map +0 -1
  178. package/dist/languages/diagnostics.d.ts.map +0 -1
  179. package/dist/languages/execute-tool.d.ts.map +0 -1
  180. package/dist/languages/execute.d.ts.map +0 -1
  181. package/dist/languages/index.d.ts.map +0 -1
  182. package/dist/languages/index.js.map +0 -7
  183. package/dist/languages/legacy-bridge.d.ts.map +0 -1
  184. package/dist/languages/package-tool.d.ts.map +0 -1
  185. package/dist/languages/plan.d.ts.map +0 -1
  186. package/dist/languages/profile-helpers.d.ts.map +0 -1
  187. package/dist/languages/profiles/additional.d.ts.map +0 -1
  188. package/dist/languages/profiles/primary.d.ts.map +0 -1
  189. package/dist/languages/registry.d.ts.map +0 -1
  190. package/dist/languages/tool.d.ts.map +0 -1
  191. package/dist/languages/types.d.ts.map +0 -1
  192. package/dist/lint.d.ts.map +0 -1
  193. package/dist/lint.js.map +0 -7
  194. package/dist/logs.d.ts.map +0 -1
  195. package/dist/logs.js.map +0 -7
  196. package/dist/memory.d.ts.map +0 -1
  197. package/dist/memory.js.map +0 -7
  198. package/dist/mode.d.ts.map +0 -1
  199. package/dist/mode.js.map +0 -7
  200. package/dist/next-steps.d.ts.map +0 -1
  201. package/dist/next-steps.js.map +0 -7
  202. package/dist/outdated.d.ts.map +0 -1
  203. package/dist/outdated.js.map +0 -7
  204. package/dist/pack.d.ts.map +0 -1
  205. package/dist/pack.js.map +0 -7
  206. package/dist/patch.d.ts.map +0 -1
  207. package/dist/patch.js.map +0 -7
  208. package/dist/plan.d.ts.map +0 -1
  209. package/dist/plan.js.map +0 -7
  210. package/dist/process-guardian.d.ts.map +0 -1
  211. package/dist/process-registry-persistent.d.ts.map +0 -1
  212. package/dist/process-registry.d.ts.map +0 -1
  213. package/dist/process-registry.js.map +0 -7
  214. package/dist/ps-slash.d.ts.map +0 -1
  215. package/dist/ps-slash.js.map +0 -7
  216. package/dist/read.d.ts.map +0 -1
  217. package/dist/read.js.map +0 -7
  218. package/dist/replace.d.ts.map +0 -1
  219. package/dist/replace.js.map +0 -7
  220. package/dist/scaffold.d.ts.map +0 -1
  221. package/dist/scaffold.js.map +0 -7
  222. package/dist/search.d.ts.map +0 -1
  223. package/dist/search.js.map +0 -7
  224. package/dist/session-kanban.d.ts.map +0 -1
  225. package/dist/session-kanban.js.map +0 -7
  226. package/dist/set-working-dir.d.ts.map +0 -1
  227. package/dist/skill.d.ts.map +0 -1
  228. package/dist/skill.js.map +0 -7
  229. package/dist/spawn-background.d.ts.map +0 -1
  230. package/dist/task.d.ts.map +0 -1
  231. package/dist/task.js.map +0 -7
  232. package/dist/test.d.ts.map +0 -1
  233. package/dist/test.js.map +0 -7
  234. package/dist/todo.d.ts.map +0 -1
  235. package/dist/todo.js.map +0 -7
  236. package/dist/tool-diff.d.ts.map +0 -1
  237. package/dist/tool-diff.js.map +0 -7
  238. package/dist/tool-help.d.ts.map +0 -1
  239. package/dist/tool-help.js.map +0 -7
  240. package/dist/tool-icon-map.d.ts.map +0 -1
  241. package/dist/tool-icons.d.ts.map +0 -1
  242. package/dist/tool-icons.js.map +0 -7
  243. package/dist/tool-search.d.ts.map +0 -1
  244. package/dist/tool-search.js.map +0 -7
  245. package/dist/tool-summary.d.ts.map +0 -1
  246. package/dist/tool-summary.js.map +0 -7
  247. package/dist/tool-tier.d.ts.map +0 -1
  248. package/dist/tool-tier.js.map +0 -7
  249. package/dist/tool-use.d.ts.map +0 -1
  250. package/dist/tool-use.js.map +0 -7
  251. package/dist/tree.d.ts.map +0 -1
  252. package/dist/tree.js.map +0 -7
  253. package/dist/typecheck.d.ts.map +0 -1
  254. package/dist/typecheck.js.map +0 -7
  255. package/dist/win32.d.ts.map +0 -1
  256. package/dist/win32.js.map +0 -7
  257. package/dist/write.d.ts.map +0 -1
  258. package/dist/write.js.map +0 -7
package/dist/install.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/install.ts
2
- import { join as join7 } from "node:path";
2
+ import { join as join8 } from "node:path";
3
3
  import {
4
4
  detectEcosystem as detectPackageEcosystem,
5
5
  recordPackageAction
@@ -85,11 +85,12 @@ function createOutputSpool(opts) {
85
85
  return {
86
86
  write(text) {
87
87
  if (finalized || !text) return;
88
- totalBytes += Buffer.byteLength(text, "utf8");
88
+ const textBytes = Buffer.byteLength(text, "utf8");
89
+ totalBytes += textBytes;
89
90
  if (!stream && !failed) {
90
- if (headBytes + text.length <= threshold) {
91
+ if (headBytes + textBytes <= threshold) {
91
92
  head += text;
92
- headBytes += text.length;
93
+ headBytes += textBytes;
93
94
  return;
94
95
  }
95
96
  head += text;
@@ -99,7 +100,7 @@ function createOutputSpool(opts) {
99
100
  }
100
101
  if (stream) {
101
102
  if (stream.writableLength > SPOOL_WRITE_HWM_BYTES) {
102
- droppedBytes += Buffer.byteLength(text, "utf8");
103
+ droppedBytes += textBytes;
103
104
  return;
104
105
  }
105
106
  stream.write(text);
@@ -121,6 +122,52 @@ function createOutputSpool(opts) {
121
122
  };
122
123
  }
123
124
 
125
+ // src/_win32-resolve.ts
126
+ import * as fs from "node:fs";
127
+ import * as path2 from "node:path";
128
+ function resolveWin32Command(cmd) {
129
+ if (process.platform !== "win32") return cmd;
130
+ if (cmd.includes("/") || cmd.includes("\\") || path2.extname(cmd.replace(/\//g, "\\"))) {
131
+ return cmd;
132
+ }
133
+ const pathext = (process.env["PATHEXT"] ?? ".COM;.EXE;.BAT;.CMD;.VBS;.JS;.WS;.MSC").toLowerCase().split(";");
134
+ const pathDirs = (process.env["PATH"] ?? "").split(path2.delimiter);
135
+ for (const dir of pathDirs) {
136
+ const base = path2.join(dir, cmd);
137
+ for (const ext of pathext) {
138
+ const full = `${base}${ext}`;
139
+ try {
140
+ fs.accessSync(full, fs.constants.X_OK);
141
+ return full;
142
+ } catch {
143
+ }
144
+ }
145
+ }
146
+ return cmd;
147
+ }
148
+ var WIN32_SHELL_META = /[&|<>"\r\n\0]/;
149
+ function assertSafeWin32ShellArgs(args) {
150
+ for (const arg of args) {
151
+ if (typeof arg === "string" && WIN32_SHELL_META.test(arg)) {
152
+ throw new Error(
153
+ '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)
154
+ );
155
+ }
156
+ }
157
+ }
158
+ function buildWin32CmdShimInvocation(command, args = []) {
159
+ assertSafeWin32ShellArgs([command, ...args]);
160
+ const line = ["call", quoteWin32CmdArg(command), ...args.map(quoteWin32CmdArg)].join(" ");
161
+ return {
162
+ command: process.env["COMSPEC"] ?? "cmd.exe",
163
+ args: ["/d", "/c", line],
164
+ windowsVerbatimArguments: true
165
+ };
166
+ }
167
+ function quoteWin32CmdArg(arg) {
168
+ return `"${arg}"`;
169
+ }
170
+
124
171
  // src/process-registry.ts
125
172
  import { spawn } from "node:child_process";
126
173
  import * as os from "node:os";
@@ -452,6 +499,7 @@ var ProcessRegistryImpl = class {
452
499
  }
453
500
  /** Get all tracked processes. */
454
501
  list() {
502
+ this._pruneAllStale();
455
503
  return Array.from(this.processes.values());
456
504
  }
457
505
  /** Get processes filtered by name (e.g. 'bash', 'exec'). */
@@ -482,6 +530,7 @@ var ProcessRegistryImpl = class {
482
530
  * Combined stats for observability — used by /ps and the TUI status bar.
483
531
  */
484
532
  stats() {
533
+ this._pruneAllStale();
485
534
  return {
486
535
  activeCount: this.activeCount,
487
536
  backgroundCount: this.activeBackgroundCount,
@@ -721,6 +770,19 @@ var ProcessRegistryImpl = class {
721
770
  this.processes.delete(pid);
722
771
  }
723
772
  }
773
+ /**
774
+ * Remove every stale entry, not just one PID. `list()`/`stats()` — the
775
+ * surfaces the TUI status bar and `/ps` poll — must prune too: a child
776
+ * whose 'close' event never fires (e.g. Windows grandchildren holding stdio
777
+ * open) would otherwise linger in the registry until someone looks up its
778
+ * exact PID, and PID reuse meanwhile makes `get()`/`kill()` target the
779
+ * wrong process. RAM-leak audit 2026-07-31, MEDIUM.
780
+ */
781
+ _pruneAllStale() {
782
+ for (const [pid, entry] of this.processes) {
783
+ if (this._isStaleEntry(entry)) this.processes.delete(pid);
784
+ }
785
+ }
724
786
  };
725
787
  var _registry;
726
788
  function getProcessRegistry() {
@@ -730,60 +792,17 @@ function getProcessRegistry() {
730
792
  return _registry;
731
793
  }
732
794
 
733
- // src/_win32-resolve.ts
734
- import * as fs from "node:fs";
735
- import * as path2 from "node:path";
736
- function resolveWin32Command(cmd) {
737
- if (process.platform !== "win32") return cmd;
738
- if (cmd.includes("/") || cmd.includes("\\") || path2.extname(cmd.replace(/\//g, "\\"))) {
739
- return cmd;
740
- }
741
- const pathext = (process.env["PATHEXT"] ?? ".COM;.EXE;.BAT;.CMD;.VBS;.JS;.WS;.MSC").toLowerCase().split(";");
742
- const pathDirs = (process.env["PATH"] ?? "").split(path2.delimiter);
743
- for (const dir of pathDirs) {
744
- const base = path2.join(dir, cmd);
745
- for (const ext of pathext) {
746
- const full = `${base}${ext}`;
747
- try {
748
- fs.accessSync(full, fs.constants.X_OK);
749
- return full;
750
- } catch {
751
- }
752
- }
753
- }
754
- return cmd;
755
- }
756
- var WIN32_SHELL_META = /[&|<>"\r\n\0]/;
757
- function assertSafeWin32ShellArgs(args) {
758
- for (const arg of args) {
759
- if (typeof arg === "string" && WIN32_SHELL_META.test(arg)) {
760
- throw new Error(
761
- '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)
762
- );
763
- }
764
- }
765
- }
766
- function buildWin32CmdShimInvocation(command, args = []) {
767
- assertSafeWin32ShellArgs([command, ...args]);
768
- const line = ["call", quoteWin32CmdArg(command), ...args.map(quoteWin32CmdArg)].join(" ");
769
- return {
770
- command: process.env["COMSPEC"] ?? "cmd.exe",
771
- args: ["/d", "/c", line],
772
- windowsVerbatimArguments: true
773
- };
774
- }
775
- function quoteWin32CmdArg(arg) {
776
- return `"${arg}"`;
777
- }
778
-
779
795
  // src/_spawn-stream.ts
780
796
  var isWin = process.platform === "win32";
781
797
  async function* spawnStream(opts) {
782
798
  const max = opts.maxBytes ?? 2e5;
783
799
  const flushAt = opts.flushBytes ?? 4 * 1024;
784
800
  const maxQueue = opts.maxQueueSize ?? 500;
801
+ const maxQueueBytes = opts.maxQueueBytes ?? 1024 * 1024;
785
802
  let stdout = "";
786
803
  let stderr = "";
804
+ let stdoutRetainedBytes = 0;
805
+ let stderrRetainedBytes = 0;
787
806
  let pending = "";
788
807
  let error;
789
808
  const spool = createOutputSpool({ tool: opts.cmd, thresholdBytes: max });
@@ -825,6 +844,7 @@ async function* spawnStream(opts) {
825
844
  });
826
845
  }
827
846
  const queue = [];
847
+ let queuedBytes = 0;
828
848
  let waiter;
829
849
  let paused = false;
830
850
  const wake = () => {
@@ -835,7 +855,7 @@ async function* spawnStream(opts) {
835
855
  }
836
856
  };
837
857
  const resume = () => {
838
- if (paused && queue.length < maxQueue) {
858
+ if (paused && queue.length <= Math.floor(maxQueue / 2) && queuedBytes <= Math.floor(maxQueueBytes / 2)) {
839
859
  paused = false;
840
860
  child.stdout?.resume();
841
861
  child.stderr?.resume();
@@ -845,11 +865,16 @@ async function* spawnStream(opts) {
845
865
  const s = c.toString();
846
866
  stdoutBytes += c.byteLength;
847
867
  emitProcessOutput({ pid, stream: "stdout", chunk: c });
848
- if (stdout.length < max) stdout += s;
868
+ if (stdoutRetainedBytes < max) {
869
+ const retained = utf8Prefix(s, max - stdoutRetainedBytes);
870
+ stdout += retained;
871
+ stdoutRetainedBytes += Buffer.byteLength(retained, "utf8");
872
+ }
849
873
  spool.write(s);
850
- queue.push({ kind: "out", data: s });
874
+ queue.push({ kind: "out", data: s, bytes: c.byteLength });
875
+ queuedBytes += c.byteLength;
851
876
  wake();
852
- if (!paused && queue.length >= maxQueue) {
877
+ if (!paused && (queue.length >= maxQueue || queuedBytes >= maxQueueBytes)) {
853
878
  paused = true;
854
879
  child.stdout?.pause();
855
880
  child.stderr?.pause();
@@ -859,11 +884,16 @@ async function* spawnStream(opts) {
859
884
  const s = c.toString();
860
885
  stderrBytes += c.byteLength;
861
886
  emitProcessOutput({ pid, stream: "stderr", chunk: c });
862
- if (stderr.length < max) stderr += s;
887
+ if (stderrRetainedBytes < max) {
888
+ const retained = utf8Prefix(s, max - stderrRetainedBytes);
889
+ stderr += retained;
890
+ stderrRetainedBytes += Buffer.byteLength(retained, "utf8");
891
+ }
863
892
  spool.write(s);
864
- queue.push({ kind: "err", data: s });
893
+ queue.push({ kind: "err", data: s, bytes: c.byteLength });
894
+ queuedBytes += c.byteLength;
865
895
  wake();
866
- if (!paused && queue.length >= maxQueue) {
896
+ if (!paused && (queue.length >= maxQueue || queuedBytes >= maxQueueBytes)) {
867
897
  paused = true;
868
898
  child.stdout?.pause();
869
899
  child.stderr?.pause();
@@ -873,7 +903,9 @@ async function* spawnStream(opts) {
873
903
  child.stderr?.on("data", onErr);
874
904
  child.on("error", (e) => {
875
905
  error = e.message;
876
- queue.push({ kind: "error", data: e.message });
906
+ const bytes = Buffer.byteLength(e.message, "utf8");
907
+ queue.push({ kind: "error", data: e.message, bytes });
908
+ queuedBytes += bytes;
877
909
  wake();
878
910
  });
879
911
  const completeTelemetry = (code, signal, timedOut = false) => {
@@ -894,7 +926,13 @@ async function* spawnStream(opts) {
894
926
  if (typeof pid === "number") registry.unregister(pid);
895
927
  const exitCode2 = code ?? (signal ? 1 : 0);
896
928
  completeTelemetry(exitCode2, signal ?? void 0);
897
- queue.push({ kind: "close", data: "", code: exitCode2, ...signal ? { signal } : {} });
929
+ queue.push({
930
+ kind: "close",
931
+ data: "",
932
+ bytes: 0,
933
+ code: exitCode2,
934
+ ...signal ? { signal } : {}
935
+ });
898
936
  wake();
899
937
  });
900
938
  const onAbort = () => {
@@ -906,7 +944,7 @@ async function* spawnStream(opts) {
906
944
  } catch {
907
945
  }
908
946
  }
909
- queue.push({ kind: "close", data: "", code: 124 });
947
+ queue.push({ kind: "close", data: "", bytes: 0, code: 124 });
910
948
  completeTelemetry(124, "SIGKILL", true);
911
949
  wake();
912
950
  };
@@ -924,6 +962,7 @@ async function* spawnStream(opts) {
924
962
  });
925
963
  }
926
964
  const chunk = queue.shift();
965
+ queuedBytes -= chunk.bytes;
927
966
  resume();
928
967
  if (chunk.kind === "close") {
929
968
  if (!spawnFailed) exitCode = chunk.code ?? 0;
@@ -950,7 +989,7 @@ async function* spawnStream(opts) {
950
989
  stdout: spooled ? stdout + spoolNote(spooled) : stdout,
951
990
  stderr,
952
991
  exitCode,
953
- truncated: stdout.length >= max || stderr.length >= max,
992
+ truncated: stdoutBytes > stdoutRetainedBytes || stderrBytes > stderrRetainedBytes,
954
993
  error,
955
994
  spoolPath: spooled?.path,
956
995
  spoolBytes: spooled?.bytes
@@ -974,6 +1013,20 @@ async function* spawnStream(opts) {
974
1013
  }
975
1014
  }
976
1015
  }
1016
+ function utf8Prefix(text, maxBytes) {
1017
+ if (maxBytes <= 0) return "";
1018
+ if (Buffer.byteLength(text, "utf8") <= maxBytes) return text;
1019
+ let low = 0;
1020
+ let high = text.length;
1021
+ while (low < high) {
1022
+ const mid = Math.ceil((low + high) / 2);
1023
+ if (Buffer.byteLength(text.slice(0, mid), "utf8") <= maxBytes) low = mid;
1024
+ else high = mid - 1;
1025
+ }
1026
+ let end = low;
1027
+ if (end > 0 && /[\uD800-\uDBFF]/.test(text[end - 1])) end--;
1028
+ return text.slice(0, end);
1029
+ }
977
1030
 
978
1031
  // src/_util.ts
979
1032
  import * as path3 from "node:path";
@@ -2555,8 +2608,8 @@ async function scanDirectory(directory, depth, profiles, limits, state, extraIgn
2555
2608
  collectFileEvidence(directory, fullPath, entry.name, profiles, state);
2556
2609
  }
2557
2610
  }
2558
- function collectFileEvidence(directory, fullPath, basename4, profiles, state) {
2559
- const lower = basename4.toLowerCase();
2611
+ function collectFileEvidence(directory, fullPath, basename5, profiles, state) {
2612
+ const lower = basename5.toLowerCase();
2560
2613
  const extension = path4.extname(lower);
2561
2614
  for (const profile of profiles) {
2562
2615
  const detector = profile.detectors.find(
@@ -2567,7 +2620,7 @@ function collectFileEvidence(directory, fullPath, basename4, profiles, state) {
2567
2620
  candidate.evidence.push({
2568
2621
  kind: detector.kind,
2569
2622
  path: fullPath,
2570
- value: basename4,
2623
+ value: basename5,
2571
2624
  weight: detector.weight
2572
2625
  });
2573
2626
  if (detector.kind === "manifest" || detector.kind === "config") {
@@ -3186,10 +3239,11 @@ var MAX_ARGUMENTS = 128;
3186
3239
  var MAX_ARGUMENT_LENGTH = 4096;
3187
3240
  var PACKAGE_NAME_RE = /^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+(?:@[a-z0-9*+._~^<>=|-]+)?$/i;
3188
3241
  var COMPOSER_PACKAGE_RE = /^[a-z0-9_.-]+\/[a-z0-9_.-]+(?::[a-z0-9*+._~^<>=|-]+)?$/i;
3189
- var GO_MODULE_RE = /^(?:[a-z0-9.-]+\.)+[a-z0-9.-]+\/[a-z0-9._~+/@-]+$/i;
3242
+ var GO_MODULE_RE = /^(?:[a-z0-9-]+\.)+[a-z0-9-]+\/[a-z0-9._~+/@-]+$/i;
3190
3243
  var PYTHON_PACKAGE_RE = /^[a-z0-9._-]+(?:\[[a-z0-9._,-]+\])?==[a-z0-9*+._!~-]+$/i;
3191
3244
  async function planLanguageOperation(options) {
3192
3245
  validateOperationOptions(options.operation, options.operationOptions?.packages);
3246
+ validateFilterOption(options.operationOptions?.filter);
3193
3247
  const detection = await detectLanguageWorkspaces(options);
3194
3248
  const candidates = detection.workspaces.filter(
3195
3249
  (workspace) => workspace.capabilities.includes(options.operation)
@@ -3362,6 +3416,15 @@ function workspaceDepth(workspaceRoot, projectRoot) {
3362
3416
  const relative5 = path6.relative(path6.resolve(projectRoot), path6.resolve(workspaceRoot));
3363
3417
  return relative5 === "" ? 0 : relative5.split(path6.sep).length;
3364
3418
  }
3419
+ function validateFilterOption(filter) {
3420
+ if (filter === void 0) return;
3421
+ if (filter.startsWith("-") || filter.includes(" --")) {
3422
+ throw new Error(`Invalid test filter (parsed as a runner option): "${filter}"`);
3423
+ }
3424
+ if (filter.length > MAX_ARGUMENT_LENGTH || /[\r\n\0]/.test(filter)) {
3425
+ throw new Error("Invalid test filter (oversized or contains a control character)");
3426
+ }
3427
+ }
3365
3428
  function validateOperationOptions(operation, packages) {
3366
3429
  if (!operation.startsWith("package-") || !packages) return;
3367
3430
  for (const value of packages) {
@@ -3744,6 +3807,10 @@ var installTool = {
3744
3807
  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.",
3745
3808
  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.",
3746
3809
  permission: "confirm",
3810
+ // WS-046: gives permission decisions something to key on.
3811
+ // What gets installed is the subject — installing a package runs its
3812
+ // lifecycle scripts, so `express` and `evil-pkg` must not be interchangeable.
3813
+ subjectKey: "packages",
3747
3814
  mutating: true,
3748
3815
  riskTier: "standard",
3749
3816
  icon: "package",
@@ -3913,10 +3980,10 @@ function resolveManifestPath(cwd, pkgManager) {
3913
3980
  case "pnpm":
3914
3981
  case "yarn":
3915
3982
  case "npm":
3916
- return join7(cwd, "package.json");
3983
+ return join8(cwd, "package.json");
3917
3984
  /* v8 ignore next 2 -- pkgManager is always pnpm/yarn/npm; the default is defensive. */
3918
3985
  default:
3919
- return join7(cwd, "package.json");
3986
+ return join8(cwd, "package.json");
3920
3987
  }
3921
3988
  }
3922
3989
  export {
package/dist/json.js CHANGED
@@ -27,12 +27,13 @@ function ensureInsideRoot(absPath, ctx) {
27
27
  function safeResolve(input, ctx) {
28
28
  return ensureInsideRoot(resolvePath(input, ctx), ctx);
29
29
  }
30
- async function assertRealInsideRoot(absPath, ctx) {
31
- if (ctx.allowOutsideProjectRoot) return;
30
+ async function resolveRealInsideRoot(absPath, ctx) {
31
+ if (ctx.allowOutsideProjectRoot) return absPath;
32
32
  const realRoots = await Promise.all(
33
33
  allowedRoots(ctx).map((r) => fsp.realpath(r).catch(() => path.resolve(r)))
34
34
  );
35
35
  let probe = absPath;
36
+ const pendingTail = [];
36
37
  for (; ; ) {
37
38
  let real;
38
39
  try {
@@ -40,13 +41,16 @@ async function assertRealInsideRoot(absPath, ctx) {
40
41
  } catch (err) {
41
42
  if (err.code === "ENOENT") {
42
43
  const parent = path.dirname(probe);
43
- if (parent === probe) return;
44
+ if (parent === probe) return absPath;
45
+ pendingTail.unshift(path.basename(probe));
44
46
  probe = parent;
45
47
  continue;
46
48
  }
47
49
  throw err;
48
50
  }
49
- if (isInsideAny(real, realRoots)) return;
51
+ if (isInsideAny(real, realRoots)) {
52
+ return pendingTail.length > 0 ? path.join(real, ...pendingTail) : real;
53
+ }
50
54
  throw new Error(
51
55
  `Path "${absPath}" resolves through a symlink outside project root "${realRoots[0]}"`
52
56
  );
@@ -54,11 +58,28 @@ async function assertRealInsideRoot(absPath, ctx) {
54
58
  }
55
59
  async function safeResolveReal(input, ctx) {
56
60
  const abs = safeResolve(input, ctx);
57
- await assertRealInsideRoot(abs, ctx);
58
- return abs;
61
+ return await resolveRealInsideRoot(abs, ctx);
59
62
  }
60
63
 
61
64
  // src/json.ts
65
+ var MAX_JSON_FILE_BYTES = 16 * 1024 * 1024;
66
+ var MAX_JSON_FILE_BYTES_HUMAN = "16 MiB";
67
+ var JsonFileTooLargeError = class extends Error {
68
+ constructor(filePath, size) {
69
+ super(
70
+ `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.`
71
+ );
72
+ this.name = "JsonFileTooLargeError";
73
+ }
74
+ };
75
+ async function readJsonFileBounded(filePath, ctx) {
76
+ const resolved = await safeResolveReal(filePath, ctx);
77
+ const stat2 = await fs.stat(resolved);
78
+ if (stat2.size > MAX_JSON_FILE_BYTES) {
79
+ throw new JsonFileTooLargeError(filePath, stat2.size);
80
+ }
81
+ return fs.readFile(resolved, "utf8");
82
+ }
62
83
  var jsonTool = {
63
84
  name: "json",
64
85
  category: "Data",
@@ -66,6 +87,7 @@ var jsonTool = {
66
87
  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.',
67
88
  permission: "auto",
68
89
  mutating: false,
90
+ maxOutputBytes: 262144,
69
91
  timeoutMs: 5e3,
70
92
  capabilities: ["fs.read"],
71
93
  icon: "json",
@@ -132,9 +154,15 @@ async function executeParse(input, ctx) {
132
154
  let raw;
133
155
  if (input.file) {
134
156
  try {
135
- raw = await fs.readFile(await safeResolveReal(input.file, ctx), "utf8");
136
- } catch {
137
- return { data: null, formatted: "", type: "unknown", action: "parse", error: "Could not read file" };
157
+ raw = await readJsonFileBounded(input.file, ctx);
158
+ } catch (error) {
159
+ return {
160
+ data: null,
161
+ formatted: "",
162
+ type: "unknown",
163
+ action: "parse",
164
+ error: error instanceof JsonFileTooLargeError ? error.message : "Could not read file"
165
+ };
138
166
  }
139
167
  } else if (input.data) {
140
168
  raw = input.data;
@@ -190,10 +218,16 @@ async function executeQuery(input, ctx) {
190
218
  let parsed;
191
219
  if (input.file) {
192
220
  try {
193
- const raw = await fs.readFile(await safeResolveReal(input.file, ctx), "utf8");
221
+ const raw = await readJsonFileBounded(input.file, ctx);
194
222
  parsed = JSON.parse(raw);
195
- } catch {
196
- return { data: null, formatted: "", type: "unknown", action: "query", error: "Could not read/parse file" };
223
+ } catch (error) {
224
+ return {
225
+ data: null,
226
+ formatted: "",
227
+ type: "unknown",
228
+ action: "query",
229
+ error: error instanceof JsonFileTooLargeError ? error.message : "Could not read/parse file"
230
+ };
197
231
  }
198
232
  } else if (input.data) {
199
233
  try {
@@ -232,10 +266,16 @@ async function executeValidate(input, ctx) {
232
266
  let parsed;
233
267
  if (input.file) {
234
268
  try {
235
- const raw = await fs.readFile(await safeResolveReal(input.file, ctx), "utf8");
269
+ const raw = await readJsonFileBounded(input.file, ctx);
236
270
  parsed = JSON.parse(raw);
237
- } catch {
238
- return { data: null, formatted: "", type: "unknown", action: "validate", error: "Could not read/parse file" };
271
+ } catch (error) {
272
+ return {
273
+ data: null,
274
+ formatted: "",
275
+ type: "unknown",
276
+ action: "validate",
277
+ error: error instanceof JsonFileTooLargeError ? error.message : "Could not read/parse file"
278
+ };
239
279
  }
240
280
  } else if (input.data) {
241
281
  try {
@@ -274,10 +314,16 @@ async function executeTransform(input, ctx) {
274
314
  let parsed;
275
315
  if (input.file) {
276
316
  try {
277
- const raw = await fs.readFile(await safeResolveReal(input.file, ctx), "utf8");
317
+ const raw = await readJsonFileBounded(input.file, ctx);
278
318
  parsed = JSON.parse(raw);
279
- } catch {
280
- return { data: null, formatted: "", type: "unknown", action: "transform", error: "Could not read/parse file" };
319
+ } catch (error) {
320
+ return {
321
+ data: null,
322
+ formatted: "",
323
+ type: "unknown",
324
+ action: "transform",
325
+ error: error instanceof JsonFileTooLargeError ? error.message : "Could not read/parse file"
326
+ };
281
327
  }
282
328
  } else if (input.data) {
283
329
  try {
@@ -8,8 +8,17 @@ export declare function boardCreateInput(input: KanbanToolInput, title: string):
8
8
  mode: "assess" | "enforce" | "off";
9
9
  decomposition: "auto" | "propose";
10
10
  };
11
- completionGate?: {
12
- enforcement: import("@wrongstack/kanban").KanbanCompletionGateEnforcement;
11
+ } | {
12
+ completionGate: {
13
+ enforcement: 'strict' | 'soft';
14
+ };
15
+ title: string;
16
+ description?: string;
17
+ tags?: string[];
18
+ generatedBy?: string;
19
+ atomicity?: {
20
+ mode: "assess" | "enforce" | "off";
21
+ decomposition: "auto" | "propose";
13
22
  };
14
23
  };
15
24
  export declare function boardUpdatePatch(input: KanbanToolInput): {
@@ -20,8 +29,16 @@ export declare function boardUpdatePatch(input: KanbanToolInput): {
20
29
  mode: "assess" | "enforce" | "off";
21
30
  decomposition: "auto" | "propose";
22
31
  };
23
- completionGate?: {
24
- enforcement: import("@wrongstack/kanban").KanbanCompletionGateEnforcement;
32
+ } | {
33
+ completionGate: {
34
+ enforcement: 'strict' | 'soft';
35
+ };
36
+ title?: string;
37
+ description?: string;
38
+ tags?: string[];
39
+ atomicity?: {
40
+ mode: "assess" | "enforce" | "off";
41
+ decomposition: "auto" | "propose";
25
42
  };
26
43
  };
27
44
  export declare function duplicateBoardOptions(input: KanbanToolInput): {
package/dist/kanban.js CHANGED
@@ -345,6 +345,10 @@ async function handleKanbanDetailAction(projectRoot, input) {
345
345
  }
346
346
 
347
347
  // src/kanban-board-inputs.ts
348
+ function agentSettableGate(enforcement) {
349
+ if (enforcement === void 0 || enforcement === "off") return {};
350
+ return { completionGate: { enforcement } };
351
+ }
348
352
  function boardCreateInput(input, title) {
349
353
  return {
350
354
  title,
@@ -357,7 +361,7 @@ function boardCreateInput(input, title) {
357
361
  decomposition: input.atomicityDecomposition ?? "propose"
358
362
  }
359
363
  } : {},
360
- ...input.gateEnforcement !== void 0 ? { completionGate: { enforcement: input.gateEnforcement } } : {}
364
+ ...agentSettableGate(input.gateEnforcement)
361
365
  };
362
366
  }
363
367
  function boardUpdatePatch(input) {
@@ -371,7 +375,7 @@ function boardUpdatePatch(input) {
371
375
  decomposition: input.atomicityDecomposition ?? "propose"
372
376
  }
373
377
  } : {},
374
- ...input.gateEnforcement !== void 0 ? { completionGate: { enforcement: input.gateEnforcement } } : {}
378
+ ...agentSettableGate(input.gateEnforcement)
375
379
  };
376
380
  }
377
381
  function duplicateBoardOptions(input) {
@@ -386,6 +390,7 @@ function duplicateBoardOptions(input) {
386
390
 
387
391
  // src/kanban-task-inputs.ts
388
392
  import { randomUUID } from "node:crypto";
393
+ import { clampSubagentCapabilities } from "@wrongstack/core/security";
389
394
  function taskInput(input) {
390
395
  const assignment = hasAssignmentInput(input) ? assignmentForTaskCreate(input) : void 0;
391
396
  return {
@@ -483,6 +488,10 @@ function taskPatch(input) {
483
488
  ...input.actualHours !== void 0 ? { actualHours: input.actualHours } : {}
484
489
  };
485
490
  }
491
+ function clampRequestedCapabilities(requested) {
492
+ if (requested === void 0) return void 0;
493
+ return clampSubagentCapabilities(requested).granted;
494
+ }
486
495
  function assignmentInput(input) {
487
496
  return {
488
497
  agentId: input.agentId,
@@ -493,7 +502,7 @@ function assignmentInput(input) {
493
502
  fallbackProfile: input.fallbackProfile,
494
503
  fallbackModels: input.fallbackModels,
495
504
  tools: input.tools,
496
- allowedCapabilities: input.allowedCapabilities,
505
+ allowedCapabilities: clampRequestedCapabilities(input.allowedCapabilities),
497
506
  assignee: input.assignee,
498
507
  leaseId: input.leaseId,
499
508
  claimedAt: input.claimedAt,
@@ -520,7 +529,7 @@ function assignmentForTaskCreate(input) {
520
529
  ...input.fallbackProfile !== void 0 ? { fallbackProfile: input.fallbackProfile } : {},
521
530
  ...input.fallbackModels !== void 0 ? { fallbackModels: input.fallbackModels } : {},
522
531
  ...input.tools !== void 0 ? { tools: input.tools } : {},
523
- ...input.allowedCapabilities !== void 0 ? { allowedCapabilities: input.allowedCapabilities } : {},
532
+ ...input.allowedCapabilities !== void 0 ? { allowedCapabilities: clampRequestedCapabilities(input.allowedCapabilities) } : {},
524
533
  ...input.leaseId !== void 0 ? { leaseId: input.leaseId } : {},
525
534
  ...input.claimedAt !== void 0 ? { claimedAt: input.claimedAt } : {},
526
535
  ...input.heartbeatAt !== void 0 ? { heartbeatAt: input.heartbeatAt } : {},
@@ -731,7 +740,13 @@ var KANBAN_INPUT_SCHEMA = {
731
740
  moveTasksToColumnId: { type: "string" },
732
741
  atomicityMode: { type: "string", enum: ["off", "assess", "enforce"] },
733
742
  atomicityDecomposition: { type: "string", enum: ["auto", "propose"] },
734
- gateEnforcement: { type: "string", enum: ["strict", "soft", "off"] },
743
+ gateEnforcement: {
744
+ type: "string",
745
+ // WS-023: `'off'` is deliberately absent. The agent whose work this gate
746
+ // checks must not be able to switch it off; it may only tighten. Turning
747
+ // a gate off stays a human decision, made through board config.
748
+ enum: ["strict", "soft"]
749
+ },
735
750
  subtasks: {
736
751
  type: "array",
737
752
  minItems: 2,