@pushpalsdev/cli 1.0.54 → 1.0.56

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.54",
3
+ "version": "1.0.56",
4
4
  "description": "PushPals terminal CLI for LocalBuddy -> RemoteBuddy orchestration",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -104,6 +104,15 @@ export interface QualityGatePolicy {
104
104
  criticMinScore: number;
105
105
  }
106
106
 
107
+ function shouldSoftPassValidationBlocker(
108
+ policy: QualityGatePolicy,
109
+ blocker: ValidationBlocker | null,
110
+ ): boolean {
111
+ if (!blocker) return false;
112
+ if (!policy.softPassOnExhausted) return false;
113
+ return policy.mode === "review_fix" || policy.mode === "merge_conflict";
114
+ }
115
+
107
116
  // ─── Utilities ───────────────────────────────────────────────────────────────
108
117
 
109
118
  export function shouldCommit(
@@ -2140,7 +2149,13 @@ export async function resumePreparedMergeConflictRebase(
2140
2149
  params?: Record<string, unknown>,
2141
2150
  onLog?: (stream: "stdout" | "stderr", line: string) => void,
2142
2151
  ): Promise<
2143
- | { ok: true; resumed: boolean; sequencer: "rebase" | "merge" | "cherry-pick" | null; detail?: string }
2152
+ | {
2153
+ ok: true;
2154
+ resumed: boolean;
2155
+ sequencer: "rebase" | "merge" | "cherry-pick" | null;
2156
+ detail?: string;
2157
+ advancedToNextConflict?: boolean;
2158
+ }
2144
2159
  | { ok: false; error: string }
2145
2160
  > {
2146
2161
  const sequencer = await activeGitOperation(repo);
@@ -2230,6 +2245,26 @@ export async function resumePreparedMergeConflictRebase(
2230
2245
  continueOutput = combinedGitOutput(rebaseContinue);
2231
2246
  }
2232
2247
  if (!rebaseContinue.ok) {
2248
+ const continuingSequencer = await activeGitOperation(repo);
2249
+ if (continuingSequencer === "rebase") {
2250
+ const nextUnresolved = await git(repo, ["diff", "--name-only", "--diff-filter=U"]);
2251
+ if (nextUnresolved.ok) {
2252
+ const nextPaths = parseChangedPathsFromNameOnlyOutput(nextUnresolved.stdout);
2253
+ if (nextPaths.length > 0) {
2254
+ onLog?.(
2255
+ "stdout",
2256
+ `[MergeConflict] Rebase advanced into another conflicted commit with ${nextPaths.length} unresolved file(s); rerunning the resolver on updated sandbox state.`,
2257
+ );
2258
+ return {
2259
+ ok: true,
2260
+ resumed: true,
2261
+ sequencer: "rebase",
2262
+ detail: `rebase advanced into another conflicted commit with ${nextPaths.length} unresolved file(s)`,
2263
+ advancedToNextConflict: true,
2264
+ };
2265
+ }
2266
+ }
2267
+ }
2233
2268
  return {
2234
2269
  ok: false,
2235
2270
  error: `Failed to continue prepared merge-conflict rebase: ${continueOutput}`,
@@ -2951,6 +2986,7 @@ export function buildWorkerCommitMessage(
2951
2986
  export type { JobResult } from "./common/types.js";
2952
2987
 
2953
2988
  const SUPPORTED_JOB_KINDS = new Set(["warmup.execute", "task.execute"]);
2989
+ const MAX_MERGE_CONFLICT_RESOLUTION_PASSES = 8;
2954
2990
 
2955
2991
  type TaskExecutePriority = "interactive" | "normal" | "background";
2956
2992
  type TaskExecuteIntent = "chat" | "status" | "code_change" | "analysis" | "other";
@@ -3583,12 +3619,12 @@ export async function executeJob(
3583
3619
  : qualityCriticMinScore.toFixed(1);
3584
3620
  onLog?.(
3585
3621
  "stdout",
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.`,
3622
+ `[QualityGate] review_fix policy active: prior_score=${priorScore}, target_threshold=${threshold}, soft_pass_on_exhausted=${qualitySoftPassOnExhausted ? "true" : "false"}; unfinished branch-state blockers fail hard, but repo/environment validation blockers soft-pass once the update is publishable.`,
3587
3623
  );
3588
3624
  } else if (qualityGatePolicy.mode === "merge_conflict") {
3589
3625
  onLog?.(
3590
3626
  "stdout",
3591
- `[QualityGate] merge_conflict policy active: soft_pass_on_exhausted=${qualitySoftPassOnExhausted ? "true" : "false"}; unfinished rebases and repo/environment blockers still fail hard.`,
3627
+ `[QualityGate] merge_conflict policy active: soft_pass_on_exhausted=${qualitySoftPassOnExhausted ? "true" : "false"}; unfinished rebases still fail hard, but repo/environment validation blockers soft-pass once the rebase is publishable.`,
3592
3628
  );
3593
3629
  }
3594
3630
 
@@ -3611,41 +3647,75 @@ export async function executeJob(
3611
3647
  exitCode: 1,
3612
3648
  };
3613
3649
  }
3614
- const result = await runExecutor(
3615
- kind,
3616
- attemptParams,
3617
- repo,
3618
- runtimeConfig,
3619
- onLog,
3620
- executeBudgets,
3621
- );
3622
- if (!result.ok) return result;
3623
- if (mergeConflictContext) {
3650
+ let result: Awaited<ReturnType<typeof runExecutor>> | null = null;
3651
+ let mergeConflictPass = 0;
3652
+ while (true) {
3653
+ const currentResult = await runExecutor(
3654
+ kind,
3655
+ attemptParams,
3656
+ repo,
3657
+ runtimeConfig,
3658
+ onLog,
3659
+ executeBudgets,
3660
+ );
3661
+ if (!currentResult.ok) return currentResult;
3662
+ result = currentResult;
3663
+ if (!mergeConflictContext) break;
3664
+
3624
3665
  const resume = await resumePreparedMergeConflictRebase(repo, kind, attemptParams, onLog);
3625
3666
  if (!resume.ok) {
3626
3667
  onLog?.("stderr", `[MergeConflict] ${resume.error}`);
3627
3668
  return {
3628
3669
  ok: false,
3629
3670
  summary: "Merge-conflict rebase continuation failed",
3630
- stdout: result.stdout,
3631
- stderr: [result.stderr ?? "", resume.error].filter(Boolean).join("\n"),
3671
+ stdout: currentResult.stdout,
3672
+ stderr: [currentResult.stderr ?? "", resume.error].filter(Boolean).join("\n"),
3632
3673
  exitCode: 4,
3633
3674
  };
3634
3675
  }
3635
3676
  const sequencer = resume.sequencer;
3636
- if (sequencer) {
3637
- const detail =
3638
- `Merge-conflict job returned with git ${sequencer} still in progress. ` +
3639
- `Finish the ${sequencer} before returning control to WorkerPals.`;
3640
- onLog?.("stderr", `[MergeConflict] ${detail}`);
3641
- return {
3642
- ok: false,
3643
- summary: detail,
3644
- stdout: result.stdout,
3645
- stderr: [result.stderr ?? "", detail].filter(Boolean).join("\n"),
3646
- exitCode: 4,
3647
- };
3677
+ if (!sequencer) break;
3678
+ if (sequencer === "rebase" && resume.resumed && resume.advancedToNextConflict) {
3679
+ mergeConflictPass += 1;
3680
+ if (mergeConflictPass >= MAX_MERGE_CONFLICT_RESOLUTION_PASSES) {
3681
+ const detail =
3682
+ `Merge-conflict rebase required more than ${MAX_MERGE_CONFLICT_RESOLUTION_PASSES} resolver passes. ` +
3683
+ "Stopping to avoid an infinite conflict-resolution loop.";
3684
+ onLog?.("stderr", `[MergeConflict] ${detail}`);
3685
+ return {
3686
+ ok: false,
3687
+ summary: detail,
3688
+ stdout: currentResult.stdout,
3689
+ stderr: [currentResult.stderr ?? "", resume.detail ?? detail].filter(Boolean).join("\n"),
3690
+ exitCode: 4,
3691
+ };
3692
+ }
3693
+ onLog?.(
3694
+ "stdout",
3695
+ `[MergeConflict] Rebase surfaced another conflicted commit after auto-continue; rerunning resolver pass ${
3696
+ mergeConflictPass + 1
3697
+ }.`,
3698
+ );
3699
+ continue;
3648
3700
  }
3701
+ const detail =
3702
+ `Merge-conflict job returned with git ${sequencer} still in progress. ` +
3703
+ `Finish the ${sequencer} before returning control to WorkerPals.`;
3704
+ onLog?.("stderr", `[MergeConflict] ${detail}`);
3705
+ return {
3706
+ ok: false,
3707
+ summary: detail,
3708
+ stdout: currentResult.stdout,
3709
+ stderr: [currentResult.stderr ?? "", detail].filter(Boolean).join("\n"),
3710
+ exitCode: 4,
3711
+ };
3712
+ }
3713
+ if (!result) {
3714
+ return {
3715
+ ok: false,
3716
+ summary: "Merge-conflict execution ended without an executor result.",
3717
+ exitCode: 4,
3718
+ };
3649
3719
  }
3650
3720
 
3651
3721
  const scopeCheck = await collectWriteScopeWarnings(repo, planning);
@@ -3693,18 +3763,36 @@ export async function executeJob(
3693
3763
  const issueSummary = issues.map((entry) => toSingleLine(entry, 180)).join(" | ");
3694
3764
  if (quality.blocker) {
3695
3765
  const blockerSummary = `Quality gate blocked by ${quality.blocker.category} issue: ${quality.blocker.detail}`;
3766
+ const blockerDiagnostics = truncate(
3767
+ [
3768
+ result.stderr ?? "",
3769
+ ...quality.validationRuns.flatMap((run) => [run.stdout, run.stderr]).filter(Boolean),
3770
+ ].join("\n"),
3771
+ outputPolicyForRuntime(runtimeConfig),
3772
+ );
3773
+ if (shouldSoftPassValidationBlocker(qualityGatePolicy, quality.blocker)) {
3774
+ onLog?.(
3775
+ "stderr",
3776
+ `[QualityGate] Soft-pass on ${quality.blocker.category} blocker for publishable ${qualityGatePolicy.mode} job: ${toSingleLine(
3777
+ quality.blocker.detail,
3778
+ 260,
3779
+ )}`,
3780
+ );
3781
+ return {
3782
+ ...result,
3783
+ summary:
3784
+ `${result.summary} ` +
3785
+ `(quality gate soft-pass on ${quality.blocker.category} blocker after publishable ${qualityGatePolicy.mode} update)`,
3786
+ stderr: blockerDiagnostics,
3787
+ exitCode: typeof result.exitCode === "number" ? result.exitCode : 0,
3788
+ };
3789
+ }
3696
3790
  onLog?.("stderr", `[QualityGate] ${blockerSummary}`);
3697
3791
  return {
3698
3792
  ok: false,
3699
3793
  summary: blockerSummary,
3700
3794
  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
- ),
3795
+ stderr: blockerDiagnostics,
3708
3796
  exitCode: 4,
3709
3797
  };
3710
3798
  }