tenseleyflow/loader / 6925565

Browse files

Recover blocked HTML quality writes

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
6925565bebf9c73a3c3c4da83c439f38d4f8be85
Parents
2c94bf4
Tree
fa6e89a

2 changed files

StatusFile+-
M src/loader/runtime/tool_batches.py 103 3
M tests/test_tool_batches.py 101 0
src/loader/runtime/tool_batches.pymodified
@@ -370,6 +370,11 @@ class ToolBatchRunner:
370370
                     tool_call,
371371
                     outcome.event_content,
372372
                 )
373
+                self._queue_blocked_html_content_quality_nudge(
374
+                    tool_call,
375
+                    outcome.event_content,
376
+                    dod=dod,
377
+                )
373378
                 self._queue_blocked_active_repair_nudge(outcome.event_content)
374379
                 self._queue_blocked_active_repair_mutation_nudge(outcome.event_content)
375380
                 self._queue_blocked_completed_artifact_scope_nudge(
@@ -1515,6 +1520,90 @@ class ToolBatchRunner:
15151520
             "not claim completion until the blocked file write succeeds."
15161521
         )
15171522
 
1523
+    def _queue_blocked_html_content_quality_nudge(
1524
+        self,
1525
+        tool_call: ToolCall,
1526
+        event_content: str,
1527
+        *,
1528
+        dod: DefinitionOfDone,
1529
+    ) -> None:
1530
+        """Keep blocked HTML chapter-quality retries on the same concrete target."""
1531
+
1532
+        if tool_call.name not in {"write", "edit", "patch"}:
1533
+            return
1534
+        if (
1535
+            "HTML content contains placeholder or stub text" not in event_content
1536
+            and "HTML guide chapter content is too thin" not in event_content
1537
+        ):
1538
+            return
1539
+
1540
+        target = str(
1541
+            tool_call.arguments.get("file_path")
1542
+            or tool_call.arguments.get("path")
1543
+            or ""
1544
+        ).strip()
1545
+        if not target:
1546
+            return
1547
+
1548
+        quality_notes: list[str] = []
1549
+        placeholder_phrases = _extract_blocked_html_target_list(
1550
+            event_content,
1551
+            "Placeholder phrase(s):",
1552
+        )
1553
+        if placeholder_phrases:
1554
+            quality_notes.append(
1555
+                "Do not reuse placeholder pattern(s): "
1556
+                + ", ".join(f"`{phrase}`" for phrase in placeholder_phrases[:4])
1557
+                + "."
1558
+            )
1559
+        thin_match = re.search(
1560
+            r"Current content has ([^.]+?); expected at least ([^.]+?)\.",
1561
+            event_content,
1562
+        )
1563
+        if thin_match:
1564
+            quality_notes.append(
1565
+                f"The blocked draft had {thin_match.group(1)}; the floor is {thin_match.group(2)}."
1566
+            )
1567
+
1568
+        missing_artifact = _next_missing_planned_artifact(
1569
+            dod,
1570
+            project_root=self.context.project_root,
1571
+            messages=list(getattr(self.context.session, "messages", []) or []),
1572
+        )
1573
+        missing_suffix = ""
1574
+        if missing_artifact is not None:
1575
+            missing_target, _ = missing_artifact
1576
+            target_path = Path(target).expanduser().resolve(strict=False)
1577
+            if target_path == missing_target.expanduser().resolve(strict=False):
1578
+                missing_suffix = (
1579
+                    " "
1580
+                    + _missing_artifact_resume_suffix(
1581
+                        missing_artifact,
1582
+                        project_root=self.context.project_root,
1583
+                        messages=list(getattr(self.context.session, "messages", []) or []),
1584
+                    ).strip()
1585
+                )
1586
+
1587
+        quality_detail = (
1588
+            " " + " ".join(quality_notes)
1589
+            if quality_notes
1590
+            else ""
1591
+        )
1592
+        self.context.queue_steering_message(
1593
+            f"The last HTML mutation for `{target}` was blocked, so the file was "
1594
+            "not created or updated. Retry that same target with one concrete "
1595
+            "`write`, `edit`, or `patch` call containing finished user-facing HTML, "
1596
+            "not a scaffold or outline."
1597
+            f"{quality_detail}"
1598
+            " Include specific explanations, commands/configuration examples, "
1599
+            "lists, and troubleshooting or operational details as appropriate for "
1600
+            "the artifact."
1601
+            f"{missing_suffix}"
1602
+            " Do not switch to a different sibling file, do not claim completion, "
1603
+            "and do not reopen unrelated reference materials before this blocked "
1604
+            "mutation succeeds."
1605
+        )
1606
+
15181607
     def _queue_blocked_invalid_mutation_nudge(
15191608
         self,
15201609
         tool_call: ToolCall,
@@ -3428,10 +3517,21 @@ def _is_recoverable_guidance_block(event_content: str) -> bool:
34283517
     """Return whether a blocked observation should steer without tripping fatal error limits."""
34293518
 
34303519
     normalized = str(event_content or "")
3431
-    return (
3432
-        "[Blocked - completed artifact set scope:" in normalized
3433
-        or "[Blocked - post-build audit loop:" in normalized
3434
-    )
3520
+    recoverable_markers = (
3521
+        "[Blocked - completed artifact set scope:",
3522
+        "[Blocked - post-build audit loop:",
3523
+        "[Blocked - active repair scope:",
3524
+        "[Blocked - active repair mutation scope:",
3525
+        "[Blocked - late reference drift:",
3526
+        "[Blocked - missing planned output artifact:",
3527
+        "[Blocked - HTML file creation falls outside the current declared artifact set]",
3528
+        "[Blocked - HTML page introduces new local targets outside the current declared artifact set]",
3529
+        "[Blocked - HTML local asset references do not exist]",
3530
+        "[Blocked - HTML content contains placeholder or stub text]",
3531
+        "[Blocked - HTML guide chapter content is too thin]",
3532
+        "[Blocked - Edited HTML links point to files that do not exist]",
3533
+    )
3534
+    return any(marker in normalized for marker in recoverable_markers)
34353535
 
34363536
 
34373537
 def _recent_edit_string_mismatch_target(recovery_context: RecoveryContext | None) -> str:
tests/test_tool_batches.pymodified
@@ -8179,6 +8179,107 @@ def test_tool_batch_runner_blocked_html_declared_file_creation_prefers_closest_t
81798179
     assert "update the guide root" not in queued[0]
81808180
 
81818181
 
8182
+@pytest.mark.asyncio
8183
+async def test_tool_batch_runner_blocked_html_quality_guidance_does_not_halt(
8184
+    temp_dir: Path,
8185
+) -> None:
8186
+    async def assess_confidence(
8187
+        tool_name: str,
8188
+        tool_args: dict,
8189
+        context: str,
8190
+    ) -> ConfidenceAssessment:
8191
+        raise AssertionError("Confidence scoring should not run in this scenario")
8192
+
8193
+    async def verify_action(
8194
+        tool_name: str,
8195
+        tool_args: dict,
8196
+        result: str,
8197
+        expected: str = "",
8198
+    ) -> ActionVerification:
8199
+        raise AssertionError("Verification should not run in this scenario")
8200
+
8201
+    target = temp_dir / "guide" / "chapters" / "06-security.html"
8202
+    implementation_plan = temp_dir / "implementation.md"
8203
+    implementation_plan.write_text(
8204
+        "\n".join(
8205
+            [
8206
+                "# Implementation Plan",
8207
+                "",
8208
+                "## File Changes",
8209
+                f"- `{target}`",
8210
+                "",
8211
+            ]
8212
+        )
8213
+    )
8214
+
8215
+    context = build_context(
8216
+        temp_dir=temp_dir,
8217
+        messages=[],
8218
+        safeguards=FakeSafeguards(),
8219
+        assess_confidence=assess_confidence,
8220
+        verify_action=verify_action,
8221
+    )
8222
+    queued: list[str] = []
8223
+    context.queue_steering_message_callback = queued.append
8224
+    runner = ToolBatchRunner(context, DefinitionOfDoneStore(temp_dir))
8225
+    dod = create_definition_of_done("Create a guide chapter.")
8226
+    dod.implementation_plan = str(implementation_plan)
8227
+
8228
+    tool_calls = [
8229
+        ToolCall(
8230
+            id=f"write-quality-{index}",
8231
+            name="write",
8232
+            arguments={"file_path": str(target), "content": "<html></html>"},
8233
+        )
8234
+        for index in range(3)
8235
+    ]
8236
+    blocked_message = (
8237
+        "[Blocked - HTML content contains placeholder or stub text] "
8238
+        "Suggestion: Replace placeholder phrases with concrete user-facing content "
8239
+        "before writing the HTML artifact. Placeholder phrase(s): generic core "
8240
+        "concepts section, generic practical workflow section. Include specific "
8241
+        "explanations, examples, commands, or structured prose instead."
8242
+    )
8243
+    executor = FakeExecutor(
8244
+        [
8245
+            tool_outcome(
8246
+                tool_call=tool_call,
8247
+                output=blocked_message,
8248
+                is_error=True,
8249
+                state=ToolExecutionState.BLOCKED,
8250
+            )
8251
+            for tool_call in tool_calls
8252
+        ]
8253
+    )
8254
+    events: list[AgentEvent] = []
8255
+
8256
+    async def emit(event: AgentEvent) -> None:
8257
+        events.append(event)
8258
+
8259
+    result = await runner.execute_batch(
8260
+        tool_calls=tool_calls,
8261
+        tool_source="native",
8262
+        pending_tool_calls_seen=set(),
8263
+        emit=emit,
8264
+        summary=TurnSummary(final_response=""),
8265
+        dod=dod,
8266
+        executor=executor,
8267
+        on_confirmation=None,
8268
+        on_user_question=None,
8269
+        emit_confirmation=None,
8270
+        consecutive_errors=0,
8271
+    )
8272
+
8273
+    assert result.halted is False
8274
+    assert result.consecutive_errors == 0
8275
+    assert queued
8276
+    assert str(target) in queued[-1]
8277
+    assert "Retry that same target" in queued[-1]
8278
+    assert "Do not reuse placeholder pattern(s)" in queued[-1]
8279
+    assert "generic core concepts section" in queued[-1]
8280
+    assert "not a scaffold or outline" in queued[-1]
8281
+
8282
+
81828283
 def test_tool_batch_runner_blocked_html_missing_target_after_outputs_exist_prefers_verify(
81838284
     temp_dir: Path,
81848285
 ) -> None: