tenseleyflow/loader / cce89ea

Browse files

Block stale repair memory reads

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
cce89ea9ab7aaa303b8d5ac906bb75fdc9bbe084
Parents
9eed110
Tree
56e87db

2 changed files

StatusFile+-
M src/loader/runtime/hooks.py 19 1
M tests/test_permissions.py 58 0
src/loader/runtime/hooks.pymodified
@@ -451,7 +451,8 @@ class RelativePathContextHook(BaseToolHook):
451451
         return str(candidate.parent)
452452
 
453453
 
454
-_OBSERVATION_TOOLS = frozenset({"read", "glob", "grep", "bash"})
454
+_DURABLE_MEMORY_READ_TOOLS = frozenset({"notepad_read", "project_memory_read"})
455
+_OBSERVATION_TOOLS = frozenset({"read", "glob", "grep", "bash"}) | _DURABLE_MEMORY_READ_TOOLS
455456
 _MUTATION_TOOLS = frozenset({"write", "edit", "patch", "bash"})
456457
 _READ_ONLY_BASH_PREFIXES = frozenset(
457458
     {"ls", "pwd", "find", "stat", "cat", "head", "tail", "rg", "grep"}
@@ -729,6 +730,23 @@ class ActiveRepairScopeHook(BaseToolHook):
729730
         if repair is None:
730731
             return HookResult()
731732
 
733
+        if context.tool_call.name in _DURABLE_MEMORY_READ_TOOLS:
734
+            allowed_preview = ", ".join(f"`{path}`" for path in repair.allowed_paths[:3])
735
+            if len(repair.allowed_paths) > 3:
736
+                allowed_preview += ", ..."
737
+            target_preview = allowed_preview or f"`{repair.artifact_path}`"
738
+            return HookResult(
739
+                decision=HookDecision.DENY,
740
+                message=(
741
+                    "[Blocked - active repair scope: verification already identified "
742
+                    "concrete repair targets, and durable memory may be stale.] "
743
+                    "Suggestion: trust the active verifier/DoD over notepad or project "
744
+                    f"memory while repairing. Continue with {target_preview}; do not use "
745
+                    "durable notes as completion evidence until verification passes."
746
+                ),
747
+                terminal_state="blocked",
748
+            )
749
+
732750
         observed_paths = _extract_observation_paths(context.tool_call)
733751
         if not observed_paths:
734752
             return HookResult()
tests/test_permissions.pymodified
@@ -678,6 +678,64 @@ async def test_active_repair_scope_hook_blocks_reference_reads_while_fixing(
678678
     assert str(repair_target) in result.message
679679
 
680680
 
681
+@pytest.mark.asyncio
682
+async def test_active_repair_scope_hook_blocks_stale_memory_reads_while_fixing(
683
+    temp_dir: Path,
684
+) -> None:
685
+    registry = create_default_registry(temp_dir)
686
+    policy = build_permission_policy(
687
+        active_mode=PermissionMode.WORKSPACE_WRITE,
688
+        workspace_root=temp_dir,
689
+        tool_requirements=registry.get_tool_requirements(),
690
+    )
691
+    dod_store = DefinitionOfDoneStore(temp_dir)
692
+    dod = create_definition_of_done("Repair the active artifact set")
693
+    dod.status = "fixing"
694
+    dod_path = dod_store.save(dod)
695
+    repair_target = temp_dir / "guide" / "chapters" / "05-load-balancing.html"
696
+    session = FakeSession(
697
+        active_dod_path=str(dod_path),
698
+        messages=[
699
+            Message(
700
+                role=Role.USER,
701
+                content=(
702
+                    "[DEFINITION OF DONE CHECK STILL FAILING]\n"
703
+                    "HTML guide content quality issues:\n"
704
+                    "Repair focus:\n"
705
+                    f"- {repair_target}: thin content (1500 text chars, expected at least 1758)\n"
706
+                    f"- Immediate next step: edit `{repair_target}`.\n"
707
+                ),
708
+            )
709
+        ],
710
+    )
711
+    hook = ActiveRepairScopeHook(
712
+        dod_store=dod_store,
713
+        project_root=temp_dir,
714
+        session=session,
715
+    )
716
+
717
+    result = await hook.pre_tool_use(
718
+        HookContext(
719
+            tool_call=ToolCall(
720
+                id="memory-1",
721
+                name="notepad_read",
722
+                arguments={},
723
+            ),
724
+            tool=registry.get("notepad_read"),
725
+            registry=registry,
726
+            permission_policy=policy,
727
+            source="native",
728
+        )
729
+    )
730
+
731
+    assert result.decision == HookDecision.DENY
732
+    assert result.terminal_state == "blocked"
733
+    assert result.message is not None
734
+    assert "durable memory may be stale" in result.message
735
+    assert "trust the active verifier/DoD" in result.message
736
+    assert str(repair_target) in result.message
737
+
738
+
681739
 @pytest.mark.asyncio
682740
 async def test_active_repair_scope_hook_allows_reads_inside_active_artifact_set(
683741
     temp_dir: Path,