@wrongstack/tools 0.296.4 → 0.298.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_spawn-stream.d.ts +2 -0
- package/dist/_util.d.ts +29 -1
- package/dist/audit.js +128 -65
- package/dist/bash.js +20 -4
- package/dist/batch-tool-use.js +70 -7
- package/dist/browser/index.js +31 -1
- package/dist/builtin.js +883 -351
- package/dist/codebase-index/index.js +95 -16
- package/dist/codebase-index/project-server-protocol.d.ts +26 -0
- package/dist/codebase-index/project-server.js +76 -16
- package/dist/codebase-index/worker.js +36 -13
- package/dist/codebase-index/writer-search-helpers.d.ts +16 -0
- package/dist/codebase-index/writer.d.ts +13 -1
- package/dist/design.d.ts +1 -1
- package/dist/design.js +4 -3
- package/dist/diff.js +11 -1
- package/dist/e2e.js +9 -6
- package/dist/edit.js +9 -6
- package/dist/exec.js +562 -516
- package/dist/format.js +131 -65
- package/dist/git.js +22 -2
- package/dist/glob.js +11 -5
- package/dist/grep.js +160 -104
- package/dist/index.js +834 -290
- package/dist/install.js +135 -68
- package/dist/json.js +64 -18
- package/dist/kanban-board-inputs.d.ts +21 -4
- package/dist/kanban.js +20 -5
- package/dist/languages/index.js +143 -71
- package/dist/lint.js +128 -65
- package/dist/next-steps.d.ts +23 -0
- package/dist/next-steps.js +4 -0
- package/dist/outdated.js +101 -26
- package/dist/pack.js +883 -351
- package/dist/patch.js +11 -1
- package/dist/plan.js +32 -10
- package/dist/process-registry.d.ts +9 -0
- package/dist/process-registry.js +15 -0
- package/dist/ps-slash.js +15 -0
- package/dist/read.js +104 -23
- package/dist/replace.js +11 -3
- package/dist/scaffold.js +9 -6
- package/dist/session-kanban.js +21 -5
- package/dist/task.js +3 -1
- package/dist/test.js +128 -65
- package/dist/todo.js +3 -1
- package/dist/tool-tier.js +883 -351
- package/dist/tool-use.d.ts +1 -1
- package/dist/tool-use.js +28 -4
- package/dist/tree.d.ts +1 -0
- package/dist/tree.js +44 -8
- package/dist/typecheck.js +128 -65
- package/dist/write.js +9 -6
- package/package.json +5 -4
- package/dist/_concurrency.d.ts.map +0 -1
- package/dist/_danger-detect.d.ts.map +0 -1
- package/dist/_edit-match.d.ts.map +0 -1
- package/dist/_env.d.ts.map +0 -1
- package/dist/_fetch-guard.d.ts.map +0 -1
- package/dist/_output-spool.d.ts.map +0 -1
- package/dist/_redact-command.d.ts.map +0 -1
- package/dist/_regex.d.ts.map +0 -1
- package/dist/_session-shell.d.ts.map +0 -1
- package/dist/_shell-pick.d.ts.map +0 -1
- package/dist/_spawn-stream.d.ts.map +0 -1
- package/dist/_syntax-check.d.ts.map +0 -1
- package/dist/_util.d.ts.map +0 -1
- package/dist/_win32-resolve.d.ts.map +0 -1
- package/dist/audit.d.ts.map +0 -1
- package/dist/audit.js.map +0 -7
- package/dist/auto-proceed-loop-guard.d.ts.map +0 -1
- package/dist/auto-proceed-loop-guard.js.map +0 -7
- package/dist/bash-kill-guard.d.ts.map +0 -1
- package/dist/bash.d.ts.map +0 -1
- package/dist/bash.js.map +0 -7
- package/dist/batch-tool-use.d.ts.map +0 -1
- package/dist/batch-tool-use.js.map +0 -7
- package/dist/browser/artifacts.d.ts.map +0 -1
- package/dist/browser/index.d.ts.map +0 -1
- package/dist/browser/index.js.map +0 -7
- package/dist/browser/manager.d.ts.map +0 -1
- package/dist/browser/network-guard-proxy.d.ts.map +0 -1
- package/dist/browser/security.d.ts.map +0 -1
- package/dist/browser/tools.d.ts.map +0 -1
- package/dist/browser/types.d.ts.map +0 -1
- package/dist/builtin.d.ts.map +0 -1
- package/dist/builtin.js.map +0 -7
- package/dist/circuit-breaker.d.ts.map +0 -1
- package/dist/circuit-breaker.js.map +0 -7
- package/dist/codebase-index/background-indexer.d.ts.map +0 -1
- package/dist/codebase-index/bm25.d.ts.map +0 -1
- package/dist/codebase-index/circuit-breaker.d.ts.map +0 -1
- package/dist/codebase-index/codebase-index-tool.d.ts.map +0 -1
- package/dist/codebase-index/codebase-search-tool.d.ts.map +0 -1
- package/dist/codebase-index/codebase-stats-tool.d.ts.map +0 -1
- package/dist/codebase-index/dead-code-scan.d.ts.map +0 -1
- package/dist/codebase-index/generic-parser.d.ts.map +0 -1
- package/dist/codebase-index/gitignore.d.ts.map +0 -1
- package/dist/codebase-index/go-parser.d.ts.map +0 -1
- package/dist/codebase-index/index-service.d.ts.map +0 -1
- package/dist/codebase-index/index.d.ts.map +0 -1
- package/dist/codebase-index/index.js.map +0 -7
- package/dist/codebase-index/indexer.d.ts.map +0 -1
- package/dist/codebase-index/json-parser.d.ts.map +0 -1
- package/dist/codebase-index/languages.d.ts.map +0 -1
- package/dist/codebase-index/lsp-kind.d.ts.map +0 -1
- package/dist/codebase-index/parser-dispatch.d.ts.map +0 -1
- package/dist/codebase-index/project-server-cache.d.ts.map +0 -1
- package/dist/codebase-index/project-server-client.d.ts.map +0 -1
- package/dist/codebase-index/project-server-endpoint.d.ts.map +0 -1
- package/dist/codebase-index/project-server-protocol.d.ts.map +0 -1
- package/dist/codebase-index/project-server.d.ts.map +0 -1
- package/dist/codebase-index/project-server.js.map +0 -7
- package/dist/codebase-index/py-parser.d.ts.map +0 -1
- package/dist/codebase-index/refs-extractor.d.ts.map +0 -1
- package/dist/codebase-index/rs-parser.d.ts.map +0 -1
- package/dist/codebase-index/schema.d.ts.map +0 -1
- package/dist/codebase-index/spawn-gate.d.ts.map +0 -1
- package/dist/codebase-index/sqlite-runtime.d.ts.map +0 -1
- package/dist/codebase-index/ts-parser.d.ts.map +0 -1
- package/dist/codebase-index/worker-protocol.d.ts.map +0 -1
- package/dist/codebase-index/worker.d.ts.map +0 -1
- package/dist/codebase-index/worker.js.map +0 -7
- package/dist/codebase-index/writer-admin.d.ts.map +0 -1
- package/dist/codebase-index/writer-bulk-insert.d.ts.map +0 -1
- package/dist/codebase-index/writer-graph-helpers.d.ts.map +0 -1
- package/dist/codebase-index/writer-graph-reader.d.ts.map +0 -1
- package/dist/codebase-index/writer-helpers.d.ts.map +0 -1
- package/dist/codebase-index/writer-pragmas.d.ts.map +0 -1
- package/dist/codebase-index/writer-ref-mapper.d.ts.map +0 -1
- package/dist/codebase-index/writer-schema.d.ts.map +0 -1
- package/dist/codebase-index/writer-search-helpers.d.ts.map +0 -1
- package/dist/codebase-index/writer-store-pool.d.ts.map +0 -1
- package/dist/codebase-index/writer.d.ts.map +0 -1
- package/dist/codebase-index/yaml-parser.d.ts.map +0 -1
- package/dist/design.d.ts.map +0 -1
- package/dist/design.js.map +0 -7
- package/dist/diff.d.ts.map +0 -1
- package/dist/diff.js.map +0 -7
- package/dist/document.d.ts.map +0 -1
- package/dist/document.js.map +0 -7
- package/dist/e2e.d.ts.map +0 -1
- package/dist/e2e.js.map +0 -7
- package/dist/edit.d.ts.map +0 -1
- package/dist/edit.js.map +0 -7
- package/dist/exec-kill-guard.d.ts.map +0 -1
- package/dist/exec.d.ts.map +0 -1
- package/dist/exec.js.map +0 -7
- package/dist/fetch.d.ts.map +0 -1
- package/dist/fetch.js.map +0 -7
- package/dist/format.d.ts.map +0 -1
- package/dist/format.js.map +0 -7
- package/dist/git.d.ts.map +0 -1
- package/dist/git.js.map +0 -7
- package/dist/glob.d.ts.map +0 -1
- package/dist/glob.js.map +0 -7
- package/dist/grep.d.ts.map +0 -1
- package/dist/grep.js.map +0 -7
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -7
- package/dist/install.d.ts.map +0 -1
- package/dist/install.js.map +0 -7
- package/dist/json.d.ts.map +0 -1
- package/dist/json.js.map +0 -7
- package/dist/kanban-board-inputs.d.ts.map +0 -1
- package/dist/kanban-decomposition-actions.d.ts.map +0 -1
- package/dist/kanban-detail-actions.d.ts.map +0 -1
- package/dist/kanban-evidence-bridge.d.ts.map +0 -1
- package/dist/kanban-presence.d.ts.map +0 -1
- package/dist/kanban-split-task-handler.d.ts.map +0 -1
- package/dist/kanban-task-inputs.d.ts.map +0 -1
- package/dist/kanban-tool-results.d.ts.map +0 -1
- package/dist/kanban-tool-schema.d.ts.map +0 -1
- package/dist/kanban-tool-types.d.ts.map +0 -1
- package/dist/kanban.d.ts.map +0 -1
- package/dist/kanban.js.map +0 -7
- package/dist/languages/detect.d.ts.map +0 -1
- package/dist/languages/diagnostics.d.ts.map +0 -1
- package/dist/languages/execute-tool.d.ts.map +0 -1
- package/dist/languages/execute.d.ts.map +0 -1
- package/dist/languages/index.d.ts.map +0 -1
- package/dist/languages/index.js.map +0 -7
- package/dist/languages/legacy-bridge.d.ts.map +0 -1
- package/dist/languages/package-tool.d.ts.map +0 -1
- package/dist/languages/plan.d.ts.map +0 -1
- package/dist/languages/profile-helpers.d.ts.map +0 -1
- package/dist/languages/profiles/additional.d.ts.map +0 -1
- package/dist/languages/profiles/primary.d.ts.map +0 -1
- package/dist/languages/registry.d.ts.map +0 -1
- package/dist/languages/tool.d.ts.map +0 -1
- package/dist/languages/types.d.ts.map +0 -1
- package/dist/lint.d.ts.map +0 -1
- package/dist/lint.js.map +0 -7
- package/dist/logs.d.ts.map +0 -1
- package/dist/logs.js.map +0 -7
- package/dist/memory.d.ts.map +0 -1
- package/dist/memory.js.map +0 -7
- package/dist/mode.d.ts.map +0 -1
- package/dist/mode.js.map +0 -7
- package/dist/next-steps.d.ts.map +0 -1
- package/dist/next-steps.js.map +0 -7
- package/dist/outdated.d.ts.map +0 -1
- package/dist/outdated.js.map +0 -7
- package/dist/pack.d.ts.map +0 -1
- package/dist/pack.js.map +0 -7
- package/dist/patch.d.ts.map +0 -1
- package/dist/patch.js.map +0 -7
- package/dist/plan.d.ts.map +0 -1
- package/dist/plan.js.map +0 -7
- package/dist/process-guardian.d.ts.map +0 -1
- package/dist/process-registry-persistent.d.ts.map +0 -1
- package/dist/process-registry.d.ts.map +0 -1
- package/dist/process-registry.js.map +0 -7
- package/dist/ps-slash.d.ts.map +0 -1
- package/dist/ps-slash.js.map +0 -7
- package/dist/read.d.ts.map +0 -1
- package/dist/read.js.map +0 -7
- package/dist/replace.d.ts.map +0 -1
- package/dist/replace.js.map +0 -7
- package/dist/scaffold.d.ts.map +0 -1
- package/dist/scaffold.js.map +0 -7
- package/dist/search.d.ts.map +0 -1
- package/dist/search.js.map +0 -7
- package/dist/session-kanban.d.ts.map +0 -1
- package/dist/session-kanban.js.map +0 -7
- package/dist/set-working-dir.d.ts.map +0 -1
- package/dist/skill.d.ts.map +0 -1
- package/dist/skill.js.map +0 -7
- package/dist/spawn-background.d.ts.map +0 -1
- package/dist/task.d.ts.map +0 -1
- package/dist/task.js.map +0 -7
- package/dist/test.d.ts.map +0 -1
- package/dist/test.js.map +0 -7
- package/dist/todo.d.ts.map +0 -1
- package/dist/todo.js.map +0 -7
- package/dist/tool-diff.d.ts.map +0 -1
- package/dist/tool-diff.js.map +0 -7
- package/dist/tool-help.d.ts.map +0 -1
- package/dist/tool-help.js.map +0 -7
- package/dist/tool-icon-map.d.ts.map +0 -1
- package/dist/tool-icons.d.ts.map +0 -1
- package/dist/tool-icons.js.map +0 -7
- package/dist/tool-search.d.ts.map +0 -1
- package/dist/tool-search.js.map +0 -7
- package/dist/tool-summary.d.ts.map +0 -1
- package/dist/tool-summary.js.map +0 -7
- package/dist/tool-tier.d.ts.map +0 -1
- package/dist/tool-tier.js.map +0 -7
- package/dist/tool-use.d.ts.map +0 -1
- package/dist/tool-use.js.map +0 -7
- package/dist/tree.d.ts.map +0 -1
- package/dist/tree.js.map +0 -7
- package/dist/typecheck.d.ts.map +0 -1
- package/dist/typecheck.js.map +0 -7
- package/dist/win32.d.ts.map +0 -1
- package/dist/win32.js.map +0 -7
- package/dist/write.d.ts.map +0 -1
- package/dist/write.js.map +0 -7
package/dist/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);
|
|
@@ -549,6 +550,7 @@ var init_process_registry = __esm({
|
|
|
549
550
|
}
|
|
550
551
|
/** Get all tracked processes. */
|
|
551
552
|
list() {
|
|
553
|
+
this._pruneAllStale();
|
|
552
554
|
return Array.from(this.processes.values());
|
|
553
555
|
}
|
|
554
556
|
/** Get processes filtered by name (e.g. 'bash', 'exec'). */
|
|
@@ -579,6 +581,7 @@ var init_process_registry = __esm({
|
|
|
579
581
|
* Combined stats for observability — used by /ps and the TUI status bar.
|
|
580
582
|
*/
|
|
581
583
|
stats() {
|
|
584
|
+
this._pruneAllStale();
|
|
582
585
|
return {
|
|
583
586
|
activeCount: this.activeCount,
|
|
584
587
|
backgroundCount: this.activeBackgroundCount,
|
|
@@ -818,6 +821,19 @@ var init_process_registry = __esm({
|
|
|
818
821
|
this.processes.delete(pid);
|
|
819
822
|
}
|
|
820
823
|
}
|
|
824
|
+
/**
|
|
825
|
+
* Remove every stale entry, not just one PID. `list()`/`stats()` — the
|
|
826
|
+
* surfaces the TUI status bar and `/ps` poll — must prune too: a child
|
|
827
|
+
* whose 'close' event never fires (e.g. Windows grandchildren holding stdio
|
|
828
|
+
* open) would otherwise linger in the registry until someone looks up its
|
|
829
|
+
* exact PID, and PID reuse meanwhile makes `get()`/`kill()` target the
|
|
830
|
+
* wrong process. RAM-leak audit 2026-07-31, MEDIUM.
|
|
831
|
+
*/
|
|
832
|
+
_pruneAllStale() {
|
|
833
|
+
for (const [pid, entry] of this.processes) {
|
|
834
|
+
if (this._isStaleEntry(entry)) this.processes.delete(pid);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
821
837
|
};
|
|
822
838
|
}
|
|
823
839
|
});
|
|
@@ -834,8 +850,11 @@ async function* spawnStream(opts) {
|
|
|
834
850
|
const max = opts.maxBytes ?? 2e5;
|
|
835
851
|
const flushAt = opts.flushBytes ?? 4 * 1024;
|
|
836
852
|
const maxQueue = opts.maxQueueSize ?? 500;
|
|
853
|
+
const maxQueueBytes = opts.maxQueueBytes ?? 1024 * 1024;
|
|
837
854
|
let stdout = "";
|
|
838
855
|
let stderr = "";
|
|
856
|
+
let stdoutRetainedBytes = 0;
|
|
857
|
+
let stderrRetainedBytes = 0;
|
|
839
858
|
let pending2 = "";
|
|
840
859
|
let error;
|
|
841
860
|
const spool = createOutputSpool({ tool: opts.cmd, thresholdBytes: max });
|
|
@@ -877,6 +896,7 @@ async function* spawnStream(opts) {
|
|
|
877
896
|
});
|
|
878
897
|
}
|
|
879
898
|
const queue = [];
|
|
899
|
+
let queuedBytes = 0;
|
|
880
900
|
let waiter;
|
|
881
901
|
let paused = false;
|
|
882
902
|
const wake = () => {
|
|
@@ -887,7 +907,7 @@ async function* spawnStream(opts) {
|
|
|
887
907
|
}
|
|
888
908
|
};
|
|
889
909
|
const resume = () => {
|
|
890
|
-
if (paused && queue.length
|
|
910
|
+
if (paused && queue.length <= Math.floor(maxQueue / 2) && queuedBytes <= Math.floor(maxQueueBytes / 2)) {
|
|
891
911
|
paused = false;
|
|
892
912
|
child.stdout?.resume();
|
|
893
913
|
child.stderr?.resume();
|
|
@@ -897,11 +917,16 @@ async function* spawnStream(opts) {
|
|
|
897
917
|
const s = c.toString();
|
|
898
918
|
stdoutBytes += c.byteLength;
|
|
899
919
|
emitProcessOutput({ pid, stream: "stdout", chunk: c });
|
|
900
|
-
if (
|
|
920
|
+
if (stdoutRetainedBytes < max) {
|
|
921
|
+
const retained = utf8Prefix(s, max - stdoutRetainedBytes);
|
|
922
|
+
stdout += retained;
|
|
923
|
+
stdoutRetainedBytes += Buffer.byteLength(retained, "utf8");
|
|
924
|
+
}
|
|
901
925
|
spool.write(s);
|
|
902
|
-
queue.push({ kind: "out", data: s });
|
|
926
|
+
queue.push({ kind: "out", data: s, bytes: c.byteLength });
|
|
927
|
+
queuedBytes += c.byteLength;
|
|
903
928
|
wake();
|
|
904
|
-
if (!paused && queue.length >= maxQueue) {
|
|
929
|
+
if (!paused && (queue.length >= maxQueue || queuedBytes >= maxQueueBytes)) {
|
|
905
930
|
paused = true;
|
|
906
931
|
child.stdout?.pause();
|
|
907
932
|
child.stderr?.pause();
|
|
@@ -911,11 +936,16 @@ async function* spawnStream(opts) {
|
|
|
911
936
|
const s = c.toString();
|
|
912
937
|
stderrBytes += c.byteLength;
|
|
913
938
|
emitProcessOutput({ pid, stream: "stderr", chunk: c });
|
|
914
|
-
if (
|
|
939
|
+
if (stderrRetainedBytes < max) {
|
|
940
|
+
const retained = utf8Prefix(s, max - stderrRetainedBytes);
|
|
941
|
+
stderr += retained;
|
|
942
|
+
stderrRetainedBytes += Buffer.byteLength(retained, "utf8");
|
|
943
|
+
}
|
|
915
944
|
spool.write(s);
|
|
916
|
-
queue.push({ kind: "err", data: s });
|
|
945
|
+
queue.push({ kind: "err", data: s, bytes: c.byteLength });
|
|
946
|
+
queuedBytes += c.byteLength;
|
|
917
947
|
wake();
|
|
918
|
-
if (!paused && queue.length >= maxQueue) {
|
|
948
|
+
if (!paused && (queue.length >= maxQueue || queuedBytes >= maxQueueBytes)) {
|
|
919
949
|
paused = true;
|
|
920
950
|
child.stdout?.pause();
|
|
921
951
|
child.stderr?.pause();
|
|
@@ -925,7 +955,9 @@ async function* spawnStream(opts) {
|
|
|
925
955
|
child.stderr?.on("data", onErr);
|
|
926
956
|
child.on("error", (e) => {
|
|
927
957
|
error = e.message;
|
|
928
|
-
|
|
958
|
+
const bytes = Buffer.byteLength(e.message, "utf8");
|
|
959
|
+
queue.push({ kind: "error", data: e.message, bytes });
|
|
960
|
+
queuedBytes += bytes;
|
|
929
961
|
wake();
|
|
930
962
|
});
|
|
931
963
|
const completeTelemetry = (code, signal, timedOut = false) => {
|
|
@@ -946,7 +978,13 @@ async function* spawnStream(opts) {
|
|
|
946
978
|
if (typeof pid === "number") registry.unregister(pid);
|
|
947
979
|
const exitCode2 = code ?? (signal ? 1 : 0);
|
|
948
980
|
completeTelemetry(exitCode2, signal ?? void 0);
|
|
949
|
-
queue.push({
|
|
981
|
+
queue.push({
|
|
982
|
+
kind: "close",
|
|
983
|
+
data: "",
|
|
984
|
+
bytes: 0,
|
|
985
|
+
code: exitCode2,
|
|
986
|
+
...signal ? { signal } : {}
|
|
987
|
+
});
|
|
950
988
|
wake();
|
|
951
989
|
});
|
|
952
990
|
const onAbort = () => {
|
|
@@ -958,7 +996,7 @@ async function* spawnStream(opts) {
|
|
|
958
996
|
} catch {
|
|
959
997
|
}
|
|
960
998
|
}
|
|
961
|
-
queue.push({ kind: "close", data: "", code: 124 });
|
|
999
|
+
queue.push({ kind: "close", data: "", bytes: 0, code: 124 });
|
|
962
1000
|
completeTelemetry(124, "SIGKILL", true);
|
|
963
1001
|
wake();
|
|
964
1002
|
};
|
|
@@ -976,6 +1014,7 @@ async function* spawnStream(opts) {
|
|
|
976
1014
|
});
|
|
977
1015
|
}
|
|
978
1016
|
const chunk = queue.shift();
|
|
1017
|
+
queuedBytes -= chunk.bytes;
|
|
979
1018
|
resume();
|
|
980
1019
|
if (chunk.kind === "close") {
|
|
981
1020
|
if (!spawnFailed) exitCode = chunk.code ?? 0;
|
|
@@ -1002,7 +1041,7 @@ async function* spawnStream(opts) {
|
|
|
1002
1041
|
stdout: spooled ? stdout + spoolNote(spooled) : stdout,
|
|
1003
1042
|
stderr,
|
|
1004
1043
|
exitCode,
|
|
1005
|
-
truncated:
|
|
1044
|
+
truncated: stdoutBytes > stdoutRetainedBytes || stderrBytes > stderrRetainedBytes,
|
|
1006
1045
|
error,
|
|
1007
1046
|
spoolPath: spooled?.path,
|
|
1008
1047
|
spoolBytes: spooled?.bytes
|
|
@@ -1026,13 +1065,27 @@ async function* spawnStream(opts) {
|
|
|
1026
1065
|
}
|
|
1027
1066
|
}
|
|
1028
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
|
+
}
|
|
1029
1082
|
var isWin;
|
|
1030
1083
|
var init_spawn_stream = __esm({
|
|
1031
1084
|
"src/_spawn-stream.ts"() {
|
|
1032
1085
|
"use strict";
|
|
1033
1086
|
init_output_spool();
|
|
1034
|
-
init_process_registry();
|
|
1035
1087
|
init_win32_resolve();
|
|
1088
|
+
init_process_registry();
|
|
1036
1089
|
isWin = process.platform === "win32";
|
|
1037
1090
|
}
|
|
1038
1091
|
});
|
|
@@ -1046,14 +1099,14 @@ function sha256hex(content) {
|
|
|
1046
1099
|
return createHash("sha256").update(content, "utf8").digest("hex");
|
|
1047
1100
|
}
|
|
1048
1101
|
async function detectPackageManager(cwd) {
|
|
1049
|
-
const { stat:
|
|
1102
|
+
const { stat: stat19 } = await import("node:fs/promises");
|
|
1050
1103
|
try {
|
|
1051
|
-
await
|
|
1104
|
+
await stat19(`${cwd}/pnpm-lock.yaml`);
|
|
1052
1105
|
return "pnpm";
|
|
1053
1106
|
} catch {
|
|
1054
1107
|
}
|
|
1055
1108
|
try {
|
|
1056
|
-
await
|
|
1109
|
+
await stat19(`${cwd}/yarn.lock`);
|
|
1057
1110
|
return "yarn";
|
|
1058
1111
|
} catch {
|
|
1059
1112
|
}
|
|
@@ -1081,11 +1134,15 @@ function safeResolve(input, ctx) {
|
|
|
1081
1134
|
return ensureInsideRoot(resolvePath(input, ctx), ctx);
|
|
1082
1135
|
}
|
|
1083
1136
|
async function assertRealInsideRoot(absPath, ctx) {
|
|
1084
|
-
|
|
1137
|
+
await resolveRealInsideRoot(absPath, ctx);
|
|
1138
|
+
}
|
|
1139
|
+
async function resolveRealInsideRoot(absPath, ctx) {
|
|
1140
|
+
if (ctx.allowOutsideProjectRoot) return absPath;
|
|
1085
1141
|
const realRoots = await Promise.all(
|
|
1086
1142
|
allowedRoots(ctx).map((r) => fsp2.realpath(r).catch(() => path3.resolve(r)))
|
|
1087
1143
|
);
|
|
1088
1144
|
let probe2 = absPath;
|
|
1145
|
+
const pendingTail = [];
|
|
1089
1146
|
for (; ; ) {
|
|
1090
1147
|
let real;
|
|
1091
1148
|
try {
|
|
@@ -1093,13 +1150,16 @@ async function assertRealInsideRoot(absPath, ctx) {
|
|
|
1093
1150
|
} catch (err) {
|
|
1094
1151
|
if (err.code === "ENOENT") {
|
|
1095
1152
|
const parent = path3.dirname(probe2);
|
|
1096
|
-
if (parent === probe2) return;
|
|
1153
|
+
if (parent === probe2) return absPath;
|
|
1154
|
+
pendingTail.unshift(path3.basename(probe2));
|
|
1097
1155
|
probe2 = parent;
|
|
1098
1156
|
continue;
|
|
1099
1157
|
}
|
|
1100
1158
|
throw err;
|
|
1101
1159
|
}
|
|
1102
|
-
if (isInsideAny(real, realRoots))
|
|
1160
|
+
if (isInsideAny(real, realRoots)) {
|
|
1161
|
+
return pendingTail.length > 0 ? path3.join(real, ...pendingTail) : real;
|
|
1162
|
+
}
|
|
1103
1163
|
throw new Error(
|
|
1104
1164
|
`Path "${absPath}" resolves through a symlink outside project root "${realRoots[0]}"`
|
|
1105
1165
|
);
|
|
@@ -1107,8 +1167,7 @@ async function assertRealInsideRoot(absPath, ctx) {
|
|
|
1107
1167
|
}
|
|
1108
1168
|
async function safeResolveReal(input, ctx) {
|
|
1109
1169
|
const abs = safeResolve(input, ctx);
|
|
1110
|
-
await
|
|
1111
|
-
return abs;
|
|
1170
|
+
return await resolveRealInsideRoot(abs, ctx);
|
|
1112
1171
|
}
|
|
1113
1172
|
function truncateMiddle(s, max) {
|
|
1114
1173
|
if (Buffer.byteLength(s, "utf8") <= max) return s;
|
|
@@ -2672,8 +2731,8 @@ async function scanDirectory(directory, depth, profiles, limits, state, extraIgn
|
|
|
2672
2731
|
collectFileEvidence(directory, fullPath, entry.name, profiles, state);
|
|
2673
2732
|
}
|
|
2674
2733
|
}
|
|
2675
|
-
function collectFileEvidence(directory, fullPath,
|
|
2676
|
-
const lower =
|
|
2734
|
+
function collectFileEvidence(directory, fullPath, basename14, profiles, state) {
|
|
2735
|
+
const lower = basename14.toLowerCase();
|
|
2677
2736
|
const extension = path4.extname(lower);
|
|
2678
2737
|
for (const profile of profiles) {
|
|
2679
2738
|
const detector = profile.detectors.find(
|
|
@@ -2684,7 +2743,7 @@ function collectFileEvidence(directory, fullPath, basename13, profiles, state) {
|
|
|
2684
2743
|
candidate.evidence.push({
|
|
2685
2744
|
kind: detector.kind,
|
|
2686
2745
|
path: fullPath,
|
|
2687
|
-
value:
|
|
2746
|
+
value: basename14,
|
|
2688
2747
|
weight: detector.weight
|
|
2689
2748
|
});
|
|
2690
2749
|
if (detector.kind === "manifest" || detector.kind === "config") {
|
|
@@ -2812,8 +2871,8 @@ function normalizeLimits(input) {
|
|
|
2812
2871
|
async function canonicalDirectory(input) {
|
|
2813
2872
|
const resolved = path4.resolve(input);
|
|
2814
2873
|
const real = await fs2.realpath(resolved);
|
|
2815
|
-
const
|
|
2816
|
-
if (!
|
|
2874
|
+
const stat19 = await fs2.stat(real);
|
|
2875
|
+
if (!stat19.isDirectory()) throw new Error(`Project root is not a directory: ${input}`);
|
|
2817
2876
|
return real;
|
|
2818
2877
|
}
|
|
2819
2878
|
async function canonicalInside(input, root, label) {
|
|
@@ -3369,6 +3428,7 @@ import * as fs3 from "node:fs/promises";
|
|
|
3369
3428
|
import * as path6 from "node:path";
|
|
3370
3429
|
async function planLanguageOperation(options) {
|
|
3371
3430
|
validateOperationOptions(options.operation, options.operationOptions?.packages);
|
|
3431
|
+
validateFilterOption(options.operationOptions?.filter);
|
|
3372
3432
|
const detection = await detectLanguageWorkspaces(options);
|
|
3373
3433
|
const candidates = detection.workspaces.filter(
|
|
3374
3434
|
(workspace) => workspace.capabilities.includes(options.operation)
|
|
@@ -3541,6 +3601,15 @@ function workspaceDepth(workspaceRoot, projectRoot) {
|
|
|
3541
3601
|
const relative13 = path6.relative(path6.resolve(projectRoot), path6.resolve(workspaceRoot));
|
|
3542
3602
|
return relative13 === "" ? 0 : relative13.split(path6.sep).length;
|
|
3543
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
|
+
}
|
|
3544
3613
|
function validateOperationOptions(operation, packages) {
|
|
3545
3614
|
if (!operation.startsWith("package-") || !packages) return;
|
|
3546
3615
|
for (const value of packages) {
|
|
@@ -3582,7 +3651,7 @@ var init_plan = __esm({
|
|
|
3582
3651
|
MAX_ARGUMENT_LENGTH = 4096;
|
|
3583
3652
|
PACKAGE_NAME_RE = /^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+(?:@[a-z0-9*+._~^<>=|-]+)?$/i;
|
|
3584
3653
|
COMPOSER_PACKAGE_RE = /^[a-z0-9_.-]+\/[a-z0-9_.-]+(?::[a-z0-9*+._~^<>=|-]+)?$/i;
|
|
3585
|
-
GO_MODULE_RE = /^(?:[a-z0-9
|
|
3654
|
+
GO_MODULE_RE = /^(?:[a-z0-9-]+\.)+[a-z0-9-]+\/[a-z0-9._~+/@-]+$/i;
|
|
3586
3655
|
PYTHON_PACKAGE_RE = /^[a-z0-9._-]+(?:\[[a-z0-9._,-]+\])?==[a-z0-9*+._!~-]+$/i;
|
|
3587
3656
|
}
|
|
3588
3657
|
});
|
|
@@ -3702,8 +3771,8 @@ async function executeInternal(options, startedAt) {
|
|
|
3702
3771
|
const target = options.plan.evidence.find((item) => item.kind === "target")?.path;
|
|
3703
3772
|
if (!target) return unavailableResult(options, "Internal syntax plan has no target evidence.");
|
|
3704
3773
|
const safeTarget = await assertContainedFile(target, options.projectRoot);
|
|
3705
|
-
const
|
|
3706
|
-
if (
|
|
3774
|
+
const stat19 = await fs4.stat(safeTarget);
|
|
3775
|
+
if (stat19.size > MAX_INTERNAL_SOURCE_BYTES) {
|
|
3707
3776
|
return unavailableResult(
|
|
3708
3777
|
options,
|
|
3709
3778
|
`Internal syntax target exceeds ${MAX_INTERNAL_SOURCE_BYTES} bytes.`
|
|
@@ -3990,8 +4059,8 @@ async function snapshotPaths(paths) {
|
|
|
3990
4059
|
const existing = [];
|
|
3991
4060
|
for (const candidate of paths) {
|
|
3992
4061
|
try {
|
|
3993
|
-
const
|
|
3994
|
-
if (!
|
|
4062
|
+
const stat19 = await fs4.stat(candidate);
|
|
4063
|
+
if (!stat19.isFile()) continue;
|
|
3995
4064
|
existing.push(candidate);
|
|
3996
4065
|
} catch {
|
|
3997
4066
|
}
|
|
@@ -4019,8 +4088,8 @@ async function snapshotSizes(paths) {
|
|
|
4019
4088
|
const sizes = /* @__PURE__ */ new Map();
|
|
4020
4089
|
for (const candidate of paths) {
|
|
4021
4090
|
try {
|
|
4022
|
-
const
|
|
4023
|
-
if (
|
|
4091
|
+
const stat19 = await fs4.stat(candidate);
|
|
4092
|
+
if (stat19.isFile()) sizes.set(candidate, stat19.size);
|
|
4024
4093
|
} catch {
|
|
4025
4094
|
}
|
|
4026
4095
|
}
|
|
@@ -4180,6 +4249,9 @@ var init_execute_tool = __esm({
|
|
|
4180
4249
|
useInstead: ["language_info", "install", "audit", "outdated"]
|
|
4181
4250
|
},
|
|
4182
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",
|
|
4183
4255
|
mutating: true,
|
|
4184
4256
|
riskTier: "standard",
|
|
4185
4257
|
capabilities: ["shell.restricted", "fs.write"],
|
|
@@ -4412,6 +4484,9 @@ var init_package_tool = __esm({
|
|
|
4412
4484
|
useInstead: ["language_info", "language", "install", "audit", "outdated"]
|
|
4413
4485
|
},
|
|
4414
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",
|
|
4415
4490
|
mutating: true,
|
|
4416
4491
|
riskTier: "destructive",
|
|
4417
4492
|
capabilities: ["shell.restricted", "fs.write", "net.outbound", "package.install"],
|
|
@@ -6466,9 +6541,9 @@ function parseSymbols6(opts) {
|
|
|
6466
6541
|
function regexParse2(opts) {
|
|
6467
6542
|
const { file, content, lang } = opts;
|
|
6468
6543
|
const symbols = [];
|
|
6469
|
-
const
|
|
6470
|
-
const isPackageJson =
|
|
6471
|
-
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";
|
|
6472
6547
|
const isJsonSchema = content.includes("$schema") || content.includes("$id") || content.includes("$ref");
|
|
6473
6548
|
const isOpenApi = content.includes("openapi") || content.includes("swagger");
|
|
6474
6549
|
const lines = content.split("\n");
|
|
@@ -8680,30 +8755,59 @@ ${hint}` : ""),
|
|
|
8680
8755
|
import {
|
|
8681
8756
|
GOVERNED_TOOL_EXECUTOR_META_KEY
|
|
8682
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;
|
|
8683
8774
|
var batchToolUseTool = {
|
|
8684
8775
|
name: "batch_tool_use",
|
|
8685
8776
|
category: "Meta",
|
|
8686
|
-
description: "Execute a batch of
|
|
8687
|
-
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(", ")}.`,
|
|
8688
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.
|
|
8689
8788
|
mutating: true,
|
|
8690
8789
|
timeoutMs: 12e4,
|
|
8691
|
-
capabilities: ["tool.
|
|
8790
|
+
capabilities: ["tool.meta"],
|
|
8692
8791
|
icon: "meta",
|
|
8693
8792
|
inputSchema: {
|
|
8694
8793
|
type: "object",
|
|
8695
8794
|
properties: {
|
|
8696
8795
|
calls: {
|
|
8697
8796
|
type: "array",
|
|
8797
|
+
maxItems: MAX_BATCH_CALLS,
|
|
8698
8798
|
items: {
|
|
8699
8799
|
type: "object",
|
|
8700
8800
|
properties: {
|
|
8701
|
-
tool: {
|
|
8801
|
+
tool: {
|
|
8802
|
+
type: "string",
|
|
8803
|
+
enum: [...BATCHABLE_TOOL_NAMES],
|
|
8804
|
+
description: "An explicitly batch-safe, read-only discovery tool."
|
|
8805
|
+
},
|
|
8702
8806
|
input: { type: "object" }
|
|
8703
8807
|
},
|
|
8704
8808
|
required: ["tool"]
|
|
8705
8809
|
},
|
|
8706
|
-
description:
|
|
8810
|
+
description: `Up to ${MAX_BATCH_CALLS} independent, batch-safe read calls.`
|
|
8707
8811
|
},
|
|
8708
8812
|
stop_on_error: {
|
|
8709
8813
|
type: "boolean",
|
|
@@ -8726,6 +8830,9 @@ var batchToolUseTool = {
|
|
|
8726
8830
|
stop_on_error: false
|
|
8727
8831
|
};
|
|
8728
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
|
+
}
|
|
8729
8836
|
const governedExecute = ctx.meta[GOVERNED_TOOL_EXECUTOR_META_KEY];
|
|
8730
8837
|
if (typeof governedExecute !== "function") {
|
|
8731
8838
|
return {
|
|
@@ -8745,8 +8852,11 @@ var batchToolUseTool = {
|
|
|
8745
8852
|
let succeeded = 0;
|
|
8746
8853
|
let failed = 0;
|
|
8747
8854
|
if (input.parallel !== false) {
|
|
8748
|
-
const
|
|
8749
|
-
|
|
8855
|
+
const allResults = await mapWithConcurrency(
|
|
8856
|
+
input.calls,
|
|
8857
|
+
MAX_PARALLEL_BATCH_CALLS,
|
|
8858
|
+
(call) => executeSingle(call, ctx, governedExecute)
|
|
8859
|
+
);
|
|
8750
8860
|
results.push(...allResults);
|
|
8751
8861
|
succeeded = allResults.filter((r) => r.success).length;
|
|
8752
8862
|
failed = allResults.filter((r) => !r.success).length;
|
|
@@ -8771,8 +8881,28 @@ var batchToolUseTool = {
|
|
|
8771
8881
|
};
|
|
8772
8882
|
}
|
|
8773
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
|
+
}
|
|
8774
8896
|
async function executeSingle(call, ctx, governedExecute) {
|
|
8775
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
|
+
}
|
|
8776
8906
|
const tool = ctx.tools.find((candidate) => candidate.name === call.tool);
|
|
8777
8907
|
if (!tool) {
|
|
8778
8908
|
return {
|
|
@@ -8782,6 +8912,14 @@ async function executeSingle(call, ctx, governedExecute) {
|
|
|
8782
8912
|
executionMs: Date.now() - start
|
|
8783
8913
|
};
|
|
8784
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
|
+
}
|
|
8785
8923
|
try {
|
|
8786
8924
|
const result = await governedExecute(call.tool, call.input);
|
|
8787
8925
|
return {
|
|
@@ -8808,8 +8946,14 @@ import * as path10 from "node:path";
|
|
|
8808
8946
|
import { atomicWrite, ulid } from "@wrongstack/core/utils";
|
|
8809
8947
|
var ARTIFACT_RETENTION_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
8810
8948
|
var sweptRoots = /* @__PURE__ */ new Set();
|
|
8949
|
+
var MAX_SWEPT_ROOTS = 128;
|
|
8811
8950
|
function sweepOldArtifacts(root) {
|
|
8812
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
|
+
}
|
|
8813
8957
|
sweptRoots.add(root);
|
|
8814
8958
|
void (async () => {
|
|
8815
8959
|
const cutoff = Date.now() - ARTIFACT_RETENTION_MS;
|
|
@@ -8832,8 +8976,8 @@ function sweepOldArtifacts(root) {
|
|
|
8832
8976
|
for (const name of names) {
|
|
8833
8977
|
const target = path10.join(dir, name);
|
|
8834
8978
|
try {
|
|
8835
|
-
const
|
|
8836
|
-
if (
|
|
8979
|
+
const stat19 = await fs7.stat(target);
|
|
8980
|
+
if (stat19.isFile() && stat19.mtimeMs < cutoff) {
|
|
8837
8981
|
await fs7.rm(target, { force: true });
|
|
8838
8982
|
removed++;
|
|
8839
8983
|
}
|
|
@@ -8861,14 +9005,14 @@ var BrowserArtifactStore = class {
|
|
|
8861
9005
|
}
|
|
8862
9006
|
async record(id, sessionId, kind, target, mimeType) {
|
|
8863
9007
|
await fs7.chmod(target, 384).catch(() => void 0);
|
|
8864
|
-
const [
|
|
9008
|
+
const [stat19, sha256] = await Promise.all([fs7.stat(target), hashFile(target)]);
|
|
8865
9009
|
const artifact = {
|
|
8866
9010
|
id,
|
|
8867
9011
|
kind,
|
|
8868
9012
|
sensitivity: "sensitive",
|
|
8869
9013
|
path: target,
|
|
8870
9014
|
mimeType,
|
|
8871
|
-
sizeBytes:
|
|
9015
|
+
sizeBytes: stat19.size,
|
|
8872
9016
|
sha256,
|
|
8873
9017
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
8874
9018
|
};
|
|
@@ -9243,7 +9387,7 @@ var BrowserSessionManager = class {
|
|
|
9243
9387
|
constructor(options, launcher = defaultLauncher) {
|
|
9244
9388
|
this.launcher = launcher;
|
|
9245
9389
|
this.artifacts = new BrowserArtifactStore(options.artifactRoot);
|
|
9246
|
-
this.allowPrivateHosts = options.allowPrivateHosts ??
|
|
9390
|
+
this.allowPrivateHosts = options.allowPrivateHosts ?? false;
|
|
9247
9391
|
this.allowedPrivateOrigins = options.allowedPrivateOrigins ?? [];
|
|
9248
9392
|
this.networkProxy = new BrowserNetworkGuardProxy({
|
|
9249
9393
|
allowPrivateHosts: this.allowPrivateHosts,
|
|
@@ -9466,8 +9610,8 @@ var BrowserSessionManager = class {
|
|
|
9466
9610
|
if (realRelative.startsWith("..") || path11.isAbsolute(realRelative)) {
|
|
9467
9611
|
throw new Error("browser: upload files must not escape the project root through a symlink");
|
|
9468
9612
|
}
|
|
9469
|
-
const
|
|
9470
|
-
if (!
|
|
9613
|
+
const stat19 = await fs8.stat(realFile);
|
|
9614
|
+
if (!stat19.isFile()) throw new Error(`browser: upload target is not a file: ${file}`);
|
|
9471
9615
|
resolved.push(realFile);
|
|
9472
9616
|
}
|
|
9473
9617
|
await this.runPageOperation(
|
|
@@ -9819,6 +9963,9 @@ var browserScreenshotTool = {
|
|
|
9819
9963
|
description: "Capture a page or element PNG and return sensitive artifact metadata with integrity hash.",
|
|
9820
9964
|
usageHint: "browser_screenshot({ sessionId, fullPage?, selector? })",
|
|
9821
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",
|
|
9822
9969
|
mutating: true,
|
|
9823
9970
|
riskTier: "safe",
|
|
9824
9971
|
capabilities: [ToolCapabilities.NET_OUTBOUND],
|
|
@@ -9848,6 +9995,9 @@ function selectorTool(name, description, run) {
|
|
|
9848
9995
|
description,
|
|
9849
9996
|
usageHint: `${name}({ sessionId, selector })`,
|
|
9850
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",
|
|
9851
10001
|
mutating: true,
|
|
9852
10002
|
riskTier: "standard",
|
|
9853
10003
|
capabilities: [ToolCapabilities.NET_OUTBOUND],
|
|
@@ -9877,6 +10027,9 @@ var browserTypeTool = {
|
|
|
9877
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.",
|
|
9878
10028
|
usageHint: "browser_type({ sessionId, selector, text? | secretEnv? })",
|
|
9879
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",
|
|
9880
10033
|
mutating: true,
|
|
9881
10034
|
riskTier: "standard",
|
|
9882
10035
|
capabilities: [ToolCapabilities.NET_OUTBOUND],
|
|
@@ -9911,6 +10064,9 @@ var browserSelectTool = {
|
|
|
9911
10064
|
description: "Select an option in an owned browser session.",
|
|
9912
10065
|
usageHint: "browser_select({ sessionId, selector, value })",
|
|
9913
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",
|
|
9914
10070
|
mutating: true,
|
|
9915
10071
|
riskTier: "standard",
|
|
9916
10072
|
capabilities: [ToolCapabilities.NET_OUTBOUND],
|
|
@@ -9939,6 +10095,9 @@ var browserPressTool = {
|
|
|
9939
10095
|
description: "Press a keyboard key in an owned browser session.",
|
|
9940
10096
|
usageHint: "browser_press({ sessionId, key })",
|
|
9941
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",
|
|
9942
10101
|
mutating: true,
|
|
9943
10102
|
riskTier: "standard",
|
|
9944
10103
|
capabilities: [ToolCapabilities.NET_OUTBOUND],
|
|
@@ -9957,6 +10116,9 @@ var browserDragTool = {
|
|
|
9957
10116
|
description: "Drag one element to another in an owned browser session.",
|
|
9958
10117
|
usageHint: "browser_drag({ sessionId, from, to })",
|
|
9959
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",
|
|
9960
10122
|
mutating: true,
|
|
9961
10123
|
riskTier: "standard",
|
|
9962
10124
|
capabilities: [ToolCapabilities.NET_OUTBOUND],
|
|
@@ -10006,6 +10168,9 @@ var browserEvaluateTool = {
|
|
|
10006
10168
|
description: "Evaluate bounded JavaScript in the page. Requires confirmation because page code is arbitrary.",
|
|
10007
10169
|
usageHint: "browser_evaluate({ sessionId, expression })",
|
|
10008
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",
|
|
10009
10174
|
mutating: true,
|
|
10010
10175
|
riskTier: "destructive",
|
|
10011
10176
|
capabilities: [ToolCapabilities.NET_OUTBOUND, ToolCapabilities.TOOL_MUTATE_ANY],
|
|
@@ -10028,6 +10193,9 @@ var browserUploadTool = {
|
|
|
10028
10193
|
description: "Upload project-local files through a file input in an owned browser session.",
|
|
10029
10194
|
usageHint: "browser_upload({ sessionId, selector, files })",
|
|
10030
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",
|
|
10031
10199
|
mutating: true,
|
|
10032
10200
|
riskTier: "destructive",
|
|
10033
10201
|
capabilities: [ToolCapabilities.NET_OUTBOUND, ToolCapabilities.FS_READ],
|
|
@@ -10971,6 +11139,7 @@ var REFS_INDEX_SQL = [
|
|
|
10971
11139
|
var SYMBOLS_FTS_SQL = "CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5(text, tokenize = 'unicode61')";
|
|
10972
11140
|
|
|
10973
11141
|
// src/codebase-index/writer-search-helpers.ts
|
|
11142
|
+
var SEARCH_CANDIDATE_SCAN_CAP = 5e3;
|
|
10974
11143
|
function normalizeSearchLimit(limit) {
|
|
10975
11144
|
return typeof limit === "number" && Number.isFinite(limit) ? Math.max(0, Math.trunc(limit)) : void 0;
|
|
10976
11145
|
}
|
|
@@ -11000,8 +11169,8 @@ function buildWriterSearchWhere(query, filter) {
|
|
|
11000
11169
|
}
|
|
11001
11170
|
if (query.trim()) {
|
|
11002
11171
|
const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
11003
|
-
conditions.push(`(${tokens.map(() => "text LIKE ?").join(" OR ")})`);
|
|
11004
|
-
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)}%`);
|
|
11005
11174
|
}
|
|
11006
11175
|
return { where: conditions.length ? `WHERE ${conditions.join(" AND ")}` : "", values };
|
|
11007
11176
|
}
|
|
@@ -11110,6 +11279,7 @@ var StorePool = class {
|
|
|
11110
11279
|
|
|
11111
11280
|
// src/codebase-index/writer.ts
|
|
11112
11281
|
var DB_FILE2 = "index.db";
|
|
11282
|
+
var MAX_STATEMENT_CACHE = 128;
|
|
11113
11283
|
var IndexStore = class _IndexStore {
|
|
11114
11284
|
db;
|
|
11115
11285
|
/** Absolute path to this project's index directory. */
|
|
@@ -11147,12 +11317,31 @@ var IndexStore = class _IndexStore {
|
|
|
11147
11317
|
// Dirty on open so the first getOrBuildBm25() rebuilds against current rows;
|
|
11148
11318
|
// an empty or pre-existing corpus makes a stale IDF table meaningless.
|
|
11149
11319
|
bm25Dirty = true;
|
|
11150
|
-
/**
|
|
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
|
+
*/
|
|
11151
11333
|
stmt(sql) {
|
|
11152
|
-
|
|
11153
|
-
if (
|
|
11154
|
-
|
|
11155
|
-
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);
|
|
11156
11345
|
}
|
|
11157
11346
|
return s;
|
|
11158
11347
|
}
|
|
@@ -11425,7 +11614,7 @@ var IndexStore = class _IndexStore {
|
|
|
11425
11614
|
const { where, values } = built;
|
|
11426
11615
|
const limit = normalizeSearchLimit(opts?.limit);
|
|
11427
11616
|
const limitSql = limit !== void 0 ? " LIMIT ?" : "";
|
|
11428
|
-
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}`;
|
|
11429
11618
|
const binds = limit !== void 0 ? [...values, limit] : values;
|
|
11430
11619
|
const rows = this.stmt(sql).all(
|
|
11431
11620
|
...binds
|
|
@@ -11554,11 +11743,13 @@ var IndexStore = class _IndexStore {
|
|
|
11554
11743
|
/** Legacy ranked path: LIKE candidates + in-process BM25 + JS snippets. */
|
|
11555
11744
|
searchRankedFallback(query, filter, limit) {
|
|
11556
11745
|
if (!query.trim()) {
|
|
11557
|
-
const
|
|
11558
|
-
if (
|
|
11559
|
-
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 };
|
|
11560
11749
|
}
|
|
11561
|
-
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 });
|
|
11562
11753
|
if (candidates.length === 0) return { results: [], total: 0 };
|
|
11563
11754
|
const candidateById = new Map(candidates.map((c) => [c.id, c]));
|
|
11564
11755
|
const bm25 = this.getOrBuildBm25();
|
|
@@ -11584,7 +11775,7 @@ var IndexStore = class _IndexStore {
|
|
|
11584
11775
|
const c = expectDefined2(candidateById.get(id));
|
|
11585
11776
|
return { ...c, score, snippet: bm25.extractSnippet(id, qTokens) };
|
|
11586
11777
|
});
|
|
11587
|
-
return { results, total
|
|
11778
|
+
return { results, total };
|
|
11588
11779
|
}
|
|
11589
11780
|
getAllIndexable() {
|
|
11590
11781
|
return getAllIndexableWithStatement((sql) => this.stmt(sql));
|
|
@@ -12003,12 +12194,12 @@ var buildIdCache;
|
|
|
12003
12194
|
function projectIndexServerBuildId(entrypoint) {
|
|
12004
12195
|
const file = entrypoint instanceof URL || entrypoint.startsWith("file:") ? fileURLToPath(entrypoint) : path16.resolve(entrypoint);
|
|
12005
12196
|
try {
|
|
12006
|
-
const
|
|
12007
|
-
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) {
|
|
12008
12199
|
return buildIdCache.buildId;
|
|
12009
12200
|
}
|
|
12010
12201
|
const buildId = createHash4("sha256").update(fs11.readFileSync(file)).digest("hex").slice(0, 24);
|
|
12011
|
-
buildIdCache = { file, mtimeMs:
|
|
12202
|
+
buildIdCache = { file, mtimeMs: stat19.mtimeMs, size: stat19.size, buildId };
|
|
12012
12203
|
return buildId;
|
|
12013
12204
|
} catch {
|
|
12014
12205
|
return `unreadable:${path16.basename(file)}`;
|
|
@@ -12178,6 +12369,8 @@ var ProjectServerConnection = class {
|
|
|
12178
12369
|
connectResolve = null;
|
|
12179
12370
|
connectReject = null;
|
|
12180
12371
|
nextId = 1;
|
|
12372
|
+
/** WS-027: read from the owner-only metadata file — see currentAuthToken. */
|
|
12373
|
+
authToken;
|
|
12181
12374
|
pending = /* @__PURE__ */ new Map();
|
|
12182
12375
|
transition(status, options = {}) {
|
|
12183
12376
|
const previous = connectionStates.get(this.endpoint);
|
|
@@ -12198,6 +12391,10 @@ var ProjectServerConnection = class {
|
|
|
12198
12391
|
isConnected() {
|
|
12199
12392
|
return this.socket !== null && !this.socket.destroyed && this.info !== null;
|
|
12200
12393
|
}
|
|
12394
|
+
/** Safe LRU candidate: no request, connect, or health probe is in flight. */
|
|
12395
|
+
isEvictable() {
|
|
12396
|
+
return this.pending.size === 0 && this.connecting === null && this.healthCheck === null;
|
|
12397
|
+
}
|
|
12201
12398
|
async checkHealth(spawnIfMissing = false, timeoutMs = SERVER_HEALTH_TIMEOUT_MS) {
|
|
12202
12399
|
await this.ensureConnected(spawnIfMissing);
|
|
12203
12400
|
if (this.healthCheck) return this.healthCheck;
|
|
@@ -12310,6 +12507,31 @@ var ProjectServerConnection = class {
|
|
|
12310
12507
|
this.transition("offline");
|
|
12311
12508
|
maybeStopHeartbeatLoop();
|
|
12312
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
|
+
}
|
|
12313
12535
|
request(message, options) {
|
|
12314
12536
|
const socket = this.socket;
|
|
12315
12537
|
if (!socket || socket.destroyed) {
|
|
@@ -12353,7 +12575,7 @@ var ProjectServerConnection = class {
|
|
|
12353
12575
|
return;
|
|
12354
12576
|
}
|
|
12355
12577
|
}
|
|
12356
|
-
this.write({ ...message, id });
|
|
12578
|
+
this.write({ ...message, id, authToken: this.currentAuthToken() });
|
|
12357
12579
|
});
|
|
12358
12580
|
}
|
|
12359
12581
|
async ensureConnected(spawnIfMissing) {
|
|
@@ -12400,6 +12622,7 @@ var ProjectServerConnection = class {
|
|
|
12400
12622
|
connectOnce() {
|
|
12401
12623
|
this.socket?.destroy();
|
|
12402
12624
|
this.socket = null;
|
|
12625
|
+
this.authToken = void 0;
|
|
12403
12626
|
this.info = null;
|
|
12404
12627
|
this.activity = null;
|
|
12405
12628
|
this.health = null;
|
|
@@ -12596,7 +12819,26 @@ var ProjectServerConnection = class {
|
|
|
12596
12819
|
}
|
|
12597
12820
|
};
|
|
12598
12821
|
var connections = /* @__PURE__ */ new Map();
|
|
12822
|
+
var MAX_CACHED_CONNECTIONS = 8;
|
|
12599
12823
|
var heartbeatTimer;
|
|
12824
|
+
function forgetConnection(endpoint, connection) {
|
|
12825
|
+
if (connections.get(endpoint) === connection) connections.delete(endpoint);
|
|
12826
|
+
connection.close();
|
|
12827
|
+
connectionStates.delete(endpoint);
|
|
12828
|
+
if (latestConnectionState.endpoint !== endpoint) return;
|
|
12829
|
+
latestConnectionState = [...connectionStates.values()].at(-1) ?? {
|
|
12830
|
+
status: isProjectIndexServerAvailable() ? "offline" : "unavailable",
|
|
12831
|
+
connected: false
|
|
12832
|
+
};
|
|
12833
|
+
}
|
|
12834
|
+
function trimConnectionCache(protectedConnection) {
|
|
12835
|
+
if (connections.size <= MAX_CACHED_CONNECTIONS) return;
|
|
12836
|
+
for (const [endpoint, connection] of connections) {
|
|
12837
|
+
if (connections.size <= MAX_CACHED_CONNECTIONS) break;
|
|
12838
|
+
if (connection === protectedConnection || !connection.isEvictable()) continue;
|
|
12839
|
+
forgetConnection(endpoint, connection);
|
|
12840
|
+
}
|
|
12841
|
+
}
|
|
12600
12842
|
function ensureHeartbeatLoop() {
|
|
12601
12843
|
if (heartbeatTimer) return;
|
|
12602
12844
|
heartbeatTimer = setInterval(() => {
|
|
@@ -12619,7 +12861,11 @@ function connectionFor(projectRoot, indexDir) {
|
|
|
12619
12861
|
if (!connection) {
|
|
12620
12862
|
connection = new ProjectServerConnection(projectRoot, indexDir, endpoint);
|
|
12621
12863
|
connections.set(endpoint, connection);
|
|
12864
|
+
} else {
|
|
12865
|
+
connections.delete(endpoint);
|
|
12866
|
+
connections.set(endpoint, connection);
|
|
12622
12867
|
}
|
|
12868
|
+
trimConnectionCache(connection);
|
|
12623
12869
|
return connection;
|
|
12624
12870
|
}
|
|
12625
12871
|
function callProjectIndexServer(op, args, options) {
|
|
@@ -13020,9 +13266,9 @@ async function runIndexerWithStore(store, opts) {
|
|
|
13020
13266
|
const statReadParse = await Promise.allSettled(
|
|
13021
13267
|
batchFiles.map(
|
|
13022
13268
|
async (file) => {
|
|
13023
|
-
let
|
|
13269
|
+
let stat19;
|
|
13024
13270
|
try {
|
|
13025
|
-
|
|
13271
|
+
stat19 = await fs17.stat(file, statOpts);
|
|
13026
13272
|
} catch (e) {
|
|
13027
13273
|
if (isAbortError(e)) throw e;
|
|
13028
13274
|
return {
|
|
@@ -13034,21 +13280,21 @@ async function runIndexerWithStore(store, opts) {
|
|
|
13034
13280
|
missing: isMissingPathError(e)
|
|
13035
13281
|
};
|
|
13036
13282
|
}
|
|
13037
|
-
if (!
|
|
13283
|
+
if (!stat19.isFile()) return { file, stat: stat19, lang: "", parsed: null };
|
|
13038
13284
|
const lang = detectLang(file);
|
|
13039
|
-
if (!lang) return { file, stat:
|
|
13040
|
-
if (
|
|
13285
|
+
if (!lang) return { file, stat: stat19, lang: "", parsed: null };
|
|
13286
|
+
if (stat19.size > MAX_INDEX_FILE_BYTES) {
|
|
13041
13287
|
return {
|
|
13042
13288
|
file,
|
|
13043
|
-
stat:
|
|
13289
|
+
stat: stat19,
|
|
13044
13290
|
lang,
|
|
13045
13291
|
parsed: null,
|
|
13046
|
-
error: `file too large (${
|
|
13292
|
+
error: `file too large (${stat19.size} bytes; max ${MAX_INDEX_FILE_BYTES})`
|
|
13047
13293
|
};
|
|
13048
13294
|
}
|
|
13049
13295
|
const meta = existingMeta.get(file);
|
|
13050
|
-
if (!force && meta && meta.mtimeMs === Math.floor(
|
|
13051
|
-
return { file, stat:
|
|
13296
|
+
if (!force && meta && meta.mtimeMs === Math.floor(stat19.mtimeMs)) {
|
|
13297
|
+
return { file, stat: stat19, lang, parsed: null, skippedMeta: meta };
|
|
13052
13298
|
}
|
|
13053
13299
|
let content;
|
|
13054
13300
|
try {
|
|
@@ -13057,7 +13303,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
13057
13303
|
if (isAbortError(e)) throw e;
|
|
13058
13304
|
return {
|
|
13059
13305
|
file,
|
|
13060
|
-
stat:
|
|
13306
|
+
stat: stat19,
|
|
13061
13307
|
lang,
|
|
13062
13308
|
parsed: null,
|
|
13063
13309
|
error: `read error: ${e instanceof Error ? e.message : String(e)}`
|
|
@@ -13069,13 +13315,13 @@ async function runIndexerWithStore(store, opts) {
|
|
|
13069
13315
|
} catch (e) {
|
|
13070
13316
|
return {
|
|
13071
13317
|
file,
|
|
13072
|
-
stat:
|
|
13318
|
+
stat: stat19,
|
|
13073
13319
|
lang,
|
|
13074
13320
|
parsed: null,
|
|
13075
13321
|
error: `parse error: ${e instanceof Error ? e.message : String(e)}`
|
|
13076
13322
|
};
|
|
13077
13323
|
}
|
|
13078
|
-
return { file, stat:
|
|
13324
|
+
return { file, stat: stat19, lang, parsed, content };
|
|
13079
13325
|
}
|
|
13080
13326
|
)
|
|
13081
13327
|
);
|
|
@@ -13096,7 +13342,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
13096
13342
|
errors.push(`${file}: ${result.error}`);
|
|
13097
13343
|
continue;
|
|
13098
13344
|
}
|
|
13099
|
-
const { stat:
|
|
13345
|
+
const { stat: stat19, lang, parsed } = result;
|
|
13100
13346
|
if (result.skippedMeta) {
|
|
13101
13347
|
langStats[lang] = (langStats[lang] ?? 0) + result.skippedMeta.symbolCount;
|
|
13102
13348
|
symbolsIndexed += result.skippedMeta.symbolCount;
|
|
@@ -13108,7 +13354,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
13108
13354
|
store.upsertFile({
|
|
13109
13355
|
file,
|
|
13110
13356
|
lang,
|
|
13111
|
-
mtimeMs: Math.floor(
|
|
13357
|
+
mtimeMs: Math.floor(stat19.mtimeMs),
|
|
13112
13358
|
symbolCount: 0,
|
|
13113
13359
|
lastIndexed: Date.now()
|
|
13114
13360
|
});
|
|
@@ -13120,7 +13366,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
13120
13366
|
store.replaceEmptyFile({
|
|
13121
13367
|
file,
|
|
13122
13368
|
lang,
|
|
13123
|
-
mtimeMs: Math.floor(
|
|
13369
|
+
mtimeMs: Math.floor(stat19.mtimeMs),
|
|
13124
13370
|
symbolCount: 0,
|
|
13125
13371
|
lastIndexed: Date.now()
|
|
13126
13372
|
});
|
|
@@ -13132,7 +13378,7 @@ async function runIndexerWithStore(store, opts) {
|
|
|
13132
13378
|
lang,
|
|
13133
13379
|
symbols: parsed.symbols,
|
|
13134
13380
|
refs: parsed.refs ?? [],
|
|
13135
|
-
mtimeMs: Math.floor(
|
|
13381
|
+
mtimeMs: Math.floor(stat19.mtimeMs),
|
|
13136
13382
|
symbolCount: parsed.symbols.length
|
|
13137
13383
|
});
|
|
13138
13384
|
deleteForFiles.push(file);
|
|
@@ -14328,8 +14574,9 @@ function runDeadCodeScan(projectRoot, opts = {}) {
|
|
|
14328
14574
|
const content = fs19.readFileSync(epFile, "utf8");
|
|
14329
14575
|
const strippedContent = content.replace(/\/\*[\s\S]*?\*\//g, (m) => " ".repeat(m.length)).replace(/\/\/[^\n]*/g, (m) => " ".repeat(m.length));
|
|
14330
14576
|
const reExportRe = /export\s+(?:(?:type\s+)?\{[\s\S]*?\}\s+from|\*\s+as\s+\w+\s+from|\*\s+from)\s+['"]([^'"]+)['"]/g;
|
|
14331
|
-
|
|
14332
|
-
|
|
14577
|
+
for (; ; ) {
|
|
14578
|
+
const match = reExportRe.exec(strippedContent);
|
|
14579
|
+
if (match === null) break;
|
|
14333
14580
|
const moduleSpec = match[1];
|
|
14334
14581
|
const resolvedFiles = resolveModulePath(epFile, moduleSpec, indexedFiles);
|
|
14335
14582
|
for (const rf of resolvedFiles) {
|
|
@@ -14535,6 +14782,9 @@ var designTool = {
|
|
|
14535
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).',
|
|
14536
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"}`.',
|
|
14537
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",
|
|
14538
14788
|
mutating: true,
|
|
14539
14789
|
capabilities: ["fs.write"],
|
|
14540
14790
|
timeoutMs: 15e3,
|
|
@@ -14715,9 +14965,7 @@ Run \`design {action:"materialize"}\` to write the tuned tokens to your theme fi
|
|
|
14715
14965
|
const abs = path25.join(absParent, path25.basename(absResolved));
|
|
14716
14966
|
const rel = path25.relative(root, abs);
|
|
14717
14967
|
if (rel.startsWith("..") || path25.isAbsolute(rel)) {
|
|
14718
|
-
throw new Error(
|
|
14719
|
-
`design: materialize path "${result.path}" would escape the project root`
|
|
14720
|
-
);
|
|
14968
|
+
throw new Error(`design: materialize path "${result.path}" would escape the project root`);
|
|
14721
14969
|
}
|
|
14722
14970
|
let exists = false;
|
|
14723
14971
|
try {
|
|
@@ -14792,6 +15040,7 @@ import { statSync as statSync3 } from "node:fs";
|
|
|
14792
15040
|
import * as fs21 from "node:fs/promises";
|
|
14793
15041
|
import * as path26 from "node:path";
|
|
14794
15042
|
import { buildChildEnv as buildChildEnv3 } from "@wrongstack/core/utils";
|
|
15043
|
+
var MAX_FILE_DUMP_BYTES = 5 * 1024 * 1024;
|
|
14795
15044
|
var diffTool = {
|
|
14796
15045
|
name: "diff",
|
|
14797
15046
|
category: "Filesystem",
|
|
@@ -14799,6 +15048,7 @@ var diffTool = {
|
|
|
14799
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).',
|
|
14800
15049
|
permission: "auto",
|
|
14801
15050
|
mutating: false,
|
|
15051
|
+
maxOutputBytes: 262144,
|
|
14802
15052
|
capabilities: ["fs.read"],
|
|
14803
15053
|
icon: "diff",
|
|
14804
15054
|
timeoutMs: 1e4,
|
|
@@ -14874,8 +15124,8 @@ function findGitDir(cwd) {
|
|
|
14874
15124
|
let dir = cwd;
|
|
14875
15125
|
for (let i = 0; i < 20; i++) {
|
|
14876
15126
|
try {
|
|
14877
|
-
const
|
|
14878
|
-
if (
|
|
15127
|
+
const stat19 = statSync3(path26.join(dir, ".git"));
|
|
15128
|
+
if (stat19.isDirectory()) return dir;
|
|
14879
15129
|
} catch {
|
|
14880
15130
|
}
|
|
14881
15131
|
const parent = path26.dirname(dir);
|
|
@@ -14917,10 +15167,18 @@ async function fileDiff(input, ctx, _signal) {
|
|
|
14917
15167
|
};
|
|
14918
15168
|
}
|
|
14919
15169
|
const results = [];
|
|
15170
|
+
let truncated = false;
|
|
14920
15171
|
for (const file of files) {
|
|
14921
15172
|
const absPath = safeResolve(file, ctx);
|
|
14922
|
-
const
|
|
14923
|
-
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
|
+
}
|
|
14924
15182
|
const content = await fs21.readFile(absPath, "utf8");
|
|
14925
15183
|
const lines = content.split(/\r?\n/);
|
|
14926
15184
|
results.push(formatWithLineNumbers(file, lines));
|
|
@@ -14928,7 +15186,7 @@ async function fileDiff(input, ctx, _signal) {
|
|
|
14928
15186
|
return {
|
|
14929
15187
|
diff: results.join("\n\n"),
|
|
14930
15188
|
files,
|
|
14931
|
-
truncated
|
|
15189
|
+
truncated,
|
|
14932
15190
|
mode: input.mode ?? "unified"
|
|
14933
15191
|
};
|
|
14934
15192
|
}
|
|
@@ -15021,8 +15279,8 @@ async function resolveFiles(filesInput, cwd) {
|
|
|
15021
15279
|
for (const f of files) {
|
|
15022
15280
|
const absPath = f.trim().startsWith("/") ? f.trim() : `${cwd}/${f.trim()}`;
|
|
15023
15281
|
try {
|
|
15024
|
-
const
|
|
15025
|
-
if (
|
|
15282
|
+
const stat19 = await fs22.stat(absPath);
|
|
15283
|
+
if (stat19.isFile()) resolved.push(absPath);
|
|
15026
15284
|
} catch {
|
|
15027
15285
|
}
|
|
15028
15286
|
}
|
|
@@ -15875,7 +16133,7 @@ var editTool = {
|
|
|
15875
16133
|
});
|
|
15876
16134
|
}
|
|
15877
16135
|
const absPath = await safeResolveReal(input.path, ctx);
|
|
15878
|
-
const
|
|
16136
|
+
const stat19 = await fs23.stat(absPath).catch((err) => {
|
|
15879
16137
|
if (err.code === "ENOENT") {
|
|
15880
16138
|
throw new ToolValidationError({
|
|
15881
16139
|
message: `edit: file "${input.path}" does not exist. Use \`write\` instead.`,
|
|
@@ -15885,7 +16143,7 @@ var editTool = {
|
|
|
15885
16143
|
}
|
|
15886
16144
|
throw err;
|
|
15887
16145
|
});
|
|
15888
|
-
if (!
|
|
16146
|
+
if (!stat19.isFile()) {
|
|
15889
16147
|
throw new ToolValidationError({
|
|
15890
16148
|
message: `edit: "${input.path}" is not a regular file`,
|
|
15891
16149
|
field: "path"
|
|
@@ -15915,7 +16173,7 @@ var editTool = {
|
|
|
15915
16173
|
});
|
|
15916
16174
|
}
|
|
15917
16175
|
}
|
|
15918
|
-
if (autoRead && updated.mtimeMs >
|
|
16176
|
+
if (autoRead && updated.mtimeMs > stat19.mtimeMs + mtimeTolerance) {
|
|
15919
16177
|
throw new ToolValidationError({
|
|
15920
16178
|
message: `edit: file "${input.path}" changed while being auto-read. Retry the edit.`,
|
|
15921
16179
|
field: "path",
|
|
@@ -16040,7 +16298,6 @@ import {
|
|
|
16040
16298
|
import { toErrorMessage as toErrorMessage3 } from "@wrongstack/core/utils/error";
|
|
16041
16299
|
init_output_spool();
|
|
16042
16300
|
init_util();
|
|
16043
|
-
init_process_registry();
|
|
16044
16301
|
init_win32_resolve();
|
|
16045
16302
|
|
|
16046
16303
|
// src/exec-kill-guard.ts
|
|
@@ -16279,6 +16536,7 @@ async function checkKillTarget(target) {
|
|
|
16279
16536
|
}
|
|
16280
16537
|
|
|
16281
16538
|
// src/exec.ts
|
|
16539
|
+
init_process_registry();
|
|
16282
16540
|
var isWin4 = process.platform === "win32";
|
|
16283
16541
|
var DEFAULT_ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
|
|
16284
16542
|
// JS / TS toolchain
|
|
@@ -16944,7 +17202,17 @@ var BLOCKED_ARG_PATTERNS = {
|
|
|
16944
17202
|
bun: [],
|
|
16945
17203
|
docker: [],
|
|
16946
17204
|
// find -exec/-ok/-execdir execute arbitrary commands
|
|
16947
|
-
find: [
|
|
17205
|
+
find: [
|
|
17206
|
+
/^-exec$/,
|
|
17207
|
+
/^-exec;$/,
|
|
17208
|
+
/^-ok$/,
|
|
17209
|
+
/^-ok;$/,
|
|
17210
|
+
/^-execdir$/,
|
|
17211
|
+
/^-execdir;$/,
|
|
17212
|
+
/^-exec=/,
|
|
17213
|
+
/^-ok=/,
|
|
17214
|
+
/^-execdir=/
|
|
17215
|
+
],
|
|
16948
17216
|
// rm -rf / is catastrophic — block absolute paths, home, dot-dirs,
|
|
16949
17217
|
// and glob patterns that could expand to dangerous targets.
|
|
16950
17218
|
// `rm -rf ./src/*` expands to project files; `rm -rf ../../` escapes upward;
|
|
@@ -17015,6 +17283,14 @@ var execTool = {
|
|
|
17015
17283
|
useInstead: ["bash"]
|
|
17016
17284
|
},
|
|
17017
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",
|
|
17018
17294
|
mutating: true,
|
|
17019
17295
|
riskTier: "standard",
|
|
17020
17296
|
timeoutMs: DEFAULT_TIMEOUT_MS3,
|
|
@@ -17231,7 +17507,14 @@ function runCommand(cmd, args, cwd, timeout, signal, sessionId, danger) {
|
|
|
17231
17507
|
const pid = child.pid;
|
|
17232
17508
|
if (typeof pid === "number") {
|
|
17233
17509
|
const fullCommand = `${cmd} ${args.join(" ")}`;
|
|
17234
|
-
registry.register({
|
|
17510
|
+
registry.register({
|
|
17511
|
+
pid,
|
|
17512
|
+
name: "exec",
|
|
17513
|
+
command: redactCommand(fullCommand),
|
|
17514
|
+
startedAt: Date.now(),
|
|
17515
|
+
sessionId,
|
|
17516
|
+
child
|
|
17517
|
+
});
|
|
17235
17518
|
}
|
|
17236
17519
|
const timer = setTimeout(() => {
|
|
17237
17520
|
killed = true;
|
|
@@ -17671,6 +17954,9 @@ var formatTool = {
|
|
|
17671
17954
|
description: "Format source files according to project style (Biome). Can also run in check-only mode.",
|
|
17672
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.",
|
|
17673
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",
|
|
17674
17960
|
mutating: true,
|
|
17675
17961
|
capabilities: ["fs.write", "shell.restricted"],
|
|
17676
17962
|
icon: "code",
|
|
@@ -17775,13 +18061,13 @@ var formatTool = {
|
|
|
17775
18061
|
}
|
|
17776
18062
|
};
|
|
17777
18063
|
async function detectFixer(cwd) {
|
|
17778
|
-
const { stat:
|
|
18064
|
+
const { stat: stat19 } = await import("node:fs/promises");
|
|
17779
18065
|
try {
|
|
17780
|
-
await
|
|
18066
|
+
await stat19(`${cwd}/biome.json`);
|
|
17781
18067
|
return "biome";
|
|
17782
18068
|
} catch {
|
|
17783
18069
|
try {
|
|
17784
|
-
await
|
|
18070
|
+
await stat19(`${cwd}/.prettierrc`);
|
|
17785
18071
|
return "prettier";
|
|
17786
18072
|
} catch {
|
|
17787
18073
|
return "biome";
|
|
@@ -17808,6 +18094,10 @@ var gitTool = {
|
|
|
17808
18094
|
// Conservative: any of these may mutate. The non-mutating commands
|
|
17809
18095
|
// (status/log/diff/branch/fetch) are still gated on `permission: 'confirm'`
|
|
17810
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",
|
|
17811
18101
|
mutating: true,
|
|
17812
18102
|
capabilities: ["fs.write", "shell.restricted"],
|
|
17813
18103
|
timeoutMs: TIMEOUT_MS2,
|
|
@@ -17876,6 +18166,8 @@ var gitTool = {
|
|
|
17876
18166
|
truncated: false
|
|
17877
18167
|
};
|
|
17878
18168
|
}
|
|
18169
|
+
const branchGuard = validateBranchInput(input);
|
|
18170
|
+
if (branchGuard) return branchGuard;
|
|
17879
18171
|
if (input.command === "worktree") {
|
|
17880
18172
|
const guard = validateWorktreeInput(input, ctx.projectRoot);
|
|
17881
18173
|
if (guard) return guard;
|
|
@@ -17924,6 +18216,18 @@ var gitTool = {
|
|
|
17924
18216
|
return result;
|
|
17925
18217
|
}
|
|
17926
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
|
+
}
|
|
17927
18231
|
function validateWorktreeInput(input, projectRoot) {
|
|
17928
18232
|
const reject = (stderr) => ({
|
|
17929
18233
|
command: "worktree",
|
|
@@ -17932,7 +18236,6 @@ function validateWorktreeInput(input, projectRoot) {
|
|
|
17932
18236
|
exitCode: 1,
|
|
17933
18237
|
truncated: false
|
|
17934
18238
|
});
|
|
17935
|
-
if (input.branch?.startsWith("-")) return reject(`unsafe branch name: ${input.branch}`);
|
|
17936
18239
|
if (input.worktreePath?.startsWith("-")) {
|
|
17937
18240
|
return reject(`unsafe worktree path: ${input.worktreePath}`);
|
|
17938
18241
|
}
|
|
@@ -17950,8 +18253,8 @@ function findGitDir2(cwd, projectRoot) {
|
|
|
17950
18253
|
let dir = cwd;
|
|
17951
18254
|
for (let i = 0; i < 20; i++) {
|
|
17952
18255
|
try {
|
|
17953
|
-
const
|
|
17954
|
-
if (
|
|
18256
|
+
const stat19 = statSync4(`${dir}/.git`);
|
|
18257
|
+
if (stat19.isDirectory() || stat19.isFile()) return dir;
|
|
17955
18258
|
} catch {
|
|
17956
18259
|
}
|
|
17957
18260
|
if (dir === root) break;
|
|
@@ -17986,7 +18289,10 @@ function buildArgs(input) {
|
|
|
17986
18289
|
...files.length ? ["--", ...files] : []
|
|
17987
18290
|
];
|
|
17988
18291
|
case "branch":
|
|
17989
|
-
return input.branch ? [
|
|
18292
|
+
return input.branch ? [
|
|
18293
|
+
"branch",
|
|
18294
|
+
...input.branch.startsWith("-") || input.branch.includes(" --") ? [] : [input.branch]
|
|
18295
|
+
] : ["branch"];
|
|
17990
18296
|
case "checkout":
|
|
17991
18297
|
return [
|
|
17992
18298
|
"checkout",
|
|
@@ -18079,7 +18385,7 @@ import * as path30 from "node:path";
|
|
|
18079
18385
|
import { compileGlob as compileGlob2, DEFAULT_WALK_IGNORE_DIRS as DEFAULT_WALK_IGNORE_DIRS2 } from "@wrongstack/core/utils";
|
|
18080
18386
|
|
|
18081
18387
|
// src/_concurrency.ts
|
|
18082
|
-
async function
|
|
18388
|
+
async function mapWithConcurrency2(items, limit, fn) {
|
|
18083
18389
|
if (items.length === 0) return [];
|
|
18084
18390
|
const effectiveLimit = Math.max(1, Math.min(limit | 0, items.length));
|
|
18085
18391
|
const results = new Array(items.length);
|
|
@@ -18214,10 +18520,10 @@ var globTool = {
|
|
|
18214
18520
|
}
|
|
18215
18521
|
if (truncated) return;
|
|
18216
18522
|
}
|
|
18217
|
-
await
|
|
18523
|
+
await mapWithConcurrency2(matchedFiles, WALK_CONCURRENCY, pushResult);
|
|
18218
18524
|
if (truncated) return;
|
|
18219
18525
|
const remainingSubdirs = truncated ? [] : subdirs;
|
|
18220
|
-
await
|
|
18526
|
+
await mapWithConcurrency2(remainingSubdirs, WALK_CONCURRENCY, ({ full, rel }) => walk2(full, rel));
|
|
18221
18527
|
};
|
|
18222
18528
|
await walk2(base, "");
|
|
18223
18529
|
results.sort((a, b) => b.mtime - a.mtime);
|
|
@@ -18226,12 +18532,16 @@ var globTool = {
|
|
|
18226
18532
|
};
|
|
18227
18533
|
|
|
18228
18534
|
// src/grep.ts
|
|
18229
|
-
import { expectDefined as expectDefined7 } from "@wrongstack/core/utils";
|
|
18230
18535
|
import { spawn as spawn11 } from "node:child_process";
|
|
18231
18536
|
import * as fs25 from "node:fs/promises";
|
|
18232
18537
|
import * as path31 from "node:path";
|
|
18233
18538
|
import { ToolValidationError as ToolValidationError4 } from "@wrongstack/core/types";
|
|
18234
|
-
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";
|
|
18235
18545
|
|
|
18236
18546
|
// src/_regex.ts
|
|
18237
18547
|
var MAX_PATTERN_LEN = 256;
|
|
@@ -18284,6 +18594,8 @@ var DEFAULT_IGNORE3 = DEFAULT_WALK_IGNORE_DIRS3;
|
|
|
18284
18594
|
var NATIVE_SCAN_CONCURRENCY = 32;
|
|
18285
18595
|
var NATIVE_READ_CHUNK_BYTES = 64 * 1024;
|
|
18286
18596
|
var NATIVE_MAX_FILE_BYTES = 1e6;
|
|
18597
|
+
var RG_MAX_QUEUE_CHUNKS = 128;
|
|
18598
|
+
var RG_MAX_QUEUE_CHARS = 8 * 1024 * 1024;
|
|
18287
18599
|
var grepTool = {
|
|
18288
18600
|
name: "grep",
|
|
18289
18601
|
category: "Search",
|
|
@@ -18411,9 +18723,19 @@ async function* runRgStream(input, base, mode, limit, signal) {
|
|
|
18411
18723
|
const FLUSH_AT = 16;
|
|
18412
18724
|
const MAX_BUF_BYTES = 1e6;
|
|
18413
18725
|
let bufOverflow = false;
|
|
18414
|
-
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
|
+
});
|
|
18415
18735
|
const queue = [];
|
|
18736
|
+
let queuedChars = 0;
|
|
18416
18737
|
let waiter;
|
|
18738
|
+
let paused = false;
|
|
18417
18739
|
const wake = () => {
|
|
18418
18740
|
if (waiter) {
|
|
18419
18741
|
const w = waiter;
|
|
@@ -18421,93 +18743,133 @@ async function* runRgStream(input, base, mode, limit, signal) {
|
|
|
18421
18743
|
w();
|
|
18422
18744
|
}
|
|
18423
18745
|
};
|
|
18424
|
-
|
|
18425
|
-
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;
|
|
18426
18762
|
wake();
|
|
18427
|
-
|
|
18428
|
-
|
|
18763
|
+
pauseIfFlooded();
|
|
18764
|
+
};
|
|
18765
|
+
const onError = (e) => {
|
|
18429
18766
|
queue.push({ kind: "error", data: e.message });
|
|
18430
18767
|
wake();
|
|
18431
|
-
}
|
|
18432
|
-
|
|
18768
|
+
};
|
|
18769
|
+
const onClose = () => {
|
|
18433
18770
|
queue.push({ kind: "close", data: "" });
|
|
18434
18771
|
wake();
|
|
18435
|
-
}
|
|
18772
|
+
};
|
|
18773
|
+
child.stdout?.on("data", onStdoutData);
|
|
18774
|
+
child.on("error", onError);
|
|
18775
|
+
child.on("close", onClose);
|
|
18436
18776
|
let pendingBatch = [];
|
|
18437
18777
|
let errored = false;
|
|
18438
|
-
|
|
18439
|
-
|
|
18440
|
-
|
|
18441
|
-
|
|
18442
|
-
|
|
18443
|
-
|
|
18444
|
-
|
|
18445
|
-
if (c.kind === "error") {
|
|
18446
|
-
errored = true;
|
|
18447
|
-
continue;
|
|
18448
|
-
}
|
|
18449
|
-
if (c.kind === "close") break;
|
|
18450
|
-
buf += c.data;
|
|
18451
|
-
if (buf.length > MAX_BUF_BYTES && !bufOverflow) {
|
|
18452
|
-
bufOverflow = true;
|
|
18453
|
-
buf = buf.slice(-MAX_BUF_BYTES);
|
|
18454
|
-
try {
|
|
18455
|
-
child.kill("SIGTERM");
|
|
18456
|
-
} catch {
|
|
18778
|
+
let closed = false;
|
|
18779
|
+
try {
|
|
18780
|
+
for (; ; ) {
|
|
18781
|
+
while (queue.length === 0) {
|
|
18782
|
+
await new Promise((r) => {
|
|
18783
|
+
waiter = r;
|
|
18784
|
+
});
|
|
18457
18785
|
}
|
|
18458
|
-
|
|
18459
|
-
|
|
18460
|
-
|
|
18461
|
-
|
|
18462
|
-
|
|
18463
|
-
|
|
18464
|
-
|
|
18465
|
-
|
|
18466
|
-
|
|
18467
|
-
|
|
18468
|
-
|
|
18469
|
-
|
|
18470
|
-
|
|
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
|
+
}
|
|
18471
18839
|
}
|
|
18472
18840
|
}
|
|
18473
|
-
if (
|
|
18841
|
+
if (pendingBatch.length > 0) {
|
|
18474
18842
|
yield {
|
|
18475
18843
|
type: "partial_output",
|
|
18476
18844
|
text: pendingBatch.join("\n"),
|
|
18477
18845
|
data: { matches_so_far: matches.length }
|
|
18478
18846
|
};
|
|
18479
|
-
pendingBatch = [];
|
|
18480
|
-
batchSinceFlush = 0;
|
|
18481
|
-
}
|
|
18482
|
-
}
|
|
18483
|
-
if (buf.trim()) {
|
|
18484
|
-
for (const line of buf.split("\n")) {
|
|
18485
|
-
if (!line) continue;
|
|
18486
|
-
totalLines++;
|
|
18487
|
-
if (mode === "count") totalCount += parseRgCountLine(line);
|
|
18488
|
-
if (matches.length < limit) {
|
|
18489
|
-
matches.push(line);
|
|
18490
|
-
pendingBatch.push(line);
|
|
18491
|
-
}
|
|
18492
18847
|
}
|
|
18493
|
-
|
|
18494
|
-
if (pendingBatch.length > 0) {
|
|
18848
|
+
if (errored) throw new Error("rg: spawn error");
|
|
18495
18849
|
yield {
|
|
18496
|
-
type: "
|
|
18497
|
-
|
|
18498
|
-
|
|
18850
|
+
type: "final",
|
|
18851
|
+
output: {
|
|
18852
|
+
matches,
|
|
18853
|
+
count: mode === "count" ? totalCount : totalLines,
|
|
18854
|
+
truncated: totalLines > limit || bufOverflow,
|
|
18855
|
+
used: "rg"
|
|
18856
|
+
}
|
|
18499
18857
|
};
|
|
18500
|
-
}
|
|
18501
|
-
|
|
18502
|
-
|
|
18503
|
-
|
|
18504
|
-
|
|
18505
|
-
|
|
18506
|
-
|
|
18507
|
-
|
|
18508
|
-
|
|
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
|
+
}
|
|
18509
18871
|
}
|
|
18510
|
-
}
|
|
18872
|
+
}
|
|
18511
18873
|
}
|
|
18512
18874
|
function parseRgCountLine(line) {
|
|
18513
18875
|
const idx = line.lastIndexOf(":");
|
|
@@ -18537,8 +18899,8 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
18537
18899
|
if (globRe && !globRe.test(name) && !globRe.test(full)) return;
|
|
18538
18900
|
if (globRe) globRe.lastIndex = 0;
|
|
18539
18901
|
try {
|
|
18540
|
-
const
|
|
18541
|
-
if (!
|
|
18902
|
+
const stat19 = await fs25.stat(full);
|
|
18903
|
+
if (!stat19.isFile() || stat19.size > maxBytes || stopped || signal.aborted) return;
|
|
18542
18904
|
const file = await fs25.open(full, "r");
|
|
18543
18905
|
try {
|
|
18544
18906
|
let bytesReadTotal = 0;
|
|
@@ -18638,8 +19000,8 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
18638
19000
|
files.push({ full, name: e.name });
|
|
18639
19001
|
}
|
|
18640
19002
|
}
|
|
18641
|
-
await
|
|
18642
|
-
await
|
|
19003
|
+
await mapWithConcurrency2(files, NATIVE_SCAN_CONCURRENCY, ({ full, name }) => scanFile(full, name));
|
|
19004
|
+
await mapWithConcurrency2(
|
|
18643
19005
|
subdirs,
|
|
18644
19006
|
Math.min(16, NATIVE_SCAN_CONCURRENCY),
|
|
18645
19007
|
({ full, rel }) => walk2(full, rel)
|
|
@@ -18658,7 +19020,7 @@ async function runNative(input, base, mode, limit, signal) {
|
|
|
18658
19020
|
init_spawn_stream();
|
|
18659
19021
|
init_util();
|
|
18660
19022
|
init_legacy_bridge();
|
|
18661
|
-
import { join as
|
|
19023
|
+
import { join as join25 } from "node:path";
|
|
18662
19024
|
import {
|
|
18663
19025
|
detectEcosystem as detectPackageEcosystem,
|
|
18664
19026
|
recordPackageAction
|
|
@@ -18669,6 +19031,10 @@ var installTool = {
|
|
|
18669
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.",
|
|
18670
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.",
|
|
18671
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",
|
|
18672
19038
|
mutating: true,
|
|
18673
19039
|
riskTier: "standard",
|
|
18674
19040
|
icon: "package",
|
|
@@ -18838,10 +19204,10 @@ function resolveManifestPath(cwd, pkgManager) {
|
|
|
18838
19204
|
case "pnpm":
|
|
18839
19205
|
case "yarn":
|
|
18840
19206
|
case "npm":
|
|
18841
|
-
return
|
|
19207
|
+
return join25(cwd, "package.json");
|
|
18842
19208
|
/* v8 ignore next 2 -- pkgManager is always pnpm/yarn/npm; the default is defensive. */
|
|
18843
19209
|
default:
|
|
18844
|
-
return
|
|
19210
|
+
return join25(cwd, "package.json");
|
|
18845
19211
|
}
|
|
18846
19212
|
}
|
|
18847
19213
|
|
|
@@ -18849,6 +19215,24 @@ function resolveManifestPath(cwd, pkgManager) {
|
|
|
18849
19215
|
init_util();
|
|
18850
19216
|
import * as fs26 from "node:fs/promises";
|
|
18851
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
|
+
}
|
|
18852
19236
|
var jsonTool = {
|
|
18853
19237
|
name: "json",
|
|
18854
19238
|
category: "Data",
|
|
@@ -18856,6 +19240,7 @@ var jsonTool = {
|
|
|
18856
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.',
|
|
18857
19241
|
permission: "auto",
|
|
18858
19242
|
mutating: false,
|
|
19243
|
+
maxOutputBytes: 262144,
|
|
18859
19244
|
timeoutMs: 5e3,
|
|
18860
19245
|
capabilities: ["fs.read"],
|
|
18861
19246
|
icon: "json",
|
|
@@ -18922,9 +19307,15 @@ async function executeParse(input, ctx) {
|
|
|
18922
19307
|
let raw;
|
|
18923
19308
|
if (input.file) {
|
|
18924
19309
|
try {
|
|
18925
|
-
raw = await
|
|
18926
|
-
} catch {
|
|
18927
|
-
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
|
+
};
|
|
18928
19319
|
}
|
|
18929
19320
|
} else if (input.data) {
|
|
18930
19321
|
raw = input.data;
|
|
@@ -18980,10 +19371,16 @@ async function executeQuery(input, ctx) {
|
|
|
18980
19371
|
let parsed;
|
|
18981
19372
|
if (input.file) {
|
|
18982
19373
|
try {
|
|
18983
|
-
const raw = await
|
|
19374
|
+
const raw = await readJsonFileBounded(input.file, ctx);
|
|
18984
19375
|
parsed = JSON.parse(raw);
|
|
18985
|
-
} catch {
|
|
18986
|
-
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
|
+
};
|
|
18987
19384
|
}
|
|
18988
19385
|
} else if (input.data) {
|
|
18989
19386
|
try {
|
|
@@ -19022,10 +19419,16 @@ async function executeValidate(input, ctx) {
|
|
|
19022
19419
|
let parsed;
|
|
19023
19420
|
if (input.file) {
|
|
19024
19421
|
try {
|
|
19025
|
-
const raw = await
|
|
19422
|
+
const raw = await readJsonFileBounded(input.file, ctx);
|
|
19026
19423
|
parsed = JSON.parse(raw);
|
|
19027
|
-
} catch {
|
|
19028
|
-
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
|
+
};
|
|
19029
19432
|
}
|
|
19030
19433
|
} else if (input.data) {
|
|
19031
19434
|
try {
|
|
@@ -19064,10 +19467,16 @@ async function executeTransform(input, ctx) {
|
|
|
19064
19467
|
let parsed;
|
|
19065
19468
|
if (input.file) {
|
|
19066
19469
|
try {
|
|
19067
|
-
const raw = await
|
|
19470
|
+
const raw = await readJsonFileBounded(input.file, ctx);
|
|
19068
19471
|
parsed = JSON.parse(raw);
|
|
19069
|
-
} catch {
|
|
19070
|
-
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
|
+
};
|
|
19071
19480
|
}
|
|
19072
19481
|
} else if (input.data) {
|
|
19073
19482
|
try {
|
|
@@ -19671,6 +20080,10 @@ async function handleKanbanDetailAction(projectRoot, input) {
|
|
|
19671
20080
|
}
|
|
19672
20081
|
|
|
19673
20082
|
// src/kanban-board-inputs.ts
|
|
20083
|
+
function agentSettableGate(enforcement) {
|
|
20084
|
+
if (enforcement === void 0 || enforcement === "off") return {};
|
|
20085
|
+
return { completionGate: { enforcement } };
|
|
20086
|
+
}
|
|
19674
20087
|
function boardCreateInput(input, title) {
|
|
19675
20088
|
return {
|
|
19676
20089
|
title,
|
|
@@ -19683,7 +20096,7 @@ function boardCreateInput(input, title) {
|
|
|
19683
20096
|
decomposition: input.atomicityDecomposition ?? "propose"
|
|
19684
20097
|
}
|
|
19685
20098
|
} : {},
|
|
19686
|
-
...input.gateEnforcement
|
|
20099
|
+
...agentSettableGate(input.gateEnforcement)
|
|
19687
20100
|
};
|
|
19688
20101
|
}
|
|
19689
20102
|
function boardUpdatePatch(input) {
|
|
@@ -19697,7 +20110,7 @@ function boardUpdatePatch(input) {
|
|
|
19697
20110
|
decomposition: input.atomicityDecomposition ?? "propose"
|
|
19698
20111
|
}
|
|
19699
20112
|
} : {},
|
|
19700
|
-
...input.gateEnforcement
|
|
20113
|
+
...agentSettableGate(input.gateEnforcement)
|
|
19701
20114
|
};
|
|
19702
20115
|
}
|
|
19703
20116
|
function duplicateBoardOptions(input) {
|
|
@@ -19712,6 +20125,7 @@ function duplicateBoardOptions(input) {
|
|
|
19712
20125
|
|
|
19713
20126
|
// src/kanban-task-inputs.ts
|
|
19714
20127
|
import { randomUUID } from "node:crypto";
|
|
20128
|
+
import { clampSubagentCapabilities } from "@wrongstack/core/security";
|
|
19715
20129
|
function taskInput(input) {
|
|
19716
20130
|
const assignment = hasAssignmentInput(input) ? assignmentForTaskCreate(input) : void 0;
|
|
19717
20131
|
return {
|
|
@@ -19809,6 +20223,10 @@ function taskPatch(input) {
|
|
|
19809
20223
|
...input.actualHours !== void 0 ? { actualHours: input.actualHours } : {}
|
|
19810
20224
|
};
|
|
19811
20225
|
}
|
|
20226
|
+
function clampRequestedCapabilities(requested) {
|
|
20227
|
+
if (requested === void 0) return void 0;
|
|
20228
|
+
return clampSubagentCapabilities(requested).granted;
|
|
20229
|
+
}
|
|
19812
20230
|
function assignmentInput(input) {
|
|
19813
20231
|
return {
|
|
19814
20232
|
agentId: input.agentId,
|
|
@@ -19819,7 +20237,7 @@ function assignmentInput(input) {
|
|
|
19819
20237
|
fallbackProfile: input.fallbackProfile,
|
|
19820
20238
|
fallbackModels: input.fallbackModels,
|
|
19821
20239
|
tools: input.tools,
|
|
19822
|
-
allowedCapabilities: input.allowedCapabilities,
|
|
20240
|
+
allowedCapabilities: clampRequestedCapabilities(input.allowedCapabilities),
|
|
19823
20241
|
assignee: input.assignee,
|
|
19824
20242
|
leaseId: input.leaseId,
|
|
19825
20243
|
claimedAt: input.claimedAt,
|
|
@@ -19846,7 +20264,7 @@ function assignmentForTaskCreate(input) {
|
|
|
19846
20264
|
...input.fallbackProfile !== void 0 ? { fallbackProfile: input.fallbackProfile } : {},
|
|
19847
20265
|
...input.fallbackModels !== void 0 ? { fallbackModels: input.fallbackModels } : {},
|
|
19848
20266
|
...input.tools !== void 0 ? { tools: input.tools } : {},
|
|
19849
|
-
...input.allowedCapabilities !== void 0 ? { allowedCapabilities: input.allowedCapabilities } : {},
|
|
20267
|
+
...input.allowedCapabilities !== void 0 ? { allowedCapabilities: clampRequestedCapabilities(input.allowedCapabilities) } : {},
|
|
19850
20268
|
...input.leaseId !== void 0 ? { leaseId: input.leaseId } : {},
|
|
19851
20269
|
...input.claimedAt !== void 0 ? { claimedAt: input.claimedAt } : {},
|
|
19852
20270
|
...input.heartbeatAt !== void 0 ? { heartbeatAt: input.heartbeatAt } : {},
|
|
@@ -20057,7 +20475,13 @@ var KANBAN_INPUT_SCHEMA = {
|
|
|
20057
20475
|
moveTasksToColumnId: { type: "string" },
|
|
20058
20476
|
atomicityMode: { type: "string", enum: ["off", "assess", "enforce"] },
|
|
20059
20477
|
atomicityDecomposition: { type: "string", enum: ["auto", "propose"] },
|
|
20060
|
-
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
|
+
},
|
|
20061
20485
|
subtasks: {
|
|
20062
20486
|
type: "array",
|
|
20063
20487
|
minItems: 2,
|
|
@@ -20099,7 +20523,7 @@ function createKanbanPresenceWrapper(projectRoot, input, ctx) {
|
|
|
20099
20523
|
|
|
20100
20524
|
// src/session-kanban.ts
|
|
20101
20525
|
import { watch } from "node:fs";
|
|
20102
|
-
import { basename as
|
|
20526
|
+
import { basename as basename13, dirname as dirname15 } from "node:path";
|
|
20103
20527
|
import { getSharedProjectMailbox } from "@wrongstack/core/coordination";
|
|
20104
20528
|
import {
|
|
20105
20529
|
loadPlan,
|
|
@@ -20189,7 +20613,9 @@ async function ensureSessionKanbanBoard(projectRoot, sessionId) {
|
|
|
20189
20613
|
description: "Live session work: todos, tasks, and plan items.",
|
|
20190
20614
|
tags: sessionBoardTags(sessionId),
|
|
20191
20615
|
columns: SESSION_KANBAN_COLUMNS,
|
|
20192
|
-
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 }
|
|
20193
20619
|
});
|
|
20194
20620
|
}
|
|
20195
20621
|
if (!sameColumns(board.columns) || !board.tags?.includes(SESSION_BOARD_TAG)) {
|
|
@@ -20424,12 +20850,26 @@ function projectSessionPlanToKanban(projectRoot, items, sessionId) {
|
|
|
20424
20850
|
function fireAndForget(context, work) {
|
|
20425
20851
|
void work.catch((err) => {
|
|
20426
20852
|
const message = err instanceof Error ? err.message : String(err);
|
|
20427
|
-
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
|
+
);
|
|
20428
20862
|
});
|
|
20429
20863
|
}
|
|
20430
20864
|
function broadcastTodoUpdate(context, todos) {
|
|
20431
20865
|
const sessionId = context.session?.id ?? "";
|
|
20432
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
|
+
}
|
|
20433
20873
|
const projectDir = resolveWstackPaths3({ projectRoot: context.projectRoot }).projectDir;
|
|
20434
20874
|
const mailbox = getSharedProjectMailbox(projectDir);
|
|
20435
20875
|
void mailbox.send({
|
|
@@ -20441,11 +20881,11 @@ function broadcastTodoUpdate(context, todos) {
|
|
|
20441
20881
|
kind: "kanban.todos.updated",
|
|
20442
20882
|
sessionId,
|
|
20443
20883
|
revision: context.state.revision,
|
|
20444
|
-
todos
|
|
20884
|
+
todoCount: todos.length,
|
|
20885
|
+
statusCounts
|
|
20445
20886
|
}),
|
|
20446
20887
|
priority: "normal",
|
|
20447
|
-
senderSessionId: sessionId
|
|
20448
|
-
ttlMs: 6 * 60 * 60 * 1e3
|
|
20888
|
+
senderSessionId: sessionId
|
|
20449
20889
|
}).catch(() => {
|
|
20450
20890
|
});
|
|
20451
20891
|
}
|
|
@@ -20581,8 +21021,8 @@ function attachSessionKanbanMirror(context) {
|
|
|
20581
21021
|
const name = filename?.toString();
|
|
20582
21022
|
const currentPlanPath = context.meta["plan.path"];
|
|
20583
21023
|
const currentTaskPath = context.meta["task.path"];
|
|
20584
|
-
const planName = typeof currentPlanPath === "string" ?
|
|
20585
|
-
const taskName = typeof currentTaskPath === "string" ?
|
|
21024
|
+
const planName = typeof currentPlanPath === "string" ? basename13(currentPlanPath) : "";
|
|
21025
|
+
const taskName = typeof currentTaskPath === "string" ? basename13(currentTaskPath) : "";
|
|
20586
21026
|
if (name && name !== planName && name !== taskName) return;
|
|
20587
21027
|
if (timer) clearTimeout(timer);
|
|
20588
21028
|
timer = setTimeout(() => fireAndForget("refresh-files", refreshFiles()), 60);
|
|
@@ -21598,11 +22038,11 @@ var lintTool = {
|
|
|
21598
22038
|
}
|
|
21599
22039
|
};
|
|
21600
22040
|
async function detectLinter(cwd) {
|
|
21601
|
-
const { stat:
|
|
22041
|
+
const { stat: stat19 } = await import("node:fs/promises");
|
|
21602
22042
|
const checks = ["biome.json", ".eslintrc.json", "tslint.json", ".eslintrc.js", "tsconfig.json"];
|
|
21603
22043
|
for (const f of checks) {
|
|
21604
22044
|
try {
|
|
21605
|
-
await
|
|
22045
|
+
await stat19(`${cwd}/${f}`);
|
|
21606
22046
|
if (f.includes("biome")) return "biome";
|
|
21607
22047
|
if (f.includes("eslint")) return "eslint";
|
|
21608
22048
|
if (f.includes("tslint")) return "tslint";
|
|
@@ -21846,6 +22286,9 @@ var outdatedTool = {
|
|
|
21846
22286
|
// tool.confirm_needed flow on every invocation. M-1 originally
|
|
21847
22287
|
// fixed four sibling tools (mcp_control, shellcheck, shellcheck (scan mode),
|
|
21848
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",
|
|
21849
22292
|
mutating: true,
|
|
21850
22293
|
// Capability is outbound network — the tool only hits the package
|
|
21851
22294
|
// registry over HTTP, never touches the filesystem or runs shell.
|
|
@@ -22027,6 +22470,10 @@ var patchTool = {
|
|
|
22027
22470
|
useInstead: ["edit"]
|
|
22028
22471
|
},
|
|
22029
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",
|
|
22030
22477
|
mutating: true,
|
|
22031
22478
|
capabilities: ["fs.write"],
|
|
22032
22479
|
icon: "edit",
|
|
@@ -22093,8 +22540,8 @@ var patchTool = {
|
|
|
22093
22540
|
const before = beforeContents.get(target) ?? null;
|
|
22094
22541
|
const after = await readTextForTracking(target);
|
|
22095
22542
|
if (after === null || after === before) continue;
|
|
22096
|
-
const
|
|
22097
|
-
if (
|
|
22543
|
+
const stat19 = await fs27.stat(target).catch(() => null);
|
|
22544
|
+
if (stat19) ctx.recordRead?.(target, stat19.mtimeMs, "write", sha256hex(after));
|
|
22098
22545
|
ctx.session?.recordFileChange?.({
|
|
22099
22546
|
path: target,
|
|
22100
22547
|
action: before === null ? "created" : "modified",
|
|
@@ -22119,8 +22566,8 @@ var patchTool = {
|
|
|
22119
22566
|
var MAX_TRACKING_BYTES = 5 * 1024 * 1024;
|
|
22120
22567
|
async function readTextForTracking(absPath) {
|
|
22121
22568
|
try {
|
|
22122
|
-
const
|
|
22123
|
-
if (!
|
|
22569
|
+
const stat19 = await fs27.stat(absPath);
|
|
22570
|
+
if (!stat19.isFile() || stat19.size > MAX_TRACKING_BYTES) return null;
|
|
22124
22571
|
const buf = await fs27.readFile(absPath);
|
|
22125
22572
|
if (buf.includes(0)) return null;
|
|
22126
22573
|
return buf.toString("utf8");
|
|
@@ -22150,7 +22597,13 @@ function runPatch(args, cwd, signal) {
|
|
|
22150
22597
|
let stdout = "";
|
|
22151
22598
|
let stderr = "";
|
|
22152
22599
|
const env = { ...buildChildEnv8(), LANG: "C", LC_ALL: "C" };
|
|
22153
|
-
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
|
+
});
|
|
22154
22607
|
child.stdout?.on("data", (c) => {
|
|
22155
22608
|
stdout += c.toString();
|
|
22156
22609
|
});
|
|
@@ -22171,6 +22624,7 @@ function extractPatchedFiles(output) {
|
|
|
22171
22624
|
}
|
|
22172
22625
|
|
|
22173
22626
|
// src/plan.ts
|
|
22627
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
22174
22628
|
import {
|
|
22175
22629
|
addPlanItem,
|
|
22176
22630
|
clearPlan,
|
|
@@ -22178,20 +22632,20 @@ import {
|
|
|
22178
22632
|
formatPlan,
|
|
22179
22633
|
getPlanTemplate,
|
|
22180
22634
|
mutatePlan as mutatePlan2,
|
|
22635
|
+
mutateTasks as mutateTasks2,
|
|
22181
22636
|
removePlanItem,
|
|
22182
22637
|
setPlanItemStatus
|
|
22183
22638
|
} from "@wrongstack/core/storage";
|
|
22184
|
-
import {
|
|
22185
|
-
mutateTasks as mutateTasks2
|
|
22186
|
-
} from "@wrongstack/core/storage";
|
|
22187
22639
|
import { formatTaskList } from "@wrongstack/core/utils";
|
|
22188
|
-
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
22189
22640
|
var planTool = {
|
|
22190
22641
|
name: "plan",
|
|
22191
22642
|
category: "Session",
|
|
22192
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.',
|
|
22193
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.',
|
|
22194
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",
|
|
22195
22649
|
mutating: true,
|
|
22196
22650
|
capabilities: ["fs.write"],
|
|
22197
22651
|
icon: "plan",
|
|
@@ -22248,7 +22702,10 @@ var planTool = {
|
|
|
22248
22702
|
let planPath;
|
|
22249
22703
|
if (input.scope === "project") {
|
|
22250
22704
|
if (typeof sessionPlanPath === "string") {
|
|
22251
|
-
const lastSep = Math.max(
|
|
22705
|
+
const lastSep = Math.max(
|
|
22706
|
+
sessionPlanPath.lastIndexOf("/"),
|
|
22707
|
+
sessionPlanPath.lastIndexOf("\\")
|
|
22708
|
+
);
|
|
22252
22709
|
planPath = lastSep >= 0 ? sessionPlanPath.slice(0, lastSep + 1) + "backlog.plan.json" : "backlog.plan.json";
|
|
22253
22710
|
}
|
|
22254
22711
|
} else {
|
|
@@ -22285,7 +22742,11 @@ var planTool = {
|
|
|
22285
22742
|
case "start":
|
|
22286
22743
|
case "done": {
|
|
22287
22744
|
if (!input.target) {
|
|
22288
|
-
early = mkResult(
|
|
22745
|
+
early = mkResult(
|
|
22746
|
+
p,
|
|
22747
|
+
false,
|
|
22748
|
+
`${input.action} requires \`target\` (id|index|substring).`
|
|
22749
|
+
);
|
|
22289
22750
|
return p;
|
|
22290
22751
|
}
|
|
22291
22752
|
const next = setPlanItemStatus(
|
|
@@ -22313,7 +22774,11 @@ var planTool = {
|
|
|
22313
22774
|
}
|
|
22314
22775
|
case "promote": {
|
|
22315
22776
|
if (!input.target) {
|
|
22316
|
-
early = mkResult(
|
|
22777
|
+
early = mkResult(
|
|
22778
|
+
p,
|
|
22779
|
+
false,
|
|
22780
|
+
`${input.action} requires \`target\` (id|index|substring).`
|
|
22781
|
+
);
|
|
22317
22782
|
return p;
|
|
22318
22783
|
}
|
|
22319
22784
|
const derived = deriveTodosFromPlanItem(p, input.target, input.subtasks);
|
|
@@ -22356,7 +22821,11 @@ var planTool = {
|
|
|
22356
22821
|
return clearPlan(p);
|
|
22357
22822
|
case "taskify": {
|
|
22358
22823
|
if (!input.target) {
|
|
22359
|
-
early = mkResult(
|
|
22824
|
+
early = mkResult(
|
|
22825
|
+
p,
|
|
22826
|
+
false,
|
|
22827
|
+
"taskify requires `target` (plan item id|index|substring)."
|
|
22828
|
+
);
|
|
22360
22829
|
return p;
|
|
22361
22830
|
}
|
|
22362
22831
|
let itemIdx = -1;
|
|
@@ -22429,7 +22898,11 @@ var planTool = {
|
|
|
22429
22898
|
${formatTaskList(taskFile.tasks)}`
|
|
22430
22899
|
);
|
|
22431
22900
|
} catch (err) {
|
|
22432
|
-
return mkResult(
|
|
22901
|
+
return mkResult(
|
|
22902
|
+
plan,
|
|
22903
|
+
false,
|
|
22904
|
+
`taskify: task not saved \u2014 ${err instanceof Error ? err.message : String(err)}`
|
|
22905
|
+
);
|
|
22433
22906
|
}
|
|
22434
22907
|
}
|
|
22435
22908
|
return mkResult(plan, true, `Plan ${input.action} ok.`);
|
|
@@ -22506,9 +22979,9 @@ var readTool = {
|
|
|
22506
22979
|
}
|
|
22507
22980
|
const absPath = await safeResolveReal(input.path, ctx);
|
|
22508
22981
|
const shouldIncludeSymbols = input.includeSymbols === true || input.includeSymbols !== false && ctx.meta[ADVANCED_MODE_META_KEY] === true;
|
|
22509
|
-
let
|
|
22982
|
+
let stat19;
|
|
22510
22983
|
try {
|
|
22511
|
-
|
|
22984
|
+
stat19 = await fs28.stat(absPath);
|
|
22512
22985
|
} catch (err) {
|
|
22513
22986
|
const code = err.code;
|
|
22514
22987
|
if (code === "ENOENT") {
|
|
@@ -22527,7 +23000,7 @@ var readTool = {
|
|
|
22527
23000
|
cause: err
|
|
22528
23001
|
});
|
|
22529
23002
|
}
|
|
22530
|
-
if (!
|
|
23003
|
+
if (!stat19.isFile()) {
|
|
22531
23004
|
throw new FsError({
|
|
22532
23005
|
message: `read: "${input.path}" is not a regular file`,
|
|
22533
23006
|
code: "FS_READ_FAILED",
|
|
@@ -22535,23 +23008,23 @@ var readTool = {
|
|
|
22535
23008
|
context: { reason: "not-a-regular-file" }
|
|
22536
23009
|
});
|
|
22537
23010
|
}
|
|
22538
|
-
if (
|
|
23011
|
+
if (stat19.size > MAX_BYTES2) {
|
|
22539
23012
|
throw new FsError({
|
|
22540
|
-
message: `read: file too large (${
|
|
23013
|
+
message: `read: file too large (${stat19.size} bytes, limit ${MAX_BYTES2})`,
|
|
22541
23014
|
code: "FS_READ_FAILED",
|
|
22542
23015
|
path: absPath,
|
|
22543
|
-
context: { size:
|
|
23016
|
+
context: { size: stat19.size, limit: MAX_BYTES2, reason: "too-large" }
|
|
22544
23017
|
});
|
|
22545
23018
|
}
|
|
22546
23019
|
const offset = Math.max(1, input.offset ?? 1);
|
|
22547
23020
|
const limit = Math.max(0, Math.min(input.limit ?? 2e3, 5e3));
|
|
22548
23021
|
const prior = getReadRangeRecord(ctx, absPath);
|
|
22549
23022
|
const requestedEnd = prior ? Math.min(offset + limit - 1, prior.totalLines) : offset + limit - 1;
|
|
22550
|
-
if (input.mode !== "summary" && limit > 0 && prior && coversRange(prior,
|
|
22551
|
-
ctx.recordRead(absPath,
|
|
23023
|
+
if (input.mode !== "summary" && limit > 0 && prior && coversRange(prior, stat19.mtimeMs, offset, requestedEnd)) {
|
|
23024
|
+
ctx.recordRead(absPath, stat19.mtimeMs);
|
|
22552
23025
|
const symResult2 = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
|
|
22553
23026
|
return {
|
|
22554
|
-
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.]`,
|
|
22555
23028
|
total_lines: prior.totalLines,
|
|
22556
23029
|
encoding: "utf8",
|
|
22557
23030
|
truncated: requestedEnd < prior.totalLines,
|
|
@@ -22569,11 +23042,11 @@ var readTool = {
|
|
|
22569
23042
|
const allLines = text.split(/\r\n|\r|\n/);
|
|
22570
23043
|
const total = allLines.length;
|
|
22571
23044
|
if (input.mode === "summary") {
|
|
22572
|
-
ctx.recordRead(absPath,
|
|
22573
|
-
rememberReadRange(ctx, absPath,
|
|
23045
|
+
ctx.recordRead(absPath, stat19.mtimeMs, "user", contentHash);
|
|
23046
|
+
rememberReadRange(ctx, absPath, stat19.mtimeMs, total, 1, Math.min(total, 200));
|
|
22574
23047
|
const symResult2 = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
|
|
22575
23048
|
return {
|
|
22576
|
-
text: summarizeFile(input.path,
|
|
23049
|
+
text: summarizeFile(input.path, stat19.size, allLines),
|
|
22577
23050
|
total_lines: total,
|
|
22578
23051
|
encoding: "utf8",
|
|
22579
23052
|
truncated: total > 200,
|
|
@@ -22585,8 +23058,8 @@ var readTool = {
|
|
|
22585
23058
|
};
|
|
22586
23059
|
}
|
|
22587
23060
|
if (limit === 0) {
|
|
22588
|
-
ctx.recordRead(absPath,
|
|
22589
|
-
rememberReadRange(ctx, absPath,
|
|
23061
|
+
ctx.recordRead(absPath, stat19.mtimeMs, "user", contentHash);
|
|
23062
|
+
rememberReadRange(ctx, absPath, stat19.mtimeMs, total, 1, 0);
|
|
22590
23063
|
const symResult2 = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
|
|
22591
23064
|
return {
|
|
22592
23065
|
text: "",
|
|
@@ -22598,8 +23071,8 @@ var readTool = {
|
|
|
22598
23071
|
};
|
|
22599
23072
|
}
|
|
22600
23073
|
if (offset > total) {
|
|
22601
|
-
ctx.recordRead(absPath,
|
|
22602
|
-
rememberReadRange(ctx, absPath,
|
|
23074
|
+
ctx.recordRead(absPath, stat19.mtimeMs, "user", contentHash);
|
|
23075
|
+
rememberReadRange(ctx, absPath, stat19.mtimeMs, total, total + 1, total + 1);
|
|
22603
23076
|
const symResult2 = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
|
|
22604
23077
|
return {
|
|
22605
23078
|
text: `[offset ${offset} is past end of file "${input.path}" \u2014 file has ${total} line(s). Do not retry this offset.]`,
|
|
@@ -22614,8 +23087,8 @@ var readTool = {
|
|
|
22614
23087
|
const truncated = offset - 1 + slice.length < total;
|
|
22615
23088
|
const width = String(offset + slice.length - 1).length;
|
|
22616
23089
|
const numbered = slice.map((line, i) => `${String(offset + i).padStart(width, " ")}\u2192${line}`).join("\n");
|
|
22617
|
-
ctx.recordRead(absPath,
|
|
22618
|
-
rememberReadRange(ctx, absPath,
|
|
23090
|
+
ctx.recordRead(absPath, stat19.mtimeMs, "user", contentHash);
|
|
23091
|
+
rememberReadRange(ctx, absPath, stat19.mtimeMs, total, offset, offset + slice.length - 1);
|
|
22619
23092
|
const symResult = shouldIncludeSymbols ? await fetchSymbolsForFile(absPath, ctx, execOpts?.signal) : void 0;
|
|
22620
23093
|
return {
|
|
22621
23094
|
text: numbered,
|
|
@@ -22721,20 +23194,20 @@ ${interesting.join("\n")}` : "symbols/imports: (none detected)"
|
|
|
22721
23194
|
}
|
|
22722
23195
|
|
|
22723
23196
|
// src/replace.ts
|
|
22724
|
-
import { expectDefined as expectDefined8 } from "@wrongstack/core/utils";
|
|
22725
23197
|
import { spawn as spawn15 } from "node:child_process";
|
|
22726
23198
|
import * as fs29 from "node:fs/promises";
|
|
22727
23199
|
import * as path33 from "node:path";
|
|
23200
|
+
import { ToolValidationError as ToolValidationError6 } from "@wrongstack/core/types";
|
|
22728
23201
|
import {
|
|
22729
23202
|
atomicWrite as atomicWrite3,
|
|
22730
23203
|
buildChildEnv as buildChildEnv9,
|
|
22731
23204
|
compileGlob as compileGlob4,
|
|
22732
23205
|
detectNewlineStyle as detectNewlineStyle2,
|
|
23206
|
+
expectDefined as expectDefined8,
|
|
22733
23207
|
normalizeToLf as normalizeToLf2,
|
|
22734
23208
|
toStyle as toStyle2,
|
|
22735
23209
|
unifiedDiff as unifiedDiff2
|
|
22736
23210
|
} from "@wrongstack/core/utils";
|
|
22737
|
-
import { ToolValidationError as ToolValidationError6 } from "@wrongstack/core/types";
|
|
22738
23211
|
init_util();
|
|
22739
23212
|
var DEFAULT_IGNORE4 = ["node_modules", ".git", "dist", "build", ".next", "coverage"];
|
|
22740
23213
|
var replaceTool = {
|
|
@@ -22743,6 +23216,10 @@ var replaceTool = {
|
|
|
22743
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.",
|
|
22744
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.",
|
|
22745
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",
|
|
22746
23223
|
mutating: true,
|
|
22747
23224
|
capabilities: ["fs.write"],
|
|
22748
23225
|
icon: "edit",
|
|
@@ -22815,8 +23292,8 @@ var replaceTool = {
|
|
|
22815
23292
|
}
|
|
22816
23293
|
const rel = path33.relative(realRoot, realPath);
|
|
22817
23294
|
if (rel.startsWith("..") || path33.isAbsolute(rel)) continue;
|
|
22818
|
-
const
|
|
22819
|
-
if (!
|
|
23295
|
+
const stat19 = await fs29.stat(realPath).catch(() => null);
|
|
23296
|
+
if (!stat19?.isFile()) continue;
|
|
22820
23297
|
let content;
|
|
22821
23298
|
try {
|
|
22822
23299
|
const buf = await fs29.readFile(realPath);
|
|
@@ -22841,7 +23318,7 @@ var replaceTool = {
|
|
|
22841
23318
|
totalReplacements += count;
|
|
22842
23319
|
if (!dryRun) {
|
|
22843
23320
|
const newContent = toStyle2(newContentLf, style);
|
|
22844
|
-
await atomicWrite3(realPath, newContent, { mode:
|
|
23321
|
+
await atomicWrite3(realPath, newContent, { mode: stat19.mode & 511 });
|
|
22845
23322
|
const written = await fs29.stat(realPath).catch(() => null);
|
|
22846
23323
|
if (written) {
|
|
22847
23324
|
ctx.recordRead?.(realPath, written.mtimeMs, "write", sha256hex(newContent));
|
|
@@ -22881,8 +23358,8 @@ async function resolveFiles2(filesInput, ctx, extraGlob) {
|
|
|
22881
23358
|
const resolved = [];
|
|
22882
23359
|
for (const p of parts) {
|
|
22883
23360
|
const absPath = safeResolve(p, ctx);
|
|
22884
|
-
const
|
|
22885
|
-
if (
|
|
23361
|
+
const stat19 = await fs29.stat(absPath).catch(() => null);
|
|
23362
|
+
if (stat19?.isFile()) {
|
|
22886
23363
|
resolved.push(absPath);
|
|
22887
23364
|
}
|
|
22888
23365
|
}
|
|
@@ -22902,7 +23379,11 @@ async function globFiles(pattern, base, extraGlob) {
|
|
|
22902
23379
|
function checkRg() {
|
|
22903
23380
|
return new Promise((resolve17) => {
|
|
22904
23381
|
try {
|
|
22905
|
-
const p = spawn15("rg", ["--version"], {
|
|
23382
|
+
const p = spawn15("rg", ["--version"], {
|
|
23383
|
+
env: buildChildEnv9(),
|
|
23384
|
+
stdio: "ignore",
|
|
23385
|
+
windowsHide: true
|
|
23386
|
+
});
|
|
22906
23387
|
p.on("error", () => resolve17(false));
|
|
22907
23388
|
p.on("close", (code) => resolve17(code === 0));
|
|
22908
23389
|
} catch {
|
|
@@ -22953,8 +23434,8 @@ async function globNative(pattern, base, extraGlob) {
|
|
|
22953
23434
|
if (DEFAULT_IGNORE4.includes(e.name)) continue;
|
|
22954
23435
|
const full = path33.join(dir, e.name);
|
|
22955
23436
|
try {
|
|
22956
|
-
const
|
|
22957
|
-
if (
|
|
23437
|
+
const stat19 = await fs29.lstat(full);
|
|
23438
|
+
if (stat19.isSymbolicLink()) continue;
|
|
22958
23439
|
} catch {
|
|
22959
23440
|
continue;
|
|
22960
23441
|
}
|
|
@@ -23078,6 +23559,7 @@ var scaffoldTool = {
|
|
|
23078
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.",
|
|
23079
23560
|
permission: "confirm",
|
|
23080
23561
|
mutating: true,
|
|
23562
|
+
subjectKey: "name",
|
|
23081
23563
|
capabilities: ["fs.write.outside-project", "fs.write"],
|
|
23082
23564
|
icon: "scaffold",
|
|
23083
23565
|
timeoutMs: 3e4,
|
|
@@ -23152,15 +23634,17 @@ async function handleBuiltIn(name, templateFiles, cwd, ctx, dryRun, vars) {
|
|
|
23152
23634
|
output: dryRun ? `Would create ${filesCreated} files: ${files.join(", ")}` : `Created ${filesCreated} files: ${files.join(", ")}`
|
|
23153
23635
|
};
|
|
23154
23636
|
}
|
|
23637
|
+
function escapeRegexLiteral(value) {
|
|
23638
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
23639
|
+
}
|
|
23155
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());
|
|
23156
23643
|
let result = content;
|
|
23157
|
-
result = result.replace(/\{\{name\}\}/g,
|
|
23158
|
-
result = result.replace(
|
|
23159
|
-
/\{\{Name\}\}/g,
|
|
23160
|
-
name.replace(/(?:^|[-_\s]+)([a-z])/g, (_, c) => c.toUpperCase())
|
|
23161
|
-
);
|
|
23644
|
+
result = result.replace(/\{\{name\}\}/g, () => kebab);
|
|
23645
|
+
result = result.replace(/\{\{Name\}\}/g, () => pascal);
|
|
23162
23646
|
for (const [k, v] of Object.entries(vars)) {
|
|
23163
|
-
result = result.replace(new RegExp(`\\{\\{${k}\\}\\}`, "g"), v);
|
|
23647
|
+
result = result.replace(new RegExp(`\\{\\{${escapeRegexLiteral(k)}\\}\\}`, "g"), () => v);
|
|
23164
23648
|
}
|
|
23165
23649
|
return result;
|
|
23166
23650
|
}
|
|
@@ -24096,11 +24580,11 @@ var testTool = {
|
|
|
24096
24580
|
}
|
|
24097
24581
|
};
|
|
24098
24582
|
async function detectRunner(cwd) {
|
|
24099
|
-
const { stat:
|
|
24583
|
+
const { stat: stat19 } = await import("node:fs/promises");
|
|
24100
24584
|
const candidates = ["vitest.config.ts", "jest.config.js", ".mocharc.json"];
|
|
24101
24585
|
for (const f of candidates) {
|
|
24102
24586
|
try {
|
|
24103
|
-
await
|
|
24587
|
+
await stat19(path35.join(cwd, f));
|
|
24104
24588
|
if (f.includes("vitest")) return "vitest";
|
|
24105
24589
|
if (f.includes("jest")) return "jest";
|
|
24106
24590
|
if (f.includes("mocha")) return "mocha";
|
|
@@ -24497,12 +24981,19 @@ var toolSearchTool = {
|
|
|
24497
24981
|
};
|
|
24498
24982
|
|
|
24499
24983
|
// src/tool-use.ts
|
|
24984
|
+
import {
|
|
24985
|
+
GOVERNED_TOOL_EXECUTOR_META_KEY as GOVERNED_TOOL_EXECUTOR_META_KEY2
|
|
24986
|
+
} from "@wrongstack/core/types";
|
|
24500
24987
|
var toolUseTool = {
|
|
24501
24988
|
name: "tool_use",
|
|
24502
24989
|
category: "Meta",
|
|
24503
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.",
|
|
24504
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.",
|
|
24505
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",
|
|
24506
24997
|
mutating: true,
|
|
24507
24998
|
timeoutMs: 6e4,
|
|
24508
24999
|
capabilities: ["tool.mutate.any"],
|
|
@@ -24521,7 +25012,7 @@ var toolUseTool = {
|
|
|
24521
25012
|
},
|
|
24522
25013
|
required: ["tool"]
|
|
24523
25014
|
},
|
|
24524
|
-
async execute(input, ctx
|
|
25015
|
+
async execute(input, ctx) {
|
|
24525
25016
|
const start = Date.now();
|
|
24526
25017
|
if (!input?.tool) {
|
|
24527
25018
|
return {
|
|
@@ -24540,6 +25031,14 @@ var toolUseTool = {
|
|
|
24540
25031
|
executionMs: Date.now() - start
|
|
24541
25032
|
};
|
|
24542
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
|
+
}
|
|
24543
25042
|
if (tool.permission === "deny") {
|
|
24544
25043
|
return {
|
|
24545
25044
|
tool: input.tool,
|
|
@@ -24548,12 +25047,21 @@ var toolUseTool = {
|
|
|
24548
25047
|
executionMs: Date.now() - start
|
|
24549
25048
|
};
|
|
24550
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
|
+
}
|
|
24551
25059
|
try {
|
|
24552
|
-
const result = await
|
|
25060
|
+
const result = await governedExecute(input.tool, input.input ?? {});
|
|
24553
25061
|
return {
|
|
24554
25062
|
tool: input.tool,
|
|
24555
|
-
success:
|
|
24556
|
-
result,
|
|
25063
|
+
success: result.success,
|
|
25064
|
+
...result.success ? { result: result.result } : { error: result.error ?? "nested tool failed" },
|
|
24557
25065
|
executionMs: Date.now() - start
|
|
24558
25066
|
};
|
|
24559
25067
|
} catch (e) {
|
|
@@ -24569,10 +25077,12 @@ var toolUseTool = {
|
|
|
24569
25077
|
|
|
24570
25078
|
// src/tree.ts
|
|
24571
25079
|
init_util();
|
|
24572
|
-
import { DEFAULT_WALK_IGNORE_DIRS as DEFAULT_WALK_IGNORE_DIRS4, expectDefined as expectDefined10 } from "@wrongstack/core/utils";
|
|
24573
25080
|
import * as fs32 from "node:fs/promises";
|
|
24574
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";
|
|
24575
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;
|
|
24576
25086
|
var treeTool = {
|
|
24577
25087
|
name: "tree",
|
|
24578
25088
|
category: "Filesystem",
|
|
@@ -24616,6 +25126,12 @@ var treeTool = {
|
|
|
24616
25126
|
show_hidden: {
|
|
24617
25127
|
type: "boolean",
|
|
24618
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
|
|
24619
25135
|
}
|
|
24620
25136
|
}
|
|
24621
25137
|
},
|
|
@@ -24629,7 +25145,7 @@ var treeTool = {
|
|
|
24629
25145
|
if (!final) throw new Error("tree: stream ended without final event");
|
|
24630
25146
|
return final;
|
|
24631
25147
|
},
|
|
24632
|
-
async *executeStream(input, ctx) {
|
|
25148
|
+
async *executeStream(input, ctx, opts) {
|
|
24633
25149
|
const basePath = input.path ? safeResolve(input.path, ctx) : ctx.cwd;
|
|
24634
25150
|
const maxDepth = input.depth ?? 3;
|
|
24635
25151
|
const showFiles = input.show_files ?? true;
|
|
@@ -24637,8 +25153,17 @@ var treeTool = {
|
|
|
24637
25153
|
const showHidden = input.show_hidden ?? false;
|
|
24638
25154
|
const exclude = /* @__PURE__ */ new Set([...DEFAULT_IGNORE5, ...input.exclude ?? []]);
|
|
24639
25155
|
const filterGlob = input.glob;
|
|
25156
|
+
const maxEntries = input.max_entries ?? DEFAULT_MAX_ENTRIES2;
|
|
25157
|
+
const signal = opts?.signal ?? ctx.signal ?? new AbortController().signal;
|
|
24640
25158
|
const lines = [basePath];
|
|
24641
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
|
+
};
|
|
24642
25167
|
const queue = [];
|
|
24643
25168
|
const FLUSH_EVERY = 200;
|
|
24644
25169
|
let lastEmittedTotal = 0;
|
|
@@ -24665,12 +25190,19 @@ var treeTool = {
|
|
|
24665
25190
|
isLast: true,
|
|
24666
25191
|
totalFiles: totals.totalFiles,
|
|
24667
25192
|
totalDirs: totals.totalDirs,
|
|
24668
|
-
onProgress: tickProgress
|
|
25193
|
+
onProgress: tickProgress,
|
|
25194
|
+
retention,
|
|
25195
|
+
signal
|
|
24669
25196
|
});
|
|
24670
25197
|
let walkDone = false;
|
|
24671
|
-
walkPromise.
|
|
24672
|
-
|
|
24673
|
-
|
|
25198
|
+
void walkPromise.then(
|
|
25199
|
+
() => {
|
|
25200
|
+
walkDone = true;
|
|
25201
|
+
},
|
|
25202
|
+
() => {
|
|
25203
|
+
walkDone = true;
|
|
25204
|
+
}
|
|
25205
|
+
);
|
|
24674
25206
|
while (!walkDone || queue.length > 0) {
|
|
24675
25207
|
if (queue.length > 0) {
|
|
24676
25208
|
yield expectDefined10(queue.shift());
|
|
@@ -24693,13 +25225,15 @@ var treeTool = {
|
|
|
24693
25225
|
tree: lines.join("\n"),
|
|
24694
25226
|
total_files: totals.totalFiles.value,
|
|
24695
25227
|
total_dirs: totals.totalDirs.value,
|
|
24696
|
-
truncated:
|
|
25228
|
+
truncated: retention.truncated,
|
|
24697
25229
|
path: basePath
|
|
24698
25230
|
}
|
|
24699
25231
|
};
|
|
24700
25232
|
}
|
|
24701
25233
|
};
|
|
24702
25234
|
async function walkDir(dir, depth, opts) {
|
|
25235
|
+
opts.signal.throwIfAborted();
|
|
25236
|
+
if (opts.retention.truncated) return;
|
|
24703
25237
|
const entries = await fs32.readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
24704
25238
|
const filtered = entries.filter((e) => {
|
|
24705
25239
|
if (!opts.showHidden && e.name.startsWith(".")) return false;
|
|
@@ -24719,6 +25253,8 @@ async function walkDir(dir, depth, opts) {
|
|
|
24719
25253
|
return a.name.localeCompare(b.name);
|
|
24720
25254
|
});
|
|
24721
25255
|
for (let i = 0; i < items.length; i++) {
|
|
25256
|
+
opts.signal.throwIfAborted();
|
|
25257
|
+
if (opts.retention.truncated) return;
|
|
24722
25258
|
const entry = items[i];
|
|
24723
25259
|
if (!entry) continue;
|
|
24724
25260
|
const isLast = i === items.length - 1;
|
|
@@ -24727,7 +25263,15 @@ async function walkDir(dir, depth, opts) {
|
|
|
24727
25263
|
const displayName = entry.name + (entry.isDirectory() ? "/" : "");
|
|
24728
25264
|
if (!opts.showDirs && entry.isDirectory()) continue;
|
|
24729
25265
|
if (!opts.showFiles && entry.isFile()) continue;
|
|
24730
|
-
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;
|
|
24731
25275
|
if (entry.isDirectory() && (opts.maxDepth === 0 || depth < opts.maxDepth)) {
|
|
24732
25276
|
const childPrefix = opts.prefix + connector;
|
|
24733
25277
|
await walkDir(path36.join(dir, entry.name), depth + 1, {
|
|
@@ -24842,11 +25386,11 @@ var typecheckTool = {
|
|
|
24842
25386
|
}
|
|
24843
25387
|
};
|
|
24844
25388
|
async function findTsConfig(cwd) {
|
|
24845
|
-
const { stat:
|
|
25389
|
+
const { stat: stat19 } = await import("node:fs/promises");
|
|
24846
25390
|
const candidates = ["tsconfig.json", "tsconfig.base.json"];
|
|
24847
25391
|
for (const f of candidates) {
|
|
24848
25392
|
try {
|
|
24849
|
-
const s = await
|
|
25393
|
+
const s = await stat19(path37.join(cwd, f));
|
|
24850
25394
|
if (s.isFile()) return path37.join(cwd, f);
|
|
24851
25395
|
} catch {
|
|
24852
25396
|
}
|
|
@@ -24921,12 +25465,12 @@ async function prepareWrite(input, ctx) {
|
|
|
24921
25465
|
let existed = false;
|
|
24922
25466
|
let prev = "";
|
|
24923
25467
|
try {
|
|
24924
|
-
const
|
|
24925
|
-
existed =
|
|
25468
|
+
const stat19 = await fs33.stat(absPath);
|
|
25469
|
+
existed = stat19.isFile();
|
|
24926
25470
|
if (existed) {
|
|
24927
25471
|
if (!ctx.hasRead(absPath)) {
|
|
24928
25472
|
prev = await fs33.readFile(absPath, "utf8");
|
|
24929
|
-
ctx.recordRead(absPath,
|
|
25473
|
+
ctx.recordRead(absPath, stat19.mtimeMs, "write", sha256hex(prev));
|
|
24930
25474
|
} else {
|
|
24931
25475
|
prev = await fs33.readFile(absPath, "utf8");
|
|
24932
25476
|
}
|
|
@@ -24943,8 +25487,8 @@ async function finishWrite(input, ctx, prepared, signal) {
|
|
|
24943
25487
|
await atomicWrite5(prepared.absPath, input.content);
|
|
24944
25488
|
const diff = prepared.existed ? unifiedDiff3(prepared.prev, input.content, { fromFile: input.path, toFile: input.path }) : `+++ ${input.path}
|
|
24945
25489
|
+ (new file, ${input.content.split("\n").length} lines)`;
|
|
24946
|
-
const
|
|
24947
|
-
ctx.recordRead(prepared.absPath,
|
|
25490
|
+
const stat19 = await fs33.stat(prepared.absPath);
|
|
25491
|
+
ctx.recordRead(prepared.absPath, stat19.mtimeMs, "write", sha256hex(input.content));
|
|
24948
25492
|
ctx.session.recordFileChange({
|
|
24949
25493
|
path: prepared.absPath,
|
|
24950
25494
|
action: prepared.existed ? "modified" : "created",
|
|
@@ -26132,9 +26676,9 @@ async function walk(root, dir, out) {
|
|
|
26132
26676
|
} else if (e.isFile()) {
|
|
26133
26677
|
if (e.name === "SKILL.md" || e.name === "SKILL.save.md") continue;
|
|
26134
26678
|
try {
|
|
26135
|
-
const
|
|
26679
|
+
const stat19 = await fs34.stat(fullPath);
|
|
26136
26680
|
const rel = path38.relative(root, fullPath).split(path38.sep).join("/");
|
|
26137
|
-
out.push({ path: rel, bytes:
|
|
26681
|
+
out.push({ path: rel, bytes: stat19.size });
|
|
26138
26682
|
} catch {
|
|
26139
26683
|
}
|
|
26140
26684
|
}
|