python-agent-harness 1.5.4.7__tar.gz → 1.5.4.8__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.8}/PKG-INFO +1 -1
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/pyproject.toml +1 -1
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/__init__.py +1 -1
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/client.py +38 -13
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/planmode.py +3 -1
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/session.py +1 -1
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/tools/edit.py +1 -1
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/tools/glob.py +2 -2
- python_agent_harness-1.5.4.8/python_agent_harness/tools/glob_mac.py +115 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/tools/grep.py +13 -1
- python_agent_harness-1.5.4.8/python_agent_harness/tools/grep_mac.py +209 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/tools/insert.py +2 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/tools/write.py +3 -1
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8/python_agent_harness.egg-info}/PKG-INFO +1 -1
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/tests/test_client.py +17 -2
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/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.8}/LICENSE +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/README.md +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/__main__.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/agent.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/cli.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/commands.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/config.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/context_manager.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/diffrender.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/mcp/__init__.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/mcp/client.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/mcp/config.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/mcp/manager.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/models.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/persistence.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/prompts/agent.md +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/prompts/build-switch.md +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/prompts/commands/explain.md +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/prompts/compact.md +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/prompts/initialize.md +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/prompts/plan-mode.md +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/prompts/plan.md +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/prompts/review.md +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/prompts/subagent.md +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/prompts/summary.md +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/prompts/task-completion-rules.md +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/prompts/title.md +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/prompts.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/subagent.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/token_estimator.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/tool_runner.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/tools/__init__.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/tools/agent_tool.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/tools/base.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/tools/bash.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/tools/diffapply.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/tools/edit_mac.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/tools/filesystem.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/tools/mcp.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/tools/mkdir.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/tools/planexit.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/tools/question.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/tools/read.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/tools/skill.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/tools/todo.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/tui/__init__.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/tui/commands.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/tui/core.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/tui/input.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/tui/render.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness.egg-info/SOURCES.txt +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness.egg-info/dependency_links.txt +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness.egg-info/entry_points.txt +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness.egg-info/requires.txt +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness.egg-info/top_level.txt +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/setup.cfg +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/tests/test_agents_md.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/tests/test_cli.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/tests/test_commands.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/tests/test_concurrent_subagents.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/tests/test_config.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/tests/test_context_rules.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/tests/test_diffrender.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/tests/test_invariants.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/tests/test_mcp.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/tests/test_models.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/tests/test_persistence.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/tests/test_planmode.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/tests/test_prompts.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/tests/test_scenarios.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/tests/test_session.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/tests/test_subagent.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/tests/test_subagent_isolation.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/tests/test_todos_scope.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/tests/test_token_estimator.py +0 -0
- {python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/tests/test_tools_misc.py +0 -0
{python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/client.py
RENAMED
|
@@ -277,12 +277,16 @@ class Client:
|
|
|
277
277
|
|
|
278
278
|
A blocked ``iter_lines()`` read must be interrupted so the agent
|
|
279
279
|
loop can stop promptly. Closing the pool alone is NOT enough:
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
the
|
|
285
|
-
|
|
280
|
+
``httpx.Client.close()`` from another thread cannot force-close a
|
|
281
|
+
socket whose fd is parked in a kernel ``recv``. So we first
|
|
282
|
+
reach into every in-flight connection socket and both
|
|
283
|
+
``shutdown(SHUT_RDWR)`` it (wakes the blocked read on Linux) and
|
|
284
|
+
``close()`` the raw fd (the reliable wake on macOS/BSD, where
|
|
285
|
+
``shutdown`` alone may not rouse a parked ``recv``); the woken
|
|
286
|
+
read surfaces as a connection error the loop treats as a cancel.
|
|
287
|
+
We then close the pool and swap in a fresh client for the next
|
|
288
|
+
request. See ``_abort_inflight_sockets`` for the platform
|
|
289
|
+
details.
|
|
286
290
|
"""
|
|
287
291
|
self._aborted = True
|
|
288
292
|
old = self._http
|
|
@@ -819,15 +823,31 @@ def _default_api_key() -> str | None:
|
|
|
819
823
|
|
|
820
824
|
|
|
821
825
|
def _abort_inflight_sockets(client: httpx.Client) -> None:
|
|
822
|
-
"""Wake any blocked stream reads
|
|
826
|
+
"""Wake any blocked stream reads on every live pool socket.
|
|
823
827
|
|
|
824
828
|
Reaches through httpx/httpcore internals (transport -> pool ->
|
|
825
|
-
connection -> network stream -> raw socket) and
|
|
826
|
-
``shutdown(SHUT_RDWR)``
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
829
|
+
connection -> network stream -> raw socket) and, for each live
|
|
830
|
+
connection, both ``shutdown(SHUT_RDWR)`` AND ``close()`` the raw
|
|
831
|
+
socket.
|
|
832
|
+
|
|
833
|
+
Both steps are needed for cross-platform reliability when another
|
|
834
|
+
thread is already blocked in ``recv``:
|
|
835
|
+
|
|
836
|
+
- On Linux, ``shutdown(SHUT_RDWR)`` reliably wakes the blocked
|
|
837
|
+
``recv`` (turning it into a clean EOF / connection error), while
|
|
838
|
+
httpx's own ``close()`` cannot — it can't force-close a socket
|
|
839
|
+
whose fd is parked in a kernel read.
|
|
840
|
+
- On macOS/BSD (notably CI's macos-latest on py3.11), ``shutdown``
|
|
841
|
+
from another thread is NOT guaranteed to wake a ``recv`` already
|
|
842
|
+
parked in the kernel; the read stays blocked until the fd itself
|
|
843
|
+
is closed. ``socket.close()`` closes the fd directly and does
|
|
844
|
+
wake it, so we always follow the shutdown with an explicit
|
|
845
|
+
close.
|
|
846
|
+
|
|
847
|
+
Closing the fd out from under httpcore is safe here: ``abort()``
|
|
848
|
+
swaps in a fresh pool immediately and closes the old one under
|
|
849
|
+
``contextlib.suppress`` right after this call, so a double-close is
|
|
850
|
+
harmless. Every step is best-effort; any failure is ignored.
|
|
831
851
|
"""
|
|
832
852
|
pool = getattr(getattr(client, "_transport", None), "_pool", None)
|
|
833
853
|
for conn in getattr(pool, "_connections", None) or ():
|
|
@@ -837,8 +857,13 @@ def _abort_inflight_sockets(client: httpx.Client) -> None:
|
|
|
837
857
|
except Exception: # noqa: BLE001 - best effort
|
|
838
858
|
sock = None
|
|
839
859
|
if sock is not None:
|
|
860
|
+
# shutdown first (wakes the recv on Linux without touching
|
|
861
|
+
# the fd), then close the fd (the reliable wake on
|
|
862
|
+
# macOS/BSD, where shutdown alone may not).
|
|
840
863
|
with contextlib.suppress(OSError):
|
|
841
864
|
sock.shutdown(_socket.SHUT_RDWR)
|
|
865
|
+
with contextlib.suppress(OSError):
|
|
866
|
+
sock.close()
|
|
842
867
|
|
|
843
868
|
|
|
844
869
|
def _iter_sse(lines: Iterator[str]) -> Iterator[str]:
|
{python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/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.8}/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.8}/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.8}/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.8}/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.8}/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.8}/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,7 +812,7 @@ 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()
|
|
@@ -821,7 +821,7 @@ class TestAbortInflightSockets(unittest.TestCase):
|
|
|
821
821
|
client._transport._pool._connections = [conn]
|
|
822
822
|
return client
|
|
823
823
|
|
|
824
|
-
def
|
|
824
|
+
def test_shuts_down_and_closes_live_socket(self):
|
|
825
825
|
import socket as _socket
|
|
826
826
|
|
|
827
827
|
from python_agent_harness.client import _abort_inflight_sockets
|
|
@@ -830,7 +830,22 @@ class TestAbortInflightSockets(unittest.TestCase):
|
|
|
830
830
|
stream = mock.Mock()
|
|
831
831
|
stream.get_extra_info.return_value = sock
|
|
832
832
|
_abort_inflight_sockets(self._fake_client_with_stream(stream))
|
|
833
|
+
# shutdown wakes the recv on Linux; close() is the reliable wake
|
|
834
|
+
# on macOS/BSD where shutdown alone may leave a parked recv stuck
|
|
833
835
|
sock.shutdown.assert_called_once_with(_socket.SHUT_RDWR)
|
|
836
|
+
sock.close.assert_called_once_with()
|
|
837
|
+
|
|
838
|
+
def test_close_still_runs_when_shutdown_raises(self):
|
|
839
|
+
"""A shutdown OSError must not skip the fd close: on macOS the
|
|
840
|
+
close is the step that actually wakes the blocked read."""
|
|
841
|
+
from python_agent_harness.client import _abort_inflight_sockets
|
|
842
|
+
|
|
843
|
+
sock = mock.Mock()
|
|
844
|
+
sock.shutdown.side_effect = OSError("not connected")
|
|
845
|
+
stream = mock.Mock()
|
|
846
|
+
stream.get_extra_info.return_value = sock
|
|
847
|
+
_abort_inflight_sockets(self._fake_client_with_stream(stream)) # no raise
|
|
848
|
+
sock.close.assert_called_once_with()
|
|
834
849
|
|
|
835
850
|
def test_empty_pool_is_noop(self):
|
|
836
851
|
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.8}/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.8}/python_agent_harness/commands.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/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.8}/python_agent_harness/diffrender.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/mcp/__init__.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/mcp/client.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/mcp/config.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/mcp/manager.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/models.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/persistence.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/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.8}/python_agent_harness/prompts/plan.md
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/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.8}/python_agent_harness/prompts/title.md
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/prompts.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/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.8}/python_agent_harness/tool_runner.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/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.8}/python_agent_harness/tools/base.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/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.8}/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.8}/python_agent_harness/tools/mcp.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/tools/mkdir.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/tools/planexit.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/tools/question.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/tools/read.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/tools/skill.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/tools/todo.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/tui/__init__.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/tui/commands.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/tui/core.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/python_agent_harness/tui/input.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.7 → python_agent_harness-1.5.4.8}/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.8}/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.8}/tests/test_subagent_isolation.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|