@pushpalsdev/cli 1.0.61 → 1.0.62

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushpalsdev/cli",
3
- "version": "1.0.61",
3
+ "version": "1.0.62",
4
4
  "description": "PushPals terminal CLI for LocalBuddy -> RemoteBuddy orchestration",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -0,0 +1,10 @@
1
+ ## Base Guidance
2
+ Command-router policy: shell commands are allowed, but invoke the actual command directly instead of wrapping it with `/bin/bash -lc`, `bash -c`, `sh -lc`, `cmd /c`, `powershell -Command`, or `pwsh -Command`. If a wrapper command is rejected, rerun its inner command directly through the command tool.
3
+
4
+ ## Recovery Guidance
5
+ Command-router recovery: the previous attempt retried disallowed shell wrappers.
6
+ Retry once using shell commands normally, but invoke the inner command directly instead of wrapping it in `/bin/bash -lc`, `bash -c`, `sh -lc`, `cmd /c`, `powershell -Command`, or `pwsh -Command`.
7
+ You are not limited to a fixed allowlist of commands. The constraint is only that command execution must target the actual program/argv directly rather than a wrapper shell.
8
+
9
+ ## Rejection Detail
10
+ Codex repeatedly attempted disallowed shell-wrapper commands that the command router rejected. Shell commands are allowed, but wrapper shells are not; invoke the inner command directly and avoid wrapper retries.
@@ -55,6 +55,7 @@ _DEFAULT_TASK_SYSTEM_PROMPT_PATH = "workerpals/openai_codex_default_system_promp
55
55
  _MANDATORY_RUNTIME_POLICY_APPENDIX_PATH = "workerpals/openai_codex_runtime_policy_appendix.md"
56
56
  _INSTRUCTION_WRAPPER_PROMPT_PATH = "workerpals/openai_codex_instruction_wrapper.md"
57
57
  _SUPPLEMENTAL_GUIDANCE_SECTION_PATH = "workerpals/openai_codex_supplemental_guidance_section.md"
58
+ _COMMAND_ROUTER_POLICY_PATH = "workerpals/openai_codex_command_router_policy.md"
58
59
  _CODEX_WORKAROUND_PATTERNS = (
59
60
  re.compile(
60
61
  r"\bcodex cli\b.{0,120}\b(isn't|is not|not)\b.{0,120}\bavailable\b.{0,120}\b(so|therefore|instead|fallback|workaround|without|using)\b",
@@ -94,12 +95,6 @@ _VALID_SANDBOX_POLICIES = {"read-only", "workspace-write", "danger-full-access"}
94
95
  _VALID_COLORS = {"always", "never", "auto"}
95
96
  _VALID_AUTH_MODES = {"auto", "api_key", "chatgpt"}
96
97
  _VALID_REASONING_EFFORTS = {"low", "medium", "high", "xhigh"}
97
- _DIRECT_COMMAND_POLICY_GUIDANCE = (
98
- "Command-router policy: use direct commands only. Do not invoke `/bin/bash -lc`, `bash -c`, "
99
- "`sh -lc`, `cmd /c`, `powershell -Command`, or `pwsh -Command`. Run the direct command "
100
- "instead, such as `pwd`, `git status --porcelain`, `git diff -- path`, `ls dir`, "
101
- "`cat file`, `sed -n '1,160p' file`, or `bun test <path>`."
102
- )
103
98
 
104
99
 
105
100
  def _model_supports_xhigh_reasoning(model: str) -> bool:
@@ -254,6 +249,64 @@ def _load_prompt_template(
254
249
  return _PROMPT_TOKEN_REGEX.sub(_replace, template)
255
250
 
256
251
 
252
+ def _load_markdown_h2_section(relative_path: str, heading: str) -> str:
253
+ document = _load_prompt_template(relative_path)
254
+ if not document:
255
+ return ""
256
+ lines = document.splitlines()
257
+ needle = f"## {heading}".strip().lower()
258
+ start: Optional[int] = None
259
+ for idx, line in enumerate(lines):
260
+ if line.strip().lower() == needle:
261
+ start = idx + 1
262
+ break
263
+ if start is None:
264
+ return ""
265
+ collected: List[str] = []
266
+ for line in lines[start:]:
267
+ if line.startswith("## "):
268
+ break
269
+ collected.append(line)
270
+ return "\n".join(collected).strip()
271
+
272
+
273
+ def _command_router_policy_guidance() -> str:
274
+ guidance = _load_markdown_h2_section(_COMMAND_ROUTER_POLICY_PATH, "Base Guidance")
275
+ if guidance:
276
+ return guidance
277
+ return (
278
+ "Command-router policy: shell commands are allowed, but invoke the actual command directly "
279
+ "instead of wrapping it with `/bin/bash -lc`, `bash -c`, `sh -lc`, `cmd /c`, "
280
+ "`powershell -Command`, or `pwsh -Command`. If a wrapper command is rejected, rerun its "
281
+ "inner command directly through the command tool."
282
+ )
283
+
284
+
285
+ def _command_router_recovery_guidance() -> str:
286
+ guidance = _load_markdown_h2_section(_COMMAND_ROUTER_POLICY_PATH, "Recovery Guidance")
287
+ if guidance:
288
+ return guidance
289
+ return (
290
+ "Command-router recovery: the previous attempt retried disallowed shell wrappers.\n"
291
+ "Retry once using shell commands normally, but invoke the inner command directly instead of "
292
+ "wrapping it in `/bin/bash -lc`, `bash -c`, `sh -lc`, `cmd /c`, `powershell -Command`, or "
293
+ "`pwsh -Command`.\n"
294
+ "You are not limited to a fixed allowlist of commands. The constraint is only that command "
295
+ "execution must target the actual program/argv directly rather than a wrapper shell."
296
+ )
297
+
298
+
299
+ def _command_router_rejection_detail_intro() -> str:
300
+ guidance = _load_markdown_h2_section(_COMMAND_ROUTER_POLICY_PATH, "Rejection Detail")
301
+ if guidance:
302
+ return guidance
303
+ return (
304
+ "Codex repeatedly attempted disallowed shell-wrapper commands that the command router "
305
+ "rejected. Shell commands are allowed, but wrapper shells are not; invoke the inner "
306
+ "command directly and avoid wrapper retries."
307
+ )
308
+
309
+
257
310
  def _to_positive_int(raw: str) -> Optional[int]:
258
311
  try:
259
312
  parsed = int(raw)
@@ -1023,11 +1076,7 @@ def _build_wrapper_recovery_guidance(rejected_commands: List[str]) -> str:
1023
1076
  continue
1024
1077
  seen.add(lowered)
1025
1078
  direct_equivalents.append(f"- `{command}` -> `{direct}`")
1026
- guidance_lines = [
1027
- "Command-router recovery: the previous attempt retried disallowed shell wrappers.",
1028
- "Retry once using direct commands only. Do not use `/bin/bash -lc`, `bash -c`, `sh -lc`, `cmd /c`, `powershell -Command`, `pwsh -Command`, pipelines, or chained shell snippets.",
1029
- "If you need to inspect files or git state, run the direct command itself (for example `git diff --name-only`, `git status --porcelain`, `ls path`, `cat file`, or `sed -n '1,120p' file`).",
1030
- ]
1079
+ guidance_lines = [_command_router_recovery_guidance()]
1031
1080
  if direct_equivalents:
1032
1081
  guidance_lines.append("Use these direct replacements for the rejected commands:")
1033
1082
  guidance_lines.extend(direct_equivalents[:6])
@@ -1064,7 +1113,7 @@ def _augment_supplemental_guidance(supplemental_guidance: List[str]) -> List[str
1064
1113
  joined = "\n".join(normalized).lower()
1065
1114
  if "direct commands only" in joined or "shell-wrapper" in joined or "/bin/bash -lc" in joined:
1066
1115
  return normalized
1067
- return [_DIRECT_COMMAND_POLICY_GUIDANCE, *normalized]
1116
+ return [_command_router_policy_guidance(), *normalized]
1068
1117
 
1069
1118
 
1070
1119
  def _read_text_if_exists(path: Path) -> str:
@@ -1503,8 +1552,7 @@ def _run_codex_task(
1503
1552
  else "- (no command details captured)"
1504
1553
  )
1505
1554
  detail = (
1506
- "Codex repeatedly attempted disallowed shell-wrapper commands that the command "
1507
- "router rejected. Switch to direct commands only and avoid wrapper retries.\n"
1555
+ f"{_command_router_rejection_detail_intro()}\n"
1508
1556
  f"Rejected commands:\n{command_lines}"
1509
1557
  )
1510
1558
  if last_message:
@@ -21,6 +21,7 @@ from executor_base import (
21
21
  from openai_codex_executor import (
22
22
  OpenAICodexRuntimeConfig,
23
23
  _augment_supplemental_guidance,
24
+ _build_wrapper_recovery_guidance,
24
25
  _resolve_reasoning_effort,
25
26
  _build_instruction,
26
27
  _collect_disallowed_shell_wrapper_rejections,
@@ -266,10 +267,19 @@ class OpenAICodexRuntimeConfigTests(unittest.TestCase):
266
267
  def test_augments_guidance_with_direct_command_policy_once(self) -> None:
267
268
  guidance = _augment_supplemental_guidance(["Run bun test tests/example.test.ts"])
268
269
  self.assertGreaterEqual(len(guidance), 2)
269
- self.assertIn("direct commands only", guidance[0].lower())
270
+ self.assertIn("shell commands are allowed", guidance[0].lower())
270
271
  guidance_again = _augment_supplemental_guidance(guidance)
271
272
  self.assertEqual(guidance_again, guidance)
272
273
 
274
+ def test_wrapper_recovery_guidance_allows_arbitrary_shell_commands_without_wrappers(self) -> None:
275
+ guidance = _build_wrapper_recovery_guidance(
276
+ ["/bin/bash -lc 'git status --porcelain'", "/bin/bash -lc pwd"]
277
+ )
278
+ lowered = guidance.lower()
279
+ self.assertIn("shell commands normally", lowered)
280
+ self.assertIn("not limited to a fixed allowlist", lowered)
281
+ self.assertIn("`/bin/bash -lc 'git status --porcelain'` -> `git status --porcelain`", guidance)
282
+
273
283
  def test_usage_falls_back_to_estimate_when_trace_has_no_usage(self) -> None:
274
284
  usage = _usage_from_trace_or_estimate({}, "abc" * 30, "done", model="gpt-5.4")
275
285
  self.assertTrue(usage["estimated"])
@@ -757,11 +757,17 @@ export class DockerExecutor {
757
757
  if (backend !== "openai_codex") return [];
758
758
 
759
759
  const hostCodexHomeRaw = (process.env.PUSHPALS_OPENAI_CODEX_HOST_CODEX_HOME || "").trim();
760
+ if (hostCodexHomeRaw && !isAbsolute(hostCodexHomeRaw)) {
761
+ console.warn(
762
+ `[DockerExecutor] Ignoring relative PUSHPALS_OPENAI_CODEX_HOST_CODEX_HOME=${hostCodexHomeRaw}; using ${resolve(
763
+ homedir(),
764
+ ".codex",
765
+ )} so Codex state stays outside the repo worktree.`,
766
+ );
767
+ }
760
768
  const hostCodexHome = (
761
- hostCodexHomeRaw
762
- ? isAbsolute(hostCodexHomeRaw)
763
- ? hostCodexHomeRaw
764
- : resolve(this.options.repo, hostCodexHomeRaw)
769
+ hostCodexHomeRaw && isAbsolute(hostCodexHomeRaw)
770
+ ? hostCodexHomeRaw
765
771
  : resolve(homedir(), ".codex")
766
772
  ).trim();
767
773
  if (!hostCodexHome) return [];
@@ -3,7 +3,7 @@
3
3
  * Used by both the host Worker (direct mode) and the Docker job runner.
4
4
  */
5
5
 
6
- import { existsSync, readFileSync, unlinkSync } from "fs";
6
+ import { existsSync, readFileSync, rmSync, unlinkSync } from "fs";
7
7
  import { resolve } from "path";
8
8
  import {
9
9
  deriveAutonomyComponentArea,
@@ -2606,6 +2606,41 @@ export async function syncHiddenRefWithRemoteBranchByRebase(
2606
2606
  publicBranchName: string,
2607
2607
  jobId: string,
2608
2608
  ): Promise<{ ok: true; sha: string } | { ok: false; error: string }> {
2609
+ const scrubKnownPreSyncArtifacts = async (): Promise<
2610
+ { ok: true } | { ok: false; error: string }
2611
+ > => {
2612
+ const codexPath = resolve(repo, ".codex");
2613
+ if (!existsSync(codexPath)) return { ok: true };
2614
+
2615
+ const trackedCodex = await git(repo, ["ls-files", "--error-unmatch", "--", ".codex"]);
2616
+ if (trackedCodex.ok) {
2617
+ return {
2618
+ ok: false,
2619
+ error:
2620
+ "Tracked .codex path blocks branch sync. Move Codex state outside the repo worktree before retrying.",
2621
+ };
2622
+ }
2623
+
2624
+ try {
2625
+ rmSync(codexPath, { recursive: true, force: true });
2626
+ } catch (error) {
2627
+ return {
2628
+ ok: false,
2629
+ error: `Failed to scrub transient .codex artifact before branch sync: ${String(error)}`,
2630
+ };
2631
+ }
2632
+
2633
+ if (existsSync(codexPath)) {
2634
+ return {
2635
+ ok: false,
2636
+ error: "Failed to scrub transient .codex artifact before branch sync: path still exists.",
2637
+ };
2638
+ }
2639
+
2640
+ console.warn("[WorkerPals] Removed transient .codex artifact before branch sync.");
2641
+ return { ok: true };
2642
+ };
2643
+
2609
2644
  const pullRebaseNonInteractive = () =>
2610
2645
  git(repo, [
2611
2646
  "-c",
@@ -2652,6 +2687,10 @@ export async function syncHiddenRefWithRemoteBranchByRebase(
2652
2687
  const maxPullRebaseAttempts = 5;
2653
2688
  let syncedWithRemote = false;
2654
2689
  for (let attempt = 1; attempt <= maxPullRebaseAttempts; attempt++) {
2690
+ const preSyncGuard = await scrubKnownPreSyncArtifacts();
2691
+ if (!preSyncGuard.ok) {
2692
+ return { ok: false, error: preSyncGuard.error };
2693
+ }
2655
2694
  let pullRebase = await pullRebaseNonInteractive();
2656
2695
  if (!pullRebase.ok && isPullRebaseDirtyWorkingTreeOutput(combinedGitOutput(pullRebase))) {
2657
2696
  // Recover from dirty index/worktree left by previous attempts and retry non-interactively.
@@ -0,0 +1,10 @@
1
+ ## Base Guidance
2
+ Command-router policy: shell commands are allowed, but invoke the actual command directly instead of wrapping it with `/bin/bash -lc`, `bash -c`, `sh -lc`, `cmd /c`, `powershell -Command`, or `pwsh -Command`. If a wrapper command is rejected, rerun its inner command directly through the command tool.
3
+
4
+ ## Recovery Guidance
5
+ Command-router recovery: the previous attempt retried disallowed shell wrappers.
6
+ Retry once using shell commands normally, but invoke the inner command directly instead of wrapping it in `/bin/bash -lc`, `bash -c`, `sh -lc`, `cmd /c`, `powershell -Command`, or `pwsh -Command`.
7
+ You are not limited to a fixed allowlist of commands. The constraint is only that command execution must target the actual program/argv directly rather than a wrapper shell.
8
+
9
+ ## Rejection Detail
10
+ Codex repeatedly attempted disallowed shell-wrapper commands that the command router rejected. Shell commands are allowed, but wrapper shells are not; invoke the inner command directly and avoid wrapper retries.