@pushpalsdev/cli 1.0.29 → 1.0.31

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.
@@ -1217,6 +1217,15 @@ var DEFAULT_RUNTIME_BOOT_TIMEOUT_MS = 90000;
1217
1217
  var DEFAULT_RUNTIME_BOOT_POLL_MS = 1000;
1218
1218
  var DEFAULT_SERVER_BOOT_TIMEOUT_MS = 20000;
1219
1219
  var DEFAULT_SERVICE_STABILITY_GRACE_MS = 4000;
1220
+ var WORKERPAL_STARTUP_READINESS_PROBE_MAX_MS = 15000;
1221
+ var EMBEDDED_RUNTIME_SAFETY_CAP_DISABLE_ENV = "PUSHPALS_DISABLE_EMBEDDED_SAFETY_CAPS";
1222
+ var EMBEDDED_RUNTIME_WINDOWS_SAFETY_CAPS = {
1223
+ REMOTEBUDDY_WORKERPAL_STARTUP_TIMEOUT_MS: "120000",
1224
+ WORKERPALS_DOCKER_AGENT_STARTUP_TIMEOUT_MS: "90000",
1225
+ WORKERPALS_SKIP_DOCKER_SELF_CHECK: "1",
1226
+ WORKERPALS_DOCKER_WARM_MEMORY_MB: "1024",
1227
+ WORKERPALS_DOCKER_WARM_CPUS: "1"
1228
+ };
1220
1229
  var GITHUB_OWNER = "PushPalsDev";
1221
1230
  var GITHUB_REPO = "pushpals";
1222
1231
  var GITHUB_API_URL = `https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}`;
@@ -1235,6 +1244,98 @@ function formatTimestampedCliLine(line, at = new Date) {
1235
1244
  }
1236
1245
  return `[${at.toISOString()}]${text}`;
1237
1246
  }
1247
+ function formatRuntimeStartupTimingSummary(input) {
1248
+ const phaseSummary = input.phases.map((phase) => `${phase.name}=${Math.max(0, Math.floor(phase.durationMs))}ms(${phase.status.trim() || "unknown"})`).join(" ");
1249
+ const detail = typeof input.detail === "string" && input.detail.trim() ? ` detail=${input.detail.trim()}` : "";
1250
+ return `[pushpals] startup timing summary: outcome=${input.outcome} ` + `total=${Math.max(0, Math.floor(input.totalDurationMs))}ms${detail}` + (phaseSummary ? ` ${phaseSummary}` : "");
1251
+ }
1252
+ function describeWorkerExecutionReadiness(opts) {
1253
+ const onlineWorkers = Math.max(0, Math.floor(opts.onlineWorkers));
1254
+ const idleWorkers = Math.max(0, Math.floor(opts.idleWorkers));
1255
+ if (idleWorkers > 0) {
1256
+ return {
1257
+ state: "ready",
1258
+ detail: `${idleWorkers} idle / ${onlineWorkers} online`
1259
+ };
1260
+ }
1261
+ if (onlineWorkers > 0) {
1262
+ return {
1263
+ state: "warming",
1264
+ detail: `${idleWorkers} idle / ${onlineWorkers} online`,
1265
+ action: "Wait for WorkerPal warmup or active jobs to finish, then retry /status or send the request again."
1266
+ };
1267
+ }
1268
+ if (!opts.autoSpawnWorkerpals) {
1269
+ return {
1270
+ state: "blocked",
1271
+ detail: "No online WorkerPals are reported and auto-spawn is disabled.",
1272
+ action: "Start a WorkerPals backend manually or enable RemoteBuddy auto-spawn."
1273
+ };
1274
+ }
1275
+ if (opts.requireDocker && opts.dockerPrecheck?.status === "failed") {
1276
+ return {
1277
+ state: "blocked",
1278
+ detail: `Docker-backed WorkerPal auto-spawn is unavailable: ${opts.dockerPrecheck.detail}`,
1279
+ action: "Start Docker Desktop or the Docker daemon, then retry startup or rerun /status."
1280
+ };
1281
+ }
1282
+ return {
1283
+ state: "warming",
1284
+ detail: "No online WorkerPals are reported yet.",
1285
+ action: "Wait for WorkerPal auto-spawn/warmup to finish, then rerun /status."
1286
+ };
1287
+ }
1288
+ function formatWorkerExecutionReadinessLines(readiness) {
1289
+ const lines = [
1290
+ `[pushpals] workerExecution=${readiness.state} detail=${readiness.detail}`
1291
+ ];
1292
+ if (readiness.action) {
1293
+ lines.push(`[pushpals] workerExecutionAction=${readiness.action}`);
1294
+ }
1295
+ return lines;
1296
+ }
1297
+ function summarizeWorkerStatusRows(workers) {
1298
+ const onlineWorkers = workers.filter((worker) => Boolean(worker?.isOnline) && String(worker?.status ?? "").trim().toLowerCase() !== "offline");
1299
+ const idleWorkers = onlineWorkers.filter((worker) => Number(worker?.activeJobCount ?? 0) <= 0);
1300
+ return {
1301
+ onlineWorkers: onlineWorkers.length,
1302
+ idleWorkers: idleWorkers.length
1303
+ };
1304
+ }
1305
+ async function resolveWorkerExecutionReadiness(opts) {
1306
+ let workers = [];
1307
+ try {
1308
+ workers = await (opts.fetchWorkersFn ?? fetchWorkerStatusRows)(opts.serverUrl, opts.ttlMs);
1309
+ } catch (error) {
1310
+ return {
1311
+ state: "blocked",
1312
+ detail: `Unable to query WorkerPal status: ${String(error)}`,
1313
+ action: "Check runtime connectivity, then retry /status or restart the runtime."
1314
+ };
1315
+ }
1316
+ const { onlineWorkers, idleWorkers } = summarizeWorkerStatusRows(workers);
1317
+ let dockerPrecheck = opts.dockerPrecheck ?? null;
1318
+ const shouldProbeDockerAvailability = onlineWorkers === 0 && opts.autoSpawnWorkerpals && opts.dockerEnabled && opts.requireDocker && !dockerPrecheck && typeof opts.repoRoot === "string" && opts.repoRoot.trim().length > 0 && typeof opts.runtimeRoot === "string" && opts.runtimeRoot.trim().length > 0 && typeof opts.preflightUsesEmbeddedRuntime === "boolean";
1319
+ if (shouldProbeDockerAvailability) {
1320
+ dockerPrecheck = await (opts.precheckDockerAvailabilityFn ?? precheckWorkerpalDockerAvailability)({
1321
+ repoRoot: opts.repoRoot,
1322
+ runtimeRoot: opts.runtimeRoot,
1323
+ preflightUsesEmbeddedRuntime: opts.preflightUsesEmbeddedRuntime,
1324
+ autoSpawnWorkerpals: opts.autoSpawnWorkerpals,
1325
+ dockerEnabled: opts.dockerEnabled,
1326
+ requireDocker: opts.requireDocker,
1327
+ sessionId: opts.sessionId,
1328
+ baseEnv: opts.baseEnv
1329
+ });
1330
+ }
1331
+ return describeWorkerExecutionReadiness({
1332
+ autoSpawnWorkerpals: opts.autoSpawnWorkerpals,
1333
+ requireDocker: opts.dockerEnabled && opts.requireDocker,
1334
+ dockerPrecheck,
1335
+ onlineWorkers,
1336
+ idleWorkers
1337
+ });
1338
+ }
1238
1339
  function normalizeCliInteractiveMessage(input) {
1239
1340
  const trimmed = String(input ?? "").trim();
1240
1341
  const command = ASK_REMOTE_BUDDY_COMMAND.toLowerCase();
@@ -1792,8 +1893,15 @@ function copyBundledRuntimeAssets(runtimeRoot, force = true) {
1792
1893
  copyRuntimeAssetBundle(bundledSource, runtimeRoot, force);
1793
1894
  return true;
1794
1895
  }
1896
+ function hasSeededRuntimePreflightAssets(runtimeRoot) {
1897
+ const protocolSchemasDir = join2(runtimeRoot, "protocol", "schemas");
1898
+ const hasProtocolSchemas = existsSync4(join2(protocolSchemasDir, "envelope.schema.json")) && existsSync4(join2(protocolSchemasDir, "events.schema.json"));
1899
+ return existsSync4(join2(runtimeRoot, ".env.example")) && existsSync4(join2(runtimeRoot, "vision.example.md")) && existsSync4(join2(runtimeRoot, "configs", "default.toml")) && existsSync4(join2(runtimeRoot, "prompts")) && hasProtocolSchemas && isCompleteWorkerpalSandboxRoot(join2(runtimeRoot, "sandbox"));
1900
+ }
1795
1901
  function seedRuntimePreflightAssets(runtimeRoot) {
1796
- copyBundledRuntimeAssets(runtimeRoot, false);
1902
+ if (!hasSeededRuntimePreflightAssets(runtimeRoot)) {
1903
+ copyBundledRuntimeAssets(runtimeRoot, false);
1904
+ }
1797
1905
  writeTextFileIfMissing(join2(runtimeRoot, ".env"), `# Local PushPals runtime environment
1798
1906
  `);
1799
1907
  const localExamplePath = join2(runtimeRoot, "configs", "local.example.toml");
@@ -1946,12 +2054,19 @@ function cleanupLegacyRuntimeBinaryLayouts(runtimeRoot, platformKey, activeBinDi
1946
2054
  function buildEmbeddedRuntimeEnv(baseEnv, opts) {
1947
2055
  const env = normalizeChildProcessEnv(baseEnv);
1948
2056
  const useRuntimeConfig = opts.useRuntimeConfig !== false;
2057
+ const platform = opts.platform ?? process.platform;
1949
2058
  const inherited = { ...env };
1950
2059
  if (!useRuntimeConfig) {
1951
2060
  delete inherited.PUSHPALS_CONFIG_DIR_OVERRIDE;
1952
2061
  delete inherited.PUSHPALS_WORKERPALS_SANDBOX_ROOT;
1953
2062
  delete inherited.PUSHPALS_RUNTIME_TAG;
1954
2063
  }
2064
+ const disableEmbeddedSafetyCaps = parseBooleanFlag(env[EMBEDDED_RUNTIME_SAFETY_CAP_DISABLE_ENV]) === true;
2065
+ const shouldApplyEmbeddedWindowsSafetyCaps = useRuntimeConfig && platform === "win32" && !disableEmbeddedSafetyCaps;
2066
+ const embeddedWindowsSafetyCaps = shouldApplyEmbeddedWindowsSafetyCaps ? Object.fromEntries(Object.entries(EMBEDDED_RUNTIME_WINDOWS_SAFETY_CAPS).filter(([key]) => {
2067
+ const existing = env[key];
2068
+ return typeof existing !== "string" || existing.trim().length === 0;
2069
+ })) : {};
1955
2070
  return {
1956
2071
  ...inherited,
1957
2072
  PUSHPALS_REPO_ROOT_OVERRIDE: opts.repoRoot,
@@ -1966,12 +2081,23 @@ function buildEmbeddedRuntimeEnv(baseEnv, opts) {
1966
2081
  },
1967
2082
  PUSHPALS_PROTOCOL_SCHEMAS_DIR: join2(opts.runtimeRoot, "protocol", "schemas"),
1968
2083
  ...typeof opts.sessionId === "string" && opts.sessionId.trim() ? { PUSHPALS_SESSION_ID: opts.sessionId.trim() } : {},
2084
+ ...embeddedWindowsSafetyCaps,
1969
2085
  ...typeof env.PUSHPALS_GIT_BIN === "string" && env.PUSHPALS_GIT_BIN.trim() ? { PUSHPALS_GIT_BIN: env.PUSHPALS_GIT_BIN.trim() } : {},
1970
2086
  ...typeof env.PUSHPALS_GIT_BIN_ABSOLUTE === "string" && env.PUSHPALS_GIT_BIN_ABSOLUTE.trim() ? { PUSHPALS_GIT_BIN_ABSOLUTE: env.PUSHPALS_GIT_BIN_ABSOLUTE.trim() } : {},
1971
2087
  ...typeof env.PUSHPALS_DOCKER_BIN === "string" && env.PUSHPALS_DOCKER_BIN.trim() ? { PUSHPALS_DOCKER_BIN: env.PUSHPALS_DOCKER_BIN.trim() } : {},
1972
2088
  ...typeof env.PUSHPALS_DOCKER_BIN_ABSOLUTE === "string" && env.PUSHPALS_DOCKER_BIN_ABSOLUTE.trim() ? { PUSHPALS_DOCKER_BIN_ABSOLUTE: env.PUSHPALS_DOCKER_BIN_ABSOLUTE.trim() } : {}
1973
2089
  };
1974
2090
  }
2091
+ function parseBooleanFlag(raw) {
2092
+ const normalized = String(raw ?? "").trim().toLowerCase();
2093
+ if (!normalized)
2094
+ return null;
2095
+ if (["1", "true", "yes", "on", "y"].includes(normalized))
2096
+ return true;
2097
+ if (["0", "false", "no", "off", "n"].includes(normalized))
2098
+ return false;
2099
+ return null;
2100
+ }
1975
2101
  function normalizeChildProcessEnv(baseEnv, platform = process.platform) {
1976
2102
  const env = {};
1977
2103
  for (const [key, value] of Object.entries(baseEnv)) {
@@ -2840,6 +2966,12 @@ async function precheckWorkerpalDockerAvailability(opts) {
2840
2966
  function resolveWorkerpalCapacityTimeoutMs(config) {
2841
2967
  return Math.max(config.remotebuddy.waitForWorkerpalMs, config.remotebuddy.workerpalStartupTimeoutMs, config.remotebuddy.workerpalDocker ? config.workerpals.dockerAgentStartupTimeoutMs + 15000 : 0, 1e4);
2842
2968
  }
2969
+ function resolveWorkerpalStartupReadinessProbeTimeoutMs(config) {
2970
+ return Math.max(5000, Math.min(resolveWorkerpalCapacityTimeoutMs(config), WORKERPAL_STARTUP_READINESS_PROBE_MAX_MS));
2971
+ }
2972
+ function shouldRunEmbeddedRuntimeStartupPrechecks(opts) {
2973
+ return !opts.serverHealthy && !opts.noAutoStart;
2974
+ }
2843
2975
  async function checkGitRemoteConfigured(repoRoot, remote, env) {
2844
2976
  const normalizedRemote = String(remote ?? "").trim();
2845
2977
  if (!normalizedRemote) {
@@ -3193,15 +3325,14 @@ async function waitForWorkerpalCapacity(opts) {
3193
3325
  let lastObservedOnline = 0;
3194
3326
  while (Date.now() < deadline) {
3195
3327
  const workers = await (opts.fetchWorkersFn ?? fetchWorkerStatusRows)(opts.serverUrl, opts.ttlMs);
3196
- const onlineWorkers = workers.filter((worker) => Boolean(worker?.isOnline) && String(worker?.status ?? "").trim().toLowerCase() !== "offline");
3197
- const idleWorkers = onlineWorkers.filter((worker) => Number(worker?.activeJobCount ?? 0) <= 0);
3198
- if (onlineWorkers.length > 0) {
3199
- lastObservedOnline = Math.max(lastObservedOnline, onlineWorkers.length);
3328
+ const summary = summarizeWorkerStatusRows(workers);
3329
+ if (summary.onlineWorkers > 0) {
3330
+ lastObservedOnline = Math.max(lastObservedOnline, summary.onlineWorkers);
3200
3331
  }
3201
- if (idleWorkers.length > 0) {
3332
+ if (summary.idleWorkers > 0) {
3202
3333
  return {
3203
3334
  ok: true,
3204
- detail: `${idleWorkers.length} idle / ${onlineWorkers.length} online`
3335
+ detail: `${summary.idleWorkers} idle / ${summary.onlineWorkers} online`
3205
3336
  };
3206
3337
  }
3207
3338
  await (opts.sleepFn ?? Bun.sleep)(DEFAULT_RUNTIME_BOOT_POLL_MS);
@@ -3324,6 +3455,29 @@ async function autoStartRuntimeServices(opts) {
3324
3455
  applyResolvedGitBinaryToRuntimeEnv(runtimeEnv, resolvedGitBinary);
3325
3456
  }
3326
3457
  const services = [];
3458
+ const startupStartedAt = Date.now();
3459
+ const startupPhases = [];
3460
+ const recordStartupPhase = (name, startedAt, status) => {
3461
+ startupPhases.push({
3462
+ name,
3463
+ durationMs: Math.max(0, Date.now() - startedAt),
3464
+ status
3465
+ });
3466
+ };
3467
+ const emitStartupTimingSummary = (outcome, detail) => {
3468
+ const summary = formatRuntimeStartupTimingSummary({
3469
+ outcome,
3470
+ totalDurationMs: Date.now() - startupStartedAt,
3471
+ phases: startupPhases,
3472
+ detail
3473
+ });
3474
+ if (outcome === "failed") {
3475
+ console.warn(summary);
3476
+ } else {
3477
+ console.log(summary);
3478
+ }
3479
+ appendRuntimeServicesLogLine(runtimeServicesLogPath, summary);
3480
+ };
3327
3481
  const runToken = timestampFileToken();
3328
3482
  const logDir = join2(runtimeRoot, "logs", "bootstrap");
3329
3483
  mkdirSync(logDir, { recursive: true });
@@ -3341,6 +3495,7 @@ async function autoStartRuntimeServices(opts) {
3341
3495
  console.log(`[pushpals] service log (source_control_manager)=${serviceLogPaths.source_control_manager}`);
3342
3496
  const serverHealthy = await probeServer(opts.serverUrl);
3343
3497
  if (!serverHealthy) {
3498
+ const serverPhaseStartedAt = Date.now();
3344
3499
  console.log("[pushpals] Starting embedded server...");
3345
3500
  const serverService = spawnRuntimeService("server", [runtimeBinaries.server], opts.repoRoot, runtimeEnv, serviceLogPaths.server, runtimeServicesLogPath);
3346
3501
  services.push(serverService);
@@ -3351,6 +3506,8 @@ async function autoStartRuntimeServices(opts) {
3351
3506
  if (serverService.exited) {
3352
3507
  const tail = readLogTail(serverService.logPath);
3353
3508
  appendRuntimeServicesLogLine(runtimeServicesLogPath, `[pushpals] embedded server exited during bootstrap (code=${serverService.exitCode ?? "unknown"}).`);
3509
+ recordStartupPhase("server", serverPhaseStartedAt, "exited");
3510
+ emitStartupTimingSummary("failed", "server exited during bootstrap");
3354
3511
  stopRuntimeServices(services);
3355
3512
  throw new Error(`Embedded server exited during bootstrap (code=${serverService.exitCode ?? "unknown"}). ` + `See ${serverService.logPath}${tail ? `
3356
3513
  --- server log tail ---
@@ -3365,29 +3522,38 @@ ${tail}` : ""}`);
3365
3522
  if (!serverIsReady) {
3366
3523
  const tail = readLogTail(serverService.logPath);
3367
3524
  appendRuntimeServicesLogLine(runtimeServicesLogPath, `[pushpals] embedded server did not become healthy within ${DEFAULT_SERVER_BOOT_TIMEOUT_MS}ms.`);
3525
+ recordStartupPhase("server", serverPhaseStartedAt, "timeout");
3526
+ emitStartupTimingSummary("failed", "server health timeout");
3368
3527
  stopRuntimeServices(services);
3369
3528
  throw new Error(`Embedded server did not become healthy within ${DEFAULT_SERVER_BOOT_TIMEOUT_MS}ms. ` + `See ${serverService.logPath}${tail ? `
3370
3529
  --- server log tail ---
3371
3530
  ${tail}` : ""}`);
3372
3531
  }
3532
+ recordStartupPhase("server", serverPhaseStartedAt, "started");
3373
3533
  console.log("[pushpals] Embedded server is healthy.");
3374
3534
  } else {
3535
+ recordStartupPhase("server", Date.now(), "reused");
3375
3536
  console.log("[pushpals] Server already healthy; skipping embedded server start.");
3376
3537
  appendRuntimeServicesLogLine(runtimeServicesLogPath, "[pushpals] server already healthy; embedded server start skipped.");
3377
3538
  }
3378
3539
  if (localBuddyEnabled) {
3540
+ const localBuddyPhaseStartedAt = Date.now();
3379
3541
  console.log("[pushpals] Starting embedded LocalBuddy...");
3380
3542
  const localbuddyService = spawnRuntimeService("localbuddy", [runtimeBinaries.localbuddy], opts.repoRoot, runtimeEnv, serviceLogPaths.localbuddy, runtimeServicesLogPath);
3381
3543
  services.push(localbuddyService);
3382
3544
  console.log(`[pushpals] localbuddy log: ${localbuddyService.logPath}`);
3545
+ recordStartupPhase("localbuddy", localBuddyPhaseStartedAt, "started");
3383
3546
  } else {
3547
+ recordStartupPhase("localbuddy", Date.now(), "skipped");
3384
3548
  console.log("[pushpals] Embedded LocalBuddy disabled for this CLI session; skipping start.");
3385
3549
  appendRuntimeServicesLogLine(runtimeServicesLogPath, "[pushpals] localbuddy disabled for this CLI session; embedded localbuddy start skipped.");
3386
3550
  }
3551
+ const remoteBuddyPhaseStartedAt = Date.now();
3387
3552
  console.log("[pushpals] Starting embedded RemoteBuddy...");
3388
3553
  const remotebuddyService = spawnRuntimeService("remotebuddy", [runtimeBinaries.remotebuddy], opts.repoRoot, runtimeEnv, serviceLogPaths.remotebuddy, runtimeServicesLogPath);
3389
3554
  services.push(remotebuddyService);
3390
3555
  console.log(`[pushpals] remotebuddy log: ${remotebuddyService.logPath}`);
3556
+ recordStartupPhase("remotebuddy", remoteBuddyPhaseStartedAt, "started");
3391
3557
  let lastReportedRemoteBuddyAutonomyState = "unknown";
3392
3558
  const reportRemoteBuddyAutonomousEngineState = () => {
3393
3559
  const autonomyState = readRemoteBuddyAutonomousEngineState(remotebuddyService.logPath);
@@ -3405,48 +3571,58 @@ ${tail}` : ""}`);
3405
3571
  };
3406
3572
  reportRemoteBuddyAutonomousEngineState();
3407
3573
  if (runtimePreflight.config.remotebuddy.autoSpawnWorkerpals) {
3408
- const workerpalReadyTimeoutMs = resolveWorkerpalCapacityTimeoutMs(runtimePreflight.config);
3574
+ const workerpalPhaseStartedAt = Date.now();
3575
+ const workerpalReadinessProbeTimeoutMs = resolveWorkerpalStartupReadinessProbeTimeoutMs(runtimePreflight.config);
3409
3576
  const workerpalCapacity = await waitForWorkerpalCapacity({
3410
3577
  serverUrl: opts.serverUrl,
3411
- timeoutMs: workerpalReadyTimeoutMs,
3578
+ timeoutMs: workerpalReadinessProbeTimeoutMs,
3412
3579
  ttlMs: runtimePreflight.config.remotebuddy.workerpalOnlineTtlMs
3413
3580
  });
3414
3581
  if (!workerpalCapacity.ok) {
3415
- const tail = readLogTail(remotebuddyService.logPath);
3416
- appendRuntimeServicesLogLine(runtimeServicesLogPath, `[pushpals] embedded workerpal capacity did not become available within ${workerpalReadyTimeoutMs}ms.`);
3417
- stopRuntimeServices(services);
3418
- throw new Error(`Embedded WorkerPal capacity did not become available within ${workerpalReadyTimeoutMs}ms (${workerpalCapacity.detail}). ` + `See ${remotebuddyService.logPath}${tail ? `
3419
- --- remotebuddy log tail ---
3420
- ${tail}` : ""}`);
3582
+ const startupProbeWarning = `embedded workerpal readiness probe did not find idle capacity within ${workerpalReadinessProbeTimeoutMs}ms ` + `(${workerpalCapacity.detail}); continuing startup while WorkerPal warmup finishes in the background.`;
3583
+ console.warn(`[pushpals] ${startupProbeWarning}`);
3584
+ appendRuntimeServicesLogLine(runtimeServicesLogPath, `[pushpals] ${startupProbeWarning}`);
3585
+ recordStartupPhase("workerpal", workerpalPhaseStartedAt, "deferred");
3586
+ } else {
3587
+ console.log(`[pushpals] Embedded WorkerPal capacity is ready (${workerpalCapacity.detail}).`);
3588
+ appendRuntimeServicesLogLine(runtimeServicesLogPath, `[pushpals] embedded workerpal capacity ready (${workerpalCapacity.detail}).`);
3589
+ recordStartupPhase("workerpal", workerpalPhaseStartedAt, "ready");
3421
3590
  }
3422
- console.log(`[pushpals] Embedded WorkerPal capacity is ready (${workerpalCapacity.detail}).`);
3423
- appendRuntimeServicesLogLine(runtimeServicesLogPath, `[pushpals] embedded workerpal capacity ready (${workerpalCapacity.detail}).`);
3591
+ } else {
3592
+ recordStartupPhase("workerpal", Date.now(), "disabled");
3424
3593
  }
3425
3594
  const scmHealthy = await probeSourceControlManager(opts.sourceControlManagerPort);
3426
3595
  const scmGitProbe = await resolveSourceControlManagerGitProbe(opts.repoRoot, runtimeEnv, process.platform);
3427
3596
  const scmRemoteStatus = await checkGitRemoteConfigured(opts.repoRoot, opts.sourceControlManagerRemote, runtimeEnv);
3428
3597
  if (!scmHealthy) {
3598
+ const scmPhaseStartedAt = Date.now();
3429
3599
  if (!scmGitProbe.ok) {
3430
3600
  console.warn("[pushpals] Git is not available to embedded SourceControlManager; skipping SCM startup.");
3431
3601
  appendRuntimeServicesLogLine(runtimeServicesLogPath, `[pushpals] source_control_manager skipped: git is unavailable in embedded runtime env (${scmGitProbe.detail}).`);
3602
+ recordStartupPhase("source_control_manager", scmPhaseStartedAt, "skipped_no_git");
3432
3603
  } else if (scmRemoteStatus.status === "error") {
3433
3604
  console.warn(`[pushpals] Could not inspect SourceControlManager git remote "${opts.sourceControlManagerRemote}"; skipping SCM startup.`);
3434
3605
  appendRuntimeServicesLogLine(runtimeServicesLogPath, `[pushpals] source_control_manager skipped: remote "${opts.sourceControlManagerRemote}" could not be inspected (${scmRemoteStatus.detail}).`);
3606
+ recordStartupPhase("source_control_manager", scmPhaseStartedAt, "skipped_remote_error");
3435
3607
  } else if (scmRemoteStatus.status === "ok") {
3436
3608
  console.log(`[pushpals] Embedded SourceControlManager git=${scmGitProbe.detail}`);
3437
3609
  console.log("[pushpals] Starting embedded SourceControlManager...");
3438
3610
  const sourceControlManagerService = spawnRuntimeService("source_control_manager", [runtimeBinaries.sourceControlManager, "--skip-clean-check"], opts.repoRoot, runtimeEnv, serviceLogPaths.source_control_manager, runtimeServicesLogPath);
3439
3611
  services.push(sourceControlManagerService);
3440
3612
  console.log(`[pushpals] source_control_manager log: ${sourceControlManagerService.logPath}`);
3613
+ recordStartupPhase("source_control_manager", scmPhaseStartedAt, "started");
3441
3614
  } else {
3442
3615
  console.log(`[pushpals] Repo has no git remote "${opts.sourceControlManagerRemote}"; skipping embedded SourceControlManager.`);
3443
3616
  appendRuntimeServicesLogLine(runtimeServicesLogPath, `[pushpals] source_control_manager skipped: repo has no remote "${opts.sourceControlManagerRemote}".`);
3617
+ recordStartupPhase("source_control_manager", scmPhaseStartedAt, "skipped_no_remote");
3444
3618
  }
3445
3619
  } else {
3620
+ recordStartupPhase("source_control_manager", Date.now(), "reused");
3446
3621
  console.log("[pushpals] SourceControlManager already healthy; skipping embedded start.");
3447
3622
  appendRuntimeServicesLogLine(runtimeServicesLogPath, "[pushpals] source_control_manager already healthy; embedded start skipped.");
3448
3623
  }
3449
3624
  const deadline = Date.now() + DEFAULT_RUNTIME_BOOT_TIMEOUT_MS;
3625
+ const readinessPhaseStartedAt = Date.now();
3450
3626
  while (Date.now() < deadline) {
3451
3627
  reportRemoteBuddyAutonomousEngineState();
3452
3628
  for (let i = services.length - 1;i >= 0; i--) {
@@ -3467,6 +3643,8 @@ ${tail2}`);
3467
3643
  }
3468
3644
  const tail = readLogTail(service.logPath);
3469
3645
  appendRuntimeServicesLogLine(runtimeServicesLogPath, `[pushpals] embedded ${service.name} exited during startup (code=${service.exitCode ?? "unknown"}).`);
3646
+ recordStartupPhase("readiness", readinessPhaseStartedAt, "failed");
3647
+ emitStartupTimingSummary("failed", `${service.name} exited during startup`);
3470
3648
  stopRuntimeServices(services);
3471
3649
  throw new Error(`Embedded ${service.name} exited during startup (code=${service.exitCode ?? "unknown"}). ` + `See ${service.logPath}${tail ? `
3472
3650
  --- ${service.name} log tail ---
@@ -3499,6 +3677,8 @@ ${tail2}`);
3499
3677
  }
3500
3678
  const tail = readLogTail(service.logPath);
3501
3679
  appendRuntimeServicesLogLine(runtimeServicesLogPath, `[pushpals] embedded ${service.name} exited immediately after bootstrap (code=${service.exitCode ?? "unknown"}).`);
3680
+ recordStartupPhase("readiness", readinessPhaseStartedAt, "failed");
3681
+ emitStartupTimingSummary("failed", `${service.name} exited immediately after bootstrap`);
3502
3682
  stopRuntimeServices(services);
3503
3683
  throw new Error(`Embedded ${service.name} exited immediately after bootstrap (code=${service.exitCode ?? "unknown"}). ` + `See ${service.logPath}${tail ? `
3504
3684
  --- ${service.name} log tail ---
@@ -3507,6 +3687,8 @@ ${tail}` : ""}`);
3507
3687
  await Bun.sleep(250);
3508
3688
  }
3509
3689
  console.log("[pushpals] Embedded runtime is ready.");
3690
+ recordStartupPhase("readiness", readinessPhaseStartedAt, "ready");
3691
+ emitStartupTimingSummary("ready");
3510
3692
  appendRuntimeServicesLogLine(runtimeServicesLogPath, "[pushpals] embedded runtime is ready.");
3511
3693
  return {
3512
3694
  services,
@@ -3519,13 +3701,19 @@ ${tail}` : ""}`);
3519
3701
  const remoteBuddyHealth = await probeRemoteBuddySessionConsumer(opts.serverUrl, opts.sessionId);
3520
3702
  if (!localBuddyEnabled && !remoteBuddyHealth.ok) {
3521
3703
  appendRuntimeServicesLogLine(runtimeServicesLogPath, `[pushpals] timed out waiting for RemoteBuddy session consumer readiness after ${DEFAULT_RUNTIME_BOOT_TIMEOUT_MS}ms (${remoteBuddyHealth.detail}).`);
3704
+ recordStartupPhase("readiness", readinessPhaseStartedAt, "timeout");
3705
+ emitStartupTimingSummary("failed", "remotebuddy readiness timeout");
3522
3706
  throw new Error(`Timed out waiting for RemoteBuddy session consumer readiness after ${DEFAULT_RUNTIME_BOOT_TIMEOUT_MS}ms (${remoteBuddyHealth.detail})`);
3523
3707
  }
3524
3708
  if (!localBuddyEnabled) {
3525
3709
  appendRuntimeServicesLogLine(runtimeServicesLogPath, `[pushpals] timed out waiting for embedded runtime readiness after ${DEFAULT_RUNTIME_BOOT_TIMEOUT_MS}ms.`);
3710
+ recordStartupPhase("readiness", readinessPhaseStartedAt, "timeout");
3711
+ emitStartupTimingSummary("failed", "embedded runtime readiness timeout");
3526
3712
  throw new Error(`Timed out waiting for embedded runtime readiness after ${DEFAULT_RUNTIME_BOOT_TIMEOUT_MS}ms`);
3527
3713
  }
3528
3714
  appendRuntimeServicesLogLine(runtimeServicesLogPath, `[pushpals] timed out waiting for LocalBuddy at ${opts.localAgentUrl} and RemoteBuddy session consumer after ${DEFAULT_RUNTIME_BOOT_TIMEOUT_MS}ms.`);
3715
+ recordStartupPhase("readiness", readinessPhaseStartedAt, "timeout");
3716
+ emitStartupTimingSummary("failed", "localbuddy and remotebuddy readiness timeout");
3529
3717
  throw new Error(`Timed out waiting for LocalBuddy at ${opts.localAgentUrl} and RemoteBuddy session consumer after ${DEFAULT_RUNTIME_BOOT_TIMEOUT_MS}ms`);
3530
3718
  }
3531
3719
  function readCliState(pathValue) {
@@ -3953,8 +4141,45 @@ function formatSessionEventLine(event) {
3953
4141
  }
3954
4142
  return null;
3955
4143
  }
4144
+ function buildSessionEventReplayFingerprint(event) {
4145
+ const type = String(event.type ?? "").trim().toLowerCase();
4146
+ if (type !== "status")
4147
+ return null;
4148
+ const payload = event.payload ?? {};
4149
+ const source = String(event.from ?? payload.agentId ?? "status").trim().toLowerCase();
4150
+ const state = String(payload.state ?? "").trim().toLowerCase();
4151
+ const detail = String(payload.detail ?? "").trim().toLowerCase();
4152
+ const message = String(payload.message ?? "").trim().toLowerCase();
4153
+ return {
4154
+ source,
4155
+ fingerprint: `${type}:${source}:${state}:${detail}:${message}`
4156
+ };
4157
+ }
4158
+ function createSessionEventReplayFilter() {
4159
+ const seenEventIds = new Set;
4160
+ const lastStatusFingerprintBySource = new Map;
4161
+ return {
4162
+ shouldRender(event) {
4163
+ const eventId = String(event.id ?? "").trim();
4164
+ if (eventId) {
4165
+ if (seenEventIds.has(eventId))
4166
+ return false;
4167
+ seenEventIds.add(eventId);
4168
+ }
4169
+ const replayStatus = buildSessionEventReplayFingerprint(event);
4170
+ if (!replayStatus)
4171
+ return true;
4172
+ const previous = lastStatusFingerprintBySource.get(replayStatus.source);
4173
+ if (previous === replayStatus.fingerprint)
4174
+ return false;
4175
+ lastStatusFingerprintBySource.set(replayStatus.source, replayStatus.fingerprint);
4176
+ return true;
4177
+ }
4178
+ };
4179
+ }
3956
4180
  async function runSessionStream(serverUrl, sessionId, client, print, signal) {
3957
4181
  let cursor = 0;
4182
+ const replayFilter = createSessionEventReplayFilter();
3958
4183
  while (!signal.aborted) {
3959
4184
  try {
3960
4185
  const response = await fetchWithTimeout(`${serverUrl}/sessions/${encodeURIComponent(sessionId)}/events${buildClientTransportQuery(cursor, client)}`, {}, 15000);
@@ -4004,6 +4229,8 @@ async function runSessionStream(serverUrl, sessionId, client, print, signal) {
4004
4229
  cursor = Math.max(cursor, blockCursor, serverCursor);
4005
4230
  if (!parsed.envelope)
4006
4231
  continue;
4232
+ if (!replayFilter.shouldRender(parsed.envelope))
4233
+ continue;
4007
4234
  const line = formatSessionEventLine(parsed.envelope);
4008
4235
  if (line)
4009
4236
  print(line);
@@ -4088,32 +4315,59 @@ async function main() {
4088
4315
  } else {
4089
4316
  console.warn("[pushpals] RemoteBuddy autonomy is disabled in config (remotebuddy.autonomy.enabled=false); continuing.");
4090
4317
  }
4091
- const scmGitPrecheck = await precheckSourceControlManagerGitAvailability({
4092
- repoRoot,
4093
- remote: config.sourceControlManager.remote,
4094
- runtimeRoot: preparedRuntime.runtimeRoot,
4095
- preflightUsesEmbeddedRuntime: preparedRuntime.preflightUsesEmbeddedRuntime
4096
- });
4097
- if (scmGitPrecheck.status === "failed") {
4098
- console.error(`[pushpals] Precheck failed: embedded SourceControlManager git command is unavailable (${scmGitPrecheck.detail}).`);
4099
- process.exit(1);
4100
- }
4101
- const workerpalDockerPrecheck = await precheckWorkerpalDockerAvailability({
4102
- repoRoot,
4103
- runtimeRoot: preparedRuntime.runtimeRoot,
4104
- preflightUsesEmbeddedRuntime: preparedRuntime.preflightUsesEmbeddedRuntime,
4105
- autoSpawnWorkerpals: Boolean(config.remotebuddy.autoSpawnWorkerpals),
4106
- dockerEnabled: Boolean(config.remotebuddy.workerpalDocker),
4107
- requireDocker: Boolean(config.remotebuddy.workerpalRequireDocker),
4108
- baseEnv: scmGitPrecheck.env
4109
- });
4110
- const precheckPassed = await enforcePushpalsRemoteBranchPrecheck(repoRoot, config.sourceControlManager.remote, config.sourceControlManager.mainBranch);
4111
- if (!precheckPassed) {
4112
- process.exit(1);
4113
- }
4114
4318
  const serverUrl = normalizeLoopbackUrl(parsed.serverUrl ?? process.env.PUSHPALS_SERVER_URL, config.server.url);
4115
4319
  const localAgentUrl = normalizeLoopbackUrl(parsed.localAgentUrl ?? process.env.EXPO_PUBLIC_LOCAL_AGENT_URL, config.client.localAgentUrl);
4116
4320
  const sessionId = String(parsed.sessionId ?? process.env.PUSHPALS_SESSION_ID ?? config.sessionId).trim();
4321
+ let serverHealthy = await probeServer(serverUrl);
4322
+ const serverWasAlreadyHealthy = serverHealthy;
4323
+ const runEmbeddedRuntimeStartupPrechecks = shouldRunEmbeddedRuntimeStartupPrechecks({
4324
+ serverHealthy,
4325
+ noAutoStart: parsed.noAutoStart
4326
+ });
4327
+ if (runEmbeddedRuntimeStartupPrechecks) {
4328
+ const precheckPassed = await enforcePushpalsRemoteBranchPrecheck(repoRoot, config.sourceControlManager.remote, config.sourceControlManager.mainBranch);
4329
+ if (!precheckPassed) {
4330
+ process.exit(1);
4331
+ }
4332
+ }
4333
+ let scmGitPrecheck = {
4334
+ status: "skipped",
4335
+ detail: runEmbeddedRuntimeStartupPrechecks ? "embedded SourceControlManager startup precheck not run" : serverHealthy ? "embedded SourceControlManager startup precheck skipped because runtime is already healthy" : "embedded SourceControlManager startup precheck skipped because auto-start is disabled",
4336
+ env: buildEmbeddedRuntimeEnv(process.env, {
4337
+ repoRoot,
4338
+ runtimeRoot: preparedRuntime.runtimeRoot,
4339
+ useRuntimeConfig: preparedRuntime.preflightUsesEmbeddedRuntime,
4340
+ sessionId
4341
+ })
4342
+ };
4343
+ let workerpalDockerPrecheck = {
4344
+ status: "skipped",
4345
+ detail: runEmbeddedRuntimeStartupPrechecks ? "embedded WorkerPal Docker startup precheck not run" : serverHealthy ? "embedded WorkerPal Docker startup precheck skipped because runtime is already healthy" : "embedded WorkerPal Docker startup precheck skipped because auto-start is disabled",
4346
+ env: scmGitPrecheck.env
4347
+ };
4348
+ if (runEmbeddedRuntimeStartupPrechecks) {
4349
+ scmGitPrecheck = await precheckSourceControlManagerGitAvailability({
4350
+ repoRoot,
4351
+ remote: config.sourceControlManager.remote,
4352
+ runtimeRoot: preparedRuntime.runtimeRoot,
4353
+ preflightUsesEmbeddedRuntime: preparedRuntime.preflightUsesEmbeddedRuntime,
4354
+ sessionId
4355
+ });
4356
+ if (scmGitPrecheck.status === "failed") {
4357
+ console.error(`[pushpals] Precheck failed: embedded SourceControlManager git command is unavailable (${scmGitPrecheck.detail}).`);
4358
+ process.exit(1);
4359
+ }
4360
+ workerpalDockerPrecheck = await precheckWorkerpalDockerAvailability({
4361
+ repoRoot,
4362
+ runtimeRoot: preparedRuntime.runtimeRoot,
4363
+ preflightUsesEmbeddedRuntime: preparedRuntime.preflightUsesEmbeddedRuntime,
4364
+ autoSpawnWorkerpals: Boolean(config.remotebuddy.autoSpawnWorkerpals),
4365
+ dockerEnabled: Boolean(config.remotebuddy.workerpalDocker),
4366
+ requireDocker: Boolean(config.remotebuddy.workerpalRequireDocker),
4367
+ sessionId,
4368
+ baseEnv: scmGitPrecheck.env
4369
+ });
4370
+ }
4117
4371
  const cliVersion = String(process.env.PUSHPALS_CLI_PACKAGE_VERSION ?? "").trim() || "unknown";
4118
4372
  const cliClient = {
4119
4373
  clientId: createRuntimeClientId("cli"),
@@ -4157,6 +4411,31 @@ async function main() {
4157
4411
  console.log(`[pushpals] ${cleanup.detail} (${phase}).`);
4158
4412
  }
4159
4413
  };
4414
+ const reportWorkerExecutionReadiness = async () => {
4415
+ const readiness = await resolveWorkerExecutionReadiness({
4416
+ serverUrl,
4417
+ ttlMs: config.remotebuddy.workerpalOnlineTtlMs,
4418
+ autoSpawnWorkerpals: Boolean(config.remotebuddy.autoSpawnWorkerpals),
4419
+ dockerEnabled: Boolean(config.remotebuddy.workerpalDocker),
4420
+ requireDocker: Boolean(config.remotebuddy.workerpalRequireDocker),
4421
+ repoRoot,
4422
+ runtimeRoot: preparedRuntime.runtimeRoot,
4423
+ preflightUsesEmbeddedRuntime: preparedRuntime.preflightUsesEmbeddedRuntime,
4424
+ sessionId,
4425
+ dockerPrecheck: workerpalDockerPrecheck,
4426
+ baseEnv: workerpalDockerPrecheck.env
4427
+ });
4428
+ for (const line of formatWorkerExecutionReadinessLines(readiness)) {
4429
+ console.log(line);
4430
+ }
4431
+ return readiness;
4432
+ };
4433
+ const reportWorkerExecutionReadinessFromSnapshot = (readiness) => {
4434
+ for (const line of formatWorkerExecutionReadinessLines(readiness)) {
4435
+ console.log(line);
4436
+ }
4437
+ return readiness;
4438
+ };
4160
4439
  const stopAutoStartedServices = () => {
4161
4440
  if (autoStartedServices.length === 0)
4162
4441
  return;
@@ -4181,14 +4460,12 @@ async function main() {
4181
4460
  await cleanupWorkerpalWarmContainersIfNeeded("cli shutdown");
4182
4461
  await cleanupPushPalsGitWorktreesIfNeeded("cli shutdown");
4183
4462
  };
4184
- let serverHealthy = await probeServer(serverUrl);
4185
- const serverWasAlreadyHealthy = serverHealthy;
4186
4463
  if (!serverHealthy && workerpalDockerPrecheck.status === "failed") {
4187
4464
  console.error(`[pushpals] Precheck failed: Docker-backed WorkerPal auto-spawn is required but Docker is unavailable (${workerpalDockerPrecheck.detail}).`);
4188
4465
  console.error("[pushpals] Precheck failed: start Docker Desktop or the Docker daemon, then retry pushpals.");
4189
4466
  process.exit(1);
4190
4467
  }
4191
- if (workerpalDockerPrecheck.status !== "failed") {
4468
+ if (runEmbeddedRuntimeStartupPrechecks && workerpalDockerPrecheck.status !== "failed") {
4192
4469
  const workerpalImagePrecheck = await prepareEmbeddedWorkerpalDockerImageIfNeeded({
4193
4470
  preparedRuntime,
4194
4471
  config,
@@ -4208,8 +4485,10 @@ async function main() {
4208
4485
  detail: `No connected RemoteBuddy session consumer found for session ${sessionId}`
4209
4486
  };
4210
4487
  if (!serverHealthy) {
4211
- await cleanupWorkerpalWarmContainersIfNeeded("startup preflight");
4212
- await cleanupPushPalsGitWorktreesIfNeeded("startup preflight");
4488
+ if (runEmbeddedRuntimeStartupPrechecks) {
4489
+ await cleanupWorkerpalWarmContainersIfNeeded("startup preflight");
4490
+ await cleanupPushPalsGitWorktreesIfNeeded("startup preflight");
4491
+ }
4213
4492
  if (!parsed.noAutoStart) {
4214
4493
  try {
4215
4494
  const startedRuntime = await autoStartRuntimeServices({
@@ -4230,6 +4509,10 @@ async function main() {
4230
4509
  } catch (err) {
4231
4510
  console.error(`[pushpals] Auto-start failed: ${String(err)}`);
4232
4511
  stopAutoStartedServices();
4512
+ if (runEmbeddedRuntimeStartupPrechecks) {
4513
+ await cleanupWorkerpalWarmContainersIfNeeded("startup failure cleanup");
4514
+ await cleanupPushPalsGitWorktreesIfNeeded("startup failure cleanup");
4515
+ }
4233
4516
  }
4234
4517
  }
4235
4518
  if (!serverHealthy) {
@@ -4279,20 +4562,33 @@ async function main() {
4279
4562
  }
4280
4563
  const workerpalCapacity = await waitForWorkerpalCapacity({
4281
4564
  serverUrl,
4282
- timeoutMs: resolveWorkerpalCapacityTimeoutMs(config),
4565
+ timeoutMs: resolveWorkerpalStartupReadinessProbeTimeoutMs(config),
4283
4566
  ttlMs: config.remotebuddy.workerpalOnlineTtlMs
4284
4567
  });
4285
4568
  if (!workerpalCapacity.ok) {
4286
- stopAutoStartedServices();
4287
- console.error(`[pushpals] WorkerPal capacity is not ready for repo ${repoRoot}: ${workerpalCapacity.detail}.`);
4569
+ console.warn(`[pushpals] WorkerPal readiness probe did not find idle capacity yet (${workerpalCapacity.detail}).`);
4570
+ console.warn("[pushpals] Continuing startup; WorkerPal warmup may still be in progress and first task dispatch can be delayed.");
4288
4571
  if (workerpalDockerPrecheck.status === "failed") {
4289
- console.error(`[pushpals] Docker precheck detail: ${workerpalDockerPrecheck.detail}`);
4572
+ console.warn(`[pushpals] Docker precheck detail: ${workerpalDockerPrecheck.detail}`);
4290
4573
  } else if (serverWasAlreadyHealthy) {
4291
- console.error("[pushpals] A PushPals runtime is already serving this repo, but it does not currently have an idle WorkerPal available.");
4292
- console.error("[pushpals] Wait for a worker to become idle or restart the runtime after fixing WorkerPal startup.");
4574
+ console.warn("[pushpals] A PushPals runtime is already serving this repo, but it does not currently have an idle WorkerPal available.");
4575
+ console.warn("[pushpals] Wait for a worker to become idle or restart the runtime after fixing WorkerPal startup.");
4293
4576
  }
4294
- process.exit(1);
4295
4577
  }
4578
+ const startupWorkerExecutionReadiness = workerpalCapacity.ok ? {
4579
+ state: "ready",
4580
+ detail: workerpalCapacity.detail
4581
+ } : workerpalDockerPrecheck.status === "failed" ? describeWorkerExecutionReadiness({
4582
+ autoSpawnWorkerpals: Boolean(config.remotebuddy.autoSpawnWorkerpals),
4583
+ requireDocker: Boolean(config.remotebuddy.workerpalDocker) && Boolean(config.remotebuddy.workerpalRequireDocker),
4584
+ dockerPrecheck: workerpalDockerPrecheck,
4585
+ onlineWorkers: 0,
4586
+ idleWorkers: 0
4587
+ }) : {
4588
+ state: "warming",
4589
+ detail: workerpalCapacity.detail,
4590
+ action: "Wait for WorkerPal auto-spawn/warmup to finish, then rerun /status."
4591
+ };
4296
4592
  const saved = statePath ? readCliState(statePath) : {};
4297
4593
  pushpalsLogPath = pushpalsLogPath || (typeof saved.pushpalsLogPath === "string" ? saved.pushpalsLogPath : undefined);
4298
4594
  const preferredHubUrl = normalizeUrl(parsed.monitoringHubUrl ?? process.env.PUSHPALS_MONITOR_URL ?? saved.monitoringHubUrl ?? "");
@@ -4330,6 +4626,7 @@ async function main() {
4330
4626
  console.log(`[pushpals] repoRoot=${repoRoot}`);
4331
4627
  console.log(`[pushpals] pushpalsLog=${pushpalsLogPath ?? "unavailable"}`);
4332
4628
  console.log(`[pushpals] cliStateFile=${statePath ?? "unavailable"}`);
4629
+ reportWorkerExecutionReadinessFromSnapshot(startupWorkerExecutionReadiness);
4333
4630
  if (parsed.runtimeOnly) {
4334
4631
  console.log("[pushpals] runtimeOnly=true");
4335
4632
  } else {
@@ -4441,6 +4738,7 @@ ${line}
4441
4738
  console.log(`[pushpals] repoRoot=${repoRoot}`);
4442
4739
  console.log(`[pushpals] pushpalsLog=${pushpalsLogPath ?? "unavailable"}`);
4443
4740
  console.log(monitoringHubUrl ? `[pushpals] monitoringHubUrl=${monitoringHubUrl}` : "[pushpals] monitoringHubUrl=unavailable");
4741
+ await reportWorkerExecutionReadiness();
4444
4742
  rl.prompt();
4445
4743
  continue;
4446
4744
  }
@@ -4479,6 +4777,8 @@ if (import.meta.main) {
4479
4777
  export {
4480
4778
  waitForWorkerpalCapacity,
4481
4779
  startEmbeddedMonitoringHub,
4780
+ shouldRunEmbeddedRuntimeStartupPrechecks,
4781
+ resolveWorkerExecutionReadiness,
4482
4782
  resolveWindowsWhereExecutableCandidatesForEnv,
4483
4783
  resolveWindowsShellExecutableCandidatesForEnv,
4484
4784
  resolveRuntimeGitExecutableCandidates,
@@ -4499,13 +4799,17 @@ export {
4499
4799
  normalizeChildProcessEnv,
4500
4800
  isCliExitCommand,
4501
4801
  injectMonitoringHubBootstrap,
4802
+ formatWorkerExecutionReadinessLines,
4502
4803
  formatTimestampedCliLine,
4503
4804
  formatSessionEventLine,
4805
+ formatRuntimeStartupTimingSummary,
4504
4806
  extractRemoteBuddySessionConsumerHealth,
4505
4807
  extractRemoteBuddyAutonomousEngineState,
4506
4808
  ensureWorkerpalDockerImageReady,
4507
4809
  ensureRuntimeBinaries,
4508
4810
  downloadRuntimeAssetsFromSourceTag,
4811
+ describeWorkerExecutionReadiness,
4812
+ createSessionEventReplayFilter,
4509
4813
  copyTrackedRepoPath,
4510
4814
  cleanupLingeringWorkerpalWarmContainers,
4511
4815
  cleanupLingeringPushPalsGitWorktrees,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushpalsdev/cli",
3
- "version": "1.0.29",
3
+ "version": "1.0.31",
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) {
@@ -44,6 +44,7 @@
44
44
  "format:check": "prettier --check .",
45
45
  "test:prompt-policy": "bun test tests/prompt-policy.enforcement.test.ts",
46
46
  "test:cli:integration": "bun test tests/cli.invocation-logging.test.ts tests/cli.runtime-bootstrap.test.ts tests/client.runtime-bootstrap.test.ts tests/shared.client-preflight.test.ts",
47
+ "test:cli:e2e": "bun test ./tests/integration/cli.e2e.ts",
47
48
  "test:root": "bun test tests",
48
49
  "test:protocol": "bun run tests/protocol.integration.ts",
49
50
  "test:integration": "python -u tests/integration/integration_controller.py --mode integration",