tenseleyflow/loader / 5943c43

Browse files

Fix external path globbing

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
5943c43d96ef6e8cc09127b6af69f6c6bedaf5b7
Parents
d68665f
Tree
882279e

3 changed files

StatusFile+-
M src/loader/runtime/prompting.py 4 0
M src/loader/tools/file_tools.py 54 7
M tests/test_tools.py 18 0
src/loader/runtime/prompting.pymodified
@@ -119,6 +119,10 @@ MODE_GUIDANCE = {
119119
 - For servers, watchers, preview commands, or anything else that keeps running,
120120
   call `bash` with `background=true`, then inspect it with `bash_wait` or
121121
   `bash_jobs` instead of blocking the turn in the foreground
122
+- If the task names an external directory like `~/Loader/...`, keep operating on
123
+  that exact path instead of falling back to the repo cwd; file tools accept
124
+  absolute and `~` paths, and `glob` works best with `path="~/Loader/..."`
125
+  plus a relative `pattern` like `*.html`
122126
 - Concise reporting is fine, and numbered lists are allowed when they
123127
   communicate plan or evidence clearly
124128
 """,
src/loader/tools/file_tools.pymodified
@@ -16,6 +16,49 @@ from .fs_safety import (
1616
     resolve_workspace_path,
1717
 )
1818
 
19
+_GLOB_MAGIC_CHARS = "*?["
20
+
21
+
22
+def _has_glob_magic(segment: str) -> bool:
23
+    """Return whether one path segment contains glob syntax."""
24
+
25
+    return any(char in segment for char in _GLOB_MAGIC_CHARS)
26
+
27
+
28
+def _resolve_glob_base_and_pattern(
29
+    pattern: str,
30
+    path: str,
31
+) -> tuple[Path, str]:
32
+    """Resolve glob inputs, including `~`/absolute patterns outside the cwd."""
33
+
34
+    expanded_pattern = Path(pattern).expanduser()
35
+    pattern_is_explicit_path = pattern.startswith("~") or expanded_pattern.is_absolute()
36
+
37
+    if not pattern_is_explicit_path:
38
+        base_path = resolve_workspace_path(path, workspace_root=None)
39
+        return base_path, pattern
40
+
41
+    base_parts: list[str] = []
42
+    pattern_parts: list[str] = []
43
+    saw_glob = False
44
+    for part in expanded_pattern.parts:
45
+        if saw_glob or _has_glob_magic(part):
46
+            saw_glob = True
47
+            pattern_parts.append(part)
48
+        else:
49
+            base_parts.append(part)
50
+
51
+    if not pattern_parts:
52
+        if expanded_pattern.name:
53
+            pattern_parts = [expanded_pattern.name]
54
+            base_parts = list(expanded_pattern.parent.parts)
55
+        else:
56
+            pattern_parts = ["*"]
57
+
58
+    raw_base = str(Path(*base_parts)) if base_parts else expanded_pattern.anchor or "."
59
+    base_path = resolve_workspace_path(raw_base, workspace_root=None)
60
+    return base_path, "/".join(pattern_parts)
61
+
1962
 
2063
 class ReadTool(Tool):
2164
     """Read file contents."""
@@ -544,7 +587,11 @@ class GlobTool(Tool):
544587
 
545588
     @property
546589
     def description(self) -> str:
547
-        return "Find files matching a glob pattern (e.g., '**/*.py', 'src/*.ts')."
590
+        return (
591
+            "Find files matching a glob pattern (e.g., '**/*.py', 'src/*.ts'). "
592
+            "For external directories, prefer path='~/Loader/animals' with "
593
+            "pattern='*.html'; absolute or '~'-prefixed patterns are also accepted."
594
+        )
548595
 
549596
     @property
550597
     def parameters(self) -> dict[str, Any]:
@@ -572,20 +619,18 @@ class GlobTool(Tool):
572619
     ) -> ToolResult:
573620
         try:
574621
             # Glob is read-only — don't enforce workspace boundary
575
-            base_path = resolve_workspace_path(
576
-                path,
577
-                workspace_root=None,
578
-            )
622
+            base_path, effective_pattern = _resolve_glob_base_and_pattern(pattern, path)
579623
         except FileNotFoundError:
580624
             return ToolResult(f"Directory not found: {path}", is_error=True)
581625
         except Exception as exc:
582626
             return ToolResult(f"Error resolving directory: {exc}", is_error=True)
583627
 
584628
         if not base_path.exists():
585
-            return ToolResult(f"Directory not found: {path}", is_error=True)
629
+            missing_target = path if path != "." else str(base_path)
630
+            return ToolResult(f"Directory not found: {missing_target}", is_error=True)
586631
 
587632
         try:
588
-            matches = list(base_path.glob(pattern))
633
+            matches = list(base_path.glob(effective_pattern))
589634
             # Sort by modification time (newest first)
590635
             matches.sort(key=lambda p: p.stat().st_mtime, reverse=True)
591636
 
@@ -606,6 +651,8 @@ class GlobTool(Tool):
606651
                 output,
607652
                 metadata={
608653
                     "base_path": str(base_path),
654
+                    "effective_pattern": effective_pattern,
655
+                    "requested_pattern": pattern,
609656
                     "num_files": len(matches),
610657
                     "truncated": truncated if "truncated" in locals() else False,
611658
                 },
tests/test_tools.pymodified
@@ -152,6 +152,24 @@ class TestGlobTool:
152152
         assert not result.is_error
153153
         assert "No files matching" in result.output
154154
 
155
+    @pytest.mark.asyncio
156
+    async def test_glob_expands_home_prefixed_pattern(self, tool, monkeypatch, temp_dir):
157
+        home_dir = temp_dir / "fake-home"
158
+        animals_dir = home_dir / "Loader" / "animals"
159
+        animals_dir.mkdir(parents=True)
160
+        (animals_dir / "penguins.html").write_text("<h1>Penguins</h1>\n")
161
+        (animals_dir / "wolves.html").write_text("<h1>Wolves</h1>\n")
162
+
163
+        monkeypatch.setenv("HOME", str(home_dir))
164
+
165
+        result = await tool.execute(pattern="~/Loader/animals/*.html")
166
+
167
+        assert not result.is_error
168
+        assert "penguins.html" in result.output
169
+        assert "wolves.html" in result.output
170
+        assert result.metadata["base_path"] == str(animals_dir.resolve())
171
+        assert result.metadata["effective_pattern"] == "*.html"
172
+
155173
 
156174
 class TestBashTool:
157175
     """Tests for BashTool."""