tenseleyflow/loader / 670da9a

Browse files

Name missing repair targets

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
670da9a62bbfba816acfc5aa8d51611261dc4ef3
Parents
d328e1c
Tree
e2ac357

2 changed files

StatusFile+-
M src/loader/runtime/hooks.py 20 1
M tests/test_permissions.py 69 0
src/loader/runtime/hooks.pymodified
@@ -654,6 +654,19 @@ def _repair_uses_artifact_set_as_source_of_truth(repair: Any) -> bool:
654654
     )
655655
 
656656
 
657
+def _next_missing_repair_target(repair: Any) -> str:
658
+    for raw_path in getattr(repair, "allowed_paths", ()) or ():
659
+        path_text = str(raw_path or "").strip()
660
+        if not path_text:
661
+            continue
662
+        try:
663
+            if not Path(path_text).exists():
664
+                return path_text
665
+        except (OSError, RuntimeError, ValueError):
666
+            continue
667
+    return ""
668
+
669
+
657670
 class ActiveRepairScopeHook(BaseToolHook):
658671
     """Keep fix-mode observations anchored to the active artifact set."""
659672
 
@@ -701,13 +714,19 @@ class ActiveRepairScopeHook(BaseToolHook):
701714
                 self._source_of_truth_observation_count
702715
                 >= self._MAX_SOURCE_OF_TRUTH_OBSERVATIONS
703716
             ):
717
+                next_missing_target = _next_missing_repair_target(repair)
718
+                missing_target_suffix = (
719
+                    f" or create `{next_missing_target}`"
720
+                    if next_missing_target
721
+                    else " or create the next missing repair target"
722
+                )
704723
                 return HookResult(
705724
                     decision=HookDecision.DENY,
706725
                     message=(
707726
                         "[Blocked - repair audit loop: the active repair artifact set has "
708727
                         "already been inspected several times without a concrete mutation.] "
709728
                         f"Suggestion: make one concrete edit, patch, or write to "
710
-                        f"`{repair.artifact_path}` or create the next missing repair target "
729
+                        f"`{repair.artifact_path}`{missing_target_suffix} "
711730
                         "instead of more rereads."
712731
                     ),
713732
                     terminal_state="blocked",
tests/test_permissions.pymodified
@@ -978,6 +978,75 @@ async def test_active_repair_scope_hook_blocks_repair_audit_loop_after_repeated_
978978
     assert "repair audit loop" in blocked.message
979979
 
980980
 
981
+@pytest.mark.asyncio
982
+async def test_active_repair_scope_audit_loop_names_next_missing_repair_target(
983
+    temp_dir: Path,
984
+) -> None:
985
+    registry = create_default_registry(temp_dir)
986
+    policy = build_permission_policy(
987
+        active_mode=PermissionMode.WORKSPACE_WRITE,
988
+        workspace_root=temp_dir,
989
+        tool_requirements=registry.get_tool_requirements(),
990
+    )
991
+    dod_store = DefinitionOfDoneStore(temp_dir)
992
+    dod = create_definition_of_done("Repair the active artifact set")
993
+    dod.status = "fixing"
994
+    dod_path = dod_store.save(dod)
995
+    guide_root = temp_dir / "guide"
996
+    chapter_dir = guide_root / "chapters"
997
+    chapter_dir.mkdir(parents=True, exist_ok=True)
998
+    repair_target = chapter_dir / "04-reverse-proxy.html"
999
+    next_missing = chapter_dir / "05-load-balancing.html"
1000
+    repair_target.write_text("<h1>Reverse Proxy</h1>\n")
1001
+    session = FakeSession(
1002
+        active_dod_path=str(dod_path),
1003
+        messages=[
1004
+            Message(
1005
+                role=Role.ASSISTANT,
1006
+                content=(
1007
+                    "Repair focus:\n"
1008
+                    f"- Fix the broken local reference `05-load-balancing.html` in `{repair_target}`.\n"
1009
+                    f"- Immediate next step: edit `{repair_target}`.\n"
1010
+                    f"- If the broken reference should remain, create `{next_missing}`; otherwise remove or replace `05-load-balancing.html`.\n"
1011
+                    "- Use the existing artifact files as the source of truth while repairing this file: "
1012
+                    f"`{repair_target}`, `{next_missing}`.\n"
1013
+                ),
1014
+            )
1015
+        ],
1016
+    )
1017
+    hook = ActiveRepairScopeHook(
1018
+        dod_store=dod_store,
1019
+        project_root=temp_dir,
1020
+        session=session,
1021
+    )
1022
+
1023
+    def make_context(index: int) -> HookContext:
1024
+        return HookContext(
1025
+            tool_call=ToolCall(
1026
+                id=f"read-{index}",
1027
+                name="read",
1028
+                arguments={"file_path": str(repair_target)},
1029
+            ),
1030
+            tool=registry.get("read"),
1031
+            registry=registry,
1032
+            permission_policy=policy,
1033
+            source="native",
1034
+        )
1035
+
1036
+    for index in range(1, 5):
1037
+        context = make_context(index)
1038
+        result = await hook.pre_tool_use(context)
1039
+        assert result.decision == HookDecision.CONTINUE
1040
+        await hook.post_tool_use(context)
1041
+
1042
+    blocked = await hook.pre_tool_use(make_context(5))
1043
+
1044
+    assert blocked.decision == HookDecision.DENY
1045
+    assert blocked.message is not None
1046
+    assert "repair audit loop" in blocked.message
1047
+    assert str(next_missing) in blocked.message
1048
+
1049
+
9811050
 @pytest.mark.asyncio
9821051
 async def test_active_repair_scope_hook_allows_scoped_glob_within_active_artifact_roots(
9831052
     temp_dir: Path,