opencode-reviewer 0.4.1 → 0.4.2
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/README.md +74 -185
- package/dist/tui.js +876 -436
- package/package.json +10 -2
package/dist/tui.js
CHANGED
|
@@ -768,10 +768,10 @@ var FileScope = class {
|
|
|
768
768
|
var fileAccess = new FileAccess();
|
|
769
769
|
|
|
770
770
|
// src/context.ts
|
|
771
|
-
async function loadInvocation(request,
|
|
771
|
+
async function loadInvocation(request, reader2, signal) {
|
|
772
772
|
signal.throwIfAborted();
|
|
773
773
|
if (!request.tool) throw new Error("Pending tool linkage unavailable");
|
|
774
|
-
const message = await
|
|
774
|
+
const message = await reader2.message(request.sessionID, request.tool.messageID, signal);
|
|
775
775
|
signal.throwIfAborted();
|
|
776
776
|
if (!message || message.info.role !== "assistant" || message.info.sessionID !== request.sessionID || message.info.id !== request.tool.messageID) {
|
|
777
777
|
throw new Error("Pending tool message unavailable or mismatched");
|
|
@@ -787,6 +787,7 @@ async function loadInvocation(request, reader, signal) {
|
|
|
787
787
|
info: message.info,
|
|
788
788
|
tool: part.tool,
|
|
789
789
|
input: part.state.input,
|
|
790
|
+
linkage: { sessionID: request.sessionID, messageID: request.tool.messageID, callID: request.tool.callID },
|
|
790
791
|
location: {
|
|
791
792
|
instanceDirectory: typeof invocation?.cwd === "string" && path2.isAbsolute(invocation.cwd) ? invocation.cwd : null,
|
|
792
793
|
instanceWorktree: typeof invocation?.root === "string" && path2.isAbsolute(invocation.root) ? invocation.root : null
|
|
@@ -824,27 +825,33 @@ function findUserPrompt(messages, sessionID) {
|
|
|
824
825
|
function latestUserPrompt(messages, sessionID) {
|
|
825
826
|
return findUserPrompt(messages, sessionID)?.text ?? null;
|
|
826
827
|
}
|
|
827
|
-
async function
|
|
828
|
+
async function nativeInvocation(request, reader2, signal, kind) {
|
|
829
|
+
try {
|
|
830
|
+
return await loadInvocation(request, reader2, signal);
|
|
831
|
+
} catch (error) {
|
|
832
|
+
signal.throwIfAborted();
|
|
833
|
+
throw new Error(`Pending native ${kind} message unavailable or arguments mismatched`, { cause: error });
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
function invocationFor(request, verified) {
|
|
837
|
+
if (verified.linkage.sessionID !== request.sessionID || verified.linkage.messageID !== request.tool?.messageID || verified.linkage.callID !== request.tool?.callID) throw new Error("Verified invocation linkage mismatched");
|
|
838
|
+
return verified;
|
|
839
|
+
}
|
|
840
|
+
async function loadContext(request, reader2, signal, scope = fileAccess.scope(signal), verified) {
|
|
841
|
+
signal.throwIfAborted();
|
|
828
842
|
if (request.permission !== "bash" && request.permission !== "external_directory") return null;
|
|
829
843
|
if (!request.tool) {
|
|
830
844
|
if (request.permission === "external_directory") return null;
|
|
831
845
|
throw new Error("Native shell tool context unavailable");
|
|
832
846
|
}
|
|
833
|
-
const
|
|
834
|
-
if (
|
|
835
|
-
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
if (part?.type === "tool" && part.tool !== "bash" && request.permission === "external_directory") return null;
|
|
839
|
-
if (!part || part.type !== "tool" || part.sessionID !== request.sessionID || part.messageID !== request.tool.messageID || part.tool !== "bash" || part.state.status !== "running") {
|
|
840
|
-
throw new Error("Pending native shell arguments unavailable");
|
|
841
|
-
}
|
|
842
|
-
const { command, workdir } = part.state.input;
|
|
847
|
+
const invocation = verified ? invocationFor(request, verified) : await nativeInvocation(request, reader2, signal, "shell");
|
|
848
|
+
if (invocation.tool !== "bash" && request.permission === "external_directory") return null;
|
|
849
|
+
if (invocation.tool !== "bash") throw new Error("Pending native shell arguments unavailable");
|
|
850
|
+
const { command, workdir } = invocation.input;
|
|
851
|
+
if (typeof command === "string" && command.length > 16 * 1024 * 1024) throw new Error("Shell command exceeds the 16 MiB preprocessing limit");
|
|
843
852
|
if (typeof command !== "string" || !command.trim()) throw new Error("Shell command unavailable");
|
|
844
853
|
const limitations = [];
|
|
845
|
-
const
|
|
846
|
-
const instanceDirectory = typeof invocation?.cwd === "string" && path2.isAbsolute(invocation.cwd) ? invocation.cwd : null;
|
|
847
|
-
const instanceWorktree = typeof invocation?.root === "string" && path2.isAbsolute(invocation.root) ? invocation.root : null;
|
|
854
|
+
const { instanceDirectory, instanceWorktree } = invocation.location;
|
|
848
855
|
const invalidWorkdir = workdir !== void 0 && typeof workdir !== "string";
|
|
849
856
|
const requestedWorkdir = typeof workdir === "string" && workdir ? workdir : null;
|
|
850
857
|
const cwd = invalidWorkdir ? null : requestedWorkdir && path2.isAbsolute(requestedWorkdir) ? requestedWorkdir : instanceDirectory ? path2.resolve(instanceDirectory, requestedWorkdir ?? ".") : null;
|
|
@@ -859,7 +866,7 @@ async function loadContext(request, reader, signal, scope = fileAccess.scope(sig
|
|
|
859
866
|
cwdSource: invalidWorkdir ? "unavailable" : requestedWorkdir && path2.isAbsolute(requestedWorkdir) ? "absolute tool.workdir" : instanceDirectory ? requestedWorkdir ? "tool.workdir relative to assistant.path.cwd" : "assistant.path.cwd" : "unavailable",
|
|
860
867
|
canonicalCwd: null
|
|
861
868
|
};
|
|
862
|
-
const conversation = await loadConversationContext(request,
|
|
869
|
+
const conversation = await loadConversationContext(request, reader2, signal);
|
|
863
870
|
if (cwd) {
|
|
864
871
|
const canonical = await scope.canonical(cwd);
|
|
865
872
|
execution.canonicalCwd = canonical.path;
|
|
@@ -885,28 +892,20 @@ function permissionContext(request, clone = true) {
|
|
|
885
892
|
tool: request.tool ? { ...request.tool } : null
|
|
886
893
|
};
|
|
887
894
|
}
|
|
888
|
-
async function loadEditContext(request,
|
|
895
|
+
async function loadEditContext(request, reader2, signal, verified) {
|
|
889
896
|
signal.throwIfAborted();
|
|
890
897
|
if (request.permission !== "edit" || !request.tool) throw new Error("Native edit tool context unavailable");
|
|
891
|
-
const
|
|
892
|
-
if (
|
|
893
|
-
throw new Error("Pending native edit message unavailable");
|
|
894
|
-
}
|
|
895
|
-
const part = message.parts.find((p) => p.type === "tool" && p.callID === request.tool.callID);
|
|
896
|
-
if (!part || part.type !== "tool" || part.sessionID !== request.sessionID || part.messageID !== request.tool.messageID || part.state.status !== "running" || part.tool !== "edit" && part.tool !== "write" && part.tool !== "apply_patch") {
|
|
898
|
+
const invocation = verified ? invocationFor(request, verified) : await nativeInvocation(request, reader2, signal, "edit");
|
|
899
|
+
if (invocation.tool !== "edit" && invocation.tool !== "write" && invocation.tool !== "apply_patch") {
|
|
897
900
|
throw new Error("Pending native edit arguments unavailable or unsupported tool");
|
|
898
901
|
}
|
|
899
|
-
const
|
|
900
|
-
const
|
|
901
|
-
instanceDirectory: typeof invocation?.cwd === "string" && path2.isAbsolute(invocation.cwd) ? invocation.cwd : null,
|
|
902
|
-
instanceWorktree: typeof invocation?.root === "string" && path2.isAbsolute(invocation.root) ? invocation.root : null
|
|
903
|
-
};
|
|
904
|
-
const conversation = await loadConversationContext(request, reader, signal);
|
|
902
|
+
const location = invocation.location;
|
|
903
|
+
const conversation = await loadConversationContext(request, reader2, signal);
|
|
905
904
|
if (!location.instanceDirectory) conversation.limitations.push("Edit invocation directory unavailable; session origin is not substituted.");
|
|
906
905
|
if (!location.instanceWorktree) conversation.limitations.push("Edit invocation worktree unavailable.");
|
|
907
|
-
return { ...conversation, kind: "edit", tool:
|
|
906
|
+
return { ...conversation, kind: "edit", tool: invocation.tool, location, permission: permissionContext(request, false) };
|
|
908
907
|
}
|
|
909
|
-
async function loadConversationContext(request,
|
|
908
|
+
async function loadConversationContext(request, reader2, signal) {
|
|
910
909
|
signal.throwIfAborted();
|
|
911
910
|
const limitations = [];
|
|
912
911
|
let current;
|
|
@@ -915,7 +914,7 @@ async function loadConversationContext(request, reader, signal) {
|
|
|
915
914
|
const contextEnd = performance.now() + Math.min(5e3, remainingTime(signal) / 3);
|
|
916
915
|
const contextMs = () => Math.max(0, contextEnd - performance.now());
|
|
917
916
|
const ancestry = async (contextSignal) => {
|
|
918
|
-
const loaded = await
|
|
917
|
+
const loaded = await reader2.session(request.sessionID, contextSignal);
|
|
919
918
|
contextSignal.throwIfAborted();
|
|
920
919
|
if (loaded?.id !== request.sessionID) throw new Error("Session mismatch");
|
|
921
920
|
current = loaded;
|
|
@@ -926,13 +925,13 @@ async function loadConversationContext(request, reader, signal) {
|
|
|
926
925
|
if (seen.has(ancestor.id) || seen.size >= 16) throw new Error("Parent chain incomplete");
|
|
927
926
|
seen.add(ancestor.id);
|
|
928
927
|
const expected = ancestor.parentID;
|
|
929
|
-
const parent = await
|
|
928
|
+
const parent = await reader2.session(expected, contextSignal);
|
|
930
929
|
contextSignal.throwIfAborted();
|
|
931
930
|
if (parent?.id !== expected) throw new Error("Parent session unavailable");
|
|
932
931
|
ancestor = parent;
|
|
933
932
|
}
|
|
934
933
|
root = ancestor;
|
|
935
|
-
const messages = await
|
|
934
|
+
const messages = await reader2.messages(root.id, contextSignal);
|
|
936
935
|
contextSignal.throwIfAborted();
|
|
937
936
|
prompt2 = latestUserPrompt(messages, root.id);
|
|
938
937
|
};
|
|
@@ -945,7 +944,7 @@ async function loadConversationContext(request, reader, signal) {
|
|
|
945
944
|
let projects = [];
|
|
946
945
|
if (current || root) {
|
|
947
946
|
try {
|
|
948
|
-
projects = await withDeadline(signal, contextMs(), (contextSignal) =>
|
|
947
|
+
projects = await withDeadline(signal, contextMs(), (contextSignal) => reader2.projects(contextSignal), "Project context lookup");
|
|
949
948
|
} catch (error) {
|
|
950
949
|
signal.throwIfAborted();
|
|
951
950
|
limitations.push(error instanceof DeadlineError ? error.message : "OpenCode project metadata unavailable.");
|
|
@@ -1122,7 +1121,9 @@ function estimatedUsage(body, requestedModel, pricing) {
|
|
|
1122
1121
|
const result = { input: usage.prompt_tokens, output: usage.completion_tokens };
|
|
1123
1122
|
const prices = pricing?.(typeof body.model === "string" ? body.model : requestedModel);
|
|
1124
1123
|
if (!prices) return result;
|
|
1125
|
-
const
|
|
1124
|
+
const rawDetails = usage.prompt_tokens_details;
|
|
1125
|
+
if (rawDetails !== void 0 && (!rawDetails || typeof rawDetails !== "object" || Array.isArray(rawDetails))) return result;
|
|
1126
|
+
const details = record(rawDetails);
|
|
1126
1127
|
const cached = details.cached_tokens === void 0 ? 0 : details.cached_tokens;
|
|
1127
1128
|
const written = details.cache_write_tokens === void 0 ? 0 : details.cache_write_tokens;
|
|
1128
1129
|
if (!count(cached) || !count(written) || cached + written > result.input || usage.cache_creation_input_tokens || usage.cache_read_input_tokens) return result;
|
|
@@ -1149,7 +1150,8 @@ function usageAttempt(baseURL, requestedModel, pricing, onUsage) {
|
|
|
1149
1150
|
finalized = true;
|
|
1150
1151
|
if (usage) {
|
|
1151
1152
|
try {
|
|
1152
|
-
onUsage?.({ ...usage })
|
|
1153
|
+
void Promise.resolve(onUsage?.({ ...usage })).catch(() => {
|
|
1154
|
+
});
|
|
1153
1155
|
} catch {
|
|
1154
1156
|
}
|
|
1155
1157
|
}
|
|
@@ -1174,7 +1176,7 @@ function sumUsage(previous, next) {
|
|
|
1174
1176
|
}
|
|
1175
1177
|
function usageText(usage) {
|
|
1176
1178
|
return [
|
|
1177
|
-
...count(usage.input) && count(usage.output) ? [`
|
|
1179
|
+
...count(usage.input) && count(usage.output) ? [`token: ${usage.input} in ${usage.output} out`] : [],
|
|
1178
1180
|
...usage.cost === void 0 ? [] : [`cost: $${usage.cost.toFixed(4)}`]
|
|
1179
1181
|
].join("\n");
|
|
1180
1182
|
}
|
|
@@ -1523,13 +1525,13 @@ function cancelBody(response) {
|
|
|
1523
1525
|
}
|
|
1524
1526
|
async function readBody(response, signal, consume) {
|
|
1525
1527
|
if (!response.body) throw new Error("Reviewer returned an empty HTTP response");
|
|
1526
|
-
const
|
|
1528
|
+
const reader2 = response.body.getReader();
|
|
1527
1529
|
try {
|
|
1528
1530
|
while (true) {
|
|
1529
1531
|
reviewStage(signal, "Reviewer response body");
|
|
1530
1532
|
let part;
|
|
1531
1533
|
try {
|
|
1532
|
-
part = await abortable(
|
|
1534
|
+
part = await abortable(reader2.read(), signal);
|
|
1533
1535
|
} catch {
|
|
1534
1536
|
signal.throwIfAborted();
|
|
1535
1537
|
throw new Error("Reviewer response read failed");
|
|
@@ -1540,10 +1542,10 @@ async function readBody(response, signal, consume) {
|
|
|
1540
1542
|
}
|
|
1541
1543
|
} finally {
|
|
1542
1544
|
try {
|
|
1543
|
-
void
|
|
1545
|
+
void reader2.cancel().catch(() => {
|
|
1544
1546
|
});
|
|
1545
1547
|
} finally {
|
|
1546
|
-
|
|
1548
|
+
reader2.releaseLock();
|
|
1547
1549
|
}
|
|
1548
1550
|
}
|
|
1549
1551
|
}
|
|
@@ -1644,7 +1646,8 @@ async function review(evidence, config, signal, fetcher = fetch, environment = p
|
|
|
1644
1646
|
const progress = (phase, preview) => {
|
|
1645
1647
|
if (signal.aborted) return;
|
|
1646
1648
|
try {
|
|
1647
|
-
onProgress?.({ attempt, phase, ...preview ? { preview: { ...preview } } : {} })
|
|
1649
|
+
void Promise.resolve(onProgress?.({ attempt, phase, ...preview ? { preview: { ...preview } } : {} })).catch(() => {
|
|
1650
|
+
});
|
|
1648
1651
|
} catch {
|
|
1649
1652
|
}
|
|
1650
1653
|
};
|
|
@@ -1697,7 +1700,7 @@ async function review(evidence, config, signal, fetcher = fetch, environment = p
|
|
|
1697
1700
|
const choices = object(envelope) ? envelope.choices : void 0;
|
|
1698
1701
|
if (!Array.isArray(choices) || choices.length !== 1) throw new Error("Reviewer API must return one completion");
|
|
1699
1702
|
const choice = choices[0], message = choice?.message;
|
|
1700
|
-
if (!object(message) || typeof message.content !== "string" || !textMetadata(message) || object(envelope) && envelope.error != null || choice.index !== void 0 && choice.index !== 0 || choice.finish_reason !== void 0 && choice.finish_reason !== "stop") {
|
|
1703
|
+
if (!object(choice) || !object(message) || typeof message.content !== "string" || !textMetadata(message) || object(envelope) && envelope.error != null || choice.error != null || choice.index !== void 0 && choice.index !== 0 || choice.finish_reason !== void 0 && choice.finish_reason !== "stop") {
|
|
1701
1704
|
throw new Error("Reviewer API did not return a text assessment");
|
|
1702
1705
|
}
|
|
1703
1706
|
if (message.content) diagnose?.("first-content");
|
|
@@ -1896,7 +1899,7 @@ var Controller = class {
|
|
|
1896
1899
|
for (const entry of this.entries.values()) if (entry.root === root) this.suspend(entry);
|
|
1897
1900
|
this.publish();
|
|
1898
1901
|
}
|
|
1899
|
-
suspend(entry) {
|
|
1902
|
+
suspend(entry, reason = "mode") {
|
|
1900
1903
|
const id = entry.view.request.id;
|
|
1901
1904
|
const state = entry.view.autoApproval?.status;
|
|
1902
1905
|
if (state) this.manual.set(id, state === "failed" || state === "allowing" ? "failed" : "cancelled");
|
|
@@ -1904,6 +1907,7 @@ var Controller = class {
|
|
|
1904
1907
|
this.clearProgress(entry);
|
|
1905
1908
|
entry.approvalAbort?.abort();
|
|
1906
1909
|
entry.abort.abort();
|
|
1910
|
+
entry.suspension = reason;
|
|
1907
1911
|
entry.view = { request: entry.view.request, status: "suspended" };
|
|
1908
1912
|
}
|
|
1909
1913
|
/** Called only after the assessment has actually been rendered, or to hide it. */
|
|
@@ -2106,7 +2110,7 @@ var Controller = class {
|
|
|
2106
2110
|
}
|
|
2107
2111
|
} catch {
|
|
2108
2112
|
if (active()) {
|
|
2109
|
-
this.suspend(entry);
|
|
2113
|
+
this.suspend(entry, "unavailable");
|
|
2110
2114
|
this.publish();
|
|
2111
2115
|
}
|
|
2112
2116
|
return null;
|
|
@@ -2157,7 +2161,7 @@ var Controller = class {
|
|
|
2157
2161
|
for (const request of requests) {
|
|
2158
2162
|
const entry = this.entries.get(request.id);
|
|
2159
2163
|
if (entry && !entry.root) entry.gateRevision = this.version;
|
|
2160
|
-
if (entry?.view.status === "suspended" && (!entry.root || this.modes?.enabled(entry.root))) {
|
|
2164
|
+
if (entry?.view.status === "suspended" && (entry.suspension === "unavailable" || !entry.root || this.modes?.enabled(entry.root))) {
|
|
2161
2165
|
this.entries.delete(request.id);
|
|
2162
2166
|
}
|
|
2163
2167
|
this.asked(request);
|
|
@@ -2199,8 +2203,7 @@ init_define_REVIEW_PROMPTS();
|
|
|
2199
2203
|
|
|
2200
2204
|
// src/evidence.ts
|
|
2201
2205
|
init_define_REVIEW_PROMPTS();
|
|
2202
|
-
|
|
2203
|
-
import path5 from "node:path";
|
|
2206
|
+
import path6 from "node:path";
|
|
2204
2207
|
import { constants as constants2 } from "node:fs";
|
|
2205
2208
|
|
|
2206
2209
|
// src/files.ts
|
|
@@ -2244,47 +2247,199 @@ var FileLimit = class {
|
|
|
2244
2247
|
function omittedFile(filename) {
|
|
2245
2248
|
return `[!] File ${JSON.stringify(filename)} not included in context.`;
|
|
2246
2249
|
}
|
|
2247
|
-
|
|
2250
|
+
var DiffBudget = class {
|
|
2251
|
+
constructor(limits = { bytes: 16 * 1024 * 1024, lines: 65536, ms: 250 }, now = () => performance.now()) {
|
|
2252
|
+
this.now = now;
|
|
2253
|
+
this.bytes = limits.bytes;
|
|
2254
|
+
this.lines = limits.lines;
|
|
2255
|
+
this.ms = limits.ms;
|
|
2256
|
+
}
|
|
2257
|
+
now;
|
|
2258
|
+
bytes;
|
|
2259
|
+
lines;
|
|
2260
|
+
ms;
|
|
2261
|
+
exhausted = false;
|
|
2262
|
+
check(signal, end) {
|
|
2263
|
+
reviewStage(signal, "Evidence collection");
|
|
2264
|
+
if (this.now() >= end) {
|
|
2265
|
+
this.exhausted = true;
|
|
2266
|
+
return false;
|
|
2267
|
+
}
|
|
2268
|
+
return true;
|
|
2269
|
+
}
|
|
2270
|
+
};
|
|
2271
|
+
function diffDelta(diff, signal, budget = new DiffBudget()) {
|
|
2272
|
+
reviewStage(signal, "Evidence collection");
|
|
2248
2273
|
if (typeof diff !== "string") return;
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
const
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
if (
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
if (
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2274
|
+
if (diff.length > budget.bytes || budget.ms <= 0) {
|
|
2275
|
+
budget.exhausted = true;
|
|
2276
|
+
return;
|
|
2277
|
+
}
|
|
2278
|
+
const startTime = budget.now(), endTime = startTime + budget.ms;
|
|
2279
|
+
try {
|
|
2280
|
+
const bytes = Buffer.byteLength(diff);
|
|
2281
|
+
const available = budget.bytes;
|
|
2282
|
+
budget.bytes = Math.max(0, available - bytes);
|
|
2283
|
+
if (bytes > available) {
|
|
2284
|
+
budget.exhausted = true;
|
|
2285
|
+
return;
|
|
2286
|
+
}
|
|
2287
|
+
let old = 0, next = 0, added = 0, removed = 0, hunks = 0;
|
|
2288
|
+
for (let start = 0; start < diff.length; ) {
|
|
2289
|
+
if (!budget.check(signal, endTime)) return;
|
|
2290
|
+
if (budget.lines <= 0) {
|
|
2291
|
+
budget.exhausted = true;
|
|
2292
|
+
return;
|
|
2293
|
+
}
|
|
2294
|
+
budget.lines--;
|
|
2295
|
+
const end = diff.indexOf("\n", start);
|
|
2296
|
+
const line = diff.slice(start, end < 0 ? diff.length : end).replace(/\r$/, "");
|
|
2297
|
+
start = end < 0 ? diff.length : end + 1;
|
|
2298
|
+
const header = /^@@ -\d+(?:,(\d+))? \+\d+(?:,(\d+))? @@(?:.*)$/.exec(line);
|
|
2299
|
+
if (header) {
|
|
2300
|
+
if (old || next) return;
|
|
2301
|
+
old = Number(header[1] ?? 1);
|
|
2302
|
+
next = Number(header[2] ?? 1);
|
|
2303
|
+
if (!Number.isSafeInteger(old) || !Number.isSafeInteger(next)) return;
|
|
2304
|
+
hunks++;
|
|
2305
|
+
} else if (line === "\") continue;
|
|
2306
|
+
else if (old || next) {
|
|
2307
|
+
if (line.startsWith("+")) {
|
|
2308
|
+
next--;
|
|
2309
|
+
added++;
|
|
2310
|
+
} else if (line.startsWith("-")) {
|
|
2311
|
+
old--;
|
|
2312
|
+
removed++;
|
|
2313
|
+
} else if (line.startsWith(" ")) {
|
|
2314
|
+
old--;
|
|
2315
|
+
next--;
|
|
2316
|
+
} else return;
|
|
2317
|
+
if (old < 0 || next < 0) return;
|
|
2318
|
+
} else if (hunks && line) return;
|
|
2319
|
+
}
|
|
2320
|
+
if (budget.check(signal, endTime) && hunks && !old && !next) return { added, removed };
|
|
2321
|
+
} finally {
|
|
2322
|
+
budget.ms = Math.max(0, budget.ms - (budget.now() - startTime));
|
|
2323
|
+
}
|
|
2278
2324
|
}
|
|
2279
2325
|
|
|
2280
|
-
// src/
|
|
2326
|
+
// src/shell-discovery.ts
|
|
2327
|
+
init_define_REVIEW_PROMPTS();
|
|
2328
|
+
var import_shell_quote = __toESM(require_shell_quote(), 1);
|
|
2329
|
+
import path5 from "node:path";
|
|
2281
2330
|
var VARIABLE = "\0UNRESOLVED_VARIABLE\0";
|
|
2282
2331
|
var python = /^python(?:[23](?:\.\d+)*)?$/;
|
|
2283
2332
|
var shell = /^(?:ba|da|k|z)?sh$/;
|
|
2284
2333
|
var assignment = /^[A-Za-z_][A-Za-z_0-9]*=/;
|
|
2285
|
-
function
|
|
2334
|
+
function literal(word) {
|
|
2335
|
+
return !word.includes(VARIABLE) && !word.startsWith("~");
|
|
2336
|
+
}
|
|
2337
|
+
function unsupportedWhitespace(char) {
|
|
2338
|
+
return /\s/.test(char) && char !== " " && char !== " ";
|
|
2339
|
+
}
|
|
2340
|
+
var DiscoveryLimit = class extends Error {
|
|
2341
|
+
};
|
|
2342
|
+
var DiscoveryBudget = class {
|
|
2343
|
+
constructor(signal) {
|
|
2344
|
+
this.signal = signal;
|
|
2345
|
+
}
|
|
2346
|
+
signal;
|
|
2347
|
+
bytes = 16 * 1024 * 1024;
|
|
2348
|
+
tokens = 65536;
|
|
2349
|
+
references = 16384;
|
|
2350
|
+
expansions = 16384;
|
|
2351
|
+
steps = 262144;
|
|
2352
|
+
notices = /* @__PURE__ */ new Set();
|
|
2353
|
+
end = performance.now() + 500;
|
|
2354
|
+
step() {
|
|
2355
|
+
if (this.signal) reviewStage(this.signal, "Evidence collection");
|
|
2356
|
+
if (--this.steps < 0 || performance.now() >= this.end) throw new DiscoveryLimit();
|
|
2357
|
+
}
|
|
2358
|
+
admit(command) {
|
|
2359
|
+
this.step();
|
|
2360
|
+
if (command.length > this.bytes) throw new DiscoveryLimit();
|
|
2361
|
+
this.bytes -= Buffer.byteLength(command);
|
|
2362
|
+
if (this.bytes < 0) throw new DiscoveryLimit();
|
|
2363
|
+
let quote = "", word = false, units = 0;
|
|
2364
|
+
for (let i = 0; i < command.length; i++) {
|
|
2365
|
+
if (!(i % 4096)) this.step();
|
|
2366
|
+
const char = command[i];
|
|
2367
|
+
if (char === "\\" && quote !== "'") {
|
|
2368
|
+
i++;
|
|
2369
|
+
if (!word) {
|
|
2370
|
+
word = true;
|
|
2371
|
+
units++;
|
|
2372
|
+
}
|
|
2373
|
+
} else if (quote) {
|
|
2374
|
+
if (char === quote) quote = "";
|
|
2375
|
+
else if (quote === '"' && char === "$" && --this.expansions < 0) throw new DiscoveryLimit();
|
|
2376
|
+
} else if (char === "'" || char === '"') {
|
|
2377
|
+
quote = char;
|
|
2378
|
+
if (!word) {
|
|
2379
|
+
word = true;
|
|
2380
|
+
units++;
|
|
2381
|
+
}
|
|
2382
|
+
} else if (char === "#" && !word) break;
|
|
2383
|
+
else if (/\s/.test(char)) word = false;
|
|
2384
|
+
else if (/[;&|()<>]/.test(char)) {
|
|
2385
|
+
word = false;
|
|
2386
|
+
units++;
|
|
2387
|
+
} else {
|
|
2388
|
+
if (char === "$" && --this.expansions < 0) throw new DiscoveryLimit();
|
|
2389
|
+
if (!word) {
|
|
2390
|
+
word = true;
|
|
2391
|
+
units++;
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2394
|
+
if (units > this.tokens) throw new DiscoveryLimit();
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
parsed(count3) {
|
|
2398
|
+
this.step();
|
|
2399
|
+
this.tokens -= count3;
|
|
2400
|
+
if (this.tokens < 0) throw new DiscoveryLimit();
|
|
2401
|
+
}
|
|
2402
|
+
reference() {
|
|
2403
|
+
this.step();
|
|
2404
|
+
if (--this.references < 0) throw new DiscoveryLimit();
|
|
2405
|
+
}
|
|
2406
|
+
notice(text) {
|
|
2407
|
+
if (this.notices.has(text)) return;
|
|
2408
|
+
if (this.notices.size >= 256) throw new DiscoveryLimit();
|
|
2409
|
+
this.notices.add(text);
|
|
2410
|
+
}
|
|
2411
|
+
};
|
|
2412
|
+
var Words = class {
|
|
2413
|
+
constructor(words, budget) {
|
|
2414
|
+
this.words = words;
|
|
2415
|
+
this.budget = budget;
|
|
2416
|
+
}
|
|
2417
|
+
words;
|
|
2418
|
+
budget;
|
|
2419
|
+
index = 0;
|
|
2420
|
+
get length() {
|
|
2421
|
+
return this.words.length - this.index;
|
|
2422
|
+
}
|
|
2423
|
+
peek() {
|
|
2424
|
+
this.budget.step();
|
|
2425
|
+
return this.words[this.index];
|
|
2426
|
+
}
|
|
2427
|
+
take() {
|
|
2428
|
+
const word = this.peek();
|
|
2429
|
+
if (this.length) this.index++;
|
|
2430
|
+
return word;
|
|
2431
|
+
}
|
|
2432
|
+
*values() {
|
|
2433
|
+
for (let i = this.index; i < this.words.length; i++) {
|
|
2434
|
+
this.budget.step();
|
|
2435
|
+
yield this.words[i];
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2438
|
+
};
|
|
2439
|
+
function balancedQuotes(command, budget) {
|
|
2286
2440
|
let quote = "";
|
|
2287
2441
|
for (let i = 0; i < command.length; i++) {
|
|
2442
|
+
if (!(i % 4096)) budget.step();
|
|
2288
2443
|
const char = command[i];
|
|
2289
2444
|
if (char === "\\" && quote !== "'") {
|
|
2290
2445
|
if (++i === command.length) return false;
|
|
@@ -2293,12 +2448,15 @@ function balancedQuotes(command) {
|
|
|
2293
2448
|
}
|
|
2294
2449
|
return !quote;
|
|
2295
2450
|
}
|
|
2296
|
-
function unsupportedShellSyntax(command) {
|
|
2451
|
+
function unsupportedShellSyntax(command, budget) {
|
|
2297
2452
|
let quote = "";
|
|
2298
2453
|
let inWord = false;
|
|
2454
|
+
const whitespace2 = "Unsupported unquoted shell whitespace is outside reliable tokenization; script targets were not resolved.";
|
|
2299
2455
|
for (let i = 0; i < command.length; i++) {
|
|
2456
|
+
if (!(i % 4096)) budget.step();
|
|
2300
2457
|
const char = command[i];
|
|
2301
2458
|
if (char === "\\" && quote !== "'") {
|
|
2459
|
+
if (!quote && unsupportedWhitespace(command[i + 1] ?? "")) return whitespace2;
|
|
2302
2460
|
i++;
|
|
2303
2461
|
inWord = true;
|
|
2304
2462
|
} else if (quote) {
|
|
@@ -2306,276 +2464,356 @@ function unsupportedShellSyntax(command) {
|
|
|
2306
2464
|
} else if (char === "'" || char === '"') {
|
|
2307
2465
|
quote = char;
|
|
2308
2466
|
inWord = true;
|
|
2309
|
-
} else if (char
|
|
2467
|
+
} else if (unsupportedWhitespace(char)) return whitespace2;
|
|
2468
|
+
else if (char === "#") {
|
|
2310
2469
|
if (!inWord) return;
|
|
2311
2470
|
return "Unquoted # within a shell word is outside reliable tokenization; script targets were not resolved.";
|
|
2312
2471
|
} else if (char === "[" || char === "{" || char === "}") {
|
|
2313
2472
|
return "Unquoted bracket glob or brace syntax is outside literal discovery; script targets were not resolved.";
|
|
2314
|
-
} else inWord = !/[\
|
|
2473
|
+
} else inWord = !/[ \t;&|()<>]/.test(char);
|
|
2315
2474
|
}
|
|
2316
2475
|
}
|
|
2317
|
-
function
|
|
2318
|
-
const
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2476
|
+
function programIdentity(executable) {
|
|
2477
|
+
const name = /^(?:\/(?:usr\/)?bin\/)?([^/]+)$/.exec(executable)?.[1];
|
|
2478
|
+
if (!name) return;
|
|
2479
|
+
if (python.test(name)) return "python";
|
|
2480
|
+
if (shell.test(name)) return "shell";
|
|
2481
|
+
if (name === "cat" || name === "head" || name === "env") return name;
|
|
2482
|
+
}
|
|
2483
|
+
var DiscoveryState = class {
|
|
2484
|
+
constructor(cwd, depth, budget) {
|
|
2485
|
+
this.cwd = cwd;
|
|
2486
|
+
this.depth = depth;
|
|
2487
|
+
this.budget = budget;
|
|
2488
|
+
}
|
|
2489
|
+
cwd;
|
|
2490
|
+
depth;
|
|
2491
|
+
budget;
|
|
2492
|
+
references = [];
|
|
2493
|
+
limitations = [];
|
|
2494
|
+
uncertainWord = false;
|
|
2495
|
+
preceded = false;
|
|
2496
|
+
previousOperator;
|
|
2497
|
+
conditionalCwd = false;
|
|
2498
|
+
explicitCdpath = false;
|
|
2499
|
+
note(text) {
|
|
2500
|
+
this.budget.notice(text);
|
|
2501
|
+
if (!this.limitations.includes(text)) this.limitations.push(text);
|
|
2502
|
+
}
|
|
2503
|
+
invalidateCwd(text) {
|
|
2504
|
+
this.cwd = null;
|
|
2505
|
+
this.note(text);
|
|
2506
|
+
}
|
|
2507
|
+
observeAssignment(word) {
|
|
2508
|
+
if (!word.startsWith("CDPATH=")) return;
|
|
2509
|
+
this.explicitCdpath = true;
|
|
2510
|
+
this.note("Explicit CDPATH assignment is not evaluated; directory-search targets may be unresolved.");
|
|
2326
2511
|
}
|
|
2327
|
-
|
|
2328
|
-
note("
|
|
2329
|
-
return { references, limitations };
|
|
2512
|
+
qualifySnapshot() {
|
|
2513
|
+
if (this.preceded) this.note("Files are current filesystem snapshots; preceding command statements may change them before execution.");
|
|
2330
2514
|
}
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2515
|
+
add(filename, executable = false) {
|
|
2516
|
+
if (!literal(filename) || this.uncertainWord) {
|
|
2517
|
+
this.note(`Script filename unresolved: ${filename.replaceAll(VARIABLE, "<variable>")}`);
|
|
2518
|
+
return;
|
|
2519
|
+
}
|
|
2520
|
+
this.budget.reference();
|
|
2521
|
+
this.qualifySnapshot();
|
|
2522
|
+
this.references.push({ filename, cwd: this.cwd, executable });
|
|
2523
|
+
}
|
|
2524
|
+
nested(command) {
|
|
2525
|
+
const nested = scan(command, this.explicitCdpath ? null : this.cwd, this.depth + 1, this.budget);
|
|
2526
|
+
if (nested.references.length) this.qualifySnapshot();
|
|
2527
|
+
this.references.push(...nested.references);
|
|
2528
|
+
nested.limitations.forEach((text) => this.note(text));
|
|
2529
|
+
}
|
|
2530
|
+
afterOperator(operator) {
|
|
2531
|
+
this.previousOperator = operator;
|
|
2532
|
+
this.uncertainWord = false;
|
|
2533
|
+
this.preceded = true;
|
|
2534
|
+
if (operator === "||") {
|
|
2535
|
+
this.invalidateCwd("Conditional fallback makes the working directory uncertain for later statements.");
|
|
2536
|
+
} else if (operator === ";" && this.conditionalCwd) {
|
|
2537
|
+
this.conditionalCwd = false;
|
|
2538
|
+
this.invalidateCwd("Earlier cd may fail or be skipped before this statement; the working directory is unresolved at the branch join.");
|
|
2539
|
+
}
|
|
2335
2540
|
}
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
let words = [];
|
|
2349
|
-
let uncertainWord = false;
|
|
2350
|
-
let preceded = false;
|
|
2351
|
-
let previousOperator;
|
|
2352
|
-
let conditionalCwd = false;
|
|
2353
|
-
let explicitCdpath = false;
|
|
2354
|
-
const observeAssignment = (word) => {
|
|
2355
|
-
if (!word.startsWith("CDPATH=")) return;
|
|
2356
|
-
explicitCdpath = true;
|
|
2357
|
-
note("Explicit CDPATH assignment is not evaluated; directory-search targets may be unresolved.");
|
|
2358
|
-
};
|
|
2359
|
-
const literal = (s) => !s.includes(VARIABLE) && !s.startsWith("~");
|
|
2360
|
-
const qualifySnapshot = () => {
|
|
2361
|
-
if (preceded) note("Files are current filesystem snapshots; preceding command statements may change them before execution.");
|
|
2362
|
-
};
|
|
2363
|
-
const add2 = (filename, executable = false) => {
|
|
2364
|
-
if (!literal(filename) || uncertainWord) {
|
|
2365
|
-
note(`Script filename unresolved: ${filename.replaceAll(VARIABLE, "<variable>")}`);
|
|
2541
|
+
};
|
|
2542
|
+
function unwrap(args, state) {
|
|
2543
|
+
let builtins = true, builtinOnly = false, wrappers = 0;
|
|
2544
|
+
while (args.peek() && assignment.test(args.peek())) state.observeAssignment(args.take());
|
|
2545
|
+
while (args.peek()) {
|
|
2546
|
+
const name = args.peek();
|
|
2547
|
+
const env = programIdentity(name) === "env";
|
|
2548
|
+
if (!env && !["command", "exec", "builtin"].includes(name)) break;
|
|
2549
|
+
if (wrappers++ >= 8) {
|
|
2550
|
+
const text = "Wrapper nesting exceeds literal discovery's eight-wrapper limit; source was not resolved.";
|
|
2551
|
+
if (builtins) state.invalidateCwd(`${text} The working directory is unresolved.`);
|
|
2552
|
+
else state.note(text);
|
|
2366
2553
|
return;
|
|
2367
2554
|
}
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
const args = words;
|
|
2373
|
-
words = [];
|
|
2374
|
-
let builtins = true;
|
|
2375
|
-
while (args[0] && assignment.test(args[0])) observeAssignment(args.shift());
|
|
2376
|
-
let wrappers = 0;
|
|
2377
|
-
while (["env", "/usr/bin/env", "command", "exec"].includes(args[0] ?? "")) {
|
|
2378
|
-
if (wrappers++ >= 8) {
|
|
2379
|
-
note("Wrapper nesting exceeds literal discovery's eight-wrapper limit; source was not resolved.");
|
|
2555
|
+
const wrapper = args.take();
|
|
2556
|
+
if (env) {
|
|
2557
|
+
if (builtinOnly) {
|
|
2558
|
+
state.note(`Unsupported wrapper composition: ${wrapper} is not assumed to be a shell builtin.`);
|
|
2380
2559
|
return;
|
|
2381
2560
|
}
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
if (
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
note("Missing or unresolved env option argument; invoked source was not resolved.");
|
|
2392
|
-
return;
|
|
2393
|
-
}
|
|
2394
|
-
args.shift();
|
|
2395
|
-
continue;
|
|
2561
|
+
builtins = false;
|
|
2562
|
+
while (args.peek()?.startsWith("-")) {
|
|
2563
|
+
const option = args.take();
|
|
2564
|
+
if (option === "--") break;
|
|
2565
|
+
if (option === "-i" || option === "--ignore-environment") continue;
|
|
2566
|
+
if (option === "-u" || option === "--unset") {
|
|
2567
|
+
if (!args.peek() || !literal(args.peek()) || state.uncertainWord) {
|
|
2568
|
+
state.note("Missing or unresolved env option argument; invoked source was not resolved.");
|
|
2569
|
+
return;
|
|
2396
2570
|
}
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
}
|
|
2400
|
-
while (args[0] && assignment.test(args[0])) observeAssignment(args.shift());
|
|
2401
|
-
} else {
|
|
2402
|
-
if (!builtins) {
|
|
2403
|
-
note(`Unsupported wrapper composition: ${wrapper} after an external launcher is not assumed to be a shell builtin.`);
|
|
2404
|
-
return;
|
|
2405
|
-
}
|
|
2406
|
-
if (wrapper === "exec") builtins = false;
|
|
2407
|
-
if (args[0] === "--") args.shift();
|
|
2408
|
-
else if (args[0]?.startsWith("-")) {
|
|
2409
|
-
note(`Unsupported ${wrapper} option; invoked script source was not resolved.`);
|
|
2410
|
-
return;
|
|
2571
|
+
args.take();
|
|
2572
|
+
continue;
|
|
2411
2573
|
}
|
|
2574
|
+
state.note("Unsupported env option; invoked script source could not be resolved.");
|
|
2575
|
+
return;
|
|
2576
|
+
}
|
|
2577
|
+
while (args.peek() && assignment.test(args.peek())) state.observeAssignment(args.take());
|
|
2578
|
+
} else {
|
|
2579
|
+
if (!builtins) {
|
|
2580
|
+
state.note(`Unsupported wrapper composition: ${wrapper} after an external launcher is not assumed to be a shell builtin.`);
|
|
2581
|
+
return;
|
|
2412
2582
|
}
|
|
2413
|
-
|
|
2414
|
-
|
|
2583
|
+
builtinOnly = wrapper === "builtin";
|
|
2584
|
+
if (wrapper === "exec") builtins = false;
|
|
2585
|
+
if (args.peek() === "--") args.take();
|
|
2586
|
+
else if (args.peek()?.startsWith("-")) {
|
|
2587
|
+
const text = `Unsupported ${wrapper} option; invoked script source was not resolved.`;
|
|
2588
|
+
if (builtins && !(wrapper === "command" && ["-v", "-V"].includes(args.peek()))) {
|
|
2589
|
+
state.invalidateCwd(`${text} The working directory is unresolved.`);
|
|
2590
|
+
} else state.note(text);
|
|
2415
2591
|
return;
|
|
2416
2592
|
}
|
|
2417
2593
|
}
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
if (executable.startsWith("-")) {
|
|
2421
|
-
note("Wrapper options prevent script discovery.");
|
|
2594
|
+
if (!args.peek()) {
|
|
2595
|
+
state.note(`No command operand after ${wrapper}; invoked script source was not resolved.`);
|
|
2422
2596
|
return;
|
|
2423
2597
|
}
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2598
|
+
}
|
|
2599
|
+
const executable = args.take();
|
|
2600
|
+
if (!executable) return;
|
|
2601
|
+
if (executable.startsWith("-")) {
|
|
2602
|
+
state.note("Wrapper options prevent script discovery.");
|
|
2603
|
+
return;
|
|
2604
|
+
}
|
|
2605
|
+
return { executable, builtins, builtinOnly };
|
|
2606
|
+
}
|
|
2607
|
+
function builtin(executable, args, state, next) {
|
|
2608
|
+
if (["export", "readonly", "declare", "typeset"].includes(executable)) {
|
|
2609
|
+
let unsupported = state.uncertainWord;
|
|
2610
|
+
for (const word of args.values()) {
|
|
2611
|
+
state.observeAssignment(word);
|
|
2612
|
+
unsupported ||= !literal(word) || word.startsWith("-") && word !== "--" || word.startsWith("+");
|
|
2430
2613
|
}
|
|
2431
|
-
if (
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2614
|
+
if (unsupported) {
|
|
2615
|
+
state.invalidateCwd(`Unsupported ${executable} operands or options may change shell state; the working directory is unresolved.`);
|
|
2616
|
+
}
|
|
2617
|
+
return true;
|
|
2618
|
+
}
|
|
2619
|
+
if (["pushd", "popd"].includes(executable)) {
|
|
2620
|
+
state.invalidateCwd("Directory-stack operations are outside literal discovery; the working directory is unresolved.");
|
|
2621
|
+
return true;
|
|
2622
|
+
}
|
|
2623
|
+
if (executable === "cd") {
|
|
2624
|
+
if (args.peek() === "--") args.take();
|
|
2625
|
+
const target = args.peek();
|
|
2626
|
+
const searchesCdpath = target && !path5.isAbsolute(target) && !/^\.\.?(?:\/|$)/.test(target);
|
|
2627
|
+
if (state.previousOperator === "|" || next === "|") {
|
|
2628
|
+
state.invalidateCwd("Pipeline directory changes do not establish a reliable cwd for later script targets.");
|
|
2629
|
+
} else if (args.length === 1 && target && !target.startsWith("-") && literal(target) && !state.uncertainWord && !(state.explicitCdpath && searchesCdpath) && next === "&&" && state.cwd) {
|
|
2630
|
+
state.cwd = path5.resolve(state.cwd, target);
|
|
2631
|
+
state.conditionalCwd = true;
|
|
2632
|
+
} else {
|
|
2633
|
+
state.invalidateCwd("Working directory after cd is unresolved; relative script targets will not be guessed.");
|
|
2634
|
+
}
|
|
2635
|
+
return true;
|
|
2636
|
+
}
|
|
2637
|
+
if (executable === "source" || executable === ".") {
|
|
2638
|
+
if (args.peek() === "--") args.take();
|
|
2639
|
+
const target = args.peek();
|
|
2640
|
+
if (target?.startsWith("-")) state.note("Unsupported sourcing option; source target was not resolved.");
|
|
2641
|
+
else if (target && !target.includes("/")) state.note("Bare source operand uses PATH lookup; its file was not resolved.");
|
|
2642
|
+
else if (target) state.add(target);
|
|
2643
|
+
state.invalidateCwd("Sourced code may change the shell environment or working directory.");
|
|
2644
|
+
return true;
|
|
2645
|
+
}
|
|
2646
|
+
if (["eval", "read", "readarray", "mapfile", "getopts", "let", "set", "shopt", "alias", "unalias", "enable", "unset"].includes(executable)) {
|
|
2647
|
+
state.invalidateCwd(`In-process shell state change through ${executable} is outside literal discovery; the working directory is unresolved.`);
|
|
2648
|
+
return true;
|
|
2649
|
+
}
|
|
2650
|
+
if (["if", "then", "else", "fi", "for", "while", "until", "do", "done", "case", "function", "{"].includes(executable)) {
|
|
2651
|
+
state.invalidateCwd("Shell control flow is outside literal discovery; script coverage is incomplete.");
|
|
2652
|
+
return true;
|
|
2653
|
+
}
|
|
2654
|
+
return false;
|
|
2655
|
+
}
|
|
2656
|
+
function reader(program, args, state) {
|
|
2657
|
+
const operands = [];
|
|
2658
|
+
let options = true;
|
|
2659
|
+
while (args.length) {
|
|
2660
|
+
const arg = args.take();
|
|
2661
|
+
if (options && arg === "--") {
|
|
2662
|
+
options = false;
|
|
2663
|
+
continue;
|
|
2664
|
+
}
|
|
2665
|
+
if (options && arg.startsWith("-") && arg !== "-") {
|
|
2666
|
+
if (program === "cat" && (/^-[AbEenstTuv]+$/.test(arg) || ["--show-all", "--number-nonblank", "--show-ends", "--number", "--squeeze-blank", "--show-tabs", "--show-nonprinting"].includes(arg))) continue;
|
|
2667
|
+
if (program === "head") {
|
|
2668
|
+
if (/^-[qvz]+$/.test(arg) || ["--quiet", "--silent", "--verbose", "--zero-terminated"].includes(arg)) continue;
|
|
2669
|
+
if (["-n", "-c", "--lines", "--bytes"].includes(arg) && /^-?\d+[kKMGTPEZY]?(?:B|iB)?$/.test(args.peek() ?? "")) {
|
|
2670
|
+
args.take();
|
|
2671
|
+
continue;
|
|
2672
|
+
}
|
|
2673
|
+
if (/^(?:-[nc]?|--(?:lines|bytes)=)-?\d+[kKMGTPEZY]?(?:B|iB)?$/.test(arg)) continue;
|
|
2444
2674
|
}
|
|
2675
|
+
state.note(`Unsupported ${program} option; file operands were not resolved.`);
|
|
2445
2676
|
return;
|
|
2446
2677
|
}
|
|
2447
|
-
if (
|
|
2448
|
-
|
|
2449
|
-
|
|
2678
|
+
if (arg === "-") state.note(`${program} reads stdin; no file was resolved for that operand.`);
|
|
2679
|
+
else operands.push(arg);
|
|
2680
|
+
}
|
|
2681
|
+
operands.forEach((operand) => state.add(operand));
|
|
2682
|
+
}
|
|
2683
|
+
function interpreter(isPython, args, state) {
|
|
2684
|
+
while (args.length) {
|
|
2685
|
+
const arg = args.take();
|
|
2686
|
+
if (arg === "--") {
|
|
2687
|
+
const operand = args.peek();
|
|
2688
|
+
if (!operand || isPython && operand === "-") state.note("Interpreter reads stdin; no separate script file was resolved.");
|
|
2689
|
+
else if (!isPython && operand === "-") state.note("Shell '-' operand after -- is outside supported script discovery.");
|
|
2690
|
+
else state.add(operand);
|
|
2450
2691
|
return;
|
|
2451
2692
|
}
|
|
2452
|
-
if (!
|
|
2453
|
-
note("
|
|
2693
|
+
if (!isPython && arg === "-") {
|
|
2694
|
+
state.note("Shell '-' invocation is outside supported script discovery.");
|
|
2454
2695
|
return;
|
|
2455
2696
|
}
|
|
2456
|
-
if (
|
|
2457
|
-
|
|
2458
|
-
let options = true;
|
|
2459
|
-
for (let i = 0; i < args.length; i++) {
|
|
2460
|
-
const arg = args[i];
|
|
2461
|
-
if (options && arg === "--") {
|
|
2462
|
-
options = false;
|
|
2463
|
-
continue;
|
|
2464
|
-
}
|
|
2465
|
-
if (options && arg.startsWith("-") && arg !== "-") {
|
|
2466
|
-
if (base === "cat" && (/^-[AbEenstTuv]+$/.test(arg) || ["--show-all", "--number-nonblank", "--show-ends", "--number", "--squeeze-blank", "--show-tabs", "--show-nonprinting"].includes(arg))) continue;
|
|
2467
|
-
if (base === "head") {
|
|
2468
|
-
if (/^-[qvz]+$/.test(arg) || ["--quiet", "--silent", "--verbose", "--zero-terminated"].includes(arg)) continue;
|
|
2469
|
-
if (["-n", "-c", "--lines", "--bytes"].includes(arg) && /^-?\d+[kKMGTPEZY]?(?:B|iB)?$/.test(args[i + 1] ?? "")) {
|
|
2470
|
-
i++;
|
|
2471
|
-
continue;
|
|
2472
|
-
}
|
|
2473
|
-
if (/^(?:-[nc]?|--(?:lines|bytes)=)-?\d+[kKMGTPEZY]?(?:B|iB)?$/.test(arg)) continue;
|
|
2474
|
-
}
|
|
2475
|
-
note(`Unsupported ${base} option; file operands were not resolved.`);
|
|
2476
|
-
return;
|
|
2477
|
-
}
|
|
2478
|
-
if (arg === "-") note(`${base} reads stdin; no file was resolved for that operand.`);
|
|
2479
|
-
else operands.push(arg);
|
|
2480
|
-
}
|
|
2481
|
-
operands.forEach((operand) => add2(operand));
|
|
2697
|
+
if (arg === "-" || !isPython && /^-[abefhiklmnptuvxBCEHPTs]*s[abefhiklmnptuvxBCEHPTs]*$/.test(arg)) {
|
|
2698
|
+
state.note("Interpreter reads stdin; no separate script file was resolved.");
|
|
2482
2699
|
return;
|
|
2483
2700
|
}
|
|
2484
|
-
if (
|
|
2485
|
-
const
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
return;
|
|
2498
|
-
}
|
|
2499
|
-
if (arg === "-" || !isPython && /^-[abefhiklmnptuvxBCEHPTs]*s[abefhiklmnptuvxBCEHPTs]*$/.test(arg)) {
|
|
2500
|
-
note("Interpreter reads stdin; no separate script file was resolved.");
|
|
2501
|
-
return;
|
|
2502
|
-
}
|
|
2503
|
-
if (isPython && arg === "-c" || !isPython && /^-[abefhiklmnptuvxBCEHPT]*c$/.test(arg)) {
|
|
2504
|
-
if (!args[i + 1] || !literal(args[i + 1]) || uncertainWord) note("Inline interpreter code contains unresolved expansion or is missing; its runtime contents are unavailable.");
|
|
2505
|
-
if (!isPython && args[i + 1] && literal(args[i + 1]) && !uncertainWord) {
|
|
2506
|
-
const nested = discover(args[i + 1], explicitCdpath ? null : currentCwd, depth + 1);
|
|
2507
|
-
references.push(...nested.references);
|
|
2508
|
-
nested.limitations.forEach(note);
|
|
2509
|
-
if (nested.references.length) qualifySnapshot();
|
|
2510
|
-
}
|
|
2511
|
-
return;
|
|
2512
|
-
}
|
|
2513
|
-
if (isPython && (arg === "-m" || arg.startsWith("-m") || arg.startsWith("-c"))) {
|
|
2514
|
-
if (!arg.startsWith("-c")) note("Python module execution: module source is not resolved by direct-script discovery.");
|
|
2515
|
-
return;
|
|
2516
|
-
}
|
|
2517
|
-
if (isPython && (arg === "-W" || arg === "-X") || !isPython && (arg === "-o" || arg === "+o")) {
|
|
2518
|
-
if (!args[i + 1] || !literal(args[i + 1]) || uncertainWord) {
|
|
2519
|
-
note(`Interpreter option ${arg} has a missing or unresolved argument; script source was not resolved.`);
|
|
2520
|
-
return;
|
|
2521
|
-
}
|
|
2522
|
-
i++;
|
|
2523
|
-
continue;
|
|
2524
|
-
}
|
|
2525
|
-
if (arg.startsWith("-") || !isPython && arg.startsWith("+")) {
|
|
2526
|
-
const known = isPython ? /^-[bBdEIOPqRsSuv]+$/.test(arg) || /^-(W|X).+/.test(arg) : /^[-+][abefhiklmnptuvxBCEHPT]+$/.test(arg) || ["--noprofile", "--norc"].includes(arg);
|
|
2527
|
-
if (known) continue;
|
|
2528
|
-
note(`Interpreter option ${arg} is outside supported script discovery.`);
|
|
2529
|
-
return;
|
|
2530
|
-
}
|
|
2531
|
-
add2(arg);
|
|
2701
|
+
if (isPython && arg === "-c" || !isPython && /^-[abefhiklmnptuvxBCEHPT]*c$/.test(arg)) {
|
|
2702
|
+
const code = args.peek();
|
|
2703
|
+
if (!code || !literal(code) || state.uncertainWord) state.note("Inline interpreter code contains unresolved expansion or is missing; its runtime contents are unavailable.");
|
|
2704
|
+
if (!isPython && code && literal(code) && !state.uncertainWord) state.nested(code);
|
|
2705
|
+
return;
|
|
2706
|
+
}
|
|
2707
|
+
if (isPython && (arg === "-m" || arg.startsWith("-m") || arg.startsWith("-c"))) {
|
|
2708
|
+
if (!arg.startsWith("-c")) state.note("Python module execution: module source is not resolved by direct-script discovery.");
|
|
2709
|
+
return;
|
|
2710
|
+
}
|
|
2711
|
+
if (isPython && (arg === "-W" || arg === "-X") || !isPython && (arg === "-o" || arg === "+o")) {
|
|
2712
|
+
if (!args.peek() || !literal(args.peek()) || state.uncertainWord) {
|
|
2713
|
+
state.note(`Interpreter option ${arg} has a missing or unresolved argument; script source was not resolved.`);
|
|
2532
2714
|
return;
|
|
2533
2715
|
}
|
|
2534
|
-
|
|
2716
|
+
args.take();
|
|
2717
|
+
continue;
|
|
2535
2718
|
}
|
|
2536
|
-
if (
|
|
2537
|
-
|
|
2538
|
-
if (
|
|
2539
|
-
|
|
2540
|
-
else if (args[0]) add2(args[0]);
|
|
2541
|
-
currentCwd = null;
|
|
2542
|
-
note("Sourced code may change the shell environment or working directory.");
|
|
2719
|
+
if (arg.startsWith("-") || !isPython && arg.startsWith("+")) {
|
|
2720
|
+
const known = isPython ? /^-[bBdEIOPqRsSuv]+$/.test(arg) || /^-(W|X).+/.test(arg) : /^[-+][abefhiklmnptuvxBCEHPT]+$/.test(arg) || ["--noprofile", "--norc"].includes(arg);
|
|
2721
|
+
if (known) continue;
|
|
2722
|
+
state.note(`Interpreter option ${arg} is outside supported script discovery.`);
|
|
2543
2723
|
return;
|
|
2544
2724
|
}
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2725
|
+
state.add(arg);
|
|
2726
|
+
return;
|
|
2727
|
+
}
|
|
2728
|
+
}
|
|
2729
|
+
function visit(args, state, next) {
|
|
2730
|
+
const words = new Words(args, state.budget);
|
|
2731
|
+
const invocation = unwrap(words, state);
|
|
2732
|
+
if (!invocation) return;
|
|
2733
|
+
const { executable, builtins, builtinOnly } = invocation;
|
|
2734
|
+
if (!literal(executable)) {
|
|
2735
|
+
const text = "Executable name contains unresolved expansion; script coverage is incomplete.";
|
|
2736
|
+
if (builtins) state.invalidateCwd(`${text} The working directory is unresolved.`);
|
|
2737
|
+
else state.note(text);
|
|
2738
|
+
return;
|
|
2739
|
+
}
|
|
2740
|
+
if (builtins && builtin(executable, words, state, next)) return;
|
|
2741
|
+
if (builtinOnly) {
|
|
2742
|
+
if ([":", "true", "false", "pwd", "echo", "printf", "test"].includes(executable)) return;
|
|
2743
|
+
state.invalidateCwd(`Unsupported builtin operand ${executable}; source and subsequent working directory were not resolved.`);
|
|
2744
|
+
return;
|
|
2745
|
+
}
|
|
2746
|
+
const program = programIdentity(executable);
|
|
2747
|
+
if (program === "cat" || program === "head") return reader(program, words, state);
|
|
2748
|
+
if (program === "python" || program === "shell") return interpreter(program === "python", words, state);
|
|
2749
|
+
if (executable.includes("/")) state.add(executable, true);
|
|
2750
|
+
else if (/\.(py|sh|bash|zsh)$/.test(executable)) state.note(`Executable ${executable} uses PATH lookup; its file was not resolved.`);
|
|
2751
|
+
else if (["sudo", "su", "ssh", "docker", "podman", "eval", "xargs", "find", "npm", "npx", "make", "uv", "poetry"].includes(executable)) {
|
|
2752
|
+
state.note(`Indirect execution through ${executable}: invoked source is not automatically discovered.`);
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
function discover(command, cwd, depth = 0, signal) {
|
|
2756
|
+
return scan(command, cwd, depth, new DiscoveryBudget(signal));
|
|
2757
|
+
}
|
|
2758
|
+
function scan(command, cwd, depth, budget) {
|
|
2759
|
+
const state = new DiscoveryState(cwd, depth, budget);
|
|
2760
|
+
const result = { references: state.references, limitations: state.limitations };
|
|
2761
|
+
try {
|
|
2762
|
+
budget.admit(command);
|
|
2763
|
+
if (depth > 4 || command.includes("\0") || /[\r\n`]/.test(command)) {
|
|
2764
|
+
state.note("Script discovery unavailable for multiline commands, backticks, NULs, or deeply nested shell strings; inspect command text directly.");
|
|
2765
|
+
return result;
|
|
2766
|
+
}
|
|
2767
|
+
if (!balancedQuotes(command, budget)) {
|
|
2768
|
+
state.note("Shell quoting could not be resolved reliably; script contents were not collected.");
|
|
2769
|
+
return result;
|
|
2549
2770
|
}
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2771
|
+
const unsupported = unsupportedShellSyntax(command, budget);
|
|
2772
|
+
if (unsupported) {
|
|
2773
|
+
state.note(unsupported);
|
|
2774
|
+
return result;
|
|
2775
|
+
}
|
|
2776
|
+
let tokens;
|
|
2777
|
+
try {
|
|
2778
|
+
tokens = (0, import_shell_quote.parse)(command, () => VARIABLE);
|
|
2779
|
+
} catch {
|
|
2780
|
+
state.note("Shell tokenization failed; script targets could not be resolved.");
|
|
2781
|
+
return result;
|
|
2782
|
+
}
|
|
2783
|
+
budget.parsed(tokens.length);
|
|
2784
|
+
if (tokens.some((t) => typeof t !== "string" && "op" in t && !["&&", "||", ";", "|", "glob"].includes(t.op))) {
|
|
2785
|
+
state.note("Shell grouping, substitution, background execution or redirection prevents reliable script discovery; inspect command text directly.");
|
|
2786
|
+
return result;
|
|
2787
|
+
}
|
|
2788
|
+
let words = [];
|
|
2789
|
+
for (const token of tokens) {
|
|
2790
|
+
budget.step();
|
|
2791
|
+
if (typeof token === "string") words.push(token);
|
|
2792
|
+
else if ("comment" in token) break;
|
|
2793
|
+
else if ("op" in token && token.op === "glob" && "pattern" in token) {
|
|
2794
|
+
words.push(String(token.pattern));
|
|
2795
|
+
state.uncertainWord = true;
|
|
2796
|
+
} else if ("op" in token && typeof token.op === "string") {
|
|
2797
|
+
visit(words, state, token.op);
|
|
2798
|
+
words = [];
|
|
2799
|
+
state.afterOperator(token.op);
|
|
2569
2800
|
}
|
|
2570
2801
|
}
|
|
2802
|
+
visit(words, state);
|
|
2803
|
+
return result;
|
|
2804
|
+
} catch (error) {
|
|
2805
|
+
if (!(error instanceof DiscoveryLimit)) throw error;
|
|
2806
|
+
const text = "Shell discovery exceeded its shared 16 MiB/65,536-token/16,384-reference/16,384-expansion/262,144-step/256-notice/500 ms work limits; remaining source targets were not resolved.";
|
|
2807
|
+
if (!state.limitations.includes(text)) state.limitations.push(text);
|
|
2808
|
+
return result;
|
|
2571
2809
|
}
|
|
2572
|
-
visit();
|
|
2573
|
-
return { references, limitations };
|
|
2574
2810
|
}
|
|
2811
|
+
|
|
2812
|
+
// src/evidence.ts
|
|
2575
2813
|
async function capture(reference, budget, signal, scope) {
|
|
2576
2814
|
const result = { filename: reference.filename, status: "unavailable" };
|
|
2577
|
-
if (!reference.cwd && !
|
|
2578
|
-
const filename =
|
|
2815
|
+
if (!reference.cwd && !path6.isAbsolute(reference.filename)) return { ...result, status: "working directory unresolved; contents not provided" };
|
|
2816
|
+
const filename = path6.isAbsolute(reference.filename) ? reference.filename : `${reference.cwd}/${reference.filename}`;
|
|
2579
2817
|
result.path = filename;
|
|
2580
2818
|
signal.throwIfAborted();
|
|
2581
2819
|
try {
|
|
@@ -2632,9 +2870,10 @@ async function capture(reference, budget, signal, scope) {
|
|
|
2632
2870
|
}
|
|
2633
2871
|
async function collectEvidence(input, limits, signal, scope = fileAccess.scope(signal)) {
|
|
2634
2872
|
signal.throwIfAborted();
|
|
2873
|
+
if (input.command.length > limits.maxEvidenceBytes) throw new Error("Command exceeds configured evidence budget");
|
|
2635
2874
|
const commandBytes = Buffer.byteLength(input.command);
|
|
2636
2875
|
if (commandBytes > limits.maxEvidenceBytes) throw new Error("Command exceeds configured evidence budget");
|
|
2637
|
-
const discovery = discover(input.command, input.cwd);
|
|
2876
|
+
const discovery = discover(input.command, input.cwd, 0, signal);
|
|
2638
2877
|
const evidence = { ...input, kind: "shell", files: [], limitations: [
|
|
2639
2878
|
...input.limitations ?? [],
|
|
2640
2879
|
"Only literal Python/shell source and supported cat/head file operands are collected. Files are full review-time snapshots, not command output. Imports, dependencies, other runtimes and calls inside source files are not recursively inspected.",
|
|
@@ -2643,72 +2882,143 @@ async function collectEvidence(input, limits, signal, scope = fileAccess.scope(s
|
|
|
2643
2882
|
if (!input.userPrompt) evidence.limitations.push("User prompt unavailable.");
|
|
2644
2883
|
let remaining = limits.maxEvidenceBytes - commandBytes;
|
|
2645
2884
|
const limit = new FileLimit(limits.maxFiles, scope);
|
|
2646
|
-
const
|
|
2885
|
+
const declared = /* @__PURE__ */ new Map();
|
|
2647
2886
|
for (const reference of discovery.references) {
|
|
2648
2887
|
signal.throwIfAborted();
|
|
2649
|
-
const filename =
|
|
2888
|
+
const filename = path6.isAbsolute(reference.filename) ? reference.filename : reference.cwd ? `${reference.cwd}/${reference.filename}` : null;
|
|
2889
|
+
const key = filename ?? /* @__PURE__ */ Symbol();
|
|
2890
|
+
const previous = declared.get(key);
|
|
2891
|
+
if (previous) previous.executable &&= reference.executable;
|
|
2892
|
+
else declared.set(key, { ...reference });
|
|
2893
|
+
}
|
|
2894
|
+
const selected = /* @__PURE__ */ new Map();
|
|
2895
|
+
for (const reference of declared.values()) {
|
|
2896
|
+
signal.throwIfAborted();
|
|
2897
|
+
const filename = path6.isAbsolute(reference.filename) ? reference.filename : reference.cwd ? `${reference.cwd}/${reference.filename}` : null;
|
|
2650
2898
|
const { key, withinLimit } = await limit.consider(filename, signal);
|
|
2651
2899
|
const previous = selected.get(key);
|
|
2652
2900
|
if (previous) {
|
|
2901
|
+
const promote = previous.reference.executable && !reference.executable;
|
|
2653
2902
|
previous.reference.executable &&= reference.executable;
|
|
2654
|
-
if (filename && filename !== previous.filename
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2903
|
+
if (filename && filename !== previous.filename) previous.aliases.add(filename);
|
|
2904
|
+
if (promote && previous.withinLimit && previous.file.status === "direct executable is not identifiable as Python/shell source; contents not provided") {
|
|
2905
|
+
const file2 = await capture(previous.reference, remaining, signal, scope);
|
|
2906
|
+
Object.assign(previous.file, file2);
|
|
2907
|
+
if (file2.contents !== void 0) {
|
|
2908
|
+
delete previous.file.warning;
|
|
2909
|
+
remaining -= Buffer.byteLength(file2.contents);
|
|
2910
|
+
}
|
|
2911
|
+
}
|
|
2661
2912
|
continue;
|
|
2662
2913
|
}
|
|
2663
|
-
const file = await capture(reference, remaining, signal, scope);
|
|
2664
|
-
if (aliases.length) file.aliases = aliases;
|
|
2914
|
+
const file = withinLimit ? await capture(reference, remaining, signal, scope) : { filename: reference.filename, ...filename ? { path: filename } : {}, status: "file-count limit reached" };
|
|
2665
2915
|
if (file.contents === void 0) file.warning = omittedFile(file.path ?? file.filename);
|
|
2916
|
+
selected.set(key, { reference, filename, withinLimit, aliases: /* @__PURE__ */ new Set(), file });
|
|
2666
2917
|
evidence.files.push(file);
|
|
2667
2918
|
remaining -= Buffer.byteLength(file.contents ?? "");
|
|
2668
2919
|
}
|
|
2920
|
+
for (const { file, aliases } of selected.values()) if (aliases.size) file.aliases = [...aliases];
|
|
2669
2921
|
signal.throwIfAborted();
|
|
2670
2922
|
evidence.limitations.push(...limit.limitations);
|
|
2671
2923
|
return evidence;
|
|
2672
2924
|
}
|
|
2673
2925
|
async function collectEditEvidence(input, limits, signal, scope = fileAccess.scope(signal)) {
|
|
2674
|
-
signal
|
|
2675
|
-
const
|
|
2926
|
+
reviewStage(signal, "Evidence collection");
|
|
2927
|
+
const metadata = input.permission.metadata;
|
|
2676
2928
|
const limitations = [
|
|
2677
2929
|
...input.limitations,
|
|
2678
2930
|
"Proposed diffs come from the pending host permission, not from applying edits. Full files, dependencies and post-approval formatter changes are not inspected; host diffs may normalize whitespace or omit BOMs."
|
|
2679
2931
|
];
|
|
2680
2932
|
const record2 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
2933
|
+
const field = (value, key) => {
|
|
2934
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
2935
|
+
if (!descriptor) return void 0;
|
|
2936
|
+
if (!Object.hasOwn(descriptor, "value")) throw new Error("Native edit metadata contains an accessor");
|
|
2937
|
+
return descriptor.value;
|
|
2938
|
+
};
|
|
2939
|
+
let headerBytes = 16 * 1024 * 1024;
|
|
2940
|
+
const text = (value) => {
|
|
2941
|
+
if (typeof value !== "string") return;
|
|
2942
|
+
reviewStage(signal, "Evidence collection");
|
|
2943
|
+
if (value.length > headerBytes) throw new Error("Native edit scope exceeds the 16 MiB normalization limit");
|
|
2944
|
+
headerBytes -= Buffer.byteLength(value);
|
|
2945
|
+
if (headerBytes < 0) throw new Error("Native edit scope exceeds the 16 MiB normalization limit");
|
|
2946
|
+
return value;
|
|
2947
|
+
};
|
|
2948
|
+
const patterns = (values) => {
|
|
2949
|
+
if (values.length > 16384) throw new Error("Native edit scope exceeds the 16,384-entry normalization limit");
|
|
2950
|
+
const result = [];
|
|
2951
|
+
for (let index = 0; index < values.length; index++) {
|
|
2952
|
+
const value = text(field(values, String(index)));
|
|
2953
|
+
if (value === void 0) throw new Error("Native edit scope contains an invalid pattern");
|
|
2954
|
+
result.push(value);
|
|
2955
|
+
}
|
|
2956
|
+
return result;
|
|
2957
|
+
};
|
|
2958
|
+
const scopeText = (value) => {
|
|
2959
|
+
const result = text(value);
|
|
2960
|
+
if (result === void 0) throw new Error("Native edit scope contains invalid identity text");
|
|
2961
|
+
return result;
|
|
2962
|
+
};
|
|
2963
|
+
const permissionScope = {
|
|
2964
|
+
id: scopeText(input.permission.id),
|
|
2965
|
+
type: scopeText(input.permission.type),
|
|
2966
|
+
patterns: patterns(input.permission.patterns),
|
|
2967
|
+
always: patterns(input.permission.always),
|
|
2968
|
+
tool: input.permission.tool ? { messageID: scopeText(input.permission.tool.messageID), callID: scopeText(input.permission.tool.callID) } : null
|
|
2969
|
+
};
|
|
2681
2970
|
const patch = input.tool === "apply_patch";
|
|
2682
|
-
const
|
|
2971
|
+
const hostFiles = patch ? field(record2(metadata), "files") : void 0;
|
|
2972
|
+
const files = patch ? Array.isArray(hostFiles) ? hostFiles : [] : [metadata];
|
|
2973
|
+
if (files.length > 16384) throw new Error("Native edit changes exceed the 16,384-entry normalization limit");
|
|
2683
2974
|
if (!files.length) limitations.push("Per-file patch metadata unavailable; affected changes could not be enumerated.");
|
|
2684
2975
|
let remaining = limits.maxEvidenceBytes;
|
|
2976
|
+
let measurementUnits = 16 * 1024 * 1024;
|
|
2685
2977
|
const changes = [];
|
|
2686
2978
|
const limit = new FileLimit(limits.maxFiles, scope);
|
|
2979
|
+
const countsBudget = new DiffBudget();
|
|
2687
2980
|
for (let index = 0; index < files.length; index++) {
|
|
2688
|
-
signal
|
|
2689
|
-
const file = record2(files
|
|
2690
|
-
const
|
|
2981
|
+
reviewStage(signal, "Evidence collection");
|
|
2982
|
+
const file = record2(patch ? field(files, String(index)) : metadata);
|
|
2983
|
+
const type = patch ? field(file, "type") : input.tool;
|
|
2984
|
+
const operation = typeof type === "string" && ["add", "update", "delete", "move", "edit", "write"].includes(type) ? type : "unknown";
|
|
2985
|
+
const filePath = text(field(file, patch ? "filePath" : "filepath"));
|
|
2986
|
+
const movePath = patch ? text(field(file, "movePath")) : void 0;
|
|
2987
|
+
const diff = field(file, patch ? "patch" : "diff");
|
|
2691
2988
|
const change = {
|
|
2692
|
-
path:
|
|
2989
|
+
path: filePath?.trim() ? filePath : null,
|
|
2693
2990
|
operation,
|
|
2694
2991
|
status: "omitted",
|
|
2695
|
-
...
|
|
2992
|
+
...movePath !== void 0 ? { movePath } : {}
|
|
2696
2993
|
};
|
|
2697
|
-
const { withinLimit } = await limit.consider(change.path &&
|
|
2994
|
+
const { withinLimit } = await limit.consider(change.path && path6.isAbsolute(change.path) ? change.path : null, signal);
|
|
2698
2995
|
if (!withinLimit) change.reason = "file-count limit reached";
|
|
2699
|
-
else if (!change.path || !
|
|
2996
|
+
else if (!change.path || !path6.isAbsolute(change.path)) change.reason = "absolute target path unavailable";
|
|
2700
2997
|
else if (operation === "unknown" || patch && (operation === "edit" || operation === "write")) change.reason = "file operation unavailable or unsupported";
|
|
2701
|
-
else if (operation === "move" && (!change.movePath || !
|
|
2702
|
-
else if (typeof
|
|
2703
|
-
else if (
|
|
2998
|
+
else if (operation === "move" && (!change.movePath || !path6.isAbsolute(change.movePath))) change.reason = "absolute move destination unavailable";
|
|
2999
|
+
else if (typeof diff !== "string" || !diff.length) change.reason = "proposed diff unavailable";
|
|
3000
|
+
else if (diff.length > remaining) change.reason = "complete diff exceeds remaining evidence byte budget";
|
|
3001
|
+
else if (diff.length > measurementUnits) change.reason = "shared diff UTF-8 measurement work limit reached";
|
|
2704
3002
|
else {
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
remaining
|
|
3003
|
+
measurementUnits -= diff.length;
|
|
3004
|
+
const bytes = Buffer.byteLength(diff);
|
|
3005
|
+
if (bytes > remaining) change.reason = "complete diff exceeds remaining evidence byte budget";
|
|
3006
|
+
else if (!diff.trim()) change.reason = "proposed diff unavailable";
|
|
3007
|
+
else {
|
|
3008
|
+
change.status = "included";
|
|
3009
|
+
change.diff = diff;
|
|
3010
|
+
remaining -= bytes;
|
|
3011
|
+
}
|
|
2708
3012
|
}
|
|
2709
3013
|
if (change.status === "omitted") {
|
|
2710
3014
|
change.warning = omittedFile(change.path);
|
|
2711
|
-
|
|
3015
|
+
let counts = diffDelta(diff, signal, countsBudget);
|
|
3016
|
+
if (!counts) {
|
|
3017
|
+
const added = field(file, "additions"), removed = field(file, "deletions");
|
|
3018
|
+
if (Number.isSafeInteger(added) && Number(added) >= 0 && Number.isSafeInteger(removed) && Number(removed) >= 0) {
|
|
3019
|
+
counts = { added: Number(added), removed: Number(removed) };
|
|
3020
|
+
}
|
|
3021
|
+
}
|
|
2712
3022
|
if (counts) change.delta = `[\u0394] ${JSON.stringify(change.path)}: +${counts.added} \u2212${counts.removed} lines`;
|
|
2713
3023
|
}
|
|
2714
3024
|
changes.push(change);
|
|
@@ -2717,7 +3027,8 @@ async function collectEditEvidence(input, limits, signal, scope = fileAccess.sco
|
|
|
2717
3027
|
if (partial) limitations.push("Edit evidence is incomplete. Omitted changes are not assessed by the supplied diffs; do not assume the whole proposal is safe.");
|
|
2718
3028
|
if (!input.userPrompt) limitations.push("User prompt unavailable.");
|
|
2719
3029
|
limitations.push(...limit.limitations);
|
|
2720
|
-
|
|
3030
|
+
if (countsBudget.exhausted) limitations.push("Some omitted-diff line counts exceeded the shared 16 MiB/65,536-line/250 ms counting allowance; valid host counts were used when available.");
|
|
3031
|
+
reviewStage(signal, "Evidence collection");
|
|
2721
3032
|
return {
|
|
2722
3033
|
kind: "edit",
|
|
2723
3034
|
tool: input.tool,
|
|
@@ -2775,10 +3086,10 @@ function patchOperations(input, signal) {
|
|
|
2775
3086
|
update = void 0;
|
|
2776
3087
|
const kind = line.startsWith("*** Add File:") ? "add" : line.startsWith("*** Delete File:") ? "delete" : line.startsWith("*** Update File:") ? "update" : void 0;
|
|
2777
3088
|
if (!kind) continue;
|
|
2778
|
-
const
|
|
2779
|
-
if (!
|
|
3089
|
+
const path11 = line.slice(line.indexOf(":") + 1).trim();
|
|
3090
|
+
if (!path11) throw new Error("Patch operation summary contains an empty target path");
|
|
2780
3091
|
if (operations.length >= 16384) throw new Error("Patch operation summary exceeds bounded JSON structure limits");
|
|
2781
|
-
const operation = { operation: kind, path:
|
|
3092
|
+
const operation = { operation: kind, path: path11 };
|
|
2782
3093
|
operations.push(operation);
|
|
2783
3094
|
if (kind === "update") update = operation;
|
|
2784
3095
|
}
|
|
@@ -2809,7 +3120,7 @@ function boundedCopy(value, budget, valueBudget = 16384) {
|
|
|
2809
3120
|
} else add2(code >= 55296 && code <= 57343 ? 6 : 3);
|
|
2810
3121
|
}
|
|
2811
3122
|
};
|
|
2812
|
-
const
|
|
3123
|
+
const visit2 = (value2, depth) => {
|
|
2813
3124
|
if (++nodes > valueBudget || depth > 32) throw new Error("Operation evidence exceeds bounded JSON structure limits");
|
|
2814
3125
|
if (value2 === null) {
|
|
2815
3126
|
add2(4);
|
|
@@ -2835,7 +3146,7 @@ function boundedCopy(value, budget, valueBudget = 16384) {
|
|
|
2835
3146
|
const descriptor = Object.getOwnPropertyDescriptor(value2, String(index));
|
|
2836
3147
|
if (!descriptor || !Object.hasOwn(descriptor, "value")) throw new Error("Operation evidence contains an array hole or accessor");
|
|
2837
3148
|
if (count3++) add2(1);
|
|
2838
|
-
entries.push(
|
|
3149
|
+
entries.push(visit2(descriptor.value, depth + 1));
|
|
2839
3150
|
}
|
|
2840
3151
|
} else {
|
|
2841
3152
|
for (const key in value2) {
|
|
@@ -2845,13 +3156,13 @@ function boundedCopy(value, budget, valueBudget = 16384) {
|
|
|
2845
3156
|
if (count3++) add2(1);
|
|
2846
3157
|
text(key);
|
|
2847
3158
|
add2(1);
|
|
2848
|
-
Object.defineProperty(result, key, { value:
|
|
3159
|
+
Object.defineProperty(result, key, { value: visit2(descriptor.value, depth + 1), enumerable: true, writable: true, configurable: true });
|
|
2849
3160
|
}
|
|
2850
3161
|
}
|
|
2851
3162
|
ancestors.delete(value2);
|
|
2852
3163
|
return result;
|
|
2853
3164
|
};
|
|
2854
|
-
return { value:
|
|
3165
|
+
return { value: visit2(value, 0), bytes, values: nodes };
|
|
2855
3166
|
}
|
|
2856
3167
|
function collectToolEvidence(context, limits, signal) {
|
|
2857
3168
|
signal.throwIfAborted();
|
|
@@ -2911,39 +3222,38 @@ function collectDirectoryEvidence(context, limits, signal) {
|
|
|
2911
3222
|
}
|
|
2912
3223
|
|
|
2913
3224
|
// src/evaluate.ts
|
|
2914
|
-
async function evaluateEvidence(request,
|
|
3225
|
+
async function evaluateEvidence(request, reader2, options, config, configError, signal, onIdentified, files) {
|
|
2915
3226
|
if (!candidateEnabled(request, options)) return null;
|
|
2916
3227
|
reviewStage(signal, "Permission context lookup");
|
|
2917
|
-
const invocation = await loadInvocation(request,
|
|
3228
|
+
const invocation = await loadInvocation(request, reader2, signal);
|
|
2918
3229
|
let ids;
|
|
2919
3230
|
try {
|
|
2920
3231
|
ids = await withDeadline(signal, Math.min(1500, remainingTime(signal) / 4), async (s) => {
|
|
2921
|
-
const value = await
|
|
3232
|
+
const value = await reader2.toolIDs?.(s);
|
|
2922
3233
|
if (!value || value.length > 16384 || !value.every((id) => typeof id === "string")) throw new Error("Tool registry unavailable");
|
|
2923
3234
|
return value;
|
|
2924
3235
|
}, "Tool registry lookup");
|
|
2925
3236
|
} catch {
|
|
2926
3237
|
signal.throwIfAborted();
|
|
2927
3238
|
}
|
|
2928
|
-
const classified = classify(request, invocation, ids,
|
|
3239
|
+
const classified = classify(request, invocation, ids, reader2.mcpServers?.() ?? []);
|
|
2929
3240
|
if (!classified) return null;
|
|
2930
3241
|
if (!enabledKind(classified.kind, options)) return null;
|
|
2931
3242
|
onIdentified();
|
|
2932
3243
|
if (!config) throw new Error(configError || "Invalid configuration");
|
|
2933
3244
|
const scope = files.scope(signal);
|
|
2934
|
-
const cachedReader = { ...reader, message: async () => invocation.message };
|
|
2935
3245
|
if (classified.kind === "shell") {
|
|
2936
|
-
const context = await loadContext(request,
|
|
3246
|
+
const context = await loadContext(request, reader2, signal, scope, invocation);
|
|
2937
3247
|
if (!context) throw new Error("Native shell context unavailable");
|
|
2938
3248
|
reviewStage(signal, "Evidence collection");
|
|
2939
3249
|
return collectEvidence(context, config, signal, scope);
|
|
2940
3250
|
}
|
|
2941
3251
|
if (classified.kind === "edit") {
|
|
2942
|
-
const context = await loadEditContext(request,
|
|
3252
|
+
const context = await loadEditContext(request, reader2, signal, invocation);
|
|
2943
3253
|
reviewStage(signal, "Evidence collection");
|
|
2944
3254
|
return collectEditEvidence(context, config, signal, scope);
|
|
2945
3255
|
}
|
|
2946
|
-
const conversation = await loadConversationContext(request,
|
|
3256
|
+
const conversation = await loadConversationContext(request, reader2, signal);
|
|
2947
3257
|
if (!invocation.location.instanceDirectory) conversation.limitations.push("Invocation directory unavailable; session origin is not substituted.");
|
|
2948
3258
|
if (!invocation.location.instanceWorktree) conversation.limitations.push("Invocation worktree unavailable.");
|
|
2949
3259
|
const common = { ...conversation, tool: invocation.tool, input: invocation.input, permission: permissionContext(request, false), location: invocation.location };
|
|
@@ -2957,7 +3267,7 @@ async function evaluateEvidence(request, reader, options, config, configError, s
|
|
|
2957
3267
|
definition = await withDeadline(
|
|
2958
3268
|
signal,
|
|
2959
3269
|
Math.min(1e3, remainingTime(signal) / 4),
|
|
2960
|
-
(s) =>
|
|
3270
|
+
(s) => reader2.definition?.(invocation.info, invocation.tool, s) ?? Promise.resolve(void 0),
|
|
2961
3271
|
"Tool definition lookup"
|
|
2962
3272
|
);
|
|
2963
3273
|
if (definition?.id !== invocation.tool) definition = void 0;
|
|
@@ -2989,11 +3299,58 @@ function approvalTransport(client, directory) {
|
|
|
2989
3299
|
};
|
|
2990
3300
|
}
|
|
2991
3301
|
|
|
3302
|
+
// src/pending-refresh.ts
|
|
3303
|
+
init_define_REVIEW_PROMPTS();
|
|
3304
|
+
var PendingRefresh = class {
|
|
3305
|
+
constructor(pending, read, parent) {
|
|
3306
|
+
this.pending = pending;
|
|
3307
|
+
this.read = read;
|
|
3308
|
+
this.signal = AbortSignal.any([parent, this.abort.signal]);
|
|
3309
|
+
}
|
|
3310
|
+
pending;
|
|
3311
|
+
read;
|
|
3312
|
+
syncing = false;
|
|
3313
|
+
stopped = false;
|
|
3314
|
+
again = false;
|
|
3315
|
+
generation = 0;
|
|
3316
|
+
abort = new AbortController();
|
|
3317
|
+
signal;
|
|
3318
|
+
async refresh() {
|
|
3319
|
+
if (this.stopped || this.signal.aborted) return;
|
|
3320
|
+
if (this.syncing) {
|
|
3321
|
+
this.again = true;
|
|
3322
|
+
return;
|
|
3323
|
+
}
|
|
3324
|
+
this.syncing = true;
|
|
3325
|
+
const generation = ++this.generation;
|
|
3326
|
+
const revision = this.pending.revision;
|
|
3327
|
+
try {
|
|
3328
|
+
const requests = await withDeadline(this.signal, 5e3, this.read, "Pending permission refresh");
|
|
3329
|
+
if (this.stopped || this.signal.aborted || this.generation !== generation || this.pending.revision !== revision) return;
|
|
3330
|
+
this.pending.reconcile(requests, revision);
|
|
3331
|
+
} catch {
|
|
3332
|
+
} finally {
|
|
3333
|
+
if (this.generation === generation) {
|
|
3334
|
+
this.syncing = false;
|
|
3335
|
+
const again = this.again;
|
|
3336
|
+
this.again = false;
|
|
3337
|
+
if (again) void this.refresh();
|
|
3338
|
+
}
|
|
3339
|
+
}
|
|
3340
|
+
}
|
|
3341
|
+
dispose() {
|
|
3342
|
+
this.stopped = true;
|
|
3343
|
+
this.again = false;
|
|
3344
|
+
this.generation++;
|
|
3345
|
+
this.abort.abort();
|
|
3346
|
+
}
|
|
3347
|
+
};
|
|
3348
|
+
|
|
2992
3349
|
// src/lifetime-view.tsx
|
|
2993
3350
|
init_define_REVIEW_PROMPTS();
|
|
2994
3351
|
import { createComponent as _$createComponent } from "@opentui/solid";
|
|
2995
3352
|
import { memo as _$memo } from "@opentui/solid";
|
|
2996
|
-
import
|
|
3353
|
+
import path8 from "node:path";
|
|
2997
3354
|
import { createSignal } from "solid-js";
|
|
2998
3355
|
|
|
2999
3356
|
// src/lifetime.ts
|
|
@@ -3001,7 +3358,7 @@ init_define_REVIEW_PROMPTS();
|
|
|
3001
3358
|
import { constants as constants3 } from "node:fs";
|
|
3002
3359
|
import { mkdir, open as open3, readdir as readdir2, rename, unlink } from "node:fs/promises";
|
|
3003
3360
|
import { randomUUID } from "node:crypto";
|
|
3004
|
-
import
|
|
3361
|
+
import path7 from "node:path";
|
|
3005
3362
|
var empty = () => ({ requests: 0, tokenRequests: 0, input: 0, output: 0, priced: 0, cost: 0, since: null });
|
|
3006
3363
|
var count2 = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
3007
3364
|
var amount = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
@@ -3025,7 +3382,7 @@ var LifetimeUsage = class {
|
|
|
3025
3382
|
constructor(directory, legacyDirectory) {
|
|
3026
3383
|
this.directory = directory;
|
|
3027
3384
|
this.legacyDirectory = legacyDirectory;
|
|
3028
|
-
if (!
|
|
3385
|
+
if (!path7.isAbsolute(directory) || legacyDirectory !== void 0 && !path7.isAbsolute(legacyDirectory)) {
|
|
3029
3386
|
throw new Error("Lifetime usage directory must be absolute");
|
|
3030
3387
|
}
|
|
3031
3388
|
}
|
|
@@ -3052,7 +3409,7 @@ var LifetimeUsage = class {
|
|
|
3052
3409
|
}).then(async () => {
|
|
3053
3410
|
this.local = add(this.local, increment);
|
|
3054
3411
|
await mkdir(this.directory, { recursive: true, mode: 448 });
|
|
3055
|
-
const temporary =
|
|
3412
|
+
const temporary = path7.join(this.directory, `${this.id}.${randomUUID()}.tmp`);
|
|
3056
3413
|
try {
|
|
3057
3414
|
const file = await open3(temporary, "wx", 384);
|
|
3058
3415
|
try {
|
|
@@ -3061,7 +3418,7 @@ var LifetimeUsage = class {
|
|
|
3061
3418
|
} finally {
|
|
3062
3419
|
await file.close();
|
|
3063
3420
|
}
|
|
3064
|
-
await rename(temporary,
|
|
3421
|
+
await rename(temporary, path7.join(this.directory, `${this.id}.json`));
|
|
3065
3422
|
const directory = await open3(this.directory, constants3.O_RDONLY | constants3.O_DIRECTORY);
|
|
3066
3423
|
try {
|
|
3067
3424
|
await directory.sync();
|
|
@@ -3082,8 +3439,9 @@ var LifetimeUsage = class {
|
|
|
3082
3439
|
async totals(signal) {
|
|
3083
3440
|
signal?.throwIfAborted();
|
|
3084
3441
|
await this.flush();
|
|
3442
|
+
signal?.throwIfAborted();
|
|
3085
3443
|
let total = empty();
|
|
3086
|
-
const directories = new Set([this.directory, ...this.legacyDirectory ? [this.legacyDirectory] : []].map((directory) =>
|
|
3444
|
+
const directories = new Set([this.directory, ...this.legacyDirectory ? [this.legacyDirectory] : []].map((directory) => path7.resolve(directory)));
|
|
3087
3445
|
for (const directory of directories) total = add(total, await this.readDirectory(directory, signal));
|
|
3088
3446
|
signal?.throwIfAborted();
|
|
3089
3447
|
return total;
|
|
@@ -3094,17 +3452,22 @@ var LifetimeUsage = class {
|
|
|
3094
3452
|
if (error.code === "ENOENT") return [];
|
|
3095
3453
|
throw error;
|
|
3096
3454
|
});
|
|
3455
|
+
signal?.throwIfAborted();
|
|
3097
3456
|
let total = empty();
|
|
3098
3457
|
for (const name of names.filter((name2) => snapshotName.test(name2))) {
|
|
3099
3458
|
signal?.throwIfAborted();
|
|
3100
|
-
const file = await open3(
|
|
3459
|
+
const file = await open3(path7.join(directory, name), constants3.O_RDONLY | constants3.O_NOFOLLOW | constants3.O_NONBLOCK);
|
|
3101
3460
|
try {
|
|
3461
|
+
signal?.throwIfAborted();
|
|
3102
3462
|
const stat = await file.stat();
|
|
3463
|
+
signal?.throwIfAborted();
|
|
3103
3464
|
if (!stat.isFile() || stat.size > 1024) throw new Error("Invalid lifetime usage snapshot");
|
|
3104
3465
|
const bytes = Buffer.alloc(1025);
|
|
3105
3466
|
let size = 0;
|
|
3106
3467
|
while (size < bytes.length) {
|
|
3468
|
+
signal?.throwIfAborted();
|
|
3107
3469
|
const read = await file.read(bytes, size, bytes.length - size, size);
|
|
3470
|
+
signal?.throwIfAborted();
|
|
3108
3471
|
if (!read.bytesRead) break;
|
|
3109
3472
|
size += read.bytesRead;
|
|
3110
3473
|
}
|
|
@@ -3123,6 +3486,7 @@ var LifetimeUsage = class {
|
|
|
3123
3486
|
} finally {
|
|
3124
3487
|
await file.close();
|
|
3125
3488
|
}
|
|
3489
|
+
signal?.throwIfAborted();
|
|
3126
3490
|
}
|
|
3127
3491
|
signal?.throwIfAborted();
|
|
3128
3492
|
return total;
|
|
@@ -3137,7 +3501,7 @@ function lifetimeReport(totals) {
|
|
|
3137
3501
|
return [
|
|
3138
3502
|
lifetimeCost(totals),
|
|
3139
3503
|
`${totals.requests} requests with recorded usage`,
|
|
3140
|
-
totals.tokenRequests ? `
|
|
3504
|
+
totals.tokenRequests ? `token: ${totals.input} in ${totals.output} out${totals.tokenRequests < totals.requests ? " (partial coverage)" : ""}` : "tokens unavailable",
|
|
3141
3505
|
`Token counts available: ${totals.tokenRequests}/${totals.requests} requests`,
|
|
3142
3506
|
`Pricing available: ${totals.priced}/${totals.requests} requests`,
|
|
3143
3507
|
...totals.since === null ? [] : [`Recorded since: ${new Date(totals.since).toISOString().slice(0, 10)}`],
|
|
@@ -3146,25 +3510,96 @@ function lifetimeReport(totals) {
|
|
|
3146
3510
|
].join("\n");
|
|
3147
3511
|
}
|
|
3148
3512
|
|
|
3513
|
+
// src/lifetime-refresh.ts
|
|
3514
|
+
init_define_REVIEW_PROMPTS();
|
|
3515
|
+
var LifetimeRefresh = class {
|
|
3516
|
+
constructor(read, publish, parent) {
|
|
3517
|
+
this.read = read;
|
|
3518
|
+
this.publish = publish;
|
|
3519
|
+
this.signal = AbortSignal.any([parent, this.abort.signal]);
|
|
3520
|
+
}
|
|
3521
|
+
read;
|
|
3522
|
+
publish;
|
|
3523
|
+
revision = 0;
|
|
3524
|
+
followUp = false;
|
|
3525
|
+
stopped = false;
|
|
3526
|
+
active;
|
|
3527
|
+
abort = new AbortController();
|
|
3528
|
+
signal;
|
|
3529
|
+
refresh() {
|
|
3530
|
+
if (this.stopped || this.signal.aborted) return;
|
|
3531
|
+
this.revision++;
|
|
3532
|
+
if (this.active) {
|
|
3533
|
+
this.followUp = true;
|
|
3534
|
+
return;
|
|
3535
|
+
}
|
|
3536
|
+
this.start();
|
|
3537
|
+
}
|
|
3538
|
+
/** A failed write invalidates active results and requests queued before it. */
|
|
3539
|
+
failed() {
|
|
3540
|
+
if (this.stopped || this.signal.aborted) return;
|
|
3541
|
+
this.revision++;
|
|
3542
|
+
this.followUp = false;
|
|
3543
|
+
this.publish(void 0);
|
|
3544
|
+
}
|
|
3545
|
+
start() {
|
|
3546
|
+
if (this.stopped || this.signal.aborted) return;
|
|
3547
|
+
this.followUp = false;
|
|
3548
|
+
const transaction = { revision: this.revision, settled: true, finished: false };
|
|
3549
|
+
this.active = transaction;
|
|
3550
|
+
void this.run(transaction);
|
|
3551
|
+
}
|
|
3552
|
+
live(transaction) {
|
|
3553
|
+
return !this.stopped && !this.signal.aborted && this.active === transaction;
|
|
3554
|
+
}
|
|
3555
|
+
async run(transaction) {
|
|
3556
|
+
try {
|
|
3557
|
+
const totals = await withDeadline(this.signal, 5e3, (signal) => {
|
|
3558
|
+
transaction.settled = false;
|
|
3559
|
+
const worker = Promise.resolve().then(() => {
|
|
3560
|
+
signal.throwIfAborted();
|
|
3561
|
+
return this.read(signal);
|
|
3562
|
+
});
|
|
3563
|
+
const settled = () => {
|
|
3564
|
+
transaction.settled = true;
|
|
3565
|
+
this.release(transaction);
|
|
3566
|
+
};
|
|
3567
|
+
void worker.then(settled, settled);
|
|
3568
|
+
return worker;
|
|
3569
|
+
}, "Lifetime usage loading");
|
|
3570
|
+
if (this.live(transaction) && this.revision === transaction.revision) this.publish(totals);
|
|
3571
|
+
} catch {
|
|
3572
|
+
if (this.live(transaction)) this.publish(void 0);
|
|
3573
|
+
} finally {
|
|
3574
|
+
transaction.finished = true;
|
|
3575
|
+
this.release(transaction);
|
|
3576
|
+
}
|
|
3577
|
+
}
|
|
3578
|
+
release(transaction) {
|
|
3579
|
+
if (this.active !== transaction || !transaction.finished || !transaction.settled) return;
|
|
3580
|
+
this.active = void 0;
|
|
3581
|
+
if (this.followUp) this.start();
|
|
3582
|
+
}
|
|
3583
|
+
dispose() {
|
|
3584
|
+
this.stopped = true;
|
|
3585
|
+
this.followUp = false;
|
|
3586
|
+
this.revision++;
|
|
3587
|
+
this.abort.abort();
|
|
3588
|
+
}
|
|
3589
|
+
};
|
|
3590
|
+
|
|
3149
3591
|
// src/lifetime-view.tsx
|
|
3150
3592
|
function lifetimeTracker(api) {
|
|
3151
|
-
const directory =
|
|
3152
|
-
const store = new LifetimeUsage(
|
|
3593
|
+
const directory = path8.join(api.state.path.state, "opencode-reviewer");
|
|
3594
|
+
const store = new LifetimeUsage(path8.join(directory, "usage-v2"), path8.join(directory, "usage-v1"));
|
|
3153
3595
|
const [totals, setTotals] = createSignal();
|
|
3154
3596
|
const [unavailable, setUnavailable] = createSignal(false);
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
setTotals(value);
|
|
3162
|
-
setUnavailable(false);
|
|
3163
|
-
}
|
|
3164
|
-
} catch {
|
|
3165
|
-
if (alive && revision === current) setUnavailable(true);
|
|
3166
|
-
}
|
|
3167
|
-
};
|
|
3597
|
+
const refresh = new LifetimeRefresh((signal) => store.totals(signal), (value) => {
|
|
3598
|
+
if (value) {
|
|
3599
|
+
setTotals(value);
|
|
3600
|
+
setUnavailable(false);
|
|
3601
|
+
} else setUnavailable(true);
|
|
3602
|
+
}, api.lifecycle.signal);
|
|
3168
3603
|
const unregister = api.keymap.registerLayer({
|
|
3169
3604
|
commands: [{
|
|
3170
3605
|
name: "opencode-reviewer.lifetime",
|
|
@@ -3172,7 +3607,7 @@ function lifetimeTracker(api) {
|
|
|
3172
3607
|
title: "Reviewer: Lifetime usage",
|
|
3173
3608
|
category: "Reviewer",
|
|
3174
3609
|
run: () => {
|
|
3175
|
-
|
|
3610
|
+
refresh.refresh();
|
|
3176
3611
|
api.ui.dialog.replace(() => _$createComponent(api.ui.DialogAlert, {
|
|
3177
3612
|
title: "Reviewer lifetime usage",
|
|
3178
3613
|
get message() {
|
|
@@ -3183,47 +3618,43 @@ function lifetimeTracker(api) {
|
|
|
3183
3618
|
}]
|
|
3184
3619
|
});
|
|
3185
3620
|
api.lifecycle.onDispose(() => {
|
|
3186
|
-
|
|
3621
|
+
refresh.dispose();
|
|
3187
3622
|
unregister();
|
|
3188
3623
|
});
|
|
3189
|
-
|
|
3624
|
+
refresh.refresh();
|
|
3190
3625
|
return {
|
|
3191
3626
|
// The controller must settle aborted review workers before this final queue drain.
|
|
3192
3627
|
flush: () => store.flush().catch(() => {
|
|
3193
3628
|
}),
|
|
3194
3629
|
text: () => unavailable() ? "lifetime: usage unavailable" : totals()?.requests ? lifetimeCost(totals()) : void 0,
|
|
3195
3630
|
record: (usage) => {
|
|
3196
|
-
void store.record(usage).then(refresh, () =>
|
|
3197
|
-
if (alive) {
|
|
3198
|
-
revision++;
|
|
3199
|
-
setUnavailable(true);
|
|
3200
|
-
}
|
|
3201
|
-
});
|
|
3631
|
+
void store.record(usage).then(() => refresh.refresh(), () => refresh.failed());
|
|
3202
3632
|
}
|
|
3203
3633
|
};
|
|
3204
3634
|
}
|
|
3205
3635
|
|
|
3206
3636
|
// src/tui.tsx
|
|
3207
|
-
import
|
|
3637
|
+
import path10 from "node:path";
|
|
3208
3638
|
|
|
3209
3639
|
// src/session-mode.ts
|
|
3210
3640
|
init_define_REVIEW_PROMPTS();
|
|
3211
3641
|
import { constants as constants4 } from "node:fs";
|
|
3212
3642
|
import { mkdir as mkdir2, open as open4, rename as rename2, unlink as unlink2 } from "node:fs/promises";
|
|
3213
3643
|
import { createHash, randomUUID as randomUUID2 } from "node:crypto";
|
|
3214
|
-
import
|
|
3644
|
+
import path9 from "node:path";
|
|
3645
|
+
var MAX_MODE_READS = 2;
|
|
3215
3646
|
var SessionModeStore = class {
|
|
3216
3647
|
constructor(directory, host) {
|
|
3217
3648
|
this.directory = directory;
|
|
3218
3649
|
this.host = host;
|
|
3219
|
-
if (!
|
|
3650
|
+
if (!path9.isAbsolute(directory)) throw new Error("Session mode directory must be absolute");
|
|
3220
3651
|
}
|
|
3221
3652
|
directory;
|
|
3222
3653
|
host;
|
|
3223
3654
|
pending = Promise.resolve();
|
|
3224
3655
|
file(root) {
|
|
3225
3656
|
const key = createHash("sha256").update(JSON.stringify([this.host, root])).digest("hex");
|
|
3226
|
-
return
|
|
3657
|
+
return path9.join(this.directory, `${key}.json`);
|
|
3227
3658
|
}
|
|
3228
3659
|
async read(root, signal) {
|
|
3229
3660
|
signal.throwIfAborted();
|
|
@@ -3238,12 +3669,14 @@ var SessionModeStore = class {
|
|
|
3238
3669
|
try {
|
|
3239
3670
|
signal.throwIfAborted();
|
|
3240
3671
|
const stat = await file.stat();
|
|
3672
|
+
signal.throwIfAborted();
|
|
3241
3673
|
if (!stat.isFile() || stat.size > 1024) throw new Error("Invalid session mode record");
|
|
3242
3674
|
const buffer = Buffer.alloc(1025);
|
|
3243
3675
|
let size = 0;
|
|
3244
3676
|
while (size < buffer.length) {
|
|
3245
3677
|
signal.throwIfAborted();
|
|
3246
3678
|
const result = await file.read(buffer, size, buffer.length - size, size);
|
|
3679
|
+
signal.throwIfAborted();
|
|
3247
3680
|
if (!result.bytesRead) break;
|
|
3248
3681
|
size += result.bytesRead;
|
|
3249
3682
|
}
|
|
@@ -3300,6 +3733,12 @@ var SessionModes = class {
|
|
|
3300
3733
|
roots = /* @__PURE__ */ new Map();
|
|
3301
3734
|
values = /* @__PURE__ */ new Map();
|
|
3302
3735
|
changes = /* @__PURE__ */ new Map();
|
|
3736
|
+
reads = /* @__PURE__ */ new Map();
|
|
3737
|
+
remember(visited, root, edges) {
|
|
3738
|
+
if (this.roots.size + visited.size > 4096) this.roots.clear();
|
|
3739
|
+
for (const child of visited) this.roots.set(child, { root, edges: edges-- });
|
|
3740
|
+
return root;
|
|
3741
|
+
}
|
|
3303
3742
|
root(sessionID, parent) {
|
|
3304
3743
|
return withDeadline(parent, 5e3, async (signal) => {
|
|
3305
3744
|
const visited = /* @__PURE__ */ new Set();
|
|
@@ -3308,29 +3747,57 @@ var SessionModes = class {
|
|
|
3308
3747
|
signal.throwIfAborted();
|
|
3309
3748
|
if (!id || visited.has(id)) throw new Error("Session ancestry unavailable");
|
|
3310
3749
|
const cached = this.roots.get(id);
|
|
3311
|
-
if (cached)
|
|
3750
|
+
if (cached) {
|
|
3751
|
+
if (depth + cached.edges > 16) throw new Error("Session ancestry limit reached");
|
|
3752
|
+
return this.remember(visited, cached.root, depth + cached.edges);
|
|
3753
|
+
}
|
|
3312
3754
|
visited.add(id);
|
|
3313
3755
|
const value = await this.session(id, signal);
|
|
3314
3756
|
signal.throwIfAborted();
|
|
3315
3757
|
if (value?.id !== id || value.parentID !== void 0 && (typeof value.parentID !== "string" || !value.parentID)) {
|
|
3316
3758
|
throw new Error("Session ancestry unavailable");
|
|
3317
3759
|
}
|
|
3318
|
-
if (!value.parentID)
|
|
3319
|
-
if (this.roots.size + visited.size > 4096) this.roots.clear();
|
|
3320
|
-
for (const child of visited) this.roots.set(child, id);
|
|
3321
|
-
return id;
|
|
3322
|
-
}
|
|
3760
|
+
if (!value.parentID) return this.remember(visited, id, depth);
|
|
3323
3761
|
id = value.parentID;
|
|
3324
3762
|
}
|
|
3325
3763
|
throw new Error("Session ancestry limit reached");
|
|
3326
3764
|
}, "Session mode ancestry");
|
|
3327
3765
|
}
|
|
3328
3766
|
async load(root, parent) {
|
|
3767
|
+
parent.throwIfAborted();
|
|
3329
3768
|
if (this.values.has(root)) return;
|
|
3769
|
+
const pending = this.reads.get(root);
|
|
3770
|
+
if (pending) return withDeadline(parent, 5e3, () => pending.wait, "Session mode loading");
|
|
3771
|
+
if (this.reads.size >= MAX_MODE_READS) throw new Error("Session mode read capacity unavailable");
|
|
3772
|
+
const read = { settled: true, finished: false };
|
|
3773
|
+
this.reads.set(root, read);
|
|
3774
|
+
return read.wait = this.loadMode(root, parent, read);
|
|
3775
|
+
}
|
|
3776
|
+
async loadMode(root, parent, read) {
|
|
3330
3777
|
const revision = this.changes.get(root);
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3778
|
+
try {
|
|
3779
|
+
const value = await withDeadline(parent, 5e3, (signal) => {
|
|
3780
|
+
read.settled = false;
|
|
3781
|
+
const worker = Promise.resolve().then(() => {
|
|
3782
|
+
signal.throwIfAborted();
|
|
3783
|
+
return this.store.read(root, signal);
|
|
3784
|
+
});
|
|
3785
|
+
const settled = () => {
|
|
3786
|
+
read.settled = true;
|
|
3787
|
+
this.releaseRead(root, read);
|
|
3788
|
+
};
|
|
3789
|
+
void worker.then(settled, settled);
|
|
3790
|
+
return worker;
|
|
3791
|
+
}, "Session mode loading");
|
|
3792
|
+
parent.throwIfAborted();
|
|
3793
|
+
if (this.changes.get(root) === revision) this.values.set(root, value);
|
|
3794
|
+
} finally {
|
|
3795
|
+
read.finished = true;
|
|
3796
|
+
this.releaseRead(root, read);
|
|
3797
|
+
}
|
|
3798
|
+
}
|
|
3799
|
+
releaseRead(root, read) {
|
|
3800
|
+
if (read.finished && read.settled && this.reads.get(root) === read) this.reads.delete(root);
|
|
3334
3801
|
}
|
|
3335
3802
|
enabled(root) {
|
|
3336
3803
|
return this.values.get(root) === true;
|
|
@@ -3341,7 +3808,7 @@ var SessionModes = class {
|
|
|
3341
3808
|
return this.store.write(root, enabled);
|
|
3342
3809
|
}
|
|
3343
3810
|
deleted(id) {
|
|
3344
|
-
for (const [child,
|
|
3811
|
+
for (const [child, value] of this.roots) if (child === id || value.root === id) this.roots.delete(child);
|
|
3345
3812
|
}
|
|
3346
3813
|
flush() {
|
|
3347
3814
|
return this.store.flush();
|
|
@@ -3697,15 +4164,15 @@ async function reviewTui(api, options, fileIO, observer) {
|
|
|
3697
4164
|
const hostTrace = observer ? new DiagnosticTrace(observer, 0) : void 0;
|
|
3698
4165
|
const traces = observer ? /* @__PURE__ */ new WeakMap() : void 0;
|
|
3699
4166
|
let reviewSequence = 0;
|
|
3700
|
-
const
|
|
3701
|
-
const modes = new SessionModes(new SessionModeStore(
|
|
4167
|
+
const reader2 = contextReader(api, hostTrace);
|
|
4168
|
+
const modes = new SessionModes(new SessionModeStore(path10.join(api.state.path.state, "opencode-reviewer", "session-mode-v1"), api.state.path.directory), async (id, signal) => api.state.session.get(id) ?? reader2.session(id, signal));
|
|
3702
4169
|
const files = new FileAccess(fileIO);
|
|
3703
4170
|
const approval = approvalTransport(api.client, api.state.path.directory);
|
|
3704
4171
|
let visibleApproval = () => void 0;
|
|
3705
4172
|
const controller = new Controller(async (request, parent, onIdentified, onProgress) => {
|
|
3706
4173
|
const trace = observer ? new DiagnosticTrace(observer, ++reviewSequence) : void 0;
|
|
3707
4174
|
if (trace) traces.set(request, trace);
|
|
3708
|
-
const reviewReader = trace ? contextReader(api, trace) :
|
|
4175
|
+
const reviewReader = trace ? contextReader(api, trace) : reader2;
|
|
3709
4176
|
let worker;
|
|
3710
4177
|
try {
|
|
3711
4178
|
return await withDeadline(parent, config?.timeoutMs ?? 3e4, async (signal) => {
|
|
@@ -3740,35 +4207,8 @@ async function reviewTui(api, options, fileIO, observer) {
|
|
|
3740
4207
|
modes.deleted(event.properties.info.id);
|
|
3741
4208
|
controller.deleted(event.properties.info.id);
|
|
3742
4209
|
});
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
let refreshAgain = false;
|
|
3746
|
-
const refresh = async () => {
|
|
3747
|
-
if (stopped) return;
|
|
3748
|
-
if (syncing) {
|
|
3749
|
-
refreshAgain = true;
|
|
3750
|
-
return;
|
|
3751
|
-
}
|
|
3752
|
-
syncing = true;
|
|
3753
|
-
const revision = controller.revision;
|
|
3754
|
-
try {
|
|
3755
|
-
await withDeadline(api.lifecycle.signal, 5e3, async (signal) => {
|
|
3756
|
-
const result = await measured(hostTrace, "pending-refresh", () => api.client.permission.list({
|
|
3757
|
-
directory: api.state.path.directory
|
|
3758
|
-
}, {
|
|
3759
|
-
signal
|
|
3760
|
-
}));
|
|
3761
|
-
if (result.data) controller.reconcile(result.data, revision);
|
|
3762
|
-
});
|
|
3763
|
-
} catch {
|
|
3764
|
-
} finally {
|
|
3765
|
-
syncing = false;
|
|
3766
|
-
if (refreshAgain) {
|
|
3767
|
-
refreshAgain = false;
|
|
3768
|
-
void refresh();
|
|
3769
|
-
}
|
|
3770
|
-
}
|
|
3771
|
-
};
|
|
4210
|
+
const pendingRefresh = new PendingRefresh(controller, (signal) => measured(hostTrace, "pending-refresh", () => approval.list(signal)), api.lifecycle.signal);
|
|
4211
|
+
const refresh = () => pendingRefresh.refresh();
|
|
3772
4212
|
const unregisterMode = sessionModeCommands(api, modes, (root) => {
|
|
3773
4213
|
controller.modeChanged(root);
|
|
3774
4214
|
void refresh();
|
|
@@ -3778,7 +4218,7 @@ async function reviewTui(api, options, fileIO, observer) {
|
|
|
3778
4218
|
api.event.on("session.idle", () => void refresh());
|
|
3779
4219
|
api.event.on("session.error", () => void refresh());
|
|
3780
4220
|
api.lifecycle.onDispose(async () => {
|
|
3781
|
-
|
|
4221
|
+
pendingRefresh.dispose();
|
|
3782
4222
|
clearInterval(interval);
|
|
3783
4223
|
setSidebar(void 0);
|
|
3784
4224
|
unregisterMode();
|
|
@@ -3912,7 +4352,7 @@ async function reviewTui(api, options, fileIO, observer) {
|
|
|
3912
4352
|
}), null);
|
|
3913
4353
|
_$insert(_el$14, _$createComponent2(Show, {
|
|
3914
4354
|
get when() {
|
|
3915
|
-
return view().status === "analyzing";
|
|
4355
|
+
return _$memo2(() => view().status === "analyzing")() && rating() === void 0;
|
|
3916
4356
|
},
|
|
3917
4357
|
get children() {
|
|
3918
4358
|
return _$createComponent2(ReviewLoading, {
|