@pushpalsdev/cli 1.0.53 → 1.0.55

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.53",
3
+ "version": "1.0.55",
4
4
  "description": "PushPals terminal CLI for LocalBuddy -> RemoteBuddy orchestration",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -64,6 +64,11 @@ interface ValidationExecutionResult {
64
64
  elapsedMs: number;
65
65
  }
66
66
 
67
+ interface ValidationBlocker {
68
+ category: "repo" | "environment";
69
+ detail: string;
70
+ }
71
+
67
72
  interface DeterministicQualityResult {
68
73
  ok: boolean;
69
74
  skipped: boolean;
@@ -71,6 +76,7 @@ interface DeterministicQualityResult {
71
76
  changedPaths: string[];
72
77
  changedTestPaths: string[];
73
78
  validationRuns: ValidationExecutionResult[];
79
+ blocker: ValidationBlocker | null;
74
80
  }
75
81
 
76
82
  interface CriticReview {
@@ -198,6 +204,30 @@ export function buildQualityGateRevisionIssues(
198
204
  return [...new Set(merged)];
199
205
  }
200
206
 
207
+ const TEST_ASSERTION_BALANCE_ISSUE =
208
+ "Changed test files do not show both positive and negative assertion coverage (expected both).";
209
+
210
+ export function relaxAdvisoryQualityIssues(
211
+ qualityIssues: string[],
212
+ validationRuns: Array<{ ok: boolean }>,
213
+ critic: CriticReview | null,
214
+ qualityCriticMinScore: number,
215
+ ): string[] {
216
+ const normalizedQualityIssues = qualityIssues
217
+ .map((entry) => String(entry ?? "").trim())
218
+ .filter(Boolean);
219
+ if (normalizedQualityIssues.length === 0) return [];
220
+
221
+ const hasPassingValidation = validationRuns.some((run) => Boolean(run?.ok));
222
+ const criticPasses = !critic || critic.score >= qualityCriticMinScore;
223
+ if (!hasPassingValidation || !criticPasses) {
224
+ return normalizedQualityIssues;
225
+ }
226
+
227
+ const relaxed = normalizedQualityIssues.filter((issue) => issue !== TEST_ASSERTION_BALANCE_ISSUE);
228
+ return relaxed;
229
+ }
230
+
201
231
  export function resolveReviewFixCompletionBranch(
202
232
  value: unknown,
203
233
  fallbackBranch: string,
@@ -323,7 +353,7 @@ export function deriveQualityGatePolicy(
323
353
  return {
324
354
  mode: "merge_conflict",
325
355
  maxAutoRevisions: baseMaxAutoRevisions,
326
- softPassOnExhausted: false,
356
+ softPassOnExhausted: baseSoftPassOnExhausted,
327
357
  criticMinScore: baseCriticMinScore,
328
358
  };
329
359
  }
@@ -341,7 +371,7 @@ export function deriveQualityGatePolicy(
341
371
  return {
342
372
  mode: "review_fix",
343
373
  maxAutoRevisions: Math.max(baseMaxAutoRevisions, 2),
344
- softPassOnExhausted: false,
374
+ softPassOnExhausted: baseSoftPassOnExhausted,
345
375
  criticMinScore: tightenedCriticMinScore,
346
376
  };
347
377
  }
@@ -533,6 +563,60 @@ async function runValidationCommand(
533
563
  };
534
564
  }
535
565
 
566
+ function extractPreparedMergeConflictPaths(params: Record<string, unknown>): string[] {
567
+ const reviewAgent =
568
+ params.reviewAgent && typeof params.reviewAgent === "object" && !Array.isArray(params.reviewAgent)
569
+ ? (params.reviewAgent as Record<string, unknown>)
570
+ : null;
571
+ const preparedPaths = Array.isArray(reviewAgent?.preparedConflictPaths)
572
+ ? reviewAgent.preparedConflictPaths
573
+ : [];
574
+ return preparedPaths
575
+ .map((entry) => String(entry ?? "").trim().replace(/\\/g, "/"))
576
+ .filter(Boolean);
577
+ }
578
+
579
+ function detectValidationBlocker(runs: ValidationExecutionResult[]): ValidationBlocker | null {
580
+ const combined = runs
581
+ .flatMap((run) => [run.stdout, run.stderr])
582
+ .filter(Boolean)
583
+ .join("\n")
584
+ .toLowerCase();
585
+ if (!combined) return null;
586
+
587
+ if (
588
+ combined.includes("cannot find module") ||
589
+ combined.includes("module not found") ||
590
+ combined.includes("failed to resolve import") ||
591
+ combined.includes("could not resolve") ||
592
+ combined.includes("no such file or directory") ||
593
+ combined.includes("package not found")
594
+ ) {
595
+ return {
596
+ category: "repo",
597
+ detail:
598
+ "Validation is blocked by missing repo dependencies or imported files. Fix the repository test/runtime setup before retrying this job.",
599
+ };
600
+ }
601
+
602
+ if (
603
+ combined.includes("read-only file system") ||
604
+ combined.includes("permission denied") ||
605
+ combined.includes("network access") ||
606
+ combined.includes("connection refused") ||
607
+ combined.includes("getaddrinfo") ||
608
+ combined.includes("eacces")
609
+ ) {
610
+ return {
611
+ category: "environment",
612
+ detail:
613
+ "Validation is blocked by sandbox environment restrictions (filesystem, permissions, or network). Retry only after the worker environment is fixed.",
614
+ };
615
+ }
616
+
617
+ return null;
618
+ }
619
+
536
620
  function stripAnsiControlSequences(value: string): string {
537
621
  return value.replace(/\u001b\[[0-9;?]*[ -/]*[@-~]/g, "");
538
622
  }
@@ -722,7 +806,7 @@ function isTestFocusedTask(
722
806
 
723
807
  function hasBalancedPositiveNegativeAssertions(paths: string[], repo: string): boolean {
724
808
  const negativeSignal =
725
- /\b(invalid|negative|error|throw|reject|null|undefined|non[- ]?existent|toThrow|toBeNull|toBeUndefined|<\s*0|<=\s*0)\b/i;
809
+ /(\.not\b|\b(invalid|negative|error|throw|reject|null|undefined|non[- ]?existent|toThrow|toBeNull|toBeUndefined|without|missing|absent|unchanged|same|remains?|stays?|prevent|avoid|zero|none)\b|<\s*0|<=\s*0)/i;
726
810
  let positiveAssertions = 0;
727
811
  let negativeAssertions = 0;
728
812
 
@@ -762,12 +846,18 @@ async function runDeterministicQualityGate(
762
846
  changedPaths: [],
763
847
  changedTestPaths: [],
764
848
  validationRuns: [],
849
+ blocker: null,
765
850
  };
766
851
  }
767
852
 
768
853
  const statusResult = await git(repo, ["status", "--porcelain"]);
769
854
  const changedPaths = statusResult.ok ? parseChangedPathsFromStatus(statusResult.stdout) : [];
770
- const changedTestPaths = changedPaths.filter((path) => isLikelyTestPath(path));
855
+ const preparedMergeConflictPaths = extractPreparedMergeConflictPaths(params);
856
+ const changedTestPaths = Array.from(
857
+ new Set(
858
+ [...changedPaths, ...preparedMergeConflictPaths].filter((path) => isLikelyTestPath(path)),
859
+ ),
860
+ );
771
861
  const issues: string[] = [];
772
862
  if (changedTestPaths.length === 0) {
773
863
  issues.push("No relevant test file was modified for this test-focused task.");
@@ -848,14 +938,16 @@ async function runDeterministicQualityGate(
848
938
  issues.push("Validation steps did not execute a recognizable test command.");
849
939
  }
850
940
  }
941
+ const blocker = detectValidationBlocker(validationRuns);
851
942
 
852
943
  return {
853
- ok: issues.length === 0,
944
+ ok: issues.length === 0 && blocker === null,
854
945
  skipped: false,
855
946
  issues,
856
947
  changedPaths,
857
948
  changedTestPaths,
858
949
  validationRuns,
950
+ blocker,
859
951
  };
860
952
  }
861
953
 
@@ -3491,12 +3583,12 @@ export async function executeJob(
3491
3583
  : qualityCriticMinScore.toFixed(1);
3492
3584
  onLog?.(
3493
3585
  "stdout",
3494
- `[QualityGate] review_fix override active: prior_score=${priorScore}, target_threshold=${threshold}, soft_pass_on_exhausted=false.`,
3586
+ `[QualityGate] review_fix policy active: prior_score=${priorScore}, target_threshold=${threshold}, soft_pass_on_exhausted=${qualitySoftPassOnExhausted ? "true" : "false"}; repo/environment blockers still fail hard.`,
3495
3587
  );
3496
3588
  } else if (qualityGatePolicy.mode === "merge_conflict") {
3497
3589
  onLog?.(
3498
3590
  "stdout",
3499
- "[QualityGate] merge_conflict override active: soft_pass_on_exhausted=false until the sandbox rebase is fully completed.",
3591
+ `[QualityGate] merge_conflict policy active: soft_pass_on_exhausted=${qualitySoftPassOnExhausted ? "true" : "false"}; unfinished rebases and repo/environment blockers still fail hard.`,
3500
3592
  );
3501
3593
  }
3502
3594
 
@@ -3567,7 +3659,20 @@ export async function executeJob(
3567
3659
  : executor === "openai_codex"
3568
3660
  ? await runCodexCriticReview(repo, attemptParams, quality, runtimeConfig, onLog)
3569
3661
  : await runTaskCriticReview(repo, attemptParams, quality, runtimeConfig, onLog);
3570
- const deterministicRequiresRevision = !quality.ok;
3662
+ const effectiveQualityIssues = relaxAdvisoryQualityIssues(
3663
+ quality.issues,
3664
+ quality.validationRuns,
3665
+ critic,
3666
+ qualityCriticMinScore,
3667
+ );
3668
+ if (effectiveQualityIssues.length !== quality.issues.length) {
3669
+ onLog?.(
3670
+ "stdout",
3671
+ "[QualityGate] Assertion-balance heuristic downgraded to advisory because validation passed and critic score met threshold.",
3672
+ );
3673
+ }
3674
+ const deterministicRequiresRevision =
3675
+ effectiveQualityIssues.length > 0 || quality.blocker !== null;
3571
3676
  const criticRequiresRevision = Boolean(critic && critic.score < qualityCriticMinScore);
3572
3677
 
3573
3678
  if (!deterministicRequiresRevision && !criticRequiresRevision) {
@@ -3580,8 +3685,29 @@ export async function executeJob(
3580
3685
  return result;
3581
3686
  }
3582
3687
 
3583
- const issues = buildQualityGateRevisionIssues(quality.issues, critic, qualityCriticMinScore);
3688
+ const issues = buildQualityGateRevisionIssues(
3689
+ effectiveQualityIssues,
3690
+ critic,
3691
+ qualityCriticMinScore,
3692
+ );
3584
3693
  const issueSummary = issues.map((entry) => toSingleLine(entry, 180)).join(" | ");
3694
+ if (quality.blocker) {
3695
+ const blockerSummary = `Quality gate blocked by ${quality.blocker.category} issue: ${quality.blocker.detail}`;
3696
+ onLog?.("stderr", `[QualityGate] ${blockerSummary}`);
3697
+ return {
3698
+ ok: false,
3699
+ summary: blockerSummary,
3700
+ stdout: result.stdout,
3701
+ stderr: truncate(
3702
+ [
3703
+ result.stderr ?? "",
3704
+ ...quality.validationRuns.flatMap((run) => [run.stdout, run.stderr]).filter(Boolean),
3705
+ ].join("\n"),
3706
+ outputPolicyForRuntime(runtimeConfig),
3707
+ ),
3708
+ exitCode: 4,
3709
+ };
3710
+ }
3585
3711
  if (revisionAttempt >= qualityMaxAutoRevisions) {
3586
3712
  if (qualitySoftPassOnExhausted) {
3587
3713
  const diagnostics = truncate(