@@ -370,6 +370,11 @@ class ToolBatchRunner: |
| 370 | tool_call, | 370 | tool_call, |
| 371 | outcome.event_content, | 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 | self._queue_blocked_active_repair_nudge(outcome.event_content) | 378 | self._queue_blocked_active_repair_nudge(outcome.event_content) |
| 374 | self._queue_blocked_active_repair_mutation_nudge(outcome.event_content) | 379 | self._queue_blocked_active_repair_mutation_nudge(outcome.event_content) |
| 375 | self._queue_blocked_completed_artifact_scope_nudge( | 380 | self._queue_blocked_completed_artifact_scope_nudge( |
@@ -1515,6 +1520,90 @@ class ToolBatchRunner: |
| 1515 | "not claim completion until the blocked file write succeeds." | 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 | def _queue_blocked_invalid_mutation_nudge( | 1607 | def _queue_blocked_invalid_mutation_nudge( |
| 1519 | self, | 1608 | self, |
| 1520 | tool_call: ToolCall, | 1609 | tool_call: ToolCall, |
@@ -3428,10 +3517,21 @@ def _is_recoverable_guidance_block(event_content: str) -> bool: |
| 3428 | """Return whether a blocked observation should steer without tripping fatal error limits.""" | 3517 | """Return whether a blocked observation should steer without tripping fatal error limits.""" |
| 3429 | | 3518 | |
| 3430 | normalized = str(event_content or "") | 3519 | normalized = str(event_content or "") |
| 3431 | - return ( | 3520 | + recoverable_markers = ( |
| 3432 | - "[Blocked - completed artifact set scope:" in normalized | 3521 | + "[Blocked - completed artifact set scope:", |
| 3433 | - or "[Blocked - post-build audit loop:" in normalized | 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 | def _recent_edit_string_mismatch_target(recovery_context: RecoveryContext | None) -> str: | 3537 | def _recent_edit_string_mismatch_target(recovery_context: RecoveryContext | None) -> str: |