@quikit/quikpilot-runner 7.0.5 → 8.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -23,18 +23,67 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
23
23
  mod
24
24
  ));
25
25
 
26
+ // src/env.ts
27
+ function required(name, value) {
28
+ if (!value) {
29
+ throw new Error(`Missing required environment variable: ${name}`);
30
+ }
31
+ return value;
32
+ }
33
+ function readRunnerEnv(env2 = process.env) {
34
+ const githubRunId = required("GITHUB_RUN_ID", env2.GITHUB_RUN_ID);
35
+ const githubServerUrl = env2.GITHUB_SERVER_URL ?? "https://github.com";
36
+ const githubRepository = required("GITHUB_REPOSITORY", env2.GITHUB_REPOSITORY);
37
+ if (env2.MODE === "readiness_check") {
38
+ return {
39
+ mode: "readiness_check",
40
+ readinessToken: required("READINESS_TOKEN", env2.READINESS_TOKEN),
41
+ readinessCallbackUrl: required("READINESS_CALLBACK_URL", env2.READINESS_CALLBACK_URL),
42
+ setupCmd: env2.SETUP_CMD ?? "",
43
+ testCmd: required("TEST_CMD", env2.TEST_CMD),
44
+ githubRunId,
45
+ githubServerUrl,
46
+ githubRepository
47
+ };
48
+ }
49
+ return {
50
+ mode: "run",
51
+ runId: required("RUN_ID", env2.RUN_ID),
52
+ callbackUrl: required("CALLBACK_URL", env2.CALLBACK_URL),
53
+ packetUrl: required("PACKET_URL", env2.PACKET_URL),
54
+ tokenUrl: required("TOKEN_URL", env2.TOKEN_URL),
55
+ installationTokenUrl: required("INSTALLATION_TOKEN_URL", env2.INSTALLATION_TOKEN_URL),
56
+ githubRunId,
57
+ githubServerUrl,
58
+ githubRepository
59
+ };
60
+ }
61
+
62
+ // src/quiktrackError.ts
63
+ async function describeQuikTrackFailure(res) {
64
+ try {
65
+ const body = await res.json();
66
+ if (body && typeof body === "object" && typeof body.reason_code === "string") {
67
+ return `${res.status} ${res.statusText} (${body.reason_code})`;
68
+ }
69
+ } catch {
70
+ }
71
+ return `${res.status} ${res.statusText}`;
72
+ }
73
+
26
74
  // src/callback.ts
27
75
  async function reportBack(params) {
28
- const { callbackUrl, runToken, status, branch } = params;
76
+ const { callbackUrl, runToken, status, branch, warnings } = params;
29
77
  const body = { status };
30
78
  if (branch) body.branch = branch;
79
+ if (warnings && warnings.length > 0) body.warnings = warnings;
31
80
  const res = await fetch(callbackUrl, {
32
81
  method: "POST",
33
82
  headers: { Authorization: `Bearer ${runToken}`, "Content-Type": "application/json" },
34
83
  body: JSON.stringify(body)
35
84
  });
36
85
  if (!res.ok) {
37
- throw new Error(`Report back to QuikTrack failed: ${res.status} ${res.statusText}`);
86
+ throw new Error(`Report back to QuikTrack failed: ${await describeQuikTrackFailure(res)}`);
38
87
  }
39
88
  }
40
89
 
@@ -130,14 +179,21 @@ function push(branch, installationToken) {
130
179
  }
131
180
 
132
181
  // src/commit.ts
133
- function commitAndPush(branch, runId, installationToken) {
182
+ function buildCommitMessage(runId, targeted, issueKey, issueTitle) {
183
+ if (!targeted) return `QuikPilot Phase 0 skeleton (run ${runId})`;
184
+ if (issueKey && issueTitle) return `${issueKey}: ${issueTitle}
185
+
186
+ QuikPilot run ${runId}`;
187
+ return `QuikPilot generated changes (run ${runId})`;
188
+ }
189
+ function commitAndPush(branch, runId, installationToken, targeted, issueKey, issueTitle) {
134
190
  if (!hasStagedChanges()) return { changed: false };
135
191
  if (!branch) {
136
192
  throw new Error("packet carried no branch name");
137
193
  }
138
194
  configureCommitIdentity();
139
195
  checkoutNewBranch(branch);
140
- commit(`QuikPilot Phase 0 skeleton (run ${runId})`);
196
+ commit(buildCommitMessage(runId, targeted, issueKey, issueTitle));
141
197
  push(branch, installationToken);
142
198
  return { changed: true, branch };
143
199
  }
@@ -147,6 +203,14 @@ var import_node_fs = require("node:fs");
147
203
  var import_node_path = require("node:path");
148
204
 
149
205
  // src/envelope.ts
206
+ var HARD_FAILURE_CODES = /* @__PURE__ */ new Set(["SEMANTIC_MISMATCH", "CONTRACT_REJECTED"]);
207
+ function checkHardFailure(envelope, useCase) {
208
+ const code = envelope?.errorCode;
209
+ if (typeof code === "string" && HARD_FAILURE_CODES.has(code)) {
210
+ const detail = envelope.error ?? envelope.normalizedText ?? "no detail";
211
+ throw new Error(`${useCase} failed (${code}, not retried): ${detail}`);
212
+ }
213
+ }
150
214
  function unwrapExecute(envelope, useCase) {
151
215
  if (envelope?.success === false) {
152
216
  throw new Error(
@@ -159,35 +223,131 @@ function unwrapExecute(envelope, useCase) {
159
223
  return envelope.structuredJson;
160
224
  }
161
225
 
226
+ // src/retryFetch.ts
227
+ var RETRYABLE_STATUS_CODES = /* @__PURE__ */ new Set([502, 503, 504]);
228
+ var RETRYABLE_NETWORK_ERROR_CODES = /* @__PURE__ */ new Set(["ECONNRESET", "ETIMEDOUT"]);
229
+ function isRetryableNetworkError(err) {
230
+ const code = err?.cause?.code ?? err?.code;
231
+ return typeof code === "string" && RETRYABLE_NETWORK_ERROR_CODES.has(code);
232
+ }
233
+ async function fetchWithRetry(url, init, { retries = 2, backoffMs = 250 } = {}) {
234
+ for (let attempt = 0; ; attempt++) {
235
+ try {
236
+ const res = await fetch(url, init);
237
+ if (RETRYABLE_STATUS_CODES.has(res.status) && attempt < retries) {
238
+ await new Promise((r) => setTimeout(r, backoffMs));
239
+ continue;
240
+ }
241
+ return res;
242
+ } catch (err) {
243
+ if (isRetryableNetworkError(err) && attempt < retries) {
244
+ await new Promise((r) => setTimeout(r, backoffMs));
245
+ continue;
246
+ }
247
+ throw err;
248
+ }
249
+ }
250
+ }
251
+
162
252
  // src/codegen.ts
163
253
  var FILE_TOO_LARGE_CODE = "CODEGEN_FILE_TOO_LARGE";
164
- function findNearestSibling(targetPath, workspaceListing) {
165
- let dir = (0, import_node_path.dirname)(targetPath);
166
- for (; ; ) {
167
- const candidate = workspaceListing.find((f) => f !== targetPath && (0, import_node_path.dirname)(f) === dir);
168
- if (candidate) return candidate;
169
- const parent = (0, import_node_path.dirname)(dir);
170
- if (parent === dir) return null;
171
- dir = parent;
254
+ var CONTEXT_TOO_LARGE_CODE = "CODEGEN_CONTEXT_TOO_LARGE";
255
+ var OUTPUT_TOO_LARGE_CODE = "CODEGEN_OUTPUT_TOO_LARGE";
256
+ var NOT_RETRIED_CODES = /* @__PURE__ */ new Set([FILE_TOO_LARGE_CODE, CONTEXT_TOO_LARGE_CODE, OUTPUT_TOO_LARGE_CODE]);
257
+ var SUSPECT_PLACEHOLDER_RE = /rest(?: of the)? (?:file|code) unchanged|remainder unchanged|\/\/\s*\.\.\.\s*(?:rest|remaining)/i;
258
+ var SUSPECT_LENGTH_DROP_RATIO = 0.6;
259
+ var SUSPECT_REMOVED_LINE_RATIO = 0.3;
260
+ var MIN_LINES_FOR_REMOVED_LINE_CHECK = 10;
261
+ var JS_TS_EXTENSION_RE = /\.(?:ts|tsx|js|jsx|mjs|cjs)$/;
262
+ var EXPORTED_SYMBOL_RE = /^export\s+(?:default\s+)?(?:async\s+)?(?:function\*?|class|const|let|var|interface|type|enum)\s+([A-Za-z_$][\w$]*)/gm;
263
+ function countRemovedLines(currentContent, newContent) {
264
+ const remaining = /* @__PURE__ */ new Map();
265
+ for (const line of newContent.split("\n")) {
266
+ remaining.set(line, (remaining.get(line) ?? 0) + 1);
267
+ }
268
+ let removed = 0;
269
+ for (const line of currentContent.split("\n")) {
270
+ const count = remaining.get(line) ?? 0;
271
+ if (count > 0) {
272
+ remaining.set(line, count - 1);
273
+ } else {
274
+ removed++;
275
+ }
276
+ }
277
+ return removed;
278
+ }
279
+ function extractExportedSymbols(content) {
280
+ const symbols = /* @__PURE__ */ new Set();
281
+ for (const match of content.matchAll(EXPORTED_SYMBOL_RE)) {
282
+ symbols.add(match[1]);
283
+ }
284
+ return symbols;
285
+ }
286
+ function removedUnexplainedSymbols(targetPath, planText, currentContent, newContent) {
287
+ if (!JS_TS_EXTENSION_RE.test(targetPath)) return [];
288
+ const before = extractExportedSymbols(currentContent);
289
+ const after = extractExportedSymbols(newContent);
290
+ const lowerPlan = planText.toLowerCase();
291
+ return [...before].filter((symbol) => !after.has(symbol) && !lowerPlan.includes(symbol.toLowerCase()));
292
+ }
293
+ function looksLikeTruncatedRewrite(target, currentContent, newContent) {
294
+ if (SUSPECT_PLACEHOLDER_RE.test(newContent)) {
295
+ return "contains placeholder text suggesting an abbreviated rewrite";
296
+ }
297
+ if (currentContent.length > 0 && newContent.length < currentContent.length * SUSPECT_LENGTH_DROP_RATIO) {
298
+ return `sharp length drop (${currentContent.length} -> ${newContent.length} chars)`;
299
+ }
300
+ const currentLineCount = currentContent.length > 0 ? currentContent.split("\n").length : 0;
301
+ if (currentLineCount >= MIN_LINES_FOR_REMOVED_LINE_CHECK) {
302
+ const removedLines = countRemovedLines(currentContent, newContent);
303
+ if (removedLines > currentLineCount * SUSPECT_REMOVED_LINE_RATIO) {
304
+ const pct = (removedLines / currentLineCount * 100).toFixed(0);
305
+ return `removed ${removedLines} of ${currentLineCount} lines (${pct}%) without a matching drop in character count`;
306
+ }
307
+ }
308
+ const planText = target.planStepsImplemented.join(" ");
309
+ const unexplainedSymbols = removedUnexplainedSymbols(target.path, planText, currentContent, newContent);
310
+ if (unexplainedSymbols.length > 0) {
311
+ return `removed exported symbol(s) the plan steps don't mention: ${unexplainedSymbols.join(", ")}`;
172
312
  }
313
+ return null;
173
314
  }
174
- async function runCodegen(gateway, targets, workspaceListing) {
315
+ var CONTRACT_VERSION = "test-targets-1";
316
+ async function runCodegen(gateway, targets, workspaceListing, requiredTests, deniedPaths) {
175
317
  const results = [];
318
+ const listing = new Set(workspaceListing);
176
319
  for (const target of targets) {
177
320
  const isNewFile = target.kind === "NEW_FILE";
178
321
  const currentContent = isNewFile ? "" : (0, import_node_fs.readFileSync)(target.path, "utf8");
322
+ const contextPaths = target.contextPaths ?? [];
323
+ for (const contextPath of contextPaths) {
324
+ if (!listing.has(contextPath)) {
325
+ throw new Error(
326
+ `pilot.codegen contextPaths named a file not found in the checkout: ${contextPath} (target ${target.path})`
327
+ );
328
+ }
329
+ }
330
+ const contextContents = contextPaths.map((path) => ({ path, content: (0, import_node_fs.readFileSync)(path, "utf8") }));
179
331
  const contextData = {
180
332
  targetPath: target.path,
181
333
  currentContent,
182
334
  planSteps: target.planStepsImplemented,
183
- kind: target.kind
335
+ kind: target.kind,
336
+ contextContents,
337
+ // QUIKTR-1083 (§15.6) — required on every call on the versioned path.
338
+ contractVersion: CONTRACT_VERSION
184
339
  };
185
340
  if (isNewFile) {
186
341
  contextData.workspaceListing = workspaceListing;
187
- const sibling = findNearestSibling(target.path, workspaceListing);
188
- if (sibling) contextData.siblingContent = (0, import_node_fs.readFileSync)(sibling, "utf8");
189
342
  }
190
- const res = await fetch(`${gateway.baseUrl}/ai/execute`, {
343
+ if (deniedPaths.length > 0) {
344
+ contextData.deniedPaths = deniedPaths;
345
+ }
346
+ if (target.automationIds && target.automationIds.length > 0) {
347
+ const wanted = new Set(target.automationIds);
348
+ contextData.requiredTests = requiredTests.filter((tc) => wanted.has(tc.automationId));
349
+ }
350
+ const res = await fetchWithRetry(`${gateway.baseUrl}/ai/execute`, {
191
351
  method: "POST",
192
352
  headers: { "content-type": "application/json", "x-run-token": gateway.token },
193
353
  body: JSON.stringify({
@@ -202,8 +362,10 @@ async function runCodegen(gateway, targets, workspaceListing) {
202
362
  throw new Error(`Codegen call failed for ${target.path}: ${res.status} ${res.statusText}`);
203
363
  }
204
364
  const envelope = await res.json();
205
- if (envelope?.errorCode === FILE_TOO_LARGE_CODE) {
206
- throw new Error(`Codegen refused ${target.path}: ${FILE_TOO_LARGE_CODE} (not retried)`);
365
+ checkHardFailure(envelope, "pilot.codegen");
366
+ if (typeof envelope?.errorCode === "string" && NOT_RETRIED_CODES.has(envelope.errorCode)) {
367
+ const detail = envelope.error ? `: ${envelope.error}` : "";
368
+ throw new Error(`Codegen refused ${target.path}: ${envelope.errorCode} (not retried)${detail}`);
207
369
  }
208
370
  const body = unwrapExecute(envelope, "pilot.codegen");
209
371
  if (body.outcome !== "GENERATED" || typeof body.newContent !== "string") {
@@ -211,6 +373,24 @@ async function runCodegen(gateway, targets, workspaceListing) {
211
373
  `Codegen refused ${target.path}${body.refusalReason ? `: ${body.refusalReason}` : ""}`
212
374
  );
213
375
  }
376
+ if (!isNewFile) {
377
+ const suspectReason = looksLikeTruncatedRewrite(target, currentContent, body.newContent);
378
+ if (suspectReason) {
379
+ throw new Error(
380
+ `Codegen output for ${target.path} looks like a truncated/abbreviated rewrite (${suspectReason}) \u2014 refusing to apply it. Interim guard per \xA716.7; the AI Runtime's own output-cap check is not live yet.`
381
+ );
382
+ }
383
+ }
384
+ console.log(
385
+ JSON.stringify({
386
+ event: "codegen.metrics",
387
+ targetPath: target.path,
388
+ kind: target.kind,
389
+ inputLength: currentContent.length,
390
+ outputLength: body.newContent.length,
391
+ removedLines: isNewFile ? null : countRemovedLines(currentContent, body.newContent)
392
+ })
393
+ );
214
394
  results.push({ targetPath: target.path, newContent: body.newContent });
215
395
  }
216
396
  return results;
@@ -265,30 +445,32 @@ var NODE_BUILTINS = /* @__PURE__ */ new Set([
265
445
  "inspector",
266
446
  "async_hooks"
267
447
  ]);
268
- function packageNameFromSpecifier(specifier) {
448
+ var DEFAULT_ALIAS_PREFIXES = ["@/", "~/"];
449
+ function packageNameFromSpecifier(specifier, aliasPrefixes = DEFAULT_ALIAS_PREFIXES) {
269
450
  if (specifier.startsWith(".") || specifier.startsWith("/")) return null;
270
451
  if (specifier.startsWith("node:")) return null;
452
+ if (aliasPrefixes.some((prefix) => specifier.startsWith(prefix))) return null;
271
453
  const segments = specifier.split("/");
272
454
  const name = specifier.startsWith("@") ? segments.slice(0, 2).join("/") : segments[0];
273
455
  if (NODE_BUILTINS.has(name)) return null;
274
456
  return name;
275
457
  }
276
- function scanImports(source) {
458
+ function scanImports(source, aliasPrefixes = DEFAULT_ALIAS_PREFIXES) {
277
459
  const found = /* @__PURE__ */ new Set();
278
460
  let match;
279
461
  IMPORT_RE.lastIndex = 0;
280
462
  while (match = IMPORT_RE.exec(source)) {
281
- const name = packageNameFromSpecifier(match[2]);
463
+ const name = packageNameFromSpecifier(match[2], aliasPrefixes);
282
464
  if (name) found.add(name);
283
465
  }
284
466
  return [...found];
285
467
  }
286
- function checkImportDependencies(results, declaredDependencies) {
468
+ function checkImportDependencies(results, declaredDependencies, aliasPrefixes = DEFAULT_ALIAS_PREFIXES) {
287
469
  if (!declaredDependencies) return { blocked: false, violations: [] };
288
470
  const violations = [];
289
471
  for (const { targetPath, newContent } of results) {
290
472
  if (!SCANNABLE_EXTENSIONS.some((ext) => targetPath.endsWith(ext))) continue;
291
- const invented = scanImports(newContent).filter((pkg) => !declaredDependencies.has(pkg));
473
+ const invented = scanImports(newContent, aliasPrefixes).filter((pkg) => !declaredDependencies.has(pkg));
292
474
  if (invented.length > 0) violations.push({ file: targetPath, packages: invented });
293
475
  }
294
476
  return { blocked: violations.length > 0, violations };
@@ -320,28 +502,86 @@ function readDeclaredDependencies(results, workspaceRoot) {
320
502
  return null;
321
503
  }
322
504
  }
323
- function describeDependencyViolations(check) {
324
- return check.violations.map((v) => `${v.file} imports ${v.packages.join(", ")}, not a dependency of this repository`).join("; ");
505
+ function stripJsonComments(text) {
506
+ let out = "";
507
+ let inString = false;
508
+ let inLineComment = false;
509
+ let inBlockComment = false;
510
+ for (let i = 0; i < text.length; i++) {
511
+ const c = text[i];
512
+ const next = text[i + 1];
513
+ if (inLineComment) {
514
+ if (c === "\n") {
515
+ inLineComment = false;
516
+ out += c;
517
+ }
518
+ continue;
519
+ }
520
+ if (inBlockComment) {
521
+ if (c === "*" && next === "/") {
522
+ inBlockComment = false;
523
+ i++;
524
+ }
525
+ continue;
526
+ }
527
+ if (inString) {
528
+ out += c;
529
+ if (c === "\\") {
530
+ out += next;
531
+ i++;
532
+ continue;
533
+ }
534
+ if (c === '"') inString = false;
535
+ continue;
536
+ }
537
+ if (c === '"') {
538
+ inString = true;
539
+ out += c;
540
+ continue;
541
+ }
542
+ if (c === "/" && next === "/") {
543
+ inLineComment = true;
544
+ i++;
545
+ continue;
546
+ }
547
+ if (c === "/" && next === "*") {
548
+ inBlockComment = true;
549
+ i++;
550
+ continue;
551
+ }
552
+ out += c;
553
+ }
554
+ return out.replace(/,(\s*[}\]])/g, "$1");
325
555
  }
326
-
327
- // src/env.ts
328
- function required(name, value) {
329
- if (!value) {
330
- throw new Error(`Missing required environment variable: ${name}`);
556
+ function resolveAliasPrefixes(results, workspaceRoot) {
557
+ const root = (0, import_node_path2.resolve)(workspaceRoot ?? ".");
558
+ for (const filename of ["tsconfig.json", "jsconfig.json"]) {
559
+ const configPath = (0, import_node_path2.join)(root, filename);
560
+ const generated = results.find((r) => (0, import_node_path2.resolve)(r.targetPath) === configPath);
561
+ let raw;
562
+ if (generated) {
563
+ raw = generated.newContent;
564
+ } else {
565
+ try {
566
+ raw = (0, import_node_fs2.readFileSync)(configPath, "utf8");
567
+ } catch {
568
+ raw = null;
569
+ }
570
+ }
571
+ if (raw === null) continue;
572
+ try {
573
+ const parsed = JSON.parse(stripJsonComments(raw));
574
+ const paths = parsed.compilerOptions?.paths;
575
+ const fromConfig = paths && typeof paths === "object" ? Object.keys(paths).map((k) => k.endsWith("*") ? k.slice(0, -1) : k) : [];
576
+ return [.../* @__PURE__ */ new Set([...DEFAULT_ALIAS_PREFIXES, ...fromConfig])];
577
+ } catch {
578
+ continue;
579
+ }
331
580
  }
332
- return value;
581
+ return DEFAULT_ALIAS_PREFIXES;
333
582
  }
334
- function readRunnerEnv(env = process.env) {
335
- return {
336
- runId: required("RUN_ID", env.RUN_ID),
337
- callbackUrl: required("CALLBACK_URL", env.CALLBACK_URL),
338
- packetUrl: required("PACKET_URL", env.PACKET_URL),
339
- tokenUrl: required("TOKEN_URL", env.TOKEN_URL),
340
- installationTokenUrl: required("INSTALLATION_TOKEN_URL", env.INSTALLATION_TOKEN_URL),
341
- githubRunId: required("GITHUB_RUN_ID", env.GITHUB_RUN_ID),
342
- githubServerUrl: env.GITHUB_SERVER_URL ?? "https://github.com",
343
- githubRepository: required("GITHUB_REPOSITORY", env.GITHUB_REPOSITORY)
344
- };
583
+ function describeDependencyViolations(check) {
584
+ return check.violations.map((v) => `${v.file} imports ${v.packages.join(", ")}, not a dependency of this repository`).join("; ");
345
585
  }
346
586
 
347
587
  // src/guardrail.ts
@@ -361,7 +601,7 @@ async function exchangeForInstallationToken(installationTokenUrl, runToken) {
361
601
  headers: { Authorization: `Bearer ${runToken}` }
362
602
  });
363
603
  if (!res.ok) {
364
- throw new Error(`Exchange installation token failed: ${res.status} ${res.statusText}`);
604
+ throw new Error(`Exchange installation token failed: ${await describeQuikTrackFailure(res)}`);
365
605
  }
366
606
  const body = await res.json();
367
607
  if (body.success !== true) {
@@ -372,14 +612,14 @@ async function exchangeForInstallationToken(installationTokenUrl, runToken) {
372
612
 
373
613
  // src/oidc.ts
374
614
  var QUIKPILOT_OIDC_AUDIENCE = "quiktrack-quikpilot";
375
- async function fetchGithubOidcToken(env = process.env) {
615
+ async function fetchGithubOidcToken(env2 = process.env) {
376
616
  const requestUrl = required(
377
617
  "ACTIONS_ID_TOKEN_REQUEST_URL",
378
- env.ACTIONS_ID_TOKEN_REQUEST_URL
618
+ env2.ACTIONS_ID_TOKEN_REQUEST_URL
379
619
  );
380
620
  const requestToken = required(
381
621
  "ACTIONS_ID_TOKEN_REQUEST_TOKEN",
382
- env.ACTIONS_ID_TOKEN_REQUEST_TOKEN
622
+ env2.ACTIONS_ID_TOKEN_REQUEST_TOKEN
383
623
  );
384
624
  const separator = requestUrl.includes("?") ? "&" : "?";
385
625
  const url = `${requestUrl}${separator}audience=${encodeURIComponent(QUIKPILOT_OIDC_AUDIENCE)}`;
@@ -396,6 +636,108 @@ async function fetchGithubOidcToken(env = process.env) {
396
636
  return body.value;
397
637
  }
398
638
 
639
+ // src/overlapCheck.ts
640
+ var TEST_PATH_RE = /(\.(test|spec)\.[jt]sx?$)|(^|\/)(__tests__|tests?)\//i;
641
+ function isTestTarget(path) {
642
+ return TEST_PATH_RE.test(path);
643
+ }
644
+ var STOPWORDS = /* @__PURE__ */ new Set([
645
+ "a",
646
+ "an",
647
+ "the",
648
+ "is",
649
+ "are",
650
+ "was",
651
+ "were",
652
+ "be",
653
+ "been",
654
+ "being",
655
+ "of",
656
+ "to",
657
+ "in",
658
+ "on",
659
+ "for",
660
+ "with",
661
+ "at",
662
+ "by",
663
+ "from",
664
+ "as",
665
+ "and",
666
+ "or",
667
+ "not",
668
+ "no",
669
+ "nor",
670
+ "this",
671
+ "that",
672
+ "these",
673
+ "those",
674
+ "it",
675
+ "its",
676
+ "cannot",
677
+ "can't",
678
+ "should",
679
+ "must",
680
+ "may",
681
+ "will",
682
+ "shall",
683
+ "when",
684
+ "if",
685
+ "than",
686
+ "then",
687
+ "so",
688
+ "do",
689
+ "does",
690
+ "did"
691
+ ]);
692
+ var MIN_WORD_LENGTH = 3;
693
+ function significantWords(step) {
694
+ return new Set(
695
+ step.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((w) => w.length >= MIN_WORD_LENGTH && !STOPWORDS.has(w))
696
+ );
697
+ }
698
+ function jaccard(a, b) {
699
+ if (a.size === 0 || b.size === 0) return 0;
700
+ let intersection = 0;
701
+ for (const w of a) if (b.has(w)) intersection++;
702
+ const union = a.size + b.size - intersection;
703
+ return intersection / union;
704
+ }
705
+ function normalizedStep(step) {
706
+ return step.trim().toLowerCase().replace(/\s+/g, " ");
707
+ }
708
+ var OVERLAP_THRESHOLD = 0.5;
709
+ function findOverlappingSteps(targets) {
710
+ const items = targets.flatMap(
711
+ (target) => target.planStepsImplemented.map((step) => ({
712
+ target: target.path,
713
+ step,
714
+ words: significantWords(step)
715
+ }))
716
+ );
717
+ const warnings = [];
718
+ for (let i = 0; i < items.length; i++) {
719
+ for (let j = i + 1; j < items.length; j++) {
720
+ if (items[i].target === items[j].target) continue;
721
+ if (isTestTarget(items[i].target) || isTestTarget(items[j].target)) continue;
722
+ if (jaccard(items[i].words, items[j].words) >= OVERLAP_THRESHOLD) {
723
+ warnings.push({
724
+ targetA: items[i].target,
725
+ stepA: items[i].step,
726
+ targetB: items[j].target,
727
+ stepB: items[j].step,
728
+ severity: normalizedStep(items[i].step) === normalizedStep(items[j].step) ? "identical" : "similar"
729
+ });
730
+ }
731
+ }
732
+ }
733
+ return warnings;
734
+ }
735
+ function describeOverlapWarnings(warnings) {
736
+ return warnings.map(
737
+ (w) => `${w.targetA} and ${w.targetB} both implement what looks like the same rule \u2014 "${w.stepA}" / "${w.stepB}". Check these aren't two independent implementations of the same behavior.`
738
+ );
739
+ }
740
+
399
741
  // src/packet.ts
400
742
  async function fetchPacket(packetUrl, runToken, githubRunId) {
401
743
  const url = `${packetUrl}?githubRunId=${encodeURIComponent(githubRunId)}`;
@@ -403,7 +745,7 @@ async function fetchPacket(packetUrl, runToken, githubRunId) {
403
745
  headers: { Authorization: `Bearer ${runToken}` }
404
746
  });
405
747
  if (!res.ok) {
406
- throw new Error(`Fetch work packet failed: ${res.status} ${res.statusText}`);
748
+ throw new Error(`Fetch work packet failed: ${await describeQuikTrackFailure(res)}`);
407
749
  }
408
750
  const body = await res.json();
409
751
  if (body.success !== true) {
@@ -479,9 +821,9 @@ async function reportToQuikTest(params) {
479
821
  try {
480
822
  console.log(`reporting ${report}`);
481
823
  const ciUrl = buildCiUrl(githubServerUrl, githubRepository, githubRunId);
482
- const { readFileSync: readFileSync4 } = await import("node:fs");
483
- const body = readFileSync4(report);
484
- const url = `${resultsUrl}?ciUrl=${encodeURIComponent(ciUrl)}`;
824
+ const { readFileSync: readFileSync5 } = await import("node:fs");
825
+ const body = readFileSync5(report);
826
+ const url = `${resultsUrl}?ciUrl=${encodeURIComponent(ciUrl)}&automationIdFormat=bracket`;
485
827
  const res = await fetch(url, {
486
828
  method: "POST",
487
829
  headers: { Authorization: `Bearer ${runToken}`, "Content-Type": "text/xml" },
@@ -502,8 +844,24 @@ var TESTCASE_RE = /<testcase\b([^>]*?)(?:\/>|>([\s\S]*?)<\/testcase>)/g;
502
844
  var NAME_ATTR_RE = /\bname="([^"]*)"/;
503
845
  var CLASSNAME_ATTR_RE = /\bclassname="([^"]*)"/;
504
846
  var FAILURE_CHILD_RE = /<failure\b|<error\b/;
847
+ var FAILURE_TAG_RE = /<(?:failure|error)\b([^>]*?)(?:\/>|>([\s\S]*?)<\/(?:failure|error)>)/;
848
+ var FAILURE_MESSAGE_ATTR_RE = /\bmessage="([^"]*)"/;
849
+ var MAX_FAILURE_TEXT = 500;
850
+ function unescapeXmlEntities(s) {
851
+ return s.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&apos;/g, "'").replace(/&amp;/g, "&");
852
+ }
853
+ function extractFailureText(testcaseBody) {
854
+ const tag = FAILURE_TAG_RE.exec(testcaseBody);
855
+ if (!tag) return null;
856
+ const [, attrs = "", inner = ""] = tag;
857
+ const raw = (FAILURE_MESSAGE_ATTR_RE.exec(attrs)?.[1] ?? inner).trim();
858
+ if (!raw) return null;
859
+ const text = unescapeXmlEntities(raw);
860
+ return text.length > MAX_FAILURE_TEXT ? `${text.slice(0, MAX_FAILURE_TEXT)}\u2026` : text;
861
+ }
505
862
  function parseJUnitOutcomes(xml) {
506
- const results = /* @__PURE__ */ new Map();
863
+ const outcomes = /* @__PURE__ */ new Map();
864
+ const failureText = /* @__PURE__ */ new Map();
507
865
  let match;
508
866
  TESTCASE_RE.lastIndex = 0;
509
867
  while (match = TESTCASE_RE.exec(xml)) {
@@ -512,9 +870,14 @@ function parseJUnitOutcomes(xml) {
512
870
  if (!name) continue;
513
871
  const classname = CLASSNAME_ATTR_RE.exec(attrs)?.[1];
514
872
  const id = classname ? `${classname}::${name}` : name;
515
- results.set(id, !FAILURE_CHILD_RE.test(body));
873
+ const failed = FAILURE_CHILD_RE.test(body);
874
+ outcomes.set(id, !failed);
875
+ if (failed) {
876
+ const text = extractFailureText(body);
877
+ if (text) failureText.set(id, text);
878
+ }
516
879
  }
517
- return results;
880
+ return { outcomes, failureText };
518
881
  }
519
882
  function readJUnitOutcomes(cwd) {
520
883
  const report = findJUnitReport(cwd);
@@ -527,35 +890,55 @@ function readJUnitOutcomes(cwd) {
527
890
  }
528
891
  function captureTestSnapshot(command, cwd = process.cwd()) {
529
892
  const outcome = runTestCommand(command);
530
- return { outcome, results: readJUnitOutcomes(cwd) };
893
+ const parsed = readJUnitOutcomes(cwd);
894
+ return { outcome, results: parsed?.outcomes ?? null, failureText: parsed?.failureText };
531
895
  }
532
896
  async function captureBaselineSnapshot(command, cwd = process.cwd()) {
533
897
  const outcome = await runTestCommandAsync(command);
534
- return { outcome, results: readJUnitOutcomes(cwd) };
898
+ const parsed = readJUnitOutcomes(cwd);
899
+ return { outcome, results: parsed?.outcomes ?? null, failureText: parsed?.failureText };
535
900
  }
536
901
  function checkForRegressions(baseline, after) {
537
902
  if (!baseline.results || !after.results) {
538
- return { blocked: false, regressions: [], newFailures: [], preExisting: [] };
903
+ return { blocked: false, regressions: [], newFailures: [], preExisting: [], failureText: /* @__PURE__ */ new Map() };
539
904
  }
540
905
  const regressions = [];
541
906
  const newFailures = [];
542
907
  const preExisting = [];
908
+ const failureText = /* @__PURE__ */ new Map();
909
+ const afterFailureText = after.failureText;
543
910
  for (const [testId, passed] of after.results) {
544
911
  if (passed) continue;
545
912
  const before = baseline.results.get(testId);
546
- if (before === true) regressions.push(testId);
547
- else if (before === false) preExisting.push(testId);
548
- else newFailures.push(testId);
913
+ if (before === true) {
914
+ regressions.push(testId);
915
+ const text = afterFailureText?.get(testId);
916
+ if (text) failureText.set(testId, text);
917
+ } else if (before === false) {
918
+ preExisting.push(testId);
919
+ } else {
920
+ newFailures.push(testId);
921
+ const text = afterFailureText?.get(testId);
922
+ if (text) failureText.set(testId, text);
923
+ }
549
924
  }
550
- return { blocked: regressions.length > 0 || newFailures.length > 0, regressions, newFailures, preExisting };
925
+ return { blocked: regressions.length > 0 || newFailures.length > 0, regressions, newFailures, preExisting, failureText };
551
926
  }
552
927
  function describeRegressions(check) {
928
+ const withText = (id) => {
929
+ const text = check.failureText.get(id);
930
+ return text ? `${id} (${text})` : id;
931
+ };
553
932
  const parts = [];
554
933
  if (check.regressions.length > 0) {
555
- parts.push(`${check.regressions.length} regression(s) (passed before, now fail): ${check.regressions.join(", ")}`);
934
+ parts.push(
935
+ `${check.regressions.length} regression(s) (passed before, now fail): ${check.regressions.map(withText).join(", ")}`
936
+ );
556
937
  }
557
938
  if (check.newFailures.length > 0) {
558
- parts.push(`${check.newFailures.length} new failure(s) (did not exist before): ${check.newFailures.join(", ")}`);
939
+ parts.push(
940
+ `${check.newFailures.length} new failure(s) (did not exist before): ${check.newFailures.map(withText).join(", ")}`
941
+ );
559
942
  }
560
943
  if (check.preExisting.length > 0) {
561
944
  parts.push(`${check.preExisting.length} pre-existing failure(s), unrelated to this change, not blocking`);
@@ -563,12 +946,99 @@ function describeRegressions(check) {
563
946
  return parts.join("; ");
564
947
  }
565
948
 
949
+ // src/scopeCheck.ts
950
+ var import_node_fs6 = require("node:fs");
951
+ var ROUTE_METHOD_RE = /\b(GET|POST|PUT|PATCH|DELETE)\b/i;
952
+ var ROUTE_PATH_RE = /\/[A-Za-z0-9_\-{}[\]]+(?:\/[A-Za-z0-9_\-:{}[\]]+)+/;
953
+ var ROUTE_EXPORT_RE = /\bexport\s+(?:async\s+)?function\s+(GET|POST|PUT|PATCH|DELETE)\s*\(/;
954
+ var NO_PATH = "(this file's route)";
955
+ function detectMentions(text) {
956
+ const found = [];
957
+ for (const line of text.split("\n")) {
958
+ const methodMatch = ROUTE_METHOD_RE.exec(line);
959
+ const pathMatch = ROUTE_PATH_RE.exec(line);
960
+ if (methodMatch && pathMatch) {
961
+ found.push({ method: methodMatch[1].toUpperCase(), path: pathMatch[0] });
962
+ }
963
+ const exportMatch = ROUTE_EXPORT_RE.exec(line);
964
+ if (exportMatch) {
965
+ found.push({ method: exportMatch[1].toUpperCase(), path: NO_PATH });
966
+ }
967
+ }
968
+ return found;
969
+ }
970
+ function normalizePath(path) {
971
+ return path.replace(/:[A-Za-z0-9_]+|\{[^}]+\}|\[[^\]]+\]/g, "*");
972
+ }
973
+ function surfaceCovers(surface, mention) {
974
+ if (!new RegExp(`\\b${mention.method}\\b`, "i").test(surface)) return false;
975
+ if (mention.path === NO_PATH) return true;
976
+ const declaredPath = normalizePath(surface);
977
+ const mentionPath = normalizePath(mention.path);
978
+ return declaredPath.includes(mentionPath) || mentionPath.includes(declaredPath);
979
+ }
980
+ function addedLines(before, after) {
981
+ const beforeLines = new Set(before.split("\n"));
982
+ return after.split("\n").filter((line) => !beforeLines.has(line)).join("\n");
983
+ }
984
+ function checkApiScope(results, targets, apiChanges) {
985
+ const targetsByPath = new Map(targets.map((t) => [t.path, t]));
986
+ const violations = [];
987
+ for (const { targetPath, newContent } of results) {
988
+ const target = targetsByPath.get(targetPath);
989
+ const isNewFile = target?.kind === "NEW_FILE";
990
+ let scanned;
991
+ if (isNewFile) {
992
+ scanned = newContent;
993
+ } else {
994
+ let currentContent;
995
+ try {
996
+ currentContent = (0, import_node_fs6.readFileSync)(targetPath, "utf8");
997
+ } catch {
998
+ currentContent = "";
999
+ }
1000
+ scanned = addedLines(currentContent, newContent);
1001
+ }
1002
+ for (const mention of detectMentions(scanned)) {
1003
+ if (!apiChanges.some((c) => surfaceCovers(c.surface, mention))) {
1004
+ violations.push({ file: targetPath, method: mention.method, path: mention.path });
1005
+ }
1006
+ }
1007
+ }
1008
+ return { blocked: violations.length > 0, violations };
1009
+ }
1010
+ function describeScopeViolations(check) {
1011
+ return check.violations.map((v) => `${v.file} adds ${v.method} ${v.path}, not declared in the design's API changes`).join("; ");
1012
+ }
1013
+
566
1014
  // src/targeting.ts
567
1015
  var import_node_path4 = require("node:path");
1016
+
1017
+ // src/deniedPaths.ts
1018
+ function globToRegExp(pattern) {
1019
+ const escaped = pattern.split("").map((c) => /[.+^${}()|[\]\\]/.test(c) ? `\\${c}` : c).join("");
1020
+ const withGlobs = escaped.replace(/\*\*\//g, " SLASH ").replace(/\*\*/g, " STAR ").replace(/\*/g, "[^/]*").replace(/ SLASH /g, "(?:.*/)?").replace(/ STAR /g, ".*");
1021
+ return new RegExp(`^${withGlobs}$`);
1022
+ }
1023
+ function matchesDeniedPath(path, patterns) {
1024
+ for (const pattern of patterns) {
1025
+ if (globToRegExp(pattern).test(path)) return pattern;
1026
+ }
1027
+ return null;
1028
+ }
1029
+
1030
+ // src/targeting.ts
1031
+ var CONTRACT_VERSION2 = "test-targets-1";
568
1032
  function listWorkspaceFiles(workspaceRoot) {
569
1033
  return listFiles(workspaceRoot ?? ".");
570
1034
  }
571
- function validateTarget(target, workspaceRoot, workspaceListing) {
1035
+ function validateTarget(target, workspaceRoot, workspaceListing, deniedPaths) {
1036
+ const denied = matchesDeniedPath(target.path, deniedPaths);
1037
+ if (denied) {
1038
+ throw new Error(
1039
+ `pilot.targeting proposed a target matching a denied path (${denied}): ${target.path}`
1040
+ );
1041
+ }
572
1042
  if (target.kind !== "EXISTING" && target.kind !== "NEW_FILE") {
573
1043
  throw new Error(`pilot.targeting returned an unknown kind "${target.kind}" for ${target.path}`);
574
1044
  }
@@ -588,8 +1058,22 @@ function validateTarget(target, workspaceRoot, workspaceListing) {
588
1058
  );
589
1059
  }
590
1060
  }
591
- async function runTargeting(gateway, design, workspaceRoot, workspaceListing) {
592
- const res = await fetch(`${gateway.baseUrl}/ai/execute`, {
1061
+ async function runTargeting(gateway, design, workspaceRoot, workspaceListing, requiredTests, deniedPaths) {
1062
+ const contextData = {
1063
+ // NESTED, NOT FLATTENED — a second vocabulary for the design shape is exactly what
1064
+ // QUIKTR-1034 cost.
1065
+ workspaceListing,
1066
+ design,
1067
+ // QUIKTR-1065/1083 — requiredTests carried through so targeting can propose a test-file
1068
+ // target naming which cases it covers (§15.1's Case shape, unchanged).
1069
+ requiredTests,
1070
+ // QUIKTR-1083 (§15.6) — required on the versioned path.
1071
+ contractVersion: CONTRACT_VERSION2
1072
+ };
1073
+ if (deniedPaths.length > 0) {
1074
+ contextData.deniedPaths = deniedPaths;
1075
+ }
1076
+ const res = await fetchWithRetry(`${gateway.baseUrl}/ai/execute`, {
593
1077
  method: "POST",
594
1078
  headers: { "content-type": "application/json", "x-run-token": gateway.token },
595
1079
  body: JSON.stringify({
@@ -597,15 +1081,14 @@ async function runTargeting(gateway, design, workspaceRoot, workspaceListing) {
597
1081
  useCase: "pilot.targeting",
598
1082
  expectedOutputType: "json",
599
1083
  userPrompt: "",
600
- // NESTED, NOT FLATTENED — a second vocabulary for the design shape is exactly what
601
- // QUIKTR-1034 cost.
602
- contextData: { workspaceListing, design }
1084
+ contextData
603
1085
  })
604
1086
  });
605
1087
  if (!res.ok) {
606
1088
  throw new Error(`Targeting call failed: ${res.status} ${res.statusText}`);
607
1089
  }
608
1090
  const envelope = await res.json();
1091
+ checkHardFailure(envelope, "pilot.targeting");
609
1092
  const body = unwrapExecute(envelope, "pilot.targeting");
610
1093
  if (body.outcome === "NO_TARGET") {
611
1094
  return { outcome: "NO_TARGET", reason: body.noTargetReason ?? null };
@@ -615,9 +1098,13 @@ async function runTargeting(gateway, design, workspaceRoot, workspaceListing) {
615
1098
  }
616
1099
  const listing = new Set(workspaceListing);
617
1100
  for (const target of body.targets) {
618
- validateTarget(target, workspaceRoot, listing);
1101
+ validateTarget(target, workspaceRoot, listing, deniedPaths);
619
1102
  }
620
- return { outcome: "TARGETED", targets: body.targets };
1103
+ return {
1104
+ outcome: "TARGETED",
1105
+ targets: body.targets,
1106
+ skippedCases: body.skippedCases ?? []
1107
+ };
621
1108
  }
622
1109
 
623
1110
  // src/tokenExchange.ts
@@ -627,7 +1114,7 @@ async function exchangeForRunToken(tokenUrl, oidcToken) {
627
1114
  headers: { Authorization: `Bearer ${oidcToken}` }
628
1115
  });
629
1116
  if (!res.ok) {
630
- throw new Error(`Exchange run token failed: ${res.status} ${res.statusText}`);
1117
+ throw new Error(`Exchange run token failed: ${await describeQuikTrackFailure(res)}`);
631
1118
  }
632
1119
  const body = await res.json();
633
1120
  if (body.success !== true) {
@@ -637,42 +1124,81 @@ async function exchangeForRunToken(tokenUrl, oidcToken) {
637
1124
  }
638
1125
 
639
1126
  // src/run.ts
640
- async function run(env = readRunnerEnv()) {
1127
+ async function run(env2) {
641
1128
  let hardFailure = false;
642
1129
  let packet;
643
1130
  let branch;
644
1131
  let runToken;
1132
+ let warnings = [];
1133
+ let targeted = false;
645
1134
  try {
646
- runToken = await exchangeForRunToken(env.tokenUrl, await fetchGithubOidcToken());
647
- packet = await fetchPacket(env.packetUrl, runToken, env.githubRunId);
1135
+ runToken = await exchangeForRunToken(env2.tokenUrl, await fetchGithubOidcToken());
1136
+ packet = await fetchPacket(env2.packetUrl, runToken, env2.githubRunId);
648
1137
  checkoutTargetBranch(packet.targetBranch);
649
1138
  const workspaceListing = listWorkspaceFiles(packet.workspaceRoot);
650
1139
  const targeting = await runTargeting(
651
1140
  packet.gateway,
652
1141
  packet.design,
653
1142
  packet.workspaceRoot,
654
- workspaceListing
1143
+ workspaceListing,
1144
+ packet.requiredTests,
1145
+ packet.guardrails.deniedPaths
655
1146
  );
656
1147
  if (targeting.outcome === "NO_TARGET") {
657
- writePhase0Marker(env.runId);
1148
+ writePhase0Marker(env2.runId);
658
1149
  runCommand(packet.commands.setup, "Setup");
659
1150
  runCommand(packet.commands.lint, "Lint");
660
1151
  runTestCommand(packet.commands.test);
661
1152
  } else {
1153
+ targeted = true;
1154
+ if (targeting.skippedCases?.length) {
1155
+ for (const skipped of targeting.skippedCases) {
1156
+ const w = `Test case ${skipped.automationId} has no given/when/then \u2014 skipped, no test was written for it.`;
1157
+ warnings.push(w);
1158
+ console.warn(`QuikPilot targeting warning (not blocking): ${w}`);
1159
+ }
1160
+ }
1161
+ const overlaps = findOverlappingSteps(targeting.targets);
1162
+ const blockingOverlaps = overlaps.filter((o) => o.severity === "identical");
1163
+ const softOverlaps = overlaps.filter((o) => o.severity === "similar");
1164
+ if (softOverlaps.length > 0) {
1165
+ warnings = describeOverlapWarnings(softOverlaps);
1166
+ for (const w of warnings) {
1167
+ console.warn(`QuikPilot targeting warning (not blocking): ${w}`);
1168
+ }
1169
+ }
1170
+ if (blockingOverlaps.length > 0) {
1171
+ throw new Error(
1172
+ `QuikPilot's generated plan gives two files identical instructions \u2014 ${describeOverlapWarnings(blockingOverlaps).join("; ")}`
1173
+ );
1174
+ }
662
1175
  runCommand(packet.commands.setup, "Setup");
663
1176
  const [baseline, results] = await Promise.all([
664
1177
  captureBaselineSnapshot(packet.commands.test),
665
- runCodegen(packet.gateway, targeting.targets, workspaceListing)
1178
+ runCodegen(
1179
+ packet.gateway,
1180
+ targeting.targets,
1181
+ workspaceListing,
1182
+ packet.requiredTests,
1183
+ packet.guardrails.deniedPaths
1184
+ )
666
1185
  ]);
667
1186
  const depCheck = checkImportDependencies(
668
1187
  results,
669
- readDeclaredDependencies(results, packet.workspaceRoot)
1188
+ readDeclaredDependencies(results, packet.workspaceRoot),
1189
+ resolveAliasPrefixes(results, packet.workspaceRoot)
670
1190
  );
671
1191
  if (depCheck.blocked) {
672
1192
  throw new Error(
673
1193
  `QuikPilot's generated code imports packages this repository doesn't have \u2014 ${describeDependencyViolations(depCheck)}`
674
1194
  );
675
1195
  }
1196
+ const scopeCheck = checkApiScope(results, targeting.targets, packet.design?.apiChanges ?? []);
1197
+ if (scopeCheck.blocked) {
1198
+ throw new Error(
1199
+ `QuikPilot's generated code exceeds the approved design \u2014 ${describeScopeViolations(scopeCheck)}`
1200
+ );
1201
+ }
676
1202
  flushCodegenResults(results);
677
1203
  try {
678
1204
  runCommand(packet.commands.format, "Format");
@@ -699,18 +1225,25 @@ async function run(env = readRunnerEnv()) {
699
1225
  await reportToQuikTest({
700
1226
  resultsUrl: packet?.quikTest?.resultsUrl,
701
1227
  runToken,
702
- githubServerUrl: env.githubServerUrl,
703
- githubRepository: env.githubRepository,
704
- githubRunId: env.githubRunId
1228
+ githubServerUrl: env2.githubServerUrl,
1229
+ githubRepository: env2.githubRepository,
1230
+ githubRunId: env2.githubRunId
705
1231
  });
706
1232
  if (!hardFailure && packet) {
707
1233
  try {
708
1234
  checkGuardrail(packet.guardrails.deniedPaths);
709
1235
  const installationToken = await exchangeForInstallationToken(
710
- env.installationTokenUrl,
1236
+ env2.installationTokenUrl,
711
1237
  runToken
712
1238
  );
713
- const result = commitAndPush(packet.branch, env.runId, installationToken);
1239
+ const result = commitAndPush(
1240
+ packet.branch,
1241
+ env2.runId,
1242
+ installationToken,
1243
+ targeted,
1244
+ packet.issueKey,
1245
+ packet.issueTitle
1246
+ );
714
1247
  branch = result.branch;
715
1248
  } catch (err) {
716
1249
  hardFailure = true;
@@ -718,16 +1251,53 @@ async function run(env = readRunnerEnv()) {
718
1251
  }
719
1252
  }
720
1253
  await reportBack({
721
- callbackUrl: env.callbackUrl,
1254
+ callbackUrl: env2.callbackUrl,
722
1255
  runToken,
723
1256
  status: hardFailure ? "failed" : "completed",
724
- branch
1257
+ branch,
1258
+ warnings
725
1259
  });
726
1260
  return { hardFailure };
727
1261
  }
728
1262
 
1263
+ // src/readinessCheck.ts
1264
+ async function runReadinessCheck(env2) {
1265
+ let setupOk = true;
1266
+ try {
1267
+ runCommand(env2.setupCmd || null, "Setup");
1268
+ } catch (err) {
1269
+ setupOk = false;
1270
+ console.error(err instanceof Error ? err.message : String(err));
1271
+ }
1272
+ const testOutcome = runTestCommand(env2.testCmd);
1273
+ const testOk = testOutcome === "success";
1274
+ const junitPath = findJUnitReport();
1275
+ const result = {
1276
+ setupOk,
1277
+ testOk,
1278
+ junitFound: junitPath !== null,
1279
+ junitPath
1280
+ };
1281
+ try {
1282
+ const res = await fetch(env2.readinessCallbackUrl, {
1283
+ method: "POST",
1284
+ headers: { Authorization: `Bearer ${env2.readinessToken}`, "Content-Type": "application/json" },
1285
+ body: JSON.stringify(result)
1286
+ });
1287
+ if (!res.ok) {
1288
+ throw new Error(`Readiness callback POST failed: ${await describeQuikTrackFailure(res)}`);
1289
+ }
1290
+ } catch (err) {
1291
+ console.warn(
1292
+ `::warning::QuikPilot readiness check failed to report its result: ${err instanceof Error ? err.message : String(err)}`
1293
+ );
1294
+ }
1295
+ }
1296
+
729
1297
  // src/index.ts
730
- run().then(({ hardFailure }) => {
1298
+ var env = readRunnerEnv();
1299
+ var task = env.mode === "readiness_check" ? runReadinessCheck(env).then(() => ({ hardFailure: false })) : run(env);
1300
+ task.then(({ hardFailure }) => {
731
1301
  process.exitCode = hardFailure ? 1 : 0;
732
1302
  }).catch((err) => {
733
1303
  console.error(err instanceof Error ? err.message : String(err));