@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.
- package/dist/_spawn-stream.d.ts +2 -0
- package/dist/_util.d.ts +29 -1
- package/dist/audit.js +113 -65
- package/dist/bash.js +5 -4
- package/dist/batch-tool-use.js +70 -7
- package/dist/browser/index.js +31 -1
- package/dist/builtin.js +841 -351
- package/dist/codebase-index/index.js +68 -16
- package/dist/codebase-index/project-server-protocol.d.ts +26 -0
- package/dist/codebase-index/project-server.js +60 -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 +560 -529
- package/dist/format.js +116 -65
- package/dist/git.js +22 -2
- package/dist/glob.js +11 -5
- package/dist/grep.js +160 -104
- package/dist/index.js +792 -290
- package/dist/install.js +120 -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 +128 -71
- package/dist/lint.js +113 -65
- package/dist/outdated.js +86 -26
- package/dist/pack.js +841 -351
- package/dist/patch.js +11 -1
- package/dist/plan.js +32 -10
- package/dist/read.js +77 -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 +113 -65
- package/dist/todo.js +3 -1
- package/dist/tool-tier.js +841 -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 +113 -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/index.js
CHANGED
|
@@ -148,11 +148,12 @@ function createOutputSpool(opts) {
|
|
|
148
148
|
return {
|
|
149
149
|
write(text) {
|
|
150
150
|
if (finalized || !text) return;
|
|
151
|
-
|
|
151
|
+
const textBytes = Buffer.byteLength(text, "utf8");
|
|
152
|
+
totalBytes += textBytes;
|
|
152
153
|
if (!stream && !failed) {
|
|
153
|
-
if (headBytes +
|
|
154
|
+
if (headBytes + textBytes <= threshold) {
|
|
154
155
|
head += text;
|
|
155
|
-
headBytes +=
|
|
156
|
+
headBytes += textBytes;
|
|
156
157
|
return;
|
|
157
158
|
}
|
|
158
159
|
head += text;
|
|
@@ -162,7 +163,7 @@ function createOutputSpool(opts) {
|
|
|
162
163
|
}
|
|
163
164
|
if (stream) {
|
|
164
165
|
if (stream.writableLength > SPOOL_WRITE_HWM_BYTES) {
|
|
165
|
-
droppedBytes +=
|
|
166
|
+
droppedBytes += textBytes;
|
|
166
167
|
return;
|
|
167
168
|
}
|
|
168
169
|
stream.write(text);
|
|
@@ -849,8 +850,11 @@ async function* spawnStream(opts) {
|
|
|
849
850
|
const max = opts.maxBytes ?? 2e5;
|
|
850
851
|
const flushAt = opts.flushBytes ?? 4 * 1024;
|
|
851
852
|
const maxQueue = opts.maxQueueSize ?? 500;
|
|
853
|
+
const maxQueueBytes = opts.maxQueueBytes ?? 1024 * 1024;
|
|
852
854
|
let stdout = "";
|
|
853
855
|
let stderr = "";
|
|
856
|
+
let stdoutRetainedBytes = 0;
|
|
857
|
+
let stderrRetainedBytes = 0;
|
|
854
858
|
let pending2 = "";
|
|
855
859
|
let error;
|
|
856
860
|
const spool = createOutputSpool({ tool: opts.cmd, thresholdBytes: max });
|
|
@@ -892,6 +896,7 @@ async function* spawnStream(opts) {
|
|
|
892
896
|
});
|
|
893
897
|
}
|
|
894
898
|
const queue = [];
|
|
899
|
+
let queuedBytes = 0;
|
|
895
900
|
let waiter;
|
|
896
901
|
let paused = false;
|
|
897
902
|
const wake = () => {
|
|
@@ -902,7 +907,7 @@ async function* spawnStream(opts) {
|
|
|
902
907
|
}
|
|
903
908
|
};
|
|
904
909
|
const resume = () => {
|
|
905
|
-
if (paused && queue.length
|
|
910
|
+
if (paused && queue.length <= Math.floor(maxQueue / 2) && queuedBytes <= Math.floor(maxQueueBytes / 2)) {
|
|
906
911
|
paused = false;
|
|
907
912
|
child.stdout?.resume();
|
|
908
913
|
child.stderr?.resume();
|
|
@@ -912,11 +917,16 @@ async function* spawnStream(opts) {
|
|
|
912
917
|
const s = c.toString();
|
|
913
918
|
stdoutBytes += c.byteLength;
|
|
914
919
|
emitProcessOutput({ pid, stream: "stdout", chunk: c });
|
|
915
|
-
if (
|
|
920
|
+
if (stdoutRetainedBytes < max) {
|
|
921
|
+
const retained = utf8Prefix(s, max - stdoutRetainedBytes);
|
|
922
|
+
stdout += retained;
|
|
923
|
+
stdoutRetainedBytes += Buffer.byteLength(retained, "utf8");
|
|
924
|
+
}
|
|
916
925
|
spool.write(s);
|
|
917
|
-
queue.push({ kind: "out", data: s });
|
|
926
|
+
queue.push({ kind: "out", data: s, bytes: c.byteLength });
|
|
927
|
+
queuedBytes += c.byteLength;
|
|
918
928
|
wake();
|
|
919
|
-
if (!paused && queue.length >= maxQueue) {
|
|
929
|
+
if (!paused && (queue.length >= maxQueue || queuedBytes >= maxQueueBytes)) {
|
|
920
930
|
paused = true;
|
|
921
931
|
child.stdout?.pause();
|
|
922
932
|
child.stderr?.pause();
|
|
@@ -926,11 +936,16 @@ async function* spawnStream(opts) {
|
|
|
926
936
|
const s = c.toString();
|
|
927
937
|
stderrBytes += c.byteLength;
|
|
928
938
|
emitProcessOutput({ pid, stream: "stderr", chunk: c });
|
|
929
|
-
if (
|
|
939
|
+
if (stderrRetainedBytes < max) {
|
|
940
|
+
const retained = utf8Prefix(s, max - stderrRetainedBytes);
|
|
941
|
+
stderr += retained;
|
|
942
|
+
stderrRetainedBytes += Buffer.byteLength(retained, "utf8");
|
|
943
|
+
}
|
|
930
944
|
spool.write(s);
|
|
931
|
-
queue.push({ kind: "err", data: s });
|
|
945
|
+
queue.push({ kind: "err", data: s, bytes: c.byteLength });
|
|
946
|
+
queuedBytes += c.byteLength;
|
|
932
947
|
wake();
|
|
933
|
-
if (!paused && queue.length >= maxQueue) {
|
|
948
|
+
if (!paused && (queue.length >= maxQueue || queuedBytes >= maxQueueBytes)) {
|
|
934
949
|
paused = true;
|
|
935
950
|
child.stdout?.pause();
|
|
936
951
|
child.stderr?.pause();
|
|
@@ -940,7 +955,9 @@ async function* spawnStream(opts) {
|
|
|
940
955
|
child.stderr?.on("data", onErr);
|
|
941
956
|
child.on("error", (e) => {
|
|
942
957
|
error = e.message;
|
|
943
|
-
|
|
958
|
+
const bytes = Buffer.byteLength(e.message, "utf8");
|
|
959
|
+
queue.push({ kind: "error", data: e.message, bytes });
|
|
960
|
+
queuedBytes += bytes;
|
|
944
961
|
wake();
|
|
945
962
|
});
|
|
946
963
|
const completeTelemetry = (code, signal, timedOut = false) => {
|
|
@@ -961,7 +978,13 @@ async function* spawnStream(opts) {
|
|
|
961
978
|
if (typeof pid === "number") registry.unregister(pid);
|
|
962
979
|
const exitCode2 = code ?? (signal ? 1 : 0);
|
|
963
980
|
completeTelemetry(exitCode2, signal ?? void 0);
|
|
964
|
-
queue.push({
|
|
981
|
+
queue.push({
|
|
982
|
+
kind: "close",
|
|
983
|
+
data: "",
|
|
984
|
+
bytes: 0,
|
|
985
|
+
code: exitCode2,
|
|
986
|
+
...signal ? { signal } : {}
|
|
987
|
+
});
|
|
965
988
|
wake();
|
|
966
989
|
});
|
|
967
990
|
const onAbort = () => {
|
|
@@ -973,7 +996,7 @@ async function* spawnStream(opts) {
|
|
|
973
996
|
} catch {
|
|
974
997
|
}
|
|
975
998
|
}
|
|
976
|
-
queue.push({ kind: "close", data: "", code: 124 });
|
|
999
|
+
queue.push({ kind: "close", data: "", bytes: 0, code: 124 });
|
|
977
1000
|
completeTelemetry(124, "SIGKILL", true);
|
|
978
1001
|
wake();
|
|
979
1002
|
};
|
|
@@ -991,6 +1014,7 @@ async function* spawnStream(opts) {
|
|
|
991
1014
|
});
|
|
992
1015
|
}
|
|
993
1016
|
const chunk = queue.shift();
|
|
1017
|
+
queuedBytes -= chunk.bytes;
|
|
994
1018
|
resume();
|
|
995
1019
|
if (chunk.kind === "close") {
|
|
996
1020
|
if (!spawnFailed) exitCode = chunk.code ?? 0;
|
|
@@ -1017,7 +1041,7 @@ async function* spawnStream(opts) {
|
|
|
1017
1041
|
stdout: spooled ? stdout + spoolNote(spooled) : stdout,
|
|
1018
1042
|
stderr,
|
|
1019
1043
|
exitCode,
|
|
1020
|
-
truncated:
|
|
1044
|
+
truncated: stdoutBytes > stdoutRetainedBytes || stderrBytes > stderrRetainedBytes,
|
|
1021
1045
|
error,
|
|
1022
1046
|
spoolPath: spooled?.path,
|
|
1023
1047
|
spoolBytes: spooled?.bytes
|
|
@@ -1041,13 +1065,27 @@ async function* spawnStream(opts) {
|
|
|
1041
1065
|
}
|
|
1042
1066
|
}
|
|
1043
1067
|
}
|
|
1068
|
+
function utf8Prefix(text, maxBytes) {
|
|
1069
|
+
if (maxBytes <= 0) return "";
|
|
1070
|
+
if (Buffer.byteLength(text, "utf8") <= maxBytes) return text;
|
|
1071
|
+
let low = 0;
|
|
1072
|
+
let high = text.length;
|
|
1073
|
+
while (low < high) {
|
|
1074
|
+
const mid = Math.ceil((low + high) / 2);
|
|
1075
|
+
if (Buffer.byteLength(text.slice(0, mid), "utf8") <= maxBytes) low = mid;
|
|
1076
|
+
else high = mid - 1;
|
|
1077
|
+
}
|
|
1078
|
+
let end = low;
|
|
1079
|
+
if (end > 0 && /[\uD800-\uDBFF]/.test(text[end - 1])) end--;
|
|
1080
|
+
return text.slice(0, end);
|
|
1081
|
+
}
|
|
1044
1082
|
var isWin;
|
|
1045
1083
|
var init_spawn_stream = __esm({
|
|
1046
1084
|
"src/_spawn-stream.ts"() {
|
|
1047
1085
|
"use strict";
|
|
1048
1086
|
init_output_spool();
|
|
1049
|
-
init_process_registry();
|
|
1050
1087
|
init_win32_resolve();
|
|
1088
|
+
init_process_registry();
|
|
1051
1089
|
isWin = process.platform === "win32";
|
|
1052
1090
|
}
|
|
1053
1091
|
});
|
|
@@ -1061,14 +1099,14 @@ function sha256hex(content) {
|
|
|
1061
1099
|
return createHash("sha256").update(content, "utf8").digest("hex");
|
|
1062
1100
|
}
|
|
1063
1101
|
async function detectPackageManager(cwd) {
|
|
1064
|
-
const { stat:
|
|
1102
|
+
const { stat: stat19 } = await import("node:fs/promises");
|
|
1065
1103
|
try {
|
|
1066
|
-
await
|
|
1104
|
+
await stat19(`${cwd}/pnpm-lock.yaml`);
|
|
1067
1105
|
return "pnpm";
|
|
1068
1106
|
} catch {
|
|
1069
1107
|
}
|
|
1070
1108
|
try {
|
|
1071
|
-
await
|
|
1109
|
+
await stat19(`${cwd}/yarn.lock`);
|
|
1072
1110
|
return "yarn";
|
|
1073
1111
|
} catch {
|
|
1074
1112
|
}
|
|
@@ -1096,11 +1134,15 @@ function safeResolve(input, ctx) {
|
|
|
1096
1134
|
return ensureInsideRoot(resolvePath(input, ctx), ctx);
|
|
1097
1135
|
}
|
|
1098
1136
|
async function assertRealInsideRoot(absPath, ctx) {
|
|
1099
|
-
|
|
1137
|
+
await resolveRealInsideRoot(absPath, ctx);
|
|
1138
|
+
}
|
|
1139
|
+
async function resolveRealInsideRoot(absPath, ctx) {
|
|
1140
|
+
if (ctx.allowOutsideProjectRoot) return absPath;
|
|
1100
1141
|
const realRoots = await Promise.all(
|
|
1101
1142
|
allowedRoots(ctx).map((r) => fsp2.realpath(r).catch(() => path3.resolve(r)))
|
|
1102
1143
|
);
|
|
1103
1144
|
let probe2 = absPath;
|
|
1145
|
+
const pendingTail = [];
|
|
1104
1146
|
for (; ; ) {
|
|
1105
1147
|
let real;
|
|
1106
1148
|
try {
|
|
@@ -1108,13 +1150,16 @@ async function assertRealInsideRoot(absPath, ctx) {
|
|
|
1108
1150
|
} catch (err) {
|
|
1109
1151
|
if (err.code === "ENOENT") {
|
|
1110
1152
|
const parent = path3.dirname(probe2);
|
|
1111
|
-
if (parent === probe2) return;
|
|
1153
|
+
if (parent === probe2) return absPath;
|
|
1154
|
+
pendingTail.unshift(path3.basename(probe2));
|
|
1112
1155
|
probe2 = parent;
|
|
1113
1156
|
continue;
|
|
1114
1157
|
}
|
|
1115
1158
|
throw err;
|
|
1116
1159
|
}
|
|
1117
|
-
if (isInsideAny(real, realRoots))
|
|
1160
|
+
if (isInsideAny(real, realRoots)) {
|
|
1161
|
+
return pendingTail.length > 0 ? path3.join(real, ...pendingTail) : real;
|
|
1162
|
+
}
|
|
1118
1163
|
throw new Error(
|
|
1119
1164
|
`Path "${absPath}" resolves through a symlink outside project root "${realRoots[0]}"`
|
|
1120
1165
|
);
|
|
@@ -1122,8 +1167,7 @@ async function assertRealInsideRoot(absPath, ctx) {
|
|
|
1122
1167
|
}
|
|
1123
1168
|
async function safeResolveReal(input, ctx) {
|
|
1124
1169
|
const abs = safeResolve(input, ctx);
|
|
1125
|
-
await
|
|
1126
|
-
return abs;
|
|
1170
|
+
return await resolveRealInsideRoot(abs, ctx);
|
|
1127
1171
|
}
|
|
1128
1172
|
function truncateMiddle(s, max) {
|
|
1129
1173
|
if (Buffer.byteLength(s, "utf8") <= max) return s;
|
|
@@ -2687,8 +2731,8 @@ async function scanDirectory(directory, depth, profiles, limits, state, extraIgn
|
|
|
2687
2731
|
collectFileEvidence(directory, fullPath, entry.name, profiles, state);
|
|
2688
2732
|
}
|
|
2689
2733
|
}
|
|
2690
|
-
function collectFileEvidence(directory, fullPath,
|
|
2691
|
-
const lower =
|
|
2734
|
+
function collectFileEvidence(directory, fullPath, basename14, profiles, state) {
|
|
2735
|
+
const lower = basename14.toLowerCase();
|
|
2692
2736
|
const extension = path4.extname(lower);
|
|
2693
2737
|
for (const profile of profiles) {
|
|
2694
2738
|
const detector = profile.detectors.find(
|
|
@@ -2699,7 +2743,7 @@ function collectFileEvidence(directory, fullPath, basename13, profiles, state) {
|
|
|
2699
2743
|
candidate.evidence.push({
|
|
2700
2744
|
kind: detector.kind,
|
|
2701
2745
|
path: fullPath,
|
|
2702
|
-
value:
|
|
2746
|
+
value: basename14,
|
|
2703
2747
|
weight: detector.weight
|
|
2704
2748
|
});
|
|
2705
2749
|
if (detector.kind === "manifest" || detector.kind === "config") {
|
|
@@ -2827,8 +2871,8 @@ function normalizeLimits(input) {
|
|
|
2827
2871
|
async function canonicalDirectory(input) {
|
|
2828
2872
|
const resolved = path4.resolve(input);
|
|
2829
2873
|
const real = await fs2.realpath(resolved);
|
|
2830
|
-
const
|
|
2831
|
-
if (!
|
|
2874
|
+
const stat19 = await fs2.stat(real);
|
|
2875
|
+
if (!stat19.isDirectory()) throw new Error(`Project root is not a directory: ${input}`);
|
|
2832
2876
|
return real;
|
|
2833
2877
|
}
|
|
2834
2878
|
async function canonicalInside(input, root, label) {
|
|
@@ -3384,6 +3428,7 @@ import * as fs3 from "node:fs/promises";
|
|
|
3384
3428
|
import * as path6 from "node:path";
|
|
3385
3429
|
async function planLanguageOperation(options) {
|
|
3386
3430
|
validateOperationOptions(options.operation, options.operationOptions?.packages);
|
|
3431
|
+
validateFilterOption(options.operationOptions?.filter);
|
|
3387
3432
|
const detection = await detectLanguageWorkspaces(options);
|
|
3388
3433
|
const candidates = detection.workspaces.filter(
|
|
3389
3434
|
(workspace) => workspace.capabilities.includes(options.operation)
|
|
@@ -3556,6 +3601,15 @@ function workspaceDepth(workspaceRoot, projectRoot) {
|
|
|
3556
3601
|
const relative13 = path6.relative(path6.resolve(projectRoot), path6.resolve(workspaceRoot));
|
|
3557
3602
|
return relative13 === "" ? 0 : relative13.split(path6.sep).length;
|
|
3558
3603
|
}
|
|
3604
|
+
function validateFilterOption(filter) {
|
|
3605
|
+
if (filter === void 0) return;
|
|
3606
|
+
if (filter.startsWith("-") || filter.includes(" --")) {
|
|
3607
|
+
throw new Error(`Invalid test filter (parsed as a runner option): "${filter}"`);
|
|
3608
|
+
}
|
|
3609
|
+
if (filter.length > MAX_ARGUMENT_LENGTH || /[\r\n\0]/.test(filter)) {
|
|
3610
|
+
throw new Error("Invalid test filter (oversized or contains a control character)");
|
|
3611
|
+
}
|
|
3612
|
+
}
|
|
3559
3613
|
function validateOperationOptions(operation, packages) {
|
|
3560
3614
|
if (!operation.startsWith("package-") || !packages) return;
|
|
3561
3615
|
for (const value of packages) {
|
|
@@ -3597,7 +3651,7 @@ var init_plan = __esm({
|
|
|
3597
3651
|
MAX_ARGUMENT_LENGTH = 4096;
|
|
3598
3652
|
PACKAGE_NAME_RE = /^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+(?:@[a-z0-9*+._~^<>=|-]+)?$/i;
|
|
3599
3653
|
COMPOSER_PACKAGE_RE = /^[a-z0-9_.-]+\/[a-z0-9_.-]+(?::[a-z0-9*+._~^<>=|-]+)?$/i;
|
|
3600
|
-
GO_MODULE_RE = /^(?:[a-z0-9
|
|
3654
|
+
GO_MODULE_RE = /^(?:[a-z0-9-]+\.)+[a-z0-9-]+\/[a-z0-9._~+/@-]+$/i;
|
|
3601
3655
|
PYTHON_PACKAGE_RE = /^[a-z0-9._-]+(?:\[[a-z0-9._,-]+\])?==[a-z0-9*+._!~-]+$/i;
|
|
3602
3656
|
}
|
|
3603
3657
|
});
|
|
@@ -3717,8 +3771,8 @@ async function executeInternal(options, startedAt) {
|
|
|
3717
3771
|
const target = options.plan.evidence.find((item) => item.kind === "target")?.path;
|
|
3718
3772
|
if (!target) return unavailableResult(options, "Internal syntax plan has no target evidence.");
|
|
3719
3773
|
const safeTarget = await assertContainedFile(target, options.projectRoot);
|
|
3720
|
-
const
|
|
3721
|
-
if (
|
|
3774
|
+
const stat19 = await fs4.stat(safeTarget);
|
|
3775
|
+
if (stat19.size > MAX_INTERNAL_SOURCE_BYTES) {
|
|
3722
3776
|
return unavailableResult(
|
|
3723
3777
|
options,
|
|
3724
3778
|
`Internal syntax target exceeds ${MAX_INTERNAL_SOURCE_BYTES} bytes.`
|
|
@@ -4005,8 +4059,8 @@ async function snapshotPaths(paths) {
|
|
|
4005
4059
|
const existing = [];
|
|
4006
4060
|
for (const candidate of paths) {
|
|
4007
4061
|
try {
|
|
4008
|
-
const
|
|
4009
|
-
if (!
|
|
4062
|
+
const stat19 = await fs4.stat(candidate);
|
|
4063
|
+
if (!stat19.isFile()) continue;
|
|
4010
4064
|
existing.push(candidate);
|
|
4011
4065
|
} catch {
|
|
4012
4066
|
}
|
|
@@ -4034,8 +4088,8 @@ async function snapshotSizes(paths) {
|
|
|
4034
4088
|
const sizes = /* @__PURE__ */ new Map();
|
|
4035
4089
|
for (const candidate of paths) {
|
|
4036
4090
|
try {
|
|
4037
|
-
const
|
|
4038
|
-
if (
|
|
4091
|
+
const stat19 = await fs4.stat(candidate);
|
|
4092
|
+
if (stat19.isFile()) sizes.set(candidate, stat19.size);
|
|
4039
4093
|
} catch {
|
|
4040
4094
|
}
|
|
4041
4095
|
}
|
|
@@ -4195,6 +4249,9 @@ var init_execute_tool = __esm({
|
|
|
4195
4249
|
useInstead: ["language_info", "install", "audit", "outdated"]
|
|
4196
4250
|
},
|
|
4197
4251
|
permission: "confirm",
|
|
4252
|
+
// WS-046: gives permission decisions something to key on.
|
|
4253
|
+
// The action performed (build/test/lint); the cwd is where, not what.
|
|
4254
|
+
subjectKey: "action",
|
|
4198
4255
|
mutating: true,
|
|
4199
4256
|
riskTier: "standard",
|
|
4200
4257
|
capabilities: ["shell.restricted", "fs.write"],
|
|
@@ -4427,6 +4484,9 @@ var init_package_tool = __esm({
|
|
|
4427
4484
|
useInstead: ["language_info", "language", "install", "audit", "outdated"]
|
|
4428
4485
|
},
|
|
4429
4486
|
permission: "confirm",
|
|
4487
|
+
// WS-046: gives permission decisions something to key on.
|
|
4488
|
+
// The package operation performed — install/remove are not interchangeable.
|
|
4489
|
+
subjectKey: "operation",
|
|
4430
4490
|
mutating: true,
|
|
4431
4491
|
riskTier: "destructive",
|
|
4432
4492
|
capabilities: ["shell.restricted", "fs.write", "net.outbound", "package.install"],
|
|
@@ -6481,9 +6541,9 @@ function parseSymbols6(opts) {
|
|
|
6481
6541
|
function regexParse2(opts) {
|
|
6482
6542
|
const { file, content, lang } = opts;
|
|
6483
6543
|
const symbols = [];
|
|
6484
|
-
const
|
|
6485
|
-
const isPackageJson =
|
|
6486
|
-
const isTsconfig =
|
|
6544
|
+
const basename14 = path22.basename(file).toLowerCase();
|
|
6545
|
+
const isPackageJson = basename14 === "package.json";
|
|
6546
|
+
const isTsconfig = basename14 === "tsconfig.json" || basename14 === "tsconfig.build.json";
|
|
6487
6547
|
const isJsonSchema = content.includes("$schema") || content.includes("$id") || content.includes("$ref");
|
|
6488
6548
|
const isOpenApi = content.includes("openapi") || content.includes("swagger");
|
|
6489
6549
|
const lines = content.split("\n");
|
|
@@ -8695,30 +8755,59 @@ ${hint}` : ""),
|
|
|
8695
8755
|
import {
|
|
8696
8756
|
GOVERNED_TOOL_EXECUTOR_META_KEY
|
|
8697
8757
|
} from "@wrongstack/core/types";
|
|
8758
|
+
var BATCHABLE_TOOL_NAMES = [
|
|
8759
|
+
"read",
|
|
8760
|
+
"glob",
|
|
8761
|
+
"grep",
|
|
8762
|
+
"tree",
|
|
8763
|
+
"codebase-stats",
|
|
8764
|
+
"codebase-search",
|
|
8765
|
+
"language_info",
|
|
8766
|
+
"tool_search",
|
|
8767
|
+
"memory_for_file",
|
|
8768
|
+
"memory_for_path",
|
|
8769
|
+
"memory_search"
|
|
8770
|
+
];
|
|
8771
|
+
var BATCHABLE_TOOLS = new Set(BATCHABLE_TOOL_NAMES);
|
|
8772
|
+
var MAX_BATCH_CALLS = 8;
|
|
8773
|
+
var MAX_PARALLEL_BATCH_CALLS = 3;
|
|
8698
8774
|
var batchToolUseTool = {
|
|
8699
8775
|
name: "batch_tool_use",
|
|
8700
8776
|
category: "Meta",
|
|
8701
|
-
description: "Execute a batch of
|
|
8702
|
-
usageHint:
|
|
8777
|
+
description: "Execute a small batch of independent, read-only discovery calls. Process, mutation, browser-action, MCP, delegation, and nested meta tools are not eligible.",
|
|
8778
|
+
usageHint: `BOUNDED READ-ONLY BATCHING:
|
|
8779
|
+
|
|
8780
|
+
- Use only for a small set of independent reads or searches that are all needed now.
|
|
8781
|
+
- Do not wrap normal sequential reasoning or every tool call in a batch.
|
|
8782
|
+
- \`parallel: true\` (default) runs at most three eligible calls concurrently.
|
|
8783
|
+
- \`stop_on_error: true\` makes it fail fast on the first error.
|
|
8784
|
+
- Eligible tools: ${BATCHABLE_TOOL_NAMES.join(", ")}.`,
|
|
8703
8785
|
permission: "confirm",
|
|
8786
|
+
// Scheduling barrier: keep the outer meta-call out of ToolExecutor's
|
|
8787
|
+
// non-mutating fan-out even though every eligible inner tool is read-only.
|
|
8704
8788
|
mutating: true,
|
|
8705
8789
|
timeoutMs: 12e4,
|
|
8706
|
-
capabilities: ["tool.
|
|
8790
|
+
capabilities: ["tool.meta"],
|
|
8707
8791
|
icon: "meta",
|
|
8708
8792
|
inputSchema: {
|
|
8709
8793
|
type: "object",
|
|
8710
8794
|
properties: {
|
|
8711
8795
|
calls: {
|
|
8712
8796
|
type: "array",
|
|
8797
|
+
maxItems: MAX_BATCH_CALLS,
|
|
8713
8798
|
items: {
|
|
8714
8799
|
type: "object",
|
|
8715
8800
|
properties: {
|
|
8716
|
-
tool: {
|
|
8801
|
+
tool: {
|
|
8802
|
+
type: "string",
|
|
8803
|
+
enum: [...BATCHABLE_TOOL_NAMES],
|
|
8804
|
+
description: "An explicitly batch-safe, read-only discovery tool."
|
|
8805
|
+
},
|
|
8717
8806
|
input: { type: "object" }
|
|
8718
8807
|
},
|
|
8719
8808
|
required: ["tool"]
|
|
8720
8809
|
},
|
|
8721
|
-
description:
|
|
8810
|
+
description: `Up to ${MAX_BATCH_CALLS} independent, batch-safe read calls.`
|
|
8722
8811
|
},
|
|
8723
8812
|
stop_on_error: {
|
|
8724
8813
|
type: "boolean",
|
|
@@ -8741,6 +8830,9 @@ var batchToolUseTool = {
|
|
|
8741
8830
|
stop_on_error: false
|
|
8742
8831
|
};
|
|
8743
8832
|
}
|
|
8833
|
+
if (input.calls.length > MAX_BATCH_CALLS) {
|
|
8834
|
+
throw new Error(`batch_tool_use: calls must contain at most ${MAX_BATCH_CALLS} items`);
|
|
8835
|
+
}
|
|
8744
8836
|
const governedExecute = ctx.meta[GOVERNED_TOOL_EXECUTOR_META_KEY];
|
|
8745
8837
|
if (typeof governedExecute !== "function") {
|
|
8746
8838
|
return {
|
|
@@ -8760,8 +8852,11 @@ var batchToolUseTool = {
|
|
|
8760
8852
|
let succeeded = 0;
|
|
8761
8853
|
let failed = 0;
|
|
8762
8854
|
if (input.parallel !== false) {
|
|
8763
|
-
const
|
|
8764
|
-
|
|
8855
|
+
const allResults = await mapWithConcurrency(
|
|
8856
|
+
input.calls,
|
|
8857
|
+
MAX_PARALLEL_BATCH_CALLS,
|
|
8858
|
+
(call) => executeSingle(call, ctx, governedExecute)
|
|
8859
|
+
);
|
|
8765
8860
|
results.push(...allResults);
|
|
8766
8861
|
succeeded = allResults.filter((r) => r.success).length;
|
|
8767
8862
|
failed = allResults.filter((r) => !r.success).length;
|
|
@@ -8786,8 +8881,28 @@ var batchToolUseTool = {
|
|
|
8786
8881
|
};
|
|
8787
8882
|
}
|
|
8788
8883
|
};
|
|
8884
|
+
async function mapWithConcurrency(items, limit, run) {
|
|
8885
|
+
const results = new Array(items.length);
|
|
8886
|
+
let nextIndex = 0;
|
|
8887
|
+
const worker2 = async () => {
|
|
8888
|
+
while (nextIndex < items.length) {
|
|
8889
|
+
const index = nextIndex++;
|
|
8890
|
+
results[index] = await run(items[index]);
|
|
8891
|
+
}
|
|
8892
|
+
};
|
|
8893
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, () => worker2()));
|
|
8894
|
+
return results;
|
|
8895
|
+
}
|
|
8789
8896
|
async function executeSingle(call, ctx, governedExecute) {
|
|
8790
8897
|
const start = Date.now();
|
|
8898
|
+
if (call.tool === batchToolUseTool.name) {
|
|
8899
|
+
return {
|
|
8900
|
+
tool: call.tool,
|
|
8901
|
+
success: false,
|
|
8902
|
+
error: "recursive batch_tool_use execution is not allowed",
|
|
8903
|
+
executionMs: Date.now() - start
|
|
8904
|
+
};
|
|
8905
|
+
}
|
|
8791
8906
|
const tool = ctx.tools.find((candidate) => candidate.name === call.tool);
|
|
8792
8907
|
if (!tool) {
|
|
8793
8908
|
return {
|
|
@@ -8797,6 +8912,14 @@ async function executeSingle(call, ctx, governedExecute) {
|
|
|
8797
8912
|
executionMs: Date.now() - start
|
|
8798
8913
|
};
|
|
8799
8914
|
}
|
|
8915
|
+
if (!BATCHABLE_TOOLS.has(tool.name) || tool.mutating) {
|
|
8916
|
+
return {
|
|
8917
|
+
tool: call.tool,
|
|
8918
|
+
success: false,
|
|
8919
|
+
error: `tool "${call.tool}" is not eligible for batch execution; call it directly. Allowed: ${BATCHABLE_TOOL_NAMES.join(", ")}`,
|
|
8920
|
+
executionMs: Date.now() - start
|
|
8921
|
+
};
|
|
8922
|
+
}
|
|
8800
8923
|
try {
|
|
8801
8924
|
const result = await governedExecute(call.tool, call.input);
|
|
8802
8925
|
return {
|
|
@@ -8823,8 +8946,14 @@ import * as path10 from "node:path";
|
|
|
8823
8946
|
import { atomicWrite, ulid } from "@wrongstack/core/utils";
|
|
8824
8947
|
var ARTIFACT_RETENTION_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
8825
8948
|
var sweptRoots = /* @__PURE__ */ new Set();
|
|
8949
|
+
var MAX_SWEPT_ROOTS = 128;
|
|
8826
8950
|
function sweepOldArtifacts(root) {
|
|
8827
8951
|
if (sweptRoots.has(root)) return;
|
|
8952
|
+
while (sweptRoots.size >= MAX_SWEPT_ROOTS) {
|
|
8953
|
+
const oldest = sweptRoots.values().next().value;
|
|
8954
|
+
if (oldest === void 0) break;
|
|
8955
|
+
sweptRoots.delete(oldest);
|
|
8956
|
+
}
|
|
8828
8957
|
sweptRoots.add(root);
|
|
8829
8958
|
void (async () => {
|
|
8830
8959
|
const cutoff = Date.now() - ARTIFACT_RETENTION_MS;
|
|
@@ -8847,8 +8976,8 @@ function sweepOldArtifacts(root) {
|
|
|
8847
8976
|
for (const name of names) {
|
|
8848
8977
|
const target = path10.join(dir, name);
|
|
8849
8978
|
try {
|
|
8850
|
-
const
|
|
8851
|
-
if (
|
|
8979
|
+
const stat19 = await fs7.stat(target);
|
|
8980
|
+
if (stat19.isFile() && stat19.mtimeMs < cutoff) {
|
|
8852
8981
|
await fs7.rm(target, { force: true });
|
|
8853
8982
|
removed++;
|
|
8854
8983
|
}
|
|
@@ -8876,14 +9005,14 @@ var BrowserArtifactStore = class {
|
|
|
8876
9005
|
}
|
|
8877
9006
|
async record(id, sessionId, kind, target, mimeType) {
|
|
8878
9007
|
await fs7.chmod(target, 384).catch(() => void 0);
|
|
8879
|
-
const [
|
|
9008
|
+
const [stat19, sha256] = await Promise.all([fs7.stat(target), hashFile(target)]);
|
|
8880
9009
|
const artifact = {
|
|
8881
9010
|
id,
|
|
8882
9011
|
kind,
|
|
8883
9012
|
sensitivity: "sensitive",
|
|
8884
9013
|
path: target,
|
|
8885
9014
|
mimeType,
|
|
8886
|
-
sizeBytes:
|
|
9015
|
+
sizeBytes: stat19.size,
|
|
8887
9016
|
sha256,
|
|
8888
9017
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
8889
9018
|
};
|
|
@@ -9258,7 +9387,7 @@ var BrowserSessionManager = class {
|
|
|
9258
9387
|
constructor(options, launcher = defaultLauncher) {
|
|
9259
9388
|
this.launcher = launcher;
|
|
9260
9389
|
this.artifacts = new BrowserArtifactStore(options.artifactRoot);
|
|
9261
|
-
this.allowPrivateHosts = options.allowPrivateHosts ??
|
|
9390
|
+
this.allowPrivateHosts = options.allowPrivateHosts ?? false;
|
|
9262
9391
|
this.allowedPrivateOrigins = options.allowedPrivateOrigins ?? [];
|
|
9263
9392
|
this.networkProxy = new BrowserNetworkGuardProxy({
|
|
9264
9393
|
allowPrivateHosts: this.allowPrivateHosts,
|
|
@@ -9481,8 +9610,8 @@ var BrowserSessionManager = class {
|
|
|
9481
9610
|
if (realRelative.startsWith("..") || path11.isAbsolute(realRelative)) {
|
|
9482
9611
|
throw new Error("browser: upload files must not escape the project root through a symlink");
|
|
9483
9612
|
}
|
|
9484
|
-
const
|
|
9485
|
-
if (!
|
|
9613
|
+
const stat19 = await fs8.stat(realFile);
|
|
9614
|
+
if (!stat19.isFile()) throw new Error(`browser: upload target is not a file: ${file}`);
|
|
9486
9615
|
resolved.push(realFile);
|
|
9487
9616
|
}
|
|
9488
9617
|
await this.runPageOperation(
|
|
@@ -9834,6 +9963,9 @@ var browserScreenshotTool = {
|
|
|
9834
9963
|
description: "Capture a page or element PNG and return sensitive artifact metadata with integrity hash.",
|
|
9835
9964
|
usageHint: "browser_screenshot({ sessionId, fullPage?, selector? })",
|
|
9836
9965
|
permission: "confirm",
|
|
9966
|
+
// WS-046: the element/expression acted on, not the sessionId — a trust
|
|
9967
|
+
// rule must distinguish clicking one control from another.
|
|
9968
|
+
subjectKey: "selector",
|
|
9837
9969
|
mutating: true,
|
|
9838
9970
|
riskTier: "safe",
|
|
9839
9971
|
capabilities: [ToolCapabilities.NET_OUTBOUND],
|
|
@@ -9863,6 +9995,9 @@ function selectorTool(name, description, run) {
|
|
|
9863
9995
|
description,
|
|
9864
9996
|
usageHint: `${name}({ sessionId, selector })`,
|
|
9865
9997
|
permission: "confirm",
|
|
9998
|
+
// WS-046: the element/expression acted on, not the sessionId — a trust
|
|
9999
|
+
// rule must distinguish clicking one control from another.
|
|
10000
|
+
subjectKey: "selector",
|
|
9866
10001
|
mutating: true,
|
|
9867
10002
|
riskTier: "standard",
|
|
9868
10003
|
capabilities: [ToolCapabilities.NET_OUTBOUND],
|
|
@@ -9892,6 +10027,9 @@ var browserTypeTool = {
|
|
|
9892
10027
|
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.",
|
|
9893
10028
|
usageHint: "browser_type({ sessionId, selector, text? | secretEnv? })",
|
|
9894
10029
|
permission: "confirm",
|
|
10030
|
+
// WS-046: the element/expression acted on, not the sessionId — a trust
|
|
10031
|
+
// rule must distinguish clicking one control from another.
|
|
10032
|
+
subjectKey: "selector",
|
|
9895
10033
|
mutating: true,
|
|
9896
10034
|
riskTier: "standard",
|
|
9897
10035
|
capabilities: [ToolCapabilities.NET_OUTBOUND],
|
|
@@ -9926,6 +10064,9 @@ var browserSelectTool = {
|
|
|
9926
10064
|
description: "Select an option in an owned browser session.",
|
|
9927
10065
|
usageHint: "browser_select({ sessionId, selector, value })",
|
|
9928
10066
|
permission: "confirm",
|
|
10067
|
+
// WS-046: the element/expression acted on, not the sessionId — a trust
|
|
10068
|
+
// rule must distinguish clicking one control from another.
|
|
10069
|
+
subjectKey: "selector",
|
|
9929
10070
|
mutating: true,
|
|
9930
10071
|
riskTier: "standard",
|
|
9931
10072
|
capabilities: [ToolCapabilities.NET_OUTBOUND],
|
|
@@ -9954,6 +10095,9 @@ var browserPressTool = {
|
|
|
9954
10095
|
description: "Press a keyboard key in an owned browser session.",
|
|
9955
10096
|
usageHint: "browser_press({ sessionId, key })",
|
|
9956
10097
|
permission: "confirm",
|
|
10098
|
+
// WS-046: the element/expression acted on, not the sessionId — a trust
|
|
10099
|
+
// rule must distinguish clicking one control from another.
|
|
10100
|
+
subjectKey: "key",
|
|
9957
10101
|
mutating: true,
|
|
9958
10102
|
riskTier: "standard",
|
|
9959
10103
|
capabilities: [ToolCapabilities.NET_OUTBOUND],
|
|
@@ -9972,6 +10116,9 @@ var browserDragTool = {
|
|
|
9972
10116
|
description: "Drag one element to another in an owned browser session.",
|
|
9973
10117
|
usageHint: "browser_drag({ sessionId, from, to })",
|
|
9974
10118
|
permission: "confirm",
|
|
10119
|
+
// WS-046: the element/expression acted on, not the sessionId — a trust
|
|
10120
|
+
// rule must distinguish clicking one control from another.
|
|
10121
|
+
subjectKey: "from",
|
|
9975
10122
|
mutating: true,
|
|
9976
10123
|
riskTier: "standard",
|
|
9977
10124
|
capabilities: [ToolCapabilities.NET_OUTBOUND],
|
|
@@ -10021,6 +10168,9 @@ var browserEvaluateTool = {
|
|
|
10021
10168
|
description: "Evaluate bounded JavaScript in the page. Requires confirmation because page code is arbitrary.",
|
|
10022
10169
|
usageHint: "browser_evaluate({ sessionId, expression })",
|
|
10023
10170
|
permission: "confirm",
|
|
10171
|
+
// WS-046: the element/expression acted on, not the sessionId — a trust
|
|
10172
|
+
// rule must distinguish clicking one control from another.
|
|
10173
|
+
subjectKey: "expression",
|
|
10024
10174
|
mutating: true,
|
|
10025
10175
|
riskTier: "destructive",
|
|
10026
10176
|
capabilities: [ToolCapabilities.NET_OUTBOUND, ToolCapabilities.TOOL_MUTATE_ANY],
|
|
@@ -10043,6 +10193,9 @@ var browserUploadTool = {
|
|
|
10043
10193
|
description: "Upload project-local files through a file input in an owned browser session.",
|
|
10044
10194
|
usageHint: "browser_upload({ sessionId, selector, files })",
|
|
10045
10195
|
permission: "confirm",
|
|
10196
|
+
// WS-046: the element/expression acted on, not the sessionId — a trust
|
|
10197
|
+
// rule must distinguish clicking one control from another.
|
|
10198
|
+
subjectKey: "selector",
|
|
10046
10199
|
mutating: true,
|
|
10047
10200
|
riskTier: "destructive",
|
|
10048
10201
|
capabilities: [ToolCapabilities.NET_OUTBOUND, ToolCapabilities.FS_READ],
|
|
@@ -10986,6 +11139,7 @@ var REFS_INDEX_SQL = [
|
|
|
10986
11139
|
var SYMBOLS_FTS_SQL = "CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5(text, tokenize = 'unicode61')";
|
|
10987
11140
|
|
|
10988
11141
|
// src/codebase-index/writer-search-helpers.ts
|
|
11142
|
+
var SEARCH_CANDIDATE_SCAN_CAP = 5e3;
|
|
10989
11143
|
function normalizeSearchLimit(limit) {
|
|
10990
11144
|
return typeof limit === "number" && Number.isFinite(limit) ? Math.max(0, Math.trunc(limit)) : void 0;
|
|
10991
11145
|
}
|
|
@@ -11015,8 +11169,8 @@ function buildWriterSearchWhere(query, filter) {
|
|
|
11015
11169
|
}
|
|
11016
11170
|
if (query.trim()) {
|
|
11017
11171
|
const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
11018
|
-
conditions.push(`(${tokens.map(() => "text LIKE ?").join(" OR ")})`);
|
|
11019
|
-
for (const token of tokens) values.push(`%${token}%`);
|
|
11172
|
+
conditions.push(`(${tokens.map(() => "text LIKE ? ESCAPE '\\'").join(" OR ")})`);
|
|
11173
|
+
for (const token of tokens) values.push(`%${escapeLike(token)}%`);
|
|
11020
11174
|
}
|
|
11021
11175
|
return { where: conditions.length ? `WHERE ${conditions.join(" AND ")}` : "", values };
|
|
11022
11176
|
}
|
|
@@ -11125,6 +11279,7 @@ var StorePool = class {
|
|
|
11125
11279
|
|
|
11126
11280
|
// src/codebase-index/writer.ts
|
|
11127
11281
|
var DB_FILE2 = "index.db";
|
|
11282
|
+
var MAX_STATEMENT_CACHE = 128;
|
|
11128
11283
|
var IndexStore = class _IndexStore {
|
|
11129
11284
|
db;
|
|
11130
11285
|
/** Absolute path to this project's index directory. */
|
|
@@ -11162,12 +11317,31 @@ var IndexStore = class _IndexStore {
|
|
|
11162
11317
|
// Dirty on open so the first getOrBuildBm25() rebuilds against current rows;
|
|
11163
11318
|
// an empty or pre-existing corpus makes a stale IDF table meaningless.
|
|
11164
11319
|
bm25Dirty = true;
|
|
11165
|
-
/**
|
|
11320
|
+
/**
|
|
11321
|
+
* Prepare-once helper: compile `sql` on first use, reuse thereafter.
|
|
11322
|
+
*
|
|
11323
|
+
* Bounded LRU rather than an open Map. The cache is keyed by SQL TEXT, and
|
|
11324
|
+
* the fallback search builder emits one `text LIKE ?` clause per query token
|
|
11325
|
+
* — so the SQL varies with the token count and a stream of differently-sized
|
|
11326
|
+
* queries grew the cache without limit. Sage's store already bounds its
|
|
11327
|
+
* equivalent at 128 (WS-096).
|
|
11328
|
+
*
|
|
11329
|
+
* Re-inserting on a hit keeps the hot fixed-SQL statements (upsertFile,
|
|
11330
|
+
* insertRefs, …) at the young end, so a burst of one-off search SQL evicts
|
|
11331
|
+
* itself rather than the reindex hot path.
|
|
11332
|
+
*/
|
|
11166
11333
|
stmt(sql) {
|
|
11167
|
-
|
|
11168
|
-
if (
|
|
11169
|
-
|
|
11170
|
-
this.stmtCache.set(sql,
|
|
11334
|
+
const cached = this.stmtCache.get(sql);
|
|
11335
|
+
if (cached !== void 0) {
|
|
11336
|
+
this.stmtCache.delete(sql);
|
|
11337
|
+
this.stmtCache.set(sql, cached);
|
|
11338
|
+
return cached;
|
|
11339
|
+
}
|
|
11340
|
+
const s = this.db.prepare(sql);
|
|
11341
|
+
this.stmtCache.set(sql, s);
|
|
11342
|
+
if (this.stmtCache.size > MAX_STATEMENT_CACHE) {
|
|
11343
|
+
const oldest = this.stmtCache.keys().next();
|
|
11344
|
+
if (!oldest.done) this.stmtCache.delete(oldest.value);
|
|
11171
11345
|
}
|
|
11172
11346
|
return s;
|
|
11173
11347
|
}
|
|
@@ -11440,7 +11614,7 @@ var IndexStore = class _IndexStore {
|
|
|
11440
11614
|
const { where, values } = built;
|
|
11441
11615
|
const limit = normalizeSearchLimit(opts?.limit);
|
|
11442
11616
|
const limitSql = limit !== void 0 ? " LIMIT ?" : "";
|
|
11443
|
-
const sql = `SELECT id, lang, kind, name, file, line, col, signature, doc_comment
|
|
11617
|
+
const sql = `SELECT id, lang, kind, name, file, line, col, signature, doc_comment FROM symbols ${where}${limitSql}`;
|
|
11444
11618
|
const binds = limit !== void 0 ? [...values, limit] : values;
|
|
11445
11619
|
const rows = this.stmt(sql).all(
|
|
11446
11620
|
...binds
|
|
@@ -11569,11 +11743,13 @@ var IndexStore = class _IndexStore {
|
|
|
11569
11743
|
/** Legacy ranked path: LIKE candidates + in-process BM25 + JS snippets. */
|
|
11570
11744
|
searchRankedFallback(query, filter, limit) {
|
|
11571
11745
|
if (!query.trim()) {
|
|
11572
|
-
const
|
|
11573
|
-
if (
|
|
11574
|
-
return { results: this.search(query, filter, { limit }), total };
|
|
11746
|
+
const total2 = this.countSearch(query, filter);
|
|
11747
|
+
if (total2 === 0) return { results: [], total: 0 };
|
|
11748
|
+
return { results: this.search(query, filter, { limit }), total: total2 };
|
|
11575
11749
|
}
|
|
11576
|
-
const
|
|
11750
|
+
const total = this.countSearch(query, filter);
|
|
11751
|
+
if (total === 0) return { results: [], total: 0 };
|
|
11752
|
+
const candidates = this.search(query, filter, { limit: SEARCH_CANDIDATE_SCAN_CAP });
|
|
11577
11753
|
if (candidates.length === 0) return { results: [], total: 0 };
|
|
11578
11754
|
const candidateById = new Map(candidates.map((c) => [c.id, c]));
|
|
11579
11755
|
const bm25 = this.getOrBuildBm25();
|
|
@@ -11599,7 +11775,7 @@ var IndexStore = class _IndexStore {
|
|
|
11599
11775
|
const c = expectDefined2(candidateById.get(id));
|
|
11600
11776
|
return { ...c, score, snippet: bm25.extractSnippet(id, qTokens) };
|
|
11601
11777
|
});
|
|
11602
|
-
return { results, total
|
|
11778
|
+
return { results, total };
|
|
11603
11779
|
}
|
|
11604
11780
|
getAllIndexable() {
|
|
11605
11781
|
return getAllIndexableWithStatement((sql) => this.stmt(sql));
|
|
@@ -12018,12 +12194,12 @@ var buildIdCache;
|
|
|
12018
12194
|
function projectIndexServerBuildId(entrypoint) {
|
|
12019
12195
|
const file = entrypoint instanceof URL || entrypoint.startsWith("file:") ? fileURLToPath(entrypoint) : path16.resolve(entrypoint);
|
|
12020
12196
|
try {
|
|
12021
|
-
const
|
|
12022
|
-
if (buildIdCache?.file === file && buildIdCache.mtimeMs ===
|
|
12197
|
+
const stat19 = fs11.statSync(file);
|
|
12198
|
+
if (buildIdCache?.file === file && buildIdCache.mtimeMs === stat19.mtimeMs && buildIdCache.size === stat19.size) {
|
|
12023
12199
|
return buildIdCache.buildId;
|
|
12024
12200
|
}
|
|
12025
12201
|
const buildId = createHash4("sha256").update(fs11.readFileSync(file)).digest("hex").slice(0, 24);
|
|
12026
|
-
buildIdCache = { file, mtimeMs:
|
|
12202
|
+
buildIdCache = { file, mtimeMs: stat19.mtimeMs, size: stat19.size, buildId };
|
|
12027
12203
|
return buildId;
|
|
12028
12204
|
} catch {
|
|
12029
12205
|
return `unreadable:${path16.basename(file)}`;
|
|
@@ -12193,6 +12369,8 @@ var ProjectServerConnection = class {
|
|
|
12193
12369
|
connectResolve = null;
|
|
12194
12370
|
connectReject = null;
|
|
12195
12371
|
nextId = 1;
|
|
12372
|
+
/** WS-027: read from the owner-only metadata file — see currentAuthToken. */
|
|
12373
|
+
authToken;
|
|
12196
12374
|
pending = /* @__PURE__ */ new Map();
|
|
12197
12375
|
transition(status, options = {}) {
|
|
12198
12376
|
const previous = connectionStates.get(this.endpoint);
|
|
@@ -12329,6 +12507,31 @@ var ProjectServerConnection = class {
|
|
|
12329
12507
|
this.transition("offline");
|
|
12330
12508
|
maybeStopHeartbeatLoop();
|
|
12331
12509
|
}
|
|
12510
|
+
/**
|
|
12511
|
+
* WS-027: the per-process token, read from the daemon's owner-only metadata
|
|
12512
|
+
* file — never from the `hello` frame, which the daemon sends to every
|
|
12513
|
+
* socket that connects (the mistake WS-028 found in the SAGE daemon).
|
|
12514
|
+
*
|
|
12515
|
+
* Read lazily and re-read while unknown: the daemon starts listening before
|
|
12516
|
+
* it writes metadata (endpoint ownership has to be won first), and a
|
|
12517
|
+
* respawned daemon mints a new token, so a cached wrong one would be sticky.
|
|
12518
|
+
*/
|
|
12519
|
+
currentAuthToken() {
|
|
12520
|
+
if (this.authToken === void 0) {
|
|
12521
|
+
try {
|
|
12522
|
+
const raw = fs12.readFileSync(
|
|
12523
|
+
projectIndexServerMetadataPath(this.projectRoot, this.indexDir),
|
|
12524
|
+
"utf8"
|
|
12525
|
+
);
|
|
12526
|
+
const parsed = JSON.parse(raw);
|
|
12527
|
+
if (typeof parsed.authToken === "string" && parsed.authToken.length > 0) {
|
|
12528
|
+
this.authToken = parsed.authToken;
|
|
12529
|
+
}
|
|
12530
|
+
} catch {
|
|
12531
|
+
}
|
|
12532
|
+
}
|
|
12533
|
+
return this.authToken;
|
|
12534
|
+
}
|
|
12332
12535
|
request(message, options) {
|
|
12333
12536
|
const socket = this.socket;
|
|
12334
12537
|
if (!socket || socket.destroyed) {
|
|
@@ -12372,7 +12575,7 @@ var ProjectServerConnection = class {
|
|
|
12372
12575
|
return;
|
|
12373
12576
|
}
|
|
12374
12577
|
}
|
|
12375
|
-
this.write({ ...message, id });
|
|
12578
|
+
this.write({ ...message, id, authToken: this.currentAuthToken() });
|
|
12376
12579
|
});
|
|
12377
12580
|
}
|
|
12378
12581
|
async ensureConnected(spawnIfMissing) {
|
|
@@ -12419,6 +12622,7 @@ var ProjectServerConnection = class {
|
|
|
12419
12622
|
connectOnce() {
|
|
12420
12623
|
this.socket?.destroy();
|
|
12421
12624
|
this.socket = null;
|
|
12625
|
+
this.authToken = void 0;
|
|
12422
12626
|
this.info = null;
|
|
12423
12627
|
this.activity = null;
|
|
12424
12628
|
this.health = null;
|
|
@@ -13062,9 +13266,9 @@ async function runIndexerWithStore(store, opts) {
|
|
|
13062
13266
|
const statReadParse = await Promise.allSettled(
|
|
13063
13267
|
batchFiles.map(
|
|
13064
13268
|
async (file) => {
|
|
13065
|
-
let
|
|
13269
|
+
let stat19;
|
|
13066
13270
|
try {
|
|
13067
|
-
|
|
13271
|
+
stat19 = await fs17.stat(file, statOpts);
|
|
13068
13272
|
} catch (e) {
|
|
13069
13273
|
if (isAbortError(e)) throw e;
|
|
13070
13274
|
return {
|
|
@@ -13076,21 +13280,21 @@ async function runIndexerWithStore(store, opts) {
|
|
|
13076
13280
|
missing: isMissingPathError(e)
|
|
13077
13281
|
};
|
|
13078
13282
|
}
|
|
13079
|
-
if (!
|
|
13283
|
+
if (!stat19.isFile()) return { file, stat: stat19, lang: "", parsed: null };
|
|
13080
13284
|
const lang = detectLang(file);
|
|
13081
|
-
if (!lang) return { file, stat:
|
|
13082
|
-
if (
|
|
13285
|
+
if (!lang) return { file, stat: stat19, lang: "", parsed: null };
|
|
13286
|
+
if (stat19.size > MAX_INDEX_FILE_BYTES) {
|
|
13083
13287
|
return {
|
|
13084
13288
|
file,
|
|
13085
|
-
stat:
|
|
13289
|
+
stat: stat19,
|
|
13086
13290
|
lang,
|
|
13087
13291
|
parsed: null,
|
|
13088
|
-
error: `file too large (${
|
|
13292
|
+
error: `file too large (${stat19.size} bytes; max ${MAX_INDEX_FILE_BYTES})`
|
|
13089
13293
|
};
|
|
13090
13294
|
}
|
|
13091
13295
|
const meta = existingMeta.get(file);
|
|
13092
|
-
if (!force && meta && meta.mtimeMs === Math.floor(
|
|
13093
|
-
return { file, stat:
|
|
13296
|
+
if (!force && meta && meta.mtimeMs === Math.floor(stat19.mtimeMs)) {
|
|
13297
|
+
return { file, stat: stat19, lang, parsed: null, skippedMeta: meta };
|
|
13094
13298
|
}
|
|
13095
13299
|
let content;
|
|
13096
13300
|
try {
|
|
@@ -13099,7 +13303,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
13099
13303
|
if (isAbortError(e)) throw e;
|
|
13100
13304
|
return {
|
|
13101
13305
|
file,
|
|
13102
|
-
stat:
|
|
13306
|
+
stat: stat19,
|
|
13103
13307
|
lang,
|
|
13104
13308
|
parsed: null,
|
|
13105
13309
|
error: `read error: ${e instanceof Error ? e.message : String(e)}`
|
|
@@ -13111,13 +13315,13 @@ async function runIndexerWithStore(store, opts) {
|
|
|
13111
13315
|
} catch (e) {
|
|
13112
13316
|
return {
|
|
13113
13317
|
file,
|
|
13114
|
-
stat:
|
|
13318
|
+
stat: stat19,
|
|
13115
13319
|
lang,
|
|
13116
13320
|
parsed: null,
|
|
13117
13321
|
error: `parse error: ${e instanceof Error ? e.message : String(e)}`
|
|
13118
13322
|
};
|
|
13119
13323
|
}
|
|
13120
|
-
return { file, stat:
|
|
13324
|
+
return { file, stat: stat19, lang, parsed, content };
|
|
13121
13325
|
}
|
|
13122
13326
|
)
|
|
13123
13327
|
);
|
|
@@ -13138,7 +13342,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
13138
13342
|
errors.push(`${file}: ${result.error}`);
|
|
13139
13343
|
continue;
|
|
13140
13344
|
}
|
|
13141
|
-
const { stat:
|
|
13345
|
+
const { stat: stat19, lang, parsed } = result;
|
|
13142
13346
|
if (result.skippedMeta) {
|
|
13143
13347
|
langStats[lang] = (langStats[lang] ?? 0) + result.skippedMeta.symbolCount;
|
|
13144
13348
|
symbolsIndexed += result.skippedMeta.symbolCount;
|
|
@@ -13150,7 +13354,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
13150
13354
|
store.upsertFile({
|
|
13151
13355
|
file,
|
|
13152
13356
|
lang,
|
|
13153
|
-
mtimeMs: Math.floor(
|
|
13357
|
+
mtimeMs: Math.floor(stat19.mtimeMs),
|
|
13154
13358
|
symbolCount: 0,
|
|
13155
13359
|
lastIndexed: Date.now()
|
|
13156
13360
|
});
|
|
@@ -13162,7 +13366,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
13162
13366
|
store.replaceEmptyFile({
|
|
13163
13367
|
file,
|
|
13164
13368
|
lang,
|
|
13165
|
-
mtimeMs: Math.floor(
|
|
13369
|
+
mtimeMs: Math.floor(stat19.mtimeMs),
|
|
13166
13370
|
symbolCount: 0,
|
|
13167
13371
|
lastIndexed: Date.now()
|
|
13168
13372
|
});
|
|
@@ -13174,7 +13378,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
13174
13378
|
lang,
|
|
13175
13379
|
symbols: parsed.symbols,
|
|
13176
13380
|
refs: parsed.refs ?? [],
|
|
13177
|
-
mtimeMs: Math.floor(
|
|
13381
|
+
mtimeMs: Math.floor(stat19.mtimeMs),
|
|
13178
13382
|
symbolCount: parsed.symbols.length
|
|
13179
13383
|
});
|
|
13180
13384
|
deleteForFiles.push(file);
|
|
@@ -14370,8 +14574,9 @@ function runDeadCodeScan(projectRoot, opts = {}) {
|
|
|
14370
14574
|
const content = fs19.readFileSync(epFile, "utf8");
|
|
14371
14575
|
const strippedContent = content.replace(/\/\*[\s\S]*?\*\//g, (m) => " ".repeat(m.length)).replace(/\/\/[^\n]*/g, (m) => " ".repeat(m.length));
|
|
14372
14576
|
const reExportRe = /export\s+(?:(?:type\s+)?\{[\s\S]*?\}\s+from|\*\s+as\s+\w+\s+from|\*\s+from)\s+['"]([^'"]+)['"]/g;
|
|
14373
|
-
|
|
14374
|
-
|
|
14577
|
+
for (; ; ) {
|
|
14578
|
+
const match = reExportRe.exec(strippedContent);
|
|
14579
|
+
if (match === null) break;
|
|
14375
14580
|
const moduleSpec = match[1];
|
|
14376
14581
|
const resolvedFiles = resolveModulePath(epFile, moduleSpec, indexedFiles);
|
|
14377
14582
|
for (const rf of resolvedFiles) {
|
|
@@ -14577,6 +14782,9 @@ var designTool = {
|
|
|
14577
14782
|
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).',
|
|
14578
14783
|
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"}`.',
|
|
14579
14784
|
permission: "confirm",
|
|
14785
|
+
// WS-046: gives permission decisions something to key on.
|
|
14786
|
+
// The action performed; `out` is optional so it cannot be the subject.
|
|
14787
|
+
subjectKey: "action",
|
|
14580
14788
|
mutating: true,
|
|
14581
14789
|
capabilities: ["fs.write"],
|
|
14582
14790
|
timeoutMs: 15e3,
|
|
@@ -14757,9 +14965,7 @@ Run \`design {action:"materialize"}\` to write the tuned tokens to your theme fi
|
|
|
14757
14965
|
const abs = path25.join(absParent, path25.basename(absResolved));
|
|
14758
14966
|
const rel = path25.relative(root, abs);
|
|
14759
14967
|
if (rel.startsWith("..") || path25.isAbsolute(rel)) {
|
|
14760
|
-
throw new Error(
|
|
14761
|
-
`design: materialize path "${result.path}" would escape the project root`
|
|
14762
|
-
);
|
|
14968
|
+
throw new Error(`design: materialize path "${result.path}" would escape the project root`);
|
|
14763
14969
|
}
|
|
14764
14970
|
let exists = false;
|
|
14765
14971
|
try {
|
|
@@ -14834,6 +15040,7 @@ import { statSync as statSync3 } from "node:fs";
|
|
|
14834
15040
|
import * as fs21 from "node:fs/promises";
|
|
14835
15041
|
import * as path26 from "node:path";
|
|
14836
15042
|
import { buildChildEnv as buildChildEnv3 } from "@wrongstack/core/utils";
|
|
15043
|
+
var MAX_FILE_DUMP_BYTES = 5 * 1024 * 1024;
|
|
14837
15044
|
var diffTool = {
|
|
14838
15045
|
name: "diff",
|
|
14839
15046
|
category: "Filesystem",
|
|
@@ -14841,6 +15048,7 @@ var diffTool = {
|
|
|
14841
15048
|
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).',
|
|
14842
15049
|
permission: "auto",
|
|
14843
15050
|
mutating: false,
|
|
15051
|
+
maxOutputBytes: 262144,
|
|
14844
15052
|
capabilities: ["fs.read"],
|
|
14845
15053
|
icon: "diff",
|
|
14846
15054
|
timeoutMs: 1e4,
|
|
@@ -14916,8 +15124,8 @@ function findGitDir(cwd) {
|
|
|
14916
15124
|
let dir = cwd;
|
|
14917
15125
|
for (let i = 0; i < 20; i++) {
|
|
14918
15126
|
try {
|
|
14919
|
-
const
|
|
14920
|
-
if (
|
|
15127
|
+
const stat19 = statSync3(path26.join(dir, ".git"));
|
|
15128
|
+
if (stat19.isDirectory()) return dir;
|
|
14921
15129
|
} catch {
|
|
14922
15130
|
}
|
|
14923
15131
|
const parent = path26.dirname(dir);
|
|
@@ -14959,10 +15167,18 @@ async function fileDiff(input, ctx, _signal) {
|
|
|
14959
15167
|
};
|
|
14960
15168
|
}
|
|
14961
15169
|
const results = [];
|
|
15170
|
+
let truncated = false;
|
|
14962
15171
|
for (const file of files) {
|
|
14963
15172
|
const absPath = safeResolve(file, ctx);
|
|
14964
|
-
const
|
|
14965
|
-
if (!
|
|
15173
|
+
const stat19 = await fs21.stat(absPath).catch(() => null);
|
|
15174
|
+
if (!stat19?.isFile()) continue;
|
|
15175
|
+
if (stat19.size > MAX_FILE_DUMP_BYTES) {
|
|
15176
|
+
truncated = true;
|
|
15177
|
+
results.push(
|
|
15178
|
+
`--- ${file} (skipped: ${stat19.size} bytes exceeds the ${MAX_FILE_DUMP_BYTES} limit; use the read tool with offset/limit) ---`
|
|
15179
|
+
);
|
|
15180
|
+
continue;
|
|
15181
|
+
}
|
|
14966
15182
|
const content = await fs21.readFile(absPath, "utf8");
|
|
14967
15183
|
const lines = content.split(/\r?\n/);
|
|
14968
15184
|
results.push(formatWithLineNumbers(file, lines));
|
|
@@ -14970,7 +15186,7 @@ async function fileDiff(input, ctx, _signal) {
|
|
|
14970
15186
|
return {
|
|
14971
15187
|
diff: results.join("\n\n"),
|
|
14972
15188
|
files,
|
|
14973
|
-
truncated
|
|
15189
|
+
truncated,
|
|
14974
15190
|
mode: input.mode ?? "unified"
|
|
14975
15191
|
};
|
|
14976
15192
|
}
|
|
@@ -15063,8 +15279,8 @@ async function resolveFiles(filesInput, cwd) {
|
|
|
15063
15279
|
for (const f of files) {
|
|
15064
15280
|
const absPath = f.trim().startsWith("/") ? f.trim() : `${cwd}/${f.trim()}`;
|
|
15065
15281
|
try {
|
|
15066
|
-
const
|
|
15067
|
-
if (
|
|
15282
|
+
const stat19 = await fs22.stat(absPath);
|
|
15283
|
+
if (stat19.isFile()) resolved.push(absPath);
|
|
15068
15284
|
} catch {
|
|
15069
15285
|
}
|
|
15070
15286
|
}
|
|
@@ -15917,7 +16133,7 @@ var editTool = {
|
|
|
15917
16133
|
});
|
|
15918
16134
|
}
|
|
15919
16135
|
const absPath = await safeResolveReal(input.path, ctx);
|
|
15920
|
-
const
|
|
16136
|
+
const stat19 = await fs23.stat(absPath).catch((err) => {
|
|
15921
16137
|
if (err.code === "ENOENT") {
|
|
15922
16138
|
throw new ToolValidationError({
|
|
15923
16139
|
message: `edit: file "${input.path}" does not exist. Use \`write\` instead.`,
|
|
@@ -15927,7 +16143,7 @@ var editTool = {
|
|
|
15927
16143
|
}
|
|
15928
16144
|
throw err;
|
|
15929
16145
|
});
|
|
15930
|
-
if (!
|
|
16146
|
+
if (!stat19.isFile()) {
|
|
15931
16147
|
throw new ToolValidationError({
|
|
15932
16148
|
message: `edit: "${input.path}" is not a regular file`,
|
|
15933
16149
|
field: "path"
|
|
@@ -15957,7 +16173,7 @@ var editTool = {
|
|
|
15957
16173
|
});
|
|
15958
16174
|
}
|
|
15959
16175
|
}
|
|
15960
|
-
if (autoRead && updated.mtimeMs >
|
|
16176
|
+
if (autoRead && updated.mtimeMs > stat19.mtimeMs + mtimeTolerance) {
|
|
15961
16177
|
throw new ToolValidationError({
|
|
15962
16178
|
message: `edit: file "${input.path}" changed while being auto-read. Retry the edit.`,
|
|
15963
16179
|
field: "path",
|
|
@@ -16082,7 +16298,6 @@ import {
|
|
|
16082
16298
|
import { toErrorMessage as toErrorMessage3 } from "@wrongstack/core/utils/error";
|
|
16083
16299
|
init_output_spool();
|
|
16084
16300
|
init_util();
|
|
16085
|
-
init_process_registry();
|
|
16086
16301
|
init_win32_resolve();
|
|
16087
16302
|
|
|
16088
16303
|
// src/exec-kill-guard.ts
|
|
@@ -16321,6 +16536,7 @@ async function checkKillTarget(target) {
|
|
|
16321
16536
|
}
|
|
16322
16537
|
|
|
16323
16538
|
// src/exec.ts
|
|
16539
|
+
init_process_registry();
|
|
16324
16540
|
var isWin4 = process.platform === "win32";
|
|
16325
16541
|
var DEFAULT_ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
|
|
16326
16542
|
// JS / TS toolchain
|
|
@@ -16986,7 +17202,17 @@ var BLOCKED_ARG_PATTERNS = {
|
|
|
16986
17202
|
bun: [],
|
|
16987
17203
|
docker: [],
|
|
16988
17204
|
// find -exec/-ok/-execdir execute arbitrary commands
|
|
16989
|
-
find: [
|
|
17205
|
+
find: [
|
|
17206
|
+
/^-exec$/,
|
|
17207
|
+
/^-exec;$/,
|
|
17208
|
+
/^-ok$/,
|
|
17209
|
+
/^-ok;$/,
|
|
17210
|
+
/^-execdir$/,
|
|
17211
|
+
/^-execdir;$/,
|
|
17212
|
+
/^-exec=/,
|
|
17213
|
+
/^-ok=/,
|
|
17214
|
+
/^-execdir=/
|
|
17215
|
+
],
|
|
16990
17216
|
// rm -rf / is catastrophic — block absolute paths, home, dot-dirs,
|
|
16991
17217
|
// and glob patterns that could expand to dangerous targets.
|
|
16992
17218
|
// `rm -rf ./src/*` expands to project files; `rm -rf ../../` escapes upward;
|
|
@@ -17057,6 +17283,14 @@ var execTool = {
|
|
|
17057
17283
|
useInstead: ["bash"]
|
|
17058
17284
|
},
|
|
17059
17285
|
permission: "confirm",
|
|
17286
|
+
// WS-046: without this, every exec call collapsed onto the bare tool name.
|
|
17287
|
+
// Three consequences followed: "always allow" stored a rule that could never
|
|
17288
|
+
// match, pressing "no" once blocked ALL exec for the session, and the
|
|
17289
|
+
// permission cache keyed one decision for every command. `subjectKey:
|
|
17290
|
+
// 'command'` renders the FULL invocation (`command` + `args`) — using the
|
|
17291
|
+
// program alone would make `exec git status` and `exec git push --force` the
|
|
17292
|
+
// same subject, so trusting one would silently authorize the other.
|
|
17293
|
+
subjectKey: "command",
|
|
17060
17294
|
mutating: true,
|
|
17061
17295
|
riskTier: "standard",
|
|
17062
17296
|
timeoutMs: DEFAULT_TIMEOUT_MS3,
|
|
@@ -17273,7 +17507,14 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
|
|
|
17273
17507
|
const pid = child.pid;
|
|
17274
17508
|
if (typeof pid === "number") {
|
|
17275
17509
|
const fullCommand = `${cmd} ${args.join(" ")}`;
|
|
17276
|
-
registry.register({
|
|
17510
|
+
registry.register({
|
|
17511
|
+
pid,
|
|
17512
|
+
name: "exec",
|
|
17513
|
+
command: redactCommand(fullCommand),
|
|
17514
|
+
startedAt: Date.now(),
|
|
17515
|
+
sessionId,
|
|
17516
|
+
child
|
|
17517
|
+
});
|
|
17277
17518
|
}
|
|
17278
17519
|
const timer = setTimeout(() => {
|
|
17279
17520
|
killed = true;
|
|
@@ -17713,6 +17954,9 @@ var formatTool = {
|
|
|
17713
17954
|
description: "Format source files according to project style (Biome). Can also run in check-only mode.",
|
|
17714
17955
|
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.",
|
|
17715
17956
|
permission: "confirm",
|
|
17957
|
+
// WS-046: gives permission decisions something to key on.
|
|
17958
|
+
// The files being rewritten are the subject; the fixer is how, not what.
|
|
17959
|
+
subjectKey: "files",
|
|
17716
17960
|
mutating: true,
|
|
17717
17961
|
capabilities: ["fs.write", "shell.restricted"],
|
|
17718
17962
|
icon: "code",
|
|
@@ -17817,13 +18061,13 @@ var formatTool = {
|
|
|
17817
18061
|
}
|
|
17818
18062
|
};
|
|
17819
18063
|
async function detectFixer(cwd) {
|
|
17820
|
-
const { stat:
|
|
18064
|
+
const { stat: stat19 } = await import("node:fs/promises");
|
|
17821
18065
|
try {
|
|
17822
|
-
await
|
|
18066
|
+
await stat19(`${cwd}/biome.json`);
|
|
17823
18067
|
return "biome";
|
|
17824
18068
|
} catch {
|
|
17825
18069
|
try {
|
|
17826
|
-
await
|
|
18070
|
+
await stat19(`${cwd}/.prettierrc`);
|
|
17827
18071
|
return "prettier";
|
|
17828
18072
|
} catch {
|
|
17829
18073
|
return "biome";
|
|
@@ -17850,6 +18094,10 @@ var gitTool = {
|
|
|
17850
18094
|
// Conservative: any of these may mutate. The non-mutating commands
|
|
17851
18095
|
// (status/log/diff/branch/fetch) are still gated on `permission: 'confirm'`
|
|
17852
18096
|
// and `MUTATING_SUBCOMMANDS` is consulted at runtime for per-call checks.
|
|
18097
|
+
// WS-046: gives permission decisions something to key on.
|
|
18098
|
+
// The git subcommand (status/commit/push) is what a trust rule needs to
|
|
18099
|
+
// distinguish; `git status` and `git push --force` must not share a subject.
|
|
18100
|
+
subjectKey: "command",
|
|
17853
18101
|
mutating: true,
|
|
17854
18102
|
capabilities: ["fs.write", "shell.restricted"],
|
|
17855
18103
|
timeoutMs: TIMEOUT_MS2,
|
|
@@ -17918,6 +18166,8 @@ var gitTool = {
|
|
|
17918
18166
|
truncated: false
|
|
17919
18167
|
};
|
|
17920
18168
|
}
|
|
18169
|
+
const branchGuard = validateBranchInput(input);
|
|
18170
|
+
if (branchGuard) return branchGuard;
|
|
17921
18171
|
if (input.command === "worktree") {
|
|
17922
18172
|
const guard = validateWorktreeInput(input, ctx.projectRoot);
|
|
17923
18173
|
if (guard) return guard;
|
|
@@ -17966,6 +18216,18 @@ var gitTool = {
|
|
|
17966
18216
|
return result;
|
|
17967
18217
|
}
|
|
17968
18218
|
};
|
|
18219
|
+
function validateBranchInput(input) {
|
|
18220
|
+
const branch = input.branch;
|
|
18221
|
+
if (branch === void 0) return null;
|
|
18222
|
+
if (!branch.startsWith("-") && !branch.includes(" --")) return null;
|
|
18223
|
+
return {
|
|
18224
|
+
command: input.command,
|
|
18225
|
+
stdout: "",
|
|
18226
|
+
stderr: `unsafe branch name (parsed as a git option): ${branch}`,
|
|
18227
|
+
exitCode: 1,
|
|
18228
|
+
truncated: false
|
|
18229
|
+
};
|
|
18230
|
+
}
|
|
17969
18231
|
function validateWorktreeInput(input, projectRoot) {
|
|
17970
18232
|
const reject = (stderr) => ({
|
|
17971
18233
|
command: "worktree",
|
|
@@ -17974,7 +18236,6 @@ function validateWorktreeInput(input, projectRoot) {
|
|
|
17974
18236
|
exitCode: 1,
|
|
17975
18237
|
truncated: false
|
|
17976
18238
|
});
|
|
17977
|
-
if (input.branch?.startsWith("-")) return reject(`unsafe branch name: ${input.branch}`);
|
|
17978
18239
|
if (input.worktreePath?.startsWith("-")) {
|
|
17979
18240
|
return reject(`unsafe worktree path: ${input.worktreePath}`);
|
|
17980
18241
|
}
|
|
@@ -17992,8 +18253,8 @@ function findGitDir2(cwd, projectRoot) {
|
|
|
17992
18253
|
let dir = cwd;
|
|
17993
18254
|
for (let i = 0; i < 20; i++) {
|
|
17994
18255
|
try {
|
|
17995
|
-
const
|
|
17996
|
-
if (
|
|
18256
|
+
const stat19 = statSync4(`${dir}/.git`);
|
|
18257
|
+
if (stat19.isDirectory() || stat19.isFile()) return dir;
|
|
17997
18258
|
} catch {
|
|
17998
18259
|
}
|
|
17999
18260
|
if (dir === root) break;
|
|
@@ -18028,7 +18289,10 @@ function buildArgs(input) {
|
|
|
18028
18289
|
...files.length ? ["--", ...files] : []
|
|
18029
18290
|
];
|
|
18030
18291
|
case "branch":
|
|
18031
|
-
return input.branch ? [
|
|
18292
|
+
return input.branch ? [
|
|
18293
|
+
"branch",
|
|
18294
|
+
...input.branch.startsWith("-") || input.branch.includes(" --") ? [] : [input.branch]
|
|
18295
|
+
] : ["branch"];
|
|
18032
18296
|
case "checkout":
|
|
18033
18297
|
return [
|
|
18034
18298
|
"checkout",
|
|
@@ -18121,7 +18385,7 @@ import * as path30 from "node:path";
|
|
|
18121
18385
|
import { compileGlob as compileGlob2, DEFAULT_WALK_IGNORE_DIRS as DEFAULT_WALK_IGNORE_DIRS2 } from "@wrongstack/core/utils";
|
|
18122
18386
|
|
|
18123
18387
|
// src/_concurrency.ts
|
|
18124
|
-
async function
|
|
18388
|
+
async function mapWithConcurrency2(items, limit, fn) {
|
|
18125
18389
|
if (items.length === 0) return [];
|
|
18126
18390
|
const effectiveLimit = Math.max(1, Math.min(limit | 0, items.length));
|
|
18127
18391
|
const results = new Array(items.length);
|
|
@@ -18256,10 +18520,10 @@ var globTool = {
|
|
|
18256
18520
|
}
|
|
18257
18521
|
if (truncated) return;
|
|
18258
18522
|
}
|
|
18259
|
-
await
|
|
18523
|
+
await mapWithConcurrency2(matchedFiles, WALK_CONCURRENCY, pushResult);
|
|
18260
18524
|
if (truncated) return;
|
|
18261
18525
|
const remainingSubdirs = truncated ? [] : subdirs;
|
|
18262
|
-
await
|
|
18526
|
+
await mapWithConcurrency2(remainingSubdirs, WALK_CONCURRENCY, ({ full, rel }) => walk2(full, rel));
|
|
18263
18527
|
};
|
|
18264
18528
|
await walk2(base, "");
|
|
18265
18529
|
results.sort((a, b) => b.mtime - a.mtime);
|
|
@@ -18268,12 +18532,16 @@ var globTool = {
|
|
|
18268
18532
|
};
|
|
18269
18533
|
|
|
18270
18534
|
// src/grep.ts
|
|
18271
|
-
import { expectDefined as expectDefined7 } from "@wrongstack/core/utils";
|
|
18272
18535
|
import { spawn as spawn11 } from "node:child_process";
|
|
18273
18536
|
import * as fs25 from "node:fs/promises";
|
|
18274
18537
|
import * as path31 from "node:path";
|
|
18275
18538
|
import { ToolValidationError as ToolValidationError4 } from "@wrongstack/core/types";
|
|
18276
|
-
import {
|
|
18539
|
+
import {
|
|
18540
|
+
buildChildEnv as buildChildEnv5,
|
|
18541
|
+
compileGlob as compileGlob3,
|
|
18542
|
+
DEFAULT_WALK_IGNORE_DIRS as DEFAULT_WALK_IGNORE_DIRS3,
|
|
18543
|
+
expectDefined as expectDefined7
|
|
18544
|
+
} from "@wrongstack/core/utils";
|
|
18277
18545
|
|
|
18278
18546
|
// src/_regex.ts
|
|
18279
18547
|
var MAX_PATTERN_LEN = 256;
|
|
@@ -18326,6 +18594,8 @@ var DEFAULT_IGNORE3 = DEFAULT_WALK_IGNORE_DIRS3;
|
|
|
18326
18594
|
var NATIVE_SCAN_CONCURRENCY = 32;
|
|
18327
18595
|
var NATIVE_READ_CHUNK_BYTES = 64 * 1024;
|
|
18328
18596
|
var NATIVE_MAX_FILE_BYTES = 1e6;
|
|
18597
|
+
var RG_MAX_QUEUE_CHUNKS = 128;
|
|
18598
|
+
var RG_MAX_QUEUE_CHARS = 8 * 1024 * 1024;
|
|
18329
18599
|
var grepTool = {
|
|
18330
18600
|
name: "grep",
|
|
18331
18601
|
category: "Search",
|
|
@@ -18453,9 +18723,19 @@ async function* runRgStream(input, base, mode, limit, signal) {
|
|
|
18453
18723
|
const FLUSH_AT = 16;
|
|
18454
18724
|
const MAX_BUF_BYTES = 1e6;
|
|
18455
18725
|
let bufOverflow = false;
|
|
18456
|
-
const child = spawn11("rg", args, {
|
|
18726
|
+
const child = spawn11("rg", args, {
|
|
18727
|
+
signal,
|
|
18728
|
+
env: buildChildEnv5(),
|
|
18729
|
+
// rg diagnostics are not part of the tool result. Ignoring stderr avoids
|
|
18730
|
+
// an unread pipe filling up and pinning the child when rg reports many
|
|
18731
|
+
// filesystem errors.
|
|
18732
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
18733
|
+
windowsHide: true
|
|
18734
|
+
});
|
|
18457
18735
|
const queue = [];
|
|
18736
|
+
let queuedChars = 0;
|
|
18458
18737
|
let waiter;
|
|
18738
|
+
let paused = false;
|
|
18459
18739
|
const wake = () => {
|
|
18460
18740
|
if (waiter) {
|
|
18461
18741
|
const w = waiter;
|
|
@@ -18463,93 +18743,133 @@ async function* runRgStream(input, base, mode, limit, signal) {
|
|
|
18463
18743
|
w();
|
|
18464
18744
|
}
|
|
18465
18745
|
};
|
|
18466
|
-
|
|
18467
|
-
queue.
|
|
18746
|
+
const pauseIfFlooded = () => {
|
|
18747
|
+
if (paused || queue.length < RG_MAX_QUEUE_CHUNKS && queuedChars < RG_MAX_QUEUE_CHARS)
|
|
18748
|
+
return;
|
|
18749
|
+
paused = true;
|
|
18750
|
+
child.stdout?.pause();
|
|
18751
|
+
};
|
|
18752
|
+
const resumeIfDrained = () => {
|
|
18753
|
+
if (!paused || queue.length >= RG_MAX_QUEUE_CHUNKS || queuedChars >= RG_MAX_QUEUE_CHARS)
|
|
18754
|
+
return;
|
|
18755
|
+
paused = false;
|
|
18756
|
+
child.stdout?.resume();
|
|
18757
|
+
};
|
|
18758
|
+
const onStdoutData = (c) => {
|
|
18759
|
+
const data = c.toString();
|
|
18760
|
+
queue.push({ kind: "out", data });
|
|
18761
|
+
queuedChars += data.length;
|
|
18468
18762
|
wake();
|
|
18469
|
-
|
|
18470
|
-
|
|
18763
|
+
pauseIfFlooded();
|
|
18764
|
+
};
|
|
18765
|
+
const onError = (e) => {
|
|
18471
18766
|
queue.push({ kind: "error", data: e.message });
|
|
18472
18767
|
wake();
|
|
18473
|
-
}
|
|
18474
|
-
|
|
18768
|
+
};
|
|
18769
|
+
const onClose = () => {
|
|
18475
18770
|
queue.push({ kind: "close", data: "" });
|
|
18476
18771
|
wake();
|
|
18477
|
-
}
|
|
18772
|
+
};
|
|
18773
|
+
child.stdout?.on("data", onStdoutData);
|
|
18774
|
+
child.on("error", onError);
|
|
18775
|
+
child.on("close", onClose);
|
|
18478
18776
|
let pendingBatch = [];
|
|
18479
18777
|
let errored = false;
|
|
18480
|
-
|
|
18481
|
-
|
|
18482
|
-
|
|
18483
|
-
|
|
18484
|
-
|
|
18485
|
-
|
|
18486
|
-
|
|
18487
|
-
if (c.kind === "error") {
|
|
18488
|
-
errored = true;
|
|
18489
|
-
continue;
|
|
18490
|
-
}
|
|
18491
|
-
if (c.kind === "close") break;
|
|
18492
|
-
buf += c.data;
|
|
18493
|
-
if (buf.length > MAX_BUF_BYTES && !bufOverflow) {
|
|
18494
|
-
bufOverflow = true;
|
|
18495
|
-
buf = buf.slice(-MAX_BUF_BYTES);
|
|
18496
|
-
try {
|
|
18497
|
-
child.kill("SIGTERM");
|
|
18498
|
-
} catch {
|
|
18778
|
+
let closed = false;
|
|
18779
|
+
try {
|
|
18780
|
+
for (; ; ) {
|
|
18781
|
+
while (queue.length === 0) {
|
|
18782
|
+
await new Promise((r) => {
|
|
18783
|
+
waiter = r;
|
|
18784
|
+
});
|
|
18499
18785
|
}
|
|
18500
|
-
|
|
18501
|
-
|
|
18502
|
-
|
|
18503
|
-
|
|
18504
|
-
|
|
18505
|
-
|
|
18506
|
-
|
|
18507
|
-
|
|
18508
|
-
|
|
18509
|
-
|
|
18510
|
-
|
|
18511
|
-
|
|
18512
|
-
|
|
18786
|
+
const c = expectDefined7(queue.shift());
|
|
18787
|
+
queuedChars = Math.max(0, queuedChars - c.data.length);
|
|
18788
|
+
resumeIfDrained();
|
|
18789
|
+
if (c.kind === "error") {
|
|
18790
|
+
errored = true;
|
|
18791
|
+
continue;
|
|
18792
|
+
}
|
|
18793
|
+
if (c.kind === "close") {
|
|
18794
|
+
closed = true;
|
|
18795
|
+
break;
|
|
18796
|
+
}
|
|
18797
|
+
buf += c.data;
|
|
18798
|
+
if (buf.length > MAX_BUF_BYTES && !bufOverflow) {
|
|
18799
|
+
bufOverflow = true;
|
|
18800
|
+
buf = buf.slice(-MAX_BUF_BYTES);
|
|
18801
|
+
try {
|
|
18802
|
+
child.kill("SIGTERM");
|
|
18803
|
+
} catch {
|
|
18804
|
+
}
|
|
18805
|
+
}
|
|
18806
|
+
const idx = buf.lastIndexOf("\n");
|
|
18807
|
+
if (idx === -1) continue;
|
|
18808
|
+
const ready = buf.slice(0, idx);
|
|
18809
|
+
buf = buf.slice(idx + 1);
|
|
18810
|
+
for (const line of ready.split("\n")) {
|
|
18811
|
+
if (!line) continue;
|
|
18812
|
+
totalLines++;
|
|
18813
|
+
if (mode === "count") totalCount += parseRgCountLine(line);
|
|
18814
|
+
if (matches.length < limit) {
|
|
18815
|
+
matches.push(line);
|
|
18816
|
+
pendingBatch.push(line);
|
|
18817
|
+
batchSinceFlush++;
|
|
18818
|
+
}
|
|
18819
|
+
}
|
|
18820
|
+
if (batchSinceFlush >= FLUSH_AT) {
|
|
18821
|
+
yield {
|
|
18822
|
+
type: "partial_output",
|
|
18823
|
+
text: pendingBatch.join("\n"),
|
|
18824
|
+
data: { matches_so_far: matches.length }
|
|
18825
|
+
};
|
|
18826
|
+
pendingBatch = [];
|
|
18827
|
+
batchSinceFlush = 0;
|
|
18828
|
+
}
|
|
18829
|
+
}
|
|
18830
|
+
if (buf.trim()) {
|
|
18831
|
+
for (const line of buf.split("\n")) {
|
|
18832
|
+
if (!line) continue;
|
|
18833
|
+
totalLines++;
|
|
18834
|
+
if (mode === "count") totalCount += parseRgCountLine(line);
|
|
18835
|
+
if (matches.length < limit) {
|
|
18836
|
+
matches.push(line);
|
|
18837
|
+
pendingBatch.push(line);
|
|
18838
|
+
}
|
|
18513
18839
|
}
|
|
18514
18840
|
}
|
|
18515
|
-
if (
|
|
18841
|
+
if (pendingBatch.length > 0) {
|
|
18516
18842
|
yield {
|
|
18517
18843
|
type: "partial_output",
|
|
18518
18844
|
text: pendingBatch.join("\n"),
|
|
18519
18845
|
data: { matches_so_far: matches.length }
|
|
18520
18846
|
};
|
|
18521
|
-
pendingBatch = [];
|
|
18522
|
-
batchSinceFlush = 0;
|
|
18523
|
-
}
|
|
18524
|
-
}
|
|
18525
|
-
if (buf.trim()) {
|
|
18526
|
-
for (const line of buf.split("\n")) {
|
|
18527
|
-
if (!line) continue;
|
|
18528
|
-
totalLines++;
|
|
18529
|
-
if (mode === "count") totalCount += parseRgCountLine(line);
|
|
18530
|
-
if (matches.length < limit) {
|
|
18531
|
-
matches.push(line);
|
|
18532
|
-
pendingBatch.push(line);
|
|
18533
|
-
}
|
|
18534
18847
|
}
|
|
18535
|
-
|
|
18536
|
-
if (pendingBatch.length > 0) {
|
|
18848
|
+
if (errored) throw new Error("rg: spawn error");
|
|
18537
18849
|
yield {
|
|
18538
|
-
type: "
|
|
18539
|
-
|
|
18540
|
-
|
|
18850
|
+
type: "final",
|
|
18851
|
+
output: {
|
|
18852
|
+
matches,
|
|
18853
|
+
count: mode === "count" ? totalCount : totalLines,
|
|
18854
|
+
truncated: totalLines > limit || bufOverflow,
|
|
18855
|
+
used: "rg"
|
|
18856
|
+
}
|
|
18541
18857
|
};
|
|
18542
|
-
}
|
|
18543
|
-
|
|
18544
|
-
|
|
18545
|
-
|
|
18546
|
-
|
|
18547
|
-
|
|
18548
|
-
|
|
18549
|
-
|
|
18550
|
-
|
|
18858
|
+
} finally {
|
|
18859
|
+
child.stdout?.off("data", onStdoutData);
|
|
18860
|
+
child.off("error", onError);
|
|
18861
|
+
child.off("close", onClose);
|
|
18862
|
+
child.stdout?.destroy();
|
|
18863
|
+
queue.length = 0;
|
|
18864
|
+
queuedChars = 0;
|
|
18865
|
+
waiter = void 0;
|
|
18866
|
+
if (!closed && child.exitCode === null) {
|
|
18867
|
+
try {
|
|
18868
|
+
child.kill("SIGTERM");
|
|
18869
|
+
} catch {
|
|
18870
|
+
}
|
|
18551
18871
|
}
|
|
18552
|
-
}
|
|
18872
|
+
}
|
|
18553
18873
|
}
|
|
18554
18874
|
function parseRgCountLine(line) {
|
|
18555
18875
|
const idx = line.lastIndexOf(":");
|
|
@@ -18579,8 +18899,8 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
18579
18899
|
if (globRe && !globRe.test(name) && !globRe.test(full)) return;
|
|
18580
18900
|
if (globRe) globRe.lastIndex = 0;
|
|
18581
18901
|
try {
|
|
18582
|
-
const
|
|
18583
|
-
if (!
|
|
18902
|
+
const stat19 = await fs25.stat(full);
|
|
18903
|
+
if (!stat19.isFile() || stat19.size > maxBytes || stopped || signal.aborted) return;
|
|
18584
18904
|
const file = await fs25.open(full, "r");
|
|
18585
18905
|
try {
|
|
18586
18906
|
let bytesReadTotal = 0;
|
|
@@ -18680,8 +19000,8 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
18680
19000
|
files.push({ full, name: e.name });
|
|
18681
19001
|
}
|
|
18682
19002
|
}
|
|
18683
|
-
await
|
|
18684
|
-
await
|
|
19003
|
+
await mapWithConcurrency2(files, NATIVE_SCAN_CONCURRENCY, ({ full, name }) => scanFile(full, name));
|
|
19004
|
+
await mapWithConcurrency2(
|
|
18685
19005
|
subdirs,
|
|
18686
19006
|
Math.min(16, NATIVE_SCAN_CONCURRENCY),
|
|
18687
19007
|
({ full, rel }) => walk2(full, rel)
|
|
@@ -18700,7 +19020,7 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
18700
19020
|
init_spawn_stream();
|
|
18701
19021
|
init_util();
|
|
18702
19022
|
init_legacy_bridge();
|
|
18703
|
-
import { join as
|
|
19023
|
+
import { join as join25 } from "node:path";
|
|
18704
19024
|
import {
|
|
18705
19025
|
detectEcosystem as detectPackageEcosystem,
|
|
18706
19026
|
recordPackageAction
|
|
@@ -18711,6 +19031,10 @@ var installTool = {
|
|
|
18711
19031
|
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.",
|
|
18712
19032
|
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.",
|
|
18713
19033
|
permission: "confirm",
|
|
19034
|
+
// WS-046: gives permission decisions something to key on.
|
|
19035
|
+
// What gets installed is the subject — installing a package runs its
|
|
19036
|
+
// lifecycle scripts, so `express` and `evil-pkg` must not be interchangeable.
|
|
19037
|
+
subjectKey: "packages",
|
|
18714
19038
|
mutating: true,
|
|
18715
19039
|
riskTier: "standard",
|
|
18716
19040
|
icon: "package",
|
|
@@ -18880,10 +19204,10 @@ function resolveManifestPath(cwd, pkgManager) {
|
|
|
18880
19204
|
case "pnpm":
|
|
18881
19205
|
case "yarn":
|
|
18882
19206
|
case "npm":
|
|
18883
|
-
return
|
|
19207
|
+
return join25(cwd, "package.json");
|
|
18884
19208
|
/* v8 ignore next 2 -- pkgManager is always pnpm/yarn/npm; the default is defensive. */
|
|
18885
19209
|
default:
|
|
18886
|
-
return
|
|
19210
|
+
return join25(cwd, "package.json");
|
|
18887
19211
|
}
|
|
18888
19212
|
}
|
|
18889
19213
|
|
|
@@ -18891,6 +19215,24 @@ function resolveManifestPath(cwd, pkgManager) {
|
|
|
18891
19215
|
init_util();
|
|
18892
19216
|
import * as fs26 from "node:fs/promises";
|
|
18893
19217
|
import { deepMerge as deepMergeCore } from "@wrongstack/core/utils";
|
|
19218
|
+
var MAX_JSON_FILE_BYTES = 16 * 1024 * 1024;
|
|
19219
|
+
var MAX_JSON_FILE_BYTES_HUMAN = "16 MiB";
|
|
19220
|
+
var JsonFileTooLargeError = class extends Error {
|
|
19221
|
+
constructor(filePath, size) {
|
|
19222
|
+
super(
|
|
19223
|
+
`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.`
|
|
19224
|
+
);
|
|
19225
|
+
this.name = "JsonFileTooLargeError";
|
|
19226
|
+
}
|
|
19227
|
+
};
|
|
19228
|
+
async function readJsonFileBounded(filePath, ctx) {
|
|
19229
|
+
const resolved = await safeResolveReal(filePath, ctx);
|
|
19230
|
+
const stat19 = await fs26.stat(resolved);
|
|
19231
|
+
if (stat19.size > MAX_JSON_FILE_BYTES) {
|
|
19232
|
+
throw new JsonFileTooLargeError(filePath, stat19.size);
|
|
19233
|
+
}
|
|
19234
|
+
return fs26.readFile(resolved, "utf8");
|
|
19235
|
+
}
|
|
18894
19236
|
var jsonTool = {
|
|
18895
19237
|
name: "json",
|
|
18896
19238
|
category: "Data",
|
|
@@ -18898,6 +19240,7 @@ var jsonTool = {
|
|
|
18898
19240
|
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.',
|
|
18899
19241
|
permission: "auto",
|
|
18900
19242
|
mutating: false,
|
|
19243
|
+
maxOutputBytes: 262144,
|
|
18901
19244
|
timeoutMs: 5e3,
|
|
18902
19245
|
capabilities: ["fs.read"],
|
|
18903
19246
|
icon: "json",
|
|
@@ -18964,9 +19307,15 @@ async function executeParse(input, ctx) {
|
|
|
18964
19307
|
let raw;
|
|
18965
19308
|
if (input.file) {
|
|
18966
19309
|
try {
|
|
18967
|
-
raw = await
|
|
18968
|
-
} catch {
|
|
18969
|
-
return {
|
|
19310
|
+
raw = await readJsonFileBounded(input.file, ctx);
|
|
19311
|
+
} catch (error) {
|
|
19312
|
+
return {
|
|
19313
|
+
data: null,
|
|
19314
|
+
formatted: "",
|
|
19315
|
+
type: "unknown",
|
|
19316
|
+
action: "parse",
|
|
19317
|
+
error: error instanceof JsonFileTooLargeError ? error.message : "Could not read file"
|
|
19318
|
+
};
|
|
18970
19319
|
}
|
|
18971
19320
|
} else if (input.data) {
|
|
18972
19321
|
raw = input.data;
|
|
@@ -19022,10 +19371,16 @@ async function executeQuery(input, ctx) {
|
|
|
19022
19371
|
let parsed;
|
|
19023
19372
|
if (input.file) {
|
|
19024
19373
|
try {
|
|
19025
|
-
const raw = await
|
|
19374
|
+
const raw = await readJsonFileBounded(input.file, ctx);
|
|
19026
19375
|
parsed = JSON.parse(raw);
|
|
19027
|
-
} catch {
|
|
19028
|
-
return {
|
|
19376
|
+
} catch (error) {
|
|
19377
|
+
return {
|
|
19378
|
+
data: null,
|
|
19379
|
+
formatted: "",
|
|
19380
|
+
type: "unknown",
|
|
19381
|
+
action: "query",
|
|
19382
|
+
error: error instanceof JsonFileTooLargeError ? error.message : "Could not read/parse file"
|
|
19383
|
+
};
|
|
19029
19384
|
}
|
|
19030
19385
|
} else if (input.data) {
|
|
19031
19386
|
try {
|
|
@@ -19064,10 +19419,16 @@ async function executeValidate(input, ctx) {
|
|
|
19064
19419
|
let parsed;
|
|
19065
19420
|
if (input.file) {
|
|
19066
19421
|
try {
|
|
19067
|
-
const raw = await
|
|
19422
|
+
const raw = await readJsonFileBounded(input.file, ctx);
|
|
19068
19423
|
parsed = JSON.parse(raw);
|
|
19069
|
-
} catch {
|
|
19070
|
-
return {
|
|
19424
|
+
} catch (error) {
|
|
19425
|
+
return {
|
|
19426
|
+
data: null,
|
|
19427
|
+
formatted: "",
|
|
19428
|
+
type: "unknown",
|
|
19429
|
+
action: "validate",
|
|
19430
|
+
error: error instanceof JsonFileTooLargeError ? error.message : "Could not read/parse file"
|
|
19431
|
+
};
|
|
19071
19432
|
}
|
|
19072
19433
|
} else if (input.data) {
|
|
19073
19434
|
try {
|
|
@@ -19106,10 +19467,16 @@ async function executeTransform(input, ctx) {
|
|
|
19106
19467
|
let parsed;
|
|
19107
19468
|
if (input.file) {
|
|
19108
19469
|
try {
|
|
19109
|
-
const raw = await
|
|
19470
|
+
const raw = await readJsonFileBounded(input.file, ctx);
|
|
19110
19471
|
parsed = JSON.parse(raw);
|
|
19111
|
-
} catch {
|
|
19112
|
-
return {
|
|
19472
|
+
} catch (error) {
|
|
19473
|
+
return {
|
|
19474
|
+
data: null,
|
|
19475
|
+
formatted: "",
|
|
19476
|
+
type: "unknown",
|
|
19477
|
+
action: "transform",
|
|
19478
|
+
error: error instanceof JsonFileTooLargeError ? error.message : "Could not read/parse file"
|
|
19479
|
+
};
|
|
19113
19480
|
}
|
|
19114
19481
|
} else if (input.data) {
|
|
19115
19482
|
try {
|
|
@@ -19713,6 +20080,10 @@ async function handleKanbanDetailAction(projectRoot, input) {
|
|
|
19713
20080
|
}
|
|
19714
20081
|
|
|
19715
20082
|
// src/kanban-board-inputs.ts
|
|
20083
|
+
function agentSettableGate(enforcement) {
|
|
20084
|
+
if (enforcement === void 0 || enforcement === "off") return {};
|
|
20085
|
+
return { completionGate: { enforcement } };
|
|
20086
|
+
}
|
|
19716
20087
|
function boardCreateInput(input, title) {
|
|
19717
20088
|
return {
|
|
19718
20089
|
title,
|
|
@@ -19725,7 +20096,7 @@ function boardCreateInput(input, title) {
|
|
|
19725
20096
|
decomposition: input.atomicityDecomposition ?? "propose"
|
|
19726
20097
|
}
|
|
19727
20098
|
} : {},
|
|
19728
|
-
...input.gateEnforcement
|
|
20099
|
+
...agentSettableGate(input.gateEnforcement)
|
|
19729
20100
|
};
|
|
19730
20101
|
}
|
|
19731
20102
|
function boardUpdatePatch(input) {
|
|
@@ -19739,7 +20110,7 @@ function boardUpdatePatch(input) {
|
|
|
19739
20110
|
decomposition: input.atomicityDecomposition ?? "propose"
|
|
19740
20111
|
}
|
|
19741
20112
|
} : {},
|
|
19742
|
-
...input.gateEnforcement
|
|
20113
|
+
...agentSettableGate(input.gateEnforcement)
|
|
19743
20114
|
};
|
|
19744
20115
|
}
|
|
19745
20116
|
function duplicateBoardOptions(input) {
|
|
@@ -19754,6 +20125,7 @@ function duplicateBoardOptions(input) {
|
|
|
19754
20125
|
|
|
19755
20126
|
// src/kanban-task-inputs.ts
|
|
19756
20127
|
import { randomUUID } from "node:crypto";
|
|
20128
|
+
import { clampSubagentCapabilities } from "@wrongstack/core/security";
|
|
19757
20129
|
function taskInput(input) {
|
|
19758
20130
|
const assignment = hasAssignmentInput(input) ? assignmentForTaskCreate(input) : void 0;
|
|
19759
20131
|
return {
|
|
@@ -19851,6 +20223,10 @@ function taskPatch(input) {
|
|
|
19851
20223
|
...input.actualHours !== void 0 ? { actualHours: input.actualHours } : {}
|
|
19852
20224
|
};
|
|
19853
20225
|
}
|
|
20226
|
+
function clampRequestedCapabilities(requested) {
|
|
20227
|
+
if (requested === void 0) return void 0;
|
|
20228
|
+
return clampSubagentCapabilities(requested).granted;
|
|
20229
|
+
}
|
|
19854
20230
|
function assignmentInput(input) {
|
|
19855
20231
|
return {
|
|
19856
20232
|
agentId: input.agentId,
|
|
@@ -19861,7 +20237,7 @@ function assignmentInput(input) {
|
|
|
19861
20237
|
fallbackProfile: input.fallbackProfile,
|
|
19862
20238
|
fallbackModels: input.fallbackModels,
|
|
19863
20239
|
tools: input.tools,
|
|
19864
|
-
allowedCapabilities: input.allowedCapabilities,
|
|
20240
|
+
allowedCapabilities: clampRequestedCapabilities(input.allowedCapabilities),
|
|
19865
20241
|
assignee: input.assignee,
|
|
19866
20242
|
leaseId: input.leaseId,
|
|
19867
20243
|
claimedAt: input.claimedAt,
|
|
@@ -19888,7 +20264,7 @@ function assignmentForTaskCreate(input) {
|
|
|
19888
20264
|
...input.fallbackProfile !== void 0 ? { fallbackProfile: input.fallbackProfile } : {},
|
|
19889
20265
|
...input.fallbackModels !== void 0 ? { fallbackModels: input.fallbackModels } : {},
|
|
19890
20266
|
...input.tools !== void 0 ? { tools: input.tools } : {},
|
|
19891
|
-
...input.allowedCapabilities !== void 0 ? { allowedCapabilities: input.allowedCapabilities } : {},
|
|
20267
|
+
...input.allowedCapabilities !== void 0 ? { allowedCapabilities: clampRequestedCapabilities(input.allowedCapabilities) } : {},
|
|
19892
20268
|
...input.leaseId !== void 0 ? { leaseId: input.leaseId } : {},
|
|
19893
20269
|
...input.claimedAt !== void 0 ? { claimedAt: input.claimedAt } : {},
|
|
19894
20270
|
...input.heartbeatAt !== void 0 ? { heartbeatAt: input.heartbeatAt } : {},
|
|
@@ -20099,7 +20475,13 @@ var KANBAN_INPUT_SCHEMA = {
|
|
|
20099
20475
|
moveTasksToColumnId: { type: "string" },
|
|
20100
20476
|
atomicityMode: { type: "string", enum: ["off", "assess", "enforce"] },
|
|
20101
20477
|
atomicityDecomposition: { type: "string", enum: ["auto", "propose"] },
|
|
20102
|
-
gateEnforcement: {
|
|
20478
|
+
gateEnforcement: {
|
|
20479
|
+
type: "string",
|
|
20480
|
+
// WS-023: `'off'` is deliberately absent. The agent whose work this gate
|
|
20481
|
+
// checks must not be able to switch it off; it may only tighten. Turning
|
|
20482
|
+
// a gate off stays a human decision, made through board config.
|
|
20483
|
+
enum: ["strict", "soft"]
|
|
20484
|
+
},
|
|
20103
20485
|
subtasks: {
|
|
20104
20486
|
type: "array",
|
|
20105
20487
|
minItems: 2,
|
|
@@ -20141,7 +20523,7 @@ function createKanbanPresenceWrapper(projectRoot, input, ctx) {
|
|
|
20141
20523
|
|
|
20142
20524
|
// src/session-kanban.ts
|
|
20143
20525
|
import { watch } from "node:fs";
|
|
20144
|
-
import { basename as
|
|
20526
|
+
import { basename as basename13, dirname as dirname15 } from "node:path";
|
|
20145
20527
|
import { getSharedProjectMailbox } from "@wrongstack/core/coordination";
|
|
20146
20528
|
import {
|
|
20147
20529
|
loadPlan,
|
|
@@ -20231,7 +20613,9 @@ async function ensureSessionKanbanBoard(projectRoot, sessionId) {
|
|
|
20231
20613
|
description: "Live session work: todos, tasks, and plan items.",
|
|
20232
20614
|
tags: sessionBoardTags(sessionId),
|
|
20233
20615
|
columns: SESSION_KANBAN_COLUMNS,
|
|
20234
|
-
generatedBy: `session-kanban:${sessionId}
|
|
20616
|
+
generatedBy: `session-kanban:${sessionId}`,
|
|
20617
|
+
kind: "session_mirror",
|
|
20618
|
+
retention: { mode: "archive_after_ttl", ttlMs: 7 * 24 * 60 * 60 * 1e3 }
|
|
20235
20619
|
});
|
|
20236
20620
|
}
|
|
20237
20621
|
if (!sameColumns(board.columns) || !board.tags?.includes(SESSION_BOARD_TAG)) {
|
|
@@ -20466,12 +20850,26 @@ function projectSessionPlanToKanban(projectRoot, items, sessionId) {
|
|
|
20466
20850
|
function fireAndForget(context, work) {
|
|
20467
20851
|
void work.catch((err) => {
|
|
20468
20852
|
const message = err instanceof Error ? err.message : String(err);
|
|
20469
|
-
console.warn(
|
|
20853
|
+
console.warn(
|
|
20854
|
+
JSON.stringify({
|
|
20855
|
+
level: "warn",
|
|
20856
|
+
event: "session-kanban",
|
|
20857
|
+
context,
|
|
20858
|
+
message,
|
|
20859
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
20860
|
+
})
|
|
20861
|
+
);
|
|
20470
20862
|
});
|
|
20471
20863
|
}
|
|
20472
20864
|
function broadcastTodoUpdate(context, todos) {
|
|
20473
20865
|
const sessionId = context.session?.id ?? "";
|
|
20474
20866
|
if (!context.agentId || !sessionId) return;
|
|
20867
|
+
const statusCounts = { pending: 0, inProgress: 0, completed: 0 };
|
|
20868
|
+
for (const todo of todos) {
|
|
20869
|
+
if (todo.status === "completed") statusCounts.completed++;
|
|
20870
|
+
else if (todo.status === "in_progress") statusCounts.inProgress++;
|
|
20871
|
+
else statusCounts.pending++;
|
|
20872
|
+
}
|
|
20475
20873
|
const projectDir = resolveWstackPaths3({ projectRoot: context.projectRoot }).projectDir;
|
|
20476
20874
|
const mailbox = getSharedProjectMailbox(projectDir);
|
|
20477
20875
|
void mailbox.send({
|
|
@@ -20483,11 +20881,11 @@ function broadcastTodoUpdate(context, todos) {
|
|
|
20483
20881
|
kind: "kanban.todos.updated",
|
|
20484
20882
|
sessionId,
|
|
20485
20883
|
revision: context.state.revision,
|
|
20486
|
-
todos
|
|
20884
|
+
todoCount: todos.length,
|
|
20885
|
+
statusCounts
|
|
20487
20886
|
}),
|
|
20488
20887
|
priority: "normal",
|
|
20489
|
-
senderSessionId: sessionId
|
|
20490
|
-
ttlMs: 6 * 60 * 60 * 1e3
|
|
20888
|
+
senderSessionId: sessionId
|
|
20491
20889
|
}).catch(() => {
|
|
20492
20890
|
});
|
|
20493
20891
|
}
|
|
@@ -20623,8 +21021,8 @@ function attachSessionKanbanMirror(context) {
|
|
|
20623
21021
|
const name = filename?.toString();
|
|
20624
21022
|
const currentPlanPath = context.meta["plan.path"];
|
|
20625
21023
|
const currentTaskPath = context.meta["task.path"];
|
|
20626
|
-
const planName = typeof currentPlanPath === "string" ?
|
|
20627
|
-
const taskName = typeof currentTaskPath === "string" ?
|
|
21024
|
+
const planName = typeof currentPlanPath === "string" ? basename13(currentPlanPath) : "";
|
|
21025
|
+
const taskName = typeof currentTaskPath === "string" ? basename13(currentTaskPath) : "";
|
|
20628
21026
|
if (name && name !== planName && name !== taskName) return;
|
|
20629
21027
|
if (timer) clearTimeout(timer);
|
|
20630
21028
|
timer = setTimeout(() => fireAndForget("refresh-files", refreshFiles()), 60);
|
|
@@ -21640,11 +22038,11 @@ var lintTool = {
|
|
|
21640
22038
|
}
|
|
21641
22039
|
};
|
|
21642
22040
|
async function detectLinter(cwd) {
|
|
21643
|
-
const { stat:
|
|
22041
|
+
const { stat: stat19 } = await import("node:fs/promises");
|
|
21644
22042
|
const checks = ["biome.json", ".eslintrc.json", "tslint.json", ".eslintrc.js", "tsconfig.json"];
|
|
21645
22043
|
for (const f of checks) {
|
|
21646
22044
|
try {
|
|
21647
|
-
await
|
|
22045
|
+
await stat19(`${cwd}/${f}`);
|
|
21648
22046
|
if (f.includes("biome")) return "biome";
|
|
21649
22047
|
if (f.includes("eslint")) return "eslint";
|
|
21650
22048
|
if (f.includes("tslint")) return "tslint";
|
|
@@ -21888,6 +22286,9 @@ var outdatedTool = {
|
|
|
21888
22286
|
// tool.confirm_needed flow on every invocation. M-1 originally
|
|
21889
22287
|
// fixed four sibling tools (mcp_control, shellcheck, shellcheck (scan mode),
|
|
21890
22288
|
// search) but missed this one; applying the same contract here.
|
|
22289
|
+
// WS-046: gives permission decisions something to key on.
|
|
22290
|
+
// The working directory scanned.
|
|
22291
|
+
subjectKey: "cwd",
|
|
21891
22292
|
mutating: true,
|
|
21892
22293
|
// Capability is outbound network — the tool only hits the package
|
|
21893
22294
|
// registry over HTTP, never touches the filesystem or runs shell.
|
|
@@ -22069,6 +22470,10 @@ var patchTool = {
|
|
|
22069
22470
|
useInstead: ["edit"]
|
|
22070
22471
|
},
|
|
22071
22472
|
permission: "confirm",
|
|
22473
|
+
// WS-046: gives permission decisions something to key on.
|
|
22474
|
+
// The target directory. The patch body itself is unstable (every diff
|
|
22475
|
+
// differs), so it could never re-match its own stored rule.
|
|
22476
|
+
subjectKey: "directory",
|
|
22072
22477
|
mutating: true,
|
|
22073
22478
|
capabilities: ["fs.write"],
|
|
22074
22479
|
icon: "edit",
|
|
@@ -22135,8 +22540,8 @@ var patchTool = {
|
|
|
22135
22540
|
const before = beforeContents.get(target) ?? null;
|
|
22136
22541
|
const after = await readTextForTracking(target);
|
|
22137
22542
|
if (after === null || after === before) continue;
|
|
22138
|
-
const
|
|
22139
|
-
if (
|
|
22543
|
+
const stat19 = await fs27.stat(target).catch(() => null);
|
|
22544
|
+
if (stat19) ctx.recordRead?.(target, stat19.mtimeMs, "write", sha256hex(after));
|
|
22140
22545
|
ctx.session?.recordFileChange?.({
|
|
22141
22546
|
path: target,
|
|
22142
22547
|
action: before === null ? "created" : "modified",
|
|
@@ -22161,8 +22566,8 @@ var patchTool = {
|
|
|
22161
22566
|
var MAX_TRACKING_BYTES = 5 * 1024 * 1024;
|
|
22162
22567
|
async function readTextForTracking(absPath) {
|
|
22163
22568
|
try {
|
|
22164
|
-
const
|
|
22165
|
-
if (!
|
|
22569
|
+
const stat19 = await fs27.stat(absPath);
|
|
22570
|
+
if (!stat19.isFile() || stat19.size > MAX_TRACKING_BYTES) return null;
|
|
22166
22571
|
const buf = await fs27.readFile(absPath);
|
|
22167
22572
|
if (buf.includes(0)) return null;
|
|
22168
22573
|
return buf.toString("utf8");
|
|
@@ -22192,7 +22597,13 @@ function runPatch(args, cwd, signal) {
|
|
|
22192
22597
|
let stdout = "";
|
|
22193
22598
|
let stderr = "";
|
|
22194
22599
|
const env = { ...buildChildEnv8(), LANG: "C", LC_ALL: "C" };
|
|
22195
|
-
const child = spawn14("patch", args, {
|
|
22600
|
+
const child = spawn14("patch", args, {
|
|
22601
|
+
cwd,
|
|
22602
|
+
signal,
|
|
22603
|
+
env,
|
|
22604
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
22605
|
+
windowsHide: true
|
|
22606
|
+
});
|
|
22196
22607
|
child.stdout?.on("data", (c) => {
|
|
22197
22608
|
stdout += c.toString();
|
|
22198
22609
|
});
|
|
@@ -22213,6 +22624,7 @@ function extractPatchedFiles(output) {
|
|
|
22213
22624
|
}
|
|
22214
22625
|
|
|
22215
22626
|
// src/plan.ts
|
|
22627
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
22216
22628
|
import {
|
|
22217
22629
|
addPlanItem,
|
|
22218
22630
|
clearPlan,
|
|
@@ -22220,20 +22632,20 @@ import {
|
|
|
22220
22632
|
formatPlan,
|
|
22221
22633
|
getPlanTemplate,
|
|
22222
22634
|
mutatePlan as mutatePlan2,
|
|
22635
|
+
mutateTasks as mutateTasks2,
|
|
22223
22636
|
removePlanItem,
|
|
22224
22637
|
setPlanItemStatus
|
|
22225
22638
|
} from "@wrongstack/core/storage";
|
|
22226
|
-
import {
|
|
22227
|
-
mutateTasks as mutateTasks2
|
|
22228
|
-
} from "@wrongstack/core/storage";
|
|
22229
22639
|
import { formatTaskList } from "@wrongstack/core/utils";
|
|
22230
|
-
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
22231
22640
|
var planTool = {
|
|
22232
22641
|
name: "plan",
|
|
22233
22642
|
category: "Session",
|
|
22234
22643
|
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.',
|
|
22235
22644
|
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.',
|
|
22236
22645
|
permission: "confirm",
|
|
22646
|
+
// WS-046: gives permission decisions something to key on.
|
|
22647
|
+
// The action performed; plan has no single file or path subject.
|
|
22648
|
+
subjectKey: "action",
|
|
22237
22649
|
mutating: true,
|
|
22238
22650
|
capabilities: ["fs.write"],
|
|
22239
22651
|
icon: "plan",
|
|
@@ -22290,7 +22702,10 @@ var planTool = {
|
|
|
22290
22702
|
let planPath;
|
|
22291
22703
|
if (input.scope === "project") {
|
|
22292
22704
|
if (typeof sessionPlanPath === "string") {
|
|
22293
|
-
const lastSep = Math.max(
|
|
22705
|
+
const lastSep = Math.max(
|
|
22706
|
+
sessionPlanPath.lastIndexOf("/"),
|
|
22707
|
+
sessionPlanPath.lastIndexOf("\\")
|
|
22708
|
+
);
|
|
22294
22709
|
planPath = lastSep >= 0 ? sessionPlanPath.slice(0, lastSep + 1) + "backlog.plan.json" : "backlog.plan.json";
|
|
22295
22710
|
}
|
|
22296
22711
|
} else {
|
|
@@ -22327,7 +22742,11 @@ var planTool = {
|
|
|
22327
22742
|
case "start":
|
|
22328
22743
|
case "done": {
|
|
22329
22744
|
if (!input.target) {
|
|
22330
|
-
early = mkResult(
|
|
22745
|
+
early = mkResult(
|
|
22746
|
+
p,
|
|
22747
|
+
false,
|
|
22748
|
+
`${input.action} requires \`target\` (id|index|substring).`
|
|
22749
|
+
);
|
|
22331
22750
|
return p;
|
|
22332
22751
|
}
|
|
22333
22752
|
const next = setPlanItemStatus(
|
|
@@ -22355,7 +22774,11 @@ var planTool = {
|
|
|
22355
22774
|
}
|
|
22356
22775
|
case "promote": {
|
|
22357
22776
|
if (!input.target) {
|
|
22358
|
-
early = mkResult(
|
|
22777
|
+
early = mkResult(
|
|
22778
|
+
p,
|
|
22779
|
+
false,
|
|
22780
|
+
`${input.action} requires \`target\` (id|index|substring).`
|
|
22781
|
+
);
|
|
22359
22782
|
return p;
|
|
22360
22783
|
}
|
|
22361
22784
|
const derived = deriveTodosFromPlanItem(p, input.target, input.subtasks);
|
|
@@ -22398,7 +22821,11 @@ var planTool = {
|
|
|
22398
22821
|
return clearPlan(p);
|
|
22399
22822
|
case "taskify": {
|
|
22400
22823
|
if (!input.target) {
|
|
22401
|
-
early = mkResult(
|
|
22824
|
+
early = mkResult(
|
|
22825
|
+
p,
|
|
22826
|
+
false,
|
|
22827
|
+
"taskify requires `target` (plan item id|index|substring)."
|
|
22828
|
+
);
|
|
22402
22829
|
return p;
|
|
22403
22830
|
}
|
|
22404
22831
|
let itemIdx = -1;
|
|
@@ -22471,7 +22898,11 @@ var planTool = {
|
|
|
22471
22898
|
${formatTaskList(taskFile.tasks)}`
|
|
22472
22899
|
);
|
|
22473
22900
|
} catch (err) {
|
|
22474
|
-
return mkResult(
|
|
22901
|
+
return mkResult(
|
|
22902
|
+
plan,
|
|
22903
|
+
false,
|
|
22904
|
+
`taskify: task not saved \u2014 ${err instanceof Error ? err.message : String(err)}`
|
|
22905
|
+
);
|
|
22475
22906
|
}
|
|
22476
22907
|
}
|
|
22477
22908
|
return mkResult(plan, true, `Plan ${input.action} ok.`);
|
|
@@ -22548,9 +22979,9 @@ var readTool = {
|
|
|
22548
22979
|
}
|
|
22549
22980
|
const absPath = await safeResolveReal(input.path, ctx);
|
|
22550
22981
|
const shouldIncludeSymbols = input.includeSymbols === true || input.includeSymbols !== false && ctx.meta[ADVANCED_MODE_META_KEY] === true;
|
|
22551
|
-
let
|
|
22982
|
+
let stat19;
|
|
22552
22983
|
try {
|
|
22553
|
-
|
|
22984
|
+
stat19 = await fs28.stat(absPath);
|
|
22554
22985
|
} catch (err) {
|
|
22555
22986
|
const code = err.code;
|
|
22556
22987
|
if (code === "ENOENT") {
|
|
@@ -22569,7 +23000,7 @@ var readTool = {
|
|
|
22569
23000
|
cause: err
|
|
22570
23001
|
});
|
|
22571
23002
|
}
|
|
22572
|
-
if (!
|
|
23003
|
+
if (!stat19.isFile()) {
|
|
22573
23004
|
throw new FsError({
|
|
22574
23005
|
message: `read: "${input.path}" is not a regular file`,
|
|
22575
23006
|
code: "FS_READ_FAILED",
|
|
@@ -22577,23 +23008,23 @@ var readTool = {
|
|
|
22577
23008
|
context: { reason: "not-a-regular-file" }
|
|
22578
23009
|
});
|
|
22579
23010
|
}
|
|
22580
|
-
if (
|
|
23011
|
+
if (stat19.size > MAX_BYTES2) {
|
|
22581
23012
|
throw new FsError({
|
|
22582
|
-
message: `read: file too large (${
|
|
23013
|
+
message: `read: file too large (${stat19.size} bytes, limit ${MAX_BYTES2})`,
|
|
22583
23014
|
code: "FS_READ_FAILED",
|
|
22584
23015
|
path: absPath,
|
|
22585
|
-
context: { size:
|
|
23016
|
+
context: { size: stat19.size, limit: MAX_BYTES2, reason: "too-large" }
|
|
22586
23017
|
});
|
|
22587
23018
|
}
|
|
22588
23019
|
const offset = Math.max(1, input.offset ?? 1);
|
|
22589
23020
|
const limit = Math.max(0, Math.min(input.limit ?? 2e3, 5e3));
|
|
22590
23021
|
const prior = getReadRangeRecord(ctx, absPath);
|
|
22591
23022
|
const requestedEnd = prior ? Math.min(offset + limit - 1, prior.totalLines) : offset + limit - 1;
|
|
22592
|
-
if (input.mode !== "summary" && limit > 0 && prior && coversRange(prior,
|
|
22593
|
-
ctx.recordRead(absPath,
|
|
23023
|
+
if (input.mode !== "summary" && limit > 0 && prior && coversRange(prior, stat19.mtimeMs, offset, requestedEnd)) {
|
|
23024
|
+
ctx.recordRead(absPath, stat19.mtimeMs);
|
|
22594
23025
|
const symResult2 = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
|
|
22595
23026
|
return {
|
|
22596
|
-
text: `[unchanged since previous read: "${input.path}" mtime=${Math.round(
|
|
23027
|
+
text: `[unchanged since previous read: "${input.path}" mtime=${Math.round(stat19.mtimeMs)}; requested lines ${offset}-${requestedEnd} were already shown. Use offset/limit for a new range if needed.]`,
|
|
22597
23028
|
total_lines: prior.totalLines,
|
|
22598
23029
|
encoding: "utf8",
|
|
22599
23030
|
truncated: requestedEnd < prior.totalLines,
|
|
@@ -22611,11 +23042,11 @@ var readTool = {
|
|
|
22611
23042
|
const allLines = text.split(/\r\n|\r|\n/);
|
|
22612
23043
|
const total = allLines.length;
|
|
22613
23044
|
if (input.mode === "summary") {
|
|
22614
|
-
ctx.recordRead(absPath,
|
|
22615
|
-
rememberReadRange(ctx, absPath,
|
|
23045
|
+
ctx.recordRead(absPath, stat19.mtimeMs, "user", contentHash);
|
|
23046
|
+
rememberReadRange(ctx, absPath, stat19.mtimeMs, total, 1, Math.min(total, 200));
|
|
22616
23047
|
const symResult2 = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
|
|
22617
23048
|
return {
|
|
22618
|
-
text: summarizeFile(input.path,
|
|
23049
|
+
text: summarizeFile(input.path, stat19.size, allLines),
|
|
22619
23050
|
total_lines: total,
|
|
22620
23051
|
encoding: "utf8",
|
|
22621
23052
|
truncated: total > 200,
|
|
@@ -22627,8 +23058,8 @@ var readTool = {
|
|
|
22627
23058
|
};
|
|
22628
23059
|
}
|
|
22629
23060
|
if (limit === 0) {
|
|
22630
|
-
ctx.recordRead(absPath,
|
|
22631
|
-
rememberReadRange(ctx, absPath,
|
|
23061
|
+
ctx.recordRead(absPath, stat19.mtimeMs, "user", contentHash);
|
|
23062
|
+
rememberReadRange(ctx, absPath, stat19.mtimeMs, total, 1, 0);
|
|
22632
23063
|
const symResult2 = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
|
|
22633
23064
|
return {
|
|
22634
23065
|
text: "",
|
|
@@ -22640,8 +23071,8 @@ var readTool = {
|
|
|
22640
23071
|
};
|
|
22641
23072
|
}
|
|
22642
23073
|
if (offset > total) {
|
|
22643
|
-
ctx.recordRead(absPath,
|
|
22644
|
-
rememberReadRange(ctx, absPath,
|
|
23074
|
+
ctx.recordRead(absPath, stat19.mtimeMs, "user", contentHash);
|
|
23075
|
+
rememberReadRange(ctx, absPath, stat19.mtimeMs, total, total + 1, total + 1);
|
|
22645
23076
|
const symResult2 = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
|
|
22646
23077
|
return {
|
|
22647
23078
|
text: `[offset ${offset} is past end of file "${input.path}" \u2014 file has ${total} line(s). Do not retry this offset.]`,
|
|
@@ -22656,8 +23087,8 @@ var readTool = {
|
|
|
22656
23087
|
const truncated = offset - 1 + slice.length < total;
|
|
22657
23088
|
const width = String(offset + slice.length - 1).length;
|
|
22658
23089
|
const numbered = slice.map((line, i) => `${String(offset + i).padStart(width, " ")}\u2192${line}`).join("\n");
|
|
22659
|
-
ctx.recordRead(absPath,
|
|
22660
|
-
rememberReadRange(ctx, absPath,
|
|
23090
|
+
ctx.recordRead(absPath, stat19.mtimeMs, "user", contentHash);
|
|
23091
|
+
rememberReadRange(ctx, absPath, stat19.mtimeMs, total, offset, offset + slice.length - 1);
|
|
22661
23092
|
const symResult = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
|
|
22662
23093
|
return {
|
|
22663
23094
|
text: numbered,
|
|
@@ -22763,20 +23194,20 @@ ${interesting.join("\n")}` : "symbols/imports: (none detected)"
|
|
|
22763
23194
|
}
|
|
22764
23195
|
|
|
22765
23196
|
// src/replace.ts
|
|
22766
|
-
import { expectDefined as expectDefined8 } from "@wrongstack/core/utils";
|
|
22767
23197
|
import { spawn as spawn15 } from "node:child_process";
|
|
22768
23198
|
import * as fs29 from "node:fs/promises";
|
|
22769
23199
|
import * as path33 from "node:path";
|
|
23200
|
+
import { ToolValidationError as ToolValidationError6 } from "@wrongstack/core/types";
|
|
22770
23201
|
import {
|
|
22771
23202
|
atomicWrite as atomicWrite3,
|
|
22772
23203
|
buildChildEnv as buildChildEnv9,
|
|
22773
23204
|
compileGlob as compileGlob4,
|
|
22774
23205
|
detectNewlineStyle as detectNewlineStyle2,
|
|
23206
|
+
expectDefined as expectDefined8,
|
|
22775
23207
|
normalizeToLf as normalizeToLf2,
|
|
22776
23208
|
toStyle as toStyle2,
|
|
22777
23209
|
unifiedDiff as unifiedDiff2
|
|
22778
23210
|
} from "@wrongstack/core/utils";
|
|
22779
|
-
import { ToolValidationError as ToolValidationError6 } from "@wrongstack/core/types";
|
|
22780
23211
|
init_util();
|
|
22781
23212
|
var DEFAULT_IGNORE4 = ["node_modules", ".git", "dist", "build", ".next", "coverage"];
|
|
22782
23213
|
var replaceTool = {
|
|
@@ -22785,6 +23216,10 @@ var replaceTool = {
|
|
|
22785
23216
|
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.",
|
|
22786
23217
|
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.",
|
|
22787
23218
|
permission: "confirm",
|
|
23219
|
+
// WS-046: gives permission decisions something to key on.
|
|
23220
|
+
// The file scope being rewritten, not the pattern: a trust rule should say
|
|
23221
|
+
// where a bulk replace may run.
|
|
23222
|
+
subjectKey: "files",
|
|
22788
23223
|
mutating: true,
|
|
22789
23224
|
capabilities: ["fs.write"],
|
|
22790
23225
|
icon: "edit",
|
|
@@ -22857,8 +23292,8 @@ var replaceTool = {
|
|
|
22857
23292
|
}
|
|
22858
23293
|
const rel = path33.relative(realRoot, realPath);
|
|
22859
23294
|
if (rel.startsWith("..") || path33.isAbsolute(rel)) continue;
|
|
22860
|
-
const
|
|
22861
|
-
if (!
|
|
23295
|
+
const stat19 = await fs29.stat(realPath).catch(() => null);
|
|
23296
|
+
if (!stat19?.isFile()) continue;
|
|
22862
23297
|
let content;
|
|
22863
23298
|
try {
|
|
22864
23299
|
const buf = await fs29.readFile(realPath);
|
|
@@ -22883,7 +23318,7 @@ var replaceTool = {
|
|
|
22883
23318
|
totalReplacements += count;
|
|
22884
23319
|
if (!dryRun) {
|
|
22885
23320
|
const newContent = toStyle2(newContentLf, style);
|
|
22886
|
-
await atomicWrite3(realPath, newContent, { mode:
|
|
23321
|
+
await atomicWrite3(realPath, newContent, { mode: stat19.mode & 511 });
|
|
22887
23322
|
const written = await fs29.stat(realPath).catch(() => null);
|
|
22888
23323
|
if (written) {
|
|
22889
23324
|
ctx.recordRead?.(realPath, written.mtimeMs, "write", sha256hex(newContent));
|
|
@@ -22923,8 +23358,8 @@ async function resolveFiles2(filesInput, ctx, extraGlob) {
|
|
|
22923
23358
|
const resolved = [];
|
|
22924
23359
|
for (const p of parts) {
|
|
22925
23360
|
const absPath = safeResolve(p, ctx);
|
|
22926
|
-
const
|
|
22927
|
-
if (
|
|
23361
|
+
const stat19 = await fs29.stat(absPath).catch(() => null);
|
|
23362
|
+
if (stat19?.isFile()) {
|
|
22928
23363
|
resolved.push(absPath);
|
|
22929
23364
|
}
|
|
22930
23365
|
}
|
|
@@ -22944,7 +23379,11 @@ async function globFiles(pattern, base, extraGlob) {
|
|
|
22944
23379
|
function checkRg() {
|
|
22945
23380
|
return new Promise((resolve17) => {
|
|
22946
23381
|
try {
|
|
22947
|
-
const p = spawn15("rg", ["--version"], {
|
|
23382
|
+
const p = spawn15("rg", ["--version"], {
|
|
23383
|
+
env: buildChildEnv9(),
|
|
23384
|
+
stdio: "ignore",
|
|
23385
|
+
windowsHide: true
|
|
23386
|
+
});
|
|
22948
23387
|
p.on("error", () => resolve17(false));
|
|
22949
23388
|
p.on("close", (code) => resolve17(code === 0));
|
|
22950
23389
|
} catch {
|
|
@@ -22995,8 +23434,8 @@ async function globNative(pattern, base, extraGlob) {
|
|
|
22995
23434
|
if (DEFAULT_IGNORE4.includes(e.name)) continue;
|
|
22996
23435
|
const full = path33.join(dir, e.name);
|
|
22997
23436
|
try {
|
|
22998
|
-
const
|
|
22999
|
-
if (
|
|
23437
|
+
const stat19 = await fs29.lstat(full);
|
|
23438
|
+
if (stat19.isSymbolicLink()) continue;
|
|
23000
23439
|
} catch {
|
|
23001
23440
|
continue;
|
|
23002
23441
|
}
|
|
@@ -23120,6 +23559,7 @@ var scaffoldTool = {
|
|
|
23120
23559
|
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.",
|
|
23121
23560
|
permission: "confirm",
|
|
23122
23561
|
mutating: true,
|
|
23562
|
+
subjectKey: "name",
|
|
23123
23563
|
capabilities: ["fs.write.outside-project", "fs.write"],
|
|
23124
23564
|
icon: "scaffold",
|
|
23125
23565
|
timeoutMs: 3e4,
|
|
@@ -23194,15 +23634,17 @@ async function handleBuiltIn(name, templateFiles, cwd, ctx, dryRun, vars) {
|
|
|
23194
23634
|
output: dryRun ? `Would create ${filesCreated} files: ${files.join(", ")}` : `Created ${filesCreated} files: ${files.join(", ")}`
|
|
23195
23635
|
};
|
|
23196
23636
|
}
|
|
23637
|
+
function escapeRegexLiteral(value) {
|
|
23638
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
23639
|
+
}
|
|
23197
23640
|
function substituteVars(content, name, vars) {
|
|
23641
|
+
const kebab = name.toLowerCase().replace(/\s+/g, "-");
|
|
23642
|
+
const pascal = name.replace(/(?:^|[-_\s]+)([a-z])/g, (_, c) => c.toUpperCase());
|
|
23198
23643
|
let result = content;
|
|
23199
|
-
result = result.replace(/\{\{name\}\}/g,
|
|
23200
|
-
result = result.replace(
|
|
23201
|
-
/\{\{Name\}\}/g,
|
|
23202
|
-
name.replace(/(?:^|[-_\s]+)([a-z])/g, (_, c) => c.toUpperCase())
|
|
23203
|
-
);
|
|
23644
|
+
result = result.replace(/\{\{name\}\}/g, () => kebab);
|
|
23645
|
+
result = result.replace(/\{\{Name\}\}/g, () => pascal);
|
|
23204
23646
|
for (const [k, v] of Object.entries(vars)) {
|
|
23205
|
-
result = result.replace(new RegExp(`\\{\\{${k}\\}\\}`, "g"), v);
|
|
23647
|
+
result = result.replace(new RegExp(`\\{\\{${escapeRegexLiteral(k)}\\}\\}`, "g"), () => v);
|
|
23206
23648
|
}
|
|
23207
23649
|
return result;
|
|
23208
23650
|
}
|
|
@@ -24138,11 +24580,11 @@ var testTool = {
|
|
|
24138
24580
|
}
|
|
24139
24581
|
};
|
|
24140
24582
|
async function detectRunner(cwd) {
|
|
24141
|
-
const { stat:
|
|
24583
|
+
const { stat: stat19 } = await import("node:fs/promises");
|
|
24142
24584
|
const candidates = ["vitest.config.ts", "jest.config.js", ".mocharc.json"];
|
|
24143
24585
|
for (const f of candidates) {
|
|
24144
24586
|
try {
|
|
24145
|
-
await
|
|
24587
|
+
await stat19(path35.join(cwd, f));
|
|
24146
24588
|
if (f.includes("vitest")) return "vitest";
|
|
24147
24589
|
if (f.includes("jest")) return "jest";
|
|
24148
24590
|
if (f.includes("mocha")) return "mocha";
|
|
@@ -24539,12 +24981,19 @@ var toolSearchTool = {
|
|
|
24539
24981
|
};
|
|
24540
24982
|
|
|
24541
24983
|
// src/tool-use.ts
|
|
24984
|
+
import {
|
|
24985
|
+
GOVERNED_TOOL_EXECUTOR_META_KEY as GOVERNED_TOOL_EXECUTOR_META_KEY2
|
|
24986
|
+
} from "@wrongstack/core/types";
|
|
24542
24987
|
var toolUseTool = {
|
|
24543
24988
|
name: "tool_use",
|
|
24544
24989
|
category: "Meta",
|
|
24545
24990
|
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.",
|
|
24546
24991
|
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.",
|
|
24547
24992
|
permission: "confirm",
|
|
24993
|
+
// WS-046: gives permission decisions something to key on.
|
|
24994
|
+
// The tool being invoked through this indirection — the whole point of a
|
|
24995
|
+
// permission decision here is WHICH tool is being reached.
|
|
24996
|
+
subjectKey: "tool",
|
|
24548
24997
|
mutating: true,
|
|
24549
24998
|
timeoutMs: 6e4,
|
|
24550
24999
|
capabilities: ["tool.mutate.any"],
|
|
@@ -24563,7 +25012,7 @@ var toolUseTool = {
|
|
|
24563
25012
|
},
|
|
24564
25013
|
required: ["tool"]
|
|
24565
25014
|
},
|
|
24566
|
-
async execute(input, ctx
|
|
25015
|
+
async execute(input, ctx) {
|
|
24567
25016
|
const start = Date.now();
|
|
24568
25017
|
if (!input?.tool) {
|
|
24569
25018
|
return {
|
|
@@ -24582,6 +25031,14 @@ var toolUseTool = {
|
|
|
24582
25031
|
executionMs: Date.now() - start
|
|
24583
25032
|
};
|
|
24584
25033
|
}
|
|
25034
|
+
if (tool.name === toolUseTool.name) {
|
|
25035
|
+
return {
|
|
25036
|
+
tool: input.tool,
|
|
25037
|
+
success: false,
|
|
25038
|
+
error: "tool_use: recursive meta-tool execution is not allowed",
|
|
25039
|
+
executionMs: Date.now() - start
|
|
25040
|
+
};
|
|
25041
|
+
}
|
|
24585
25042
|
if (tool.permission === "deny") {
|
|
24586
25043
|
return {
|
|
24587
25044
|
tool: input.tool,
|
|
@@ -24590,12 +25047,21 @@ var toolUseTool = {
|
|
|
24590
25047
|
executionMs: Date.now() - start
|
|
24591
25048
|
};
|
|
24592
25049
|
}
|
|
25050
|
+
const governedExecute = ctx.meta[GOVERNED_TOOL_EXECUTOR_META_KEY2];
|
|
25051
|
+
if (typeof governedExecute !== "function") {
|
|
25052
|
+
return {
|
|
25053
|
+
tool: input.tool,
|
|
25054
|
+
success: false,
|
|
25055
|
+
error: "tool_use: governed nested execution is unavailable; call the tool directly",
|
|
25056
|
+
executionMs: Date.now() - start
|
|
25057
|
+
};
|
|
25058
|
+
}
|
|
24593
25059
|
try {
|
|
24594
|
-
const result = await
|
|
25060
|
+
const result = await governedExecute(input.tool, input.input ?? {});
|
|
24595
25061
|
return {
|
|
24596
25062
|
tool: input.tool,
|
|
24597
|
-
success:
|
|
24598
|
-
result,
|
|
25063
|
+
success: result.success,
|
|
25064
|
+
...result.success ? { result: result.result } : { error: result.error ?? "nested tool failed" },
|
|
24599
25065
|
executionMs: Date.now() - start
|
|
24600
25066
|
};
|
|
24601
25067
|
} catch (e) {
|
|
@@ -24611,10 +25077,12 @@ var toolUseTool = {
|
|
|
24611
25077
|
|
|
24612
25078
|
// src/tree.ts
|
|
24613
25079
|
init_util();
|
|
24614
|
-
import { DEFAULT_WALK_IGNORE_DIRS as DEFAULT_WALK_IGNORE_DIRS4, expectDefined as expectDefined10 } from "@wrongstack/core/utils";
|
|
24615
25080
|
import * as fs32 from "node:fs/promises";
|
|
24616
25081
|
import * as path36 from "node:path";
|
|
25082
|
+
import { DEFAULT_WALK_IGNORE_DIRS as DEFAULT_WALK_IGNORE_DIRS4, expectDefined as expectDefined10 } from "@wrongstack/core/utils";
|
|
24617
25083
|
var DEFAULT_IGNORE5 = [...DEFAULT_WALK_IGNORE_DIRS4, ".wrongstack", ".ssh", ".gnupg", ".aws"];
|
|
25084
|
+
var DEFAULT_MAX_ENTRIES2 = 5e3;
|
|
25085
|
+
var MAX_TREE_OUTPUT_BYTES = 256 * 1024;
|
|
24618
25086
|
var treeTool = {
|
|
24619
25087
|
name: "tree",
|
|
24620
25088
|
category: "Filesystem",
|
|
@@ -24658,6 +25126,12 @@ var treeTool = {
|
|
|
24658
25126
|
show_hidden: {
|
|
24659
25127
|
type: "boolean",
|
|
24660
25128
|
description: "Show hidden files starting with . (default: false)"
|
|
25129
|
+
},
|
|
25130
|
+
max_entries: {
|
|
25131
|
+
type: "integer",
|
|
25132
|
+
description: `Maximum entries retained in the rendered tree (default ${DEFAULT_MAX_ENTRIES2}).`,
|
|
25133
|
+
minimum: 1,
|
|
25134
|
+
maximum: 5e4
|
|
24661
25135
|
}
|
|
24662
25136
|
}
|
|
24663
25137
|
},
|
|
@@ -24671,7 +25145,7 @@ var treeTool = {
|
|
|
24671
25145
|
if (!final) throw new Error("tree: stream ended without final event");
|
|
24672
25146
|
return final;
|
|
24673
25147
|
},
|
|
24674
|
-
async *executeStream(input, ctx) {
|
|
25148
|
+
async *executeStream(input, ctx, opts) {
|
|
24675
25149
|
const basePath = input.path ? safeResolve(input.path, ctx) : ctx.cwd;
|
|
24676
25150
|
const maxDepth = input.depth ?? 3;
|
|
24677
25151
|
const showFiles = input.show_files ?? true;
|
|
@@ -24679,8 +25153,17 @@ var treeTool = {
|
|
|
24679
25153
|
const showHidden = input.show_hidden ?? false;
|
|
24680
25154
|
const exclude = /* @__PURE__ */ new Set([...DEFAULT_IGNORE5, ...input.exclude ?? []]);
|
|
24681
25155
|
const filterGlob = input.glob;
|
|
25156
|
+
const maxEntries = input.max_entries ?? DEFAULT_MAX_ENTRIES2;
|
|
25157
|
+
const signal = opts?.signal ?? ctx.signal ?? new AbortController().signal;
|
|
24682
25158
|
const lines = [basePath];
|
|
24683
25159
|
const totals = { totalFiles: { value: 0 }, totalDirs: { value: 0 } };
|
|
25160
|
+
const retention = {
|
|
25161
|
+
entries: 0,
|
|
25162
|
+
outputBytes: Buffer.byteLength(basePath, "utf8"),
|
|
25163
|
+
maxEntries,
|
|
25164
|
+
maxOutputBytes: MAX_TREE_OUTPUT_BYTES,
|
|
25165
|
+
truncated: false
|
|
25166
|
+
};
|
|
24684
25167
|
const queue = [];
|
|
24685
25168
|
const FLUSH_EVERY = 200;
|
|
24686
25169
|
let lastEmittedTotal = 0;
|
|
@@ -24707,12 +25190,19 @@ var treeTool = {
|
|
|
24707
25190
|
isLast: true,
|
|
24708
25191
|
totalFiles: totals.totalFiles,
|
|
24709
25192
|
totalDirs: totals.totalDirs,
|
|
24710
|
-
onProgress: tickProgress
|
|
25193
|
+
onProgress: tickProgress,
|
|
25194
|
+
retention,
|
|
25195
|
+
signal
|
|
24711
25196
|
});
|
|
24712
25197
|
let walkDone = false;
|
|
24713
|
-
walkPromise.
|
|
24714
|
-
|
|
24715
|
-
|
|
25198
|
+
void walkPromise.then(
|
|
25199
|
+
() => {
|
|
25200
|
+
walkDone = true;
|
|
25201
|
+
},
|
|
25202
|
+
() => {
|
|
25203
|
+
walkDone = true;
|
|
25204
|
+
}
|
|
25205
|
+
);
|
|
24716
25206
|
while (!walkDone || queue.length > 0) {
|
|
24717
25207
|
if (queue.length > 0) {
|
|
24718
25208
|
yield expectDefined10(queue.shift());
|
|
@@ -24735,13 +25225,15 @@ var treeTool = {
|
|
|
24735
25225
|
tree: lines.join("\n"),
|
|
24736
25226
|
total_files: totals.totalFiles.value,
|
|
24737
25227
|
total_dirs: totals.totalDirs.value,
|
|
24738
|
-
truncated:
|
|
25228
|
+
truncated: retention.truncated,
|
|
24739
25229
|
path: basePath
|
|
24740
25230
|
}
|
|
24741
25231
|
};
|
|
24742
25232
|
}
|
|
24743
25233
|
};
|
|
24744
25234
|
async function walkDir(dir, depth, opts) {
|
|
25235
|
+
opts.signal.throwIfAborted();
|
|
25236
|
+
if (opts.retention.truncated) return;
|
|
24745
25237
|
const entries = await fs32.readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
24746
25238
|
const filtered = entries.filter((e) => {
|
|
24747
25239
|
if (!opts.showHidden && e.name.startsWith(".")) return false;
|
|
@@ -24761,6 +25253,8 @@ async function walkDir(dir, depth, opts) {
|
|
|
24761
25253
|
return a.name.localeCompare(b.name);
|
|
24762
25254
|
});
|
|
24763
25255
|
for (let i = 0; i < items.length; i++) {
|
|
25256
|
+
opts.signal.throwIfAborted();
|
|
25257
|
+
if (opts.retention.truncated) return;
|
|
24764
25258
|
const entry = items[i];
|
|
24765
25259
|
if (!entry) continue;
|
|
24766
25260
|
const isLast = i === items.length - 1;
|
|
@@ -24769,7 +25263,15 @@ async function walkDir(dir, depth, opts) {
|
|
|
24769
25263
|
const displayName = entry.name + (entry.isDirectory() ? "/" : "");
|
|
24770
25264
|
if (!opts.showDirs && entry.isDirectory()) continue;
|
|
24771
25265
|
if (!opts.showFiles && entry.isFile()) continue;
|
|
24772
|
-
opts.
|
|
25266
|
+
const line = opts.prefix + branch + displayName;
|
|
25267
|
+
const lineBytes = Buffer.byteLength(line, "utf8") + 1;
|
|
25268
|
+
if (opts.retention.entries >= opts.retention.maxEntries || opts.retention.outputBytes + lineBytes > opts.retention.maxOutputBytes) {
|
|
25269
|
+
opts.retention.truncated = true;
|
|
25270
|
+
return;
|
|
25271
|
+
}
|
|
25272
|
+
opts.lines.push(line);
|
|
25273
|
+
opts.retention.entries++;
|
|
25274
|
+
opts.retention.outputBytes += lineBytes;
|
|
24773
25275
|
if (entry.isDirectory() && (opts.maxDepth === 0 || depth < opts.maxDepth)) {
|
|
24774
25276
|
const childPrefix = opts.prefix + connector;
|
|
24775
25277
|
await walkDir(path36.join(dir, entry.name), depth + 1, {
|
|
@@ -24884,11 +25386,11 @@ var typecheckTool = {
|
|
|
24884
25386
|
}
|
|
24885
25387
|
};
|
|
24886
25388
|
async function findTsConfig(cwd) {
|
|
24887
|
-
const { stat:
|
|
25389
|
+
const { stat: stat19 } = await import("node:fs/promises");
|
|
24888
25390
|
const candidates = ["tsconfig.json", "tsconfig.base.json"];
|
|
24889
25391
|
for (const f of candidates) {
|
|
24890
25392
|
try {
|
|
24891
|
-
const s = await
|
|
25393
|
+
const s = await stat19(path37.join(cwd, f));
|
|
24892
25394
|
if (s.isFile()) return path37.join(cwd, f);
|
|
24893
25395
|
} catch {
|
|
24894
25396
|
}
|
|
@@ -24963,12 +25465,12 @@ async function prepareWrite(input, ctx) {
|
|
|
24963
25465
|
let existed = false;
|
|
24964
25466
|
let prev = "";
|
|
24965
25467
|
try {
|
|
24966
|
-
const
|
|
24967
|
-
existed =
|
|
25468
|
+
const stat19 = await fs33.stat(absPath);
|
|
25469
|
+
existed = stat19.isFile();
|
|
24968
25470
|
if (existed) {
|
|
24969
25471
|
if (!ctx.hasRead(absPath)) {
|
|
24970
25472
|
prev = await fs33.readFile(absPath, "utf8");
|
|
24971
|
-
ctx.recordRead(absPath,
|
|
25473
|
+
ctx.recordRead(absPath, stat19.mtimeMs, "write", sha256hex(prev));
|
|
24972
25474
|
} else {
|
|
24973
25475
|
prev = await fs33.readFile(absPath, "utf8");
|
|
24974
25476
|
}
|
|
@@ -24985,8 +25487,8 @@ async function finishWrite(input, ctx, prepared, signal) {
|
|
|
24985
25487
|
await atomicWrite5(prepared.absPath, input.content);
|
|
24986
25488
|
const diff = prepared.existed ? unifiedDiff3(prepared.prev, input.content, { fromFile: input.path, toFile: input.path }) : `+++ ${input.path}
|
|
24987
25489
|
+ (new file, ${input.content.split("\n").length} lines)`;
|
|
24988
|
-
const
|
|
24989
|
-
ctx.recordRead(prepared.absPath,
|
|
25490
|
+
const stat19 = await fs33.stat(prepared.absPath);
|
|
25491
|
+
ctx.recordRead(prepared.absPath, stat19.mtimeMs, "write", sha256hex(input.content));
|
|
24990
25492
|
ctx.session.recordFileChange({
|
|
24991
25493
|
path: prepared.absPath,
|
|
24992
25494
|
action: prepared.existed ? "modified" : "created",
|
|
@@ -26174,9 +26676,9 @@ async function walk(root, dir, out) {
|
|
|
26174
26676
|
} else if (e.isFile()) {
|
|
26175
26677
|
if (e.name === "SKILL.md" || e.name === "SKILL.save.md") continue;
|
|
26176
26678
|
try {
|
|
26177
|
-
const
|
|
26679
|
+
const stat19 = await fs34.stat(fullPath);
|
|
26178
26680
|
const rel = path38.relative(root, fullPath).split(path38.sep).join("/");
|
|
26179
|
-
out.push({ path: rel, bytes:
|
|
26681
|
+
out.push({ path: rel, bytes: stat19.size });
|
|
26180
26682
|
} catch {
|
|
26181
26683
|
}
|
|
26182
26684
|
}
|