tenseleyflow/loader / 39c0fab

Browse files

Anchor workspace mirror paths

Authored by espadonne
SHA
39c0fab3d89a9ba5f52b2ce6c3d9628786961b9d
Parents
07b7555
Tree
baa5aaf

2 changed files

StatusFile+-
M src/loader/runtime/hooks.py 32 1
M tests/test_permissions.py 9 0
src/loader/runtime/hooks.pymodified
@@ -259,11 +259,16 @@ class RelativePathContextHook(BaseToolHook):
259259
 
260260
         require_existing = context.tool_call.name in {"read", "glob", "grep", "edit", "patch"}
261261
         resolved: str | None = None
262
+        injected_messages: list[str] = []
262263
         if raw_path.startswith("/"):
263264
             resolved = self._resolve_workspace_mirror_path(
264265
                 raw_path,
265266
                 require_existing=require_existing,
266267
             )
268
+            if resolved is not None:
269
+                injected_messages.append(
270
+                    self._workspace_mirror_correction_message(raw_path, resolved)
271
+                )
267272
         elif not raw_path.startswith("~"):
268273
             resolved = self._resolve_recent_context_path(
269274
                 raw_path,
@@ -274,7 +279,10 @@ class RelativePathContextHook(BaseToolHook):
274279
 
275280
         updated_arguments = dict(arguments)
276281
         updated_arguments[argument_key] = resolved
277
-        return HookResult(updated_arguments=updated_arguments)
282
+        return HookResult(
283
+            updated_arguments=updated_arguments,
284
+            injected_messages=injected_messages,
285
+        )
278286
 
279287
     def _argument_key(self, tool_name: str) -> str | None:
280288
         if tool_name in self._FILE_TOOLS:
@@ -356,6 +364,29 @@ class RelativePathContextHook(BaseToolHook):
356364
                 return str(remapped)
357365
         return None
358366
 
367
+    def _workspace_mirror_correction_message(self, raw_path: str, resolved_path: str) -> str:
368
+        raw_name = Path(str(raw_path)).name or str(raw_path)
369
+        resolved_root = self._describe_anchor_root(resolved_path)
370
+        return (
371
+            "[Path anchor correction] A repo-local mirror path was remapped to the established "
372
+            f"output root under `{resolved_root}`. Keep future file/search tool calls on that "
373
+            f"external root and use `{raw_name}` there instead of re-anchoring work to the "
374
+            "workspace checkout."
375
+        )
376
+
377
+    def _describe_anchor_root(self, path_value: str) -> str:
378
+        resolved = Path(path_value).expanduser()
379
+        try:
380
+            candidate = resolved.resolve(strict=False)
381
+        except Exception:
382
+            candidate = resolved
383
+
384
+        parts = candidate.parts
385
+        if "Loader" in parts:
386
+            loader_index = parts.index("Loader")
387
+            return str(Path(*parts[: loader_index + 1]))
388
+        return str(candidate.parent)
389
+
359390
 
360391
 _OBSERVATION_TOOLS = frozenset({"read", "glob", "grep", "bash"})
361392
 _MUTATION_TOOLS = frozenset({"write", "edit", "patch", "bash"})
tests/test_permissions.pymodified
@@ -528,6 +528,15 @@ async def test_relative_path_context_hook_remaps_workspace_mirror_of_external_ro
528528
 
529529
     assert result.updated_arguments is not None
530530
     assert Path(result.updated_arguments["file_path"]).resolve() == expected_external_path.resolve()
531
+    resolved_loader_root = (external_root / "Loader").resolve()
532
+    assert result.injected_messages == [
533
+        (
534
+            "[Path anchor correction] A repo-local mirror path was remapped to the "
535
+            f"established output root under `{resolved_loader_root}`. Keep future "
536
+            "file/search tool calls on that external root and use `index.html` there "
537
+            "instead of re-anchoring work to the workspace checkout."
538
+        )
539
+    ]
531540
 
532541
 
533542
 class FakeSession: