python-agent-harness 1.5.4.7__tar.gz → 1.5.4.9__tar.gz
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.
- {python_agent_harness-1.5.4.7/python_agent_harness.egg-info → python_agent_harness-1.5.4.9}/PKG-INFO +1 -1
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/pyproject.toml +1 -1
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/__init__.py +1 -1
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/client.py +100 -23
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/planmode.py +3 -1
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/session.py +1 -1
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/tools/edit.py +1 -1
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/tools/glob.py +2 -2
- python_agent_harness-1.5.4.9/python_agent_harness/tools/glob_mac.py +115 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/tools/grep.py +13 -1
- python_agent_harness-1.5.4.9/python_agent_harness/tools/grep_mac.py +209 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/tools/insert.py +2 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/tools/write.py +3 -1
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9/python_agent_harness.egg-info}/PKG-INFO +1 -1
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/tests/test_client.py +18 -2
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/tests/test_filesystem.py +1 -1
- python_agent_harness-1.5.4.7/python_agent_harness/tools/glob_mac.py +0 -97
- python_agent_harness-1.5.4.7/python_agent_harness/tools/grep_mac.py +0 -92
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/LICENSE +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/README.md +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/__main__.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/agent.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/cli.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/commands.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/config.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/context_manager.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/diffrender.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/mcp/__init__.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/mcp/client.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/mcp/config.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/mcp/manager.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/models.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/persistence.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/prompts/agent.md +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/prompts/build-switch.md +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/prompts/commands/explain.md +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/prompts/compact.md +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/prompts/initialize.md +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/prompts/plan-mode.md +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/prompts/plan.md +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/prompts/review.md +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/prompts/subagent.md +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/prompts/summary.md +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/prompts/task-completion-rules.md +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/prompts/title.md +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/prompts.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/subagent.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/token_estimator.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/tool_runner.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/tools/__init__.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/tools/agent_tool.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/tools/base.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/tools/bash.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/tools/diffapply.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/tools/edit_mac.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/tools/filesystem.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/tools/mcp.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/tools/mkdir.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/tools/planexit.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/tools/question.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/tools/read.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/tools/skill.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/tools/todo.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/tui/__init__.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/tui/commands.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/tui/core.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/tui/input.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/tui/render.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness.egg-info/SOURCES.txt +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness.egg-info/dependency_links.txt +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness.egg-info/entry_points.txt +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness.egg-info/requires.txt +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness.egg-info/top_level.txt +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/setup.cfg +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/tests/test_agents_md.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/tests/test_cli.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/tests/test_commands.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/tests/test_concurrent_subagents.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/tests/test_config.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/tests/test_context_rules.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/tests/test_diffrender.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/tests/test_invariants.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/tests/test_mcp.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/tests/test_models.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/tests/test_persistence.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/tests/test_planmode.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/tests/test_prompts.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/tests/test_scenarios.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/tests/test_session.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/tests/test_subagent.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/tests/test_subagent_isolation.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/tests/test_todos_scope.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/tests/test_token_estimator.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/tests/test_tools_misc.py +0 -0
{python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/client.py
RENAMED
|
@@ -12,6 +12,7 @@ import json
|
|
|
12
12
|
import os
|
|
13
13
|
import random
|
|
14
14
|
import socket as _socket
|
|
15
|
+
import sys
|
|
15
16
|
import threading
|
|
16
17
|
import time
|
|
17
18
|
import uuid
|
|
@@ -277,12 +278,16 @@ class Client:
|
|
|
277
278
|
|
|
278
279
|
A blocked ``iter_lines()`` read must be interrupted so the agent
|
|
279
280
|
loop can stop promptly. Closing the pool alone is NOT enough:
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
the
|
|
285
|
-
|
|
281
|
+
``httpx.Client.close()`` from another thread cannot force-close a
|
|
282
|
+
socket whose fd is parked in a kernel ``recv``. So we first
|
|
283
|
+
reach into every in-flight connection socket and both
|
|
284
|
+
``shutdown(SHUT_RDWR)`` it (wakes the blocked read on Linux) and
|
|
285
|
+
``close()`` the raw fd (the reliable wake on macOS/BSD, where
|
|
286
|
+
``shutdown`` alone may not rouse a parked ``recv``); the woken
|
|
287
|
+
read surfaces as a connection error the loop treats as a cancel.
|
|
288
|
+
We then close the pool and swap in a fresh client for the next
|
|
289
|
+
request. See ``_abort_inflight_sockets`` for the platform
|
|
290
|
+
details.
|
|
286
291
|
"""
|
|
287
292
|
self._aborted = True
|
|
288
293
|
old = self._http
|
|
@@ -818,27 +823,99 @@ def _default_api_key() -> str | None:
|
|
|
818
823
|
return os.environ.get("OPENAI_API_KEY") or os.environ.get("DEEPSEEK_API_KEY") or None
|
|
819
824
|
|
|
820
825
|
|
|
826
|
+
def _extract_socket(conn: object) -> _socket.socket | None:
|
|
827
|
+
"""Best-effort extraction of the raw socket from an httpcore connection.
|
|
828
|
+
|
|
829
|
+
For direct (non-proxy) connections the socket lives at
|
|
830
|
+
``conn._connection._network_stream``. Proxy connections are wrapped
|
|
831
|
+
(ForwardHTTPConnection/TunnelHTTPConnection -> HTTPConnection ->
|
|
832
|
+
HTTP11Connection), so we unwrap up to 4 levels looking for the
|
|
833
|
+
``_network_stream`` attribute that owns the raw socket.
|
|
834
|
+
"""
|
|
835
|
+
try:
|
|
836
|
+
obj: object = conn
|
|
837
|
+
stream = None
|
|
838
|
+
for _ in range(4):
|
|
839
|
+
stream = getattr(obj, "_network_stream", None)
|
|
840
|
+
if stream is not None:
|
|
841
|
+
break
|
|
842
|
+
obj = getattr(obj, "_connection", None)
|
|
843
|
+
if obj is None:
|
|
844
|
+
break
|
|
845
|
+
return stream.get_extra_info("socket") if stream is not None else None
|
|
846
|
+
except Exception: # noqa: BLE001 - best effort
|
|
847
|
+
return None
|
|
848
|
+
|
|
849
|
+
|
|
850
|
+
def _shutdown_and_close(sock: _socket.socket) -> None:
|
|
851
|
+
"""Shutdown then close a socket to wake blocked reads.
|
|
852
|
+
|
|
853
|
+
On macOS the socket is in non-blocking mode (httpcore calls
|
|
854
|
+
settimeout for the read timeout), so a blocked recv parks in
|
|
855
|
+
select(); closing the fd before that select has processed the
|
|
856
|
+
shutdown wakeup can lose the wakeup and leave the read parked
|
|
857
|
+
forever. Give the woken thread a moment to observe the EOF first.
|
|
858
|
+
"""
|
|
859
|
+
with contextlib.suppress(OSError):
|
|
860
|
+
sock.shutdown(_socket.SHUT_RDWR)
|
|
861
|
+
if sys.platform == "darwin":
|
|
862
|
+
time.sleep(0.05)
|
|
863
|
+
with contextlib.suppress(OSError):
|
|
864
|
+
sock.close()
|
|
865
|
+
|
|
866
|
+
|
|
821
867
|
def _abort_inflight_sockets(client: httpx.Client) -> None:
|
|
822
|
-
"""Wake any blocked stream reads
|
|
868
|
+
"""Wake any blocked stream reads on every live pool socket.
|
|
823
869
|
|
|
824
870
|
Reaches through httpx/httpcore internals (transport -> pool ->
|
|
825
|
-
connection -> network stream -> raw socket) and
|
|
826
|
-
``shutdown(SHUT_RDWR)``
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
871
|
+
connection -> network stream -> raw socket) and, for each live
|
|
872
|
+
connection, both ``shutdown(SHUT_RDWR)`` AND ``close()`` the raw
|
|
873
|
+
socket.
|
|
874
|
+
|
|
875
|
+
Both steps are needed for cross-platform reliability when another
|
|
876
|
+
thread is already blocked in ``recv``:
|
|
877
|
+
|
|
878
|
+
- On Linux, ``shutdown(SHUT_RDWR)`` reliably wakes the blocked
|
|
879
|
+
``recv`` (turning it into a clean EOF / connection error), while
|
|
880
|
+
httpx's own ``close()`` cannot — it can't force-close a socket
|
|
881
|
+
whose fd is parked in a kernel read.
|
|
882
|
+
- On macOS/BSD (notably CI's macos-latest on py3.11), ``shutdown``
|
|
883
|
+
from another thread is NOT guaranteed to wake a ``recv`` already
|
|
884
|
+
parked in the kernel; the read stays blocked until the fd itself
|
|
885
|
+
is closed. ``socket.close()`` closes the fd directly and does
|
|
886
|
+
wake it, so we always follow the shutdown with an explicit
|
|
887
|
+
close.
|
|
888
|
+
|
|
889
|
+
Closing the fd out from under httpcore is safe here: ``abort()``
|
|
890
|
+
swaps in a fresh pool immediately and closes the old one under
|
|
891
|
+
``contextlib.suppress`` right after this call, so a double-close is
|
|
892
|
+
harmless. Every step is best-effort; any failure is ignored.
|
|
831
893
|
"""
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
894
|
+
pools: list[object] = []
|
|
895
|
+
base_pool = getattr(getattr(client, "_transport", None), "_pool", None)
|
|
896
|
+
if base_pool is not None:
|
|
897
|
+
pools.append(base_pool)
|
|
898
|
+
|
|
899
|
+
if sys.platform == "darwin":
|
|
900
|
+
# macOS only: with proxy env vars (HTTP_PROXY/HTTPS_PROXY) set,
|
|
901
|
+
# httpx routes requests through proxy transports registered in
|
|
902
|
+
# client._mounts (pools are httpcore.HTTPProxy), leaving the
|
|
903
|
+
# base transport pool above empty. On Linux this never happens
|
|
904
|
+
# in CI and the code path above is sufficient, so this extra
|
|
905
|
+
# walk is deliberately restricted to macOS.
|
|
906
|
+
mounts = getattr(client, "_mounts", None)
|
|
907
|
+
if isinstance(mounts, dict):
|
|
908
|
+
for transport in mounts.values():
|
|
909
|
+
if transport is not None:
|
|
910
|
+
pool = getattr(transport, "_pool", None)
|
|
911
|
+
if pool is not None:
|
|
912
|
+
pools.append(pool)
|
|
913
|
+
|
|
914
|
+
for pool in pools:
|
|
915
|
+
for conn in getattr(pool, "_connections", None) or ():
|
|
916
|
+
sock = _extract_socket(conn)
|
|
917
|
+
if sock is not None:
|
|
918
|
+
_shutdown_and_close(sock)
|
|
842
919
|
|
|
843
920
|
|
|
844
921
|
def _iter_sse(lines: Iterator[str]) -> Iterator[str]:
|
{python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/planmode.py
RENAMED
|
@@ -112,4 +112,6 @@ class PlanMode:
|
|
|
112
112
|
return self.mode == AgentMode.PLAN
|
|
113
113
|
|
|
114
114
|
def plan_reminder(self) -> str:
|
|
115
|
-
return config.PLAN_MODE_SUBAGENT_REMINDER
|
|
115
|
+
return config.PLAN_MODE_SUBAGENT_REMINDER.replace(
|
|
116
|
+
"%s", self.plan_file or self.plan_file_path()
|
|
117
|
+
)
|
{python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/session.py
RENAMED
|
@@ -444,7 +444,7 @@ class Session:
|
|
|
444
444
|
)
|
|
445
445
|
if approved:
|
|
446
446
|
self.switch_to_build()
|
|
447
|
-
msg = config.PLAN_EXIT_APPROVED_MESSAGE
|
|
447
|
+
msg = config.PLAN_EXIT_APPROVED_MESSAGE.replace("%s", self.plan_mode.plan_file or "")
|
|
448
448
|
self.pending_user_prompts.append(msg)
|
|
449
449
|
return (
|
|
450
450
|
"User approved switching to build agent. You are now in "
|
{python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/tools/edit.py
RENAMED
|
@@ -90,7 +90,7 @@ class Edit(Tool):
|
|
|
90
90
|
if old is None:
|
|
91
91
|
return "Error: old_str is required for non-diff edits"
|
|
92
92
|
try:
|
|
93
|
-
with open(path, encoding="utf-8") as f:
|
|
93
|
+
with open(path, encoding="utf-8", errors="replace") as f:
|
|
94
94
|
content = f.read()
|
|
95
95
|
except OSError as e:
|
|
96
96
|
return f"Error: cannot read {path}: {e}"
|
{python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/tools/glob.py
RENAMED
|
@@ -107,7 +107,7 @@ class GlobTool(Tool):
|
|
|
107
107
|
return f"Error: {e}"
|
|
108
108
|
if proc.returncode != 0:
|
|
109
109
|
# Failure banner is prepended to whatever git emitted.
|
|
110
|
-
banner = f"Glob failed with exit code {proc.returncode}\
|
|
110
|
+
banner = f"Glob failed with exit code {proc.returncode}\nSTDOUT:\n\n"
|
|
111
111
|
return _spool(banner + (proc.stdout or "") + (proc.stderr or ""), "glob")
|
|
112
112
|
return _git_glob_results(proc.stdout, git_root, base, depth)
|
|
113
113
|
|
|
@@ -141,7 +141,7 @@ class GlobTool(Tool):
|
|
|
141
141
|
return f"Error: {e}"
|
|
142
142
|
out = proc.stdout
|
|
143
143
|
if proc.returncode != 0:
|
|
144
|
-
out = f"Glob failed with exit code {proc.returncode}\
|
|
144
|
+
out = f"Glob failed with exit code {proc.returncode}\nSTDOUT:\n\n" + out
|
|
145
145
|
return _spool(out, "glob")
|
|
146
146
|
|
|
147
147
|
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""macOS Glob tool: ``git ls-files`` inside git repos, ``find`` outside.
|
|
2
|
+
|
|
3
|
+
Apple ships neither GNU ``tree`` nor a compatible equivalent, so the
|
|
4
|
+
non-git fallback in :class:`GlobTool` (which shells out to ``tree``)
|
|
5
|
+
fails on a stock macOS install. ``GlobMac`` replaces that fallback
|
|
6
|
+
with a hybrid approach: ``find`` (C binary, pre-installed on macOS)
|
|
7
|
+
handles directory traversal and pattern matching, while Python handles
|
|
8
|
+
mtime sorting — which ``find`` does not support.
|
|
9
|
+
|
|
10
|
+
This is significantly faster than the previous pure-Python
|
|
11
|
+
:meth:`pathlib.Path.rglob` approach on large directory trees, while
|
|
12
|
+
producing identical results.
|
|
13
|
+
|
|
14
|
+
Only the non-git fallback differs; the git path, the tool name, and
|
|
15
|
+
the result format are inherited unchanged so callers, the tool
|
|
16
|
+
registry, and the plan-mode write guard are platform-independent.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import os
|
|
22
|
+
import subprocess
|
|
23
|
+
|
|
24
|
+
from .base import ToolContext
|
|
25
|
+
from .filesystem import _natnump, _spool
|
|
26
|
+
from .glob import GlobTool
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class GlobMac(GlobTool):
|
|
30
|
+
"""Glob with a hybrid ``find`` + Python-sort non-git fallback for macOS."""
|
|
31
|
+
|
|
32
|
+
def _find_fallback(self, pattern: str, base: str, depth: object) -> str:
|
|
33
|
+
"""Use ``find`` for traversal + matching, Python for mtime sort.
|
|
34
|
+
|
|
35
|
+
``find`` (C binary) walks the directory tree and applies pattern
|
|
36
|
+
matching natively, which is significantly faster than
|
|
37
|
+
:meth:`pathlib.Path.rglob` on large trees. Results are then
|
|
38
|
+
sorted by modification time (newest first) in Python, matching
|
|
39
|
+
the ``tree --sort=mtime`` order of the Linux fallback. Hidden
|
|
40
|
+
directories (``.git``, etc.) are skipped via ``-path */.* -prune``.
|
|
41
|
+
Symlinks are followed (``-L``), matching ``tree -l`` and the old
|
|
42
|
+
:meth:`pathlib.Path.rglob` behavior.
|
|
43
|
+
"""
|
|
44
|
+
cmd = [
|
|
45
|
+
"find",
|
|
46
|
+
"-L",
|
|
47
|
+
base,
|
|
48
|
+
"-path",
|
|
49
|
+
"*/.*",
|
|
50
|
+
"-prune",
|
|
51
|
+
"-o",
|
|
52
|
+
"-type",
|
|
53
|
+
"f",
|
|
54
|
+
"-iname",
|
|
55
|
+
pattern,
|
|
56
|
+
"-print",
|
|
57
|
+
]
|
|
58
|
+
if _natnump(depth):
|
|
59
|
+
cmd[3:3] = ["-maxdepth", str(depth)]
|
|
60
|
+
try:
|
|
61
|
+
proc = subprocess.run(
|
|
62
|
+
cmd,
|
|
63
|
+
capture_output=True,
|
|
64
|
+
text=True,
|
|
65
|
+
encoding="utf-8",
|
|
66
|
+
errors="replace",
|
|
67
|
+
timeout=60,
|
|
68
|
+
)
|
|
69
|
+
except (OSError, subprocess.TimeoutExpired) as e:
|
|
70
|
+
return f"Error: {e}"
|
|
71
|
+
lines = [line for line in proc.stdout.splitlines() if line]
|
|
72
|
+
if not lines:
|
|
73
|
+
if proc.returncode != 0:
|
|
74
|
+
out = f"Glob failed with exit code {proc.returncode}\n"
|
|
75
|
+
out += proc.stderr or ""
|
|
76
|
+
return _spool(out, "glob")
|
|
77
|
+
return ""
|
|
78
|
+
|
|
79
|
+
matches: list[tuple[float, str]] = []
|
|
80
|
+
for path in lines:
|
|
81
|
+
try:
|
|
82
|
+
mtime = os.path.getmtime(path)
|
|
83
|
+
except OSError:
|
|
84
|
+
mtime = 0.0
|
|
85
|
+
matches.append((mtime, path))
|
|
86
|
+
|
|
87
|
+
matches.sort(key=lambda t: t[0], reverse=True)
|
|
88
|
+
out = "\n".join(path for _, path in matches)
|
|
89
|
+
if not out:
|
|
90
|
+
return ""
|
|
91
|
+
return _spool(out + "\n", "glob")
|
|
92
|
+
|
|
93
|
+
def run(self, args: dict, ctx: ToolContext) -> str:
|
|
94
|
+
pattern = args.get("pattern") or ""
|
|
95
|
+
if not pattern:
|
|
96
|
+
return "Error: pattern must not be empty"
|
|
97
|
+
path = args.get("path")
|
|
98
|
+
if path:
|
|
99
|
+
if not (os.path.isdir(path) and os.access(path, os.R_OK)):
|
|
100
|
+
return f"Error: path {path} is not readable"
|
|
101
|
+
else:
|
|
102
|
+
path = ctx.cwd
|
|
103
|
+
base = os.path.realpath(path)
|
|
104
|
+
depth = args.get("depth")
|
|
105
|
+
|
|
106
|
+
from .filesystem import _git_root
|
|
107
|
+
|
|
108
|
+
git_root = _git_root(base)
|
|
109
|
+
|
|
110
|
+
if git_root:
|
|
111
|
+
# The git path is identical to the parent — delegate.
|
|
112
|
+
return super().run(args, ctx)
|
|
113
|
+
|
|
114
|
+
# Non-git: hybrid find + Python sort fallback instead of `tree`.
|
|
115
|
+
return self._find_fallback(pattern, base, depth)
|
{python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/tools/grep.py
RENAMED
|
@@ -49,7 +49,7 @@ class Grep(Tool):
|
|
|
49
49
|
if context is not None:
|
|
50
50
|
context = max(0, min(15, int(context)))
|
|
51
51
|
|
|
52
|
-
git_root = _git_root(path)
|
|
52
|
+
git_root = _git_root(path)
|
|
53
53
|
if git_root:
|
|
54
54
|
rel = os.path.relpath(path, git_root)
|
|
55
55
|
pathspec = rel
|
|
@@ -84,6 +84,18 @@ class Grep(Tool):
|
|
|
84
84
|
proc = None
|
|
85
85
|
if proc is not None and proc.returncode in (0, 1):
|
|
86
86
|
return _grep_out(proc, "git")
|
|
87
|
+
return self._fallback_rg_grep(regex, path, glob, context)
|
|
88
|
+
|
|
89
|
+
def _fallback_rg_grep(
|
|
90
|
+
self, regex: str, path: str, glob: str | None, context: int | None
|
|
91
|
+
) -> str:
|
|
92
|
+
"""rg → grep fallback chain (no git grep).
|
|
93
|
+
|
|
94
|
+
Shared by :class:`Grep` (after git grep -P fails) and
|
|
95
|
+
:class:`GrepMac` (after git grep -E fails). Extracted here so
|
|
96
|
+
the Mac variant can skip the parent's ``git grep -P`` attempt
|
|
97
|
+
without duplicating the rg/grep logic.
|
|
98
|
+
"""
|
|
87
99
|
if shutil.which("rg"):
|
|
88
100
|
cmd = [
|
|
89
101
|
"rg",
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""macOS Grep tool: ``git grep -E`` instead of ``git grep -P``.
|
|
2
|
+
|
|
3
|
+
Apple's default Git (installed via Xcode Command Line Tools) is built
|
|
4
|
+
without PCRE support, so ``git grep -P`` (Perl-compatible regex) fails
|
|
5
|
+
with::
|
|
6
|
+
|
|
7
|
+
fatal: cannot use Perl-compatible regexes when not compiled with USE_LIBPCRE
|
|
8
|
+
|
|
9
|
+
The Linux :class:`Grep` tool uses ``-P`` as the first-choice backend;
|
|
10
|
+
when that fails on macOS the fallback chain (``rg`` → ``grep``) still
|
|
11
|
+
works, but:
|
|
12
|
+
|
|
13
|
+
- ``rg`` (ripgrep) is not installed by default on macOS.
|
|
14
|
+
- BSD ``grep`` supports only POSIX ERE, losing Perl features
|
|
15
|
+
(``\\d``, ``\\w``, lookaheads).
|
|
16
|
+
- Two unnecessary failed attempts add latency before reaching a
|
|
17
|
+
working backend.
|
|
18
|
+
|
|
19
|
+
``GrepMac`` replaces ``-P`` with ``-E`` (POSIX extended regex) in the
|
|
20
|
+
``git grep`` command, so the git path — the fastest and most common
|
|
21
|
+
backend — works on stock macOS. PCRE character class shorthands
|
|
22
|
+
(``\\d``, ``\\w``, ``\\s`` and their negations) are translated to
|
|
23
|
+
POSIX bracket expressions so the LLM's regexes produce the same
|
|
24
|
+
matches they would under ``-P``. The translation is context-aware:
|
|
25
|
+
it skips shorthands inside existing character classes (``[\\d.]``)
|
|
26
|
+
and shorthands preceded by a literal backslash (``\\\\d``), so the
|
|
27
|
+
regex structure is never corrupted. ``\\b`` (word boundary) is
|
|
28
|
+
translated to ``[[:<:]]`` / ``[[:>:]]`` on macOS git, which supports
|
|
29
|
+
these GNU-compatible extensions.
|
|
30
|
+
|
|
31
|
+
Features with no POSIX equivalent (lookaheads, backreferences) are
|
|
32
|
+
left as-is — they will cause a syntax error, which is better than
|
|
33
|
+
silently matching wrong content.
|
|
34
|
+
|
|
35
|
+
The fallback chain skips the parent's ``git grep -P`` attempt (guaranteed
|
|
36
|
+
to fail on macOS) and goes directly to ``rg`` / ``grep`` via the shared
|
|
37
|
+
:meth:`Grep._fallback_rg_grep` method.
|
|
38
|
+
|
|
39
|
+
Only the ``git grep`` regex flag differs; the tool name and result
|
|
40
|
+
format are inherited so callers, the tool registry, and the plan-mode
|
|
41
|
+
write guard are platform-independent.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
from __future__ import annotations
|
|
45
|
+
|
|
46
|
+
import os
|
|
47
|
+
import re
|
|
48
|
+
import subprocess
|
|
49
|
+
|
|
50
|
+
from .base import ToolContext
|
|
51
|
+
from .filesystem import _git_root
|
|
52
|
+
from .grep import Grep, _grep_out
|
|
53
|
+
|
|
54
|
+
_PCRE_CLASS_MAP: dict[str, str] = {
|
|
55
|
+
r"\d": "[0-9]",
|
|
56
|
+
r"\D": "[^0-9]",
|
|
57
|
+
r"\w": "[A-Za-z0-9_]",
|
|
58
|
+
r"\W": "[^A-Za-z0-9_]",
|
|
59
|
+
r"\s": "[ \t\n\r\f\v]",
|
|
60
|
+
r"\S": "[^ \t\n\r\f\v]",
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
# Match \d, \D, \w, \W, \s, \S, or \b that is NOT preceded by an
|
|
64
|
+
# odd number of backslashes (i.e. not escaped). The lookbehind
|
|
65
|
+
# (?<!\\) ensures we don't match a shorthand that is itself escaped.
|
|
66
|
+
# The ``prefix`` group captures any preceding pairs of literal
|
|
67
|
+
# backslashes so they can be preserved in the replacement — without
|
|
68
|
+
# this, re.sub would discard them along with the full match.
|
|
69
|
+
_PCRE_SHORTHAND_RE = re.compile(r"(?<!\\)(?P<prefix>(?:\\\\)*)(?P<shorthand>\\[dDwWsSb])")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _is_escaped(s: str, pos: int) -> bool:
|
|
73
|
+
"""True if the character at *pos* is preceded by an odd number of backslashes."""
|
|
74
|
+
count = 0
|
|
75
|
+
j = pos - 1
|
|
76
|
+
while j >= 0 and s[j] == "\\":
|
|
77
|
+
count += 1
|
|
78
|
+
j -= 1
|
|
79
|
+
return count % 2 == 1
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _find_char_class_end(s: str, start: int) -> int | None:
|
|
83
|
+
"""Find the closing ``]`` of a character class starting at *start*.
|
|
84
|
+
|
|
85
|
+
Handles ``[]]`` (``]`` as first char) and ``[^]]``. Returns the
|
|
86
|
+
index of the closing ``]`` or ``None`` if unterminated.
|
|
87
|
+
"""
|
|
88
|
+
i = start + 1
|
|
89
|
+
if i < len(s) and s[i] == "^":
|
|
90
|
+
i += 1
|
|
91
|
+
if i < len(s) and s[i] == "]":
|
|
92
|
+
i += 1
|
|
93
|
+
while i < len(s):
|
|
94
|
+
if s[i] == "\\" and i + 1 < len(s):
|
|
95
|
+
i += 2
|
|
96
|
+
continue
|
|
97
|
+
if s[i] == "]":
|
|
98
|
+
return i
|
|
99
|
+
i += 1
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _pcre_to_ere(regex: str) -> str:
|
|
104
|
+
"""Translate PCRE character class shorthands to POSIX ERE.
|
|
105
|
+
|
|
106
|
+
Translates ``\\d``, ``\\w``, ``\\s``, their negations, and ``\\b``
|
|
107
|
+
(word boundary) to POSIX-compatible equivalents. The translation is
|
|
108
|
+
context-aware:
|
|
109
|
+
|
|
110
|
+
- Shorthands **inside** an existing character class (``[\\d.]``) are
|
|
111
|
+
left untouched — replacing them would create nested ``[]`` which
|
|
112
|
+
is invalid in POSIX ERE. ``git grep -E`` on macOS handles
|
|
113
|
+
``\\d`` inside ``[]`` natively, so this is safe.
|
|
114
|
+
- Shorthands preceded by a literal backslash (``\\\\d``) are left
|
|
115
|
+
untouched — they represent a literal backslash + letter, not a
|
|
116
|
+
character class.
|
|
117
|
+
|
|
118
|
+
Features with no POSIX equivalent (lookaheads, backreferences) are
|
|
119
|
+
left as-is so they produce a clear syntax error instead of silently
|
|
120
|
+
matching wrong content.
|
|
121
|
+
"""
|
|
122
|
+
# Find all character class spans so we can skip shorthands inside them.
|
|
123
|
+
char_class_spans: list[tuple[int, int]] = []
|
|
124
|
+
i = 0
|
|
125
|
+
while i < len(regex):
|
|
126
|
+
if regex[i] == "[" and not _is_escaped(regex, i):
|
|
127
|
+
end = _find_char_class_end(regex, i)
|
|
128
|
+
if end is not None:
|
|
129
|
+
char_class_spans.append((i, end))
|
|
130
|
+
i = end + 1
|
|
131
|
+
continue
|
|
132
|
+
i += 1
|
|
133
|
+
|
|
134
|
+
def _in_char_class(pos: int) -> bool:
|
|
135
|
+
return any(start < pos < end for start, end in char_class_spans)
|
|
136
|
+
|
|
137
|
+
def _replace(m: re.Match) -> str:
|
|
138
|
+
if _in_char_class(m.start()):
|
|
139
|
+
return m.group(0)
|
|
140
|
+
prefix = m.group("prefix")
|
|
141
|
+
shorthand = m.group("shorthand")
|
|
142
|
+
if shorthand == r"\b":
|
|
143
|
+
pos = m.start("shorthand")
|
|
144
|
+
after = regex[pos + 2] if pos + 2 < len(regex) else ""
|
|
145
|
+
before = regex[pos - 1] if pos > 0 else ""
|
|
146
|
+
if before and (before.isalnum() or before == "_"):
|
|
147
|
+
return prefix + "[[:>:]]"
|
|
148
|
+
if after and (after.isalnum() or after == "_"):
|
|
149
|
+
return prefix + "[[:<:]]"
|
|
150
|
+
return prefix + "[[:<:]]"
|
|
151
|
+
return prefix + _PCRE_CLASS_MAP.get(shorthand, shorthand)
|
|
152
|
+
|
|
153
|
+
return _PCRE_SHORTHAND_RE.sub(_replace, regex)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class GrepMac(Grep):
|
|
157
|
+
"""Grep with ``git grep -E`` for macOS (no PCRE dependency)."""
|
|
158
|
+
|
|
159
|
+
def run(self, args: dict, ctx: ToolContext) -> str:
|
|
160
|
+
regex = args["regex"]
|
|
161
|
+
path = os.path.realpath(args["path"])
|
|
162
|
+
if not os.path.isdir(path) and not os.path.isfile(path):
|
|
163
|
+
return f"Error: path {args['path']} is not readable"
|
|
164
|
+
glob = args.get("glob")
|
|
165
|
+
context = args.get("context_lines")
|
|
166
|
+
if context is not None:
|
|
167
|
+
context = max(0, min(15, int(context)))
|
|
168
|
+
|
|
169
|
+
git_root = _git_root(path)
|
|
170
|
+
if git_root:
|
|
171
|
+
rel = os.path.relpath(path, git_root)
|
|
172
|
+
pathspec = rel
|
|
173
|
+
if glob and os.path.isdir(path):
|
|
174
|
+
pathspec = os.path.join(rel, glob).replace(os.sep, "/")
|
|
175
|
+
ere_regex = _pcre_to_ere(regex)
|
|
176
|
+
cmd = [
|
|
177
|
+
"git",
|
|
178
|
+
"grep",
|
|
179
|
+
"--line-number",
|
|
180
|
+
"--no-color",
|
|
181
|
+
"--max-count=1000",
|
|
182
|
+
"--untracked",
|
|
183
|
+
"-E",
|
|
184
|
+
"-e",
|
|
185
|
+
ere_regex,
|
|
186
|
+
"--",
|
|
187
|
+
pathspec,
|
|
188
|
+
]
|
|
189
|
+
if context:
|
|
190
|
+
cmd = cmd[:3] + [f"-C{context}"] + cmd[3:]
|
|
191
|
+
try:
|
|
192
|
+
proc = subprocess.run(
|
|
193
|
+
cmd,
|
|
194
|
+
cwd=git_root,
|
|
195
|
+
capture_output=True,
|
|
196
|
+
text=True,
|
|
197
|
+
encoding="utf-8",
|
|
198
|
+
errors="replace",
|
|
199
|
+
timeout=60,
|
|
200
|
+
)
|
|
201
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
202
|
+
proc = None
|
|
203
|
+
if proc is not None and proc.returncode in (0, 1):
|
|
204
|
+
return _grep_out(proc, "git")
|
|
205
|
+
|
|
206
|
+
# Non-git or git grep -E failed: skip the parent's git grep -P
|
|
207
|
+
# attempt (guaranteed to fail on macOS) and go directly to the
|
|
208
|
+
# shared rg/grep fallback chain.
|
|
209
|
+
return self._fallback_rg_grep(regex, path, glob, context)
|
{python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/tools/insert.py
RENAMED
|
@@ -43,6 +43,8 @@ class Insert(Tool):
|
|
|
43
43
|
new_str = args["new_str"]
|
|
44
44
|
if not new_str.endswith("\n"):
|
|
45
45
|
new_str += "\n"
|
|
46
|
+
if ln < -1:
|
|
47
|
+
return f"Error: line_number {ln} is invalid (use 0 for beginning, -1 for end)"
|
|
46
48
|
if ln == -1 or ln >= len(lines):
|
|
47
49
|
lines.append(new_str)
|
|
48
50
|
elif ln == 0:
|
{python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/tools/write.py
RENAMED
|
@@ -26,7 +26,9 @@ class Write(Tool):
|
|
|
26
26
|
def run(self, args: dict, ctx: ToolContext) -> str:
|
|
27
27
|
dir_path = args.get("path") or "."
|
|
28
28
|
filename = args.get("filename") or ""
|
|
29
|
-
content = args.get("content")
|
|
29
|
+
content = args.get("content")
|
|
30
|
+
if content is None:
|
|
31
|
+
return "Error: Required argument `content' missing"
|
|
30
32
|
# LLM may put the full file path in "filename" or in "path"
|
|
31
33
|
if filename:
|
|
32
34
|
path = os.path.realpath(os.path.abspath(os.path.join(dir_path, filename)))
|
|
@@ -812,16 +812,17 @@ class TestClientAbort(unittest.TestCase):
|
|
|
812
812
|
|
|
813
813
|
class TestAbortInflightSockets(unittest.TestCase):
|
|
814
814
|
"""_abort_inflight_sockets wakes live pool sockets via
|
|
815
|
-
shutdown(SHUT_RDWR) and tolerates broken internals."""
|
|
815
|
+
shutdown(SHUT_RDWR) + close() and tolerates broken internals."""
|
|
816
816
|
|
|
817
817
|
def _fake_client_with_stream(self, stream):
|
|
818
818
|
conn = mock.Mock()
|
|
819
|
+
conn._network_stream = None
|
|
819
820
|
conn._connection._network_stream = stream
|
|
820
821
|
client = mock.Mock()
|
|
821
822
|
client._transport._pool._connections = [conn]
|
|
822
823
|
return client
|
|
823
824
|
|
|
824
|
-
def
|
|
825
|
+
def test_shuts_down_and_closes_live_socket(self):
|
|
825
826
|
import socket as _socket
|
|
826
827
|
|
|
827
828
|
from python_agent_harness.client import _abort_inflight_sockets
|
|
@@ -830,7 +831,22 @@ class TestAbortInflightSockets(unittest.TestCase):
|
|
|
830
831
|
stream = mock.Mock()
|
|
831
832
|
stream.get_extra_info.return_value = sock
|
|
832
833
|
_abort_inflight_sockets(self._fake_client_with_stream(stream))
|
|
834
|
+
# shutdown wakes the recv on Linux; close() is the reliable wake
|
|
835
|
+
# on macOS/BSD where shutdown alone may leave a parked recv stuck
|
|
833
836
|
sock.shutdown.assert_called_once_with(_socket.SHUT_RDWR)
|
|
837
|
+
sock.close.assert_called_once_with()
|
|
838
|
+
|
|
839
|
+
def test_close_still_runs_when_shutdown_raises(self):
|
|
840
|
+
"""A shutdown OSError must not skip the fd close: on macOS the
|
|
841
|
+
close is the step that actually wakes the blocked read."""
|
|
842
|
+
from python_agent_harness.client import _abort_inflight_sockets
|
|
843
|
+
|
|
844
|
+
sock = mock.Mock()
|
|
845
|
+
sock.shutdown.side_effect = OSError("not connected")
|
|
846
|
+
stream = mock.Mock()
|
|
847
|
+
stream.get_extra_info.return_value = sock
|
|
848
|
+
_abort_inflight_sockets(self._fake_client_with_stream(stream)) # no raise
|
|
849
|
+
sock.close.assert_called_once_with()
|
|
834
850
|
|
|
835
851
|
def test_empty_pool_is_noop(self):
|
|
836
852
|
from python_agent_harness.client import _abort_inflight_sockets
|
|
@@ -1606,7 +1606,7 @@ class TestGrepMac(unittest.TestCase):
|
|
|
1606
1606
|
with (
|
|
1607
1607
|
mock.patch("shutil.which", return_value="/usr/bin/rg"),
|
|
1608
1608
|
mock.patch(
|
|
1609
|
-
"python_agent_harness.tools.
|
|
1609
|
+
"python_agent_harness.tools.grep.subprocess.run",
|
|
1610
1610
|
return_value=proc,
|
|
1611
1611
|
),
|
|
1612
1612
|
):
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
"""macOS Glob tool: ``git ls-files`` inside git repos, ``pathlib`` outside.
|
|
2
|
-
|
|
3
|
-
Apple ships neither GNU ``tree`` nor a compatible equivalent, so the
|
|
4
|
-
non-git fallback in :class:`GlobTool` (which shells out to ``tree``)
|
|
5
|
-
fails on a stock macOS install. ``GlobMac`` replaces that fallback
|
|
6
|
-
with :meth:`pathlib.Path.rglob`, which is pure-Python, requires no
|
|
7
|
-
external binary, and produces identical results.
|
|
8
|
-
|
|
9
|
-
Only the non-git fallback differs; the git path, the tool name, and
|
|
10
|
-
the result format are inherited unchanged so callers, the tool
|
|
11
|
-
registry, and the plan-mode write guard are platform-independent.
|
|
12
|
-
"""
|
|
13
|
-
|
|
14
|
-
from __future__ import annotations
|
|
15
|
-
|
|
16
|
-
import fnmatch
|
|
17
|
-
import os
|
|
18
|
-
from pathlib import Path
|
|
19
|
-
|
|
20
|
-
from .base import ToolContext
|
|
21
|
-
from .filesystem import _natnump, _spool
|
|
22
|
-
from .glob import GlobTool
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
class GlobMac(GlobTool):
|
|
26
|
-
"""Glob with a pure-Python non-git fallback for macOS."""
|
|
27
|
-
|
|
28
|
-
def _tree_fallback(self, pattern: str, base: str, depth: object) -> str:
|
|
29
|
-
"""Walk *base* with :func:`pathlib.Path.rglob` instead of ``tree``.
|
|
30
|
-
|
|
31
|
-
Results are absolute paths sorted by modification time (newest
|
|
32
|
-
first), matching the ``tree --sort=mtime`` order of the Linux
|
|
33
|
-
fallback. Hidden directories (``.git``, etc.) are skipped.
|
|
34
|
-
"""
|
|
35
|
-
root = Path(base)
|
|
36
|
-
matches: list[tuple[float, str]] = []
|
|
37
|
-
if _natnump(depth):
|
|
38
|
-
# Depth-limited: walk manually so we can count levels.
|
|
39
|
-
for dirpath, dirnames, filenames in os.walk(base):
|
|
40
|
-
# Skip hidden directories (mirrors tree's -I .git and
|
|
41
|
-
# the general expectation that dotfiles are excluded).
|
|
42
|
-
dirnames[:] = [d for d in dirnames if not d.startswith(".")]
|
|
43
|
-
rel = os.path.relpath(dirpath, base)
|
|
44
|
-
level = 0 if rel == "." else rel.count(os.sep) + 1
|
|
45
|
-
if level >= depth:
|
|
46
|
-
dirnames.clear()
|
|
47
|
-
continue
|
|
48
|
-
for name in filenames:
|
|
49
|
-
if fnmatch.fnmatch(name.lower(), pattern.lower()):
|
|
50
|
-
full = os.path.join(dirpath, name)
|
|
51
|
-
try:
|
|
52
|
-
mtime = os.path.getmtime(full)
|
|
53
|
-
except OSError:
|
|
54
|
-
mtime = 0.0
|
|
55
|
-
matches.append((mtime, full))
|
|
56
|
-
else:
|
|
57
|
-
# Unlimited depth: rglob is simpler.
|
|
58
|
-
for p in root.rglob("*"):
|
|
59
|
-
if any(part.startswith(".") for part in p.relative_to(root).parts):
|
|
60
|
-
continue
|
|
61
|
-
if p.is_file() and fnmatch.fnmatch(p.name.lower(), pattern.lower()):
|
|
62
|
-
try:
|
|
63
|
-
mtime = p.stat().st_mtime
|
|
64
|
-
except OSError:
|
|
65
|
-
mtime = 0.0
|
|
66
|
-
matches.append((mtime, str(p)))
|
|
67
|
-
|
|
68
|
-
# Sort newest-first (like tree --sort=mtime).
|
|
69
|
-
matches.sort(key=lambda t: t[0], reverse=True)
|
|
70
|
-
out = "\n".join(path for _, path in matches)
|
|
71
|
-
if not out:
|
|
72
|
-
return ""
|
|
73
|
-
return _spool(out + "\n", "glob")
|
|
74
|
-
|
|
75
|
-
def run(self, args: dict, ctx: ToolContext) -> str:
|
|
76
|
-
pattern = args.get("pattern") or ""
|
|
77
|
-
if not pattern:
|
|
78
|
-
return "Error: pattern must not be empty"
|
|
79
|
-
path = args.get("path")
|
|
80
|
-
if path:
|
|
81
|
-
if not (os.path.isdir(path) and os.access(path, os.R_OK)):
|
|
82
|
-
return f"Error: path {path} is not readable"
|
|
83
|
-
else:
|
|
84
|
-
path = ctx.cwd
|
|
85
|
-
base = os.path.realpath(path)
|
|
86
|
-
depth = args.get("depth")
|
|
87
|
-
|
|
88
|
-
from .filesystem import _git_root
|
|
89
|
-
|
|
90
|
-
git_root = _git_root(base)
|
|
91
|
-
|
|
92
|
-
if git_root:
|
|
93
|
-
# The git path is identical to the parent — delegate.
|
|
94
|
-
return super().run(args, ctx)
|
|
95
|
-
|
|
96
|
-
# Non-git: pure-Python fallback instead of `tree`.
|
|
97
|
-
return self._tree_fallback(pattern, base, depth)
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
"""macOS Grep tool: ``git grep -E`` instead of ``git grep -P``.
|
|
2
|
-
|
|
3
|
-
Apple's default Git (installed via Xcode Command Line Tools) is built
|
|
4
|
-
without PCRE support, so ``git grep -P`` (Perl-compatible regex) fails
|
|
5
|
-
with::
|
|
6
|
-
|
|
7
|
-
fatal: cannot use Perl-compatible regexes when not compiled with USE_LIBPCRE
|
|
8
|
-
|
|
9
|
-
The Linux :class:`Grep` tool uses ``-P`` as the first-choice backend;
|
|
10
|
-
when that fails on macOS the fallback chain (``rg`` → ``grep``) still
|
|
11
|
-
works, but:
|
|
12
|
-
|
|
13
|
-
- ``rg`` (ripgrep) is not installed by default on macOS.
|
|
14
|
-
- BSD ``grep`` supports only POSIX ERE, losing Perl features
|
|
15
|
-
(``\\d``, ``\\w``, lookaheads).
|
|
16
|
-
- Two unnecessary failed attempts add latency before reaching a
|
|
17
|
-
working backend.
|
|
18
|
-
|
|
19
|
-
``GrepMac`` replaces ``-P`` with ``-E`` (POSIX extended regex) in the
|
|
20
|
-
``git grep`` command, so the git path — the fastest and most common
|
|
21
|
-
backend — works on stock macOS. The rest of the fallback chain (rg,
|
|
22
|
-
grep) is inherited unchanged.
|
|
23
|
-
|
|
24
|
-
Only the ``git grep`` regex flag differs; the tool name and result
|
|
25
|
-
format are inherited so callers, the tool registry, and the plan-mode
|
|
26
|
-
write guard are platform-independent.
|
|
27
|
-
"""
|
|
28
|
-
|
|
29
|
-
from __future__ import annotations
|
|
30
|
-
|
|
31
|
-
import os
|
|
32
|
-
import subprocess
|
|
33
|
-
|
|
34
|
-
from .base import ToolContext
|
|
35
|
-
from .filesystem import _git_root
|
|
36
|
-
from .grep import Grep, _grep_out
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
class GrepMac(Grep):
|
|
40
|
-
"""Grep with ``git grep -E`` for macOS (no PCRE dependency)."""
|
|
41
|
-
|
|
42
|
-
def run(self, args: dict, ctx: ToolContext) -> str:
|
|
43
|
-
regex = args["regex"]
|
|
44
|
-
path = os.path.realpath(args["path"])
|
|
45
|
-
if not os.path.isdir(path) and not os.path.isfile(path):
|
|
46
|
-
return f"Error: path {args['path']} is not readable"
|
|
47
|
-
glob = args.get("glob")
|
|
48
|
-
context = args.get("context_lines")
|
|
49
|
-
if context is not None:
|
|
50
|
-
context = max(0, min(15, int(context)))
|
|
51
|
-
|
|
52
|
-
git_root = _git_root(path) if os.path.isdir(path) else None
|
|
53
|
-
if git_root:
|
|
54
|
-
rel = os.path.relpath(path, git_root)
|
|
55
|
-
pathspec = rel
|
|
56
|
-
if glob and os.path.isdir(path):
|
|
57
|
-
pathspec = os.path.join(rel, glob).replace(os.sep, "/")
|
|
58
|
-
# -E (extended regex) instead of -P (Perl regex): Apple's
|
|
59
|
-
# Git is built without PCRE, so -P fails on stock macOS.
|
|
60
|
-
cmd = [
|
|
61
|
-
"git",
|
|
62
|
-
"grep",
|
|
63
|
-
"--line-number",
|
|
64
|
-
"--no-color",
|
|
65
|
-
"--max-count=1000",
|
|
66
|
-
"--untracked",
|
|
67
|
-
"-E",
|
|
68
|
-
"-e",
|
|
69
|
-
regex,
|
|
70
|
-
"--",
|
|
71
|
-
pathspec,
|
|
72
|
-
]
|
|
73
|
-
if context:
|
|
74
|
-
cmd = cmd[:3] + [f"-C{context}"] + cmd[3:]
|
|
75
|
-
try:
|
|
76
|
-
proc = subprocess.run(
|
|
77
|
-
cmd,
|
|
78
|
-
cwd=git_root,
|
|
79
|
-
capture_output=True,
|
|
80
|
-
text=True,
|
|
81
|
-
encoding="utf-8",
|
|
82
|
-
errors="replace",
|
|
83
|
-
timeout=60,
|
|
84
|
-
)
|
|
85
|
-
except (OSError, subprocess.TimeoutExpired):
|
|
86
|
-
proc = None
|
|
87
|
-
if proc is not None and proc.returncode in (0, 1):
|
|
88
|
-
return _grep_out(proc, "git")
|
|
89
|
-
|
|
90
|
-
# Non-git or git grep failed: delegate to the parent's rg/grep
|
|
91
|
-
# fallback chain (identical on both platforms).
|
|
92
|
-
return super().run(args, ctx)
|
|
File without changes
|
|
File without changes
|
{python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/__main__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/commands.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/config.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/diffrender.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/mcp/__init__.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/mcp/client.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/mcp/config.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/mcp/manager.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/models.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/persistence.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/prompts/agent.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/prompts/plan.md
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/prompts/review.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/prompts/title.md
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/prompts.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/subagent.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/tool_runner.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/tools/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/tools/base.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/tools/bash.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/tools/edit_mac.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/tools/mcp.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/tools/mkdir.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/tools/planexit.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/tools/question.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/tools/read.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/tools/skill.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/tools/todo.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/tui/__init__.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/tui/commands.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/tui/core.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/tui/input.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/python_agent_harness/tui/render.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/tests/test_concurrent_subagents.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.9}/tests/test_subagent_isolation.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|