@quikit/quikpilot-runner 9.1.0 → 9.2.0-rc.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 +17 -0
- package/dist/index.js +811 -26
- package/dist/index.js.map +4 -4
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -624,8 +624,8 @@ var TS_SIBLINGS = { ".js": [".ts", ".tsx"], ".jsx": [".tsx"], ".mjs": [".mts"],
|
|
|
624
624
|
function resolveImport(fromPath, specifier, listing) {
|
|
625
625
|
if (/^(?:@\/|~\/|#|\$|\/)/.test(specifier)) return { kind: "unresolved", reason: "alias" };
|
|
626
626
|
const clean = specifier.replace(/[?#].*$/, "");
|
|
627
|
-
const
|
|
628
|
-
if (!
|
|
627
|
+
const relative5 = clean === "." || clean === ".." || clean.startsWith("./") || clean.startsWith("../");
|
|
628
|
+
if (!relative5) return { kind: "external" };
|
|
629
629
|
const base = import_node_path.posix.normalize(import_node_path.posix.join(import_node_path.posix.dirname(fromPath), clean));
|
|
630
630
|
if (base === ".." || base.startsWith("../") || import_node_path.posix.isAbsolute(base)) return { kind: "unresolved", reason: "not found" };
|
|
631
631
|
const ext = import_node_path.posix.extname(base);
|
|
@@ -1563,8 +1563,8 @@ async function reportToQuikTest(params) {
|
|
|
1563
1563
|
try {
|
|
1564
1564
|
console.log(`reporting ${report}`);
|
|
1565
1565
|
const ciUrl = buildCiUrl(githubServerUrl, githubRepository, githubRunId);
|
|
1566
|
-
const { readFileSync:
|
|
1567
|
-
const body =
|
|
1566
|
+
const { readFileSync: readFileSync8 } = await import("node:fs");
|
|
1567
|
+
const body = readFileSync8(report);
|
|
1568
1568
|
let url = `${resultsUrl}?ciUrl=${encodeURIComponent(ciUrl)}&automationIdFormat=bracket`;
|
|
1569
1569
|
if (generatedFiles) {
|
|
1570
1570
|
url += `&generatedFiles=${encodeURIComponent(generatedFiles.join(","))}`;
|
|
@@ -1982,7 +1982,7 @@ var FAILURE_ARTIFACT_DIR = "quikpilot-failure";
|
|
|
1982
1982
|
var REPORT_COPY = "final-test-report.xml";
|
|
1983
1983
|
var MAX_REPORT_BYTES = 5 * 1024 * 1024;
|
|
1984
1984
|
var MAX_FILES = 200;
|
|
1985
|
-
var RUNNER_VERSION = true ? "9.
|
|
1985
|
+
var RUNNER_VERSION = true ? "9.2.0-rc.2" : "dev";
|
|
1986
1986
|
function safeDestination(base, repoPath) {
|
|
1987
1987
|
if (!repoPath || (0, import_node_path6.isAbsolute)(repoPath)) return null;
|
|
1988
1988
|
const destination = (0, import_node_path6.resolve)(base, repoPath);
|
|
@@ -2118,7 +2118,685 @@ function describeScopeViolations(check) {
|
|
|
2118
2118
|
}
|
|
2119
2119
|
|
|
2120
2120
|
// src/targeting.ts
|
|
2121
|
+
var import_node_path9 = require("node:path");
|
|
2122
|
+
|
|
2123
|
+
// src/targetingErrors.ts
|
|
2124
|
+
var DeniedPathError = class extends Error {
|
|
2125
|
+
constructor(message) {
|
|
2126
|
+
super(message);
|
|
2127
|
+
this.name = "DeniedPathError";
|
|
2128
|
+
}
|
|
2129
|
+
};
|
|
2130
|
+
var TargetingHttpError = class extends Error {
|
|
2131
|
+
status;
|
|
2132
|
+
constructor(status, statusText) {
|
|
2133
|
+
super(`Targeting call failed: ${status} ${statusText}`);
|
|
2134
|
+
this.name = "TargetingHttpError";
|
|
2135
|
+
this.status = status;
|
|
2136
|
+
}
|
|
2137
|
+
};
|
|
2138
|
+
|
|
2139
|
+
// src/targetingMode.ts
|
|
2140
|
+
var TARGETING_MODE_ENV = "QUIKPILOT_TARGETING_MODE";
|
|
2141
|
+
function parse(value) {
|
|
2142
|
+
if (typeof value !== "string") return null;
|
|
2143
|
+
const v = value.trim().toLowerCase();
|
|
2144
|
+
if (v === "loop") return "loop";
|
|
2145
|
+
if (v === "single") return "single";
|
|
2146
|
+
return null;
|
|
2147
|
+
}
|
|
2148
|
+
function resolveTargetingMode(env2 = process.env, packetSetting) {
|
|
2149
|
+
const fromSetting = parse(packetSetting);
|
|
2150
|
+
if (fromSetting) return fromSetting;
|
|
2151
|
+
return parse(env2[TARGETING_MODE_ENV]) ?? "single";
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
// src/targetingPaths.ts
|
|
2121
2155
|
var import_node_path7 = require("node:path");
|
|
2156
|
+
var MAX_PATH_CHARS = 300;
|
|
2157
|
+
function hasControlChar(s) {
|
|
2158
|
+
for (let i = 0; i < s.length; i++) {
|
|
2159
|
+
const c = s.charCodeAt(i);
|
|
2160
|
+
if (c < 32 || c === 127) return true;
|
|
2161
|
+
}
|
|
2162
|
+
return false;
|
|
2163
|
+
}
|
|
2164
|
+
function normaliseToolPath(raw, options = {}) {
|
|
2165
|
+
const allowRoot = options.allowRoot === true;
|
|
2166
|
+
if ((raw === void 0 || raw === null || raw === "") && allowRoot) return { ok: true, path: "" };
|
|
2167
|
+
if (typeof raw !== "string" || raw.length === 0 || raw.length > MAX_PATH_CHARS) {
|
|
2168
|
+
return { ok: false, reason: "path_invalid" };
|
|
2169
|
+
}
|
|
2170
|
+
if (hasControlChar(raw)) return { ok: false, reason: "path_invalid" };
|
|
2171
|
+
if (raw.includes("\\")) return { ok: false, reason: "path_invalid" };
|
|
2172
|
+
if (raw.startsWith("/") || /^[A-Za-z]:/.test(raw)) return { ok: false, reason: "path_invalid" };
|
|
2173
|
+
if (raw.split("/").includes("..")) return { ok: false, reason: "outside_workspace" };
|
|
2174
|
+
const normalised = import_node_path7.posix.normalize(raw).replace(/\/+$/, "");
|
|
2175
|
+
if (normalised === "" || normalised === ".") {
|
|
2176
|
+
return allowRoot ? { ok: true, path: "" } : { ok: false, reason: "path_invalid" };
|
|
2177
|
+
}
|
|
2178
|
+
return { ok: true, path: normalised };
|
|
2179
|
+
}
|
|
2180
|
+
function matchesPrefix(path, prefix) {
|
|
2181
|
+
if (prefix === "") return true;
|
|
2182
|
+
return path === prefix || path.startsWith(`${prefix}/`);
|
|
2183
|
+
}
|
|
2184
|
+
function buildVisibleListing(listing, deniedPaths) {
|
|
2185
|
+
return listing.filter((p) => !matchesDeniedPath(p, deniedPaths)).sort();
|
|
2186
|
+
}
|
|
2187
|
+
|
|
2188
|
+
// src/targetingTools.ts
|
|
2189
|
+
var import_node_fs9 = require("node:fs");
|
|
2190
|
+
var import_node_path8 = require("node:path");
|
|
2191
|
+
var DEFAULT_LIMITS = {
|
|
2192
|
+
maxTurns: 12,
|
|
2193
|
+
maxToolCalls: 25,
|
|
2194
|
+
maxCallsPerTurn: 8,
|
|
2195
|
+
readCharsPerRead: 2e4,
|
|
2196
|
+
// The per-run read cap stops the COST; the per-turn ceiling below stops the FAILURE. Different jobs.
|
|
2197
|
+
readCharsTotal: 6e4,
|
|
2198
|
+
resultCharsTotal: 8e4,
|
|
2199
|
+
// 131,072 (the smallest 128k-class window) - 8,192 reserved output - a 3,741-token real request leaves
|
|
2200
|
+
// ~116,000; 90,000 keeps ~26,000 for a repository whose listing is thousands of paths, and stays well
|
|
2201
|
+
// clear of the per-minute quota, which binds before the window does. Set by the AI Runtime owner.
|
|
2202
|
+
maxRequestTokens: 9e4,
|
|
2203
|
+
grepMaxMatches: 40,
|
|
2204
|
+
grepLineChars: 200,
|
|
2205
|
+
grepFileBytes: 262144,
|
|
2206
|
+
grepMaxFiles: 3e3,
|
|
2207
|
+
grepMaxBytes: 2e7,
|
|
2208
|
+
fileBytesMax: 1e6,
|
|
2209
|
+
listMaxEntries: 500,
|
|
2210
|
+
maxReasoningChars: 500,
|
|
2211
|
+
maxPatternChars: 200
|
|
2212
|
+
};
|
|
2213
|
+
function createNodeToolFs(rootDir = process.cwd()) {
|
|
2214
|
+
const rootReal = (0, import_node_fs9.realpathSync)(rootDir);
|
|
2215
|
+
return {
|
|
2216
|
+
rootReal,
|
|
2217
|
+
lstat(rel) {
|
|
2218
|
+
const s = (0, import_node_fs9.lstatSync)((0, import_node_path8.join)(rootDir, rel));
|
|
2219
|
+
return { isSymlink: s.isSymbolicLink(), isFile: s.isFile(), size: s.size };
|
|
2220
|
+
},
|
|
2221
|
+
realpathInside(rel) {
|
|
2222
|
+
const real = (0, import_node_fs9.realpathSync)((0, import_node_path8.join)(rootDir, rel));
|
|
2223
|
+
const r = (0, import_node_path8.relative)(rootReal, real);
|
|
2224
|
+
return r !== "" && !r.startsWith("..") && !(0, import_node_path8.isAbsolute)(r);
|
|
2225
|
+
},
|
|
2226
|
+
readBytes(rel) {
|
|
2227
|
+
return (0, import_node_fs9.readFileSync)((0, import_node_path8.join)(rootDir, rel));
|
|
2228
|
+
}
|
|
2229
|
+
};
|
|
2230
|
+
}
|
|
2231
|
+
function newLedger() {
|
|
2232
|
+
return { toolCalls: 0, readChars: 0, resultChars: 0, refused: 0, seen: /* @__PURE__ */ new Map() };
|
|
2233
|
+
}
|
|
2234
|
+
var INVALID_PATH = "(invalid path)";
|
|
2235
|
+
var CUT = "\u2026[line cut]";
|
|
2236
|
+
function isPlainObject(x) {
|
|
2237
|
+
return typeof x === "object" && x !== null && !Array.isArray(x);
|
|
2238
|
+
}
|
|
2239
|
+
function bad(message) {
|
|
2240
|
+
return { fail: { status: "bad_args", message } };
|
|
2241
|
+
}
|
|
2242
|
+
function optionalInt(v) {
|
|
2243
|
+
if (v === void 0 || v === null) return void 0;
|
|
2244
|
+
return typeof v === "number" && Number.isInteger(v) ? v : "bad";
|
|
2245
|
+
}
|
|
2246
|
+
function hasNul(bytes) {
|
|
2247
|
+
const n = Math.min(bytes.length, 8e3);
|
|
2248
|
+
for (let i = 0; i < n; i++) if (bytes[i] === 0) return true;
|
|
2249
|
+
return false;
|
|
2250
|
+
}
|
|
2251
|
+
function gatePath(raw, env2) {
|
|
2252
|
+
const n = normaliseToolPath(raw);
|
|
2253
|
+
if (!n.ok) {
|
|
2254
|
+
return { ok: false, path: INVALID_PATH, fail: { status: "refused", reason: n.reason, message: "That path is not allowed." } };
|
|
2255
|
+
}
|
|
2256
|
+
if (matchesDeniedPath(n.path, env2.deniedPaths)) {
|
|
2257
|
+
return { ok: false, path: n.path, fail: { status: "refused", reason: "denied", message: "That path is protected and cannot be read." } };
|
|
2258
|
+
}
|
|
2259
|
+
if (!env2.listing.has(n.path)) {
|
|
2260
|
+
return { ok: false, path: n.path, fail: { status: "not_found", message: "Not a tracked file. Use list_files to see the available paths." } };
|
|
2261
|
+
}
|
|
2262
|
+
return { ok: true, path: n.path };
|
|
2263
|
+
}
|
|
2264
|
+
function prepareReadFile(args, env2, ledger) {
|
|
2265
|
+
const start = optionalInt(args.startLine);
|
|
2266
|
+
const end = optionalInt(args.endLine);
|
|
2267
|
+
if (start === "bad" || end === "bad") return bad("startLine and endLine must be whole numbers.");
|
|
2268
|
+
if (start !== void 0 && end !== void 0 && end < start) return bad("endLine must not be before startLine.");
|
|
2269
|
+
if (typeof args.path !== "string") return bad("path must be a string.");
|
|
2270
|
+
const gate = gatePath(args.path, env2);
|
|
2271
|
+
if (!gate.ok) {
|
|
2272
|
+
const read = gate.fail.status === "refused" ? { path: gate.path, chars: 0, refused: true } : void 0;
|
|
2273
|
+
return { fail: gate.fail, ...read ? { read } : {} };
|
|
2274
|
+
}
|
|
2275
|
+
const path = gate.path;
|
|
2276
|
+
const key = JSON.stringify({ path, startLine: start ?? null, endLine: end ?? null });
|
|
2277
|
+
return {
|
|
2278
|
+
key,
|
|
2279
|
+
exec: () => {
|
|
2280
|
+
let size;
|
|
2281
|
+
try {
|
|
2282
|
+
const st = env2.fs.lstat(path);
|
|
2283
|
+
if (st.isSymlink) {
|
|
2284
|
+
return { result: { status: "refused", reason: "symlink", message: "Links are not followed." }, read: { path, chars: 0, refused: true } };
|
|
2285
|
+
}
|
|
2286
|
+
if (!st.isFile) return { result: { status: "unreadable", reason: "not_a_file", message: "That entry is not a regular file." } };
|
|
2287
|
+
size = st.size;
|
|
2288
|
+
if (size > env2.limits.fileBytesMax) {
|
|
2289
|
+
return { result: { status: "unreadable", reason: "too_large", message: "That file is too large to read." } };
|
|
2290
|
+
}
|
|
2291
|
+
if (!env2.fs.realpathInside(path)) {
|
|
2292
|
+
return { result: { status: "refused", reason: "outside_workspace", message: "That path is not allowed." }, read: { path, chars: 0, refused: true } };
|
|
2293
|
+
}
|
|
2294
|
+
} catch {
|
|
2295
|
+
return { result: { status: "unreadable", reason: "io", message: "That file could not be read." } };
|
|
2296
|
+
}
|
|
2297
|
+
let bytes;
|
|
2298
|
+
try {
|
|
2299
|
+
bytes = env2.fs.readBytes(path);
|
|
2300
|
+
} catch {
|
|
2301
|
+
return { result: { status: "unreadable", reason: "io", message: "That file could not be read." } };
|
|
2302
|
+
}
|
|
2303
|
+
if (hasNul(bytes)) return { result: { status: "unreadable", reason: "binary", message: "That file is binary." } };
|
|
2304
|
+
let text;
|
|
2305
|
+
try {
|
|
2306
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
2307
|
+
} catch {
|
|
2308
|
+
return { result: { status: "unreadable", reason: "not_utf8", message: "That file is not valid UTF-8 text." } };
|
|
2309
|
+
}
|
|
2310
|
+
if (text.charCodeAt(0) === 65279) text = text.slice(1);
|
|
2311
|
+
text = text.replace(/\r\n/g, "\n");
|
|
2312
|
+
const lines = text === "" ? [] : text.split("\n");
|
|
2313
|
+
if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
|
|
2314
|
+
const totalLines = lines.length;
|
|
2315
|
+
const startLine = Math.max(1, start ?? 1);
|
|
2316
|
+
const endWanted = Math.min(totalLines, end ?? totalLines);
|
|
2317
|
+
if (startLine > totalLines) {
|
|
2318
|
+
return {
|
|
2319
|
+
result: { status: "ok", output: { path, startLine, endLine: totalLines, totalLines, truncated: false, content: "" } },
|
|
2320
|
+
read: { path, startLine, endLine: totalLines, chars: 0 }
|
|
2321
|
+
};
|
|
2322
|
+
}
|
|
2323
|
+
const budget = Math.max(0, Math.min(env2.limits.readCharsPerRead, env2.limits.readCharsTotal - ledger.readChars));
|
|
2324
|
+
const out = [];
|
|
2325
|
+
let used = 0;
|
|
2326
|
+
let endLine = startLine - 1;
|
|
2327
|
+
let truncated = false;
|
|
2328
|
+
for (let n = startLine; n <= endWanted; n++) {
|
|
2329
|
+
const line = `${n} ${lines[n - 1]}`;
|
|
2330
|
+
const cost = line.length + (out.length > 0 ? 1 : 0);
|
|
2331
|
+
if (used + cost <= budget) {
|
|
2332
|
+
out.push(line);
|
|
2333
|
+
used += cost;
|
|
2334
|
+
endLine = n;
|
|
2335
|
+
continue;
|
|
2336
|
+
}
|
|
2337
|
+
truncated = true;
|
|
2338
|
+
if (out.length === 0) {
|
|
2339
|
+
const keep = Math.max(0, budget - CUT.length);
|
|
2340
|
+
out.push(line.slice(0, keep) + CUT);
|
|
2341
|
+
used = out[0].length;
|
|
2342
|
+
endLine = n;
|
|
2343
|
+
}
|
|
2344
|
+
break;
|
|
2345
|
+
}
|
|
2346
|
+
let content = out.join("\n");
|
|
2347
|
+
ledger.readChars += used;
|
|
2348
|
+
const output = { path, startLine, endLine, totalLines, truncated };
|
|
2349
|
+
if (truncated) {
|
|
2350
|
+
if (endLine < totalLines) {
|
|
2351
|
+
output.nextStartLine = endLine + 1;
|
|
2352
|
+
content += `
|
|
2353
|
+
[truncated: showing lines ${startLine}-${endLine} of ${totalLines}; call read_file with startLine=${endLine + 1}]`;
|
|
2354
|
+
} else {
|
|
2355
|
+
content += `
|
|
2356
|
+
[truncated: line ${endLine} was cut]`;
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
output.content = content;
|
|
2360
|
+
return { result: { status: "ok", output }, read: { path, startLine, endLine, chars: used } };
|
|
2361
|
+
}
|
|
2362
|
+
};
|
|
2363
|
+
}
|
|
2364
|
+
function prepareListFiles(args, env2) {
|
|
2365
|
+
const max = optionalInt(args.maxEntries);
|
|
2366
|
+
if (max === "bad" || max !== void 0 && max < 1) return bad("maxEntries must be a whole number of at least 1.");
|
|
2367
|
+
if (args.prefix !== void 0 && args.prefix !== null && typeof args.prefix !== "string") return bad("prefix must be a string.");
|
|
2368
|
+
const n = normaliseToolPath(args.prefix, { allowRoot: true });
|
|
2369
|
+
if (!n.ok) return { fail: { status: "refused", reason: n.reason, message: "That path is not allowed." } };
|
|
2370
|
+
const prefix = n.path;
|
|
2371
|
+
const cap = Math.min(max ?? env2.limits.listMaxEntries, env2.limits.listMaxEntries);
|
|
2372
|
+
return {
|
|
2373
|
+
key: JSON.stringify({ prefix, maxEntries: cap }),
|
|
2374
|
+
exec: () => {
|
|
2375
|
+
const matching = env2.visible.filter((p) => matchesPrefix(p, prefix));
|
|
2376
|
+
const entries = matching.slice(0, cap);
|
|
2377
|
+
const truncated = matching.length > cap;
|
|
2378
|
+
const output = { entries, total: matching.length, truncated };
|
|
2379
|
+
if (truncated) {
|
|
2380
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2381
|
+
for (const p of matching) {
|
|
2382
|
+
const rest = prefix === "" ? p : p.slice(prefix.length + 1);
|
|
2383
|
+
const slash = rest.indexOf("/");
|
|
2384
|
+
if (slash > 0) counts.set(rest.slice(0, slash), (counts.get(rest.slice(0, slash)) ?? 0) + 1);
|
|
2385
|
+
}
|
|
2386
|
+
output.subdirs = [...counts.entries()].map(([name, count]) => ({ name, count })).sort((a, b) => b.count - a.count || (a.name < b.name ? -1 : 1)).slice(0, 50);
|
|
2387
|
+
}
|
|
2388
|
+
return { result: { status: "ok", output } };
|
|
2389
|
+
}
|
|
2390
|
+
};
|
|
2391
|
+
}
|
|
2392
|
+
var WORD_CHAR = /[A-Za-z0-9_$]/;
|
|
2393
|
+
function lineMatches(line, needle, ignoreCase, wholeWord) {
|
|
2394
|
+
const hay = ignoreCase ? line.toLowerCase() : line;
|
|
2395
|
+
let from = 0;
|
|
2396
|
+
for (; ; ) {
|
|
2397
|
+
const i = hay.indexOf(needle, from);
|
|
2398
|
+
if (i === -1) return -1;
|
|
2399
|
+
if (!wholeWord) return i;
|
|
2400
|
+
const before = i > 0 ? hay[i - 1] : "";
|
|
2401
|
+
const after = i + needle.length < hay.length ? hay[i + needle.length] : "";
|
|
2402
|
+
if (!(before && WORD_CHAR.test(before)) && !(after && WORD_CHAR.test(after))) return i;
|
|
2403
|
+
from = i + 1;
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
function excerpt(line, at, max) {
|
|
2407
|
+
const t = line.trim();
|
|
2408
|
+
if (t.length <= max) return t;
|
|
2409
|
+
const lead = line.length - line.trimStart().length;
|
|
2410
|
+
const pos = Math.max(0, at - lead);
|
|
2411
|
+
const from = Math.max(0, Math.min(pos - 40, t.length - max));
|
|
2412
|
+
const piece = t.slice(from, from + max);
|
|
2413
|
+
return `${from > 0 ? "\u2026" : ""}${piece}${from + max < t.length ? "\u2026" : ""}`;
|
|
2414
|
+
}
|
|
2415
|
+
function prepareGrep(args, env2) {
|
|
2416
|
+
const pattern = args.pattern;
|
|
2417
|
+
if (typeof pattern !== "string" || pattern.length === 0 || pattern.length > env2.limits.maxPatternChars) {
|
|
2418
|
+
return bad("pattern must be a non-empty string within the length limit.");
|
|
2419
|
+
}
|
|
2420
|
+
if (/[\u0000\r\n]/.test(pattern)) return bad("pattern must be a single line of plain text.");
|
|
2421
|
+
for (const flag of ["ignoreCase", "wholeWord"]) {
|
|
2422
|
+
if (args[flag] !== void 0 && typeof args[flag] !== "boolean") return bad(`${flag} must be true or false.`);
|
|
2423
|
+
}
|
|
2424
|
+
const maxIn = optionalInt(args.maxResults);
|
|
2425
|
+
if (maxIn === "bad" || maxIn !== void 0 && maxIn < 1) return bad("maxResults must be a whole number of at least 1.");
|
|
2426
|
+
if (args.pathPrefix !== void 0 && args.pathPrefix !== null && typeof args.pathPrefix !== "string") return bad("pathPrefix must be a string.");
|
|
2427
|
+
const n = normaliseToolPath(args.pathPrefix, { allowRoot: true });
|
|
2428
|
+
if (!n.ok) return { fail: { status: "refused", reason: n.reason, message: "That path is not allowed." } };
|
|
2429
|
+
const prefix = n.path;
|
|
2430
|
+
const ignoreCase = args.ignoreCase === true;
|
|
2431
|
+
const wholeWord = args.wholeWord === true;
|
|
2432
|
+
const maxMatches = Math.min(maxIn ?? env2.limits.grepMaxMatches, env2.limits.grepMaxMatches);
|
|
2433
|
+
const needle = ignoreCase ? pattern.toLowerCase() : pattern;
|
|
2434
|
+
return {
|
|
2435
|
+
key: JSON.stringify({ pattern, pathPrefix: prefix, maxResults: maxMatches, ignoreCase, wholeWord }),
|
|
2436
|
+
exec: () => {
|
|
2437
|
+
const matches = [];
|
|
2438
|
+
const skipped = { large: 0, binary: 0 };
|
|
2439
|
+
let filesScanned = 0;
|
|
2440
|
+
let bytesScanned = 0;
|
|
2441
|
+
let scanCapped = false;
|
|
2442
|
+
let truncated = false;
|
|
2443
|
+
scan: for (const path of env2.visible) {
|
|
2444
|
+
if (!matchesPrefix(path, prefix)) continue;
|
|
2445
|
+
if (filesScanned >= env2.limits.grepMaxFiles || bytesScanned >= env2.limits.grepMaxBytes) {
|
|
2446
|
+
scanCapped = true;
|
|
2447
|
+
break;
|
|
2448
|
+
}
|
|
2449
|
+
filesScanned += 1;
|
|
2450
|
+
let bytes;
|
|
2451
|
+
try {
|
|
2452
|
+
const st = env2.fs.lstat(path);
|
|
2453
|
+
if (st.isSymlink || !st.isFile) continue;
|
|
2454
|
+
if (st.size > env2.limits.grepFileBytes) {
|
|
2455
|
+
skipped.large += 1;
|
|
2456
|
+
continue;
|
|
2457
|
+
}
|
|
2458
|
+
if (!env2.fs.realpathInside(path)) continue;
|
|
2459
|
+
bytes = env2.fs.readBytes(path);
|
|
2460
|
+
} catch {
|
|
2461
|
+
continue;
|
|
2462
|
+
}
|
|
2463
|
+
bytesScanned += bytes.length;
|
|
2464
|
+
if (hasNul(bytes)) {
|
|
2465
|
+
skipped.binary += 1;
|
|
2466
|
+
continue;
|
|
2467
|
+
}
|
|
2468
|
+
const lines = new TextDecoder("utf-8").decode(bytes).replace(/\r\n/g, "\n").split("\n");
|
|
2469
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2470
|
+
const at = lineMatches(lines[i], needle, ignoreCase, wholeWord);
|
|
2471
|
+
if (at === -1) continue;
|
|
2472
|
+
if (matches.length >= maxMatches) {
|
|
2473
|
+
truncated = true;
|
|
2474
|
+
break scan;
|
|
2475
|
+
}
|
|
2476
|
+
matches.push({ path, line: i + 1, text: excerpt(lines[i], at, env2.limits.grepLineChars) });
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
const output = { matches, truncated, filesScanned, skipped };
|
|
2480
|
+
if (scanCapped) output.scanCapped = true;
|
|
2481
|
+
return { result: { status: "ok", output } };
|
|
2482
|
+
}
|
|
2483
|
+
};
|
|
2484
|
+
}
|
|
2485
|
+
function budgetResult(reason) {
|
|
2486
|
+
return { status: "budget_exhausted", reason, message: "That limit has been reached; give your answer from what you have." };
|
|
2487
|
+
}
|
|
2488
|
+
function executeToolCall(call, env2, ledger) {
|
|
2489
|
+
const wrap = (r) => ({ id: call.id, name: call.name, ...r });
|
|
2490
|
+
const finish = (r, read) => {
|
|
2491
|
+
const result = wrap(r);
|
|
2492
|
+
ledger.resultChars += JSON.stringify(result).length;
|
|
2493
|
+
if (result.status === "refused") ledger.refused += 1;
|
|
2494
|
+
return read ? { result, read } : { result };
|
|
2495
|
+
};
|
|
2496
|
+
if (ledger.toolCalls >= env2.limits.maxToolCalls) return finish(budgetResult("tool_calls"));
|
|
2497
|
+
if (ledger.resultChars >= env2.limits.resultCharsTotal) return finish(budgetResult("result_chars"));
|
|
2498
|
+
if (call.name === "read_file" && ledger.readChars >= env2.limits.readCharsTotal) return finish(budgetResult("read_chars"));
|
|
2499
|
+
ledger.toolCalls += 1;
|
|
2500
|
+
let prepared;
|
|
2501
|
+
try {
|
|
2502
|
+
if (call.name !== "read_file" && call.name !== "list_files" && call.name !== "grep") {
|
|
2503
|
+
return finish({ status: "unknown_tool", message: "Unknown tool. Use list_files, read_file or grep." });
|
|
2504
|
+
}
|
|
2505
|
+
if (!isPlainObject(call.args)) return finish({ status: "bad_args", message: "Arguments must be an object." });
|
|
2506
|
+
prepared = call.name === "read_file" ? prepareReadFile(call.args, env2, ledger) : call.name === "list_files" ? prepareListFiles(call.args, env2) : prepareGrep(call.args, env2);
|
|
2507
|
+
} catch {
|
|
2508
|
+
return finish({ status: "bad_args", message: "Those arguments could not be used." });
|
|
2509
|
+
}
|
|
2510
|
+
if ("fail" in prepared) return finish(prepared.fail, prepared.read);
|
|
2511
|
+
const dupKey = `${call.name}:${prepared.key}`;
|
|
2512
|
+
const earlier = ledger.seen.get(dupKey);
|
|
2513
|
+
if (earlier !== void 0) {
|
|
2514
|
+
return finish({ status: "duplicate", reason: earlier, message: "Identical call already made; see its result." });
|
|
2515
|
+
}
|
|
2516
|
+
ledger.seen.set(dupKey, call.id);
|
|
2517
|
+
let executed;
|
|
2518
|
+
try {
|
|
2519
|
+
executed = prepared.exec();
|
|
2520
|
+
} catch {
|
|
2521
|
+
executed = { result: { status: "unreadable", reason: "io", message: "That could not be read." } };
|
|
2522
|
+
}
|
|
2523
|
+
return finish(executed.result, executed.read);
|
|
2524
|
+
}
|
|
2525
|
+
|
|
2526
|
+
// src/tokenEstimate.ts
|
|
2527
|
+
var TOKENS_PER_ALPHA_CHAR = 0.3;
|
|
2528
|
+
var TOKENS_PER_DIGIT_CHAR = 0.18;
|
|
2529
|
+
var TOKENS_PER_SPACE_CHAR = 0.05;
|
|
2530
|
+
var TOKENS_PER_PUNCTUATION_CHAR = 1.05;
|
|
2531
|
+
var TOKENS_PER_NON_ASCII_CHAR = 1;
|
|
2532
|
+
var PROMPT_OVERHEAD_TOKENS = 5e3;
|
|
2533
|
+
function estimateTokens(text) {
|
|
2534
|
+
let alpha = 0;
|
|
2535
|
+
let digit = 0;
|
|
2536
|
+
let space = 0;
|
|
2537
|
+
let punct = 0;
|
|
2538
|
+
let nonAscii = 0;
|
|
2539
|
+
for (const ch of text) {
|
|
2540
|
+
const code = ch.codePointAt(0);
|
|
2541
|
+
if (code >= 128) nonAscii++;
|
|
2542
|
+
else if (code >= 65 && code <= 90 || code >= 97 && code <= 122) alpha++;
|
|
2543
|
+
else if (code >= 48 && code <= 57) digit++;
|
|
2544
|
+
else if (code === 32 || code === 9 || code === 13 || code === 10) space++;
|
|
2545
|
+
else punct++;
|
|
2546
|
+
}
|
|
2547
|
+
return Math.ceil(
|
|
2548
|
+
alpha * TOKENS_PER_ALPHA_CHAR + digit * TOKENS_PER_DIGIT_CHAR + space * TOKENS_PER_SPACE_CHAR + punct * TOKENS_PER_PUNCTUATION_CHAR + nonAscii * TOKENS_PER_NON_ASCII_CHAR
|
|
2549
|
+
);
|
|
2550
|
+
}
|
|
2551
|
+
function estimateRequestTokens(contextData) {
|
|
2552
|
+
return PROMPT_OVERHEAD_TOKENS + estimateTokens(JSON.stringify(contextData ?? {}, null, 2));
|
|
2553
|
+
}
|
|
2554
|
+
|
|
2555
|
+
// src/targetingLoop.ts
|
|
2556
|
+
var LOOP_CONTRACT_VERSION = "targeting-loop-1";
|
|
2557
|
+
var LoopFailure = class extends Error {
|
|
2558
|
+
reason;
|
|
2559
|
+
runtimeCode;
|
|
2560
|
+
/** What the loop had done when it failed, so reads made before the failure are not lost. */
|
|
2561
|
+
summary;
|
|
2562
|
+
constructor(reason, runtimeCode, cause) {
|
|
2563
|
+
super(`pilot.targeting loop failed: ${reason}${runtimeCode ? ` (${runtimeCode})` : ""}`);
|
|
2564
|
+
this.name = "LoopFailure";
|
|
2565
|
+
this.reason = reason;
|
|
2566
|
+
this.runtimeCode = runtimeCode;
|
|
2567
|
+
if (cause !== void 0) this.cause = cause;
|
|
2568
|
+
}
|
|
2569
|
+
};
|
|
2570
|
+
var SafetyRefusalError = class extends RuntimeReplyError {
|
|
2571
|
+
constructor(message, summary) {
|
|
2572
|
+
super(message, summary);
|
|
2573
|
+
this.name = "SafetyRefusalError";
|
|
2574
|
+
}
|
|
2575
|
+
};
|
|
2576
|
+
var NO_FALLBACK_CODES = /* @__PURE__ */ new Set(["RUN_BUDGET_EXCEEDED", "RUN_CREDENTIAL_SCOPE"]);
|
|
2577
|
+
var TOOL_NAMES = /* @__PURE__ */ new Set(["list_files", "read_file", "grep"]);
|
|
2578
|
+
var MAX_ID_CHARS = 64;
|
|
2579
|
+
var MAX_CALLS_IN_A_REPLY = 50;
|
|
2580
|
+
var MAX_ARGS_JSON_CHARS = 1e3;
|
|
2581
|
+
function isPlainObject2(x) {
|
|
2582
|
+
return typeof x === "object" && x !== null && !Array.isArray(x);
|
|
2583
|
+
}
|
|
2584
|
+
function safeArgs(args) {
|
|
2585
|
+
try {
|
|
2586
|
+
return JSON.stringify(args).length <= MAX_ARGS_JSON_CHARS ? args : { _omitted: "too large" };
|
|
2587
|
+
} catch {
|
|
2588
|
+
return { _omitted: "unreadable" };
|
|
2589
|
+
}
|
|
2590
|
+
}
|
|
2591
|
+
function classifyTransport(err) {
|
|
2592
|
+
if (err instanceof TargetingHttpError) return new LoopFailure("http_error", void 0, err);
|
|
2593
|
+
const name = err?.name;
|
|
2594
|
+
if (name === "TimeoutError" || name === "AbortError") return new LoopFailure("turn_timeout", void 0, err);
|
|
2595
|
+
return new LoopFailure("transport", void 0, err);
|
|
2596
|
+
}
|
|
2597
|
+
async function runTargetingLoop(args, deps) {
|
|
2598
|
+
const limits = { ...DEFAULT_LIMITS, ...deps.limits };
|
|
2599
|
+
const now = deps.now ?? Date.now;
|
|
2600
|
+
const log = deps.log ?? ((e) => console.log(JSON.stringify(e)));
|
|
2601
|
+
const turnTimeoutMs = deps.turnTimeoutMs ?? 18e4;
|
|
2602
|
+
const deadlineMs = deps.deadlineMs ?? 6e5;
|
|
2603
|
+
const env2 = {
|
|
2604
|
+
listing: new Set(args.listing),
|
|
2605
|
+
visible: buildVisibleListing(args.listing, args.deniedPaths),
|
|
2606
|
+
deniedPaths: args.deniedPaths,
|
|
2607
|
+
fs: deps.fs ?? createNodeToolFs(),
|
|
2608
|
+
limits
|
|
2609
|
+
};
|
|
2610
|
+
const ledger = newLedger();
|
|
2611
|
+
const reads = [];
|
|
2612
|
+
const transcript = [];
|
|
2613
|
+
const toolResults = [];
|
|
2614
|
+
const usedIds = /* @__PURE__ */ new Set();
|
|
2615
|
+
let turns = 0;
|
|
2616
|
+
let noProgressStreak = 0;
|
|
2617
|
+
let forced = null;
|
|
2618
|
+
const started = now();
|
|
2619
|
+
const buildContext = (turn, isFinal, finalReason) => ({
|
|
2620
|
+
...args.baseContext,
|
|
2621
|
+
contractVersion: LOOP_CONTRACT_VERSION,
|
|
2622
|
+
loop: {
|
|
2623
|
+
turn,
|
|
2624
|
+
maxTurns: limits.maxTurns,
|
|
2625
|
+
final: isFinal,
|
|
2626
|
+
...isFinal ? { finalReason } : {},
|
|
2627
|
+
remaining: {
|
|
2628
|
+
toolCalls: Math.max(0, limits.maxToolCalls - ledger.toolCalls),
|
|
2629
|
+
readChars: Math.max(0, limits.readCharsTotal - ledger.readChars)
|
|
2630
|
+
},
|
|
2631
|
+
transcript,
|
|
2632
|
+
toolResults
|
|
2633
|
+
}
|
|
2634
|
+
});
|
|
2635
|
+
const snapshot = (stopReason) => ({
|
|
2636
|
+
turns,
|
|
2637
|
+
toolCalls: ledger.toolCalls,
|
|
2638
|
+
readChars: ledger.readChars,
|
|
2639
|
+
refused: ledger.refused,
|
|
2640
|
+
stopReason,
|
|
2641
|
+
reads: [...reads]
|
|
2642
|
+
});
|
|
2643
|
+
log({ event: "targeting.loop_start", mode: "loop", maxTurns: limits.maxTurns, maxToolCalls: limits.maxToolCalls, listingFiles: args.listing.length });
|
|
2644
|
+
try {
|
|
2645
|
+
for (let turn = 1; turn <= limits.maxTurns; turn++) {
|
|
2646
|
+
turns = turn;
|
|
2647
|
+
if (now() - started > deadlineMs) throw new LoopFailure("loop_deadline");
|
|
2648
|
+
const isFinal = forced !== null || turn === limits.maxTurns;
|
|
2649
|
+
const finalReason = forced ?? "max_turns";
|
|
2650
|
+
const contextData = buildContext(turn, isFinal, finalReason);
|
|
2651
|
+
const requestChars = JSON.stringify(contextData).length;
|
|
2652
|
+
const requestTokens = estimateRequestTokens(contextData);
|
|
2653
|
+
if (requestTokens > limits.maxRequestTokens) {
|
|
2654
|
+
log({ event: "targeting.loop_ceiling", turn, action: "not_sent", projectedTokens: requestTokens, ceiling: limits.maxRequestTokens });
|
|
2655
|
+
throw new LoopFailure("token_ceiling");
|
|
2656
|
+
}
|
|
2657
|
+
const turnStarted = now();
|
|
2658
|
+
let envelope;
|
|
2659
|
+
try {
|
|
2660
|
+
envelope = await deps.post(contextData, AbortSignal.timeout(turnTimeoutMs));
|
|
2661
|
+
} catch (err) {
|
|
2662
|
+
throw classifyTransport(err);
|
|
2663
|
+
}
|
|
2664
|
+
const code = envelope?.errorCode;
|
|
2665
|
+
try {
|
|
2666
|
+
checkHardFailure(envelope, "pilot.targeting");
|
|
2667
|
+
} catch (err) {
|
|
2668
|
+
throw new LoopFailure(code === "CONTRACT_REJECTED" ? "contract_rejected" : "semantic_mismatch", String(code), err);
|
|
2669
|
+
}
|
|
2670
|
+
if (envelope?.success === false) {
|
|
2671
|
+
const runtimeCode = typeof code === "string" ? code : void 0;
|
|
2672
|
+
if (runtimeCode && NO_FALLBACK_CODES.has(runtimeCode)) {
|
|
2673
|
+
let inner;
|
|
2674
|
+
try {
|
|
2675
|
+
unwrapExecute(envelope, "pilot.targeting");
|
|
2676
|
+
} catch (e) {
|
|
2677
|
+
inner = e instanceof RuntimeReplyError ? e : void 0;
|
|
2678
|
+
}
|
|
2679
|
+
const fallbackText = `pilot.targeting failed: ${runtimeCode}`;
|
|
2680
|
+
throw new SafetyRefusalError(inner?.message ?? fallbackText, inner?.summary ?? fallbackText);
|
|
2681
|
+
}
|
|
2682
|
+
throw new LoopFailure("runtime_failed", runtimeCode);
|
|
2683
|
+
}
|
|
2684
|
+
let body;
|
|
2685
|
+
try {
|
|
2686
|
+
body = unwrapExecute(envelope, "pilot.targeting");
|
|
2687
|
+
} catch (err) {
|
|
2688
|
+
throw new LoopFailure("bad_reply", void 0, err);
|
|
2689
|
+
}
|
|
2690
|
+
if (body.outcome === "TARGETED" || body.outcome === "NO_TARGET") {
|
|
2691
|
+
log({ event: "targeting.loop_turn", turn, outcome: "final", requestChars, requestTokens, elapsedMs: now() - turnStarted, calls: [] });
|
|
2692
|
+
const stopReason = isFinal ? `forced_${finalReason}` : "model_final";
|
|
2693
|
+
log({ event: "targeting.loop_done", turns, toolCalls: ledger.toolCalls, readChars: ledger.readChars, refused: ledger.refused, stopReason });
|
|
2694
|
+
return { body, summary: snapshot(stopReason) };
|
|
2695
|
+
}
|
|
2696
|
+
if (body.outcome !== "TOOL_CALLS") throw new LoopFailure("bad_reply");
|
|
2697
|
+
if (isFinal) throw new LoopFailure("final_not_produced");
|
|
2698
|
+
const rawCalls = body.toolCalls;
|
|
2699
|
+
if (!Array.isArray(rawCalls) || rawCalls.length === 0 || rawCalls.length > MAX_CALLS_IN_A_REPLY) {
|
|
2700
|
+
throw new LoopFailure("bad_reply");
|
|
2701
|
+
}
|
|
2702
|
+
const calls = [];
|
|
2703
|
+
for (const c of rawCalls) {
|
|
2704
|
+
if (!isPlainObject2(c) || typeof c.id !== "string" || c.id.length === 0 || c.id.length > MAX_ID_CHARS || typeof c.name !== "string") {
|
|
2705
|
+
throw new LoopFailure("bad_reply");
|
|
2706
|
+
}
|
|
2707
|
+
if (usedIds.has(c.id) || calls.some((x) => x.id === c.id)) throw new LoopFailure("duplicate_call_ids");
|
|
2708
|
+
calls.push({ id: c.id, name: c.name, args: c.args });
|
|
2709
|
+
}
|
|
2710
|
+
for (const c of calls) usedIds.add(c.id);
|
|
2711
|
+
const results = [];
|
|
2712
|
+
const logged = [];
|
|
2713
|
+
const turnReads = [];
|
|
2714
|
+
calls.forEach((call, index) => {
|
|
2715
|
+
let result;
|
|
2716
|
+
let read;
|
|
2717
|
+
if (index >= limits.maxCallsPerTurn) {
|
|
2718
|
+
result = {
|
|
2719
|
+
id: call.id,
|
|
2720
|
+
name: call.name,
|
|
2721
|
+
status: "budget_exhausted",
|
|
2722
|
+
reason: "too_many_calls",
|
|
2723
|
+
message: "Too many calls in one turn; this one was not run."
|
|
2724
|
+
};
|
|
2725
|
+
} else {
|
|
2726
|
+
try {
|
|
2727
|
+
({ result, read } = executeToolCall(call, env2, ledger));
|
|
2728
|
+
} catch (err) {
|
|
2729
|
+
throw new LoopFailure("executor_error", void 0, err);
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
results.push(result);
|
|
2733
|
+
turnReads[index] = read;
|
|
2734
|
+
if (read) reads.push(read);
|
|
2735
|
+
const tool = TOOL_NAMES.has(call.name) ? call.name : "unknown";
|
|
2736
|
+
const path = read && read.path !== "(invalid path)" ? { path: read.path } : {};
|
|
2737
|
+
logged.push({ tool, status: result.status, ...result.reason && result.status !== "duplicate" ? { reason: result.reason } : {}, ...path });
|
|
2738
|
+
if (result.status === "refused") {
|
|
2739
|
+
log({ event: "targeting.loop_refused", turn, tool, reason: result.reason, ...path });
|
|
2740
|
+
}
|
|
2741
|
+
});
|
|
2742
|
+
const reasoning = typeof body.reasoning === "string" ? body.reasoning.slice(0, limits.maxReasoningChars) : void 0;
|
|
2743
|
+
transcript.push({
|
|
2744
|
+
turn,
|
|
2745
|
+
...reasoning ? { reasoning } : {},
|
|
2746
|
+
toolCalls: calls.map((c) => ({ id: c.id, name: c.name, args: safeArgs(c.args) }))
|
|
2747
|
+
});
|
|
2748
|
+
toolResults.push(...results);
|
|
2749
|
+
noProgressStreak = results.every((r) => r.status === "duplicate") ? noProgressStreak + 1 : 0;
|
|
2750
|
+
if (noProgressStreak >= 2) forced = "no_progress";
|
|
2751
|
+
else if (ledger.toolCalls >= limits.maxToolCalls) forced = "tool_budget";
|
|
2752
|
+
else if (ledger.readChars >= limits.readCharsTotal || ledger.resultChars >= limits.resultCharsTotal) forced = "read_budget";
|
|
2753
|
+
const nextTurn = turn + 1;
|
|
2754
|
+
if (nextTurn <= limits.maxTurns) {
|
|
2755
|
+
const nextFinal = forced !== null || nextTurn === limits.maxTurns;
|
|
2756
|
+
const projected = estimateRequestTokens(buildContext(nextTurn, nextFinal, forced ?? "max_turns"));
|
|
2757
|
+
if (projected > limits.maxRequestTokens) {
|
|
2758
|
+
results.forEach((r, idx) => {
|
|
2759
|
+
if (r.status !== "ok") return;
|
|
2760
|
+
toolResults[toolResults.indexOf(r)] = {
|
|
2761
|
+
id: r.id,
|
|
2762
|
+
name: r.name,
|
|
2763
|
+
status: "budget_exhausted",
|
|
2764
|
+
reason: "token_ceiling",
|
|
2765
|
+
message: "Too large to include; answer from what you have."
|
|
2766
|
+
};
|
|
2767
|
+
const dropped = turnReads[idx];
|
|
2768
|
+
if (dropped) {
|
|
2769
|
+
const at = reads.indexOf(dropped);
|
|
2770
|
+
if (at >= 0) reads.splice(at, 1);
|
|
2771
|
+
}
|
|
2772
|
+
logged[idx].status = "budget_exhausted";
|
|
2773
|
+
logged[idx].reason = "token_ceiling";
|
|
2774
|
+
});
|
|
2775
|
+
forced = "read_budget";
|
|
2776
|
+
const trimmed = estimateRequestTokens(buildContext(nextTurn, true, "read_budget"));
|
|
2777
|
+
const fits = trimmed <= limits.maxRequestTokens;
|
|
2778
|
+
log({
|
|
2779
|
+
event: "targeting.loop_ceiling",
|
|
2780
|
+
turn,
|
|
2781
|
+
action: fits ? "dropped_results" : "fallback",
|
|
2782
|
+
projectedTokens: projected,
|
|
2783
|
+
ceiling: limits.maxRequestTokens
|
|
2784
|
+
});
|
|
2785
|
+
if (!fits) throw new LoopFailure("token_ceiling");
|
|
2786
|
+
}
|
|
2787
|
+
}
|
|
2788
|
+
log({ event: "targeting.loop_turn", turn, outcome: "tool_calls", requestChars, requestTokens, elapsedMs: now() - turnStarted, calls: logged });
|
|
2789
|
+
}
|
|
2790
|
+
throw new LoopFailure("final_not_produced");
|
|
2791
|
+
} catch (err) {
|
|
2792
|
+
if (err instanceof SafetyRefusalError) throw err;
|
|
2793
|
+
const failure = err instanceof LoopFailure ? err : new LoopFailure("internal_error", void 0, err);
|
|
2794
|
+
failure.summary ??= snapshot("fallback");
|
|
2795
|
+
throw failure;
|
|
2796
|
+
}
|
|
2797
|
+
}
|
|
2798
|
+
|
|
2799
|
+
// src/targeting.ts
|
|
2122
2800
|
var CONTRACT_VERSION2 = "test-targets-1";
|
|
2123
2801
|
function listWorkspaceFiles(workspaceRoot) {
|
|
2124
2802
|
return listFiles(workspaceRoot ?? ".");
|
|
@@ -2126,13 +2804,13 @@ function listWorkspaceFiles(workspaceRoot) {
|
|
|
2126
2804
|
function validateTarget(target, workspaceRoot, workspaceListing, deniedPaths) {
|
|
2127
2805
|
const denied = matchesDeniedPath(target.path, deniedPaths);
|
|
2128
2806
|
if (denied) {
|
|
2129
|
-
throw new
|
|
2807
|
+
throw new DeniedPathError(
|
|
2130
2808
|
`pilot.targeting proposed a target matching a denied path (${denied}): ${target.path}`
|
|
2131
2809
|
);
|
|
2132
2810
|
}
|
|
2133
2811
|
const deniedContext = findDeniedContextPath(target.contextPaths ?? [], deniedPaths);
|
|
2134
2812
|
if (deniedContext) {
|
|
2135
|
-
throw new
|
|
2813
|
+
throw new DeniedPathError(
|
|
2136
2814
|
`pilot.targeting cited a denied path as context (${deniedContext.pattern}): ${deniedContext.path} (target ${target.path})`
|
|
2137
2815
|
);
|
|
2138
2816
|
}
|
|
@@ -2147,15 +2825,15 @@ function validateTarget(target, workspaceRoot, workspaceListing, deniedPaths) {
|
|
|
2147
2825
|
}
|
|
2148
2826
|
return;
|
|
2149
2827
|
}
|
|
2150
|
-
const root = (0,
|
|
2151
|
-
const rel = (0,
|
|
2152
|
-
if (rel.startsWith("..") || (0,
|
|
2828
|
+
const root = (0, import_node_path9.resolve)(workspaceRoot ?? ".");
|
|
2829
|
+
const rel = (0, import_node_path9.relative)(root, (0, import_node_path9.resolve)(target.path));
|
|
2830
|
+
if (rel.startsWith("..") || (0, import_node_path9.isAbsolute)(rel)) {
|
|
2153
2831
|
throw new Error(
|
|
2154
2832
|
`pilot.targeting named a NEW_FILE path that escapes the workspace root: ${target.path}`
|
|
2155
2833
|
);
|
|
2156
2834
|
}
|
|
2157
2835
|
}
|
|
2158
|
-
|
|
2836
|
+
function buildContextData(design, workspaceListing, requiredTests, deniedPaths, testDiscovery, outOfScope) {
|
|
2159
2837
|
const contextData = {
|
|
2160
2838
|
// NESTED, NOT FLATTENED — a second vocabulary for the design shape is exactly what
|
|
2161
2839
|
// QUIKTR-1034 cost.
|
|
@@ -2174,6 +2852,9 @@ async function runTargeting(gateway, design, workspaceRoot, workspaceListing, re
|
|
|
2174
2852
|
contextData.outOfScope = outOfScope;
|
|
2175
2853
|
}
|
|
2176
2854
|
contextData.testDiscovery = testDiscovery;
|
|
2855
|
+
return contextData;
|
|
2856
|
+
}
|
|
2857
|
+
async function postTargeting(gateway, contextData, signal) {
|
|
2177
2858
|
const res = await fetchWithRetry(`${gateway.baseUrl}/ai/execute`, {
|
|
2178
2859
|
method: "POST",
|
|
2179
2860
|
headers: { "content-type": "application/json", "x-run-token": gateway.token },
|
|
@@ -2183,14 +2864,15 @@ async function runTargeting(gateway, design, workspaceRoot, workspaceListing, re
|
|
|
2183
2864
|
expectedOutputType: "json",
|
|
2184
2865
|
userPrompt: "",
|
|
2185
2866
|
contextData
|
|
2186
|
-
})
|
|
2867
|
+
}),
|
|
2868
|
+
...signal ? { signal } : {}
|
|
2187
2869
|
});
|
|
2188
2870
|
if (!res.ok) {
|
|
2189
|
-
throw new
|
|
2871
|
+
throw new TargetingHttpError(res.status, res.statusText);
|
|
2190
2872
|
}
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2873
|
+
return res.json();
|
|
2874
|
+
}
|
|
2875
|
+
function finalizeTargeting(body, workspaceRoot, workspaceListing, deniedPaths) {
|
|
2194
2876
|
if (body.outcome === "NO_TARGET") {
|
|
2195
2877
|
return { outcome: "NO_TARGET", reason: body.noTargetReason ?? null };
|
|
2196
2878
|
}
|
|
@@ -2219,6 +2901,66 @@ async function runTargeting(gateway, design, workspaceRoot, workspaceListing, re
|
|
|
2219
2901
|
...correctedKinds.length > 0 ? { correctedKinds } : {}
|
|
2220
2902
|
};
|
|
2221
2903
|
}
|
|
2904
|
+
async function runSingleShot(gateway, design, workspaceRoot, workspaceListing, requiredTests, deniedPaths, testDiscovery, outOfScope = []) {
|
|
2905
|
+
const contextData = buildContextData(design, workspaceListing, requiredTests, deniedPaths, testDiscovery, outOfScope);
|
|
2906
|
+
const envelope = await postTargeting(gateway, contextData);
|
|
2907
|
+
checkHardFailure(envelope, "pilot.targeting");
|
|
2908
|
+
const body = unwrapExecute(envelope, "pilot.targeting");
|
|
2909
|
+
return finalizeTargeting(body, workspaceRoot, workspaceListing, deniedPaths);
|
|
2910
|
+
}
|
|
2911
|
+
var CODE_LIKE = /^[A-Z0-9_]{1,40}$/;
|
|
2912
|
+
async function runTargeting(gateway, design, workspaceRoot, workspaceListing, requiredTests, deniedPaths, testDiscovery, outOfScope = [], options = {}) {
|
|
2913
|
+
const mode = options.mode ?? resolveTargetingMode();
|
|
2914
|
+
if (mode !== "loop") {
|
|
2915
|
+
return runSingleShot(gateway, design, workspaceRoot, workspaceListing, requiredTests, deniedPaths, testDiscovery, outOfScope);
|
|
2916
|
+
}
|
|
2917
|
+
const baseContext = buildContextData(design, workspaceListing, requiredTests, deniedPaths, testDiscovery, outOfScope);
|
|
2918
|
+
let summary;
|
|
2919
|
+
let runtimeCode;
|
|
2920
|
+
let reason;
|
|
2921
|
+
try {
|
|
2922
|
+
const looped = await runTargetingLoop(
|
|
2923
|
+
{ baseContext, listing: workspaceListing, deniedPaths },
|
|
2924
|
+
{ ...options.loopDeps, post: (contextData, signal) => postTargeting(gateway, contextData, signal) }
|
|
2925
|
+
);
|
|
2926
|
+
summary = looped.summary;
|
|
2927
|
+
const result = finalizeTargeting(looped.body, workspaceRoot, workspaceListing, deniedPaths);
|
|
2928
|
+
return { ...result, loop: summary };
|
|
2929
|
+
} catch (err) {
|
|
2930
|
+
if (err instanceof DeniedPathError || err instanceof SafetyRefusalError) throw err;
|
|
2931
|
+
if (err instanceof LoopFailure) {
|
|
2932
|
+
reason = err.reason;
|
|
2933
|
+
runtimeCode = err.runtimeCode;
|
|
2934
|
+
summary = err.summary ?? summary;
|
|
2935
|
+
} else {
|
|
2936
|
+
reason = "final_invalid";
|
|
2937
|
+
}
|
|
2938
|
+
}
|
|
2939
|
+
const fellBack = {
|
|
2940
|
+
turns: summary?.turns ?? 0,
|
|
2941
|
+
toolCalls: summary?.toolCalls ?? 0,
|
|
2942
|
+
readChars: summary?.readChars ?? 0,
|
|
2943
|
+
refused: summary?.refused ?? 0,
|
|
2944
|
+
reads: summary?.reads ?? [],
|
|
2945
|
+
stopReason: "fallback",
|
|
2946
|
+
fellBack: true,
|
|
2947
|
+
fallbackReason: reason
|
|
2948
|
+
};
|
|
2949
|
+
const log = options.loopDeps?.log ?? ((e) => console.log(JSON.stringify(e)));
|
|
2950
|
+
log({
|
|
2951
|
+
event: "targeting.loop_fallback",
|
|
2952
|
+
reason,
|
|
2953
|
+
turn: fellBack.turns,
|
|
2954
|
+
toolCalls: fellBack.toolCalls,
|
|
2955
|
+
...runtimeCode && CODE_LIKE.test(runtimeCode) ? { runtimeCode } : {}
|
|
2956
|
+
});
|
|
2957
|
+
const single = await runSingleShot(gateway, design, workspaceRoot, workspaceListing, requiredTests, deniedPaths, testDiscovery, outOfScope);
|
|
2958
|
+
return {
|
|
2959
|
+
...single,
|
|
2960
|
+
loop: fellBack,
|
|
2961
|
+
warnings: [`QuikPilot's read-only targeting could not complete (${reason}); standard targeting was used instead.`]
|
|
2962
|
+
};
|
|
2963
|
+
}
|
|
2222
2964
|
|
|
2223
2965
|
// src/targetingRecord.ts
|
|
2224
2966
|
var MAX_TARGETS = 25;
|
|
@@ -2228,9 +2970,11 @@ var MAX_ADDED_PATHS = 20;
|
|
|
2228
2970
|
var MAX_SKIPPED_CONTEXT = 20;
|
|
2229
2971
|
var MAX_SKIPPED_CASES = 200;
|
|
2230
2972
|
var MAX_CONTEXT_RECORDS = 25;
|
|
2231
|
-
var
|
|
2232
|
-
var
|
|
2973
|
+
var MAX_PATH_CHARS2 = 300;
|
|
2974
|
+
var MAX_ID_CHARS2 = 100;
|
|
2233
2975
|
var MAX_REASON_CHARS = 300;
|
|
2976
|
+
var MAX_LOOP_READS = 50;
|
|
2977
|
+
var MAX_FALLBACK_REASON_CHARS = 40;
|
|
2234
2978
|
function clampList(values, max, maxChars) {
|
|
2235
2979
|
let cut = false;
|
|
2236
2980
|
const kept = [];
|
|
@@ -2245,6 +2989,41 @@ function clampList(values, max, maxChars) {
|
|
|
2245
2989
|
}
|
|
2246
2990
|
return { kept, cut };
|
|
2247
2991
|
}
|
|
2992
|
+
function wholeNumber(n) {
|
|
2993
|
+
return typeof n === "number" && Number.isInteger(n) && n >= 0 ? n : 0;
|
|
2994
|
+
}
|
|
2995
|
+
function clampLoop(loop) {
|
|
2996
|
+
let cut = false;
|
|
2997
|
+
const reads = [];
|
|
2998
|
+
for (const r of loop.reads ?? []) {
|
|
2999
|
+
if (reads.length >= MAX_LOOP_READS) {
|
|
3000
|
+
cut = true;
|
|
3001
|
+
break;
|
|
3002
|
+
}
|
|
3003
|
+
const path = typeof r.path === "string" ? r.path.trim() : "";
|
|
3004
|
+
if (path.length === 0 || path.length > MAX_PATH_CHARS2) {
|
|
3005
|
+
cut = true;
|
|
3006
|
+
continue;
|
|
3007
|
+
}
|
|
3008
|
+
reads.push({
|
|
3009
|
+
path,
|
|
3010
|
+
...Number.isInteger(r.startLine) ? { startLine: r.startLine } : {},
|
|
3011
|
+
...Number.isInteger(r.endLine) ? { endLine: r.endLine } : {},
|
|
3012
|
+
chars: wholeNumber(r.chars),
|
|
3013
|
+
...r.refused ? { refused: true } : {}
|
|
3014
|
+
});
|
|
3015
|
+
}
|
|
3016
|
+
return {
|
|
3017
|
+
turns: wholeNumber(loop.turns),
|
|
3018
|
+
toolCalls: wholeNumber(loop.toolCalls),
|
|
3019
|
+
refused: wholeNumber(loop.refused),
|
|
3020
|
+
stopReason: loop.stopReason,
|
|
3021
|
+
...loop.fellBack ? { fellBack: true } : {},
|
|
3022
|
+
...loop.fallbackReason ? { fallbackReason: String(loop.fallbackReason).slice(0, MAX_FALLBACK_REASON_CHARS) } : {},
|
|
3023
|
+
reads,
|
|
3024
|
+
...cut ? { truncated: true } : {}
|
|
3025
|
+
};
|
|
3026
|
+
}
|
|
2248
3027
|
function buildTargetingRecord(targeting, contextLog) {
|
|
2249
3028
|
let truncated = false;
|
|
2250
3029
|
const note = (cut) => {
|
|
@@ -2257,6 +3036,7 @@ function buildTargetingRecord(targeting, contextLog) {
|
|
|
2257
3036
|
record2.noTargetReason = reason.slice(0, MAX_REASON_CHARS);
|
|
2258
3037
|
if (reason.length > MAX_REASON_CHARS) record2.noTargetReasonTruncated = true;
|
|
2259
3038
|
}
|
|
3039
|
+
if (targeting.loop) record2.loop = clampLoop(targeting.loop);
|
|
2260
3040
|
return record2;
|
|
2261
3041
|
}
|
|
2262
3042
|
const targets = [];
|
|
@@ -2266,12 +3046,12 @@ function buildTargetingRecord(targeting, contextLog) {
|
|
|
2266
3046
|
break;
|
|
2267
3047
|
}
|
|
2268
3048
|
const path = typeof t.path === "string" ? t.path.trim() : "";
|
|
2269
|
-
if (path.length === 0 || path.length >
|
|
3049
|
+
if (path.length === 0 || path.length > MAX_PATH_CHARS2) {
|
|
2270
3050
|
truncated = true;
|
|
2271
3051
|
continue;
|
|
2272
3052
|
}
|
|
2273
|
-
const contextPaths = clampList(t.contextPaths, MAX_CONTEXT_PATHS,
|
|
2274
|
-
const automationIds = clampList(t.automationIds, MAX_IDS_PER_TARGET,
|
|
3053
|
+
const contextPaths = clampList(t.contextPaths, MAX_CONTEXT_PATHS, MAX_PATH_CHARS2);
|
|
3054
|
+
const automationIds = clampList(t.automationIds, MAX_IDS_PER_TARGET, MAX_ID_CHARS2);
|
|
2275
3055
|
note(contextPaths.cut);
|
|
2276
3056
|
note(automationIds.cut);
|
|
2277
3057
|
targets.push({ path, kind: t.kind, contextPaths: contextPaths.kept, automationIds: automationIds.kept });
|
|
@@ -2279,7 +3059,7 @@ function buildTargetingRecord(targeting, contextLog) {
|
|
|
2279
3059
|
const skipped = clampList(
|
|
2280
3060
|
(targeting.skippedCases ?? []).map((c) => c.automationId),
|
|
2281
3061
|
MAX_SKIPPED_CASES,
|
|
2282
|
-
|
|
3062
|
+
MAX_ID_CHARS2
|
|
2283
3063
|
);
|
|
2284
3064
|
note(skipped.cut);
|
|
2285
3065
|
const codegenContext = [];
|
|
@@ -2289,16 +3069,16 @@ function buildTargetingRecord(targeting, contextLog) {
|
|
|
2289
3069
|
break;
|
|
2290
3070
|
}
|
|
2291
3071
|
const targetPath = typeof entry.targetPath === "string" ? entry.targetPath.trim() : "";
|
|
2292
|
-
if (targetPath.length === 0 || targetPath.length >
|
|
3072
|
+
if (targetPath.length === 0 || targetPath.length > MAX_PATH_CHARS2) {
|
|
2293
3073
|
truncated = true;
|
|
2294
3074
|
continue;
|
|
2295
3075
|
}
|
|
2296
|
-
const contextPaths = clampList(entry.contextPaths, MAX_CONTEXT_PATHS,
|
|
2297
|
-
const addedPaths = clampList(entry.addedPaths, MAX_ADDED_PATHS,
|
|
3076
|
+
const contextPaths = clampList(entry.contextPaths, MAX_CONTEXT_PATHS, MAX_PATH_CHARS2);
|
|
3077
|
+
const addedPaths = clampList(entry.addedPaths, MAX_ADDED_PATHS, MAX_PATH_CHARS2);
|
|
2298
3078
|
const skippedPaths = clampList(
|
|
2299
3079
|
(entry.skipped ?? []).map((s) => s.path),
|
|
2300
3080
|
MAX_SKIPPED_CONTEXT,
|
|
2301
|
-
|
|
3081
|
+
MAX_PATH_CHARS2
|
|
2302
3082
|
);
|
|
2303
3083
|
note(contextPaths.cut);
|
|
2304
3084
|
note(addedPaths.cut);
|
|
@@ -2319,6 +3099,7 @@ function buildTargetingRecord(targeting, contextLog) {
|
|
|
2319
3099
|
codegenContext
|
|
2320
3100
|
};
|
|
2321
3101
|
if (truncated) record.truncated = true;
|
|
3102
|
+
if (targeting.loop) record.loop = clampLoop(targeting.loop);
|
|
2322
3103
|
return record;
|
|
2323
3104
|
}
|
|
2324
3105
|
|
|
@@ -2431,6 +3212,10 @@ async function run(env2) {
|
|
|
2431
3212
|
outOfScope
|
|
2432
3213
|
);
|
|
2433
3214
|
targetingResult = targeting;
|
|
3215
|
+
for (const w of targeting.warnings ?? []) {
|
|
3216
|
+
warnings.push(w);
|
|
3217
|
+
console.warn(`QuikPilot targeting warning (not blocking): ${w}`);
|
|
3218
|
+
}
|
|
2434
3219
|
if (targeting.outcome === "TARGETED") {
|
|
2435
3220
|
for (const path of targeting.correctedKinds ?? []) {
|
|
2436
3221
|
const w = `QuikPilot named ${path} as a new file, but it already exists in the repository. It was treated as an existing file so its current content was kept in view.`;
|