@quikit/quikpilot-runner 9.0.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/dist/index.js CHANGED
@@ -230,7 +230,7 @@ function commitAndPush(branch, runId, installationToken, targeted, issueKey, iss
230
230
 
231
231
  // src/codegen.ts
232
232
  var import_node_fs = require("node:fs");
233
- var import_node_path = require("node:path");
233
+ var import_node_path2 = require("node:path");
234
234
 
235
235
  // src/deniedPaths.ts
236
236
  function globToRegExp(pattern) {
@@ -382,6 +382,315 @@ function unwrapExecute(envelope, useCase) {
382
382
  return envelope.structuredJson;
383
383
  }
384
384
 
385
+ // src/importContext.ts
386
+ var import_node_path = require("node:path");
387
+ var REGEX_PRECEDING_KEYWORDS = /* @__PURE__ */ new Set([
388
+ "return",
389
+ "typeof",
390
+ "instanceof",
391
+ "in",
392
+ "of",
393
+ "new",
394
+ "delete",
395
+ "void",
396
+ "throw",
397
+ "case",
398
+ "do",
399
+ "else",
400
+ "yield",
401
+ "await"
402
+ ]);
403
+ var SPACE_CODES = /* @__PURE__ */ new Set([32, 9, 10, 11, 12, 13, 160, 5760, 8232, 8233, 8239, 8287, 12288, 65279]);
404
+ var isSpace = (c) => {
405
+ const code = c.charCodeAt(0);
406
+ return SPACE_CODES.has(code) || code >= 8192 && code <= 8202;
407
+ };
408
+ var isIdStart = (c) => !isSpace(c) && (/[A-Za-z_$]/.test(c) || c.charCodeAt(0) > 127);
409
+ var isIdPart = (c) => !isSpace(c) && (/[A-Za-z0-9_$]/.test(c) || c.charCodeAt(0) > 127);
410
+ function tokenize(src) {
411
+ const out = [];
412
+ const n = src.length;
413
+ function regexAllowed() {
414
+ const prev = out[out.length - 1];
415
+ if (!prev) return true;
416
+ if (prev.k === "p") return prev.v !== ")" && prev.v !== "]" && prev.v !== "}";
417
+ if (prev.k === "id") return REGEX_PRECEDING_KEYWORDS.has(prev.v);
418
+ return false;
419
+ }
420
+ function scan(start, inSubstitution) {
421
+ let i = start;
422
+ let depth = 0;
423
+ while (i < n) {
424
+ const c = src[i];
425
+ if (isSpace(c)) {
426
+ i++;
427
+ continue;
428
+ }
429
+ if (c === "/" && src[i + 1] === "/") {
430
+ while (i < n && src[i] !== "\n") i++;
431
+ continue;
432
+ }
433
+ if (c === "/" && src[i + 1] === "*") {
434
+ const end = src.indexOf("*/", i + 2);
435
+ i = end === -1 ? n : end + 2;
436
+ continue;
437
+ }
438
+ if (c === "'" || c === '"') {
439
+ let j = i + 1;
440
+ let closed = false;
441
+ while (j < n) {
442
+ const d = src[j];
443
+ if (d === "\\") {
444
+ j += 2;
445
+ continue;
446
+ }
447
+ if (d === c) {
448
+ closed = true;
449
+ break;
450
+ }
451
+ if (d === "\n" || d === "\r") break;
452
+ j++;
453
+ }
454
+ if (closed) out.push({ k: "str", v: src.slice(i + 1, j) });
455
+ i = closed ? j + 1 : j;
456
+ continue;
457
+ }
458
+ if (c === "`") {
459
+ let j = i + 1;
460
+ let hasSubstitution = false;
461
+ let closed = false;
462
+ const textStart = j;
463
+ let textEnd = j;
464
+ while (j < n) {
465
+ const d = src[j];
466
+ if (d === "\\") {
467
+ j += 2;
468
+ continue;
469
+ }
470
+ if (d === "`") {
471
+ closed = true;
472
+ textEnd = hasSubstitution ? textEnd : j;
473
+ break;
474
+ }
475
+ if (d === "$" && src[j + 1] === "{") {
476
+ hasSubstitution = true;
477
+ j = scan(j + 2, true);
478
+ continue;
479
+ }
480
+ j++;
481
+ }
482
+ if (closed) out.push(hasSubstitution ? { k: "tpl", v: "" } : { k: "str", v: src.slice(textStart, textEnd) });
483
+ i = closed ? j + 1 : n;
484
+ continue;
485
+ }
486
+ if (c === "/" && regexAllowed()) {
487
+ let j = i + 1;
488
+ let inClass = false;
489
+ let closed = false;
490
+ while (j < n) {
491
+ const d = src[j];
492
+ if (d === "\\") {
493
+ j += 2;
494
+ continue;
495
+ }
496
+ if (d === "\n" || d === "\r") break;
497
+ if (d === "[") inClass = true;
498
+ else if (d === "]") inClass = false;
499
+ else if (d === "/" && !inClass) {
500
+ closed = true;
501
+ break;
502
+ }
503
+ j++;
504
+ }
505
+ if (closed) {
506
+ j++;
507
+ while (j < n && /[a-z]/i.test(src[j])) j++;
508
+ out.push({ k: "num", v: "/re/" });
509
+ i = j;
510
+ continue;
511
+ }
512
+ out.push({ k: "p", v: "/" });
513
+ i++;
514
+ continue;
515
+ }
516
+ if (/[0-9]/.test(c)) {
517
+ let j = i + 1;
518
+ while (j < n && /[0-9A-Za-z_.]/.test(src[j])) j++;
519
+ out.push({ k: "num", v: src.slice(i, j) });
520
+ i = j;
521
+ continue;
522
+ }
523
+ if (isIdStart(c)) {
524
+ let j = i + 1;
525
+ while (j < n && isIdPart(src[j])) j++;
526
+ out.push({ k: "id", v: src.slice(i, j) });
527
+ i = j;
528
+ continue;
529
+ }
530
+ if (inSubstitution) {
531
+ if (c === "{") depth++;
532
+ else if (c === "}") {
533
+ if (depth === 0) return i + 1;
534
+ depth--;
535
+ }
536
+ }
537
+ out.push({ k: "p", v: c });
538
+ i++;
539
+ }
540
+ return n;
541
+ }
542
+ scan(0, false);
543
+ return out;
544
+ }
545
+ function extractImportSpecifiers(source) {
546
+ const t = tokenize(source);
547
+ const found = [];
548
+ const seen = /* @__PURE__ */ new Set();
549
+ const add = (s) => {
550
+ if (!seen.has(s)) {
551
+ seen.add(s);
552
+ found.push(s);
553
+ }
554
+ };
555
+ const isP = (tok, v) => tok?.k === "p" && tok.v === v;
556
+ for (let i = 0; i < t.length; i++) {
557
+ const tok = t[i];
558
+ if (tok.k !== "id") continue;
559
+ if (isP(t[i - 1], ".")) continue;
560
+ if (tok.v === "require") {
561
+ if (isP(t[i + 1], "(") && t[i + 2]?.k === "str" && isP(t[i + 3], ")")) add(t[i + 2].v);
562
+ continue;
563
+ }
564
+ if (tok.v !== "import" && tok.v !== "export") continue;
565
+ const next = t[i + 1];
566
+ if (tok.v === "import") {
567
+ if (next?.k === "str") {
568
+ add(next.v);
569
+ continue;
570
+ }
571
+ if (isP(next, "(")) {
572
+ if (t[i + 2]?.k === "str" && (isP(t[i + 3], ")") || isP(t[i + 3], ","))) add(t[i + 2].v);
573
+ continue;
574
+ }
575
+ }
576
+ for (let j = i + 1; j < t.length; j++) {
577
+ const c = t[j];
578
+ if (c.k === "id") {
579
+ if (c.v === "import" || c.v === "export") break;
580
+ if (c.v === "from" && t[j + 1]?.k === "str") {
581
+ add(t[j + 1].v);
582
+ break;
583
+ }
584
+ continue;
585
+ }
586
+ if (c.k === "p" && (c.v === "{" || c.v === "}" || c.v === "," || c.v === "*")) continue;
587
+ break;
588
+ }
589
+ }
590
+ return found;
591
+ }
592
+ function supportsImportScan(path) {
593
+ return /\.(?:[cm]?[jt]sx?)$/i.test(path);
594
+ }
595
+ var CODE_EXTS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts", ".json"];
596
+ var ASSET_EXTS = /* @__PURE__ */ new Set([
597
+ ".css",
598
+ ".scss",
599
+ ".sass",
600
+ ".less",
601
+ ".svg",
602
+ ".png",
603
+ ".jpg",
604
+ ".jpeg",
605
+ ".gif",
606
+ ".webp",
607
+ ".avif",
608
+ ".ico",
609
+ ".bmp",
610
+ ".woff",
611
+ ".woff2",
612
+ ".ttf",
613
+ ".otf",
614
+ ".eot",
615
+ ".mp3",
616
+ ".mp4",
617
+ ".webm",
618
+ ".wav",
619
+ ".pdf",
620
+ ".zip",
621
+ ".wasm"
622
+ ]);
623
+ var TS_SIBLINGS = { ".js": [".ts", ".tsx"], ".jsx": [".tsx"], ".mjs": [".mts"], ".cjs": [".cts"] };
624
+ function resolveImport(fromPath, specifier, listing) {
625
+ if (/^(?:@\/|~\/|#|\$|\/)/.test(specifier)) return { kind: "unresolved", reason: "alias" };
626
+ const clean = specifier.replace(/[?#].*$/, "");
627
+ const relative5 = clean === "." || clean === ".." || clean.startsWith("./") || clean.startsWith("../");
628
+ if (!relative5) return { kind: "external" };
629
+ const base = import_node_path.posix.normalize(import_node_path.posix.join(import_node_path.posix.dirname(fromPath), clean));
630
+ if (base === ".." || base.startsWith("../") || import_node_path.posix.isAbsolute(base)) return { kind: "unresolved", reason: "not found" };
631
+ const ext = import_node_path.posix.extname(base);
632
+ if (ASSET_EXTS.has(ext.toLowerCase())) return { kind: "unresolved", reason: "asset" };
633
+ const candidates = [];
634
+ if (CODE_EXTS.includes(ext)) {
635
+ candidates.push(base);
636
+ for (const sibling of TS_SIBLINGS[ext] ?? []) candidates.push(base.slice(0, -ext.length) + sibling);
637
+ }
638
+ for (const e of CODE_EXTS) candidates.push(base + e);
639
+ candidates.push(base + ".d.ts");
640
+ for (const e of CODE_EXTS) candidates.push(`${base}/index${e}`);
641
+ for (const candidate of candidates) {
642
+ if (listing.has(candidate)) return { kind: "repo", path: candidate };
643
+ }
644
+ return { kind: "unresolved", reason: "not found", attempted: base };
645
+ }
646
+ var ACTIONABLE_IMPORT_SKIPS = /* @__PURE__ */ new Set(["import: too large", "import: over budget", "import: unreadable"]);
647
+ function collectImportContext(p) {
648
+ const out = { entries: [], skipped: [], importedPaths: [], aliasImports: 0 };
649
+ if (!supportsImportScan(p.targetPath)) return out;
650
+ const seen = new Set(p.alreadyNamed);
651
+ seen.add(p.targetPath);
652
+ let total = p.spentChars;
653
+ for (const specifier of extractImportSpecifiers(p.source)) {
654
+ const resolved = resolveImport(p.targetPath, specifier, p.listing);
655
+ if (resolved.kind === "external") continue;
656
+ if (resolved.kind === "unresolved") {
657
+ if (resolved.reason === "alias") out.aliasImports++;
658
+ else if (resolved.reason === "not found") {
659
+ const path2 = resolved.attempted ?? specifier;
660
+ if (!seen.has(path2)) {
661
+ seen.add(path2);
662
+ out.skipped.push({ path: path2, reason: "import: not in workspace" });
663
+ }
664
+ }
665
+ continue;
666
+ }
667
+ const path = resolved.path;
668
+ if (seen.has(path)) continue;
669
+ seen.add(path);
670
+ if (matchesDeniedPath(path, p.deniedPaths)) {
671
+ out.skipped.push({ path, reason: "import: denied" });
672
+ continue;
673
+ }
674
+ let content;
675
+ try {
676
+ content = p.readFile(path);
677
+ } catch {
678
+ out.skipped.push({ path, reason: "import: unreadable" });
679
+ continue;
680
+ }
681
+ if (content.length > p.maxFileChars) {
682
+ out.skipped.push({ path, reason: "import: too large" });
683
+ } else if (total + content.length > p.maxTotalChars) {
684
+ out.skipped.push({ path, reason: "import: over budget" });
685
+ } else {
686
+ total += content.length;
687
+ out.entries.push({ path, content });
688
+ out.importedPaths.push(path);
689
+ }
690
+ }
691
+ return out;
692
+ }
693
+
385
694
  // src/retryFetch.ts
386
695
  var RETRYABLE_STATUS_CODES = /* @__PURE__ */ new Set([502, 503, 504]);
387
696
  var RETRYABLE_NETWORK_ERROR_CODES = /* @__PURE__ */ new Set(["ECONNRESET", "ETIMEDOUT"]);
@@ -604,16 +913,38 @@ async function runCodegen(gateway, targets, workspaceListing, requiredTests, den
604
913
  }
605
914
  }
606
915
  const namedContents = contextPaths.map((path2) => ({ path: path2, content: (0, import_node_fs.readFileSync)(path2, "utf8") }));
607
- const added = collectRunContext(target, targets, new Set(contextPaths), deniedPaths);
608
- const contextContents = [...namedContents, ...added.entries];
609
- for (const s of added.skipped) {
916
+ const runFiles = collectRunContext(target, targets, new Set(contextPaths), deniedPaths);
917
+ for (const s of runFiles.skipped) {
610
918
  warnings.push(`not shown to the test for ${target.path}: ${s.path} (${s.reason})`);
611
919
  }
920
+ const imports = isNewFile ? { entries: [], skipped: [], importedPaths: [], aliasImports: 0 } : collectImportContext({
921
+ targetPath: target.path,
922
+ source: currentContent,
923
+ alreadyNamed: /* @__PURE__ */ new Set([...contextPaths, ...runFiles.entries.map((e) => e.path)]),
924
+ deniedPaths,
925
+ listing,
926
+ readFile: (p) => (0, import_node_fs.readFileSync)(p, "utf8"),
927
+ maxFileChars: MAX_ADDED_FILE_CHARS,
928
+ maxTotalChars: MAX_ADDED_CONTEXT_CHARS,
929
+ spentChars: runFiles.entries.reduce((n, e) => n + e.content.length, 0)
930
+ });
931
+ for (const s of imports.skipped) {
932
+ if (ACTIONABLE_IMPORT_SKIPS.has(s.reason)) {
933
+ warnings.push(`not shown with ${target.path}: ${s.path} (imported by it; ${s.reason.replace(/^import: /, "")})`);
934
+ }
935
+ }
936
+ const added = {
937
+ entries: [...runFiles.entries, ...imports.entries],
938
+ skipped: [...runFiles.skipped, ...imports.skipped]
939
+ };
940
+ const contextContents = [...namedContents, ...added.entries];
612
941
  const contextRecord = {
613
942
  targetPath: target.path,
614
943
  contextPaths,
615
944
  addedPaths: added.entries.map((e) => e.path),
616
- skipped: added.skipped
945
+ skipped: added.skipped,
946
+ ...imports.importedPaths.length > 0 ? { importedPaths: imports.importedPaths } : {},
947
+ ...imports.aliasImports > 0 ? { aliasImports: imports.aliasImports } : {}
617
948
  };
618
949
  console.log(JSON.stringify({ event: "codegen.context", ...contextRecord }));
619
950
  contextLog?.push(contextRecord);
@@ -679,6 +1010,7 @@ async function runCodegen(gateway, targets, workspaceListing, requiredTests, den
679
1010
  contextData.contextContents = namedContents;
680
1011
  warnings.push(`context for ${target.path} was too large with the run's other files; retried without them`);
681
1012
  contextRecord.addedPaths = [];
1013
+ delete contextRecord.importedPaths;
682
1014
  contextRecord.retriedWithoutAdded = true;
683
1015
  res = await callCodegen();
684
1016
  if (!res.ok) {
@@ -794,14 +1126,14 @@ async function runCodegen(gateway, targets, workspaceListing, requiredTests, den
794
1126
  }
795
1127
  function flushCodegenResults(results) {
796
1128
  for (const { targetPath, newContent } of results) {
797
- (0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(targetPath), { recursive: true });
1129
+ (0, import_node_fs.mkdirSync)((0, import_node_path2.dirname)(targetPath), { recursive: true });
798
1130
  (0, import_node_fs.writeFileSync)(targetPath, newContent);
799
1131
  }
800
1132
  }
801
1133
 
802
1134
  // src/dependencyCheck.ts
803
1135
  var import_node_fs2 = require("node:fs");
804
- var import_node_path2 = require("node:path");
1136
+ var import_node_path3 = require("node:path");
805
1137
  var SCANNABLE_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"];
806
1138
  var IMPORT_RE = /(?:\bimport\b[^'"]*?\bfrom\s*|\bimport\s*\(\s*|\bexport\b[^'"]*?\bfrom\s*|\brequire\s*\(\s*|\bimport\s+)(['"])([^'"]+)\1/g;
807
1139
  var NODE_BUILTINS = /* @__PURE__ */ new Set([
@@ -880,8 +1212,8 @@ function dependencyNames(pkg) {
880
1212
  ]);
881
1213
  }
882
1214
  function readDeclaredDependencies(results, workspaceRoot) {
883
- const packageJsonPath = (0, import_node_path2.join)((0, import_node_path2.resolve)(workspaceRoot ?? "."), "package.json");
884
- const generated = results.find((r) => (0, import_node_path2.resolve)(r.targetPath) === packageJsonPath);
1215
+ const packageJsonPath = (0, import_node_path3.join)((0, import_node_path3.resolve)(workspaceRoot ?? "."), "package.json");
1216
+ const generated = results.find((r) => (0, import_node_path3.resolve)(r.targetPath) === packageJsonPath);
885
1217
  let raw;
886
1218
  if (generated) {
887
1219
  raw = generated.newContent;
@@ -951,10 +1283,10 @@ function stripJsonComments(text) {
951
1283
  return out.replace(/,(\s*[}\]])/g, "$1");
952
1284
  }
953
1285
  function resolveAliasPrefixes(results, workspaceRoot) {
954
- const root = (0, import_node_path2.resolve)(workspaceRoot ?? ".");
1286
+ const root = (0, import_node_path3.resolve)(workspaceRoot ?? ".");
955
1287
  for (const filename of ["tsconfig.json", "jsconfig.json"]) {
956
- const configPath = (0, import_node_path2.join)(root, filename);
957
- const generated = results.find((r) => (0, import_node_path2.resolve)(r.targetPath) === configPath);
1288
+ const configPath = (0, import_node_path3.join)(root, filename);
1289
+ const generated = results.find((r) => (0, import_node_path3.resolve)(r.targetPath) === configPath);
958
1290
  let raw;
959
1291
  if (generated) {
960
1292
  raw = generated.newContent;
@@ -1161,7 +1493,7 @@ function writePhase0Marker(runId) {
1161
1493
 
1162
1494
  // src/quiktest.ts
1163
1495
  var import_node_fs4 = require("node:fs");
1164
- var import_node_path3 = require("node:path");
1496
+ var import_node_path4 = require("node:path");
1165
1497
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git"]);
1166
1498
  function walkForJUnitByName(dir) {
1167
1499
  let entries;
@@ -1172,7 +1504,7 @@ function walkForJUnitByName(dir) {
1172
1504
  }
1173
1505
  for (const entry of entries) {
1174
1506
  if (SKIP_DIRS.has(entry)) continue;
1175
- const full = (0, import_node_path3.join)(dir, entry);
1507
+ const full = (0, import_node_path4.join)(dir, entry);
1176
1508
  let stat;
1177
1509
  try {
1178
1510
  stat = (0, import_node_fs4.statSync)(full);
@@ -1196,7 +1528,7 @@ function firstXmlIn(dir, namePattern) {
1196
1528
  return null;
1197
1529
  }
1198
1530
  const match = entries.find((e) => namePattern.test(e));
1199
- return match ? (0, import_node_path3.join)(dir, match) : null;
1531
+ return match ? (0, import_node_path4.join)(dir, match) : null;
1200
1532
  }
1201
1533
  function readTestReportBytes(cwd = process.cwd()) {
1202
1534
  try {
@@ -1207,7 +1539,7 @@ function readTestReportBytes(cwd = process.cwd()) {
1207
1539
  }
1208
1540
  }
1209
1541
  function findJUnitReport(cwd = process.cwd()) {
1210
- return walkForJUnitByName(cwd) ?? firstXmlIn((0, import_node_path3.join)(cwd, "test-results"), /\.xml$/) ?? firstXmlIn((0, import_node_path3.join)(cwd, "reports"), /^junit.*\.xml$/) ?? firstXmlIn((0, import_node_path3.join)(cwd, "target", "surefire-reports"), /\.xml$/);
1542
+ return walkForJUnitByName(cwd) ?? firstXmlIn((0, import_node_path4.join)(cwd, "test-results"), /\.xml$/) ?? firstXmlIn((0, import_node_path4.join)(cwd, "reports"), /^junit.*\.xml$/) ?? firstXmlIn((0, import_node_path4.join)(cwd, "target", "surefire-reports"), /\.xml$/);
1211
1543
  }
1212
1544
  function buildCiUrl(serverUrl, repository, githubRunId) {
1213
1545
  return `${serverUrl}/${repository}/actions/runs/${githubRunId}`;
@@ -1231,8 +1563,8 @@ async function reportToQuikTest(params) {
1231
1563
  try {
1232
1564
  console.log(`reporting ${report}`);
1233
1565
  const ciUrl = buildCiUrl(githubServerUrl, githubRepository, githubRunId);
1234
- const { readFileSync: readFileSync7 } = await import("node:fs");
1235
- const body = readFileSync7(report);
1566
+ const { readFileSync: readFileSync8 } = await import("node:fs");
1567
+ const body = readFileSync8(report);
1236
1568
  let url = `${resultsUrl}?ciUrl=${encodeURIComponent(ciUrl)}&automationIdFormat=bracket`;
1237
1569
  if (generatedFiles) {
1238
1570
  url += `&generatedFiles=${encodeURIComponent(generatedFiles.join(","))}`;
@@ -1262,7 +1594,7 @@ async function reportToQuikTest(params) {
1262
1594
  // src/baselineCheck.ts
1263
1595
  var import_node_child_process3 = require("node:child_process");
1264
1596
  var import_node_fs6 = require("node:fs");
1265
- var import_node_path4 = require("node:path");
1597
+ var import_node_path5 = require("node:path");
1266
1598
 
1267
1599
  // src/regressionCheck.ts
1268
1600
  var import_node_fs5 = require("node:fs");
@@ -1448,16 +1780,16 @@ function checkableIds(ids, types) {
1448
1780
  });
1449
1781
  }
1450
1782
  function insideCheckout(cwd, path) {
1451
- const abs = (0, import_node_path4.resolve)(cwd, path);
1452
- const rel = (0, import_node_path4.relative)(cwd, abs);
1453
- return rel === "" || rel.startsWith("..") || (0, import_node_path4.isAbsolute)(rel) ? null : abs;
1783
+ const abs = (0, import_node_path5.resolve)(cwd, path);
1784
+ const rel = (0, import_node_path5.relative)(cwd, abs);
1785
+ return rel === "" || rel.startsWith("..") || (0, import_node_path5.isAbsolute)(rel) ? null : abs;
1454
1786
  }
1455
1787
  function writeForCheck(abs, content) {
1456
1788
  const original = (0, import_node_fs6.existsSync)(abs) ? (0, import_node_fs6.readFileSync)(abs) : null;
1457
1789
  const createdDirs = [];
1458
1790
  if (original === null) {
1459
1791
  const missing = [];
1460
- for (let d = (0, import_node_path4.dirname)(abs); !(0, import_node_fs6.existsSync)(d); d = (0, import_node_path4.dirname)(d)) missing.push(d);
1792
+ for (let d = (0, import_node_path5.dirname)(abs); !(0, import_node_fs6.existsSync)(d); d = (0, import_node_path5.dirname)(d)) missing.push(d);
1461
1793
  for (const d of missing.reverse()) {
1462
1794
  (0, import_node_fs6.mkdirSync)(d);
1463
1795
  createdDirs.push(d);
@@ -1609,7 +1941,7 @@ async function runBaselineCheck(params) {
1609
1941
 
1610
1942
  // src/failureArtifact.ts
1611
1943
  var import_node_fs7 = require("node:fs");
1612
- var import_node_path5 = require("node:path");
1944
+ var import_node_path6 = require("node:path");
1613
1945
 
1614
1946
  // src/failureReason.ts
1615
1947
  var MAX_FAILURE_REASON = 1e3;
@@ -1650,18 +1982,18 @@ var FAILURE_ARTIFACT_DIR = "quikpilot-failure";
1650
1982
  var REPORT_COPY = "final-test-report.xml";
1651
1983
  var MAX_REPORT_BYTES = 5 * 1024 * 1024;
1652
1984
  var MAX_FILES = 200;
1653
- var RUNNER_VERSION = true ? "9.0.0" : "dev";
1985
+ var RUNNER_VERSION = true ? "9.2.0-rc.2" : "dev";
1654
1986
  function safeDestination(base, repoPath) {
1655
- if (!repoPath || (0, import_node_path5.isAbsolute)(repoPath)) return null;
1656
- const destination = (0, import_node_path5.resolve)(base, repoPath);
1657
- const rel = (0, import_node_path5.relative)(base, destination);
1658
- if (!rel || rel.startsWith("..") || (0, import_node_path5.isAbsolute)(rel)) return null;
1987
+ if (!repoPath || (0, import_node_path6.isAbsolute)(repoPath)) return null;
1988
+ const destination = (0, import_node_path6.resolve)(base, repoPath);
1989
+ const rel = (0, import_node_path6.relative)(base, destination);
1990
+ if (!rel || rel.startsWith("..") || (0, import_node_path6.isAbsolute)(rel)) return null;
1659
1991
  return destination;
1660
1992
  }
1661
1993
  function writeFailureArtifact(input) {
1662
1994
  if (input.results.length === 0) return { written: false };
1663
1995
  try {
1664
- const base = (0, import_node_path5.resolve)(input.cwd, FAILURE_ARTIFACT_DIR);
1996
+ const base = (0, import_node_path6.resolve)(input.cwd, FAILURE_ARTIFACT_DIR);
1665
1997
  (0, import_node_fs7.rmSync)(base, { recursive: true, force: true });
1666
1998
  (0, import_node_fs7.mkdirSync)(base, { recursive: true });
1667
1999
  const files = [];
@@ -1672,7 +2004,7 @@ function writeFailureArtifact(input) {
1672
2004
  skipped.push(result.targetPath);
1673
2005
  continue;
1674
2006
  }
1675
- (0, import_node_fs7.mkdirSync)((0, import_node_path5.dirname)(destination), { recursive: true });
2007
+ (0, import_node_fs7.mkdirSync)((0, import_node_path6.dirname)(destination), { recursive: true });
1676
2008
  (0, import_node_fs7.writeFileSync)(destination, result.newContent);
1677
2009
  files.push({ path: result.targetPath, bytes: Buffer.byteLength(result.newContent) });
1678
2010
  }
@@ -1681,7 +2013,7 @@ function writeFailureArtifact(input) {
1681
2013
  if (input.testReport.byteLength > MAX_REPORT_BYTES) {
1682
2014
  testReport = "too-large";
1683
2015
  } else {
1684
- (0, import_node_fs7.writeFileSync)((0, import_node_path5.join)(base, REPORT_COPY), input.testReport);
2016
+ (0, import_node_fs7.writeFileSync)((0, import_node_path6.join)(base, REPORT_COPY), input.testReport);
1685
2017
  testReport = "included";
1686
2018
  }
1687
2019
  }
@@ -1701,8 +2033,8 @@ function writeFailureArtifact(input) {
1701
2033
  failureReasons: input.failureReasons
1702
2034
  };
1703
2035
  const text = JSON.stringify(manifest, (_key, value) => typeof value === "string" ? scrubSecrets(value, input.secrets) : value, 2);
1704
- (0, import_node_fs7.writeFileSync)((0, import_node_path5.join)(base, "manifest.json"), text);
1705
- return { written: (0, import_node_fs7.existsSync)((0, import_node_path5.join)(base, "manifest.json")) };
2036
+ (0, import_node_fs7.writeFileSync)((0, import_node_path6.join)(base, "manifest.json"), text);
2037
+ return { written: (0, import_node_fs7.existsSync)((0, import_node_path6.join)(base, "manifest.json")) };
1706
2038
  } catch (err) {
1707
2039
  console.warn(`::warning::Could not keep the failed run's files: ${err instanceof Error ? err.message : String(err)}`);
1708
2040
  return { written: false };
@@ -1786,7 +2118,685 @@ function describeScopeViolations(check) {
1786
2118
  }
1787
2119
 
1788
2120
  // src/targeting.ts
1789
- var import_node_path6 = require("node:path");
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
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
1790
2800
  var CONTRACT_VERSION2 = "test-targets-1";
1791
2801
  function listWorkspaceFiles(workspaceRoot) {
1792
2802
  return listFiles(workspaceRoot ?? ".");
@@ -1794,13 +2804,13 @@ function listWorkspaceFiles(workspaceRoot) {
1794
2804
  function validateTarget(target, workspaceRoot, workspaceListing, deniedPaths) {
1795
2805
  const denied = matchesDeniedPath(target.path, deniedPaths);
1796
2806
  if (denied) {
1797
- throw new Error(
2807
+ throw new DeniedPathError(
1798
2808
  `pilot.targeting proposed a target matching a denied path (${denied}): ${target.path}`
1799
2809
  );
1800
2810
  }
1801
2811
  const deniedContext = findDeniedContextPath(target.contextPaths ?? [], deniedPaths);
1802
2812
  if (deniedContext) {
1803
- throw new Error(
2813
+ throw new DeniedPathError(
1804
2814
  `pilot.targeting cited a denied path as context (${deniedContext.pattern}): ${deniedContext.path} (target ${target.path})`
1805
2815
  );
1806
2816
  }
@@ -1815,15 +2825,15 @@ function validateTarget(target, workspaceRoot, workspaceListing, deniedPaths) {
1815
2825
  }
1816
2826
  return;
1817
2827
  }
1818
- const root = (0, import_node_path6.resolve)(workspaceRoot ?? ".");
1819
- const rel = (0, import_node_path6.relative)(root, (0, import_node_path6.resolve)(target.path));
1820
- if (rel.startsWith("..") || (0, import_node_path6.isAbsolute)(rel)) {
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)) {
1821
2831
  throw new Error(
1822
2832
  `pilot.targeting named a NEW_FILE path that escapes the workspace root: ${target.path}`
1823
2833
  );
1824
2834
  }
1825
2835
  }
1826
- async function runTargeting(gateway, design, workspaceRoot, workspaceListing, requiredTests, deniedPaths, testDiscovery, outOfScope = []) {
2836
+ function buildContextData(design, workspaceListing, requiredTests, deniedPaths, testDiscovery, outOfScope) {
1827
2837
  const contextData = {
1828
2838
  // NESTED, NOT FLATTENED — a second vocabulary for the design shape is exactly what
1829
2839
  // QUIKTR-1034 cost.
@@ -1842,6 +2852,9 @@ async function runTargeting(gateway, design, workspaceRoot, workspaceListing, re
1842
2852
  contextData.outOfScope = outOfScope;
1843
2853
  }
1844
2854
  contextData.testDiscovery = testDiscovery;
2855
+ return contextData;
2856
+ }
2857
+ async function postTargeting(gateway, contextData, signal) {
1845
2858
  const res = await fetchWithRetry(`${gateway.baseUrl}/ai/execute`, {
1846
2859
  method: "POST",
1847
2860
  headers: { "content-type": "application/json", "x-run-token": gateway.token },
@@ -1851,14 +2864,15 @@ async function runTargeting(gateway, design, workspaceRoot, workspaceListing, re
1851
2864
  expectedOutputType: "json",
1852
2865
  userPrompt: "",
1853
2866
  contextData
1854
- })
2867
+ }),
2868
+ ...signal ? { signal } : {}
1855
2869
  });
1856
2870
  if (!res.ok) {
1857
- throw new Error(`Targeting call failed: ${res.status} ${res.statusText}`);
2871
+ throw new TargetingHttpError(res.status, res.statusText);
1858
2872
  }
1859
- const envelope = await res.json();
1860
- checkHardFailure(envelope, "pilot.targeting");
1861
- const body = unwrapExecute(envelope, "pilot.targeting");
2873
+ return res.json();
2874
+ }
2875
+ function finalizeTargeting(body, workspaceRoot, workspaceListing, deniedPaths) {
1862
2876
  if (body.outcome === "NO_TARGET") {
1863
2877
  return { outcome: "NO_TARGET", reason: body.noTargetReason ?? null };
1864
2878
  }
@@ -1887,6 +2901,66 @@ async function runTargeting(gateway, design, workspaceRoot, workspaceListing, re
1887
2901
  ...correctedKinds.length > 0 ? { correctedKinds } : {}
1888
2902
  };
1889
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
+ }
1890
2964
 
1891
2965
  // src/targetingRecord.ts
1892
2966
  var MAX_TARGETS = 25;
@@ -1896,9 +2970,11 @@ var MAX_ADDED_PATHS = 20;
1896
2970
  var MAX_SKIPPED_CONTEXT = 20;
1897
2971
  var MAX_SKIPPED_CASES = 200;
1898
2972
  var MAX_CONTEXT_RECORDS = 25;
1899
- var MAX_PATH_CHARS = 300;
1900
- var MAX_ID_CHARS = 100;
2973
+ var MAX_PATH_CHARS2 = 300;
2974
+ var MAX_ID_CHARS2 = 100;
1901
2975
  var MAX_REASON_CHARS = 300;
2976
+ var MAX_LOOP_READS = 50;
2977
+ var MAX_FALLBACK_REASON_CHARS = 40;
1902
2978
  function clampList(values, max, maxChars) {
1903
2979
  let cut = false;
1904
2980
  const kept = [];
@@ -1913,6 +2989,41 @@ function clampList(values, max, maxChars) {
1913
2989
  }
1914
2990
  return { kept, cut };
1915
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
+ }
1916
3027
  function buildTargetingRecord(targeting, contextLog) {
1917
3028
  let truncated = false;
1918
3029
  const note = (cut) => {
@@ -1925,6 +3036,7 @@ function buildTargetingRecord(targeting, contextLog) {
1925
3036
  record2.noTargetReason = reason.slice(0, MAX_REASON_CHARS);
1926
3037
  if (reason.length > MAX_REASON_CHARS) record2.noTargetReasonTruncated = true;
1927
3038
  }
3039
+ if (targeting.loop) record2.loop = clampLoop(targeting.loop);
1928
3040
  return record2;
1929
3041
  }
1930
3042
  const targets = [];
@@ -1934,12 +3046,12 @@ function buildTargetingRecord(targeting, contextLog) {
1934
3046
  break;
1935
3047
  }
1936
3048
  const path = typeof t.path === "string" ? t.path.trim() : "";
1937
- if (path.length === 0 || path.length > MAX_PATH_CHARS) {
3049
+ if (path.length === 0 || path.length > MAX_PATH_CHARS2) {
1938
3050
  truncated = true;
1939
3051
  continue;
1940
3052
  }
1941
- const contextPaths = clampList(t.contextPaths, MAX_CONTEXT_PATHS, MAX_PATH_CHARS);
1942
- const automationIds = clampList(t.automationIds, MAX_IDS_PER_TARGET, MAX_ID_CHARS);
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);
1943
3055
  note(contextPaths.cut);
1944
3056
  note(automationIds.cut);
1945
3057
  targets.push({ path, kind: t.kind, contextPaths: contextPaths.kept, automationIds: automationIds.kept });
@@ -1947,7 +3059,7 @@ function buildTargetingRecord(targeting, contextLog) {
1947
3059
  const skipped = clampList(
1948
3060
  (targeting.skippedCases ?? []).map((c) => c.automationId),
1949
3061
  MAX_SKIPPED_CASES,
1950
- MAX_ID_CHARS
3062
+ MAX_ID_CHARS2
1951
3063
  );
1952
3064
  note(skipped.cut);
1953
3065
  const codegenContext = [];
@@ -1957,16 +3069,16 @@ function buildTargetingRecord(targeting, contextLog) {
1957
3069
  break;
1958
3070
  }
1959
3071
  const targetPath = typeof entry.targetPath === "string" ? entry.targetPath.trim() : "";
1960
- if (targetPath.length === 0 || targetPath.length > MAX_PATH_CHARS) {
3072
+ if (targetPath.length === 0 || targetPath.length > MAX_PATH_CHARS2) {
1961
3073
  truncated = true;
1962
3074
  continue;
1963
3075
  }
1964
- const contextPaths = clampList(entry.contextPaths, MAX_CONTEXT_PATHS, MAX_PATH_CHARS);
1965
- const addedPaths = clampList(entry.addedPaths, MAX_ADDED_PATHS, MAX_PATH_CHARS);
3076
+ const contextPaths = clampList(entry.contextPaths, MAX_CONTEXT_PATHS, MAX_PATH_CHARS2);
3077
+ const addedPaths = clampList(entry.addedPaths, MAX_ADDED_PATHS, MAX_PATH_CHARS2);
1966
3078
  const skippedPaths = clampList(
1967
3079
  (entry.skipped ?? []).map((s) => s.path),
1968
3080
  MAX_SKIPPED_CONTEXT,
1969
- MAX_PATH_CHARS
3081
+ MAX_PATH_CHARS2
1970
3082
  );
1971
3083
  note(contextPaths.cut);
1972
3084
  note(addedPaths.cut);
@@ -1987,6 +3099,7 @@ function buildTargetingRecord(targeting, contextLog) {
1987
3099
  codegenContext
1988
3100
  };
1989
3101
  if (truncated) record.truncated = true;
3102
+ if (targeting.loop) record.loop = clampLoop(targeting.loop);
1990
3103
  return record;
1991
3104
  }
1992
3105
 
@@ -2099,6 +3212,10 @@ async function run(env2) {
2099
3212
  outOfScope
2100
3213
  );
2101
3214
  targetingResult = targeting;
3215
+ for (const w of targeting.warnings ?? []) {
3216
+ warnings.push(w);
3217
+ console.warn(`QuikPilot targeting warning (not blocking): ${w}`);
3218
+ }
2102
3219
  if (targeting.outcome === "TARGETED") {
2103
3220
  for (const path of targeting.correctedKinds ?? []) {
2104
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.`;