@@ -370,6 +370,11 @@ class ToolBatchRunner: |
| 370 | 370 | tool_call, |
| 371 | 371 | outcome.event_content, |
| 372 | 372 | ) |
| 373 | + self._queue_blocked_html_content_quality_nudge( |
| 374 | + tool_call, |
| 375 | + outcome.event_content, |
| 376 | + dod=dod, |
| 377 | + ) |
| 373 | 378 | self._queue_blocked_active_repair_nudge(outcome.event_content) |
| 374 | 379 | self._queue_blocked_active_repair_mutation_nudge(outcome.event_content) |
| 375 | 380 | self._queue_blocked_completed_artifact_scope_nudge( |
@@ -1515,6 +1520,90 @@ class ToolBatchRunner: |
| 1515 | 1520 | "not claim completion until the blocked file write succeeds." |
| 1516 | 1521 | ) |
| 1517 | 1522 | |
| 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 | + |
| 1518 | 1607 | def _queue_blocked_invalid_mutation_nudge( |
| 1519 | 1608 | self, |
| 1520 | 1609 | tool_call: ToolCall, |
@@ -3428,10 +3517,21 @@ def _is_recoverable_guidance_block(event_content: str) -> bool: |
| 3428 | 3517 | """Return whether a blocked observation should steer without tripping fatal error limits.""" |
| 3429 | 3518 | |
| 3430 | 3519 | normalized = str(event_content or "") |
| 3431 | | - return ( |
| 3432 | | - "[Blocked - completed artifact set scope:" in normalized |
| 3433 | | - or "[Blocked - post-build audit loop:" in normalized |
| 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]", |
| 3434 | 3533 | ) |
| 3534 | + return any(marker in normalized for marker in recoverable_markers) |
| 3435 | 3535 | |
| 3436 | 3536 | |
| 3437 | 3537 | def _recent_edit_string_mismatch_target(recovery_context: RecoveryContext | None) -> str: |