@pushpalsdev/cli 1.1.20 → 1.1.21
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
CHANGED
|
@@ -29,6 +29,9 @@ interface GenericPythonExecutorConfig {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
const BACKEND_TIMEOUT_RESULT_GRACE_MS = 30_000;
|
|
32
|
+
const OPENAI_CODEX_MIN_VALIDATION_RESERVE_MS = 180_000;
|
|
33
|
+
const OPENAI_CODEX_MAX_VALIDATION_RESERVE_MS = 600_000;
|
|
34
|
+
const OPENAI_CODEX_MIN_PRIMARY_TURN_BUDGET_MS = 600_000;
|
|
32
35
|
|
|
33
36
|
function estimateTokensFromText(text: string): number {
|
|
34
37
|
return Math.max(0, Math.ceil(String(text ?? "").length / 3));
|
|
@@ -143,6 +146,27 @@ export function resolveGenericPythonExecutorTimeoutMs(params: {
|
|
|
143
146
|
return configuredTimeoutMs;
|
|
144
147
|
}
|
|
145
148
|
|
|
149
|
+
export function resolveOpenAICodexValidationReserveMs(
|
|
150
|
+
executionBudgetMs: number | null | undefined,
|
|
151
|
+
): number {
|
|
152
|
+
if (typeof executionBudgetMs !== "number" || !Number.isFinite(executionBudgetMs)) return 0;
|
|
153
|
+
const budgetMs = Math.max(10_000, Math.floor(executionBudgetMs));
|
|
154
|
+
const targetReserveMs = Math.floor(
|
|
155
|
+
Math.min(
|
|
156
|
+
budgetMs,
|
|
157
|
+
Math.max(
|
|
158
|
+
OPENAI_CODEX_MIN_VALIDATION_RESERVE_MS,
|
|
159
|
+
Math.min(OPENAI_CODEX_MAX_VALIDATION_RESERVE_MS, budgetMs * 0.35),
|
|
160
|
+
),
|
|
161
|
+
),
|
|
162
|
+
);
|
|
163
|
+
const maxReserveAfterPrimaryTurn = Math.max(
|
|
164
|
+
0,
|
|
165
|
+
budgetMs - OPENAI_CODEX_MIN_PRIMARY_TURN_BUDGET_MS,
|
|
166
|
+
);
|
|
167
|
+
return Math.max(0, Math.min(targetReserveMs, maxReserveAfterPrimaryTurn));
|
|
168
|
+
}
|
|
169
|
+
|
|
146
170
|
export function resolveGenericPythonExecutorChildTimeoutMs(params: {
|
|
147
171
|
backendName: string;
|
|
148
172
|
hostTimeoutMs: number;
|
|
@@ -154,8 +178,11 @@ export function resolveGenericPythonExecutorChildTimeoutMs(params: {
|
|
|
154
178
|
typeof params.executionBudgetMs === "number" && Number.isFinite(params.executionBudgetMs)
|
|
155
179
|
? Math.max(10_000, Math.floor(params.executionBudgetMs))
|
|
156
180
|
: null;
|
|
181
|
+
const validationReserveMs = resolveOpenAICodexValidationReserveMs(executionBudgetMs);
|
|
157
182
|
const childBudgetMs =
|
|
158
|
-
executionBudgetMs == null
|
|
183
|
+
executionBudgetMs == null
|
|
184
|
+
? hostTimeoutMs
|
|
185
|
+
: Math.min(hostTimeoutMs, Math.max(1_000, executionBudgetMs - validationReserveMs));
|
|
159
186
|
const graceMs = Math.min(
|
|
160
187
|
BACKEND_TIMEOUT_RESULT_GRACE_MS,
|
|
161
188
|
Math.max(2_000, Math.floor(childBudgetMs / 10)),
|
|
@@ -305,15 +332,30 @@ export function createGenericPythonExecutor(
|
|
|
305
332
|
"utf-8",
|
|
306
333
|
).toString("base64");
|
|
307
334
|
const args = [pythonBin, scriptPath, payloadBase64];
|
|
308
|
-
const
|
|
335
|
+
const childTimeoutMs = resolveGenericPythonExecutorChildTimeoutMs({
|
|
309
336
|
backendName,
|
|
310
337
|
hostTimeoutMs: timeoutMs,
|
|
311
338
|
executionBudgetMs,
|
|
312
339
|
});
|
|
340
|
+
const childTimeoutEnv =
|
|
341
|
+
childTimeoutMs == null
|
|
342
|
+
? {}
|
|
343
|
+
: {
|
|
344
|
+
WORKERPALS_OPENAI_CODEX_TIMEOUT_MS: String(childTimeoutMs),
|
|
345
|
+
WORKERPALS_OPENAI_CODEX_TIMEOUT_S: String(
|
|
346
|
+
Math.max(1, Math.floor(childTimeoutMs / 1000)),
|
|
347
|
+
),
|
|
348
|
+
};
|
|
349
|
+
const childTimeoutDetail =
|
|
350
|
+
childTimeoutMs != null
|
|
351
|
+
? `; codex_child_timeout=${childTimeoutMs}ms; reserved_validation_budget=${resolveOpenAICodexValidationReserveMs(
|
|
352
|
+
executionBudgetMs,
|
|
353
|
+
)}ms`
|
|
354
|
+
: "";
|
|
313
355
|
|
|
314
356
|
onLog?.(
|
|
315
357
|
"stdout",
|
|
316
|
-
`[${backendLabel}Executor] Spawning ${backendName} executor (timeout=${timeoutMs}ms; ${timeoutDetail})`,
|
|
358
|
+
`[${backendLabel}Executor] Spawning ${backendName} executor (timeout=${timeoutMs}ms; ${timeoutDetail}${childTimeoutDetail})`,
|
|
317
359
|
);
|
|
318
360
|
|
|
319
361
|
try {
|
|
@@ -190,6 +190,7 @@ export interface QualityGatePolicy {
|
|
|
190
190
|
}
|
|
191
191
|
|
|
192
192
|
const BROWSER_VALIDATION_MAX_AUTO_REVISIONS = 3;
|
|
193
|
+
const CRITIC_COMPACT_RETRY_MIN_REDUCTION_RATIO = 0.25;
|
|
193
194
|
|
|
194
195
|
export function qualityRevisionLoopUpperBound(policy: {
|
|
195
196
|
maxAutoRevisions: number;
|
|
@@ -232,6 +233,37 @@ export function qualityRevisionBudgetDecision(opts: {
|
|
|
232
233
|
};
|
|
233
234
|
}
|
|
234
235
|
|
|
236
|
+
export function shouldRetryCriticTimeoutWithCompact(opts: {
|
|
237
|
+
timeoutBehavior: string;
|
|
238
|
+
qualityOk: boolean;
|
|
239
|
+
validationPassed: boolean;
|
|
240
|
+
initialPromptChars: number;
|
|
241
|
+
compactPromptChars: number;
|
|
242
|
+
}): boolean {
|
|
243
|
+
if (opts.timeoutBehavior !== "retry_once") return false;
|
|
244
|
+
if (!opts.qualityOk || !opts.validationPassed) return true;
|
|
245
|
+
const initialPromptChars = Math.max(1, Math.floor(opts.initialPromptChars));
|
|
246
|
+
const compactPromptChars = Math.max(0, Math.floor(opts.compactPromptChars));
|
|
247
|
+
const reductionRatio = 1 - compactPromptChars / initialPromptChars;
|
|
248
|
+
return reductionRatio >= CRITIC_COMPACT_RETRY_MIN_REDUCTION_RATIO;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function shouldSkipCriticAfterExecutorTimeout(opts: {
|
|
252
|
+
executor: string;
|
|
253
|
+
policyMode: string;
|
|
254
|
+
executorText: string;
|
|
255
|
+
qualityOk: boolean;
|
|
256
|
+
validationPassed: boolean;
|
|
257
|
+
qualityIssues: string[];
|
|
258
|
+
changedPaths: string[];
|
|
259
|
+
}): boolean {
|
|
260
|
+
if (opts.executor !== "openai_codex") return false;
|
|
261
|
+
if (opts.policyMode !== "default") return false;
|
|
262
|
+
if (!opts.qualityOk || !opts.validationPassed) return false;
|
|
263
|
+
if (opts.qualityIssues.length > 0 || opts.changedPaths.length === 0) return false;
|
|
264
|
+
return /\b(openai_codex|codex(?: exec)?)\b[^\r\n]*\btimed out\b/i.test(opts.executorText);
|
|
265
|
+
}
|
|
266
|
+
|
|
235
267
|
export function workerAttemptRolloutScore(params: {
|
|
236
268
|
executorElapsedMs: number;
|
|
237
269
|
qualityElapsedMs: number;
|
|
@@ -879,6 +911,10 @@ function parseJsonObjectLoose(text: string): Record<string, unknown> | null {
|
|
|
879
911
|
}
|
|
880
912
|
|
|
881
913
|
const COMMIT_MSG_MAX_DIFF_CHARS = 120_000;
|
|
914
|
+
const COMMIT_MSG_LLM_MAX_CHANGED_PATHS = 20;
|
|
915
|
+
const COMMIT_MSG_GENERATOR_DEFAULT_TIMEOUT_MS = 15_000;
|
|
916
|
+
const COMMIT_MSG_GENERATOR_MIN_TIMEOUT_MS = 3_000;
|
|
917
|
+
const COMMIT_MSG_GENERATOR_MAX_TIMEOUT_MS = 30_000;
|
|
882
918
|
|
|
883
919
|
const SHELL_CONTROL_TOKENS = new Set(["&&", "||", ";", "|"]);
|
|
884
920
|
|
|
@@ -2096,6 +2132,18 @@ function classifyBrowserValidationFailureKindFromText(text: string): BrowserVali
|
|
|
2096
2132
|
return "unknown";
|
|
2097
2133
|
}
|
|
2098
2134
|
|
|
2135
|
+
export function shouldRetryBrowserValidationRunOnce(run: ValidationExecutionResult): boolean {
|
|
2136
|
+
if (run.ok || !isLongRunningBrowserValidationCommand(run.command)) return false;
|
|
2137
|
+
const combined = stripAnsiControlSequences([run.stderr, run.stdout].filter(Boolean).join("\n"));
|
|
2138
|
+
const digest = extractValidationFailureDigest(run);
|
|
2139
|
+
const failureKind = classifyBrowserValidationFailureKindFromText(`${digest}\n${combined}`);
|
|
2140
|
+
if (failureKind === "runtime" || failureKind === "network") return true;
|
|
2141
|
+
if (failureKind === "startup") return true;
|
|
2142
|
+
return /\b(Route\/startup smoke failure|startup smoke failure|home route startup)\b/i.test(
|
|
2143
|
+
`${digest}\n${combined}`,
|
|
2144
|
+
);
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2099
2147
|
function extractBrowserValidationStage(text: string): string | null {
|
|
2100
2148
|
const patterns = [
|
|
2101
2149
|
/\bBrowser validation failed during\s+([^:.\r\n|]+?)\s+stage\b/i,
|
|
@@ -3662,12 +3710,34 @@ async function runDeterministicQualityGate(
|
|
|
3662
3710
|
continue;
|
|
3663
3711
|
}
|
|
3664
3712
|
onLog?.("stdout", `[ValidationGate] Running "${command}"`);
|
|
3665
|
-
|
|
3713
|
+
let run = await runValidationCommand(
|
|
3666
3714
|
repo,
|
|
3667
3715
|
command,
|
|
3668
3716
|
resolveValidationCommandTimeoutMs(command, qualityValidationStepTimeoutMs),
|
|
3669
3717
|
outputPolicy,
|
|
3670
3718
|
);
|
|
3719
|
+
const firstDigest = run.ok ? "" : extractValidationFailureDigest(run);
|
|
3720
|
+
if (shouldRetryBrowserValidationRunOnce(run)) {
|
|
3721
|
+
onLog?.(
|
|
3722
|
+
"stderr",
|
|
3723
|
+
`[ValidationGate] Retrying browser validation once after retryable startup/runtime failure: ${command}${firstDigest ? ` - ${firstDigest}` : ""}`,
|
|
3724
|
+
);
|
|
3725
|
+
const retryRun = await runValidationCommand(
|
|
3726
|
+
repo,
|
|
3727
|
+
command,
|
|
3728
|
+
resolveValidationCommandTimeoutMs(command, qualityValidationStepTimeoutMs),
|
|
3729
|
+
outputPolicy,
|
|
3730
|
+
);
|
|
3731
|
+
if (!retryRun.ok && firstDigest) {
|
|
3732
|
+
retryRun.stderr = [
|
|
3733
|
+
`Previous browser validation attempt failed before retry: ${firstDigest}`,
|
|
3734
|
+
retryRun.stderr,
|
|
3735
|
+
]
|
|
3736
|
+
.filter(Boolean)
|
|
3737
|
+
.join("\n");
|
|
3738
|
+
}
|
|
3739
|
+
run = retryRun;
|
|
3740
|
+
}
|
|
3671
3741
|
validationRuns.push(run);
|
|
3672
3742
|
const digest = run.ok ? "" : extractValidationFailureDigest(run);
|
|
3673
3743
|
const runSummary = `[ValidationGate] ${run.ok ? "Passed" : "Failed"} (${run.elapsedMs}ms, exit ${run.exitCode}): ${command}${digest ? ` - ${digest}` : ""}`;
|
|
@@ -4768,17 +4838,19 @@ export async function createJobCommit(
|
|
|
4768
4838
|
...toNonEmptyStringArray(jobPlanning?.requiredValidationSteps),
|
|
4769
4839
|
...loadRequiredValidationStepsFromVision(repo),
|
|
4770
4840
|
];
|
|
4771
|
-
const llmCommitMsg =
|
|
4772
|
-
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
|
|
4841
|
+
const llmCommitMsg = shouldUseLlmCommitMessageForStagedDiff({ changedPaths, diff })
|
|
4842
|
+
? await generateCommitMessageFromDiff(
|
|
4843
|
+
diff,
|
|
4844
|
+
{
|
|
4845
|
+
instruction: String(job.params?.instruction ?? ""),
|
|
4846
|
+
type: normalizeCommitType(job.kind, job.params),
|
|
4847
|
+
area: inferCommitArea(job.kind, job.params, changedPaths),
|
|
4848
|
+
validationSteps: jobValidationSteps,
|
|
4849
|
+
},
|
|
4850
|
+
repo,
|
|
4851
|
+
runtimeConfig,
|
|
4852
|
+
).catch(() => null)
|
|
4853
|
+
: null;
|
|
4782
4854
|
if (!llmCommitMsg) {
|
|
4783
4855
|
console.warn(
|
|
4784
4856
|
`[WorkerPals] Commit message generator unavailable for job ${job.id}; using deterministic fallback.`,
|
|
@@ -5745,17 +5817,19 @@ async function createMergeConflictJobCommit(
|
|
|
5745
5817
|
...toNonEmptyStringArray(jobPlanning?.requiredValidationSteps),
|
|
5746
5818
|
...loadRequiredValidationStepsFromVision(repo),
|
|
5747
5819
|
];
|
|
5748
|
-
const llmCommitMsg =
|
|
5749
|
-
|
|
5750
|
-
|
|
5751
|
-
|
|
5752
|
-
|
|
5753
|
-
|
|
5754
|
-
|
|
5755
|
-
|
|
5756
|
-
|
|
5757
|
-
|
|
5758
|
-
|
|
5820
|
+
const llmCommitMsg = shouldUseLlmCommitMessageForStagedDiff({ changedPaths, diff })
|
|
5821
|
+
? await generateCommitMessageFromDiff(
|
|
5822
|
+
diff,
|
|
5823
|
+
{
|
|
5824
|
+
instruction: String(job.params?.instruction ?? ""),
|
|
5825
|
+
type: normalizeCommitType(job.kind, job.params),
|
|
5826
|
+
area: inferCommitArea(job.kind, job.params, changedPaths),
|
|
5827
|
+
validationSteps: jobValidationSteps,
|
|
5828
|
+
},
|
|
5829
|
+
repo,
|
|
5830
|
+
runtimeConfig,
|
|
5831
|
+
).catch(() => null)
|
|
5832
|
+
: null;
|
|
5759
5833
|
if (!llmCommitMsg) {
|
|
5760
5834
|
console.warn(
|
|
5761
5835
|
`[WorkerPals] Commit message generator unavailable for merge-conflict job ${job.id}; using deterministic fallback.`,
|
|
@@ -6206,6 +6280,38 @@ async function generateCommitMessageFromDiff(
|
|
|
6206
6280
|
return generateCommitMessageFromDiffViaHttp(prompt, opts, runtimeConfig);
|
|
6207
6281
|
}
|
|
6208
6282
|
|
|
6283
|
+
export function resolveCommitMessageGeneratorTimeoutMs(
|
|
6284
|
+
runtimeConfig: WorkerpalsRuntimeConfig = DEFAULT_CONFIG,
|
|
6285
|
+
): number {
|
|
6286
|
+
const workerpalsConfig = runtimeConfig.workerpals as Record<string, unknown>;
|
|
6287
|
+
const llmConfig =
|
|
6288
|
+
workerpalsConfig.llm && typeof workerpalsConfig.llm === "object"
|
|
6289
|
+
? (workerpalsConfig.llm as Record<string, unknown>)
|
|
6290
|
+
: {};
|
|
6291
|
+
const configuredRaw =
|
|
6292
|
+
workerpalsConfig.commitMessageTimeoutMs ??
|
|
6293
|
+
workerpalsConfig.commit_message_timeout_ms ??
|
|
6294
|
+
llmConfig.commitMessageTimeoutMs ??
|
|
6295
|
+
llmConfig.commit_message_timeout_ms ??
|
|
6296
|
+
Bun.env.WORKERPALS_COMMIT_MESSAGE_TIMEOUT_MS;
|
|
6297
|
+
const configured = Number(configuredRaw);
|
|
6298
|
+
const value = Number.isFinite(configured)
|
|
6299
|
+
? configured
|
|
6300
|
+
: COMMIT_MSG_GENERATOR_DEFAULT_TIMEOUT_MS;
|
|
6301
|
+
return Math.max(
|
|
6302
|
+
COMMIT_MSG_GENERATOR_MIN_TIMEOUT_MS,
|
|
6303
|
+
Math.min(COMMIT_MSG_GENERATOR_MAX_TIMEOUT_MS, Math.floor(value)),
|
|
6304
|
+
);
|
|
6305
|
+
}
|
|
6306
|
+
|
|
6307
|
+
export function shouldUseLlmCommitMessageForStagedDiff(params: {
|
|
6308
|
+
changedPaths: string[];
|
|
6309
|
+
diff: string;
|
|
6310
|
+
}): boolean {
|
|
6311
|
+
if (!String(params.diff ?? "").trim()) return false;
|
|
6312
|
+
return params.changedPaths.length <= COMMIT_MSG_LLM_MAX_CHANGED_PATHS;
|
|
6313
|
+
}
|
|
6314
|
+
|
|
6209
6315
|
type CommitMessagePrompt = {
|
|
6210
6316
|
systemPrompt: string;
|
|
6211
6317
|
userMessage: string;
|
|
@@ -6244,11 +6350,7 @@ async function generateCommitMessageFromDiffViaCodex(
|
|
|
6244
6350
|
if (!model) return null;
|
|
6245
6351
|
const codexPrefix = await resolveCodexCommandPrefix(repo, runtimeConfig.workerpals.llm.codexBin);
|
|
6246
6352
|
if (!codexPrefix) return null;
|
|
6247
|
-
const timeoutMs = (
|
|
6248
|
-
const value = Number(runtimeConfig.workerpals.llm.codexTimeoutMs);
|
|
6249
|
-
if (!Number.isFinite(value)) return 120_000;
|
|
6250
|
-
return Math.max(10_000, Math.min(600_000, Math.floor(value)));
|
|
6251
|
-
})();
|
|
6353
|
+
const timeoutMs = resolveCommitMessageGeneratorTimeoutMs(runtimeConfig);
|
|
6252
6354
|
const reasoningEffort = normalizeCodexReasoningEffort(
|
|
6253
6355
|
runtimeConfig.workerpals.llm.reasoningEffort,
|
|
6254
6356
|
model,
|
|
@@ -6338,7 +6440,7 @@ async function generateCommitMessageFromDiffViaHttp(
|
|
|
6338
6440
|
if (apiKey) headers.Authorization = `Bearer ${apiKey}`;
|
|
6339
6441
|
|
|
6340
6442
|
const controller = new AbortController();
|
|
6341
|
-
const timer = setTimeout(() => controller.abort(),
|
|
6443
|
+
const timer = setTimeout(() => controller.abort(), resolveCommitMessageGeneratorTimeoutMs(runtimeConfig));
|
|
6342
6444
|
try {
|
|
6343
6445
|
const response = await fetch(endpoint, {
|
|
6344
6446
|
method: "POST",
|
|
@@ -6952,6 +7054,7 @@ async function runCodexCriticReview(
|
|
|
6952
7054
|
validationChars: validationSummary.length,
|
|
6953
7055
|
};
|
|
6954
7056
|
};
|
|
7057
|
+
type CodexCriticPayload = Awaited<ReturnType<typeof buildCriticInstruction>>;
|
|
6955
7058
|
|
|
6956
7059
|
const tmpOutputPath = `/tmp/pushpals-critic-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.txt`;
|
|
6957
7060
|
const buildCmd = () => {
|
|
@@ -6980,13 +7083,17 @@ async function runCodexCriticReview(
|
|
|
6980
7083
|
const runAttempt = async (
|
|
6981
7084
|
attempt: number,
|
|
6982
7085
|
compact: boolean,
|
|
6983
|
-
|
|
7086
|
+
payloadOverride?: CodexCriticPayload,
|
|
7087
|
+
): Promise<
|
|
7088
|
+
| { status: "timeout"; payload: CodexCriticPayload }
|
|
7089
|
+
| { status: "done"; review: CriticReview | null; payload: CodexCriticPayload }
|
|
7090
|
+
> => {
|
|
6984
7091
|
try {
|
|
6985
7092
|
unlinkSync(tmpOutputPath);
|
|
6986
7093
|
} catch {
|
|
6987
7094
|
/* ignore stale/missing critic output */
|
|
6988
7095
|
}
|
|
6989
|
-
const payload = await buildCriticInstruction(compact);
|
|
7096
|
+
const payload = payloadOverride ?? (await buildCriticInstruction(compact));
|
|
6990
7097
|
const startedAt = Date.now();
|
|
6991
7098
|
onLog?.(
|
|
6992
7099
|
"stdout",
|
|
@@ -7014,7 +7121,7 @@ async function runCodexCriticReview(
|
|
|
7014
7121
|
clearTimeout(timer);
|
|
7015
7122
|
|
|
7016
7123
|
if (timedOut) {
|
|
7017
|
-
return { status: "timeout" };
|
|
7124
|
+
return { status: "timeout", payload };
|
|
7018
7125
|
}
|
|
7019
7126
|
if (exitCode !== 0) {
|
|
7020
7127
|
const stderrText = await new Response(proc.stderr).text();
|
|
@@ -7022,7 +7129,7 @@ async function runCodexCriticReview(
|
|
|
7022
7129
|
"stderr",
|
|
7023
7130
|
`[CriticGate] Codex exited ${exitCode}: ${toSingleLine(stderrText, 220)}`,
|
|
7024
7131
|
);
|
|
7025
|
-
return { status: "done", review: null };
|
|
7132
|
+
return { status: "done", review: null, payload };
|
|
7026
7133
|
}
|
|
7027
7134
|
|
|
7028
7135
|
let lastMessage = "";
|
|
@@ -7039,7 +7146,7 @@ async function runCodexCriticReview(
|
|
|
7039
7146
|
|
|
7040
7147
|
if (!lastMessage) {
|
|
7041
7148
|
onLog?.("stderr", "[CriticGate] Codex: no output message captured; skipping.");
|
|
7042
|
-
return { status: "done", review: null };
|
|
7149
|
+
return { status: "done", review: null, payload };
|
|
7043
7150
|
}
|
|
7044
7151
|
|
|
7045
7152
|
const reviewObj = parseJsonObjectLoose(lastMessage);
|
|
@@ -7048,7 +7155,7 @@ async function runCodexCriticReview(
|
|
|
7048
7155
|
"stderr",
|
|
7049
7156
|
`[CriticGate] Codex returned non-JSON: ${toSingleLine(lastMessage, 220)}`,
|
|
7050
7157
|
);
|
|
7051
|
-
return { status: "done", review: null };
|
|
7158
|
+
return { status: "done", review: null, payload };
|
|
7052
7159
|
}
|
|
7053
7160
|
|
|
7054
7161
|
const scoreRaw = Number(reviewObj.score);
|
|
@@ -7068,6 +7175,7 @@ async function runCodexCriticReview(
|
|
|
7068
7175
|
);
|
|
7069
7176
|
return {
|
|
7070
7177
|
status: "done",
|
|
7178
|
+
payload,
|
|
7071
7179
|
review: {
|
|
7072
7180
|
score,
|
|
7073
7181
|
findings,
|
|
@@ -7081,11 +7189,36 @@ async function runCodexCriticReview(
|
|
|
7081
7189
|
try {
|
|
7082
7190
|
let attempt = await runAttempt(1, false);
|
|
7083
7191
|
if (attempt.status === "timeout" && timeoutBehavior === "retry_once") {
|
|
7084
|
-
|
|
7085
|
-
|
|
7086
|
-
|
|
7087
|
-
|
|
7088
|
-
|
|
7192
|
+
const compactPayload = await buildCriticInstruction(true);
|
|
7193
|
+
const validationPassed =
|
|
7194
|
+
quality.validationRuns.length > 0 && quality.validationRuns.every((run) => run.ok);
|
|
7195
|
+
if (
|
|
7196
|
+
shouldRetryCriticTimeoutWithCompact({
|
|
7197
|
+
timeoutBehavior,
|
|
7198
|
+
qualityOk: quality.ok,
|
|
7199
|
+
validationPassed,
|
|
7200
|
+
initialPromptChars: attempt.payload.promptChars,
|
|
7201
|
+
compactPromptChars: compactPayload.promptChars,
|
|
7202
|
+
})
|
|
7203
|
+
) {
|
|
7204
|
+
onLog?.(
|
|
7205
|
+
"stderr",
|
|
7206
|
+
`[CriticGate] Codex timed out after ${qualityCriticTimeoutMs}ms; retrying once with compact critic input.`,
|
|
7207
|
+
);
|
|
7208
|
+
attempt = await runAttempt(2, true, compactPayload);
|
|
7209
|
+
} else {
|
|
7210
|
+
const reductionPct = Math.max(
|
|
7211
|
+
0,
|
|
7212
|
+
Math.round(
|
|
7213
|
+
(1 - compactPayload.promptChars / Math.max(1, attempt.payload.promptChars)) * 100,
|
|
7214
|
+
),
|
|
7215
|
+
);
|
|
7216
|
+
onLog?.(
|
|
7217
|
+
"stderr",
|
|
7218
|
+
`[CriticGate] Codex timed out after ${qualityCriticTimeoutMs}ms; compact critic input only reduced prompt by ${reductionPct}% after clean validation; skipping retry.`,
|
|
7219
|
+
);
|
|
7220
|
+
return null;
|
|
7221
|
+
}
|
|
7089
7222
|
}
|
|
7090
7223
|
if (attempt.status === "timeout") {
|
|
7091
7224
|
if (timeoutBehavior === "block") {
|
|
@@ -7316,6 +7449,42 @@ export async function executeJob(
|
|
|
7316
7449
|
);
|
|
7317
7450
|
continue;
|
|
7318
7451
|
}
|
|
7452
|
+
if (sequencer === "rebase" && !resume.resumed) {
|
|
7453
|
+
mergeConflictPass += 1;
|
|
7454
|
+
const budget = qualityRevisionBudgetDecision({
|
|
7455
|
+
jobElapsedMs: Date.now() - attemptStartedAt,
|
|
7456
|
+
executionBudgetMs,
|
|
7457
|
+
});
|
|
7458
|
+
if (mergeConflictPass < MAX_MERGE_CONFLICT_RESOLUTION_PASSES && budget.shouldStart) {
|
|
7459
|
+
const retryDetail =
|
|
7460
|
+
resume.detail ??
|
|
7461
|
+
"the previous resolver pass returned before the prepared rebase completed";
|
|
7462
|
+
const previousHint = String(attemptParams.qualityRevisionHint ?? "").trim();
|
|
7463
|
+
attemptParams.qualityRevisionHint = [
|
|
7464
|
+
previousHint,
|
|
7465
|
+
[
|
|
7466
|
+
`Merge-conflict resolver pass ${mergeConflictPass} left the rebase unfinished: ${retryDetail}.`,
|
|
7467
|
+
"Focus only on completing the active rebase. Inspect unresolved files with `git diff --name-only --diff-filter=U`, remove remaining conflict markers, stage resolved files, and run `git -c core.editor=true rebase --continue` until no rebase remains.",
|
|
7468
|
+
"Do not broaden the patch or run full validation before the rebase is complete.",
|
|
7469
|
+
].join("\n"),
|
|
7470
|
+
]
|
|
7471
|
+
.filter(Boolean)
|
|
7472
|
+
.join("\n\n");
|
|
7473
|
+
onLog?.(
|
|
7474
|
+
"stdout",
|
|
7475
|
+
`[MergeConflict] ${retryDetail}; rerunning resolver pass ${
|
|
7476
|
+
mergeConflictPass + 1
|
|
7477
|
+
} with focused rebase-completion guidance.`,
|
|
7478
|
+
);
|
|
7479
|
+
continue;
|
|
7480
|
+
}
|
|
7481
|
+
if (!budget.shouldStart) {
|
|
7482
|
+
onLog?.(
|
|
7483
|
+
"stderr",
|
|
7484
|
+
`[MergeConflict] Not rerunning unfinished rebase resolver: remaining execution budget is ${budget.remainingBudgetMs}ms (< ${budget.minimumRevisionBudgetMs}ms).`,
|
|
7485
|
+
);
|
|
7486
|
+
}
|
|
7487
|
+
}
|
|
7319
7488
|
const detail =
|
|
7320
7489
|
`Merge-conflict job returned with git ${sequencer} still in progress. ` +
|
|
7321
7490
|
`Finish the ${sequencer} before returning control to WorkerPals.`;
|
|
@@ -7440,14 +7609,30 @@ export async function executeJob(
|
|
|
7440
7609
|
blocker: null,
|
|
7441
7610
|
}
|
|
7442
7611
|
: quality;
|
|
7612
|
+
const validationPassed =
|
|
7613
|
+
quality.validationRuns.length > 0 && quality.validationRuns.every((run) => run.ok);
|
|
7614
|
+
const skipCriticAfterExecutorTimeout = shouldSkipCriticAfterExecutorTimeout({
|
|
7615
|
+
executor,
|
|
7616
|
+
policyMode: qualityGatePolicy.mode,
|
|
7617
|
+
executorText,
|
|
7618
|
+
qualityOk: quality.ok,
|
|
7619
|
+
validationPassed,
|
|
7620
|
+
qualityIssues: qualityForCritic.issues,
|
|
7621
|
+
changedPaths: quality.changedPaths,
|
|
7622
|
+
});
|
|
7443
7623
|
const critic =
|
|
7444
|
-
quality.skipped || !qualityGatePolicy.criticGateEnabled
|
|
7624
|
+
quality.skipped || !qualityGatePolicy.criticGateEnabled || skipCriticAfterExecutorTimeout
|
|
7445
7625
|
? null
|
|
7446
7626
|
: executor === "openai_codex"
|
|
7447
7627
|
? await runCodexCriticReview(repo, attemptParams, qualityForCritic, runtimeConfig, onLog)
|
|
7448
7628
|
: await runTaskCriticReview(repo, attemptParams, qualityForCritic, runtimeConfig, onLog);
|
|
7449
7629
|
if (!qualityGatePolicy.criticGateEnabled) {
|
|
7450
7630
|
onLog?.("stdout", "[CriticGate] Disabled by workerpals.quality_critic_gate_enabled=false.");
|
|
7631
|
+
} else if (skipCriticAfterExecutorTimeout) {
|
|
7632
|
+
onLog?.(
|
|
7633
|
+
"stdout",
|
|
7634
|
+
"[CriticGate] Skipping Codex critic after primary Codex executor timeout because deterministic quality and validation are clean.",
|
|
7635
|
+
);
|
|
7451
7636
|
}
|
|
7452
7637
|
const rolloutScore = workerAttemptRolloutScore({
|
|
7453
7638
|
executorElapsedMs,
|
|
@@ -197,6 +197,15 @@ function buildPlannerGuidance(
|
|
|
197
197
|
lines.push(
|
|
198
198
|
"- Use direct commands only while resolving this rebase. Prefer `git diff -- <path>`, `git add <path>`, and `git -c core.editor=true rebase --continue` instead of `/bin/bash -lc`, `sh -lc`, `awk`, or chained shell snippets.",
|
|
199
199
|
);
|
|
200
|
+
lines.push(
|
|
201
|
+
"- Primary success condition: finish the git rebase and leave no active rebase/merge/cherry-pick state. Do not spend budget polishing, broadening, or refactoring tests beyond what is required to remove conflict markers and keep both sides' intended behavior.",
|
|
202
|
+
);
|
|
203
|
+
lines.push(
|
|
204
|
+
"- Rebase convergence rule: after resolving each conflicted file, run `git diff --name-only --diff-filter=U`. If no unresolved paths remain, stage the resolved files and continue the rebase immediately before doing broader validation.",
|
|
205
|
+
);
|
|
206
|
+
lines.push(
|
|
207
|
+
"- Budget rule: if conflict resolution is running long, choose the smallest side-preserving resolution, stage it, and continue the rebase. A clean rebased branch with focused follow-up validation is better than a richer partial patch left mid-rebase.",
|
|
208
|
+
);
|
|
200
209
|
lines.push(
|
|
201
210
|
"- After editing, run `git add <files>` and `git -c core.editor=true rebase --continue` until the rebase completes.",
|
|
202
211
|
);
|