@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.
package/dist/pushpals-cli.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
3197
|
-
|
|
3198
|
-
|
|
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
|
|
3332
|
+
if (summary.idleWorkers > 0) {
|
|
3202
3333
|
return {
|
|
3203
3334
|
ok: true,
|
|
3204
|
-
detail: `${idleWorkers
|
|
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
|
|
3574
|
+
const workerpalPhaseStartedAt = Date.now();
|
|
3575
|
+
const workerpalReadinessProbeTimeoutMs = resolveWorkerpalStartupReadinessProbeTimeoutMs(runtimePreflight.config);
|
|
3409
3576
|
const workerpalCapacity = await waitForWorkerpalCapacity({
|
|
3410
3577
|
serverUrl: opts.serverUrl,
|
|
3411
|
-
timeoutMs:
|
|
3578
|
+
timeoutMs: workerpalReadinessProbeTimeoutMs,
|
|
3412
3579
|
ttlMs: runtimePreflight.config.remotebuddy.workerpalOnlineTtlMs
|
|
3413
3580
|
});
|
|
3414
3581
|
if (!workerpalCapacity.ok) {
|
|
3415
|
-
const
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
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
|
-
|
|
3423
|
-
|
|
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
|
-
|
|
4212
|
-
|
|
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:
|
|
4565
|
+
timeoutMs: resolveWorkerpalStartupReadinessProbeTimeoutMs(config),
|
|
4283
4566
|
ttlMs: config.remotebuddy.workerpalOnlineTtlMs
|
|
4284
4567
|
});
|
|
4285
4568
|
if (!workerpalCapacity.ok) {
|
|
4286
|
-
|
|
4287
|
-
console.
|
|
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.
|
|
4572
|
+
console.warn(`[pushpals] Docker precheck detail: ${workerpalDockerPrecheck.detail}`);
|
|
4290
4573
|
} else if (serverWasAlreadyHealthy) {
|
|
4291
|
-
console.
|
|
4292
|
-
console.
|
|
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
|
@@ -130,14 +130,51 @@ export function redactSensitiveText(value: string): string {
|
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
export function buildCriticRevisionIssues(
|
|
133
|
-
critic:
|
|
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
|
-
|
|
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
|
|
3022
|
-
|
|
3023
|
-
|
|
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",
|