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