@pushpalsdev/cli 1.0.45 → 1.0.47

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.
@@ -692,6 +692,8 @@ function loadPushPalsConfig(options = {}) {
692
692
  const remoteNode = getObject(merged, "remotebuddy");
693
693
  const remoteStatusHeartbeatMs = Math.max(0, asInt(parseIntEnv("REMOTEBUDDY_STATUS_HEARTBEAT_MS") ?? globalStatusHeartbeatMs ?? remoteNode.status_heartbeat_ms, 120000));
694
694
  const remotePollMs = Math.max(200, asInt(parseIntEnv("REMOTEBUDDY_POLL_MS") ?? remoteNode.poll_ms, 2000));
695
+ const remoteMaxWorkerpals = Math.max(1, asInt(parseIntEnv("REMOTEBUDDY_MAX_WORKERPALS") ?? remoteNode.max_workerpals, 20));
696
+ const remoteMinWorkerpals = Math.max(1, Math.min(remoteMaxWorkerpals, asInt(parseIntEnv("REMOTEBUDDY_MIN_WORKERPALS") ?? remoteNode.min_workerpals, 1)));
695
697
  const remoteLlm = resolveLlmConfig(remoteNode, "REMOTEBUDDY", {
696
698
  backend: "lmstudio",
697
699
  endpoint: "http://127.0.0.1:1234",
@@ -944,7 +946,8 @@ function loadPushPalsConfig(options = {}) {
944
946
  workerpalOnlineTtlMs: Math.max(1000, asInt(parseIntEnv("REMOTEBUDDY_WORKERPAL_ONLINE_TTL_MS") ?? remoteNode.workerpal_online_ttl_ms, 15000)),
945
947
  waitForWorkerpalMs: Math.max(0, asInt(parseIntEnv("REMOTEBUDDY_WAIT_FOR_WORKERPAL_MS") ?? remoteNode.wait_for_workerpal_ms, 15000)),
946
948
  autoSpawnWorkerpals: parseBoolEnv("REMOTEBUDDY_AUTO_SPAWN_WORKERPALS") ?? asBoolean(remoteNode.auto_spawn_workerpals, true),
947
- maxWorkerpals: Math.max(1, asInt(parseIntEnv("REMOTEBUDDY_MAX_WORKERPALS") ?? remoteNode.max_workerpals, 20)),
949
+ minWorkerpals: remoteMinWorkerpals,
950
+ maxWorkerpals: remoteMaxWorkerpals,
948
951
  workerpalStartupTimeoutMs: Math.max(1000, asInt(parseIntEnv("REMOTEBUDDY_WORKERPAL_STARTUP_TIMEOUT_MS") ?? remoteNode.workerpal_startup_timeout_ms, 1e4)),
949
952
  workerpalDocker: parseBoolEnv("REMOTEBUDDY_WORKERPAL_DOCKER") ?? asBoolean(remoteNode.workerpal_docker, true),
950
953
  workerpalRequireDocker: parseBoolEnv("REMOTEBUDDY_WORKERPAL_REQUIRE_DOCKER") ?? asBoolean(remoteNode.workerpal_require_docker, true),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushpalsdev/cli",
3
- "version": "1.0.45",
3
+ "version": "1.0.47",
4
4
  "description": "PushPals terminal CLI for LocalBuddy -> RemoteBuddy orchestration",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -40,7 +40,8 @@ status_heartbeat_ms = 120000
40
40
  workerpal_online_ttl_ms = 15000
41
41
  wait_for_workerpal_ms = 15000
42
42
  auto_spawn_workerpals = true
43
- max_workerpals = 3
43
+ min_workerpals = 1
44
+ max_workerpals = 4
44
45
  workerpal_startup_timeout_ms = 10000
45
46
  workerpal_docker = true
46
47
  workerpal_require_docker = true
@@ -23,7 +23,8 @@ codex_timeout_ms = 120000
23
23
  reasoning_effort = "high"
24
24
 
25
25
  [remotebuddy]
26
- max_workerpals = 3
26
+ min_workerpals = 1
27
+ max_workerpals = 4
27
28
  crash_restart_enabled = true
28
29
  crash_restart_max_restarts = 3
29
30
  crash_restart_backoff_ms = 3000
@@ -62,7 +62,6 @@ _CODEX_WORKAROUND_PATTERNS = (
62
62
  ),
63
63
  re.compile(r"\bwithout requiring\b.{0,120}\bcodex\b", re.IGNORECASE),
64
64
  re.compile(r"\bavoid(?:ing)?\b.{0,120}\bcodex\b.{0,120}\bcall", re.IGNORECASE),
65
- re.compile(r"\b(fell back|fallback|worked around|workaround|bypass(?:ed)?|switched to)\b.{0,120}\bcodex\b", re.IGNORECASE),
66
65
  )
67
66
  _CODEX_WORKAROUND_NEGATION_HINTS = (
68
67
  "do not",
@@ -75,6 +74,15 @@ _CODEX_WORKAROUND_NEGATION_HINTS = (
75
74
  "explicit failure",
76
75
  "codex cli is required infrastructure",
77
76
  )
77
+ _REJECTED_EXEC_COMMAND_PATTERN = re.compile(r"exec_command failed for `([^`]+)`", re.IGNORECASE)
78
+ _DISALLOWED_SHELL_WRAPPER_PREFIXES = (
79
+ "/bin/bash -lc ",
80
+ "bash -lc ",
81
+ "sh -lc ",
82
+ "cmd /c ",
83
+ "powershell -command ",
84
+ "pwsh -command ",
85
+ )
78
86
 
79
87
  _VALID_APPROVAL_POLICIES = {"untrusted", "on-failure", "on-request", "never"}
80
88
  _VALID_SANDBOX_POLICIES = {"read-only", "workspace-write", "danger-full-access"}
@@ -942,6 +950,37 @@ def _detect_codex_workaround_signal(*texts: str) -> Optional[str]:
942
950
  return None
943
951
 
944
952
 
953
+ def _normalize_command_text(command: str) -> str:
954
+ return re.sub(r"\s+", " ", str(command or "")).strip()
955
+
956
+
957
+ def _is_disallowed_shell_wrapper_command(command: str) -> bool:
958
+ normalized = _normalize_command_text(command).lower()
959
+ return any(normalized.startswith(prefix) for prefix in _DISALLOWED_SHELL_WRAPPER_PREFIXES)
960
+
961
+
962
+ def _extract_rejected_exec_command(text: str) -> str:
963
+ match = _REJECTED_EXEC_COMMAND_PATTERN.search(str(text or ""))
964
+ if not match:
965
+ return ""
966
+ return _normalize_command_text(match.group(1))
967
+
968
+
969
+ def _collect_disallowed_shell_wrapper_rejections(*texts: str) -> List[str]:
970
+ rejected: List[str] = []
971
+ seen: set[str] = set()
972
+ for text in texts:
973
+ command = _extract_rejected_exec_command(str(text or ""))
974
+ if not command or not _is_disallowed_shell_wrapper_command(command):
975
+ continue
976
+ lowered = command.lower()
977
+ if lowered in seen:
978
+ continue
979
+ seen.add(lowered)
980
+ rejected.append(command)
981
+ return rejected
982
+
983
+
945
984
  def _read_text_if_exists(path: Path) -> str:
946
985
  try:
947
986
  if not path.exists():
@@ -1166,6 +1205,7 @@ def _run_codex_task(
1166
1205
  stdout_trace_state = _empty_codex_trace()
1167
1206
  trace_lock = threading.Lock()
1168
1207
  last_activity_at = {"ts": started_at}
1208
+ wrapper_rejection_state: Dict[str, Any] = {"count": 0, "commands": []}
1169
1209
 
1170
1210
  def _drain_stdout() -> None:
1171
1211
  stream = proc.stdout
@@ -1199,6 +1239,21 @@ def _run_codex_task(
1199
1239
  if chunk == "":
1200
1240
  break
1201
1241
  stderr_chunks.append(chunk)
1242
+ rejected_commands = _collect_disallowed_shell_wrapper_rejections(chunk)
1243
+ if rejected_commands:
1244
+ with trace_lock:
1245
+ wrapper_rejection_state["count"] = to_int(
1246
+ wrapper_rejection_state.get("count"), 0
1247
+ ) + len(rejected_commands)
1248
+ tracked = wrapper_rejection_state.get("commands")
1249
+ if not isinstance(tracked, list):
1250
+ tracked = []
1251
+ for command in rejected_commands:
1252
+ lowered = command.lower()
1253
+ if any(str(item).lower() == lowered for item in tracked):
1254
+ continue
1255
+ tracked.append(command)
1256
+ wrapper_rejection_state["commands"] = tracked[:6]
1202
1257
  except Exception:
1203
1258
  pass
1204
1259
  finally:
@@ -1226,6 +1281,7 @@ def _run_codex_task(
1226
1281
  )
1227
1282
  next_progress_at = started_at + float(progress_interval_s)
1228
1283
  timed_out = False
1284
+ command_policy_rejection_loop = False
1229
1285
 
1230
1286
  while proc.poll() is None:
1231
1287
  now = time.monotonic()
@@ -1234,6 +1290,13 @@ def _run_codex_task(
1234
1290
  _terminate_active_child()
1235
1291
  break
1236
1292
 
1293
+ with trace_lock:
1294
+ wrapper_rejections = to_int(wrapper_rejection_state.get("count"), 0)
1295
+ if wrapper_rejections >= 3:
1296
+ command_policy_rejection_loop = True
1297
+ _terminate_active_child()
1298
+ break
1299
+
1237
1300
  if now >= next_progress_at:
1238
1301
  elapsed = int(max(0.0, now - started_at))
1239
1302
  with trace_lock:
@@ -1279,6 +1342,18 @@ def _run_codex_task(
1279
1342
  part for part in (stdout, stderr, trace_excerpt) if str(part or "").strip()
1280
1343
  )
1281
1344
  usage = _usage_from_trace_or_estimate(stdout_trace, prompt, usage_output_text, model=model)
1345
+ rejected_shell_wrappers = _collect_disallowed_shell_wrapper_rejections(stdout, stderr)
1346
+ with trace_lock:
1347
+ tracked = wrapper_rejection_state.get("commands")
1348
+ if isinstance(tracked, list):
1349
+ for command in tracked:
1350
+ text = _normalize_command_text(str(command))
1351
+ if not text:
1352
+ continue
1353
+ lowered = text.lower()
1354
+ if any(entry.lower() == lowered for entry in rejected_shell_wrappers):
1355
+ continue
1356
+ rejected_shell_wrappers.append(text)
1282
1357
 
1283
1358
  if timed_out:
1284
1359
  detail = (
@@ -1300,6 +1375,30 @@ def _run_codex_task(
1300
1375
  last_message = _read_text_if_exists(last_message_path)
1301
1376
  log_git_status(repo, log)
1302
1377
 
1378
+ if command_policy_rejection_loop:
1379
+ command_lines = (
1380
+ "\n".join(f"- {command}" for command in rejected_shell_wrappers[:6])
1381
+ if rejected_shell_wrappers
1382
+ else "- (no command details captured)"
1383
+ )
1384
+ detail = (
1385
+ "Codex repeatedly attempted disallowed shell-wrapper commands that the command "
1386
+ "router rejected. Switch to direct commands only and avoid wrapper retries.\n"
1387
+ f"Rejected commands:\n{command_lines}"
1388
+ )
1389
+ if last_message:
1390
+ detail = f"{detail}\nLast assistant message:\n{last_message}"
1391
+ if trace_excerpt:
1392
+ detail = f"{detail}\n{trace_excerpt}"
1393
+ return {
1394
+ "ok": False,
1395
+ "summary": "openai_codex command policy rejection loop",
1396
+ "stdout": _truncate(stdout),
1397
+ "stderr": _truncate(detail),
1398
+ "exitCode": 6,
1399
+ "usage": usage,
1400
+ }
1401
+
1303
1402
  if _INTERRUPTED_SIGNAL is not None:
1304
1403
  return {
1305
1404
  "ok": False,
@@ -15,6 +15,7 @@ from openai_codex_executor import (
15
15
  OpenAICodexRuntimeConfig,
16
16
  _resolve_reasoning_effort,
17
17
  _build_instruction,
18
+ _collect_disallowed_shell_wrapper_rejections,
18
19
  _detect_codex_workaround_signal,
19
20
  _extract_usage_counts,
20
21
  _load_prompt_template,
@@ -163,6 +164,12 @@ class OpenAICodexRuntimeConfigTests(unittest.TestCase):
163
164
  )
164
165
  self.assertIsNone(signal)
165
166
 
167
+ def test_ignores_generic_workaround_language_without_unavailable_codex_context(self) -> None:
168
+ signal = _detect_codex_workaround_signal(
169
+ "This is a workaround case, so I am stopping here until the command router issue is fixed.",
170
+ )
171
+ self.assertIsNone(signal)
172
+
166
173
  def test_discovers_repo_root_for_prompt_loading(self) -> None:
167
174
  repo_root = _repo_root_for_prompt_loading()
168
175
  self.assertTrue((repo_root / "prompts").is_dir())
@@ -190,6 +197,14 @@ class OpenAICodexRuntimeConfigTests(unittest.TestCase):
190
197
  {"prompt_tokens": 120, "completion_tokens": 30, "total_tokens": 150},
191
198
  )
192
199
 
200
+ def test_collects_disallowed_shell_wrapper_rejections(self) -> None:
201
+ commands = _collect_disallowed_shell_wrapper_rejections(
202
+ "error=exec_command failed for `/bin/bash -lc pwd`: CreateProcess { message: \"Rejected\" }",
203
+ "error=exec_command failed for `sh -lc \"git diff\"`: Rejected",
204
+ "error=exec_command failed for `pwd`: Rejected",
205
+ )
206
+ self.assertEqual(commands, ["/bin/bash -lc pwd", 'sh -lc "git diff"'])
207
+
193
208
  def test_usage_falls_back_to_estimate_when_trace_has_no_usage(self) -> None:
194
209
  usage = _usage_from_trace_or_estimate({}, "abc" * 30, "done", model="gpt-5.4")
195
210
  self.assertTrue(usage["estimated"])
@@ -40,7 +40,8 @@ status_heartbeat_ms = 120000
40
40
  workerpal_online_ttl_ms = 15000
41
41
  wait_for_workerpal_ms = 15000
42
42
  auto_spawn_workerpals = true
43
- max_workerpals = 3
43
+ min_workerpals = 1
44
+ max_workerpals = 4
44
45
  workerpal_startup_timeout_ms = 10000
45
46
  workerpal_docker = true
46
47
  workerpal_require_docker = true
@@ -23,7 +23,8 @@ codex_timeout_ms = 120000
23
23
  reasoning_effort = "high"
24
24
 
25
25
  [remotebuddy]
26
- max_workerpals = 3
26
+ min_workerpals = 1
27
+ max_workerpals = 4
27
28
  crash_restart_enabled = true
28
29
  crash_restart_max_restarts = 3
29
30
  crash_restart_backoff_ms = 3000
@@ -97,6 +97,7 @@ export interface PushPalsConfig {
97
97
  workerpalOnlineTtlMs: number;
98
98
  waitForWorkerpalMs: number;
99
99
  autoSpawnWorkerpals: boolean;
100
+ minWorkerpals: number;
100
101
  maxWorkerpals: number;
101
102
  workerpalStartupTimeoutMs: number;
102
103
  workerpalDocker: boolean;
@@ -533,6 +534,7 @@ function resolveLlmConfig(
533
534
  10_000,
534
535
  asInt(parseIntEnv(`${envPrefix}_LLM_CODEX_TIMEOUT_MS`) ?? llmNode.codex_timeout_ms, 120_000),
535
536
  );
537
+
536
538
  return {
537
539
  backend,
538
540
  endpoint,
@@ -726,6 +728,17 @@ export function loadPushPalsConfig(options: LoadOptions = {}): PushPalsConfig {
726
728
  200,
727
729
  asInt(parseIntEnv("REMOTEBUDDY_POLL_MS") ?? remoteNode.poll_ms, 2_000),
728
730
  );
731
+ const remoteMaxWorkerpals = Math.max(
732
+ 1,
733
+ asInt(parseIntEnv("REMOTEBUDDY_MAX_WORKERPALS") ?? remoteNode.max_workerpals, 20),
734
+ );
735
+ const remoteMinWorkerpals = Math.max(
736
+ 1,
737
+ Math.min(
738
+ remoteMaxWorkerpals,
739
+ asInt(parseIntEnv("REMOTEBUDDY_MIN_WORKERPALS") ?? remoteNode.min_workerpals, 1),
740
+ ),
741
+ );
729
742
  const remoteLlm = resolveLlmConfig(
730
743
  remoteNode,
731
744
  "REMOTEBUDDY",
@@ -1498,10 +1511,8 @@ export function loadPushPalsConfig(options: LoadOptions = {}): PushPalsConfig {
1498
1511
  autoSpawnWorkerpals:
1499
1512
  parseBoolEnv("REMOTEBUDDY_AUTO_SPAWN_WORKERPALS") ??
1500
1513
  asBoolean(remoteNode.auto_spawn_workerpals, true),
1501
- maxWorkerpals: Math.max(
1502
- 1,
1503
- asInt(parseIntEnv("REMOTEBUDDY_MAX_WORKERPALS") ?? remoteNode.max_workerpals, 20),
1504
- ),
1514
+ minWorkerpals: remoteMinWorkerpals,
1515
+ maxWorkerpals: remoteMaxWorkerpals,
1505
1516
  workerpalStartupTimeoutMs: Math.max(
1506
1517
  1_000,
1507
1518
  asInt(