@pushpalsdev/cli 1.1.18 → 1.1.19
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/package.json +1 -1
- package/runtime/sandbox/.pushpals-remotebuddy-fallback.js +159 -5
- package/runtime/sandbox/apps/workerpals/src/backends/openai_codex/openai_codex_executor.py +250 -6
- package/runtime/sandbox/apps/workerpals/src/backends/openai_codex/test_openai_codex_runtime_config.py +223 -0
- package/runtime/sandbox/apps/workerpals/src/backends/shared/executor_base.py +9 -0
- package/runtime/sandbox/apps/workerpals/src/execute_job.ts +450 -5
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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",
|