@pushpalsdev/cli 1.1.6 → 1.1.7
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/apps/workerpals/src/backends/openai_codex_backend.ts +1 -0
- package/runtime/sandbox/apps/workerpals/src/common/generic_python_executor.ts +22 -4
- package/runtime/sandbox/apps/workerpals/src/docker_executor.ts +28 -2
- package/runtime/sandbox/apps/workerpals/src/execute_job.ts +37 -3
- package/runtime/sandbox/apps/workerpals/src/merge_conflict_job.ts +21 -1
- package/runtime/sandbox/apps/workerpals/src/workerpals_main.ts +5 -1
package/package.json
CHANGED
|
@@ -63,5 +63,6 @@ export const OPENAI_CODEX_BACKEND: DockerBackendSpec = {
|
|
|
63
63
|
scriptPath: resolve(import.meta.dir, "openai_codex", "openai_codex_executor.py"),
|
|
64
64
|
pythonConfigKey: "openaiCodexPython",
|
|
65
65
|
timeoutConfigKey: "openaiCodexTimeoutMs",
|
|
66
|
+
capTimeoutToExecutionBudget: false,
|
|
66
67
|
}),
|
|
67
68
|
};
|
|
@@ -25,6 +25,7 @@ interface GenericPythonExecutorConfig {
|
|
|
25
25
|
scriptPath: string;
|
|
26
26
|
pythonConfigKey: string;
|
|
27
27
|
timeoutConfigKey: string;
|
|
28
|
+
capTimeoutToExecutionBudget?: boolean;
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
function estimateTokensFromText(text: string): number {
|
|
@@ -119,6 +120,22 @@ function resolveRuntimeSettings(
|
|
|
119
120
|
return { pythonBin, timeoutMs };
|
|
120
121
|
}
|
|
121
122
|
|
|
123
|
+
export function resolveGenericPythonExecutorTimeoutMs(params: {
|
|
124
|
+
configuredTimeoutMs: number;
|
|
125
|
+
executionBudgetMs?: number | null;
|
|
126
|
+
capTimeoutToExecutionBudget?: boolean;
|
|
127
|
+
}): number {
|
|
128
|
+
const configuredTimeoutMs = Math.max(10_000, Math.floor(params.configuredTimeoutMs));
|
|
129
|
+
const executionBudgetMs =
|
|
130
|
+
typeof params.executionBudgetMs === "number" && Number.isFinite(params.executionBudgetMs)
|
|
131
|
+
? Math.max(10_000, Math.floor(params.executionBudgetMs))
|
|
132
|
+
: null;
|
|
133
|
+
if (executionBudgetMs != null && params.capTimeoutToExecutionBudget !== false) {
|
|
134
|
+
return Math.min(configuredTimeoutMs, executionBudgetMs);
|
|
135
|
+
}
|
|
136
|
+
return configuredTimeoutMs;
|
|
137
|
+
}
|
|
138
|
+
|
|
122
139
|
export function createGenericPythonExecutor(
|
|
123
140
|
config: GenericPythonExecutorConfig,
|
|
124
141
|
): BackendTaskExecutor {
|
|
@@ -150,10 +167,11 @@ export function createGenericPythonExecutor(
|
|
|
150
167
|
typeof budgets?.executionBudgetMs === "number" && Number.isFinite(budgets.executionBudgetMs)
|
|
151
168
|
? Math.max(10_000, Math.floor(budgets.executionBudgetMs))
|
|
152
169
|
: null;
|
|
153
|
-
const timeoutMs =
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
170
|
+
const timeoutMs = resolveGenericPythonExecutorTimeoutMs({
|
|
171
|
+
configuredTimeoutMs,
|
|
172
|
+
executionBudgetMs,
|
|
173
|
+
capTimeoutToExecutionBudget: config.capTimeoutToExecutionBudget,
|
|
174
|
+
});
|
|
157
175
|
const payloadBase64 = Buffer.from(
|
|
158
176
|
JSON.stringify({
|
|
159
177
|
kind,
|
|
@@ -107,7 +107,12 @@ function dockerBuildFileArg(root: string, dockerfilePath: string): string {
|
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
function isMissingDockerImageDetail(detail: string): boolean {
|
|
110
|
-
|
|
110
|
+
const text = String(detail ?? "");
|
|
111
|
+
return (
|
|
112
|
+
/\b(no such object|no such image|not found)\b/i.test(text) ||
|
|
113
|
+
/\bunable to find image\b.*\blocally\b/i.test(text) ||
|
|
114
|
+
/\bpull access denied\b.*\brepository does not exist\b/i.test(text)
|
|
115
|
+
);
|
|
111
116
|
}
|
|
112
117
|
|
|
113
118
|
type ParsedWorktreeRecord = {
|
|
@@ -1600,12 +1605,28 @@ export class DockerExecutor {
|
|
|
1600
1605
|
onLog?: (stream: "stdout" | "stderr", line: string) => void,
|
|
1601
1606
|
): Promise<void> {
|
|
1602
1607
|
const backend = resolveExecutor(this.config);
|
|
1603
|
-
|
|
1608
|
+
let attempt = 1;
|
|
1609
|
+
let recoveredMissingImage = false;
|
|
1610
|
+
while (attempt <= this.warmSetupMaxAttempts) {
|
|
1604
1611
|
try {
|
|
1605
1612
|
await this.ensureWarmContainer();
|
|
1606
1613
|
await this.ensureBackendWarmup(backend);
|
|
1607
1614
|
return;
|
|
1608
1615
|
} catch (err) {
|
|
1616
|
+
if (this.isMissingDockerImageError(err) && !recoveredMissingImage) {
|
|
1617
|
+
recoveredMissingImage = true;
|
|
1618
|
+
const rebuildNote = `[DockerExecutor] Warm runtime image ${this.options.imageName} is missing locally; rebuilding before retrying warm container startup.`;
|
|
1619
|
+
console.warn(rebuildNote);
|
|
1620
|
+
onLog?.("stderr", rebuildNote);
|
|
1621
|
+
await this.stopWarmContainer("missing image recovery", true);
|
|
1622
|
+
this.warmedBackends.clear();
|
|
1623
|
+
if (await this.pullImage()) {
|
|
1624
|
+
const retryNote = `[DockerExecutor] Warm runtime image ${this.options.imageName} is available again; retrying warm container startup.`;
|
|
1625
|
+
console.log(retryNote);
|
|
1626
|
+
onLog?.("stdout", retryNote);
|
|
1627
|
+
continue;
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1609
1630
|
const retryable = this.isRetryableError(err);
|
|
1610
1631
|
if (attempt >= this.warmSetupMaxAttempts || !retryable) {
|
|
1611
1632
|
if (
|
|
@@ -1631,6 +1652,7 @@ export class DockerExecutor {
|
|
|
1631
1652
|
onLog?.("stderr", note);
|
|
1632
1653
|
await this.stopWarmContainer("warm setup retry", true);
|
|
1633
1654
|
await this.sleep(retryInMs);
|
|
1655
|
+
attempt += 1;
|
|
1634
1656
|
}
|
|
1635
1657
|
}
|
|
1636
1658
|
}
|
|
@@ -1727,6 +1749,10 @@ export class DockerExecutor {
|
|
|
1727
1749
|
return this.matchesRetryablePattern(text);
|
|
1728
1750
|
}
|
|
1729
1751
|
|
|
1752
|
+
private isMissingDockerImageError(err: unknown): boolean {
|
|
1753
|
+
return isMissingDockerImageDetail(this.compactError(err));
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1730
1756
|
private isRetryableJobFailure(result: DockerJobResult): boolean {
|
|
1731
1757
|
const text = `${result.summary ?? ""}\n${result.stderr ?? ""}`.toLowerCase();
|
|
1732
1758
|
return this.matchesRetryablePattern(text);
|
|
@@ -635,6 +635,7 @@ export function tokenizeValidationCommandArgv(command: string): string[] | null
|
|
|
635
635
|
const out: string[] = [];
|
|
636
636
|
let current = "";
|
|
637
637
|
let quote: "'" | '"' | null = null;
|
|
638
|
+
let escaped = false;
|
|
638
639
|
|
|
639
640
|
const pushCurrent = () => {
|
|
640
641
|
if (!current) return;
|
|
@@ -643,7 +644,16 @@ export function tokenizeValidationCommandArgv(command: string): string[] | null
|
|
|
643
644
|
};
|
|
644
645
|
|
|
645
646
|
for (const ch of trimmed) {
|
|
647
|
+
if (escaped) {
|
|
648
|
+
current += ch;
|
|
649
|
+
escaped = false;
|
|
650
|
+
continue;
|
|
651
|
+
}
|
|
646
652
|
if (quote) {
|
|
653
|
+
if (quote === '"' && ch === "\\") {
|
|
654
|
+
escaped = true;
|
|
655
|
+
continue;
|
|
656
|
+
}
|
|
647
657
|
if (ch === quote) {
|
|
648
658
|
quote = null;
|
|
649
659
|
} else {
|
|
@@ -662,6 +672,7 @@ export function tokenizeValidationCommandArgv(command: string): string[] | null
|
|
|
662
672
|
}
|
|
663
673
|
current += ch;
|
|
664
674
|
}
|
|
675
|
+
if (escaped) current += "\\";
|
|
665
676
|
if (quote) return null;
|
|
666
677
|
pushCurrent();
|
|
667
678
|
if (out.length === 0) return null;
|
|
@@ -2289,14 +2300,19 @@ export function inferFallbackValidationCommandsForTestTask(
|
|
|
2289
2300
|
/\b(pytest|python)\b/.test(lowerInstruction) ||
|
|
2290
2301
|
changedTestPaths.some((entry) => entry.toLowerCase().endsWith(".py"));
|
|
2291
2302
|
|
|
2303
|
+
const bunTestPath = (path: string) => formatBunTestPathArg(path);
|
|
2292
2304
|
const normalizedTarget = (targetPath ?? "").replace(/\\/g, "/").trim();
|
|
2293
2305
|
if (normalizedTarget && isLikelyTestPath(normalizedTarget)) {
|
|
2294
|
-
add(pythonSignal ? `pytest ${normalizedTarget}` : `bun test ${normalizedTarget}`);
|
|
2306
|
+
add(pythonSignal ? `pytest ${normalizedTarget}` : `bun test ${bunTestPath(normalizedTarget)}`);
|
|
2295
2307
|
}
|
|
2296
2308
|
|
|
2297
2309
|
if (changedTestPaths.length > 0) {
|
|
2298
|
-
const focused = changedTestPaths.slice(0, 4)
|
|
2299
|
-
add(
|
|
2310
|
+
const focused = changedTestPaths.slice(0, 4);
|
|
2311
|
+
add(
|
|
2312
|
+
pythonSignal
|
|
2313
|
+
? `pytest ${focused.join(" ")}`
|
|
2314
|
+
: `bun test ${focused.map((entry) => bunTestPath(entry)).join(" ")}`,
|
|
2315
|
+
);
|
|
2300
2316
|
}
|
|
2301
2317
|
|
|
2302
2318
|
const scopeHints = [
|
|
@@ -2324,6 +2340,24 @@ export function inferFallbackValidationCommandsForTestTask(
|
|
|
2324
2340
|
return candidates.slice(0, 4);
|
|
2325
2341
|
}
|
|
2326
2342
|
|
|
2343
|
+
export function formatBunTestPathArg(path: string): string {
|
|
2344
|
+
const normalized = String(path ?? "").replace(/\\/g, "/").trim();
|
|
2345
|
+
if (!normalized) return normalized;
|
|
2346
|
+
const pathArg =
|
|
2347
|
+
normalized.startsWith("./") ||
|
|
2348
|
+
normalized.startsWith("../") ||
|
|
2349
|
+
normalized.startsWith("/") ||
|
|
2350
|
+
/^[A-Za-z]:\//.test(normalized)
|
|
2351
|
+
? normalized
|
|
2352
|
+
: `./${normalized}`;
|
|
2353
|
+
return quoteValidationCommandArg(pathArg);
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2356
|
+
function quoteValidationCommandArg(arg: string): string {
|
|
2357
|
+
if (!/[\s"\\]/.test(arg)) return arg;
|
|
2358
|
+
return `"${arg.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2327
2361
|
export function isTestFocusedTask(
|
|
2328
2362
|
instruction: string,
|
|
2329
2363
|
planning: TaskExecutePlanning,
|
|
@@ -125,11 +125,31 @@ function isTestPath(path: string): boolean {
|
|
|
125
125
|
return /(^tests\/|__tests__\/|\.test\.[cm]?[jt]sx?$|\.spec\.[cm]?[jt]sx?$)/i.test(path);
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
function formatBunTestPathArg(path: string): string {
|
|
129
|
+
const normalized = String(path ?? "").replace(/\\/g, "/").trim();
|
|
130
|
+
if (!normalized) return normalized;
|
|
131
|
+
const pathArg =
|
|
132
|
+
normalized.startsWith("./") ||
|
|
133
|
+
normalized.startsWith("../") ||
|
|
134
|
+
normalized.startsWith("/") ||
|
|
135
|
+
/^[A-Za-z]:\//.test(normalized)
|
|
136
|
+
? normalized
|
|
137
|
+
: `./${normalized}`;
|
|
138
|
+
return quoteValidationCommandArg(pathArg);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function quoteValidationCommandArg(arg: string): string {
|
|
142
|
+
if (!/[\s"\\]/.test(arg)) return arg;
|
|
143
|
+
return `"${arg.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
144
|
+
}
|
|
145
|
+
|
|
128
146
|
function deriveValidationSteps(existing: unknown, conflictPaths: string[]): string[] {
|
|
129
147
|
const preserved = Array.isArray(existing)
|
|
130
148
|
? existing.map((entry) => String(entry ?? "").trim()).filter(Boolean)
|
|
131
149
|
: [];
|
|
132
|
-
const targeted = conflictPaths
|
|
150
|
+
const targeted = conflictPaths
|
|
151
|
+
.filter(isTestPath)
|
|
152
|
+
.map((entry) => `bun test ${formatBunTestPathArg(entry)}`);
|
|
133
153
|
const merged = dedupeStrings([...targeted, ...preserved], 8);
|
|
134
154
|
return merged.length > 0 ? merged : ["bun test"];
|
|
135
155
|
}
|
|
@@ -987,7 +987,11 @@ function failNoChangeReviewFixJob(jobId: string, result: WorkerJobResult): Worke
|
|
|
987
987
|
ok: false,
|
|
988
988
|
summary:
|
|
989
989
|
`Rejected review-fix job ${jobId} produced no code changes; refusing unchanged branch re-review.`,
|
|
990
|
-
stderr: [
|
|
990
|
+
stderr: [
|
|
991
|
+
result.stderr,
|
|
992
|
+
"Review-fix jobs must make at least one concrete code/test/docs change before requesting another review.",
|
|
993
|
+
"If the reviewer feedback is invalid, commit a narrow explanatory change that documents the decision; unchanged branch re-review is refused.",
|
|
994
|
+
]
|
|
991
995
|
.filter(Boolean)
|
|
992
996
|
.join("\n"),
|
|
993
997
|
exitCode: typeof result.exitCode === "number" ? result.exitCode : 4,
|