@pushpalsdev/cli 1.0.86 → 1.0.94
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 +1 -1
- package/package.json +2 -2
- package/runtime/prompts/remotebuddy/autonomy_ideation_system_prompt.md +2 -1
- package/runtime/prompts/remotebuddy/autonomy_planning_system_prompt.md +1 -1
- package/runtime/prompts/remotebuddy/remotebuddy_system_prompt.md +4 -4
- package/runtime/prompts/workerpals/miniswe_completion_requirement.md +1 -1
- package/runtime/prompts/workerpals/miniswe_explicit_targets_block.md +1 -1
- package/runtime/prompts/workerpals/openai_codex_task_execute_system_prompt.md +4 -1
- package/runtime/prompts/workerpals/openhands_minimal_system_prompt.j2 +3 -1
- package/runtime/prompts/workerpals/openhands_task_execute_system_prompt.md +2 -1
- package/runtime/prompts/workerpals/workerpals_system_prompt.md +2 -2
- package/runtime/sandbox/.pushpals-remotebuddy-fallback.js +248 -98
- package/runtime/sandbox/apps/workerpals/src/backends/miniswe/miniswe_executor.py +5 -34
- package/runtime/sandbox/apps/workerpals/src/backends/openai_codex/openai_codex_executor.py +219 -130
- package/runtime/sandbox/apps/workerpals/src/backends/openai_codex/test_openai_codex_runtime_config.py +57 -0
- package/runtime/sandbox/apps/workerpals/src/backends/openhands/openhands_executor.py +3 -2
- package/runtime/sandbox/apps/workerpals/src/execute_job.ts +142 -134
- package/runtime/sandbox/apps/workerpals/src/workerpals_main.ts +70 -25
- package/runtime/sandbox/packages/shared/src/autonomy_policy.ts +14 -8
- package/runtime/sandbox/packages/shared/src/communication.ts +4 -1
- package/runtime/sandbox/packages/shared/src/config.ts +1 -1
- package/runtime/sandbox/prompts/workerpals/miniswe_completion_requirement.md +1 -1
- package/runtime/sandbox/prompts/workerpals/miniswe_explicit_targets_block.md +1 -1
- package/runtime/sandbox/prompts/workerpals/openai_codex_task_execute_system_prompt.md +4 -1
- package/runtime/sandbox/prompts/workerpals/openhands_minimal_system_prompt.j2 +3 -1
- package/runtime/sandbox/prompts/workerpals/openhands_task_execute_system_prompt.md +2 -1
- package/runtime/sandbox/prompts/workerpals/workerpals_system_prompt.md +2 -2
|
@@ -465,25 +465,9 @@ def _extract_write_globs_from_payload(payload: Optional[Dict[str, Any]]) -> List
|
|
|
465
465
|
|
|
466
466
|
|
|
467
467
|
def _assert_write_allowed(repo: str, path: str, write_globs: Optional[List[str]]) -> None:
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
if not normalized:
|
|
472
|
-
raise RuntimeError(f"Invalid write path for scope enforcement: {path!r}")
|
|
473
|
-
for glob in write_globs:
|
|
474
|
-
pattern = str(glob or "").strip()
|
|
475
|
-
if not pattern:
|
|
476
|
-
continue
|
|
477
|
-
if any(ch in pattern for ch in "*?[]"):
|
|
478
|
-
if fnmatch.fnmatchcase(normalized, pattern):
|
|
479
|
-
return
|
|
480
|
-
continue
|
|
481
|
-
if normalized == pattern or normalized.startswith(pattern + "/"):
|
|
482
|
-
return
|
|
483
|
-
raise RuntimeError(
|
|
484
|
-
"Scope violation: attempted write outside writeGlobs. "
|
|
485
|
-
f"path={normalized!r} write_globs={write_globs!r}"
|
|
486
|
-
)
|
|
468
|
+
# WorkerPal jobs run in isolated sandboxes. Scope hints are used for review
|
|
469
|
+
# relevance, not per-write filesystem enforcement.
|
|
470
|
+
return
|
|
487
471
|
|
|
488
472
|
|
|
489
473
|
def _read_text_file(repo: str, path: str, max_chars: int = 60000) -> str:
|
|
@@ -1587,11 +1571,6 @@ def _broker_run(
|
|
|
1587
1571
|
if expected_targets and changed_paths:
|
|
1588
1572
|
changed_set = {str(p).strip().replace("\\", "/") for p in changed_paths}
|
|
1589
1573
|
expected_set = {str(p).strip().replace("\\", "/") for p in expected_targets}
|
|
1590
|
-
strict_target_match = bool(
|
|
1591
|
-
explicit_target_set
|
|
1592
|
-
and not any(t in {".", "/"} for t in explicit_target_set)
|
|
1593
|
-
and not any(any(ch in t for ch in "*?[]") for t in explicit_target_set)
|
|
1594
|
-
)
|
|
1595
1574
|
matched = any(
|
|
1596
1575
|
_target_hint_matches_changed_path(expected, changed)
|
|
1597
1576
|
for expected in expected_set
|
|
@@ -1602,14 +1581,6 @@ def _broker_run(
|
|
|
1602
1581
|
"Expected one of target paths to change, but observed different files. "
|
|
1603
1582
|
f"expected={sorted(expected_set)} observed={sorted(changed_set)}"
|
|
1604
1583
|
)
|
|
1605
|
-
if strict_target_match:
|
|
1606
|
-
return {
|
|
1607
|
-
"ok": False,
|
|
1608
|
-
"summary": "tool broker failed: changed files do not match explicit target paths",
|
|
1609
|
-
"stdout": stdout + "\n\nChanged files:\n" + "\n".join(f"- {p}" for p in changed_paths),
|
|
1610
|
-
"stderr": msg,
|
|
1611
|
-
"exitCode": 3,
|
|
1612
|
-
}
|
|
1613
1584
|
stdout += "\n\nTarget-path mismatch (heuristic, non-fatal):\n" + msg
|
|
1614
1585
|
if edits_made and not shell_validation_ran:
|
|
1615
1586
|
stdout += (
|
|
@@ -1786,10 +1757,10 @@ def _run_miniswe_task(
|
|
|
1786
1757
|
agent = None
|
|
1787
1758
|
agent_messages: List[Dict[str, Any]] = []
|
|
1788
1759
|
broker_enabled = _tool_broker_enabled(base_url)
|
|
1789
|
-
prefer_broker_for_scoped_writes =
|
|
1760
|
+
prefer_broker_for_scoped_writes = False
|
|
1790
1761
|
ran_primary_broker = False
|
|
1791
1762
|
if prefer_broker_for_scoped_writes and broker_enabled:
|
|
1792
|
-
log.info("Using tool broker shim for
|
|
1763
|
+
log.info("Using tool broker shim for task execution.")
|
|
1793
1764
|
broker_result = _run_broker_with_recovery()
|
|
1794
1765
|
if not bool(broker_result.get("ok")):
|
|
1795
1766
|
return {
|
|
@@ -10,7 +10,7 @@ from __future__ import annotations
|
|
|
10
10
|
import json
|
|
11
11
|
import os
|
|
12
12
|
import re
|
|
13
|
-
from shutil import which
|
|
13
|
+
from shutil import rmtree, which
|
|
14
14
|
import shlex
|
|
15
15
|
import signal
|
|
16
16
|
import subprocess
|
|
@@ -21,7 +21,7 @@ import time
|
|
|
21
21
|
import traceback
|
|
22
22
|
from dataclasses import dataclass
|
|
23
23
|
from pathlib import Path
|
|
24
|
-
from typing import Any, Dict, List, Optional
|
|
24
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
25
25
|
|
|
26
26
|
_SHARED_DIR = Path(__file__).resolve().parents[1] / "shared"
|
|
27
27
|
if str(_SHARED_DIR) not in sys.path:
|
|
@@ -386,6 +386,87 @@ def _is_git_repo(repo: str) -> bool:
|
|
|
386
386
|
return False
|
|
387
387
|
|
|
388
388
|
|
|
389
|
+
def _codex_project_config_roots(repo: str, env: Dict[str, str]) -> List[Path]:
|
|
390
|
+
roots: List[Path] = []
|
|
391
|
+
seen: set[str] = set()
|
|
392
|
+
|
|
393
|
+
def add(raw: object) -> None:
|
|
394
|
+
text = str(raw or "").strip()
|
|
395
|
+
if not text:
|
|
396
|
+
return
|
|
397
|
+
try:
|
|
398
|
+
path = Path(text).resolve()
|
|
399
|
+
except Exception:
|
|
400
|
+
return
|
|
401
|
+
key = str(path)
|
|
402
|
+
if key in seen:
|
|
403
|
+
return
|
|
404
|
+
seen.add(key)
|
|
405
|
+
roots.append(path)
|
|
406
|
+
|
|
407
|
+
add(repo)
|
|
408
|
+
try:
|
|
409
|
+
proc = subprocess.run(
|
|
410
|
+
["git", "rev-parse", "--show-toplevel"],
|
|
411
|
+
cwd=repo,
|
|
412
|
+
capture_output=True,
|
|
413
|
+
text=True,
|
|
414
|
+
timeout=10,
|
|
415
|
+
check=False,
|
|
416
|
+
)
|
|
417
|
+
if proc.returncode == 0:
|
|
418
|
+
add((proc.stdout or "").strip())
|
|
419
|
+
except Exception:
|
|
420
|
+
pass
|
|
421
|
+
|
|
422
|
+
for key in (
|
|
423
|
+
"PUSHPALS_REPO_ROOT_OVERRIDE",
|
|
424
|
+
"PUSHPALS_PROJECT_ROOT_OVERRIDE",
|
|
425
|
+
"PUSHPALS_ASSIGNED_REPO_ROOT",
|
|
426
|
+
"PUSHPALS_REPO_PATH",
|
|
427
|
+
):
|
|
428
|
+
add(env.get(key))
|
|
429
|
+
return roots
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def _mask_repo_local_codex_files(repo: str, env: Dict[str, str]) -> List[Tuple[Path, Path]]:
|
|
433
|
+
masked: List[Tuple[Path, Path]] = []
|
|
434
|
+
for root in _codex_project_config_roots(repo, env):
|
|
435
|
+
codex_path = root / ".codex"
|
|
436
|
+
if not os.path.lexists(codex_path):
|
|
437
|
+
continue
|
|
438
|
+
if codex_path.is_dir():
|
|
439
|
+
continue
|
|
440
|
+
backup = root / f".codex.pushpals-masked-{os.getpid()}-{len(masked)}"
|
|
441
|
+
suffix = 0
|
|
442
|
+
while os.path.lexists(backup):
|
|
443
|
+
suffix += 1
|
|
444
|
+
backup = root / f".codex.pushpals-masked-{os.getpid()}-{len(masked)}-{suffix}"
|
|
445
|
+
try:
|
|
446
|
+
os.replace(codex_path, backup)
|
|
447
|
+
masked.append((codex_path, backup))
|
|
448
|
+
log.info(
|
|
449
|
+
f"Temporarily masked repo-local .codex file so Codex CLI can use CODEX_HOME: {codex_path}"
|
|
450
|
+
)
|
|
451
|
+
except Exception as exc:
|
|
452
|
+
log.warning(f"Failed to mask repo-local .codex file {codex_path}: {exc}")
|
|
453
|
+
return masked
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def _restore_repo_local_codex_files(masked: List[Tuple[Path, Path]]) -> None:
|
|
457
|
+
for codex_path, backup in reversed(masked):
|
|
458
|
+
try:
|
|
459
|
+
if os.path.lexists(codex_path):
|
|
460
|
+
if codex_path.is_dir() and not codex_path.is_symlink():
|
|
461
|
+
rmtree(codex_path)
|
|
462
|
+
else:
|
|
463
|
+
codex_path.unlink()
|
|
464
|
+
if os.path.lexists(backup):
|
|
465
|
+
os.replace(backup, codex_path)
|
|
466
|
+
except Exception as exc:
|
|
467
|
+
log.warning(f"Failed to restore repo-local .codex file {codex_path}: {exc}")
|
|
468
|
+
|
|
469
|
+
|
|
389
470
|
def _resolve_codex_command_prefix(config: OpenAICodexRuntimeConfig) -> List[str]:
|
|
390
471
|
override_json = config.codex_bin_json
|
|
391
472
|
if override_json:
|
|
@@ -698,7 +779,7 @@ def _summarize_json_event(obj: Dict[str, Any]) -> str:
|
|
|
698
779
|
event_type = "event"
|
|
699
780
|
# Skip noisy streaming deltas unless they contain meaningful text fragments.
|
|
700
781
|
delta_like = event_type.endswith(".delta") or event_type.endswith("_delta")
|
|
701
|
-
# Reasoning/thinking events are always surfaced
|
|
782
|
+
# Reasoning/thinking events are always surfaced because they show the model's reasoning process.
|
|
702
783
|
reasoning_like = _contains_reasoning_marker(event_type) or _event_contains_reasoning(obj)
|
|
703
784
|
|
|
704
785
|
tool_name = ""
|
|
@@ -1487,7 +1568,11 @@ def _run_codex_task(
|
|
|
1487
1568
|
env.pop("OPENAI_API_KEY", None)
|
|
1488
1569
|
env.pop("OPENAI_BASE_URL", None)
|
|
1489
1570
|
env.pop("OPENAI_API_BASE", None)
|
|
1490
|
-
|
|
1571
|
+
codex_project_mask = _mask_repo_local_codex_files(repo, env)
|
|
1572
|
+
try:
|
|
1573
|
+
login_status = _run_codex_login_status(codex_cmd_prefix, repo, env)
|
|
1574
|
+
finally:
|
|
1575
|
+
_restore_repo_local_codex_files(codex_project_mask)
|
|
1491
1576
|
if not login_status.get("ok"):
|
|
1492
1577
|
detail = (
|
|
1493
1578
|
str(login_status.get("stderr") or "").strip()
|
|
@@ -1554,148 +1639,152 @@ def _run_codex_task(
|
|
|
1554
1639
|
if communicate_timeout_s:
|
|
1555
1640
|
log.debug(f"communicate timeout: {communicate_timeout_s}s")
|
|
1556
1641
|
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
if chunk == "":
|
|
1586
|
-
break
|
|
1587
|
-
stdout_chunks.append(chunk)
|
|
1588
|
-
line = chunk.strip()
|
|
1589
|
-
if not line:
|
|
1590
|
-
continue
|
|
1591
|
-
with trace_lock:
|
|
1592
|
-
last_activity_at["ts"] = time.monotonic()
|
|
1593
|
-
_record_live_codex_stdout_line(line, use_json, stdout_trace_state)
|
|
1594
|
-
except Exception:
|
|
1595
|
-
pass
|
|
1596
|
-
finally:
|
|
1642
|
+
codex_project_mask = _mask_repo_local_codex_files(repo, env)
|
|
1643
|
+
try:
|
|
1644
|
+
proc = subprocess.Popen(
|
|
1645
|
+
cmd,
|
|
1646
|
+
cwd=repo,
|
|
1647
|
+
env=env,
|
|
1648
|
+
stdout=subprocess.PIPE,
|
|
1649
|
+
stderr=subprocess.PIPE,
|
|
1650
|
+
stdin=subprocess.PIPE,
|
|
1651
|
+
text=True,
|
|
1652
|
+
encoding="utf-8",
|
|
1653
|
+
errors="replace",
|
|
1654
|
+
)
|
|
1655
|
+
_ACTIVE_CHILD = proc
|
|
1656
|
+
started_at = time.monotonic()
|
|
1657
|
+
progress_interval_s = _resolve_progress_log_interval_seconds(runtime_config)
|
|
1658
|
+
|
|
1659
|
+
stdout_chunks: List[str] = []
|
|
1660
|
+
stderr_chunks: List[str] = []
|
|
1661
|
+
stdout_trace_state = _empty_codex_trace()
|
|
1662
|
+
trace_lock = threading.Lock()
|
|
1663
|
+
last_activity_at = {"ts": started_at}
|
|
1664
|
+
wrapper_rejection_state: Dict[str, Any] = {"count": 0, "commands": []}
|
|
1665
|
+
|
|
1666
|
+
def _drain_stdout() -> None:
|
|
1667
|
+
stream = proc.stdout
|
|
1668
|
+
if stream is None:
|
|
1669
|
+
return
|
|
1597
1670
|
try:
|
|
1598
|
-
stream.
|
|
1671
|
+
for chunk in iter(stream.readline, ""):
|
|
1672
|
+
if chunk == "":
|
|
1673
|
+
break
|
|
1674
|
+
stdout_chunks.append(chunk)
|
|
1675
|
+
line = chunk.strip()
|
|
1676
|
+
if not line:
|
|
1677
|
+
continue
|
|
1678
|
+
with trace_lock:
|
|
1679
|
+
last_activity_at["ts"] = time.monotonic()
|
|
1680
|
+
_record_live_codex_stdout_line(line, use_json, stdout_trace_state)
|
|
1599
1681
|
except Exception:
|
|
1600
1682
|
pass
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
rejected_commands = _collect_disallowed_shell_wrapper_rejections(chunk)
|
|
1612
|
-
if rejected_commands:
|
|
1613
|
-
with trace_lock:
|
|
1614
|
-
wrapper_rejection_state["count"] = to_int(
|
|
1615
|
-
wrapper_rejection_state.get("count"), 0
|
|
1616
|
-
) + len(rejected_commands)
|
|
1617
|
-
tracked = wrapper_rejection_state.get("commands")
|
|
1618
|
-
if not isinstance(tracked, list):
|
|
1619
|
-
tracked = []
|
|
1620
|
-
for command in rejected_commands:
|
|
1621
|
-
lowered = command.lower()
|
|
1622
|
-
if any(str(item).lower() == lowered for item in tracked):
|
|
1623
|
-
continue
|
|
1624
|
-
tracked.append(command)
|
|
1625
|
-
wrapper_rejection_state["commands"] = tracked[:6]
|
|
1626
|
-
except Exception:
|
|
1627
|
-
pass
|
|
1628
|
-
finally:
|
|
1683
|
+
finally:
|
|
1684
|
+
try:
|
|
1685
|
+
stream.close()
|
|
1686
|
+
except Exception:
|
|
1687
|
+
pass
|
|
1688
|
+
|
|
1689
|
+
def _drain_stderr() -> None:
|
|
1690
|
+
stream = proc.stderr
|
|
1691
|
+
if stream is None:
|
|
1692
|
+
return
|
|
1629
1693
|
try:
|
|
1630
|
-
stream.
|
|
1694
|
+
for chunk in iter(stream.readline, ""):
|
|
1695
|
+
if chunk == "":
|
|
1696
|
+
break
|
|
1697
|
+
stderr_chunks.append(chunk)
|
|
1698
|
+
rejected_commands = _collect_disallowed_shell_wrapper_rejections(chunk)
|
|
1699
|
+
if rejected_commands:
|
|
1700
|
+
with trace_lock:
|
|
1701
|
+
wrapper_rejection_state["count"] = to_int(
|
|
1702
|
+
wrapper_rejection_state.get("count"), 0
|
|
1703
|
+
) + len(rejected_commands)
|
|
1704
|
+
tracked = wrapper_rejection_state.get("commands")
|
|
1705
|
+
if not isinstance(tracked, list):
|
|
1706
|
+
tracked = []
|
|
1707
|
+
for command in rejected_commands:
|
|
1708
|
+
lowered = command.lower()
|
|
1709
|
+
if any(str(item).lower() == lowered for item in tracked):
|
|
1710
|
+
continue
|
|
1711
|
+
tracked.append(command)
|
|
1712
|
+
wrapper_rejection_state["commands"] = tracked[:6]
|
|
1713
|
+
except Exception:
|
|
1714
|
+
pass
|
|
1715
|
+
finally:
|
|
1716
|
+
try:
|
|
1717
|
+
stream.close()
|
|
1718
|
+
except Exception:
|
|
1719
|
+
pass
|
|
1720
|
+
|
|
1721
|
+
stdout_thread = threading.Thread(target=_drain_stdout, daemon=True)
|
|
1722
|
+
stderr_thread = threading.Thread(target=_drain_stderr, daemon=True)
|
|
1723
|
+
stdout_thread.start()
|
|
1724
|
+
stderr_thread.start()
|
|
1725
|
+
|
|
1726
|
+
if proc.stdin is not None:
|
|
1727
|
+
try:
|
|
1728
|
+
proc.stdin.write(prompt)
|
|
1729
|
+
proc.stdin.close()
|
|
1631
1730
|
except Exception:
|
|
1632
1731
|
pass
|
|
1633
1732
|
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
proc.stdin.close()
|
|
1643
|
-
except Exception:
|
|
1644
|
-
pass
|
|
1645
|
-
|
|
1646
|
-
deadline = (
|
|
1647
|
-
started_at + float(communicate_timeout_s)
|
|
1648
|
-
if communicate_timeout_s and communicate_timeout_s > 0
|
|
1649
|
-
else None
|
|
1650
|
-
)
|
|
1651
|
-
next_progress_at = started_at + float(progress_interval_s)
|
|
1652
|
-
timed_out = False
|
|
1653
|
-
command_policy_rejection_loop = False
|
|
1654
|
-
|
|
1655
|
-
while proc.poll() is None:
|
|
1656
|
-
now = time.monotonic()
|
|
1657
|
-
if deadline is not None and now >= deadline:
|
|
1658
|
-
timed_out = True
|
|
1659
|
-
_terminate_active_child()
|
|
1660
|
-
break
|
|
1733
|
+
deadline = (
|
|
1734
|
+
started_at + float(communicate_timeout_s)
|
|
1735
|
+
if communicate_timeout_s and communicate_timeout_s > 0
|
|
1736
|
+
else None
|
|
1737
|
+
)
|
|
1738
|
+
next_progress_at = started_at + float(progress_interval_s)
|
|
1739
|
+
timed_out = False
|
|
1740
|
+
command_policy_rejection_loop = False
|
|
1661
1741
|
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1742
|
+
while proc.poll() is None:
|
|
1743
|
+
now = time.monotonic()
|
|
1744
|
+
if deadline is not None and now >= deadline:
|
|
1745
|
+
timed_out = True
|
|
1746
|
+
_terminate_active_child()
|
|
1747
|
+
break
|
|
1668
1748
|
|
|
1669
|
-
if now >= next_progress_at:
|
|
1670
|
-
elapsed = int(max(0.0, now - started_at))
|
|
1671
1749
|
with trace_lock:
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
)
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1750
|
+
wrapper_rejections = to_int(wrapper_rejection_state.get("count"), 0)
|
|
1751
|
+
if wrapper_rejections >= 3:
|
|
1752
|
+
command_policy_rejection_loop = True
|
|
1753
|
+
_terminate_active_child()
|
|
1754
|
+
break
|
|
1755
|
+
|
|
1756
|
+
if now >= next_progress_at:
|
|
1757
|
+
elapsed = int(max(0.0, now - started_at))
|
|
1758
|
+
with trace_lock:
|
|
1759
|
+
last_event = float(last_activity_at.get("ts", started_at))
|
|
1760
|
+
valid_json = to_int(stdout_trace_state.get("valid_json"), 0)
|
|
1761
|
+
total_lines = to_int(stdout_trace_state.get("line_count"), 0)
|
|
1762
|
+
idle_for = int(max(0.0, now - last_event))
|
|
1763
|
+
if use_json:
|
|
1764
|
+
log.info(
|
|
1765
|
+
f"codex exec still running ({elapsed}s elapsed, json_events={valid_json}, idle={idle_for}s)"
|
|
1766
|
+
)
|
|
1767
|
+
else:
|
|
1768
|
+
log.info(
|
|
1769
|
+
f"codex exec still running ({elapsed}s elapsed, stdout_lines={total_lines}, idle={idle_for}s)"
|
|
1770
|
+
)
|
|
1771
|
+
next_progress_at = now + float(progress_interval_s)
|
|
1685
1772
|
|
|
1686
|
-
|
|
1773
|
+
time.sleep(1.0)
|
|
1687
1774
|
|
|
1688
|
-
try:
|
|
1689
|
-
proc.wait(timeout=5)
|
|
1690
|
-
except Exception:
|
|
1691
1775
|
try:
|
|
1692
|
-
proc.kill()
|
|
1693
1776
|
proc.wait(timeout=5)
|
|
1694
1777
|
except Exception:
|
|
1695
|
-
|
|
1778
|
+
try:
|
|
1779
|
+
proc.kill()
|
|
1780
|
+
proc.wait(timeout=5)
|
|
1781
|
+
except Exception:
|
|
1782
|
+
pass
|
|
1696
1783
|
|
|
1697
|
-
|
|
1698
|
-
|
|
1784
|
+
stdout_thread.join(timeout=2)
|
|
1785
|
+
stderr_thread.join(timeout=2)
|
|
1786
|
+
finally:
|
|
1787
|
+
_restore_repo_local_codex_files(codex_project_mask)
|
|
1699
1788
|
|
|
1700
1789
|
return_code = proc.returncode
|
|
1701
1790
|
_ACTIVE_CHILD = None
|
|
@@ -33,7 +33,9 @@ from openai_codex_executor import (
|
|
|
33
33
|
_detect_codex_workaround_signal,
|
|
34
34
|
_extract_usage_counts,
|
|
35
35
|
_load_prompt_template,
|
|
36
|
+
_mask_repo_local_codex_files,
|
|
36
37
|
_repo_root_for_prompt_loading,
|
|
38
|
+
_restore_repo_local_codex_files,
|
|
37
39
|
_resolve_codex_command_prefix,
|
|
38
40
|
_unwrap_shell_wrapper_command,
|
|
39
41
|
_usage_from_trace_or_estimate,
|
|
@@ -80,6 +82,61 @@ class OpenAICodexRuntimeConfigTests(unittest.TestCase):
|
|
|
80
82
|
self.assertEqual(cfg.reasoning_effort, "xhigh")
|
|
81
83
|
self.assertFalse(cfg.json_output)
|
|
82
84
|
|
|
85
|
+
def test_masks_and_restores_repo_local_codex_file(self) -> None:
|
|
86
|
+
with tempfile.TemporaryDirectory(prefix="pushpals-codex-mask-") as root:
|
|
87
|
+
repo = Path(root) / "repo"
|
|
88
|
+
repo.mkdir()
|
|
89
|
+
codex_file = repo / ".codex"
|
|
90
|
+
codex_file.write_text("tracked repo sentinel\n", encoding="utf-8")
|
|
91
|
+
|
|
92
|
+
masked = _mask_repo_local_codex_files(str(repo), {})
|
|
93
|
+
try:
|
|
94
|
+
self.assertFalse(codex_file.exists())
|
|
95
|
+
self.assertEqual(len(masked), 1)
|
|
96
|
+
self.assertTrue(masked[0][1].exists())
|
|
97
|
+
finally:
|
|
98
|
+
_restore_repo_local_codex_files(masked)
|
|
99
|
+
|
|
100
|
+
self.assertEqual(codex_file.read_text(encoding="utf-8"), "tracked repo sentinel\n")
|
|
101
|
+
|
|
102
|
+
def test_masks_project_root_override_codex_file_for_worktree_runs(self) -> None:
|
|
103
|
+
with tempfile.TemporaryDirectory(prefix="pushpals-codex-mask-root-") as root:
|
|
104
|
+
project_root = Path(root) / "project"
|
|
105
|
+
worktree = project_root / ".worktrees" / "job-123"
|
|
106
|
+
worktree.mkdir(parents=True)
|
|
107
|
+
root_codex_file = project_root / ".codex"
|
|
108
|
+
worktree_codex_file = worktree / ".codex"
|
|
109
|
+
root_codex_file.write_text("root sentinel\n", encoding="utf-8")
|
|
110
|
+
worktree_codex_file.write_text("worktree sentinel\n", encoding="utf-8")
|
|
111
|
+
|
|
112
|
+
masked = _mask_repo_local_codex_files(
|
|
113
|
+
str(worktree),
|
|
114
|
+
{"PUSHPALS_REPO_ROOT_OVERRIDE": str(project_root)},
|
|
115
|
+
)
|
|
116
|
+
try:
|
|
117
|
+
self.assertFalse(root_codex_file.exists())
|
|
118
|
+
self.assertFalse(worktree_codex_file.exists())
|
|
119
|
+
self.assertEqual(len(masked), 2)
|
|
120
|
+
finally:
|
|
121
|
+
_restore_repo_local_codex_files(masked)
|
|
122
|
+
|
|
123
|
+
self.assertEqual(root_codex_file.read_text(encoding="utf-8"), "root sentinel\n")
|
|
124
|
+
self.assertEqual(worktree_codex_file.read_text(encoding="utf-8"), "worktree sentinel\n")
|
|
125
|
+
|
|
126
|
+
def test_does_not_mask_repo_local_codex_directory(self) -> None:
|
|
127
|
+
with tempfile.TemporaryDirectory(prefix="pushpals-codex-mask-dir-") as root:
|
|
128
|
+
repo = Path(root) / "repo"
|
|
129
|
+
codex_dir = repo / ".codex"
|
|
130
|
+
codex_dir.mkdir(parents=True)
|
|
131
|
+
(codex_dir / "config.toml").write_text("[hooks]\n", encoding="utf-8")
|
|
132
|
+
|
|
133
|
+
masked = _mask_repo_local_codex_files(str(repo), {})
|
|
134
|
+
try:
|
|
135
|
+
self.assertEqual(masked, [])
|
|
136
|
+
self.assertTrue((codex_dir / "config.toml").exists())
|
|
137
|
+
finally:
|
|
138
|
+
_restore_repo_local_codex_files(masked)
|
|
139
|
+
|
|
83
140
|
def test_reasoning_effort_defaults_to_extra_high_for_default_gpt_5_5(self) -> None:
|
|
84
141
|
cfg = OpenAICodexRuntimeConfig.from_sources(
|
|
85
142
|
SettingsResolver(env={}, config_loader=lambda: {}),
|
|
@@ -352,9 +352,10 @@ def _build_path_handling_message(target_paths: List[str], repo: str) -> str:
|
|
|
352
352
|
"- Prefer the repo-relative paths for shell commands.\n"
|
|
353
353
|
"- If FileEditor rejects a repo-relative path, retry with the matching absolute path.\n"
|
|
354
354
|
"- Do not run broad filesystem scans when concrete target paths are listed.\n"
|
|
355
|
-
"
|
|
355
|
+
"- These paths are starting points, not hard write boundaries; edit other behavior-owning files when needed and explain why.\n"
|
|
356
|
+
"Target path hints (repo-relative):\n"
|
|
356
357
|
f"{listed_rel}\n"
|
|
357
|
-
"
|
|
358
|
+
"Target path hints (absolute):\n"
|
|
358
359
|
f"{listed_abs}"
|
|
359
360
|
)
|
|
360
361
|
|