python-agent-harness 1.5.4.2__tar.gz → 1.5.4.3__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.2/python_agent_harness.egg-info → python_agent_harness-1.5.4.3}/PKG-INFO +2 -2
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/README.md +1 -1
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/pyproject.toml +1 -1
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/__init__.py +1 -1
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/mcp/manager.py +5 -1
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/planmode.py +5 -2
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/tools/__init__.py +11 -2
- python_agent_harness-1.5.4.3/python_agent_harness/tools/diffapply.py +246 -0
- python_agent_harness-1.5.4.3/python_agent_harness/tools/edit_mac.py +56 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/tools/glob.py +4 -1
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/tools/grep.py +1 -1
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3/python_agent_harness.egg-info}/PKG-INFO +2 -2
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness.egg-info/SOURCES.txt +2 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/tests/test_agents_md.py +5 -5
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/tests/test_filesystem.py +179 -13
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/tests/test_invariants.py +26 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/tests/test_prompts.py +2 -2
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/tests/test_session.py +16 -4
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/LICENSE +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/__main__.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/agent.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/cli.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/client.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/commands.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/config.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/context_manager.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/diffrender.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/mcp/__init__.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/mcp/client.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/mcp/config.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/models.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/persistence.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/prompts/agent.md +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/prompts/build-switch.md +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/prompts/commands/explain.md +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/prompts/compact.md +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/prompts/initialize.md +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/prompts/plan-mode.md +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/prompts/plan.md +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/prompts/review.md +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/prompts/subagent.md +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/prompts/summary.md +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/prompts/task-completion-rules.md +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/prompts/title.md +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/prompts.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/session.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/subagent.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/token_estimator.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/tool_runner.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/tools/agent_tool.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/tools/base.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/tools/bash.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/tools/edit.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/tools/filesystem.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/tools/insert.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/tools/mcp.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/tools/mkdir.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/tools/planexit.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/tools/question.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/tools/read.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/tools/skill.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/tools/todo.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/tools/write.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/tui/__init__.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/tui/commands.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/tui/core.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/tui/input.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/tui/render.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness.egg-info/dependency_links.txt +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness.egg-info/entry_points.txt +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness.egg-info/requires.txt +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness.egg-info/top_level.txt +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/setup.cfg +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/tests/test_cli.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/tests/test_client.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/tests/test_commands.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/tests/test_concurrent_subagents.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/tests/test_config.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/tests/test_context_rules.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/tests/test_diffrender.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/tests/test_mcp.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/tests/test_models.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/tests/test_persistence.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/tests/test_planmode.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/tests/test_scenarios.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/tests/test_subagent.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/tests/test_subagent_isolation.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/tests/test_todos_scope.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/tests/test_token_estimator.py +0 -0
- {python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/tests/test_tools_misc.py +0 -0
{python_agent_harness-1.5.4.2/python_agent_harness.egg-info → python_agent_harness-1.5.4.3}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-agent-harness
|
|
3
|
-
Version: 1.5.4.
|
|
3
|
+
Version: 1.5.4.3
|
|
4
4
|
Summary: A lightweight, hackable mini-OpenCode written in Python.
|
|
5
5
|
Author: huming
|
|
6
6
|
License-Expression: MIT
|
|
@@ -248,7 +248,7 @@ python_agent_harness/
|
|
|
248
248
|
|
|
249
249
|
## Development
|
|
250
250
|
|
|
251
|
-
Requires Python ≥ 3.11. CI runs against Python 3.11, 3.12, and 3.13.
|
|
251
|
+
Requires Python ≥ 3.11. CI runs against Python 3.11, 3.12, and 3.13 on Linux and macOS.
|
|
252
252
|
|
|
253
253
|
```sh
|
|
254
254
|
make test # unit tests
|
|
@@ -225,7 +225,7 @@ python_agent_harness/
|
|
|
225
225
|
|
|
226
226
|
## Development
|
|
227
227
|
|
|
228
|
-
Requires Python ≥ 3.11. CI runs against Python 3.11, 3.12, and 3.13.
|
|
228
|
+
Requires Python ≥ 3.11. CI runs against Python 3.11, 3.12, and 3.13 on Linux and macOS.
|
|
229
229
|
|
|
230
230
|
```sh
|
|
231
231
|
make test # unit tests
|
{python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/mcp/manager.py
RENAMED
|
@@ -23,6 +23,7 @@ SDK/connection errors that the tool adapter turns into error strings.
|
|
|
23
23
|
from __future__ import annotations
|
|
24
24
|
|
|
25
25
|
import asyncio
|
|
26
|
+
import concurrent.futures
|
|
26
27
|
import contextlib
|
|
27
28
|
import threading
|
|
28
29
|
import time
|
|
@@ -102,7 +103,10 @@ class _LoopThread:
|
|
|
102
103
|
while True:
|
|
103
104
|
try:
|
|
104
105
|
exc = future.exception(timeout=0.1)
|
|
105
|
-
|
|
106
|
+
# concurrent.futures.TimeoutError is the builtin TimeoutError on
|
|
107
|
+
# 3.11+ but a distinct class on 3.10 — catch the futures one so
|
|
108
|
+
# the poll-timeout branch works on every supported Python.
|
|
109
|
+
except concurrent.futures.TimeoutError:
|
|
106
110
|
# future still running: enforce deadline / cancellation
|
|
107
111
|
if deadline is not None and time.monotonic() >= deadline:
|
|
108
112
|
future.cancel()
|
{python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/planmode.py
RENAMED
|
@@ -64,8 +64,11 @@ class PlanMode:
|
|
|
64
64
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
65
65
|
if not os.path.exists(path):
|
|
66
66
|
Path(path).write_text("", encoding="utf-8")
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
# Store the canonical path so the plan-mode write guard (which
|
|
68
|
+
# compares against realpath'd tool targets) matches — on macOS
|
|
69
|
+
# TMPDIR (/var) is a symlink to /private/var.
|
|
70
|
+
self.plan_file = os.path.realpath(path)
|
|
71
|
+
return self.plan_file
|
|
69
72
|
|
|
70
73
|
def cleanup_plan_file(self) -> None:
|
|
71
74
|
if not self.plan_file:
|
{python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/tools/__init__.py
RENAMED
|
@@ -2,10 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import sys
|
|
6
|
+
|
|
5
7
|
from .agent_tool import AgentTool
|
|
6
8
|
from .base import PendingToolResult, Registry, Tool, ToolContext
|
|
7
9
|
from .bash import Bash
|
|
8
|
-
from .
|
|
10
|
+
from .edit import Edit
|
|
11
|
+
from .edit_mac import EditMac
|
|
12
|
+
from .filesystem import GlobTool, Grep, Insert, Mkdir, Read, Write
|
|
9
13
|
from .mcp import MCPTool, mcp_tools_from_manager, normalize_mcp_result
|
|
10
14
|
from .planexit import PlanExit
|
|
11
15
|
from .question import Question
|
|
@@ -20,6 +24,7 @@ __all__ = [
|
|
|
20
24
|
"AgentTool",
|
|
21
25
|
"Bash",
|
|
22
26
|
"Edit",
|
|
27
|
+
"EditMac",
|
|
23
28
|
"GlobTool",
|
|
24
29
|
"Grep",
|
|
25
30
|
"Insert",
|
|
@@ -38,6 +43,10 @@ __all__ = [
|
|
|
38
43
|
|
|
39
44
|
def default_registry() -> Registry:
|
|
40
45
|
reg = Registry()
|
|
46
|
+
# macOS's BSD patch rejects well-formed hunks that GNU patch accepts,
|
|
47
|
+
# so macOS uses EditMac (pure-Python diff applier); Linux keeps the
|
|
48
|
+
# patch-binary Edit. Both register under the name "Edit".
|
|
49
|
+
edit_tool = EditMac() if sys.platform == "darwin" else Edit()
|
|
41
50
|
for tool in (
|
|
42
51
|
AgentTool(),
|
|
43
52
|
TodoWrite(),
|
|
@@ -45,7 +54,7 @@ def default_registry() -> Registry:
|
|
|
45
54
|
Grep(),
|
|
46
55
|
Read(),
|
|
47
56
|
Insert(),
|
|
48
|
-
|
|
57
|
+
edit_tool,
|
|
49
58
|
Write(),
|
|
50
59
|
Mkdir(),
|
|
51
60
|
Bash(),
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"""Pure-Python unified diff applier (macOS Edit backend).
|
|
2
|
+
|
|
3
|
+
macOS ships Apple's old BSD ``/usr/bin/patch`` (patch 2.0-12u11-Apple),
|
|
4
|
+
which rejects well-formed hunks that GNU patch accepts: asymmetric
|
|
5
|
+
context (leading but no trailing context, or vice versa), hunks whose
|
|
6
|
+
last body line is a ``---``-rendered content line, etc. Rather than
|
|
7
|
+
rewriting every diff into a shape that specific patch binary likes, the
|
|
8
|
+
macOS Edit tool applies diffs here, in Python:
|
|
9
|
+
|
|
10
|
+
- the diff is parsed STRUCTURALLY, so hunk-header counts are advisory
|
|
11
|
+
(they are recounted from the body, like ``_fix_patch_headers``) and
|
|
12
|
+
``---``/``+++`` content lines are only file headers when followed by
|
|
13
|
+
their ``+++``/``---`` partner;
|
|
14
|
+
- hunks are matched against the file by content with fuzz (context
|
|
15
|
+
lines may mismatch up to ``FUZZ`` times, removed lines must match
|
|
16
|
+
exactly), preferring the position the header claims;
|
|
17
|
+
- hunks are applied bottom-up so earlier positions stay valid;
|
|
18
|
+
- files are written only when every hunk of the section matched.
|
|
19
|
+
|
|
20
|
+
The applier is platform-independent, so its behavior is covered by
|
|
21
|
+
tests on every OS.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import os
|
|
27
|
+
import re
|
|
28
|
+
|
|
29
|
+
_HUNK_HEADER_RE = re.compile(r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@")
|
|
30
|
+
_FILE_OLD_RE = re.compile(r"^---[ \t]")
|
|
31
|
+
_FILE_NEW_RE = re.compile(r"^\+\+\+[ \t]")
|
|
32
|
+
_HUNK_BOUNDARY_RE = re.compile(r"^@@ ")
|
|
33
|
+
|
|
34
|
+
# Maximum number of context lines that may mismatch when anchoring a
|
|
35
|
+
# hunk (removed lines must always match exactly).
|
|
36
|
+
FUZZ = 3
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class _Hunk:
|
|
40
|
+
__slots__ = ("old_start", "body")
|
|
41
|
+
|
|
42
|
+
def __init__(self, old_start: int, body: list[tuple[str, str, bool]]) -> None:
|
|
43
|
+
# body entries: (kind, content, no_newline); kind in ' ', '-', '+'.
|
|
44
|
+
self.old_start = old_start
|
|
45
|
+
self.body = body
|
|
46
|
+
|
|
47
|
+
def old_seq(self) -> list[tuple[str, str]]:
|
|
48
|
+
"""(kind, content-without-newline) for the lines that must exist
|
|
49
|
+
in the old file (' ' context and '-' removed lines)."""
|
|
50
|
+
return [
|
|
51
|
+
(kind, content.rstrip("\n")) for kind, content, _ in self.body if kind in (" ", "-")
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class _Section:
|
|
56
|
+
__slots__ = ("new_path", "hunks")
|
|
57
|
+
|
|
58
|
+
def __init__(self, new_path: str, hunks: list[_Hunk]) -> None:
|
|
59
|
+
self.new_path = new_path
|
|
60
|
+
self.hunks = hunks
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _section_header_at(lines: list[str], idx: int) -> bool:
|
|
64
|
+
"""Whether lines[idx] begins a file section: a ``--- path`` line
|
|
65
|
+
immediately followed by a ``+++ path`` line. Removed/added content
|
|
66
|
+
lines rendered ``---``/``+++`` inside a hunk body are only mistaken
|
|
67
|
+
for headers when the pair shape matches, so the trailing ``+++``
|
|
68
|
+
partner is required (mirrors ``_starts_file_section``)."""
|
|
69
|
+
if idx + 1 >= len(lines):
|
|
70
|
+
return False
|
|
71
|
+
return bool(_FILE_OLD_RE.match(lines[idx]) and _FILE_NEW_RE.match(lines[idx + 1]))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _parse(diff_text: str) -> list[_Section]:
|
|
75
|
+
"""Parse a unified diff into file sections (paths + hunks)."""
|
|
76
|
+
lines = diff_text.splitlines(keepends=True)
|
|
77
|
+
sections: list[_Section] = []
|
|
78
|
+
i = 0
|
|
79
|
+
n = len(lines)
|
|
80
|
+
while i < n:
|
|
81
|
+
if not _section_header_at(lines, i):
|
|
82
|
+
i += 1
|
|
83
|
+
continue
|
|
84
|
+
new_path = lines[i + 1][len("+++") :].strip()
|
|
85
|
+
i += 2
|
|
86
|
+
hunks: list[_Hunk] = []
|
|
87
|
+
while i < n:
|
|
88
|
+
m = _HUNK_HEADER_RE.match(lines[i])
|
|
89
|
+
if m:
|
|
90
|
+
old_start = int(m.group(1))
|
|
91
|
+
body: list[tuple[str, str, bool]] = []
|
|
92
|
+
i += 1
|
|
93
|
+
while (
|
|
94
|
+
i < n
|
|
95
|
+
and not _HUNK_BOUNDARY_RE.match(lines[i])
|
|
96
|
+
and not _section_header_at(lines, i)
|
|
97
|
+
):
|
|
98
|
+
line = lines[i]
|
|
99
|
+
if line.startswith("\\"): # ""
|
|
100
|
+
if body and body[-1][0] in (" ", "-", "+"):
|
|
101
|
+
kind, content, _ = body[-1]
|
|
102
|
+
body[-1] = (kind, content, True)
|
|
103
|
+
elif line[:1] in (" ", "-", "+"):
|
|
104
|
+
body.append((line[0], line[1:], False))
|
|
105
|
+
i += 1
|
|
106
|
+
hunks.append(_Hunk(old_start, body))
|
|
107
|
+
continue
|
|
108
|
+
if _section_header_at(lines, i):
|
|
109
|
+
break
|
|
110
|
+
i += 1 # stray line (e.g. "diff --git", "index ...") — skip
|
|
111
|
+
sections.append(_Section(new_path, hunks))
|
|
112
|
+
return sections
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _candidate_positions(start: int, limit: int) -> list[int]:
|
|
116
|
+
"""Match offsets around *start* (0-based), closest first, within
|
|
117
|
+
``FUZZ`` lines and bounded by [0, *limit*]."""
|
|
118
|
+
out: list[int] = []
|
|
119
|
+
seen: set[int] = set()
|
|
120
|
+
for d in range(FUZZ + 1):
|
|
121
|
+
for p in (start - d, start + d):
|
|
122
|
+
if 0 <= p <= limit and p not in seen:
|
|
123
|
+
seen.add(p)
|
|
124
|
+
out.append(p)
|
|
125
|
+
return out
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _match_hunk(hunk: _Hunk, file_lines: list[str]) -> int | None:
|
|
129
|
+
"""Find the 0-based position where the hunk's old lines match.
|
|
130
|
+
|
|
131
|
+
Removed lines must match exactly; up to ``FUZZ`` context lines may
|
|
132
|
+
mismatch (patch-style fuzz). Pure-insertion hunks (no old lines)
|
|
133
|
+
anchor at ``old_start - 1``.
|
|
134
|
+
"""
|
|
135
|
+
old_seq = hunk.old_seq()
|
|
136
|
+
if not old_seq:
|
|
137
|
+
p = hunk.old_start - 1
|
|
138
|
+
return max(0, min(p, len(file_lines)))
|
|
139
|
+
start = hunk.old_start - 1
|
|
140
|
+
limit = len(file_lines) - len(old_seq)
|
|
141
|
+
for p in _candidate_positions(start, limit):
|
|
142
|
+
mismatches = 0
|
|
143
|
+
ok = True
|
|
144
|
+
for i, (kind, content) in enumerate(old_seq):
|
|
145
|
+
file_content = file_lines[p + i].rstrip("\n")
|
|
146
|
+
if kind == "-":
|
|
147
|
+
if file_content != content:
|
|
148
|
+
ok = False
|
|
149
|
+
break
|
|
150
|
+
elif file_content != content:
|
|
151
|
+
mismatches += 1
|
|
152
|
+
if mismatches > FUZZ:
|
|
153
|
+
ok = False
|
|
154
|
+
break
|
|
155
|
+
if ok:
|
|
156
|
+
return p
|
|
157
|
+
return None
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _apply_hunk(hunk: _Hunk, pos: int, file_lines: list[str]) -> None:
|
|
161
|
+
"""Replace the matched region with the hunk's new lines in place."""
|
|
162
|
+
old_count = sum(1 for kind, _, _ in hunk.body if kind in (" ", "-"))
|
|
163
|
+
new_block: list[str] = []
|
|
164
|
+
p = pos
|
|
165
|
+
for kind, content, no_newline in hunk.body:
|
|
166
|
+
if kind == " ":
|
|
167
|
+
new_block.append(file_lines[p])
|
|
168
|
+
p += 1
|
|
169
|
+
elif kind == "-":
|
|
170
|
+
p += 1
|
|
171
|
+
else: # "+"
|
|
172
|
+
if no_newline:
|
|
173
|
+
new_block.append(content.rstrip("\n"))
|
|
174
|
+
elif content.endswith("\n"):
|
|
175
|
+
new_block.append(content)
|
|
176
|
+
else:
|
|
177
|
+
new_block.append(content + "\n")
|
|
178
|
+
file_lines[pos : pos + old_count] = new_block
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _resolve_target(new_path: str, cwd: str, fallback_path: str | None) -> str:
|
|
182
|
+
"""Resolve a diff's ``+++ path`` against *cwd*, with the single-file
|
|
183
|
+
*fallback_path* as backup when the name does not match an existing
|
|
184
|
+
file (e.g. the model wrote a bare basename for a deep path)."""
|
|
185
|
+
name = new_path
|
|
186
|
+
for prefix in ("a/", "b/"):
|
|
187
|
+
if name.startswith(prefix):
|
|
188
|
+
name = name[len(prefix) :]
|
|
189
|
+
break
|
|
190
|
+
if os.path.isabs(name):
|
|
191
|
+
return name
|
|
192
|
+
target = os.path.join(cwd, name)
|
|
193
|
+
if not os.path.isfile(target) and fallback_path and os.path.isfile(fallback_path):
|
|
194
|
+
return fallback_path
|
|
195
|
+
return target
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _apply_section(section: _Section, cwd: str, fallback_path: str | None) -> tuple[bool, str]:
|
|
199
|
+
target = _resolve_target(section.new_path, cwd, fallback_path)
|
|
200
|
+
if not os.path.isfile(target):
|
|
201
|
+
return False, f"target file does not exist: {target}"
|
|
202
|
+
try:
|
|
203
|
+
with open(target, encoding="utf-8", errors="replace") as f:
|
|
204
|
+
file_lines = f.readlines()
|
|
205
|
+
except OSError as e:
|
|
206
|
+
return False, f"cannot read {target}: {e}"
|
|
207
|
+
# Match every hunk against the ORIGINAL content first; only write
|
|
208
|
+
# when all matched (a failed hunk leaves the file untouched).
|
|
209
|
+
plan: list[tuple[_Hunk, int]] = []
|
|
210
|
+
for hunk in section.hunks:
|
|
211
|
+
pos = _match_hunk(hunk, file_lines)
|
|
212
|
+
if pos is None:
|
|
213
|
+
return (
|
|
214
|
+
False,
|
|
215
|
+
f"hunk at line {hunk.old_start} failed to match the file content",
|
|
216
|
+
)
|
|
217
|
+
plan.append((hunk, pos))
|
|
218
|
+
new_lines = list(file_lines)
|
|
219
|
+
for hunk, pos in reversed(plan): # bottom-up: earlier positions stay valid
|
|
220
|
+
_apply_hunk(hunk, pos, new_lines)
|
|
221
|
+
try:
|
|
222
|
+
with open(target, "w", encoding="utf-8") as f:
|
|
223
|
+
f.writelines(new_lines)
|
|
224
|
+
except OSError as e:
|
|
225
|
+
return False, f"cannot write {target}: {e}"
|
|
226
|
+
return True, f"patched {target}"
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def apply_unified_diff(
|
|
230
|
+
diff_text: str, cwd: str, fallback_path: str | None = None
|
|
231
|
+
) -> tuple[bool, str]:
|
|
232
|
+
"""Apply a unified diff to files under *cwd*.
|
|
233
|
+
|
|
234
|
+
Returns ``(ok, message)``; on failure no file is written. Multi-file
|
|
235
|
+
diffs are applied section by section. *fallback_path* is used when a
|
|
236
|
+
section's target cannot be resolved to an existing file (single-file
|
|
237
|
+
mode).
|
|
238
|
+
"""
|
|
239
|
+
sections = _parse(diff_text)
|
|
240
|
+
if not sections:
|
|
241
|
+
return False, "no file sections found in diff"
|
|
242
|
+
for section in sections:
|
|
243
|
+
ok, msg = _apply_section(section, cwd, fallback_path)
|
|
244
|
+
if not ok:
|
|
245
|
+
return False, msg
|
|
246
|
+
return True, f"applied {len(sections)} file section(s)"
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""macOS Edit tool: string replacement like ``Edit``, but diff/patch
|
|
2
|
+
mode applies diffs with the built-in Python applier instead of
|
|
3
|
+
``/usr/bin/patch``.
|
|
4
|
+
|
|
5
|
+
Apple's BSD ``patch`` (patch 2.0-12u11-Apple) rejects well-formed hunks
|
|
6
|
+
that GNU patch accepts — asymmetric context, ``---``/``+++``-rendered
|
|
7
|
+
content lines at hunk edges, and so on — so model-generated diffs that
|
|
8
|
+
apply cleanly on Linux fail on macOS. The Python applier
|
|
9
|
+
(``diffapply.py``) parses the diff structurally and matches hunks with
|
|
10
|
+
fuzz, so the same diffs apply on both platforms.
|
|
11
|
+
|
|
12
|
+
Only the diff-application strategy differs; the tool name stays
|
|
13
|
+
``Edit`` so callers, the tool registry and the plan-mode write guard
|
|
14
|
+
are platform-independent.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import os
|
|
20
|
+
|
|
21
|
+
from ..diffrender import unified_diff
|
|
22
|
+
from .base import ToolContext
|
|
23
|
+
from .diffapply import apply_unified_diff
|
|
24
|
+
from .edit import Edit, _strip_diff_fence
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class EditMac(Edit):
|
|
28
|
+
"""Edit with a pure-Python diff/patch backend for macOS."""
|
|
29
|
+
|
|
30
|
+
def _apply_patch(self, path: str, cwd: str, diff: str, ctx: ToolContext) -> str:
|
|
31
|
+
text = diff if diff.endswith("\n") else diff + "\n"
|
|
32
|
+
text = _strip_diff_fence(text)
|
|
33
|
+
is_file = os.path.isfile(path)
|
|
34
|
+
old_content = None
|
|
35
|
+
if is_file:
|
|
36
|
+
try:
|
|
37
|
+
with open(path, encoding="utf-8", errors="replace") as f:
|
|
38
|
+
old_content = f.read()
|
|
39
|
+
except OSError:
|
|
40
|
+
old_content = None
|
|
41
|
+
ok, msg = apply_unified_diff(text, cwd, path if is_file else None)
|
|
42
|
+
if not ok:
|
|
43
|
+
return f"Error: Failed to apply diff to {path}.\n{msg}"
|
|
44
|
+
if old_content is not None and os.path.isfile(path):
|
|
45
|
+
try:
|
|
46
|
+
with open(path, encoding="utf-8", errors="replace") as f:
|
|
47
|
+
new_content = f.read()
|
|
48
|
+
diff_text = unified_diff(old_content, new_content, path)
|
|
49
|
+
if diff_text:
|
|
50
|
+
ctx.record_diff(diff_text)
|
|
51
|
+
except OSError:
|
|
52
|
+
pass
|
|
53
|
+
return (
|
|
54
|
+
f"Diff successfully applied to {path}.\n"
|
|
55
|
+
f"Applied with the built-in Python diff applier.\n{msg}"
|
|
56
|
+
)
|
{python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/tools/glob.py
RENAMED
|
@@ -70,7 +70,10 @@ class GlobTool(Tool):
|
|
|
70
70
|
return f"Error: path {path} is not readable"
|
|
71
71
|
else:
|
|
72
72
|
path = ctx.cwd
|
|
73
|
-
|
|
73
|
+
# realpath (not abspath): _git_root resolves symlinks (macOS /var ->
|
|
74
|
+
# /private/var), so the base must be canonical or relpath produces a
|
|
75
|
+
# pathspec git rejects as "outside repository".
|
|
76
|
+
base = os.path.realpath(path)
|
|
74
77
|
depth = args.get("depth")
|
|
75
78
|
|
|
76
79
|
git_root = _git_root(base)
|
{python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/tools/grep.py
RENAMED
|
@@ -41,7 +41,7 @@ class Grep(Tool):
|
|
|
41
41
|
|
|
42
42
|
def run(self, args: dict, ctx: ToolContext) -> str:
|
|
43
43
|
regex = args["regex"]
|
|
44
|
-
path = os.path.
|
|
44
|
+
path = os.path.realpath(args["path"])
|
|
45
45
|
if not os.path.isdir(path) and not os.path.isfile(path):
|
|
46
46
|
return f"Error: path {args['path']} is not readable"
|
|
47
47
|
glob = args.get("glob")
|
{python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3/python_agent_harness.egg-info}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-agent-harness
|
|
3
|
-
Version: 1.5.4.
|
|
3
|
+
Version: 1.5.4.3
|
|
4
4
|
Summary: A lightweight, hackable mini-OpenCode written in Python.
|
|
5
5
|
Author: huming
|
|
6
6
|
License-Expression: MIT
|
|
@@ -248,7 +248,7 @@ python_agent_harness/
|
|
|
248
248
|
|
|
249
249
|
## Development
|
|
250
250
|
|
|
251
|
-
Requires Python ≥ 3.11. CI runs against Python 3.11, 3.12, and 3.13.
|
|
251
|
+
Requires Python ≥ 3.11. CI runs against Python 3.11, 3.12, and 3.13 on Linux and macOS.
|
|
252
252
|
|
|
253
253
|
```sh
|
|
254
254
|
make test # unit tests
|
|
@@ -44,7 +44,9 @@ python_agent_harness/tools/__init__.py
|
|
|
44
44
|
python_agent_harness/tools/agent_tool.py
|
|
45
45
|
python_agent_harness/tools/base.py
|
|
46
46
|
python_agent_harness/tools/bash.py
|
|
47
|
+
python_agent_harness/tools/diffapply.py
|
|
47
48
|
python_agent_harness/tools/edit.py
|
|
49
|
+
python_agent_harness/tools/edit_mac.py
|
|
48
50
|
python_agent_harness/tools/filesystem.py
|
|
49
51
|
python_agent_harness/tools/glob.py
|
|
50
52
|
python_agent_harness/tools/grep.py
|
|
@@ -46,7 +46,7 @@ class TestFindAgentsMdFiles(unittest.TestCase):
|
|
|
46
46
|
with tempfile.TemporaryDirectory() as d:
|
|
47
47
|
(Path(d) / "AGENTS.md").write_text("# Rules\n", encoding="utf-8")
|
|
48
48
|
result = find_agents_md_files(d)
|
|
49
|
-
self.assertEqual([Path(p) for p in result], [Path(d) / "AGENTS.md"])
|
|
49
|
+
self.assertEqual([Path(p) for p in result], [(Path(d) / "AGENTS.md").resolve()])
|
|
50
50
|
|
|
51
51
|
@unittest.skipUnless(shutil.which("git"), "git not available")
|
|
52
52
|
def test_finds_agents_md_in_ancestor(self):
|
|
@@ -57,7 +57,7 @@ class TestFindAgentsMdFiles(unittest.TestCase):
|
|
|
57
57
|
sub.mkdir(parents=True)
|
|
58
58
|
(Path(d) / "AGENTS.md").write_text("# Root rules\n", encoding="utf-8")
|
|
59
59
|
result = find_agents_md_files(str(sub))
|
|
60
|
-
self.assertEqual([Path(p) for p in result], [Path(d) / "AGENTS.md"])
|
|
60
|
+
self.assertEqual([Path(p) for p in result], [(Path(d) / "AGENTS.md").resolve()])
|
|
61
61
|
|
|
62
62
|
@unittest.skipUnless(shutil.which("git"), "git not available")
|
|
63
63
|
def test_stacks_every_ancestor_nearest_first(self):
|
|
@@ -76,7 +76,7 @@ class TestFindAgentsMdFiles(unittest.TestCase):
|
|
|
76
76
|
result = find_agents_md_files(str(sub))
|
|
77
77
|
self.assertEqual(
|
|
78
78
|
[Path(p) for p in result],
|
|
79
|
-
[pkg / "AGENTS.md", Path(d) / "AGENTS.md"],
|
|
79
|
+
[(pkg / "AGENTS.md").resolve(), (Path(d) / "AGENTS.md").resolve()],
|
|
80
80
|
)
|
|
81
81
|
|
|
82
82
|
@unittest.skipUnless(shutil.which("git"), "git not available")
|
|
@@ -92,7 +92,7 @@ class TestFindAgentsMdFiles(unittest.TestCase):
|
|
|
92
92
|
# above the git root: must NOT be picked up
|
|
93
93
|
(Path(outer) / "AGENTS.md").write_text("# Outer rules\n", encoding="utf-8")
|
|
94
94
|
result = find_agents_md_files(str(sub))
|
|
95
|
-
self.assertEqual([Path(p) for p in result], [repo / "AGENTS.md"])
|
|
95
|
+
self.assertEqual([Path(p) for p in result], [(repo / "AGENTS.md").resolve()])
|
|
96
96
|
|
|
97
97
|
def test_non_git_dir_is_its_own_bound(self):
|
|
98
98
|
"""Outside a repo the search is bounded by the project dir."""
|
|
@@ -147,7 +147,7 @@ class TestAgentsMdAsContextFiles(unittest.TestCase):
|
|
|
147
147
|
result = load_context_files(None, extra_files=find_agents_md_files(d))
|
|
148
148
|
self.assertEqual(
|
|
149
149
|
result,
|
|
150
|
-
f"Request context:\n\nIn file `{agents}`:\n\n# Project Rules\nUse 4 spaces.",
|
|
150
|
+
f"Request context:\n\nIn file `{agents.resolve()}`:\n\n# Project Rules\nUse 4 spaces.",
|
|
151
151
|
)
|
|
152
152
|
|
|
153
153
|
def test_empty_file_yields_no_context(self):
|
|
@@ -6,11 +6,13 @@ import os
|
|
|
6
6
|
import re
|
|
7
7
|
import shutil
|
|
8
8
|
import subprocess
|
|
9
|
+
import sys
|
|
9
10
|
import tempfile
|
|
10
11
|
import unittest
|
|
11
12
|
from unittest import mock
|
|
12
13
|
|
|
13
14
|
from python_agent_harness.tools.base import ToolContext
|
|
15
|
+
from python_agent_harness.tools.edit_mac import EditMac
|
|
14
16
|
from python_agent_harness.tools.filesystem import (
|
|
15
17
|
Edit,
|
|
16
18
|
GlobTool,
|
|
@@ -24,6 +26,13 @@ from python_agent_harness.tools.filesystem import (
|
|
|
24
26
|
)
|
|
25
27
|
|
|
26
28
|
|
|
29
|
+
def edit_tool() -> Edit:
|
|
30
|
+
"""The Edit tool active on this platform: Linux uses the patch
|
|
31
|
+
binary, macOS the built-in Python diff applier (Apple's BSD patch
|
|
32
|
+
rejects well-formed hunks that GNU patch accepts)."""
|
|
33
|
+
return EditMac() if sys.platform == "darwin" else Edit()
|
|
34
|
+
|
|
35
|
+
|
|
27
36
|
def _big_output(lines: int = 6000, width: int = 80) -> str:
|
|
28
37
|
return "".join(f"line{i:04d}-{'x' * width}\n" for i in range(lines))
|
|
29
38
|
|
|
@@ -512,11 +521,11 @@ class TestGlobGrepTools(unittest.TestCase):
|
|
|
512
521
|
self._mkdir("proj", "sub")
|
|
513
522
|
open(os.path.join(d, "sub", "b.py"), "w").close()
|
|
514
523
|
out = GlobTool().run({"pattern": "*.py", "path": d}, self.ctx)
|
|
515
|
-
self.assertIn(os.path.join(d, "a.py"), out)
|
|
516
|
-
self.assertIn(os.path.join(d, "sub", "b.py"), out)
|
|
524
|
+
self.assertIn(os.path.realpath(os.path.join(d, "a.py")), out)
|
|
525
|
+
self.assertIn(os.path.realpath(os.path.join(d, "sub", "b.py")), out)
|
|
517
526
|
out1 = GlobTool().run({"pattern": "*.py", "path": d, "depth": 1}, self.ctx)
|
|
518
|
-
self.assertIn(os.path.join(d, "a.py"), out1)
|
|
519
|
-
self.assertNotIn(os.path.join(d, "sub", "b.py"), out1)
|
|
527
|
+
self.assertIn(os.path.realpath(os.path.join(d, "a.py")), out1)
|
|
528
|
+
self.assertNotIn(os.path.realpath(os.path.join(d, "sub", "b.py")), out1)
|
|
520
529
|
|
|
521
530
|
@unittest.skipUnless(shutil.which("git"), "git not available")
|
|
522
531
|
def test_glob_and_grep_use_git_backend_in_repo(self):
|
|
@@ -527,8 +536,8 @@ class TestGlobGrepTools(unittest.TestCase):
|
|
|
527
536
|
f.write(content)
|
|
528
537
|
subprocess.run(["git", "add", "."], cwd=repo, check=True)
|
|
529
538
|
out = GlobTool().run({"pattern": "*", "path": repo}, self.ctx)
|
|
530
|
-
self.assertIn(os.path.join(repo, "a.py"), out)
|
|
531
|
-
self.assertIn(os.path.join(repo, "b.txt"), out)
|
|
539
|
+
self.assertIn(os.path.realpath(os.path.join(repo, "a.py")), out)
|
|
540
|
+
self.assertIn(os.path.realpath(os.path.join(repo, "b.txt")), out)
|
|
532
541
|
out = Grep().run({"regex": "hello", "path": repo}, self.ctx)
|
|
533
542
|
self.assertIn("a.py", out)
|
|
534
543
|
self.assertIn("hello", out)
|
|
@@ -537,6 +546,27 @@ class TestGlobGrepTools(unittest.TestCase):
|
|
|
537
546
|
self.assertIn("a.py", out)
|
|
538
547
|
self.assertIn("hello", out)
|
|
539
548
|
|
|
549
|
+
@unittest.skipUnless(shutil.which("git"), "git not available")
|
|
550
|
+
def test_glob_and_grep_work_through_symlinked_path(self):
|
|
551
|
+
"""Regression: a search path that goes through a symlink (macOS
|
|
552
|
+
/var -> /private/var) must be resolved before the git backend
|
|
553
|
+
computes relpaths, or git rejects the pathspec as outside the
|
|
554
|
+
repository."""
|
|
555
|
+
real = self._mkdir("real")
|
|
556
|
+
link = os.path.join(self.tmp.name, "link")
|
|
557
|
+
os.symlink(real, link)
|
|
558
|
+
subprocess.run(["git", "init", "-q", real], check=True)
|
|
559
|
+
with open(os.path.join(real, "a.py"), "w") as f:
|
|
560
|
+
f.write("hello world\n")
|
|
561
|
+
subprocess.run(["git", "add", "."], cwd=real, check=True)
|
|
562
|
+
out = GlobTool().run({"pattern": "*", "path": link}, self.ctx)
|
|
563
|
+
self.assertNotIn("outside repository", out)
|
|
564
|
+
self.assertNotIn("Glob failed", out)
|
|
565
|
+
self.assertIn(os.path.realpath(os.path.join(real, "a.py")), out)
|
|
566
|
+
out = Grep().run({"regex": "hello", "path": link}, self.ctx)
|
|
567
|
+
self.assertIn("a.py", out)
|
|
568
|
+
self.assertIn("hello", out)
|
|
569
|
+
|
|
540
570
|
def test_glob_nonexistent_path_errors(self):
|
|
541
571
|
out = GlobTool().run(
|
|
542
572
|
{"pattern": "*", "path": os.path.join(self.tmp.name, "nope")}, self.ctx
|
|
@@ -604,7 +634,7 @@ class TestEditTool(unittest.TestCase):
|
|
|
604
634
|
f.write("line1\nline2\nline3\n")
|
|
605
635
|
ctx, sess = make_ctx()
|
|
606
636
|
diff = "--- a/f.txt\n+++ b/f.txt\n@@ -1,3 +1,3 @@\n line1\n-line2\n+lineTWO\n line3\n"
|
|
607
|
-
result =
|
|
637
|
+
result = edit_tool().run({"path": path, "new_str": diff, "diff": True}, ctx)
|
|
608
638
|
self.assertIn("Diff successfully applied", result)
|
|
609
639
|
with open(path) as f:
|
|
610
640
|
self.assertEqual(f.read(), "line1\nlineTWO\nline3\n")
|
|
@@ -618,7 +648,7 @@ class TestEditTool(unittest.TestCase):
|
|
|
618
648
|
f.write(original)
|
|
619
649
|
ctx, sess = make_ctx()
|
|
620
650
|
bad_diff = "--- a/f.txt\n+++ b/f.txt\n@@ -1,2 +1,2 @@\n line1\n-NOPE\n+lineTWO\n"
|
|
621
|
-
result =
|
|
651
|
+
result = edit_tool().run({"path": path, "new_str": bad_diff, "diff": True}, ctx)
|
|
622
652
|
self.assertTrue(result.startswith("Error:"))
|
|
623
653
|
with open(path) as f:
|
|
624
654
|
self.assertEqual(f.read(), original) # unchanged on failure
|
|
@@ -1134,7 +1164,7 @@ class TestEditDiffModePatch(unittest.TestCase):
|
|
|
1134
1164
|
f.write("a\nb\nc\n")
|
|
1135
1165
|
ctx, _ = make_ctx()
|
|
1136
1166
|
diff = "```diff\n--- a/f.txt\n+++ b/f.txt\n@@ -1,3 +1,3 @@\n a\n-b\n+B\n c\n```\n"
|
|
1137
|
-
result =
|
|
1167
|
+
result = edit_tool().run({"path": path, "new_str": diff, "diff": True}, ctx)
|
|
1138
1168
|
self.assertIn("Diff successfully applied", result)
|
|
1139
1169
|
with open(path) as f:
|
|
1140
1170
|
self.assertEqual(f.read(), "a\nB\nc\n")
|
|
@@ -1148,7 +1178,7 @@ class TestEditDiffModePatch(unittest.TestCase):
|
|
|
1148
1178
|
f.write("a\nb\nc\n")
|
|
1149
1179
|
ctx, _ = make_ctx()
|
|
1150
1180
|
diff = "--- a/f.txt\n+++ b/f.txt\n@@ -1,9 +1,9 @@\n a\n-b\n+B\n c\n"
|
|
1151
|
-
result =
|
|
1181
|
+
result = edit_tool().run({"path": path, "new_str": diff, "diff": True}, ctx)
|
|
1152
1182
|
self.assertIn("Diff successfully applied", result)
|
|
1153
1183
|
with open(path) as f:
|
|
1154
1184
|
self.assertEqual(f.read(), "a\nB\nc\n")
|
|
@@ -1163,7 +1193,7 @@ class TestEditDiffModePatch(unittest.TestCase):
|
|
|
1163
1193
|
f.write("a\n--removed\nzzz\nc\n")
|
|
1164
1194
|
ctx, _ = make_ctx()
|
|
1165
1195
|
diff = "--- a/f.txt\n+++ b/f.txt\n@@ -1,9 +1,9 @@\n a\n---removed\n+++added\n c\n"
|
|
1166
|
-
result =
|
|
1196
|
+
result = edit_tool().run({"path": path, "new_str": diff, "diff": True}, ctx)
|
|
1167
1197
|
self.assertIn("Diff successfully applied", result)
|
|
1168
1198
|
with open(path) as f:
|
|
1169
1199
|
self.assertEqual(f.read(), "a\n++added\nzzz\nc\n")
|
|
@@ -1182,7 +1212,7 @@ class TestEditDiffModePatch(unittest.TestCase):
|
|
|
1182
1212
|
"@@ -2,3 +2,2 @@\n id INT\n );\n--- old comment\n"
|
|
1183
1213
|
"@@ -6,1 +6,1 @@\n--- tail\n+-- new tail\n"
|
|
1184
1214
|
)
|
|
1185
|
-
result =
|
|
1215
|
+
result = edit_tool().run({"path": path, "new_str": diff, "diff": True}, ctx)
|
|
1186
1216
|
self.assertIn("Diff successfully applied", result)
|
|
1187
1217
|
with open(path) as f:
|
|
1188
1218
|
self.assertEqual(
|
|
@@ -1204,7 +1234,7 @@ class TestEditDiffModePatch(unittest.TestCase):
|
|
|
1204
1234
|
"--- a/f2.txt\n+++ b/f2.txt\n@@ -1 +1 @@\n-two\n+TWO\n"
|
|
1205
1235
|
)
|
|
1206
1236
|
# Trailing slash -> patch runs inside the directory.
|
|
1207
|
-
result =
|
|
1237
|
+
result = edit_tool().run({"path": d + os.sep, "new_str": diff, "diff": True}, ctx)
|
|
1208
1238
|
self.assertIn("Diff successfully applied", result)
|
|
1209
1239
|
with open(os.path.join(d, "f1.txt")) as f:
|
|
1210
1240
|
self.assertEqual(f.read(), "ONE\n")
|
|
@@ -1212,5 +1242,141 @@ class TestEditDiffModePatch(unittest.TestCase):
|
|
|
1212
1242
|
self.assertEqual(f.read(), "TWO\n")
|
|
1213
1243
|
|
|
1214
1244
|
|
|
1245
|
+
class TestEditMac(unittest.TestCase):
|
|
1246
|
+
"""The macOS Edit backend (pure-Python diff applier) must accept the
|
|
1247
|
+
same model diffs that GNU patch accepts on Linux. Run on every
|
|
1248
|
+
platform so the macOS path is covered by Linux CI too."""
|
|
1249
|
+
|
|
1250
|
+
def _file(self, d: str, name: str, content: str) -> str:
|
|
1251
|
+
path = os.path.join(d, name)
|
|
1252
|
+
with open(path, "w") as f:
|
|
1253
|
+
f.write(content)
|
|
1254
|
+
return path
|
|
1255
|
+
|
|
1256
|
+
def test_dashed_content_line_ending_a_hunk_applies(self):
|
|
1257
|
+
"""The macOS CI failure: a removed line starting with -- as the
|
|
1258
|
+
LAST body line of a hunk, followed by a second hunk. Apple's
|
|
1259
|
+
BSD patch rejects it; the Python applier must apply it."""
|
|
1260
|
+
with tempfile.TemporaryDirectory() as d:
|
|
1261
|
+
path = self._file(
|
|
1262
|
+
d,
|
|
1263
|
+
"schema.sql",
|
|
1264
|
+
"CREATE TABLE t (\n id INT\n);\n-- old comment\nSELECT 1;\n-- tail\n",
|
|
1265
|
+
)
|
|
1266
|
+
ctx, sess = make_ctx()
|
|
1267
|
+
diff = (
|
|
1268
|
+
"--- a/schema.sql\n+++ b/schema.sql\n"
|
|
1269
|
+
"@@ -2,3 +2,2 @@\n id INT\n );\n--- old comment\n"
|
|
1270
|
+
"@@ -6,1 +6,1 @@\n--- tail\n+-- new tail\n"
|
|
1271
|
+
)
|
|
1272
|
+
result = EditMac().run({"path": path, "new_str": diff, "diff": True}, ctx)
|
|
1273
|
+
self.assertIn("Diff successfully applied", result)
|
|
1274
|
+
with open(path) as f:
|
|
1275
|
+
self.assertEqual(
|
|
1276
|
+
f.read(), "CREATE TABLE t (\n id INT\n);\nSELECT 1;\n-- new tail\n"
|
|
1277
|
+
)
|
|
1278
|
+
self.assertEqual(len(sess.recorded_diffs), 1)
|
|
1279
|
+
|
|
1280
|
+
def test_simple_replace_applies(self):
|
|
1281
|
+
with tempfile.TemporaryDirectory() as d:
|
|
1282
|
+
path = self._file(d, "f.txt", "line1\nline2\nline3\n")
|
|
1283
|
+
ctx, sess = make_ctx()
|
|
1284
|
+
diff = "--- a/f.txt\n+++ b/f.txt\n@@ -1,3 +1,3 @@\n line1\n-line2\n+lineTWO\n line3\n"
|
|
1285
|
+
result = EditMac().run({"path": path, "new_str": diff, "diff": True}, ctx)
|
|
1286
|
+
self.assertIn("Diff successfully applied", result)
|
|
1287
|
+
with open(path) as f:
|
|
1288
|
+
self.assertEqual(f.read(), "line1\nlineTWO\nline3\n")
|
|
1289
|
+
self.assertEqual(len(sess.recorded_diffs), 1)
|
|
1290
|
+
|
|
1291
|
+
def test_wrong_hunk_counts_recounted(self):
|
|
1292
|
+
with tempfile.TemporaryDirectory() as d:
|
|
1293
|
+
path = self._file(d, "f.txt", "a\nb\nc\n")
|
|
1294
|
+
ctx, _ = make_ctx()
|
|
1295
|
+
diff = "--- a/f.txt\n+++ b/f.txt\n@@ -1,9 +1,9 @@\n a\n-b\n+B\n c\n"
|
|
1296
|
+
result = EditMac().run({"path": path, "new_str": diff, "diff": True}, ctx)
|
|
1297
|
+
self.assertIn("Diff successfully applied", result)
|
|
1298
|
+
with open(path) as f:
|
|
1299
|
+
self.assertEqual(f.read(), "a\nB\nc\n")
|
|
1300
|
+
|
|
1301
|
+
def test_fenced_diff_applies(self):
|
|
1302
|
+
with tempfile.TemporaryDirectory() as d:
|
|
1303
|
+
path = self._file(d, "f.txt", "a\nb\nc\n")
|
|
1304
|
+
ctx, _ = make_ctx()
|
|
1305
|
+
diff = "```diff\n--- a/f.txt\n+++ b/f.txt\n@@ -1,3 +1,3 @@\n a\n-b\n+B\n c\n```\n"
|
|
1306
|
+
result = EditMac().run({"path": path, "new_str": diff, "diff": True}, ctx)
|
|
1307
|
+
self.assertIn("Diff successfully applied", result)
|
|
1308
|
+
with open(path) as f:
|
|
1309
|
+
self.assertEqual(f.read(), "a\nB\nc\n")
|
|
1310
|
+
|
|
1311
|
+
def test_dashed_content_lines_apply(self):
|
|
1312
|
+
with tempfile.TemporaryDirectory() as d:
|
|
1313
|
+
path = self._file(d, "f.txt", "a\n--removed\nzzz\nc\n")
|
|
1314
|
+
ctx, _ = make_ctx()
|
|
1315
|
+
diff = "--- a/f.txt\n+++ b/f.txt\n@@ -1,9 +1,9 @@\n a\n---removed\n+++added\n c\n"
|
|
1316
|
+
result = EditMac().run({"path": path, "new_str": diff, "diff": True}, ctx)
|
|
1317
|
+
self.assertIn("Diff successfully applied", result)
|
|
1318
|
+
with open(path) as f:
|
|
1319
|
+
self.assertEqual(f.read(), "a\n++added\nzzz\nc\n")
|
|
1320
|
+
|
|
1321
|
+
def test_mismatch_errors_and_leaves_file_untouched(self):
|
|
1322
|
+
with tempfile.TemporaryDirectory() as d:
|
|
1323
|
+
path = self._file(d, "f.txt", "line1\nline2\nline3\n")
|
|
1324
|
+
ctx, sess = make_ctx()
|
|
1325
|
+
bad_diff = "--- a/f.txt\n+++ b/f.txt\n@@ -1,2 +1,2 @@\n line1\n-NOPE\n+lineTWO\n"
|
|
1326
|
+
result = EditMac().run({"path": path, "new_str": bad_diff, "diff": True}, ctx)
|
|
1327
|
+
self.assertTrue(result.startswith("Error:"))
|
|
1328
|
+
with open(path) as f:
|
|
1329
|
+
self.assertEqual(f.read(), "line1\nline2\nline3\n")
|
|
1330
|
+
self.assertEqual(sess.recorded_diffs, [])
|
|
1331
|
+
|
|
1332
|
+
def test_directory_multifile_diff_applies(self):
|
|
1333
|
+
with tempfile.TemporaryDirectory() as d:
|
|
1334
|
+
self._file(d, "f1.txt", "one\n")
|
|
1335
|
+
self._file(d, "f2.txt", "two\n")
|
|
1336
|
+
ctx, _ = make_ctx()
|
|
1337
|
+
diff = (
|
|
1338
|
+
"--- a/f1.txt\n+++ b/f1.txt\n@@ -1 +1 @@\n-one\n+ONE\n"
|
|
1339
|
+
"--- a/f2.txt\n+++ b/f2.txt\n@@ -1 +1 @@\n-two\n+TWO\n"
|
|
1340
|
+
)
|
|
1341
|
+
result = EditMac().run({"path": d + os.sep, "new_str": diff, "diff": True}, ctx)
|
|
1342
|
+
self.assertIn("Diff successfully applied", result)
|
|
1343
|
+
with open(os.path.join(d, "f1.txt")) as f:
|
|
1344
|
+
self.assertEqual(f.read(), "ONE\n")
|
|
1345
|
+
with open(os.path.join(d, "f2.txt")) as f:
|
|
1346
|
+
self.assertEqual(f.read(), "TWO\n")
|
|
1347
|
+
|
|
1348
|
+
def test_insert_only_hunk(self):
|
|
1349
|
+
with tempfile.TemporaryDirectory() as d:
|
|
1350
|
+
path = self._file(d, "f.txt", "a\nc\n")
|
|
1351
|
+
ctx, _ = make_ctx()
|
|
1352
|
+
diff = "--- a/f.txt\n+++ b/f.txt\n@@ -1,1 +1,2 @@\n a\n+b\n c\n"
|
|
1353
|
+
result = EditMac().run({"path": path, "new_str": diff, "diff": True}, ctx)
|
|
1354
|
+
self.assertIn("Diff successfully applied", result)
|
|
1355
|
+
with open(path) as f:
|
|
1356
|
+
self.assertEqual(f.read(), "a\nb\nc\n")
|
|
1357
|
+
|
|
1358
|
+
def test_fuzz_matches_context_one_line_off(self):
|
|
1359
|
+
"""A context line that does not match (model drift) still
|
|
1360
|
+
applies when the removed lines match, up to the fuzz limit."""
|
|
1361
|
+
with tempfile.TemporaryDirectory() as d:
|
|
1362
|
+
path = self._file(d, "f.txt", "x\na\nzzz\nc\n")
|
|
1363
|
+
ctx, _ = make_ctx()
|
|
1364
|
+
diff = "--- a/f.txt\n+++ b/f.txt\n@@ -1,3 +1,3 @@\n a\n-zzz\n+c\n"
|
|
1365
|
+
result = EditMac().run({"path": path, "new_str": diff, "diff": True}, ctx)
|
|
1366
|
+
self.assertIn("Diff successfully applied", result)
|
|
1367
|
+
with open(path) as f:
|
|
1368
|
+
self.assertEqual(f.read(), "x\na\nc\nc\n")
|
|
1369
|
+
|
|
1370
|
+
def test_no_newline_at_eof_handled(self):
|
|
1371
|
+
with tempfile.TemporaryDirectory() as d:
|
|
1372
|
+
path = self._file(d, "f.txt", "a\nb")
|
|
1373
|
+
ctx, _ = make_ctx()
|
|
1374
|
+
diff = "--- a/f.txt\n+++ b/f.txt\n@@ -1,2 +1,2 @@\n a\n-b\n+b\n\\n"
|
|
1375
|
+
result = EditMac().run({"path": path, "new_str": diff, "diff": True}, ctx)
|
|
1376
|
+
self.assertIn("Diff successfully applied", result)
|
|
1377
|
+
with open(path) as f:
|
|
1378
|
+
self.assertEqual(f.read(), "a\nb")
|
|
1379
|
+
|
|
1380
|
+
|
|
1215
1381
|
if __name__ == "__main__":
|
|
1216
1382
|
unittest.main()
|
|
@@ -780,6 +780,32 @@ class TestPlanModeWriteGuard(unittest.TestCase):
|
|
|
780
780
|
with open(plan_file) as f:
|
|
781
781
|
self.assertEqual(f.read(), "via link")
|
|
782
782
|
|
|
783
|
+
def test_plan_file_writable_when_temp_dir_is_symlinked(self):
|
|
784
|
+
"""Regression: when the plan dir goes through a symlink (macOS
|
|
785
|
+
TMPDIR /var -> /private/var), the plan file must still be
|
|
786
|
+
recognized as the plan file and stay writable in plan mode."""
|
|
787
|
+
with tempfile.TemporaryDirectory(prefix="pah-prop-plan-") as tmpdir:
|
|
788
|
+
real = os.path.join(tmpdir, "real")
|
|
789
|
+
os.makedirs(real)
|
|
790
|
+
link = os.path.join(tmpdir, "link")
|
|
791
|
+
os.symlink(real, link)
|
|
792
|
+
with mock.patch(
|
|
793
|
+
"python_agent_harness.planmode._plan_temp_dir",
|
|
794
|
+
return_value=link,
|
|
795
|
+
):
|
|
796
|
+
session = self.make_plan_session(tmpdir)
|
|
797
|
+
plan_file = session.plan_mode.plan_file
|
|
798
|
+
self.assertTrue(os.path.isfile(plan_file))
|
|
799
|
+
dirname, basename = os.path.split(plan_file)
|
|
800
|
+
r = session.execute_tool(
|
|
801
|
+
"Write",
|
|
802
|
+
{"path": dirname, "filename": basename, "content": "# plan\n"},
|
|
803
|
+
call_id="w1",
|
|
804
|
+
)
|
|
805
|
+
self.assertNotIn("blocked by plan mode", r)
|
|
806
|
+
with open(plan_file) as f:
|
|
807
|
+
self.assertEqual(f.read(), "# plan\n")
|
|
808
|
+
|
|
783
809
|
def test_read_only_tools_pass_through_plan_mode(self):
|
|
784
810
|
with tempfile.TemporaryDirectory(prefix="pah-prop-plan-") as tmpdir:
|
|
785
811
|
session = self.make_plan_session(tmpdir)
|
|
@@ -200,7 +200,7 @@ class TestIndexSkills(unittest.TestCase):
|
|
|
200
200
|
encoding="utf-8",
|
|
201
201
|
)
|
|
202
202
|
index = index_skills(d)
|
|
203
|
-
self.assertEqual(index, {"天气预报助手": (
|
|
203
|
+
self.assertEqual(index, {"天气预报助手": (os.path.realpath(skill), "does things")})
|
|
204
204
|
|
|
205
205
|
def test_no_frontmatter_name_skipped(self):
|
|
206
206
|
with tempfile.TemporaryDirectory() as d:
|
|
@@ -219,7 +219,7 @@ class TestIndexSkills(unittest.TestCase):
|
|
|
219
219
|
skill = sub / "SKILL.md"
|
|
220
220
|
skill.write_text("---\nname: linked-skill\n---\nbody", encoding="utf-8")
|
|
221
221
|
os.symlink(sub, Path(d) / "linked")
|
|
222
|
-
self.assertEqual(index_skills(d)["linked-skill"][0],
|
|
222
|
+
self.assertEqual(index_skills(d)["linked-skill"][0], os.path.realpath(skill))
|
|
223
223
|
|
|
224
224
|
|
|
225
225
|
class TestLoadContextFiles(unittest.TestCase):
|
|
@@ -152,7 +152,10 @@ class TestFindSkill(unittest.TestCase):
|
|
|
152
152
|
with open(os.path.join(sub, "SKILL.md"), "w") as f:
|
|
153
153
|
f.write("---\nname: my-rules\ndescription: rules\n---\n# Rules")
|
|
154
154
|
session._skill_dir = d
|
|
155
|
-
self.assertEqual(
|
|
155
|
+
self.assertEqual(
|
|
156
|
+
session.find_skill("my-rules"),
|
|
157
|
+
os.path.realpath(os.path.join(sub, "SKILL.md")),
|
|
158
|
+
)
|
|
156
159
|
|
|
157
160
|
def test_frontmatter_name_differs_from_directory_name(self):
|
|
158
161
|
"""Regression test: the advertised name (frontmatter) must
|
|
@@ -165,7 +168,10 @@ class TestFindSkill(unittest.TestCase):
|
|
|
165
168
|
with open(os.path.join(sub, "SKILL.md"), "w") as f:
|
|
166
169
|
f.write("---\nname: 天气预报助手\n---\n# body")
|
|
167
170
|
session._skill_dir = d
|
|
168
|
-
self.assertEqual(
|
|
171
|
+
self.assertEqual(
|
|
172
|
+
session.find_skill("天气预报助手"),
|
|
173
|
+
os.path.realpath(os.path.join(sub, "SKILL.md")),
|
|
174
|
+
)
|
|
169
175
|
|
|
170
176
|
def test_nested_skill_dir_discovered(self):
|
|
171
177
|
session = RecordingSession()
|
|
@@ -175,7 +181,10 @@ class TestFindSkill(unittest.TestCase):
|
|
|
175
181
|
with open(os.path.join(nested, "SKILL.md"), "w") as f:
|
|
176
182
|
f.write("---\nname: deep-skill\n---\nbody")
|
|
177
183
|
session._skill_dir = d
|
|
178
|
-
self.assertEqual(
|
|
184
|
+
self.assertEqual(
|
|
185
|
+
session.find_skill("deep-skill"),
|
|
186
|
+
os.path.realpath(os.path.join(nested, "SKILL.md")),
|
|
187
|
+
)
|
|
179
188
|
|
|
180
189
|
def test_flat_file_without_frontmatter_returns_none(self):
|
|
181
190
|
"""Flat files (skills/style.md) are no longer resolvable: only
|
|
@@ -210,7 +219,10 @@ class TestFindSkill(unittest.TestCase):
|
|
|
210
219
|
f.write("---\nname: linked-skill\n---\n# Linked")
|
|
211
220
|
os.symlink(sub, os.path.join(d, "linked"))
|
|
212
221
|
session._skill_dir = d
|
|
213
|
-
self.assertEqual(
|
|
222
|
+
self.assertEqual(
|
|
223
|
+
session.find_skill("linked-skill"),
|
|
224
|
+
os.path.realpath(os.path.join(sub, "SKILL.md")),
|
|
225
|
+
)
|
|
214
226
|
|
|
215
227
|
def test_traversal_skill_returns_none(self):
|
|
216
228
|
"""Path-traversal inputs are inert: lookup only matches names in
|
|
File without changes
|
{python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/__main__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/client.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/commands.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/config.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/diffrender.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/mcp/__init__.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/mcp/client.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/mcp/config.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/models.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/persistence.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/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.2 → python_agent_harness-1.5.4.3}/python_agent_harness/prompts/plan.md
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/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.2 → python_agent_harness-1.5.4.3}/python_agent_harness/prompts/title.md
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/prompts.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/session.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/subagent.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/tool_runner.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/tools/base.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/tools/bash.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/tools/edit.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/tools/insert.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/tools/mcp.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/tools/mkdir.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/tools/planexit.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/tools/question.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/tools/read.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/tools/skill.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/tools/todo.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/tools/write.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/tui/__init__.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/tui/commands.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/tui/core.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/python_agent_harness/tui/input.py
RENAMED
|
File without changes
|
{python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/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
|
{python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/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
|
{python_agent_harness-1.5.4.2 → python_agent_harness-1.5.4.3}/tests/test_subagent_isolation.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|