@pushpalsdev/cli 1.1.7 → 1.1.9

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.
@@ -1644,6 +1644,7 @@ var WINDOWS_TASKKILL_TIMEOUT_MS2 = 5000;
1644
1644
  var RUNTIME_BINARY_DOWNLOAD_ATTEMPTS = 3;
1645
1645
  var DEFAULT_STARTUP_GIT_PROBE_TIMEOUT_MS = 5000;
1646
1646
  var DEFAULT_STARTUP_GIT_REMOTE_TIMEOUT_MS = 1e4;
1647
+ var DEFAULT_EMBEDDED_SERVICE_LAUNCH_WARN_MS = 5000;
1647
1648
  var EMBEDDED_SERVICE_RESTART_MAX_ATTEMPTS = 4;
1648
1649
  var WORKERPAL_STARTUP_READINESS_PROBE_MAX_MS = 15000;
1649
1650
  var EMBEDDED_RUNTIME_SAFETY_CAP_DISABLE_ENV = "PUSHPALS_DISABLE_EMBEDDED_SAFETY_CAPS";
@@ -1677,6 +1678,13 @@ function formatRuntimeStartupTimingSummary(input) {
1677
1678
  const detail = typeof input.detail === "string" && input.detail.trim() ? ` detail=${input.detail.trim()}` : "";
1678
1679
  return `[pushpals] startup timing summary: outcome=${input.outcome} ` + `total=${Math.max(0, Math.floor(input.totalDurationMs))}ms${detail}` + (phaseSummary ? ` ${phaseSummary}` : "");
1679
1680
  }
1681
+ function formatEmbeddedServiceLaunchDelayWarning(input) {
1682
+ const durationMs = Math.max(0, Math.floor(Number(input.durationMs) || 0));
1683
+ const serviceName = String(input.serviceName || "service");
1684
+ const platform = String(input.platform ?? process.platform);
1685
+ const windowsHint = platform === "win32" ? " On Windows, first-run standalone binaries can be delayed while security software scans them." : "";
1686
+ return `[pushpals] Embedded ${serviceName} process launch took ${durationMs}ms; startup is continuing.` + windowsHint;
1687
+ }
1680
1688
  function describeWorkerExecutionReadiness(opts) {
1681
1689
  const onlineWorkers = Math.max(0, Math.floor(opts.onlineWorkers));
1682
1690
  const idleWorkers = Math.max(0, Math.floor(opts.idleWorkers));
@@ -4481,10 +4489,34 @@ async function autoStartRuntimeServices(opts) {
4481
4489
  };
4482
4490
  };
4483
4491
  const launchService = (name, command, launchOpts) => {
4484
- return serviceManager.startService(buildManagedServiceSpec(name, command, launchOpts));
4492
+ const launchStartedAt = Date.now();
4493
+ const service = serviceManager.startService(buildManagedServiceSpec(name, command, launchOpts));
4494
+ const launchDurationMs = Date.now() - launchStartedAt;
4495
+ if (launchDurationMs >= DEFAULT_EMBEDDED_SERVICE_LAUNCH_WARN_MS) {
4496
+ const warning = formatEmbeddedServiceLaunchDelayWarning({
4497
+ serviceName: name,
4498
+ durationMs: launchDurationMs,
4499
+ platform: process.platform
4500
+ });
4501
+ console.warn(warning);
4502
+ appendRuntimeServicesLogLine(runtimeServicesLogPath, warning);
4503
+ }
4504
+ return service;
4485
4505
  };
4486
4506
  const replaceService = (name, command, launchOpts) => {
4487
- return serviceManager.replaceService(buildManagedServiceSpec(name, command, launchOpts));
4507
+ const launchStartedAt = Date.now();
4508
+ const service = serviceManager.replaceService(buildManagedServiceSpec(name, command, launchOpts));
4509
+ const launchDurationMs = Date.now() - launchStartedAt;
4510
+ if (launchDurationMs >= DEFAULT_EMBEDDED_SERVICE_LAUNCH_WARN_MS) {
4511
+ const warning = formatEmbeddedServiceLaunchDelayWarning({
4512
+ serviceName: name,
4513
+ durationMs: launchDurationMs,
4514
+ platform: process.platform
4515
+ });
4516
+ console.warn(warning);
4517
+ appendRuntimeServicesLogLine(runtimeServicesLogPath, warning);
4518
+ }
4519
+ return service;
4488
4520
  };
4489
4521
  const sandboxPaths = buildWorkerpalSandboxPaths(runtimeRoot);
4490
4522
  const remoteBuddyFallbackBun = process.platform === "win32" ? resolveEmbeddedBunExecutableFromEnv(runtimeEnv, process.platform, process.execPath) : "";
@@ -5905,6 +5937,7 @@ export {
5905
5937
  formatTimestampedCliLine,
5906
5938
  formatSessionEventLine,
5907
5939
  formatRuntimeStartupTimingSummary,
5940
+ formatEmbeddedServiceLaunchDelayWarning,
5908
5941
  formatEmbeddedRuntimeHealthLines2 as formatEmbeddedRuntimeHealthLines,
5909
5942
  extractRemoteBuddySessionConsumerHealth,
5910
5943
  extractRemoteBuddyAutonomousEngineState,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushpalsdev/cli",
3
- "version": "1.1.7",
3
+ "version": "1.1.9",
4
4
  "description": "PushPals terminal CLI for LocalBuddy -> RemoteBuddy orchestration",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -2245,11 +2245,14 @@ export function collectQualityGateValidationCommands(params: {
2245
2245
  planning: TaskExecutePlanning;
2246
2246
  changedTestPaths: string[];
2247
2247
  isTestTask: boolean;
2248
+ repo?: string;
2249
+ changedPaths?: string[];
2248
2250
  }): {
2249
2251
  commandsToRun: string[];
2250
2252
  requiredRunnableSteps: string[];
2251
2253
  plannerRunnableSteps: string[];
2252
2254
  fallbackValidationSteps: string[];
2255
+ inferredRepoNativeValidationSteps: string[];
2253
2256
  } {
2254
2257
  const requiredRunnableSteps = runnableValidationCommandsFromSteps(
2255
2258
  params.planning.requiredValidationSteps,
@@ -2266,15 +2269,20 @@ export function collectQualityGateValidationCommands(params: {
2266
2269
  params.changedTestPaths,
2267
2270
  )
2268
2271
  : [];
2272
+ const inferredRepoNativeValidationSteps = params.repo
2273
+ ? inferRepoNativeValidationCommands(params.repo, params.changedPaths ?? [])
2274
+ : [];
2269
2275
  const commandsToRun = dedupeValidationCommands(
2270
2276
  requiredRunnableSteps,
2271
2277
  plannerRunnableSteps.length > 0 ? plannerRunnableSteps : fallbackValidationSteps,
2278
+ inferredRepoNativeValidationSteps,
2272
2279
  ).slice(0, 16);
2273
2280
  return {
2274
2281
  commandsToRun,
2275
2282
  requiredRunnableSteps,
2276
2283
  plannerRunnableSteps,
2277
2284
  fallbackValidationSteps,
2285
+ inferredRepoNativeValidationSteps,
2278
2286
  };
2279
2287
  }
2280
2288
 
@@ -2416,6 +2424,114 @@ function hasBalancedPositiveNegativeAssertions(paths: string[], repo: string): b
2416
2424
  return positiveAssertions > 0 && negativeAssertions > 0;
2417
2425
  }
2418
2426
 
2427
+ function asRecord(value: unknown): Record<string, unknown> | null {
2428
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
2429
+ return value as Record<string, unknown>;
2430
+ }
2431
+
2432
+ function changedPathMentionsGuidance(pathPattern: RegExp, guidance: string): boolean {
2433
+ return pathPattern.test(guidance);
2434
+ }
2435
+
2436
+ export function collectPrePublishHygieneIssues(params: {
2437
+ repo: string;
2438
+ changedPaths: string[];
2439
+ instruction: string;
2440
+ targetPath?: string;
2441
+ planning: TaskExecutePlanning;
2442
+ reviewAgent?: Record<string, unknown> | null;
2443
+ }): string[] {
2444
+ const changedPaths = params.changedPaths.map((path) => path.replace(/\\/g, "/"));
2445
+ const changedPathSet = new Set(changedPaths);
2446
+ const guidance = [
2447
+ params.instruction,
2448
+ params.targetPath ?? "",
2449
+ ...(params.planning.targetPaths ?? []),
2450
+ ...(params.planning.scope.writeGlobs ?? []),
2451
+ ...(params.planning.acceptanceCriteria ?? []),
2452
+ ...(params.planning.validationSteps ?? []),
2453
+ ...((params.reviewAgent?.reviewerFindings as string[] | undefined) ?? []),
2454
+ ]
2455
+ .join("\n")
2456
+ .toLowerCase();
2457
+ const issues: string[] = [];
2458
+
2459
+ if (
2460
+ changedPathSet.has(".gitignore") &&
2461
+ !changedPathMentionsGuidance(/\b(gitignore|ignore file|node_modules|dependency cache)\b/i, guidance)
2462
+ ) {
2463
+ issues.push(
2464
+ "modified .gitignore without task or reviewer guidance requesting ignore-policy changes.",
2465
+ );
2466
+ }
2467
+
2468
+ if (changedPathSet.has("tests/reactNativeMock.ts")) {
2469
+ const changedTestPaths = changedPaths.filter((path) => isAssertionCoverageTestPath(path));
2470
+ const hasConsumerInChangedTests = changedTestPaths.some((rel) => {
2471
+ try {
2472
+ return /reactNativeMock/i.test(readFileSync(resolve(params.repo, rel), "utf8"));
2473
+ } catch {
2474
+ return false;
2475
+ }
2476
+ });
2477
+ const explicitlyRequested = changedPathMentionsGuidance(/reactnativemock|react native mock/i, guidance);
2478
+ if (!hasConsumerInChangedTests && !explicitlyRequested) {
2479
+ issues.push(
2480
+ "changed tests/reactNativeMock.ts without a changed test importing it or explicit reviewer guidance.",
2481
+ );
2482
+ }
2483
+ }
2484
+
2485
+ if (changedPaths.some((path) => /(^|\/)node_modules(\/|$)/i.test(path))) {
2486
+ issues.push("attempted to publish node_modules changes; dependency installs must not become PR content.");
2487
+ }
2488
+
2489
+ return Array.from(new Set(issues));
2490
+ }
2491
+
2492
+ export function inferRepoNativeValidationCommands(repo: string, changedPaths: string[]): string[] {
2493
+ const packageJsonPath = resolve(repo, "package.json");
2494
+ if (!existsSync(packageJsonPath)) return [];
2495
+
2496
+ let packageJson: {
2497
+ scripts?: Record<string, unknown>;
2498
+ dependencies?: Record<string, unknown>;
2499
+ devDependencies?: Record<string, unknown>;
2500
+ } = {};
2501
+ try {
2502
+ packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
2503
+ } catch {
2504
+ return [];
2505
+ }
2506
+
2507
+ const scripts = packageJson.scripts ?? {};
2508
+ const dependencies = {
2509
+ ...(packageJson.dependencies ?? {}),
2510
+ ...(packageJson.devDependencies ?? {}),
2511
+ };
2512
+ const normalizedPaths = changedPaths.map((path) => path.replace(/\\/g, "/"));
2513
+ const hasNonDocChange = normalizedPaths.some((path) => !/\.(?:md|mdx|txt)$/i.test(path));
2514
+ const hasTsChange = normalizedPaths.some((path) => /\.[cm]?tsx?$/i.test(path));
2515
+ const commands: string[] = [];
2516
+
2517
+ if (hasTsChange) {
2518
+ if (typeof scripts.typecheck === "string" && scripts.typecheck.trim()) {
2519
+ commands.push("bun run typecheck");
2520
+ } else if (
2521
+ existsSync(resolve(repo, "tsconfig.json")) ||
2522
+ Object.prototype.hasOwnProperty.call(dependencies, "typescript")
2523
+ ) {
2524
+ commands.push("bun x tsc --noEmit");
2525
+ }
2526
+ }
2527
+
2528
+ if (hasNonDocChange && typeof scripts.lint === "string" && scripts.lint.trim()) {
2529
+ commands.push("bun run lint");
2530
+ }
2531
+
2532
+ return dedupeValidationCommands(commands).slice(0, 4);
2533
+ }
2534
+
2419
2535
  async function runDeterministicQualityGate(
2420
2536
  repo: string,
2421
2537
  params: Record<string, unknown>,
@@ -2485,6 +2601,16 @@ async function runDeterministicQualityGate(
2485
2601
  if (!statusResult.ok) {
2486
2602
  addScopeIssue("could not evaluate changed paths from git status.");
2487
2603
  }
2604
+ for (const issue of collectPrePublishHygieneIssues({
2605
+ repo,
2606
+ changedPaths,
2607
+ instruction,
2608
+ targetPath,
2609
+ planning,
2610
+ reviewAgent: asRecord(params.reviewAgent ?? params.review_agent),
2611
+ })) {
2612
+ addScopeIssue(issue);
2613
+ }
2488
2614
  for (const issue of collectWriteScopeIssuesFromChangedPaths(changedPaths, planning)) {
2489
2615
  addScopeIssue(issue);
2490
2616
  }
@@ -2525,6 +2651,8 @@ async function runDeterministicQualityGate(
2525
2651
  planning,
2526
2652
  changedTestPaths,
2527
2653
  isTestTask,
2654
+ repo,
2655
+ changedPaths,
2528
2656
  });
2529
2657
  const validationRuns: ValidationExecutionResult[] = [];
2530
2658
  const outputPolicy = outputPolicyForRuntime(runtimeConfig);
@@ -4478,54 +4606,104 @@ export async function resumePreparedMergeConflictRebase(
4478
4606
  };
4479
4607
  }
4480
4608
 
4481
- let rebaseContinue = await git(repo, ["-c", "core.editor=true", "rebase", "--continue"]);
4482
- let continueOutput = combinedGitOutput(rebaseContinue);
4483
- if (!rebaseContinue.ok && isRebaseEditorPromptOutput(continueOutput)) {
4484
- rebaseContinue = await git(repo, ["-c", "core.editor=true", "rebase", "--continue"]);
4485
- continueOutput = combinedGitOutput(rebaseContinue);
4486
- }
4487
- if (!rebaseContinue.ok) {
4488
- const continuingSequencer = await activeGitOperation(repo);
4489
- if (continuingSequencer === "rebase") {
4490
- const nextUnresolved = await git(repo, ["diff", "--name-only", "--diff-filter=U"]);
4491
- if (nextUnresolved.ok) {
4492
- const nextPaths = parseChangedPathsFromNameOnlyOutput(nextUnresolved.stdout);
4493
- if (nextPaths.length > 0) {
4494
- onLog?.(
4495
- "stdout",
4496
- `[MergeConflict] Rebase advanced into another conflicted commit with ${nextPaths.length} unresolved file(s); rerunning the resolver on updated sandbox state.`,
4497
- );
4609
+ const maxContinuationPasses = Math.max(1, MAX_MERGE_CONFLICT_RESOLUTION_PASSES);
4610
+ let lastContinueOutput = "";
4611
+ for (let pass = 1; pass <= maxContinuationPasses; pass += 1) {
4612
+ let rebaseContinue = await git(repo, ["-c", "core.editor=true", "rebase", "--continue"]);
4613
+ let continueOutput = combinedGitOutput(rebaseContinue);
4614
+ if (!rebaseContinue.ok && isRebaseEditorPromptOutput(continueOutput)) {
4615
+ rebaseContinue = await git(repo, ["-c", "core.editor=true", "rebase", "--continue"]);
4616
+ continueOutput = combinedGitOutput(rebaseContinue);
4617
+ }
4618
+ lastContinueOutput = continueOutput;
4619
+
4620
+ if (!rebaseContinue.ok) {
4621
+ if (/no rebase in progress/i.test(continueOutput)) {
4622
+ onLog?.(
4623
+ "stdout",
4624
+ "[MergeConflict] Prepared rebase was already complete after continuation.",
4625
+ );
4626
+ return { ok: true, resumed: true, sequencer: null };
4627
+ }
4628
+ if (/no changes - did you forget to use 'git add'|nothing to commit/i.test(continueOutput)) {
4629
+ const rebaseSkip = await git(repo, ["rebase", "--skip"]);
4630
+ const skipOutput = combinedGitOutput(rebaseSkip);
4631
+ lastContinueOutput = skipOutput || continueOutput;
4632
+ if (!rebaseSkip.ok && !isRebaseConflictOutput(skipOutput)) {
4498
4633
  return {
4499
- ok: true,
4500
- resumed: true,
4501
- sequencer: "rebase",
4502
- detail: `rebase advanced into another conflicted commit with ${nextPaths.length} unresolved file(s)`,
4503
- advancedToNextConflict: true,
4634
+ ok: false,
4635
+ error: `Failed to skip empty prepared merge-conflict rebase commit: ${skipOutput}`,
4504
4636
  };
4505
4637
  }
4638
+ } else {
4639
+ const continuingSequencer = await activeGitOperation(repo);
4640
+ if (continuingSequencer === "rebase") {
4641
+ const nextUnresolved = await git(repo, ["diff", "--name-only", "--diff-filter=U"]);
4642
+ if (nextUnresolved.ok) {
4643
+ const nextPaths = parseChangedPathsFromNameOnlyOutput(nextUnresolved.stdout);
4644
+ if (nextPaths.length > 0) {
4645
+ onLog?.(
4646
+ "stdout",
4647
+ `[MergeConflict] Rebase advanced into another conflicted commit with ${nextPaths.length} unresolved file(s); rerunning the resolver on updated sandbox state.`,
4648
+ );
4649
+ return {
4650
+ ok: true,
4651
+ resumed: true,
4652
+ sequencer: "rebase",
4653
+ detail: `rebase advanced into another conflicted commit with ${nextPaths.length} unresolved file(s)`,
4654
+ advancedToNextConflict: true,
4655
+ };
4656
+ }
4657
+ }
4658
+ }
4659
+ return {
4660
+ ok: false,
4661
+ error: `Failed to continue prepared merge-conflict rebase: ${continueOutput}`,
4662
+ };
4663
+ }
4664
+ }
4665
+
4666
+ const remainingSequencer = await activeGitOperation(repo);
4667
+ if (!remainingSequencer) {
4668
+ onLog?.(
4669
+ "stdout",
4670
+ "[MergeConflict] Auto-continued the prepared rebase after the executor returned with no unresolved conflicts.",
4671
+ );
4672
+ return { ok: true, resumed: true, sequencer: null };
4673
+ }
4674
+ if (remainingSequencer !== "rebase") {
4675
+ return { ok: true, resumed: true, sequencer: remainingSequencer };
4676
+ }
4677
+
4678
+ const nextUnresolved = await git(repo, ["diff", "--name-only", "--diff-filter=U"]);
4679
+ if (nextUnresolved.ok) {
4680
+ const nextPaths = parseChangedPathsFromNameOnlyOutput(nextUnresolved.stdout);
4681
+ if (nextPaths.length > 0) {
4682
+ onLog?.(
4683
+ "stdout",
4684
+ `[MergeConflict] Rebase advanced into another conflicted commit with ${nextPaths.length} unresolved file(s); rerunning the resolver on updated sandbox state.`,
4685
+ );
4686
+ return {
4687
+ ok: true,
4688
+ resumed: true,
4689
+ sequencer: "rebase",
4690
+ detail: `rebase advanced into another conflicted commit with ${nextPaths.length} unresolved file(s)`,
4691
+ advancedToNextConflict: true,
4692
+ };
4506
4693
  }
4507
4694
  }
4508
- return {
4509
- ok: false,
4510
- error: `Failed to continue prepared merge-conflict rebase: ${continueOutput}`,
4511
- };
4512
- }
4513
4695
 
4514
- const remainingSequencer = await activeGitOperation(repo);
4515
- if (!remainingSequencer) {
4516
4696
  onLog?.(
4517
4697
  "stdout",
4518
- "[MergeConflict] Auto-continued the prepared rebase after the executor returned with no unresolved conflicts.",
4698
+ `[MergeConflict] Rebase still active after continuation pass ${pass}/${maxContinuationPasses}; trying another non-interactive continue.`,
4519
4699
  );
4520
4700
  }
4701
+
4521
4702
  return {
4522
- ok: true,
4523
- resumed: true,
4524
- sequencer: remainingSequencer,
4525
- detail:
4526
- remainingSequencer === "rebase"
4527
- ? "rebase advanced but another continuation step is still required"
4528
- : undefined,
4703
+ ok: false,
4704
+ error:
4705
+ `Prepared merge-conflict rebase remained active after ${maxContinuationPasses} continuation pass(es).` +
4706
+ (lastContinueOutput ? ` Last output: ${lastContinueOutput}` : ""),
4529
4707
  };
4530
4708
  }
4531
4709