tenseleyflow/loader / 37acacf

Browse files

Prefer verification after full build

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
37acacf2230e2fd35289c3b4815b9e2c18a0d3f7
Parents
b3febff
Tree
10ba7e1

4 changed files

StatusFile+-
M src/loader/runtime/dod.py 25 5
M src/loader/runtime/tool_batches.py 80 15
M tests/test_dod.py 2 0
M tests/test_tool_batches.py 318 5
src/loader/runtime/dod.pymodified
@@ -746,6 +746,30 @@ def all_planned_artifacts_exist(
746746
     *,
747747
     project_root: Path,
748748
     max_paths: int | None = None,
749
+) -> bool:
750
+    if not all_planned_artifact_outputs_exist(
751
+        dod,
752
+        project_root=project_root,
753
+        max_paths=max_paths,
754
+    ):
755
+        return False
756
+    targets = collect_planned_artifact_targets(
757
+        dod,
758
+        project_root=project_root,
759
+        max_paths=max_paths,
760
+    )
761
+    return not _planned_html_outputs_have_missing_local_links(
762
+        dod,
763
+        project_root=project_root,
764
+        targets=targets,
765
+    )
766
+
767
+
768
+def all_planned_artifact_outputs_exist(
769
+    dod: DefinitionOfDone,
770
+    *,
771
+    project_root: Path,
772
+    max_paths: int | None = None,
749773
 ) -> bool:
750774
     targets = collect_planned_artifact_targets(
751775
         dod,
@@ -769,11 +793,7 @@ def all_planned_artifacts_exist(
769793
         project_root=project_root,
770794
     ):
771795
         return False
772
-    return not _planned_html_outputs_have_missing_local_links(
773
-        dod,
774
-        project_root=project_root,
775
-        targets=targets,
776
-    )
796
+    return True
777797
 
778798
 
779799
 def planned_artifact_target_satisfied(
src/loader/runtime/tool_batches.pymodified
@@ -14,6 +14,7 @@ from .context import RuntimeContext
1414
 from .dod import (
1515
     DefinitionOfDone,
1616
     DefinitionOfDoneStore,
17
+    all_planned_artifact_outputs_exist,
1718
     all_planned_artifacts_exist,
1819
     begin_new_verification_attempt,
1920
     collect_planned_artifact_targets,
@@ -325,11 +326,17 @@ class ToolBatchRunner:
325326
                 self._queue_blocked_html_declared_file_creation_nudge(
326327
                     tool_call,
327328
                     outcome.event_content,
329
+                    dod=dod,
328330
                 )
329331
                 self._queue_blocked_html_declared_target_nudge(
330332
                     tool_call,
331333
                     outcome.event_content,
332334
                 )
335
+                self._queue_blocked_html_missing_target_nudge(
336
+                    tool_call,
337
+                    outcome.event_content,
338
+                    dod=dod,
339
+                )
333340
                 self._queue_blocked_active_repair_nudge(outcome.event_content)
334341
                 self._queue_blocked_active_repair_mutation_nudge(outcome.event_content)
335342
                 self._queue_blocked_completed_artifact_scope_nudge(
@@ -473,7 +480,7 @@ class ToolBatchRunner:
473480
             )
474481
             return
475482
 
476
-        if all_planned_artifacts_exist(dod, project_root=self.context.project_root):
483
+        if all_planned_artifact_outputs_exist(dod, project_root=self.context.project_root):
477484
             verification_commands = dod.verification_commands or derive_verification_commands(
478485
                 dod,
479486
                 project_root=self.context.project_root,
@@ -487,9 +494,10 @@ class ToolBatchRunner:
487494
             )
488495
             self.context.queue_steering_message(
489496
                 "Reuse the earlier observation instead of repeating it. "
490
-                "All explicitly planned artifacts already exist. "
497
+                "All explicitly planned artifacts already exist on disk. "
491498
                 "Use the current task artifacts as the source of truth and do not reopen "
492499
                 "reference materials unless one specific gap is still unknown. "
500
+                "If anything is still wrong, repair the current files instead of expanding the artifact set. "
493501
                 + verification_suffix
494502
             )
495503
             return
@@ -586,7 +594,7 @@ class ToolBatchRunner:
586594
             return
587595
         if extract_active_repair_context(self.context.session.messages) is not None:
588596
             return
589
-        if not all_planned_artifacts_exist(dod, project_root=self.context.project_root):
597
+        if not all_planned_artifact_outputs_exist(dod, project_root=self.context.project_root):
590598
             return
591599
 
592600
         observed_paths = _extract_observation_paths(tool_call)
@@ -930,6 +938,8 @@ class ToolBatchRunner:
930938
         self,
931939
         tool_call: ToolCall,
932940
         event_content: str,
941
+        *,
942
+        dod: DefinitionOfDone,
933943
     ) -> None:
934944
         """Steer blocked undeclared HTML file creation back through the root guide."""
935945
 
@@ -954,6 +964,26 @@ class ToolBatchRunner:
954964
         except ValueError:
955965
             relative_target = target_path.name
956966
 
967
+        if all_planned_artifact_outputs_exist(dod, project_root=self.context.project_root):
968
+            verification_commands = dod.verification_commands or derive_verification_commands(
969
+                dod,
970
+                project_root=self.context.project_root,
971
+                task_statement=getattr(self.context.session, "current_task", "") or "",
972
+                supplement_existing=True,
973
+            )
974
+            verification_suffix = (
975
+                " Move to verification or final confirmation using the files already on disk."
976
+                if verification_commands
977
+                else " Finish the task using the files already on disk."
978
+            )
979
+            self.context.queue_steering_message(
980
+                "All explicitly planned artifacts already exist on disk. "
981
+                f"Do not expand the output set with `{relative_target}`. "
982
+                "Use the current generated files as the source of truth and repair or verify them instead."
983
+                + verification_suffix
984
+            )
985
+            return
986
+
957987
         guidance = (
958988
             "That new HTML file is outside the current root-declared artifact set. "
959989
             f"Before creating `{relative_target}`, update `{root_index}` so the guide root "
@@ -962,6 +992,40 @@ class ToolBatchRunner:
962992
         )
963993
         self.context.queue_steering_message(guidance)
964994
 
995
+    def _queue_blocked_html_missing_target_nudge(
996
+        self,
997
+        tool_call: ToolCall,
998
+        event_content: str,
999
+        *,
1000
+        dod: DefinitionOfDone,
1001
+    ) -> None:
1002
+        """Turn post-build missing-link expansions into verify/repair handoffs."""
1003
+
1004
+        if tool_call.name not in {"write", "edit", "patch"}:
1005
+            return
1006
+        if "Edited HTML links point to files that do not exist" not in event_content:
1007
+            return
1008
+        if not all_planned_artifact_outputs_exist(dod, project_root=self.context.project_root):
1009
+            return
1010
+
1011
+        verification_commands = dod.verification_commands or derive_verification_commands(
1012
+            dod,
1013
+            project_root=self.context.project_root,
1014
+            task_statement=getattr(self.context.session, "current_task", "") or "",
1015
+            supplement_existing=True,
1016
+        )
1017
+        verification_suffix = (
1018
+            " Move to verification or final confirmation using the files already on disk."
1019
+            if verification_commands
1020
+            else " Finish the task using the files already on disk."
1021
+        )
1022
+        self.context.queue_steering_message(
1023
+            "All explicitly planned artifacts already exist on disk. "
1024
+            "Do not introduce new local-link targets beyond the current output set. "
1025
+            "Repair the existing generated files instead of expanding the guide."
1026
+            + verification_suffix
1027
+        )
1028
+
9651029
     def _queue_blocked_invalid_mutation_nudge(
9661030
         self,
9671031
         tool_call: ToolCall,
@@ -1270,7 +1334,7 @@ class ToolBatchRunner:
12701334
     ) -> None:
12711335
         if not is_state_mutating_tool_call(tool_call):
12721336
             return
1273
-        if not all_planned_artifacts_exist(dod, project_root=self.context.project_root):
1337
+        if not all_planned_artifact_outputs_exist(dod, project_root=self.context.project_root):
12741338
             return
12751339
 
12761340
         next_pending = preferred_pending_todo_item(
@@ -1291,7 +1355,7 @@ class ToolBatchRunner:
12911355
                 else " Avoid another full reread unless one specific inconsistency is still unknown."
12921356
             )
12931357
             self.context.queue_steering_message(
1294
-                "All explicitly planned artifacts now exist. "
1358
+                "All explicitly planned artifacts now exist on disk. "
12951359
                 f"Continue with the next pending item: `{next_pending}`. "
12961360
                 "Use the files already on disk as the source of truth instead of restarting "
12971361
                 "discovery or inventing alternate filenames."
@@ -1301,7 +1365,7 @@ class ToolBatchRunner:
13011365
 
13021366
         if verification_commands:
13031367
             self.context.queue_steering_message(
1304
-                "All explicitly planned artifacts now exist. "
1368
+                "All explicitly planned artifacts now exist on disk. "
13051369
                 "Do not expand the artifact set or restart discovery unless a specific gap is "
13061370
                 "still known. Move to verification or final confirmation using the files that "
13071371
                 "already exist."
@@ -1469,8 +1533,12 @@ class ToolBatchRunner:
14691533
             next_pending=next_pending,
14701534
             project_root=self.context.project_root,
14711535
         )
1536
+        outputs_exist = all_planned_artifact_outputs_exist(
1537
+            dod,
1538
+            project_root=self.context.project_root,
1539
+        )
14721540
         if missing_artifact is None:
1473
-            if next_pending and _todo_is_mutation_step(next_pending):
1541
+            if next_pending and _todo_is_mutation_step(next_pending) and not outputs_exist:
14741542
                 pending_target = infer_pending_todo_output_target(
14751543
                     dod,
14761544
                     next_pending,
@@ -1511,10 +1579,7 @@ class ToolBatchRunner:
15111579
             if (
15121580
                 next_pending
15131581
                 and _todo_is_consistency_review_step(next_pending)
1514
-                and not all_planned_artifacts_exist(
1515
-                    dod,
1516
-                    project_root=self.context.project_root,
1517
-                )
1582
+                and not outputs_exist
15181583
             ):
15191584
                 self.context.queue_ephemeral_steering_message(
15201585
                     "Todo tracking is updated. Continue with the next pending item: "
@@ -1524,7 +1589,7 @@ class ToolBatchRunner:
15241589
                 )
15251590
                 return
15261591
 
1527
-            if not all_planned_artifacts_exist(dod, project_root=self.context.project_root):
1592
+            if not outputs_exist:
15281593
                 return
15291594
 
15301595
             verification_commands = dod.verification_commands or derive_verification_commands(
@@ -1540,7 +1605,7 @@ class ToolBatchRunner:
15401605
                     else " Finish the targeted consistency pass without reopening reference materials."
15411606
                 )
15421607
                 self.context.queue_ephemeral_steering_message(
1543
-                    "Todo tracking is updated. All explicitly planned artifacts now exist. "
1608
+                    "Todo tracking is updated. All explicitly planned artifacts now exist on disk. "
15441609
                     f"Continue with the next pending item: `{next_pending}`. "
15451610
                     "Use the current output files as the source of truth, and do not restart "
15461611
                     "early discovery or reopen reference materials."
@@ -1554,9 +1619,9 @@ class ToolBatchRunner:
15541619
                 else " Finish the task using the files already on disk."
15551620
             )
15561621
             self.context.queue_ephemeral_steering_message(
1557
-                "Todo tracking is updated. All explicitly planned artifacts now exist. "
1622
+                "Todo tracking is updated. All explicitly planned artifacts now exist on disk. "
15581623
                 "Do not restart discovery, reopen reference materials, or spend another turn "
1559
-                "on TodoWrite alone."
1624
+                "on TodoWrite alone. Repair or verify the current files instead of expanding the artifact set."
15601625
                 + verification_suffix
15611626
             )
15621627
             return
tests/test_dod.pymodified
@@ -6,6 +6,7 @@ from loader.llm.base import ToolCall
66
 from loader.runtime.dod import (
77
     DefinitionOfDoneStore,
88
     VerificationEvidence,
9
+    all_planned_artifact_outputs_exist,
910
     all_planned_artifacts_exist,
1011
     begin_new_verification_attempt,
1112
     build_verification_summary,
@@ -570,6 +571,7 @@ def test_all_planned_artifacts_exist_stays_false_while_touched_html_links_missin
570571
     dod.completed_items = ["Create chapter files with appropriate content"]
571572
 
572573
     assert all_planned_artifacts_exist(dod, project_root=tmp_path) is False
574
+    assert all_planned_artifact_outputs_exist(dod, project_root=tmp_path) is True
573575
 
574576
     (chapters / "02-setup.html").write_text("<h1>Setup</h1>\n")
575577
 
tests/test_tool_batches.pymodified
@@ -1830,7 +1830,7 @@ async def test_tool_batch_runner_duplicate_read_after_plan_complete_pushes_verif
18301830
     )
18311831
 
18321832
     assert len(persistent_messages) == 1
1833
-    assert "All explicitly planned artifacts already exist." in persistent_messages[0]
1833
+    assert "All explicitly planned artifacts already exist on disk." in persistent_messages[0]
18341834
     assert (
18351835
         "Move to verification or final confirmation using the files already on disk."
18361836
         in persistent_messages[0]
@@ -1951,7 +1951,7 @@ async def test_tool_batch_runner_duplicate_read_after_plan_complete_ignores_stal
19511951
     )
19521952
 
19531953
     assert len(persistent_messages) == 1
1954
-    assert "All explicitly planned artifacts already exist." in persistent_messages[0]
1954
+    assert "All explicitly planned artifacts already exist on disk." in persistent_messages[0]
19551955
     assert (
19561956
         "Move to verification or final confirmation using the files already on disk."
19571957
         in persistent_messages[0]
@@ -3148,7 +3148,7 @@ async def test_tool_batch_runner_hands_off_to_verification_once_planned_artifact
31483148
     )
31493149
 
31503150
     assert any(
3151
-        "All explicitly planned artifacts now exist." in message
3151
+        "All explicitly planned artifacts now exist on disk." in message
31523152
         for message in persistent_messages
31533153
     )
31543154
     assert any(
@@ -3395,7 +3395,7 @@ async def test_tool_batch_runner_large_plan_does_not_claim_completion_early(
33953395
         for message in ephemeral_messages
33963396
     )
33973397
     assert not any(
3398
-        "All explicitly planned artifacts now exist." in message
3398
+        "All explicitly planned artifacts now exist on disk." in message
33993399
         for message in ephemeral_messages
34003400
     )
34013401
 
@@ -3802,13 +3802,149 @@ async def test_tool_batch_runner_todowrite_after_artifacts_exist_pushes_verifica
38023802
 
38033803
     assert queued_messages
38043804
     message = queued_messages[-1]
3805
-    assert "Todo tracking is updated. All explicitly planned artifacts now exist." in message
3805
+    assert "Todo tracking is updated. All explicitly planned artifacts now exist on disk." in message
38063806
     assert "Verify all guide files are linked and complete" in message
38073807
     assert "Move to verification once no specific mismatch remains." in message
38083808
     assert "reopen reference materials" in message
38093809
     assert "Fortran guide structure" not in message
38103810
 
38113811
 
3812
+@pytest.mark.asyncio
3813
+async def test_tool_batch_runner_todowrite_after_outputs_exist_but_links_missing_still_handoffs_to_verify(
3814
+    temp_dir: Path,
3815
+) -> None:
3816
+    async def assess_confidence(
3817
+        tool_name: str,
3818
+        tool_args: dict,
3819
+        context: str,
3820
+    ) -> ConfidenceAssessment:
3821
+        raise AssertionError("Confidence scoring should not run for this scenario")
3822
+
3823
+    async def verify_action(
3824
+        tool_name: str,
3825
+        tool_args: dict,
3826
+        result: str,
3827
+        expected: str = "",
3828
+    ) -> ActionVerification:
3829
+        raise AssertionError("Verification should not run for this scenario")
3830
+
3831
+    guide_root = temp_dir / "guides" / "nginx"
3832
+    chapters = guide_root / "chapters"
3833
+    guide_root.mkdir(parents=True)
3834
+    chapters.mkdir()
3835
+    index_path = guide_root / "index.html"
3836
+    chapter_one = chapters / "01-introduction.html"
3837
+    chapter_two = chapters / "02-installation.html"
3838
+    index_path.write_text(
3839
+        "\n".join(
3840
+            [
3841
+                '<a href="chapters/01-introduction.html">Intro</a>',
3842
+                '<a href="chapters/02-installation.html">Install</a>',
3843
+                '<a href="../index.html">Back</a>',
3844
+                "",
3845
+            ]
3846
+        )
3847
+    )
3848
+    chapter_one.write_text("<html></html>\n")
3849
+    chapter_two.write_text("<html></html>\n")
3850
+
3851
+    implementation_plan = temp_dir / "implementation.md"
3852
+    implementation_plan.write_text(
3853
+        "\n".join(
3854
+            [
3855
+                "# Implementation Plan",
3856
+                "",
3857
+                "## File Changes",
3858
+                f"- `{guide_root}/`",
3859
+                f"- `{chapters}/`",
3860
+                f"- `{index_path}`",
3861
+                f"- `{chapter_one}`",
3862
+                f"- `{chapter_two}`",
3863
+                "",
3864
+            ]
3865
+        )
3866
+    )
3867
+
3868
+    context = build_context(
3869
+        temp_dir=temp_dir,
3870
+        messages=[],
3871
+        safeguards=FakeSafeguards(),
3872
+        assess_confidence=assess_confidence,
3873
+        verify_action=verify_action,
3874
+        auto_recover=False,
3875
+    )
3876
+    queued_messages: list[str] = []
3877
+    context.queue_steering_message_callback = queued_messages.append
3878
+    runner = ToolBatchRunner(context, DefinitionOfDoneStore(temp_dir))
3879
+    dod = create_definition_of_done("Create a multi-file nginx guide.")
3880
+    dod.implementation_plan = str(implementation_plan)
3881
+    dod.verification_commands = [f"ls -la {guide_root}"]
3882
+    sync_todos_to_definition_of_done(
3883
+        dod,
3884
+        [
3885
+            {
3886
+                "content": "Create chapter files following the established pattern",
3887
+                "active_form": "Creating chapter files",
3888
+                "status": "in_progress",
3889
+            }
3890
+        ],
3891
+        project_root=temp_dir,
3892
+    )
3893
+
3894
+    tool_call = ToolCall(
3895
+        id="todo-post-build",
3896
+        name="TodoWrite",
3897
+        arguments={
3898
+            "todos": [
3899
+                {
3900
+                    "content": "Create chapter files following the established pattern",
3901
+                    "active_form": "Creating chapter files",
3902
+                    "status": "in_progress",
3903
+                }
3904
+            ]
3905
+        },
3906
+    )
3907
+    executor = FakeExecutor(
3908
+        [
3909
+            tool_outcome(
3910
+                tool_call=tool_call,
3911
+                output="Todos updated",
3912
+                is_error=False,
3913
+                metadata={
3914
+                    "new_todos": [
3915
+                        {
3916
+                            "content": "Create chapter files following the established pattern",
3917
+                            "active_form": "Creating chapter files",
3918
+                            "status": "in_progress",
3919
+                        }
3920
+                    ]
3921
+                },
3922
+            )
3923
+        ]
3924
+    )
3925
+
3926
+    summary = TurnSummary(final_response="")
3927
+    await runner.execute_batch(
3928
+        tool_calls=[tool_call],
3929
+        tool_source="assistant",
3930
+        pending_tool_calls_seen=set(),
3931
+        emit=_noop_emit,
3932
+        summary=summary,
3933
+        dod=dod,
3934
+        executor=executor,  # type: ignore[arg-type]
3935
+        on_confirmation=None,
3936
+        on_user_question=None,
3937
+        emit_confirmation=None,
3938
+        consecutive_errors=0,
3939
+    )
3940
+
3941
+    assert queued_messages
3942
+    message = queued_messages[-1]
3943
+    assert "Todo tracking is updated. All explicitly planned artifacts now exist on disk." in message
3944
+    assert "Repair or verify the current files instead of expanding the artifact set." in message
3945
+    assert "Move to verification or final confirmation using the files already on disk." in message
3946
+
3947
+
38123948
 @pytest.mark.asyncio
38133949
 async def test_tool_batch_runner_todowrite_with_existing_output_roots_requeues_next_mutation(
38143950
     temp_dir: Path,
@@ -5849,6 +5985,7 @@ def test_tool_batch_runner_blocked_html_declared_file_creation_nudge_points_to_r
58495985
     queued: list[str] = []
58505986
     context.queue_steering_message_callback = queued.append
58515987
     runner = ToolBatchRunner(context, DefinitionOfDoneStore(temp_dir))
5988
+    dod = create_definition_of_done("Create a guide.")
58525989
 
58535990
     target = temp_dir / "guide" / "chapters" / "troubleshooting.html"
58545991
     runner._queue_blocked_html_declared_file_creation_nudge(
@@ -5865,6 +6002,7 @@ def test_tool_batch_runner_blocked_html_declared_file_creation_nudge_points_to_r
58656002
             "Already-declared local targets include: chapters/advanced-topics.html, "
58666003
             "chapters/basic-usage.html, chapters/configuration.html"
58676004
         ),
6005
+        dod=dod,
58686006
     )
58696007
 
58706008
     assert queued
@@ -5874,6 +6012,181 @@ def test_tool_batch_runner_blocked_html_declared_file_creation_nudge_points_to_r
58746012
     assert "retry the file creation" in queued[0]
58756013
 
58766014
 
6015
+def test_tool_batch_runner_blocked_html_declared_file_creation_after_outputs_exist_prefers_verify(
6016
+    temp_dir: Path,
6017
+) -> None:
6018
+    async def assess_confidence(
6019
+        tool_name: str,
6020
+        tool_args: dict,
6021
+        context: str,
6022
+    ) -> ConfidenceAssessment:
6023
+        raise AssertionError("Confidence scoring should not run in this scenario")
6024
+
6025
+    async def verify_action(
6026
+        tool_name: str,
6027
+        tool_args: dict,
6028
+        result: str,
6029
+        expected: str = "",
6030
+    ) -> ActionVerification:
6031
+        raise AssertionError("Verification should not run in this scenario")
6032
+
6033
+    guide = temp_dir / "guide"
6034
+    chapters = guide / "chapters"
6035
+    guide.mkdir()
6036
+    chapters.mkdir()
6037
+    index = guide / "index.html"
6038
+    index.write_text(
6039
+        "\n".join(
6040
+            [
6041
+                '<a href="chapters/01-introduction.html">Intro</a>',
6042
+                '<a href="chapters/02-installation.html">Install</a>',
6043
+                '<a href="../index.html">Back</a>',
6044
+                "",
6045
+            ]
6046
+        )
6047
+    )
6048
+    (chapters / "01-introduction.html").write_text("<html></html>\n")
6049
+    (chapters / "02-installation.html").write_text("<html></html>\n")
6050
+
6051
+    implementation_plan = temp_dir / "implementation.md"
6052
+    implementation_plan.write_text(
6053
+        "\n".join(
6054
+            [
6055
+                "# Implementation Plan",
6056
+                "",
6057
+                "## File Changes",
6058
+                f"- `{index}`",
6059
+                f"- `{chapters / '01-introduction.html'}`",
6060
+                f"- `{chapters / '02-installation.html'}`",
6061
+                "",
6062
+            ]
6063
+        )
6064
+    )
6065
+
6066
+    context = build_context(
6067
+        temp_dir=temp_dir,
6068
+        messages=[],
6069
+        safeguards=FakeSafeguards(),
6070
+        assess_confidence=assess_confidence,
6071
+        verify_action=verify_action,
6072
+    )
6073
+    queued: list[str] = []
6074
+    context.queue_steering_message_callback = queued.append
6075
+    runner = ToolBatchRunner(context, DefinitionOfDoneStore(temp_dir))
6076
+    dod = create_definition_of_done("Create a guide.")
6077
+    dod.implementation_plan = str(implementation_plan)
6078
+    dod.verification_commands = [f"ls -la {guide}"]
6079
+    dod.touched_files = [str(index), str(chapters / "01-introduction.html"), str(chapters / "02-installation.html")]
6080
+
6081
+    target = guide / "chapters" / "08-advanced-configuration.html"
6082
+    runner._queue_blocked_html_declared_file_creation_nudge(
6083
+        ToolCall(
6084
+            id="write-extra",
6085
+            name="write",
6086
+            arguments={"file_path": str(target)},
6087
+        ),
6088
+        (
6089
+            "[Blocked - HTML file creation falls outside the current declared artifact set] "
6090
+            "Suggestion: Keep new non-root HTML files within the root-declared artifact set and "
6091
+            f"update the guide root `{index.resolve(strict=False)}` before creating undeclared sibling pages, "
6092
+            "for example: chapters/08-advanced-configuration.html."
6093
+        ),
6094
+        dod=dod,
6095
+    )
6096
+
6097
+    assert queued
6098
+    assert "All explicitly planned artifacts already exist on disk." in queued[0]
6099
+    assert "Do not expand the output set with `chapters/08-advanced-configuration.html`." in queued[0]
6100
+    assert "Move to verification or final confirmation using the files already on disk." in queued[0]
6101
+    assert "update the guide root" not in queued[0]
6102
+
6103
+
6104
+def test_tool_batch_runner_blocked_html_missing_target_after_outputs_exist_prefers_verify(
6105
+    temp_dir: Path,
6106
+) -> None:
6107
+    async def assess_confidence(
6108
+        tool_name: str,
6109
+        tool_args: dict,
6110
+        context: str,
6111
+    ) -> ConfidenceAssessment:
6112
+        raise AssertionError("Confidence scoring should not run in this scenario")
6113
+
6114
+    async def verify_action(
6115
+        tool_name: str,
6116
+        tool_args: dict,
6117
+        result: str,
6118
+        expected: str = "",
6119
+    ) -> ActionVerification:
6120
+        raise AssertionError("Verification should not run in this scenario")
6121
+
6122
+    guide = temp_dir / "guide"
6123
+    chapters = guide / "chapters"
6124
+    guide.mkdir()
6125
+    chapters.mkdir()
6126
+    index = guide / "index.html"
6127
+    index.write_text(
6128
+        "\n".join(
6129
+            [
6130
+                '<a href="chapters/01-introduction.html">Intro</a>',
6131
+                '<a href="chapters/02-installation.html">Install</a>',
6132
+                '<a href="../index.html">Back</a>',
6133
+                "",
6134
+            ]
6135
+        )
6136
+    )
6137
+    (chapters / "01-introduction.html").write_text("<html></html>\n")
6138
+    (chapters / "02-installation.html").write_text("<html></html>\n")
6139
+
6140
+    implementation_plan = temp_dir / "implementation.md"
6141
+    implementation_plan.write_text(
6142
+        "\n".join(
6143
+            [
6144
+                "# Implementation Plan",
6145
+                "",
6146
+                "## File Changes",
6147
+                f"- `{index}`",
6148
+                f"- `{chapters / '01-introduction.html'}`",
6149
+                f"- `{chapters / '02-installation.html'}`",
6150
+                "",
6151
+            ]
6152
+        )
6153
+    )
6154
+
6155
+    context = build_context(
6156
+        temp_dir=temp_dir,
6157
+        messages=[],
6158
+        safeguards=FakeSafeguards(),
6159
+        assess_confidence=assess_confidence,
6160
+        verify_action=verify_action,
6161
+    )
6162
+    queued: list[str] = []
6163
+    context.queue_steering_message_callback = queued.append
6164
+    runner = ToolBatchRunner(context, DefinitionOfDoneStore(temp_dir))
6165
+    dod = create_definition_of_done("Create a guide.")
6166
+    dod.implementation_plan = str(implementation_plan)
6167
+    dod.verification_commands = [f"ls -la {guide}"]
6168
+    dod.touched_files = [str(index), str(chapters / "01-introduction.html"), str(chapters / "02-installation.html")]
6169
+
6170
+    runner._queue_blocked_html_missing_target_nudge(
6171
+        ToolCall(
6172
+            id="edit-root",
6173
+            name="edit",
6174
+            arguments={"file_path": str(index)},
6175
+        ),
6176
+        (
6177
+            "[Blocked - Edited HTML links point to files that do not exist] "
6178
+            "Suggestion: Use only existing local targets for href values and avoid introducing missing links, "
6179
+            "for example fix: chapters/08-advanced-configuration.html"
6180
+        ),
6181
+        dod=dod,
6182
+    )
6183
+
6184
+    assert queued
6185
+    assert "All explicitly planned artifacts already exist on disk." in queued[0]
6186
+    assert "Do not introduce new local-link targets beyond the current output set." in queued[0]
6187
+    assert "Repair the existing generated files instead of expanding the guide." in queued[0]
6188
+
6189
+
58776190
 @pytest.mark.asyncio
58786191
 async def test_tool_batch_runner_blocked_empty_file_path_nudges_concrete_next_artifact(
58796192
     temp_dir: Path,