@pushpalsdev/cli 1.1.18 → 1.1.20

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.
@@ -34,6 +34,7 @@ import type {
34
34
  DockerWarmShellResult,
35
35
  DockerWarmStartupContext,
36
36
  } from "./backends/types.js";
37
+ import { resolveFreshWorktreeBaseRef } from "./worktree_base_ref.js";
37
38
 
38
39
  const DEFAULT_OPENHANDS_MODEL = "local-model";
39
40
  const DEFAULT_CONFIG = loadPushPalsConfig();
@@ -2106,7 +2107,27 @@ export class DockerExecutor {
2106
2107
  reviewAgent && typeof reviewAgent.resolutionType === "string"
2107
2108
  ? reviewAgent.resolutionType.trim().toLowerCase()
2108
2109
  : "";
2109
- if (resolutionType !== "merge_conflict") return this.options.baseRef;
2110
+ if (resolutionType !== "merge_conflict") {
2111
+ return resolveFreshWorktreeBaseRef({
2112
+ requestedRef: this.options.baseRef,
2113
+ integrationBranch:
2114
+ this.config.sourceControlManager.mainBranch ||
2115
+ this.config.workerpals.baseRef ||
2116
+ this.options.baseRef,
2117
+ sourceBaseBranch: this.config.sourceControlManager.baseBranch,
2118
+ git: (args) => this.runGitBaseRefCommand(args),
2119
+ log: (level, message) => {
2120
+ const line = `[DockerExecutor] ${message}`;
2121
+ if (level === "warn") {
2122
+ console.warn(line);
2123
+ onLog?.("stderr", line);
2124
+ } else {
2125
+ console.log(line);
2126
+ onLog?.("stdout", line);
2127
+ }
2128
+ },
2129
+ });
2130
+ }
2110
2131
 
2111
2132
  const normalizedHeadRef = normalizeMergeConflictHeadRef(reviewAgent?.prHeadRef);
2112
2133
  if (!normalizedHeadRef) {
@@ -2150,6 +2171,26 @@ export class DockerExecutor {
2150
2171
  return remoteRef;
2151
2172
  }
2152
2173
 
2174
+ private async runGitBaseRefCommand(
2175
+ args: string[],
2176
+ ): Promise<{ ok: boolean; stdout: string; stderr: string }> {
2177
+ const proc = Bun.spawn(["git", ...args], {
2178
+ cwd: this.options.repo,
2179
+ stdout: "pipe",
2180
+ stderr: "pipe",
2181
+ });
2182
+ const [exitCode, stdout, stderr] = await Promise.all([
2183
+ proc.exited,
2184
+ new Response(proc.stdout).text(),
2185
+ new Response(proc.stderr).text(),
2186
+ ]);
2187
+ return {
2188
+ ok: exitCode === 0,
2189
+ stdout,
2190
+ stderr,
2191
+ };
2192
+ }
2193
+
2153
2194
  /**
2154
2195
  * Pull the Docker image
2155
2196
  */
@@ -66,6 +66,7 @@ export interface TaskExecutePlanning {
66
66
  acceptanceCriteria: string[];
67
67
  validationSteps: string[];
68
68
  requiredValidationSteps?: string[];
69
+ repoHintDiagnostics?: string[];
69
70
  queuePriority: TaskExecutePriority;
70
71
  queueWaitBudgetMs: number;
71
72
  executionBudgetMs: number;
@@ -132,6 +133,18 @@ interface BrowserFailureMemoryEntry {
132
133
  suggestedRemedy: string;
133
134
  }
134
135
 
136
+ interface ValidationRemedyMemoryEntry {
137
+ key: string;
138
+ jobFamily: string;
139
+ command: string;
140
+ failureClass: string;
141
+ digest: string;
142
+ count: number;
143
+ firstSeenAt: string;
144
+ lastSeenAt: string;
145
+ suggestedRemedy: string;
146
+ }
147
+
135
148
  interface DeterministicQualityResult {
136
149
  ok: boolean;
137
150
  skipped: boolean;
@@ -219,6 +232,70 @@ export function qualityRevisionBudgetDecision(opts: {
219
232
  };
220
233
  }
221
234
 
235
+ export function workerAttemptRolloutScore(params: {
236
+ executorElapsedMs: number;
237
+ qualityElapsedMs: number;
238
+ changedPaths: string[];
239
+ validationRuns: ValidationExecutionResult[];
240
+ qualityIssues: string[];
241
+ criticScore?: number | null;
242
+ }): { score: number; reasons: string[] } {
243
+ let score = 0;
244
+ const reasons: string[] = [];
245
+ const publishable = publishableChangedPaths(params.changedPaths);
246
+ if (publishable.length > 0) {
247
+ score += 35;
248
+ reasons.push("publishable_diff");
249
+ } else if (params.changedPaths.length > 0) {
250
+ score -= 35;
251
+ reasons.push("artifact_only_diff");
252
+ } else {
253
+ score -= 20;
254
+ reasons.push("no_diff");
255
+ }
256
+ const passedFast = params.validationRuns.filter(
257
+ (run) => run.ok && !isLongRunningBrowserValidationCommand(run.command),
258
+ ).length;
259
+ const failedFast = params.validationRuns.filter(
260
+ (run) => !run.ok && !isLongRunningBrowserValidationCommand(run.command),
261
+ ).length;
262
+ if (passedFast > 0) {
263
+ score += Math.min(20, passedFast * 8);
264
+ reasons.push("fast_validation_passed");
265
+ }
266
+ if (failedFast > 0) {
267
+ score -= Math.min(20, failedFast * 8);
268
+ reasons.push("fast_validation_failed");
269
+ }
270
+ if (params.validationRuns.some((run) => run.ok && isLongRunningBrowserValidationCommand(run.command))) {
271
+ score += 15;
272
+ reasons.push("long_validation_passed");
273
+ }
274
+ if (params.qualityIssues.length === 0) {
275
+ score += 20;
276
+ reasons.push("quality_clean");
277
+ } else {
278
+ score -= Math.min(30, params.qualityIssues.length * 6);
279
+ reasons.push("quality_issues");
280
+ }
281
+ if (typeof params.criticScore === "number" && Number.isFinite(params.criticScore)) {
282
+ score += Math.max(-20, Math.min(20, Math.round((params.criticScore - 8) * 5)));
283
+ reasons.push("critic_scored");
284
+ }
285
+ const totalElapsedMs = Math.max(0, params.executorElapsedMs + params.qualityElapsedMs);
286
+ if (totalElapsedMs > 1_800_000) {
287
+ score -= 20;
288
+ reasons.push("over_30m");
289
+ } else if (totalElapsedMs <= 1_200_000) {
290
+ score += 10;
291
+ reasons.push("under_20m");
292
+ }
293
+ return {
294
+ score: Math.max(-100, Math.min(100, score)),
295
+ reasons: reasons.slice(0, 8),
296
+ };
297
+ }
298
+
222
299
  function taskRequestsBrowserValidation(params: Record<string, unknown>): boolean {
223
300
  const candidates: string[] = [];
224
301
  const collect = (value: unknown) => {
@@ -429,6 +506,7 @@ function collectPlanningText(planning: TaskExecutePlanning): string {
429
506
  ...(planning.acceptanceCriteria ?? []),
430
507
  ...(planning.validationSteps ?? []),
431
508
  ...(planning.requiredValidationSteps ?? []),
509
+ ...(planning.repoHintDiagnostics ?? []),
432
510
  ...(planning.discovery?.keywords ?? []),
433
511
  ...(planning.discovery?.likelyDirs ?? []),
434
512
  ...(planning.discovery?.ripgrepQueries ?? []),
@@ -1193,6 +1271,36 @@ export function isParallelSafeFastValidationCommand(repo: string, command: strin
1193
1271
  return false;
1194
1272
  }
1195
1273
 
1274
+ function isDeterministicFastValidationFailure(run: ValidationExecutionResult): boolean {
1275
+ if (run.ok || run.exitCode === 127 || isLongRunningBrowserValidationCommand(run.command)) {
1276
+ return false;
1277
+ }
1278
+ const combined = stripAnsiControlSequences([run.stderr, run.stdout].filter(Boolean).join("\n"));
1279
+ if (!combined.trim()) return false;
1280
+ return (
1281
+ /\bCannot find module\b|\bmodule not found\b|\bfailed to resolve import\b|\bcould not resolve\b|\bNo such file or directory\b|\bENOENT\b/i.test(
1282
+ combined,
1283
+ ) ||
1284
+ /\bTS\d{4}\b|\btype error\b|\bno exported member\b|\bdoes not exist on type\b|\bis not assignable to\b/i.test(
1285
+ combined,
1286
+ ) ||
1287
+ /\berror:\s+"eslint"\s+exited with code\s+\d+\b/i.test(combined) ||
1288
+ /\bSyntaxError\b|\bReferenceError\b|\bTypeError\b/i.test(combined)
1289
+ );
1290
+ }
1291
+
1292
+ export function shouldDeferLongValidationAfterFastFailures(
1293
+ command: string,
1294
+ previousRuns: ValidationExecutionResult[],
1295
+ ): string | null {
1296
+ if (!isLongRunningBrowserValidationCommand(command)) return null;
1297
+ const deterministicFailures = previousRuns.filter(isDeterministicFastValidationFailure);
1298
+ if (deterministicFailures.length === 0) return null;
1299
+ const first = deterministicFailures[0];
1300
+ const digest = extractValidationFailureDigest(first);
1301
+ return `fast validation already failed for "${first.command}"${digest ? ` (${digest})` : ""}`;
1302
+ }
1303
+
1196
1304
  function readPackageJson(repo: string): {
1197
1305
  scripts?: Record<string, unknown>;
1198
1306
  dependencies?: Record<string, unknown>;
@@ -2427,6 +2535,21 @@ function resolveFailureMemoryPath(repo: string): string {
2427
2535
  return resolve(root, "outputs", "data", "workerpals-failure-memory.json");
2428
2536
  }
2429
2537
 
2538
+ function resolveRemedyMemoryPath(repo: string): string {
2539
+ const rootCandidates = [
2540
+ process.env.PUSHPALS_PROJECT_ROOT_OVERRIDE,
2541
+ process.env.PUSHPALS_REPO_ROOT_OVERRIDE,
2542
+ process.env.PUSHPALS_REPO_PATH,
2543
+ repo,
2544
+ ]
2545
+ .map((entry) => String(entry ?? "").trim())
2546
+ .filter(Boolean);
2547
+ const root = rootCandidates.find((entry) => existsSync(entry)) ?? repo;
2548
+ const gitStatePath = resolveGitStateFilePath(root, "pushpals-worker-remedy-memory.json");
2549
+ if (gitStatePath) return gitStatePath;
2550
+ return resolve(root, "outputs", "data", "workerpals-remedy-memory.json");
2551
+ }
2552
+
2430
2553
  function readBrowserFailureMemory(repo: string): BrowserFailureMemoryEntry[] {
2431
2554
  const memoryPath = resolveFailureMemoryPath(repo);
2432
2555
  try {
@@ -2513,6 +2636,149 @@ export function recordBrowserFailureMemory(
2513
2636
  }
2514
2637
  }
2515
2638
 
2639
+ function classifyValidationFailureForRemedy(run: ValidationExecutionResult): string {
2640
+ const combined = stripAnsiControlSequences([run.stderr, run.stdout].filter(Boolean).join("\n"));
2641
+ if (isLongRunningBrowserValidationCommand(run.command)) return "browser";
2642
+ if (/\bCannot find module\b|\bmodule not found\b|\bfailed to resolve import\b|\bcould not resolve\b/i.test(combined)) {
2643
+ return "module-resolution";
2644
+ }
2645
+ if (/\bTS\d{4}\b|\btype error\b|\bno exported member\b|\bdoes not exist on type\b|\bis not assignable to\b/i.test(combined)) {
2646
+ return "typecheck";
2647
+ }
2648
+ if (/\bESLint\b|\beslint\b|\blint\b/i.test(run.command) || /\berror:\s+"eslint"\s+exited/i.test(combined)) {
2649
+ return "lint";
2650
+ }
2651
+ if (/\bNo such file or directory\b|\bENOENT\b|\bpath does not exist\b/i.test(combined)) {
2652
+ return "missing-path";
2653
+ }
2654
+ if (/\breact[- ]native|mock|__mocks__|setupTests?|jest|vitest|test helper\b/i.test(combined)) {
2655
+ return "test-harness";
2656
+ }
2657
+ return "validation";
2658
+ }
2659
+
2660
+ function validationRemedyMemoryKey(jobFamily: string, run: ValidationExecutionResult): string {
2661
+ const failureClass = classifyValidationFailureForRemedy(run);
2662
+ const digest = extractValidationFailureRetryDigest(run);
2663
+ return [
2664
+ jobFamily,
2665
+ validationCommandKey(run.command),
2666
+ failureClass,
2667
+ normalizeFailureMemoryToken(digest),
2668
+ ]
2669
+ .filter(Boolean)
2670
+ .join("|");
2671
+ }
2672
+
2673
+ function validationFailureSuggestedRemedy(run: ValidationExecutionResult): string {
2674
+ const failureClass = classifyValidationFailureForRemedy(run);
2675
+ switch (failureClass) {
2676
+ case "module-resolution":
2677
+ return "Fix or avoid the missing import/path first; do not run long browser validation while module resolution is broken.";
2678
+ case "typecheck":
2679
+ return "Fix TypeScript/type errors before broader validation; prefer the smallest type-safe patch over test-harness expansion.";
2680
+ case "lint":
2681
+ return "Fix lint/static issues before expensive runtime checks; avoid unrelated formatting churn.";
2682
+ case "missing-path":
2683
+ return "Treat absent hinted paths as stale unless the task explicitly asks to create them; switch to an existing repo-native owner.";
2684
+ case "test-harness":
2685
+ return "If failures are in mocks/import setup, reduce to smaller helper/state coverage instead of broad shared mock expansion.";
2686
+ default:
2687
+ return "Repair the first deterministic fast validation failure before running long browser/e2e validation.";
2688
+ }
2689
+ }
2690
+
2691
+ function readValidationRemedyMemory(repo: string): ValidationRemedyMemoryEntry[] {
2692
+ const memoryPath = resolveRemedyMemoryPath(repo);
2693
+ try {
2694
+ const parsed = JSON.parse(readFileSync(memoryPath, "utf8")) as { entries?: unknown };
2695
+ if (!Array.isArray(parsed.entries)) return [];
2696
+ return parsed.entries
2697
+ .filter((entry): entry is ValidationRemedyMemoryEntry =>
2698
+ Boolean(entry && typeof entry === "object"),
2699
+ )
2700
+ .slice(0, 120);
2701
+ } catch {
2702
+ return [];
2703
+ }
2704
+ }
2705
+
2706
+ export function knownValidationRemedyHintsForRuns(
2707
+ repo: string,
2708
+ jobFamily: string,
2709
+ runs: ValidationExecutionResult[],
2710
+ ): string[] {
2711
+ const failed = runs.filter((run) => !run.ok && !isLongRunningBrowserValidationCommand(run.command));
2712
+ if (failed.length === 0) return [];
2713
+ const entries = readValidationRemedyMemory(repo);
2714
+ const hints: string[] = [];
2715
+ for (const run of failed.slice(0, 4)) {
2716
+ const failureClass = classifyValidationFailureForRemedy(run);
2717
+ const commandKey = validationCommandKey(run.command);
2718
+ const matches = entries
2719
+ .filter(
2720
+ (entry) =>
2721
+ entry.jobFamily === jobFamily &&
2722
+ validationCommandKey(entry.command) === commandKey &&
2723
+ entry.failureClass === failureClass,
2724
+ )
2725
+ .sort((a, b) => b.count - a.count || b.lastSeenAt.localeCompare(a.lastSeenAt))
2726
+ .slice(0, 2);
2727
+ for (const entry of matches) {
2728
+ hints.push(
2729
+ toSingleLine(
2730
+ `${entry.command} ${entry.failureClass} seen ${entry.count}x before; last=${entry.lastSeenAt}; remedy=${entry.suggestedRemedy}`,
2731
+ 360,
2732
+ ),
2733
+ );
2734
+ }
2735
+ }
2736
+ return Array.from(new Set(hints)).slice(0, 5);
2737
+ }
2738
+
2739
+ export function recordValidationRemedyMemory(
2740
+ repo: string,
2741
+ jobFamily: string,
2742
+ runs: ValidationExecutionResult[],
2743
+ ): void {
2744
+ const failed = runs.filter((run) => !run.ok && !isLongRunningBrowserValidationCommand(run.command));
2745
+ if (failed.length === 0) return;
2746
+ const memoryPath = resolveRemedyMemoryPath(repo);
2747
+ const now = new Date().toISOString();
2748
+ const entries = readValidationRemedyMemory(repo);
2749
+ for (const run of failed.slice(0, 6)) {
2750
+ const key = validationRemedyMemoryKey(jobFamily, run);
2751
+ const existing = entries.find((entry) => entry.key === key);
2752
+ if (existing) {
2753
+ existing.count += 1;
2754
+ existing.lastSeenAt = now;
2755
+ existing.digest = extractValidationFailureRetryDigest(run);
2756
+ existing.suggestedRemedy = validationFailureSuggestedRemedy(run);
2757
+ } else {
2758
+ entries.push({
2759
+ key,
2760
+ jobFamily,
2761
+ command: run.command,
2762
+ failureClass: classifyValidationFailureForRemedy(run),
2763
+ digest: extractValidationFailureRetryDigest(run),
2764
+ count: 1,
2765
+ firstSeenAt: now,
2766
+ lastSeenAt: now,
2767
+ suggestedRemedy: validationFailureSuggestedRemedy(run),
2768
+ });
2769
+ }
2770
+ }
2771
+ const next = entries
2772
+ .sort((a, b) => b.lastSeenAt.localeCompare(a.lastSeenAt))
2773
+ .slice(0, 120);
2774
+ try {
2775
+ mkdirSync(resolve(memoryPath, ".."), { recursive: true });
2776
+ writeFileSync(memoryPath, `${JSON.stringify({ version: 1, entries: next }, null, 2)}\n`);
2777
+ } catch {
2778
+ // Remedy memory is advisory; never fail a worker job because persistence is unavailable.
2779
+ }
2780
+ }
2781
+
2516
2782
  export function extractValidationFailureRetryDigest(
2517
2783
  run: {
2518
2784
  command: string;
@@ -3295,6 +3561,26 @@ async function runDeterministicQualityGate(
3295
3561
  );
3296
3562
  continue;
3297
3563
  }
3564
+ const deferredReason = shouldDeferLongValidationAfterFastFailures(command, validationRuns);
3565
+ if (deferredReason) {
3566
+ const stderr =
3567
+ `Skipped long validation command because ${deferredReason}. ` +
3568
+ "Fix the deterministic fast validation blocker first; PushPals will run long browser/e2e validation after the fast layer is clean.";
3569
+ validationRuns.push({
3570
+ step: command,
3571
+ command,
3572
+ ok: false,
3573
+ exitCode: 125,
3574
+ stdout: "",
3575
+ stderr,
3576
+ elapsedMs: 1,
3577
+ });
3578
+ onLog?.(
3579
+ "stderr",
3580
+ `[ValidationGate] Deferred long validation after fast failure: ${command} (${deferredReason})`,
3581
+ );
3582
+ continue;
3583
+ }
3298
3584
  const commandNeedsPlaywrightBrowserRuntime = shouldEnsurePlaywrightBrowserRuntime(
3299
3585
  repo,
3300
3586
  command,
@@ -3764,6 +4050,7 @@ export function buildQualityRevisionHint(
3764
4050
  validationBlocker: ValidationBlocker | null = null,
3765
4051
  browserRepairPacket: BrowserValidationRepairPacket | null = null,
3766
4052
  changedPaths: string[] = [],
4053
+ validationRemedyHints: string[] = [],
3767
4054
  ): string {
3768
4055
  const lines: string[] = [];
3769
4056
  lines.push("Quality revision required before completion.");
@@ -3781,6 +4068,21 @@ export function buildQualityRevisionHint(
3781
4068
  validationRuns,
3782
4069
  );
3783
4070
  if (testHarnessConvergenceWarning) lines.push(testHarnessConvergenceWarning);
4071
+ if ((planning.repoHintDiagnostics ?? []).length > 0) {
4072
+ lines.push("Repo hint diagnostics:");
4073
+ for (const hint of planning.repoHintDiagnostics ?? []) {
4074
+ lines.push(`- ${hint}`);
4075
+ }
4076
+ lines.push(
4077
+ "Hint handling rule: stale or absent path hints are advisory context, not permission to invent repo-specific scaffolding. Prefer an existing behavior owner or existing nearby test.",
4078
+ );
4079
+ }
4080
+ if (validationRemedyHints.length > 0) {
4081
+ lines.push("Known issue/remedy memory for this repo/job family:");
4082
+ for (const hint of validationRemedyHints.slice(0, 5)) {
4083
+ lines.push(`- ${hint}`);
4084
+ }
4085
+ }
3784
4086
  if (planningLooksLikeVisualDerivationTask(planning)) {
3785
4087
  lines.push(
3786
4088
  "Visual derivation testing rule: prefer pure helper/state/style-prop tests for planet/projectile/ownership/readability cues. Only add a full React Native render regression when this repo already has a stable harness for that exact surface; otherwise keep render-visible behavior covered through the derived inputs that drive it.",
@@ -6221,16 +6523,112 @@ export function collectWriteScopeIssuesFromChangedPaths(
6221
6523
  return [];
6222
6524
  }
6223
6525
 
6224
- function sanitizeTaskExecutePlanningPathHints(value: unknown): unknown {
6526
+ function pathHintHasGlob(value: string): boolean {
6527
+ return /[*?[\]{}]/.test(value);
6528
+ }
6529
+
6530
+ function pathHintLooksLikeConcreteFile(value: string): boolean {
6531
+ const normalized = value.replace(/\\/g, "/").replace(/^\.\/+/, "");
6532
+ const tail = normalized.split("/").pop() ?? normalized;
6533
+ return /\.[A-Za-z0-9][A-Za-z0-9_-]{0,12}$/.test(tail);
6534
+ }
6535
+
6536
+ function taskTextAllowsCreatingMissingPaths(value: string): boolean {
6537
+ return /\b(create|add|new|scaffold|generate|introduce|write)\b.{0,80}\b(file|test|module|component|script|page|route|fixture|helper)\b/i.test(
6538
+ value,
6539
+ );
6540
+ }
6541
+
6542
+ function shouldTreatMissingPathHintAsStale(
6543
+ repo: string,
6544
+ path: string,
6545
+ taskText: string,
6546
+ ): boolean {
6547
+ const normalized = normalizeStagePath(path);
6548
+ if (!normalized || normalized === "." || pathHintHasGlob(normalized)) return false;
6549
+ if (existsSync(resolve(repo, normalized))) return false;
6550
+ if (!pathHintLooksLikeConcreteFile(normalized)) return false;
6551
+ if (taskTextAllowsCreatingMissingPaths(taskText)) return false;
6552
+ return true;
6553
+ }
6554
+
6555
+ function pathParentExists(repo: string, path: string): boolean {
6556
+ const normalized = normalizeStagePath(path);
6557
+ if (!normalized || normalized === "." || pathHintHasGlob(normalized)) return true;
6558
+ const parts = normalized.split("/");
6559
+ if (parts.length <= 1) return true;
6560
+ return existsSync(resolve(repo, parts.slice(0, -1).join("/")));
6561
+ }
6562
+
6563
+ function sanitizeStalePathHints(
6564
+ repo: string,
6565
+ values: unknown,
6566
+ taskText: string,
6567
+ ): { values: string[]; stale: string[]; diagnostics: string[] } {
6568
+ const stale: string[] = [];
6569
+ const diagnostics: string[] = [];
6570
+ const seen = new Set<string>();
6571
+ const out: string[] = [];
6572
+ for (const raw of toStringArray(values)) {
6573
+ if (seen.has(raw.toLowerCase())) continue;
6574
+ seen.add(raw.toLowerCase());
6575
+ if (shouldTreatMissingPathHintAsStale(repo, raw, taskText)) {
6576
+ stale.push(raw);
6577
+ diagnostics.push(`Path hint "${raw}" does not exist in this checkout; treat it as stale unless the task explicitly asks to create it.`);
6578
+ continue;
6579
+ }
6580
+ if (!pathParentExists(repo, raw) && !taskTextAllowsCreatingMissingPaths(taskText)) {
6581
+ diagnostics.push(`Path hint "${raw}" has a missing parent directory; verify the existing repo owner before editing.`);
6582
+ }
6583
+ out.push(raw);
6584
+ }
6585
+ return { values: out, stale, diagnostics };
6586
+ }
6587
+
6588
+ function validationStepMentionsAnyPath(step: string, paths: string[]): boolean {
6589
+ const lower = step.replace(/\\/g, "/").toLowerCase();
6590
+ return paths.some((path) => lower.includes(path.replace(/\\/g, "/").toLowerCase()));
6591
+ }
6592
+
6593
+ export function sanitizeTaskExecutePlanningPathHints(
6594
+ value: unknown,
6595
+ repo?: string,
6596
+ instruction = "",
6597
+ ): unknown {
6225
6598
  if (!value || typeof value !== "object" || Array.isArray(value)) return value;
6226
6599
  const planning = value as Record<string, unknown>;
6227
6600
  const out: Record<string, unknown> = { ...planning };
6601
+ const taskText = [
6602
+ instruction,
6603
+ planning.intent,
6604
+ ...(isStringArray(planning.targetPaths) ? planning.targetPaths : []),
6605
+ ...(isStringArray(planning.acceptanceCriteria) ? planning.acceptanceCriteria : []),
6606
+ ...(isStringArray(planning.validationSteps) ? planning.validationSteps : []),
6607
+ ]
6608
+ .map((entry) => String(entry ?? ""))
6609
+ .join("\n");
6610
+ const repoDiagnostics: string[] = isStringArray(planning.repoHintDiagnostics)
6611
+ ? toStringArray(planning.repoHintDiagnostics)
6612
+ : [];
6613
+ const staleHints: string[] = [];
6614
+
6615
+ if (repo && isStringArray(planning.targetPaths)) {
6616
+ const sanitized = sanitizeStalePathHints(repo, planning.targetPaths, taskText);
6617
+ out.targetPaths = sanitized.values;
6618
+ staleHints.push(...sanitized.stale);
6619
+ repoDiagnostics.push(...sanitized.diagnostics);
6620
+ }
6228
6621
 
6229
6622
  if (planning.scope && typeof planning.scope === "object" && !Array.isArray(planning.scope)) {
6230
6623
  const scope = planning.scope as Record<string, unknown>;
6231
6624
  const normalizedScope: Record<string, unknown> = { ...scope };
6232
6625
  if (isStringArray(scope.writeGlobs)) {
6233
- normalizedScope.writeGlobs = toStringArray(scope.writeGlobs);
6626
+ const sanitized = repo
6627
+ ? sanitizeStalePathHints(repo, scope.writeGlobs, taskText)
6628
+ : { values: toStringArray(scope.writeGlobs), stale: [], diagnostics: [] };
6629
+ normalizedScope.writeGlobs = sanitized.values;
6630
+ staleHints.push(...sanitized.stale);
6631
+ repoDiagnostics.push(...sanitized.diagnostics);
6234
6632
  }
6235
6633
  if (isStringArray(scope.forbiddenGlobs)) {
6236
6634
  normalizedScope.forbiddenGlobs = toStringArray(scope.forbiddenGlobs);
@@ -6246,11 +6644,30 @@ function sanitizeTaskExecutePlanningPathHints(value: unknown): unknown {
6246
6644
  const discovery = planning.discovery as Record<string, unknown>;
6247
6645
  const normalizedDiscovery: Record<string, unknown> = { ...discovery };
6248
6646
  if (isStringArray(discovery.likelyDirs)) {
6249
- normalizedDiscovery.likelyDirs = toStringArray(discovery.likelyDirs);
6647
+ const sanitized = repo
6648
+ ? sanitizeStalePathHints(repo, discovery.likelyDirs, taskText)
6649
+ : { values: toStringArray(discovery.likelyDirs), stale: [], diagnostics: [] };
6650
+ normalizedDiscovery.likelyDirs = sanitized.values;
6651
+ staleHints.push(...sanitized.stale);
6652
+ repoDiagnostics.push(...sanitized.diagnostics);
6250
6653
  }
6251
6654
  out.discovery = normalizedDiscovery;
6252
6655
  }
6253
6656
 
6657
+ if (staleHints.length > 0 && isStringArray(planning.validationSteps)) {
6658
+ out.validationSteps = toStringArray(planning.validationSteps).filter(
6659
+ (step) => !validationStepMentionsAnyPath(step, staleHints),
6660
+ );
6661
+ }
6662
+ if (staleHints.length > 0 && isStringArray(planning.requiredValidationSteps)) {
6663
+ out.requiredValidationSteps = toStringArray(planning.requiredValidationSteps).filter(
6664
+ (step) => !validationStepMentionsAnyPath(step, staleHints),
6665
+ );
6666
+ }
6667
+ if (repoDiagnostics.length > 0) {
6668
+ out.repoHintDiagnostics = Array.from(new Set(repoDiagnostics)).slice(0, 8);
6669
+ }
6670
+
6254
6671
  return out;
6255
6672
  }
6256
6673
 
@@ -6748,7 +7165,8 @@ export async function executeJob(
6748
7165
  exitCode: 2,
6749
7166
  };
6750
7167
  }
6751
- const sanitizedPlanning = sanitizeTaskExecutePlanningPathHints(params.planning);
7168
+ const instruction = String(params.instruction ?? "").trim();
7169
+ const sanitizedPlanning = sanitizeTaskExecutePlanningPathHints(params.planning, repo, instruction);
6752
7170
  const planning = sanitizedPlanning as TaskExecutePlanning;
6753
7171
  if (origin === "autonomy" && toStringArray(planning.scope.writeGlobs ?? []).length === 0) {
6754
7172
  onLog?.(
@@ -6756,8 +7174,16 @@ export async function executeJob(
6756
7174
  "[TaskExecute] Scope suggestion: planning.scope.writeGlobs is empty for autonomy-origin task.",
6757
7175
  );
6758
7176
  }
7177
+ if ((planning.repoHintDiagnostics ?? []).length > 0) {
7178
+ onLog?.(
7179
+ "stdout",
7180
+ `[TaskExecute] Repo hint preflight: ${(planning.repoHintDiagnostics ?? [])
7181
+ .slice(0, 3)
7182
+ .map((entry) => toSingleLine(entry, 180))
7183
+ .join(" | ")}`,
7184
+ );
7185
+ }
6759
7186
 
6760
- const instruction = String(params.instruction ?? "").trim();
6761
7187
  if (!instruction) {
6762
7188
  return {
6763
7189
  ok: false,
@@ -6979,6 +7405,12 @@ export async function executeJob(
6979
7405
  "stdout",
6980
7406
  `[JobRunner] Performance summary: attempt=${revisionAttempt}, executor=${executorElapsedMs}ms, quality=${qualityElapsedMs}ms, validation_commands=${quality.validationRuns.length}, validation_command_time=${validationCommandElapsedMs}ms, changed_files=${quality.changedPaths.length}`,
6981
7407
  );
7408
+ recordValidationRemedyMemory(repo, failureJobFamily, quality.validationRuns);
7409
+ const validationRemedyHints = knownValidationRemedyHintsForRuns(
7410
+ repo,
7411
+ failureJobFamily,
7412
+ quality.validationRuns,
7413
+ );
6982
7414
  let browserRepairPacket = buildBrowserValidationRepairPacket(
6983
7415
  quality.validationRuns,
6984
7416
  previousValidationFailureDigests,
@@ -7017,6 +7449,18 @@ export async function executeJob(
7017
7449
  if (!qualityGatePolicy.criticGateEnabled) {
7018
7450
  onLog?.("stdout", "[CriticGate] Disabled by workerpals.quality_critic_gate_enabled=false.");
7019
7451
  }
7452
+ const rolloutScore = workerAttemptRolloutScore({
7453
+ executorElapsedMs,
7454
+ qualityElapsedMs,
7455
+ changedPaths: quality.changedPaths,
7456
+ validationRuns: quality.validationRuns,
7457
+ qualityIssues: quality.issues,
7458
+ criticScore: critic?.score,
7459
+ });
7460
+ onLog?.(
7461
+ "stdout",
7462
+ `[JobRunner] Rollout score: score=${rolloutScore.score} reasons=${rolloutScore.reasons.join(",") || "none"}`,
7463
+ );
7020
7464
  const advisoryRelaxedQualityIssues = relaxAdvisoryQualityIssues(
7021
7465
  quality.issues,
7022
7466
  quality.validationRuns,
@@ -7299,6 +7743,7 @@ export async function executeJob(
7299
7743
  validationOutsideTaskScope ? null : quality.blocker,
7300
7744
  validationOutsideTaskScope ? null : browserRepairPacket,
7301
7745
  quality.changedPaths,
7746
+ validationRemedyHints,
7302
7747
  );
7303
7748
  onLog?.(
7304
7749
  "stderr",