@pushpalsdev/cli 1.0.28 → 1.0.30

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.
@@ -1946,8 +1946,14 @@ function cleanupLegacyRuntimeBinaryLayouts(runtimeRoot, platformKey, activeBinDi
1946
1946
  function buildEmbeddedRuntimeEnv(baseEnv, opts) {
1947
1947
  const env = normalizeChildProcessEnv(baseEnv);
1948
1948
  const useRuntimeConfig = opts.useRuntimeConfig !== false;
1949
+ const inherited = { ...env };
1950
+ if (!useRuntimeConfig) {
1951
+ delete inherited.PUSHPALS_CONFIG_DIR_OVERRIDE;
1952
+ delete inherited.PUSHPALS_WORKERPALS_SANDBOX_ROOT;
1953
+ delete inherited.PUSHPALS_RUNTIME_TAG;
1954
+ }
1949
1955
  return {
1950
- ...env,
1956
+ ...inherited,
1951
1957
  PUSHPALS_REPO_ROOT_OVERRIDE: opts.repoRoot,
1952
1958
  PUSHPALS_PROJECT_ROOT_OVERRIDE: opts.repoRoot,
1953
1959
  ...useRuntimeConfig ? {
@@ -2484,6 +2490,36 @@ async function resolveWorkerpalDockerProbe(cwd, env, platform = process.platform
2484
2490
  var WORKERPAL_SANDBOX_RUNTIME_TAG_LABEL = "pushpals.runtime_tag";
2485
2491
  var WORKERPAL_SANDBOX_COMPONENT_LABEL = "pushpals.component=workerpals-sandbox";
2486
2492
  var WORKERPAL_WARM_COMPONENT_LABEL = "pushpals.component=workerpals-warm";
2493
+ var SOURCE_CONTROL_MANAGER_TEMP_BRANCH_PREFIX = "_source_control_manager/";
2494
+ function normalizeFsPathForComparison(value) {
2495
+ const resolved = resolve4(String(value ?? "").trim()).replace(/\\/g, "/").replace(/\/+$/, "");
2496
+ return process.platform === "win32" ? resolved.toLowerCase() : resolved;
2497
+ }
2498
+ function parseGitWorktreeListPorcelain(stdout) {
2499
+ const entries = [];
2500
+ const blocks = String(stdout ?? "").split(/\r?\n\r?\n/g).map((block) => block.trim()).filter(Boolean);
2501
+ for (const block of blocks) {
2502
+ const lines = block.split(/\r?\n/g);
2503
+ const pathLine = lines.find((line) => line.startsWith("worktree "));
2504
+ if (!pathLine)
2505
+ continue;
2506
+ const branchLine = lines.find((line) => line.startsWith("branch "));
2507
+ entries.push({
2508
+ path: pathLine.slice("worktree ".length).trim(),
2509
+ branch: branchLine ? branchLine.slice("branch ".length).trim() : null,
2510
+ detached: lines.includes("detached")
2511
+ });
2512
+ }
2513
+ return entries;
2514
+ }
2515
+ function isWorkerpalEphemeralWorktreePath(repoRoot, worktreePath) {
2516
+ const expectedPrefix = `${normalizeFsPathForComparison(join2(repoRoot, ".worktrees"))}/`;
2517
+ const normalizedPath = normalizeFsPathForComparison(worktreePath);
2518
+ if (!normalizedPath.startsWith(expectedPrefix))
2519
+ return false;
2520
+ const leaf = basename(normalizedPath);
2521
+ return /^(job|selfcheck)-.*-workerpal-[a-z0-9._-]+/i.test(leaf);
2522
+ }
2487
2523
  function resolveConfiguredDockerExecutable(env, platform = process.platform) {
2488
2524
  const configured = String(env.PUSHPALS_DOCKER_BIN_ABSOLUTE ?? env.PUSHPALS_DOCKER_BIN ?? (platform === "win32" ? "docker.exe" : "docker")).trim();
2489
2525
  return configured || (platform === "win32" ? "docker.exe" : "docker");
@@ -2531,6 +2567,80 @@ async function cleanupLingeringWorkerpalWarmContainers(opts) {
2531
2567
  removed: containerIds.length
2532
2568
  };
2533
2569
  }
2570
+ async function cleanupLingeringPushPalsGitWorktrees(opts) {
2571
+ const runCommandWithEnvFn = opts.runCommandWithEnvFn ?? runCommandWithEnv;
2572
+ const list = await runCommandWithEnvFn(["git", "worktree", "list", "--porcelain"], opts.repoRoot, opts.env);
2573
+ if (!list.ok) {
2574
+ const detail = list.stderr || list.stdout || `exit ${list.exitCode}`;
2575
+ return {
2576
+ ok: false,
2577
+ detail: `failed to inspect lingering PushPals git artifacts: ${detail}`,
2578
+ removed: 0
2579
+ };
2580
+ }
2581
+ const currentRepoPath = normalizeFsPathForComparison(opts.repoRoot);
2582
+ const removable = parseGitWorktreeListPorcelain(list.stdout).filter((entry) => {
2583
+ const normalizedPath = normalizeFsPathForComparison(entry.path);
2584
+ if (normalizedPath === currentRepoPath)
2585
+ return false;
2586
+ if (entry.branch?.startsWith(`refs/heads/${SOURCE_CONTROL_MANAGER_TEMP_BRANCH_PREFIX}`)) {
2587
+ return true;
2588
+ }
2589
+ return isWorkerpalEphemeralWorktreePath(opts.repoRoot, entry.path);
2590
+ });
2591
+ let removed = 0;
2592
+ const failures = [];
2593
+ for (const entry of removable) {
2594
+ const remove = await runCommandWithEnvFn(["git", "worktree", "remove", "--force", "--force", entry.path], opts.repoRoot, opts.env);
2595
+ if (remove.ok) {
2596
+ removed += 1;
2597
+ continue;
2598
+ }
2599
+ failures.push(`${entry.path}: ${remove.stderr || remove.stdout || `exit ${remove.exitCode}`}`);
2600
+ }
2601
+ const prune = await runCommandWithEnvFn(["git", "worktree", "prune"], opts.repoRoot, opts.env);
2602
+ if (!prune.ok) {
2603
+ failures.push(`prune: ${prune.stderr || prune.stdout || `exit ${prune.exitCode}`}`);
2604
+ }
2605
+ const deleteTempBranches = await runCommandWithEnvFn([
2606
+ "git",
2607
+ "for-each-ref",
2608
+ "--format=%(refname:short)",
2609
+ `refs/heads/${SOURCE_CONTROL_MANAGER_TEMP_BRANCH_PREFIX}`
2610
+ ], opts.repoRoot, opts.env);
2611
+ if (!deleteTempBranches.ok) {
2612
+ failures.push(`list temp branches: ${deleteTempBranches.stderr || deleteTempBranches.stdout || `exit ${deleteTempBranches.exitCode}`}`);
2613
+ } else {
2614
+ const branches = deleteTempBranches.stdout.split(/\r?\n/g).map((value) => value.trim()).filter(Boolean);
2615
+ for (const branch of branches) {
2616
+ const deleteResult = await runCommandWithEnvFn(["git", "branch", "-D", branch], opts.repoRoot, opts.env);
2617
+ if (!deleteResult.ok) {
2618
+ failures.push(`${branch}: ${deleteResult.stderr || deleteResult.stdout || `exit ${deleteResult.exitCode}`}`);
2619
+ } else {
2620
+ removed += 1;
2621
+ }
2622
+ }
2623
+ }
2624
+ if (removed === 0 && failures.length === 0) {
2625
+ return {
2626
+ ok: true,
2627
+ detail: "no lingering PushPals git artifacts found",
2628
+ removed: 0
2629
+ };
2630
+ }
2631
+ if (failures.length > 0) {
2632
+ return {
2633
+ ok: false,
2634
+ detail: `removed ${removed} lingering PushPals git artifact(s), but cleanup was incomplete: ${failures.join(" | ")}`,
2635
+ removed
2636
+ };
2637
+ }
2638
+ return {
2639
+ ok: true,
2640
+ detail: `removed ${removed} lingering PushPals git artifact(s)`,
2641
+ removed
2642
+ };
2643
+ }
2534
2644
  async function inspectDockerImageRuntimeTag(dockerExecutable, imageName, cwd, env) {
2535
2645
  const inspect = await runCommandWithEnv([
2536
2646
  dockerExecutable,
@@ -3843,8 +3953,45 @@ function formatSessionEventLine(event) {
3843
3953
  }
3844
3954
  return null;
3845
3955
  }
3956
+ function buildSessionEventReplayFingerprint(event) {
3957
+ const type = String(event.type ?? "").trim().toLowerCase();
3958
+ if (type !== "status")
3959
+ return null;
3960
+ const payload = event.payload ?? {};
3961
+ const source = String(event.from ?? payload.agentId ?? "status").trim().toLowerCase();
3962
+ const state = String(payload.state ?? "").trim().toLowerCase();
3963
+ const detail = String(payload.detail ?? "").trim().toLowerCase();
3964
+ const message = String(payload.message ?? "").trim().toLowerCase();
3965
+ return {
3966
+ source,
3967
+ fingerprint: `${type}:${source}:${state}:${detail}:${message}`
3968
+ };
3969
+ }
3970
+ function createSessionEventReplayFilter() {
3971
+ const seenEventIds = new Set;
3972
+ const lastStatusFingerprintBySource = new Map;
3973
+ return {
3974
+ shouldRender(event) {
3975
+ const eventId = String(event.id ?? "").trim();
3976
+ if (eventId) {
3977
+ if (seenEventIds.has(eventId))
3978
+ return false;
3979
+ seenEventIds.add(eventId);
3980
+ }
3981
+ const replayStatus = buildSessionEventReplayFingerprint(event);
3982
+ if (!replayStatus)
3983
+ return true;
3984
+ const previous = lastStatusFingerprintBySource.get(replayStatus.source);
3985
+ if (previous === replayStatus.fingerprint)
3986
+ return false;
3987
+ lastStatusFingerprintBySource.set(replayStatus.source, replayStatus.fingerprint);
3988
+ return true;
3989
+ }
3990
+ };
3991
+ }
3846
3992
  async function runSessionStream(serverUrl, sessionId, client, print, signal) {
3847
3993
  let cursor = 0;
3994
+ const replayFilter = createSessionEventReplayFilter();
3848
3995
  while (!signal.aborted) {
3849
3996
  try {
3850
3997
  const response = await fetchWithTimeout(`${serverUrl}/sessions/${encodeURIComponent(sessionId)}/events${buildClientTransportQuery(cursor, client)}`, {}, 15000);
@@ -3894,6 +4041,8 @@ async function runSessionStream(serverUrl, sessionId, client, print, signal) {
3894
4041
  cursor = Math.max(cursor, blockCursor, serverCursor);
3895
4042
  if (!parsed.envelope)
3896
4043
  continue;
4044
+ if (!replayFilter.shouldRender(parsed.envelope))
4045
+ continue;
3897
4046
  const line = formatSessionEventLine(parsed.envelope);
3898
4047
  if (line)
3899
4048
  print(line);
@@ -4034,6 +4183,19 @@ async function main() {
4034
4183
  console.log(`[pushpals] ${cleanup.detail} (${phase}).`);
4035
4184
  }
4036
4185
  };
4186
+ const cleanupPushPalsGitWorktreesIfNeeded = async (phase) => {
4187
+ const cleanup = await cleanupLingeringPushPalsGitWorktrees({
4188
+ repoRoot,
4189
+ env: workerpalDockerPrecheck.env
4190
+ });
4191
+ if (!cleanup.ok) {
4192
+ console.warn(`[pushpals] PushPals worktree cleanup warning (${phase}): ${cleanup.detail}`);
4193
+ return;
4194
+ }
4195
+ if (cleanup.removed > 0) {
4196
+ console.log(`[pushpals] ${cleanup.detail} (${phase}).`);
4197
+ }
4198
+ };
4037
4199
  const stopAutoStartedServices = () => {
4038
4200
  if (autoStartedServices.length === 0)
4039
4201
  return;
@@ -4056,6 +4218,7 @@ async function main() {
4056
4218
  }
4057
4219
  await stopRuntimeServicesGracefully(services);
4058
4220
  await cleanupWorkerpalWarmContainersIfNeeded("cli shutdown");
4221
+ await cleanupPushPalsGitWorktreesIfNeeded("cli shutdown");
4059
4222
  };
4060
4223
  let serverHealthy = await probeServer(serverUrl);
4061
4224
  const serverWasAlreadyHealthy = serverHealthy;
@@ -4085,6 +4248,7 @@ async function main() {
4085
4248
  };
4086
4249
  if (!serverHealthy) {
4087
4250
  await cleanupWorkerpalWarmContainersIfNeeded("startup preflight");
4251
+ await cleanupPushPalsGitWorktreesIfNeeded("startup preflight");
4088
4252
  if (!parsed.noAutoStart) {
4089
4253
  try {
4090
4254
  const startedRuntime = await autoStartRuntimeServices({
@@ -4381,8 +4545,10 @@ export {
4381
4545
  ensureWorkerpalDockerImageReady,
4382
4546
  ensureRuntimeBinaries,
4383
4547
  downloadRuntimeAssetsFromSourceTag,
4548
+ createSessionEventReplayFilter,
4384
4549
  copyTrackedRepoPath,
4385
4550
  cleanupLingeringWorkerpalWarmContainers,
4551
+ cleanupLingeringPushPalsGitWorktrees,
4386
4552
  bundledMonitoringHubNeedsRefresh,
4387
4553
  buildWorkerpalSandboxPaths,
4388
4554
  buildServiceStopCommand,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushpalsdev/cli",
3
- "version": "1.0.28",
3
+ "version": "1.0.30",
4
4
  "description": "PushPals terminal CLI for LocalBuddy -> RemoteBuddy orchestration",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -130,14 +130,51 @@ export function redactSensitiveText(value: string): string {
130
130
  }
131
131
 
132
132
  export function buildCriticRevisionIssues(
133
- critic: { score: number; mustFix: string[] } | null | undefined,
133
+ critic:
134
+ | {
135
+ score: number;
136
+ findings?: string[];
137
+ mustFix?: string[];
138
+ revisionGuidance?: string;
139
+ }
140
+ | null
141
+ | undefined,
134
142
  qualityCriticMinScore: number,
135
143
  ): string[] {
136
144
  if (!critic) return [];
137
145
  if (critic.score >= qualityCriticMinScore) return [];
138
- return [
146
+ const issues = [
139
147
  `Critic score ${critic.score.toFixed(1)} is below required threshold ${qualityCriticMinScore}.`,
140
148
  ];
149
+ const mustFix = Array.isArray(critic.mustFix) ? critic.mustFix : [];
150
+ const findings = Array.isArray(critic.findings) ? critic.findings : [];
151
+ const revisionGuidance = String(critic.revisionGuidance ?? "").trim();
152
+ const actionableItems = (mustFix.length > 0 ? mustFix : findings)
153
+ .map((entry) => toSingleLine(entry, 180))
154
+ .filter(Boolean)
155
+ .slice(0, 3);
156
+ for (const item of actionableItems) {
157
+ issues.push(mustFix.length > 0 ? `Critic must-fix: ${item}` : `Critic finding: ${item}`);
158
+ }
159
+ if (revisionGuidance) {
160
+ issues.push(`Critic revision guidance: ${toSingleLine(revisionGuidance, 220)}`);
161
+ }
162
+ return issues;
163
+ }
164
+
165
+ export function buildQualityGateRevisionIssues(
166
+ qualityIssues: string[],
167
+ critic: CriticReview | null,
168
+ qualityCriticMinScore: number,
169
+ ): string[] {
170
+ const normalizedQualityIssues = qualityIssues
171
+ .map((entry) => String(entry ?? "").trim())
172
+ .filter(Boolean);
173
+ if (!critic || critic.score >= qualityCriticMinScore) {
174
+ return [...normalizedQualityIssues];
175
+ }
176
+ const merged = [...normalizedQualityIssues, ...buildCriticRevisionIssues(critic, qualityCriticMinScore)];
177
+ return [...new Set(merged)];
141
178
  }
142
179
 
143
180
  export function resolveReviewFixCompletionBranch(
@@ -3006,9 +3043,10 @@ export async function executeJob(
3006
3043
  : executor === "openai_codex"
3007
3044
  ? await runCodexCriticReview(repo, attemptParams, quality, runtimeConfig, onLog)
3008
3045
  : await runTaskCriticReview(repo, attemptParams, quality, runtimeConfig, onLog);
3046
+ const deterministicRequiresRevision = !quality.ok;
3009
3047
  const criticRequiresRevision = Boolean(critic && critic.score < qualityCriticMinScore);
3010
3048
 
3011
- if (!criticRequiresRevision) {
3049
+ if (!deterministicRequiresRevision && !criticRequiresRevision) {
3012
3050
  if (critic) {
3013
3051
  onLog?.(
3014
3052
  "stdout",
@@ -3018,10 +3056,11 @@ export async function executeJob(
3018
3056
  return result;
3019
3057
  }
3020
3058
 
3021
- const issues: string[] = [];
3022
- if (criticRequiresRevision && critic) {
3023
- issues.push(...buildCriticRevisionIssues(critic, qualityCriticMinScore));
3024
- }
3059
+ const issues = buildQualityGateRevisionIssues(
3060
+ quality.issues,
3061
+ critic,
3062
+ qualityCriticMinScore,
3063
+ );
3025
3064
  const issueSummary = issues.map((entry) => toSingleLine(entry, 180)).join(" | ");
3026
3065
  if (revisionAttempt >= qualityMaxAutoRevisions) {
3027
3066
  if (qualitySoftPassOnExhausted) {