tenseleyflow/loader / 28de433

Browse files

Steer blocked HTML link drift

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
28de433e6abc247ac57d05f93c0291212a5989c1
Parents
c6e1cc1
Tree
ba57c22

2 changed files

StatusFile+-
M src/loader/runtime/tool_batches.py 64 0
M tests/test_tool_batches.py 50 0
src/loader/runtime/tool_batches.pymodified
@@ -285,6 +285,10 @@ class ToolBatchRunner:
285285
                     outcome.event_content,
286286
                     dod=dod,
287287
                 )
288
+                self._queue_blocked_html_declared_target_nudge(
289
+                    tool_call,
290
+                    outcome.event_content,
291
+                )
288292
                 self._queue_blocked_active_repair_nudge(outcome.event_content)
289293
                 self._queue_blocked_active_repair_mutation_nudge(outcome.event_content)
290294
                 self._queue_blocked_completed_artifact_scope_nudge(
@@ -685,6 +689,56 @@ class ToolBatchRunner:
685689
             "Do not reopen unrelated reference materials while this concrete repair target is unresolved."
686690
         )
687691
 
692
+    def _queue_blocked_html_declared_target_nudge(
693
+        self,
694
+        tool_call: ToolCall,
695
+        event_content: str,
696
+    ) -> None:
697
+        """Steer blocked HTML graph edits back to the root-declared local targets."""
698
+
699
+        if tool_call.name not in {"write", "edit", "patch"}:
700
+            return
701
+        if "HTML page introduces new local targets outside the current declared artifact set" not in event_content:
702
+            return
703
+
704
+        target = str(
705
+            tool_call.arguments.get("file_path")
706
+            or tool_call.arguments.get("path")
707
+            or ""
708
+        ).strip()
709
+        if not target:
710
+            return
711
+
712
+        closest_targets = _extract_blocked_html_target_list(
713
+            event_content,
714
+            "Closest declared local targets include:",
715
+        )
716
+        declared_targets = _extract_blocked_html_target_list(
717
+            event_content,
718
+            "Already-declared local targets include:",
719
+        )
720
+
721
+        guidance = (
722
+            "That HTML mutation introduced sibling targets outside the current declared local-link set. "
723
+            f"Stay on `{target}`."
724
+        )
725
+        if closest_targets:
726
+            guidance += (
727
+                " Replace the invented hrefs with the closest declared target(s): "
728
+                + ", ".join(f"`{candidate}`" for candidate in closest_targets[:3])
729
+                + "."
730
+            )
731
+        elif declared_targets:
732
+            guidance += (
733
+                " Keep local links within the declared target set, for example: "
734
+                + ", ".join(f"`{candidate}`" for candidate in declared_targets[:3])
735
+                + "."
736
+            )
737
+        guidance += (
738
+            " Resend one concrete mutation for that same file now instead of rereading the reference guide."
739
+        )
740
+        self.context.queue_steering_message(guidance)
741
+
688742
     def _queue_blocked_invalid_mutation_nudge(
689743
         self,
690744
         tool_call: ToolCall,
@@ -1587,6 +1641,16 @@ def _invalid_mutation_call_shape(tool_name: str) -> str:
15871641
     return f"`{tool_name}(...)`"
15881642
 
15891643
 
1644
+def _extract_blocked_html_target_list(event_content: str, marker: str) -> list[str]:
1645
+    if marker not in event_content:
1646
+        return []
1647
+    tail = event_content.split(marker, 1)[1].strip()
1648
+    target_text = tail.split(". ", 1)[0].strip()
1649
+    if not target_text:
1650
+        return []
1651
+    return [item.strip() for item in target_text.split(",") if item.strip()]
1652
+
1653
+
15901654
 def _resume_suffix_for_target(
15911655
     target: Path,
15921656
     *,
tests/test_tool_batches.pymodified
@@ -4602,6 +4602,56 @@ def test_tool_batch_runner_blocked_completed_artifact_scope_nudge_prefers_verifi
46024602
     assert "Do not reopen earlier reference materials." in queued[0]
46034603
 
46044604
 
4605
+def test_tool_batch_runner_blocked_html_declared_target_nudge_uses_closest_declared_target(
4606
+    temp_dir: Path,
4607
+) -> None:
4608
+    async def assess_confidence(
4609
+        tool_name: str,
4610
+        tool_args: dict,
4611
+        context: str,
4612
+    ) -> ConfidenceAssessment:
4613
+        raise AssertionError("Confidence scoring should be disabled in this scenario")
4614
+
4615
+    async def verify_action(
4616
+        tool_name: str,
4617
+        tool_args: dict,
4618
+        result: str,
4619
+        expected: str = "",
4620
+    ) -> ActionVerification:
4621
+        raise AssertionError("Verification should not run in this scenario")
4622
+
4623
+    context = build_context(
4624
+        temp_dir=temp_dir,
4625
+        messages=[],
4626
+        safeguards=FakeSafeguards(),
4627
+        assess_confidence=assess_confidence,
4628
+        verify_action=verify_action,
4629
+    )
4630
+    queued: list[str] = []
4631
+    context.queue_steering_message_callback = queued.append
4632
+    runner = ToolBatchRunner(context, DefinitionOfDoneStore(temp_dir))
4633
+
4634
+    runner._queue_blocked_html_declared_target_nudge(
4635
+        ToolCall(
4636
+            id="write-ch1",
4637
+            name="write",
4638
+            arguments={"file_path": str(temp_dir / "guide" / "chapters" / "01-introduction.html")},
4639
+        ),
4640
+        (
4641
+            "[Blocked - HTML page introduces new local targets outside the current declared artifact set] "
4642
+            "Suggestion: Keep non-root HTML pages within the root-declared local-link set and avoid "
4643
+            "introducing new sibling targets that the guide root does not declare, for example fix: 02-setup.html. "
4644
+            "Already-declared local targets include: chapters/01-introduction.html, chapters/02-installation.html, "
4645
+            "chapters/03-configuration.html. Closest declared local targets include: chapters/02-installation.html"
4646
+        ),
4647
+    )
4648
+
4649
+    assert queued
4650
+    assert str(temp_dir / "guide" / "chapters" / "01-introduction.html") in queued[0]
4651
+    assert "`chapters/02-installation.html`" in queued[0]
4652
+    assert "same file now" in queued[0]
4653
+
4654
+
46054655
 @pytest.mark.asyncio
46064656
 async def test_tool_batch_runner_blocked_empty_file_path_nudges_concrete_next_artifact(
46074657
     temp_dir: Path,