@pushpalsdev/cli 1.0.93 → 1.0.95
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 +120 -15
- package/package.json +1 -1
- package/runtime/prompts/remotebuddy/remotebuddy_system_prompt.md +2 -2
- package/runtime/sandbox/.pushpals-remotebuddy-fallback.js +207 -53
- 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/execute_job.ts +138 -105
|
@@ -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: {}),
|