@pushpalsdev/cli 1.1.38 → 1.1.40

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.
@@ -58,11 +58,13 @@ function shouldRestartService(attempts, maxAttempts = DEFAULT_SERVICE_MANAGER_MA
58
58
  return normalizedAttempts < normalizedMax;
59
59
  }
60
60
  function pipeProcessStreamToLines(stream, onLine) {
61
- if (!stream || typeof stream === "number" || typeof stream.getReader !== "function")
62
- return;
61
+ if (!stream || typeof stream === "number" || typeof stream.getReader !== "function") {
62
+ return { cancel: () => {} };
63
+ }
63
64
  const reader = stream.getReader();
64
65
  const decoder = new TextDecoder;
65
66
  let pending = "";
67
+ let cancelled = false;
66
68
  (async () => {
67
69
  try {
68
70
  while (true) {
@@ -86,9 +88,21 @@ function pipeProcessStreamToLines(stream, onLine) {
86
88
  if (tail)
87
89
  onLine?.(tail);
88
90
  } catch {} finally {
89
- reader.releaseLock();
91
+ try {
92
+ reader.releaseLock();
93
+ } catch {}
90
94
  }
91
95
  })();
96
+ return {
97
+ cancel: () => {
98
+ if (cancelled)
99
+ return;
100
+ cancelled = true;
101
+ try {
102
+ reader.cancel().catch(() => {});
103
+ } catch {}
104
+ }
105
+ };
92
106
  }
93
107
  function spawnManagedService(spec) {
94
108
  const env = { ...spec.env ?? {} };
@@ -98,8 +112,8 @@ function spawnManagedService(spec) {
98
112
  stdout: "pipe",
99
113
  stderr: "pipe"
100
114
  });
101
- pipeProcessStreamToLines(proc.stdout, spec.onStdoutLine);
102
- pipeProcessStreamToLines(proc.stderr, spec.onStderrLine);
115
+ const stdoutPipe = pipeProcessStreamToLines(proc.stdout, spec.onStdoutLine);
116
+ const stderrPipe = pipeProcessStreamToLines(proc.stderr, spec.onStderrLine);
103
117
  const service = {
104
118
  name: spec.name,
105
119
  proc,
@@ -109,7 +123,11 @@ function spawnManagedService(spec) {
109
123
  exited: false,
110
124
  exitCode: null,
111
125
  launchedAtMs: Date.now(),
112
- logPath: spec.logPath
126
+ logPath: spec.logPath,
127
+ stopOutputPipes: () => {
128
+ stdoutPipe.cancel();
129
+ stderrPipe.cancel();
130
+ }
113
131
  };
114
132
  proc.exited.then((code) => {
115
133
  service.exited = true;
@@ -161,6 +179,7 @@ class ServiceManager {
161
179
  const existing = this.services.get(spec.name);
162
180
  if (existing && !existing.exited) {
163
181
  try {
182
+ existing.stopOutputPipes?.();
164
183
  const pid = existing.proc.pid;
165
184
  if (process.platform === "win32" && typeof pid === "number" && pid > 0) {
166
185
  Bun.spawnSync(["taskkill", "/PID", String(pid), "/T", "/F"], {
@@ -224,6 +243,7 @@ class ServiceManager {
224
243
  this.stopped = true;
225
244
  for (const service of this.services.values()) {
226
245
  try {
246
+ service.stopOutputPipes?.();
227
247
  const pid = service.proc.pid;
228
248
  if (process.platform === "win32" && typeof pid === "number" && pid > 0) {
229
249
  Bun.spawnSync(["taskkill", "/PID", String(pid), "/T", "/F"], {
@@ -2169,6 +2189,97 @@ function withWindowsGitSchannelEnv(env, platform = process.platform) {
2169
2189
  return env;
2170
2190
  return appendGitConfigEnv(env, "http.sslBackend", "schannel");
2171
2191
  }
2192
+ var WINDOWS_NODE_EXTRA_CA_CERTS_DISABLE_ENV = "PUSHPALS_DISABLE_WINDOWS_NODE_EXTRA_CA_CERTS";
2193
+ var WINDOWS_NODE_EXTRA_CA_CERTS_BUNDLE_RELATIVE_PATH = ["certs", "windows-root-ca.pem"];
2194
+ function resolveWindowsNodeExtraCaCertsBundlePath(runtimeRoot) {
2195
+ return join2(runtimeRoot, ...WINDOWS_NODE_EXTRA_CA_CERTS_BUNDLE_RELATIVE_PATH);
2196
+ }
2197
+ function hasUsablePemCertificate(pathValue) {
2198
+ try {
2199
+ return /-----BEGIN CERTIFICATE-----/.test(readFileSync4(pathValue, "utf8"));
2200
+ } catch {
2201
+ return false;
2202
+ }
2203
+ }
2204
+ function ensureWindowsNodeExtraCaCertsBundle(outPath, env) {
2205
+ if (hasUsablePemCertificate(outPath))
2206
+ return outPath;
2207
+ const outDir = dirname(outPath);
2208
+ try {
2209
+ mkdirSync(outDir, { recursive: true });
2210
+ } catch {
2211
+ return "";
2212
+ }
2213
+ const script = String.raw`
2214
+ $ErrorActionPreference = "Stop"
2215
+ $outPath = $env:PUSHPALS_WINDOWS_NODE_EXTRA_CA_CERTS_OUT
2216
+ if (-not $outPath) { throw "PUSHPALS_WINDOWS_NODE_EXTRA_CA_CERTS_OUT is required" }
2217
+ $outDir = Split-Path -Parent $outPath
2218
+ if ($outDir) { [System.IO.Directory]::CreateDirectory($outDir) | Out-Null }
2219
+ $stores = @("Cert:\CurrentUser\Root", "Cert:\LocalMachine\Root")
2220
+ $seen = @{}
2221
+ $lines = New-Object System.Collections.Generic.List[string]
2222
+ foreach ($store in $stores) {
2223
+ if (-not (Test-Path $store)) { continue }
2224
+ foreach ($cert in Get-ChildItem $store) {
2225
+ if (-not $cert.RawData) { continue }
2226
+ if ($cert.NotAfter -lt (Get-Date)) { continue }
2227
+ $thumbprint = [string]$cert.Thumbprint
2228
+ if ($seen.ContainsKey($thumbprint)) { continue }
2229
+ $seen[$thumbprint] = $true
2230
+ $lines.Add("-----BEGIN CERTIFICATE-----")
2231
+ $encoded = [Convert]::ToBase64String($cert.RawData, [Base64FormattingOptions]::InsertLineBreaks)
2232
+ foreach ($line in [regex]::Split($encoded, '\r?\n')) {
2233
+ if ($line) { $lines.Add($line) }
2234
+ }
2235
+ $lines.Add("-----END CERTIFICATE-----")
2236
+ }
2237
+ }
2238
+ if ($lines.Count -eq 0) { throw "No Windows root certificates found" }
2239
+ [System.IO.File]::WriteAllLines($outPath, $lines, [System.Text.Encoding]::ASCII)
2240
+ `;
2241
+ const encodedScript = Buffer.from(script, "utf16le").toString("base64");
2242
+ const childEnv = normalizeChildProcessEnv({
2243
+ ...env,
2244
+ PUSHPALS_WINDOWS_NODE_EXTRA_CA_CERTS_OUT: outPath
2245
+ });
2246
+ const result = Bun.spawnSync([
2247
+ "powershell.exe",
2248
+ "-NoProfile",
2249
+ "-NonInteractive",
2250
+ "-ExecutionPolicy",
2251
+ "Bypass",
2252
+ "-EncodedCommand",
2253
+ encodedScript
2254
+ ], {
2255
+ cwd: process.cwd(),
2256
+ env: childEnv,
2257
+ stdout: "pipe",
2258
+ stderr: "pipe"
2259
+ });
2260
+ if (result.exitCode !== 0)
2261
+ return "";
2262
+ return hasUsablePemCertificate(outPath) ? outPath : "";
2263
+ }
2264
+ function withWindowsNodeExtraCaCertsEnv(env, opts) {
2265
+ const platform = opts.platform ?? process.platform;
2266
+ if (platform !== "win32")
2267
+ return env;
2268
+ if (parseBooleanFlag(env[WINDOWS_NODE_EXTRA_CA_CERTS_DISABLE_ENV]) === true)
2269
+ return env;
2270
+ if (typeof env.NODE_EXTRA_CA_CERTS === "string" && env.NODE_EXTRA_CA_CERTS.trim())
2271
+ return env;
2272
+ const runtimeRoot = String(opts.runtimeRoot ?? "").trim();
2273
+ if (!runtimeRoot || !existsSync5(runtimeRoot))
2274
+ return env;
2275
+ const bundlePath = ensureWindowsNodeExtraCaCertsBundle(resolveWindowsNodeExtraCaCertsBundlePath(runtimeRoot), env);
2276
+ if (!bundlePath)
2277
+ return env;
2278
+ return {
2279
+ ...env,
2280
+ NODE_EXTRA_CA_CERTS: bundlePath
2281
+ };
2282
+ }
2172
2283
  async function runGitWithEnv(args, cwd, env, timeoutMs) {
2173
2284
  return await runCommandWithEnv(["git", ...args], cwd, withWindowsGitSchannelEnv(env), timeoutMs);
2174
2285
  }
@@ -2844,7 +2955,11 @@ function buildEmbeddedRuntimeEnv(baseEnv, opts) {
2844
2955
  ...typeof env.PUSHPALS_DOCKER_BIN === "string" && env.PUSHPALS_DOCKER_BIN.trim() ? { PUSHPALS_DOCKER_BIN: env.PUSHPALS_DOCKER_BIN.trim() } : {},
2845
2956
  ...typeof env.PUSHPALS_DOCKER_BIN_ABSOLUTE === "string" && env.PUSHPALS_DOCKER_BIN_ABSOLUTE.trim() ? { PUSHPALS_DOCKER_BIN_ABSOLUTE: env.PUSHPALS_DOCKER_BIN_ABSOLUTE.trim() } : {}
2846
2957
  };
2847
- return withWindowsGitSchannelEnv(runtimeEnv, platform);
2958
+ const runtimeEnvWithWindowsCa = withWindowsNodeExtraCaCertsEnv(runtimeEnv, {
2959
+ platform,
2960
+ runtimeRoot: opts.runtimeRoot
2961
+ });
2962
+ return withWindowsGitSchannelEnv(runtimeEnvWithWindowsCa, platform);
2848
2963
  }
2849
2964
  function parseBooleanFlag(raw) {
2850
2965
  const normalized = String(raw ?? "").trim().toLowerCase();
@@ -3126,6 +3241,7 @@ function buildServiceStopCommand(pid, platform = process.platform) {
3126
3241
  function stopRuntimeServices(services) {
3127
3242
  for (const service of services) {
3128
3243
  try {
3244
+ service.stopOutputPipes?.();
3129
3245
  const stopCommand = buildServiceStopCommand(service.proc.pid, process.platform);
3130
3246
  if (stopCommand) {
3131
3247
  Bun.spawnSync(stopCommand, {
@@ -3141,6 +3257,52 @@ function stopRuntimeServices(services) {
3141
3257
  } catch {}
3142
3258
  }
3143
3259
  }
3260
+ async function runWindowsServiceStopCommand(command, timeoutMs) {
3261
+ const proc = Bun.spawn(command, {
3262
+ stdin: "ignore",
3263
+ stdout: "ignore",
3264
+ stderr: "ignore"
3265
+ });
3266
+ let timeout = null;
3267
+ let timedOut = false;
3268
+ const exitCode = await Promise.race([
3269
+ proc.exited,
3270
+ new Promise((resolveTimeout) => {
3271
+ timeout = setTimeout(() => {
3272
+ timedOut = true;
3273
+ try {
3274
+ proc.kill("SIGKILL");
3275
+ } catch {}
3276
+ resolveTimeout(-1);
3277
+ }, Math.max(250, timeoutMs));
3278
+ })
3279
+ ]);
3280
+ if (timeout) {
3281
+ clearTimeout(timeout);
3282
+ timeout = null;
3283
+ }
3284
+ if (timedOut) {
3285
+ await Promise.race([proc.exited.catch(() => -1), Bun.sleep(250)]);
3286
+ return false;
3287
+ }
3288
+ return exitCode === 0;
3289
+ }
3290
+ async function stopRuntimeServicesOnWindows(services, timeoutMs) {
3291
+ const deadline = Date.now() + Math.max(1000, timeoutMs);
3292
+ for (const service of services) {
3293
+ service.stopOutputPipes?.();
3294
+ const stopCommand = buildServiceStopCommand(service.proc.pid, "win32");
3295
+ if (stopCommand) {
3296
+ const remainingMs = Math.max(250, deadline - Date.now());
3297
+ const stopped = await runWindowsServiceStopCommand(stopCommand, Math.min(WINDOWS_TASKKILL_TIMEOUT_MS2, remainingMs));
3298
+ if (stopped)
3299
+ continue;
3300
+ }
3301
+ try {
3302
+ service.proc.kill("SIGKILL");
3303
+ } catch {}
3304
+ }
3305
+ }
3144
3306
  function resolveGracefulShutdownPriority(name) {
3145
3307
  if (name === "source_control_manager")
3146
3308
  return 0;
@@ -3169,7 +3331,7 @@ async function stopRuntimeServicesGracefully(services, timeoutMs = 1e4) {
3169
3331
  return;
3170
3332
  const ordered = [...running].sort((a, b) => resolveGracefulShutdownPriority(a.name) - resolveGracefulShutdownPriority(b.name));
3171
3333
  if (process.platform === "win32") {
3172
- stopRuntimeServices(ordered);
3334
+ await stopRuntimeServicesOnWindows(ordered, timeoutMs);
3173
3335
  await waitForRuntimeServicesExit(ordered, Math.min(1000, timeoutMs));
3174
3336
  return;
3175
3337
  }
@@ -3200,6 +3362,7 @@ async function shutdownEmbeddedServiceManagerGracefully(options) {
3200
3362
  reason,
3201
3363
  requestShutdown = requestLocalRuntimeShutdown,
3202
3364
  shutdownAcceptedDelayMs = 1500,
3365
+ serviceStopTimeoutMs = 1e4,
3203
3366
  onLog = (line) => console.log(line),
3204
3367
  onWarn = (line) => console.warn(line),
3205
3368
  cleanupTasks = []
@@ -3215,7 +3378,7 @@ async function shutdownEmbeddedServiceManagerGracefully(options) {
3215
3378
  } else if (shutdown.detail) {
3216
3379
  onWarn(`[pushpals] ${shutdown.detail}`);
3217
3380
  }
3218
- await stopRuntimeServicesGracefully(services);
3381
+ await stopRuntimeServicesGracefully(services, serviceStopTimeoutMs);
3219
3382
  for (const task of cleanupTasks) {
3220
3383
  await task();
3221
3384
  }
@@ -6034,10 +6197,13 @@ ${line}
6034
6197
  console.log("[pushpals] Runtime-only mode is active. Send `exit` on stdin or terminate the process to stop.");
6035
6198
  await new Promise((resolveStop) => {
6036
6199
  let resolved = false;
6200
+ let exitRequestedFromInput = false;
6201
+ const keepAlive = setInterval(() => {}, 60000);
6037
6202
  const finish = () => {
6038
6203
  if (resolved)
6039
6204
  return;
6040
6205
  resolved = true;
6206
+ clearInterval(keepAlive);
6041
6207
  resolveStop();
6042
6208
  };
6043
6209
  process.once("SIGINT", finish);
@@ -6050,13 +6216,17 @@ ${line}
6050
6216
  runtimeOnlyInput.on("line", (line) => {
6051
6217
  if (!isCliExitCommand(line))
6052
6218
  return;
6219
+ exitRequestedFromInput = true;
6053
6220
  requestStop();
6054
6221
  runtimeOnlyInput.close();
6055
6222
  finish();
6056
6223
  });
6057
6224
  runtimeOnlyInput.on("close", () => {
6058
- requestStop();
6059
- finish();
6225
+ if (exitRequestedFromInput || resolved) {
6226
+ finish();
6227
+ return;
6228
+ }
6229
+ console.log("[pushpals] Runtime-only stdin closed; continuing until terminated.");
6060
6230
  });
6061
6231
  });
6062
6232
  await requestStop();
@@ -6138,6 +6308,7 @@ if (import.meta.main) {
6138
6308
  });
6139
6309
  }
6140
6310
  export {
6311
+ withWindowsNodeExtraCaCertsEnv,
6141
6312
  waitForWorkerpalCapacity,
6142
6313
  waitForRemoteBuddySessionConsumer,
6143
6314
  startEmbeddedMonitoringHub,
@@ -6154,6 +6325,7 @@ export {
6154
6325
  resolveWorkerExecutionReadiness,
6155
6326
  resolveWindowsWhereExecutableCandidatesForEnv,
6156
6327
  resolveWindowsShellExecutableCandidatesForEnv,
6328
+ resolveWindowsNodeExtraCaCertsBundlePath,
6157
6329
  resolveWindowsFreshRuntimeWorkerpalPrewarmDelayMs,
6158
6330
  resolveRuntimeGitExecutableCandidates,
6159
6331
  resolveRuntimeDockerExecutableCandidates,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushpalsdev/cli",
3
- "version": "1.1.38",
3
+ "version": "1.1.40",
4
4
  "description": "PushPals terminal CLI for LocalBuddy -> RemoteBuddy orchestration",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -127,6 +127,7 @@ _SMALL_TASK_ROLLOUT_WATCHDOG_S = 240
127
127
  _NARROW_TEST_TASK_ROLLOUT_WATCHDOG_S = 150
128
128
  _WEB_REVIEW_ROLLOUT_WATCHDOG_S = 180
129
129
  _BACKGROUND_ROLLOUT_WATCHDOG_S = 90
130
+ _MIN_AUTO_WATCHDOG_TIMEOUT_S = 180
130
131
  _MIN_CODEX_RECOVERY_ATTEMPT_S = 120
131
132
  _NO_PUBLISHABLE_FAILURE_COOLDOWN_MS = 10 * 60 * 1000
132
133
  _CODEX_STARTUP_ONLY_EVENT_TYPES = {"thread.started", "turn.started"}
@@ -664,6 +665,9 @@ def _looks_like_narrow_test_task_prompt(prompt: str) -> bool:
664
665
  "ranking contract",
665
666
  "regression coverage",
666
667
  "focused coverage",
668
+ "focused test",
669
+ "focused tests",
670
+ "focused testing",
667
671
  "focused regression",
668
672
  "test-only",
669
673
  "test only",
@@ -742,7 +746,7 @@ def _resolve_no_edit_watchdog_seconds(
742
746
  else:
743
747
  return max(1, min(parsed, max(1, communicate_timeout_s - 1)))
744
748
 
745
- if communicate_timeout_s < 600:
749
+ if communicate_timeout_s < _MIN_AUTO_WATCHDOG_TIMEOUT_S:
746
750
  return None
747
751
 
748
752
  prompt_text = str(prompt or "").lower()
@@ -887,7 +891,7 @@ def _resolve_rollout_watchdog_seconds(
887
891
  communicate_timeout_s: Optional[int],
888
892
  no_edit_watchdog_s: Optional[int],
889
893
  ) -> Optional[int]:
890
- if not communicate_timeout_s or communicate_timeout_s < 600:
894
+ if not communicate_timeout_s or communicate_timeout_s < _MIN_AUTO_WATCHDOG_TIMEOUT_S:
891
895
  return None
892
896
 
893
897
  raw = os.environ.get("WORKERPALS_OPENAI_CODEX_ROLLOUT_WATCHDOG_S", "").strip()
@@ -2156,6 +2156,24 @@ class OpenAICodexRuntimeConfigTests(unittest.TestCase):
2156
2156
 
2157
2157
  self.assertEqual(watchdog_s, 180)
2158
2158
 
2159
+ def test_review_fix_child_budget_below_ten_minutes_still_uses_watchdogs(self) -> None:
2160
+ prompt = (
2161
+ "Rejected PR revision brief: Previous ReviewAgent score: 8.0 / 10. "
2162
+ "Add focused tests for createCleanupHarness.runTask covering successful execution, "
2163
+ "execute failure, cleanup failure, invalid task input, and cleanup execution after "
2164
+ "successful task completion."
2165
+ )
2166
+ env = {
2167
+ "WORKERPALS_OPENAI_CODEX_NO_EDIT_WATCHDOG_S": "",
2168
+ "WORKERPALS_OPENAI_CODEX_ROLLOUT_WATCHDOG_S": "",
2169
+ }
2170
+ with mock.patch.dict(os.environ, env, clear=False):
2171
+ no_edit_s = _resolve_no_edit_watchdog_seconds(prompt, 570)
2172
+ rollout_s = _resolve_rollout_watchdog_seconds(prompt, 570, no_edit_s)
2173
+
2174
+ self.assertEqual(no_edit_s, 180)
2175
+ self.assertEqual(rollout_s, 120)
2176
+
2159
2177
  def test_no_edit_recovery_guidance_warns_against_artifact_only_progress(self) -> None:
2160
2178
  guidance = _build_no_edit_recovery_guidance(
2161
2179
  "item.completed | still inspecting",