@pushpalsdev/cli 1.0.99 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushpalsdev/cli",
3
- "version": "1.0.99",
3
+ "version": "1.0.100",
4
4
  "description": "PushPals terminal CLI for LocalBuddy -> RemoteBuddy orchestration",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -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.
@@ -647,7 +647,10 @@ async function terminateValidationProcessTree(
647
647
  }
648
648
  }
649
649
 
650
- function captureValidationStream(stream: ReadableStream<Uint8Array> | null) {
650
+ function captureValidationStream(
651
+ stream: ReadableStream<Uint8Array> | null,
652
+ onChunk?: (chunk: string) => void,
653
+ ) {
651
654
  let text = "";
652
655
  let done = false;
653
656
  const reader = stream?.getReader();
@@ -657,7 +660,9 @@ function captureValidationStream(stream: ReadableStream<Uint8Array> | null) {
657
660
  while (true) {
658
661
  const result = await reader.read();
659
662
  if (result.done) break;
660
- text += Buffer.from(result.value).toString("utf8");
663
+ const chunk = Buffer.from(result.value).toString("utf8");
664
+ text += chunk;
665
+ onChunk?.(chunk);
661
666
  }
662
667
  } catch {
663
668
  // Stream cancellation after process exit is expected when descendants
@@ -689,6 +694,41 @@ function captureValidationStream(stream: ReadableStream<Uint8Array> | null) {
689
694
  };
690
695
  }
691
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
+
692
732
  export async function runValidationArgv(
693
733
  repo: string,
694
734
  command: string,
@@ -707,26 +747,62 @@ export async function runValidationArgv(
707
747
  stderr: "pipe",
708
748
  detached: process.platform !== "win32",
709
749
  });
710
- const stdoutCapture = captureValidationStream(proc.stdout);
711
- const stderrCapture = captureValidationStream(proc.stderr);
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);
712
756
  let timedOut = false;
757
+ let stoppedAfterFailureSignal = false;
713
758
  const timeout = Math.max(1_000, timeoutMs);
714
759
  let timeoutTimer: ReturnType<typeof setTimeout> | null = null;
715
- const timeoutPromise = new Promise<"timeout">((resolveTimeout) => {
760
+ const timeoutPromise = new Promise<{ type: "timeout" }>((resolveTimeout) => {
716
761
  timeoutTimer = setTimeout(() => {
717
762
  timedOut = true;
718
- void terminateValidationProcessTree(proc);
719
- resolveTimeout("timeout");
763
+ resolveTimeout({ type: "timeout" });
720
764
  }, timeout);
721
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
+
722
786
  const exitOrTimeout = await Promise.race([
723
787
  proc.exited.then((code) => ({ type: "exit" as const, code })),
724
788
  timeoutPromise,
789
+ failureSignalPromise,
725
790
  ]);
726
791
  if (timeoutTimer) clearTimeout(timeoutTimer);
727
- const exitCode = exitOrTimeout === "timeout" ? 124 : exitOrTimeout.code;
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;
728
804
 
729
- if (!timedOut) {
805
+ if (!timedOut && !stoppedAfterFailureSignal) {
730
806
  await Promise.race([
731
807
  Promise.all([stdoutCapture.promise, stderrCapture.promise]),
732
808
  Bun.sleep(1_000),
@@ -754,6 +830,9 @@ export async function runValidationArgv(
754
830
  [
755
831
  stderrCapture.text().trim(),
756
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
+ : "",
757
836
  ]
758
837
  .filter(Boolean)
759
838
  .join("\n"),
@@ -1474,6 +1553,12 @@ export function extractValidationFailureDigest(run: {
1474
1553
  /\bCould not resolve\s+['"`]?[^'"`\r\n]+['"`]?[^\r\n]*/i,
1475
1554
  /\bModule not found[^\r\n]*/i,
1476
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,
1477
1562
  /\bExecutable doesn't exist[^\r\n]*/i,
1478
1563
  /\bPlease run the following command to download new browsers:[^\r\n]*(?:\r?\n\s+[^\r\n]+)?/i,
1479
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.