@pushpalsdev/cli 1.0.98 → 1.0.100
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/prompts/workerpals/openai_codex_task_execute_system_prompt.md +1 -0
- package/runtime/sandbox/apps/workerpals/src/common/sandbox_env.ts +8 -0
- package/runtime/sandbox/apps/workerpals/src/execute_job.ts +167 -15
- package/runtime/sandbox/prompts/workerpals/openai_codex_task_execute_system_prompt.md +1 -0
package/package.json
CHANGED
|
@@ -16,6 +16,7 @@ Execution rules:
|
|
|
16
16
|
- If the hinted file is a thin wrapper or the behavior lives elsewhere, edit the behavior-owning file(s) needed to solve the task and explain the scope expansion in your final response.
|
|
17
17
|
- Avoid irrelevant sprawl; the review agent will judge whether changed files are necessary for the requested outcome.
|
|
18
18
|
- Read relevant files before editing, then run focused validation.
|
|
19
|
+
- PushPals runs the deterministic ValidationGate after your edit, including any repo-required `vision.md` commands. During the editing turn, prefer focused/fast validation. Do not spend the main Codex execution budget repeatedly running long browser/e2e smoke commands such as `bun run web:e2e`; run them only when the task is specifically about the browser harness or when you need a final targeted confirmation and can stop promptly on a clear failure.
|
|
19
20
|
- Use direct commands without shell wrappers. Prefer plain commands like `git diff -- path`, `git add <path>`, `git status --porcelain`, and `pwd`.
|
|
20
21
|
- Do not wrap commands in `/bin/bash -lc`, `sh -lc`, `cmd /c`, or `powershell -Command`, and avoid pipelines, `awk`, heredocs, or multi-command shell snippets unless they are truly unavoidable.
|
|
21
22
|
- If the command router rejects a command, simplify it to a single direct command instead of retrying more shell wrappers.
|
|
@@ -53,6 +53,12 @@ function ensureSandboxGitConfig(homeDir: string): void {
|
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
function withNodeDnsIpv4First(value: string | undefined): string {
|
|
57
|
+
const existing = (value ?? "").trim();
|
|
58
|
+
if (/(^|\s)--dns-result-order=/.test(existing)) return existing;
|
|
59
|
+
return [existing, "--dns-result-order=ipv4first"].filter(Boolean).join(" ");
|
|
60
|
+
}
|
|
61
|
+
|
|
56
62
|
function resolveOriginalHome(env: Record<string, string>): string {
|
|
57
63
|
return env.HOME || env.USERPROFILE || homedir();
|
|
58
64
|
}
|
|
@@ -100,6 +106,8 @@ export function buildWorkerSandboxWritableEnv(
|
|
|
100
106
|
EXPO_NO_INTERACTIVE: env.EXPO_NO_INTERACTIVE ?? "1",
|
|
101
107
|
CI: env.CI ?? "1",
|
|
102
108
|
BROWSER: env.BROWSER ?? "none",
|
|
109
|
+
NODE_OPTIONS: withNodeDnsIpv4First(env.NODE_OPTIONS),
|
|
110
|
+
REACT_NATIVE_PACKAGER_HOSTNAME: env.REACT_NATIVE_PACKAGER_HOSTNAME ?? "127.0.0.1",
|
|
103
111
|
EXPO_DEV_SERVER_PORT: env.EXPO_DEV_SERVER_PORT ?? defaultExpoPort,
|
|
104
112
|
RCT_METRO_PORT: env.RCT_METRO_PORT ?? defaultExpoPort,
|
|
105
113
|
PUSHPALS_VALIDATION_REPO: repo,
|
|
@@ -647,7 +647,89 @@ async function terminateValidationProcessTree(
|
|
|
647
647
|
}
|
|
648
648
|
}
|
|
649
649
|
|
|
650
|
-
|
|
650
|
+
function captureValidationStream(
|
|
651
|
+
stream: ReadableStream<Uint8Array> | null,
|
|
652
|
+
onChunk?: (chunk: string) => void,
|
|
653
|
+
) {
|
|
654
|
+
let text = "";
|
|
655
|
+
let done = false;
|
|
656
|
+
const reader = stream?.getReader();
|
|
657
|
+
const promise = reader
|
|
658
|
+
? (async () => {
|
|
659
|
+
try {
|
|
660
|
+
while (true) {
|
|
661
|
+
const result = await reader.read();
|
|
662
|
+
if (result.done) break;
|
|
663
|
+
const chunk = Buffer.from(result.value).toString("utf8");
|
|
664
|
+
text += chunk;
|
|
665
|
+
onChunk?.(chunk);
|
|
666
|
+
}
|
|
667
|
+
} catch {
|
|
668
|
+
// Stream cancellation after process exit is expected when descendants
|
|
669
|
+
// inherit pipes from failed browser/dev-server launchers.
|
|
670
|
+
} finally {
|
|
671
|
+
done = true;
|
|
672
|
+
try {
|
|
673
|
+
reader.releaseLock();
|
|
674
|
+
} catch {
|
|
675
|
+
// ignore
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
})()
|
|
679
|
+
: Promise.resolve().then(() => {
|
|
680
|
+
done = true;
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
return {
|
|
684
|
+
cancel: async () => {
|
|
685
|
+
try {
|
|
686
|
+
await reader?.cancel();
|
|
687
|
+
} catch {
|
|
688
|
+
// ignore
|
|
689
|
+
}
|
|
690
|
+
},
|
|
691
|
+
isDone: () => done,
|
|
692
|
+
promise,
|
|
693
|
+
text: () => text,
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
const DEFAULT_BROWSER_VALIDATION_FAILURE_IDLE_MS = 15_000;
|
|
698
|
+
|
|
699
|
+
function browserValidationFailureIdleMs(env: Record<string, string>): number {
|
|
700
|
+
const configured = Number(env.PUSHPALS_VALIDATION_FAILURE_IDLE_MS ?? "");
|
|
701
|
+
if (Number.isFinite(configured) && configured >= 250) {
|
|
702
|
+
return Math.min(120_000, Math.trunc(configured));
|
|
703
|
+
}
|
|
704
|
+
return DEFAULT_BROWSER_VALIDATION_FAILURE_IDLE_MS;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function hasBrowserValidationFailureSignal(output: string): boolean {
|
|
708
|
+
const text = String(output ?? "");
|
|
709
|
+
if (!text.trim()) return false;
|
|
710
|
+
const patterns = [
|
|
711
|
+
/\bAssertionError\b/i,
|
|
712
|
+
/\bTimeoutError\b/i,
|
|
713
|
+
/\bWeb end-to-end smoke test failed:/i,
|
|
714
|
+
/\bexpect\([^)]*\)\.[a-z0-9_]+\([^)]*\)\s+failed/i,
|
|
715
|
+
/\bError:\s+expect\(/i,
|
|
716
|
+
/\blocator\.[a-z0-9_]+:\s+Timeout\s+\d+ms\s+exceeded\b/i,
|
|
717
|
+
/\bpage\.[a-z0-9_]+:\s+Timeout\s+\d+ms\s+exceeded\b/i,
|
|
718
|
+
/\bTimeout\s+\d+ms\s+exceeded\b/i,
|
|
719
|
+
/\bTest timeout of \d+ms exceeded\b/i,
|
|
720
|
+
/\bCall log:\s*(?:\r?\n|$)/i,
|
|
721
|
+
/\bwaiting for getBy(?:TestId|Role|Text|Label|Placeholder|Title)\([^)]*\)/i,
|
|
722
|
+
/\bpage\.[a-z0-9_]+:\s+net::ERR_[A-Z0-9_]+/i,
|
|
723
|
+
/\bbrowserType\.launch:/i,
|
|
724
|
+
/\bERR_SOCKET_BAD_PORT\b/i,
|
|
725
|
+
/\blisten\s+EPERM\b/i,
|
|
726
|
+
/\bEADDRINUSE\b/i,
|
|
727
|
+
/\berror:\s+script\s+"[^"]+"\s+exited with code\s+\d+/i,
|
|
728
|
+
];
|
|
729
|
+
return patterns.some((pattern) => pattern.test(text));
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
export async function runValidationArgv(
|
|
651
733
|
repo: string,
|
|
652
734
|
command: string,
|
|
653
735
|
argv: string[],
|
|
@@ -665,32 +747,92 @@ async function runValidationArgv(
|
|
|
665
747
|
stderr: "pipe",
|
|
666
748
|
detached: process.platform !== "win32",
|
|
667
749
|
});
|
|
750
|
+
let lastOutputAt = Date.now();
|
|
751
|
+
const noteOutput = () => {
|
|
752
|
+
lastOutputAt = Date.now();
|
|
753
|
+
};
|
|
754
|
+
const stdoutCapture = captureValidationStream(proc.stdout, noteOutput);
|
|
755
|
+
const stderrCapture = captureValidationStream(proc.stderr, noteOutput);
|
|
668
756
|
let timedOut = false;
|
|
669
|
-
|
|
670
|
-
|
|
757
|
+
let stoppedAfterFailureSignal = false;
|
|
758
|
+
const timeout = Math.max(1_000, timeoutMs);
|
|
759
|
+
let timeoutTimer: ReturnType<typeof setTimeout> | null = null;
|
|
760
|
+
const timeoutPromise = new Promise<{ type: "timeout" }>((resolveTimeout) => {
|
|
761
|
+
timeoutTimer = setTimeout(() => {
|
|
671
762
|
timedOut = true;
|
|
672
|
-
|
|
673
|
-
},
|
|
674
|
-
|
|
675
|
-
|
|
763
|
+
resolveTimeout({ type: "timeout" });
|
|
764
|
+
}, timeout);
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
let failureSignalTimer: ReturnType<typeof setInterval> | null = null;
|
|
768
|
+
const failureSignalPromise = isLongRunningBrowserValidationCommand(command)
|
|
769
|
+
? new Promise<{ type: "failure-signal" }>((resolveFailureSignal) => {
|
|
770
|
+
const idleMs = browserValidationFailureIdleMs(env);
|
|
771
|
+
failureSignalTimer = setInterval(() => {
|
|
772
|
+
const combinedOutput = `${stdoutCapture.text()}\n${stderrCapture.text()}`;
|
|
773
|
+
if (
|
|
774
|
+
hasBrowserValidationFailureSignal(combinedOutput) &&
|
|
775
|
+
Date.now() - lastOutputAt >= idleMs
|
|
776
|
+
) {
|
|
777
|
+
stoppedAfterFailureSignal = true;
|
|
778
|
+
resolveFailureSignal({ type: "failure-signal" });
|
|
779
|
+
}
|
|
780
|
+
}, 250);
|
|
781
|
+
})
|
|
782
|
+
: new Promise<never>(() => {
|
|
783
|
+
// Non-browser validations should only end on process exit or timeout.
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
const exitOrTimeout = await Promise.race([
|
|
787
|
+
proc.exited.then((code) => ({ type: "exit" as const, code })),
|
|
788
|
+
timeoutPromise,
|
|
789
|
+
failureSignalPromise,
|
|
790
|
+
]);
|
|
791
|
+
if (timeoutTimer) clearTimeout(timeoutTimer);
|
|
792
|
+
if (failureSignalTimer) clearInterval(failureSignalTimer);
|
|
793
|
+
|
|
794
|
+
if (timedOut || stoppedAfterFailureSignal) {
|
|
795
|
+
await terminateValidationProcessTree(proc);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
const exitCode =
|
|
799
|
+
exitOrTimeout.type === "timeout"
|
|
800
|
+
? 124
|
|
801
|
+
: exitOrTimeout.type === "failure-signal"
|
|
802
|
+
? 1
|
|
803
|
+
: exitOrTimeout.code;
|
|
804
|
+
|
|
805
|
+
if (!timedOut && !stoppedAfterFailureSignal) {
|
|
806
|
+
await Promise.race([
|
|
807
|
+
Promise.all([stdoutCapture.promise, stderrCapture.promise]),
|
|
808
|
+
Bun.sleep(1_000),
|
|
809
|
+
]);
|
|
810
|
+
if (!stdoutCapture.isDone() || !stderrCapture.isDone()) {
|
|
811
|
+
await terminateValidationProcessTree(proc);
|
|
812
|
+
await Promise.all([stdoutCapture.cancel(), stderrCapture.cancel()]);
|
|
813
|
+
}
|
|
814
|
+
} else {
|
|
815
|
+
await Promise.all([stdoutCapture.cancel(), stderrCapture.cancel()]);
|
|
816
|
+
}
|
|
676
817
|
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
proc.exited,
|
|
818
|
+
await Promise.race([
|
|
819
|
+
Promise.all([stdoutCapture.promise, stderrCapture.promise]),
|
|
820
|
+
Bun.sleep(500),
|
|
681
821
|
]);
|
|
682
|
-
clearTimeout(timer);
|
|
683
822
|
|
|
684
823
|
return {
|
|
685
824
|
step: command,
|
|
686
825
|
command,
|
|
687
826
|
ok: !timedOut && exitCode === 0,
|
|
688
|
-
exitCode
|
|
689
|
-
stdout: compactJobOutput(
|
|
827
|
+
exitCode,
|
|
828
|
+
stdout: compactJobOutput(stdoutCapture.text().trim(), outputPolicy),
|
|
690
829
|
stderr: compactJobOutput(
|
|
691
830
|
[
|
|
692
|
-
|
|
831
|
+
stderrCapture.text().trim(),
|
|
693
832
|
timedOut ? timeoutMessage : "",
|
|
833
|
+
stoppedAfterFailureSignal
|
|
834
|
+
? `Validation command emitted a browser/e2e failure signal and then produced no output for ${browserValidationFailureIdleMs(env)}ms. PushPals terminated the leaked process tree and preserved the captured failure output for repair.`
|
|
835
|
+
: "",
|
|
694
836
|
]
|
|
695
837
|
.filter(Boolean)
|
|
696
838
|
.join("\n"),
|
|
@@ -1242,6 +1384,10 @@ function detectValidationBlocker(runs: ValidationExecutionResult[]): ValidationB
|
|
|
1242
1384
|
combined.includes("network access") ||
|
|
1243
1385
|
combined.includes("connection refused") ||
|
|
1244
1386
|
combined.includes("getaddrinfo") ||
|
|
1387
|
+
combined.includes("err_socket_bad_port") ||
|
|
1388
|
+
combined.includes("expo exited early") ||
|
|
1389
|
+
combined.includes("eperm") ||
|
|
1390
|
+
combined.includes("operation not permitted") ||
|
|
1245
1391
|
combined.includes("eacces")
|
|
1246
1392
|
) {
|
|
1247
1393
|
return {
|
|
@@ -1407,6 +1553,12 @@ export function extractValidationFailureDigest(run: {
|
|
|
1407
1553
|
/\bCould not resolve\s+['"`]?[^'"`\r\n]+['"`]?[^\r\n]*/i,
|
|
1408
1554
|
/\bModule not found[^\r\n]*/i,
|
|
1409
1555
|
/\bbrowserType\.launch:[^\r\n]*/i,
|
|
1556
|
+
/\bWeb end-to-end smoke test failed:[^\r\n]*/i,
|
|
1557
|
+
/\blocator\.[a-z0-9_]+:\s+Timeout\s+\d+ms\s+exceeded[^\r\n]*/i,
|
|
1558
|
+
/\bpage\.[a-z0-9_]+:\s+Timeout\s+\d+ms\s+exceeded[^\r\n]*/i,
|
|
1559
|
+
/\bTimeout\s+\d+ms\s+exceeded[^\r\n]*/i,
|
|
1560
|
+
/\bwaiting for getBy(?:TestId|Role|Text|Label|Placeholder|Title)\([^)]*\)[^\r\n]*/i,
|
|
1561
|
+
/\bpage\.[a-z0-9_]+:\s+net::ERR_[A-Z0-9_]+[^\r\n]*/i,
|
|
1410
1562
|
/\bExecutable doesn't exist[^\r\n]*/i,
|
|
1411
1563
|
/\bPlease run the following command to download new browsers:[^\r\n]*(?:\r?\n\s+[^\r\n]+)?/i,
|
|
1412
1564
|
/\bRun ["`]?npx playwright install[^'"`\r\n]*["`]?[^\r\n]*/i,
|
|
@@ -16,6 +16,7 @@ Execution rules:
|
|
|
16
16
|
- If the hinted file is a thin wrapper or the behavior lives elsewhere, edit the behavior-owning file(s) needed to solve the task and explain the scope expansion in your final response.
|
|
17
17
|
- Avoid irrelevant sprawl; the review agent will judge whether changed files are necessary for the requested outcome.
|
|
18
18
|
- Read relevant files before editing, then run focused validation.
|
|
19
|
+
- PushPals runs the deterministic ValidationGate after your edit, including any repo-required `vision.md` commands. During the editing turn, prefer focused/fast validation. Do not spend the main Codex execution budget repeatedly running long browser/e2e smoke commands such as `bun run web:e2e`; run them only when the task is specifically about the browser harness or when you need a final targeted confirmation and can stop promptly on a clear failure.
|
|
19
20
|
- Use direct commands without shell wrappers. Prefer plain commands like `git diff -- path`, `git add <path>`, `git status --porcelain`, and `pwd`.
|
|
20
21
|
- Do not wrap commands in `/bin/bash -lc`, `sh -lc`, `cmd /c`, or `powershell -Command`, and avoid pipelines, `awk`, heredocs, or multi-command shell snippets unless they are truly unavoidable.
|
|
21
22
|
- If the command router rejects a command, simplify it to a single direct command instead of retrying more shell wrappers.
|