Surface outline labels
- SHA
c3b0cbc3ff49da229f256bf604eef5ceca9d5899- Parents
-
a53d8d8 - Tree
045ccc0
c3b0cbc
c3b0cbc3ff49da229f256bf604eef5ceca9d5899a53d8d8
045ccc0| Status | File | + | - |
|---|---|---|---|
| M |
src/loader/runtime/repair.py
|
21 | 0 |
| M |
src/loader/runtime/workflow.py
|
38 | 0 |
| M |
tests/test_repair.py
|
5 | 0 |
src/loader/runtime/repair.pymodified@@ -17,6 +17,7 @@ from .dod import ( | ||
| 17 | 17 | from .parsing import parse_tool_calls |
| 18 | 18 | from .recovery import detect_missing_mutation_payload |
| 19 | 19 | from .workflow import ( |
| 20 | + infer_output_outline_label, | |
| 20 | 21 | infer_pending_todo_output_target, |
| 21 | 22 | preferred_pending_todo_item, |
| 22 | 23 | reconcile_aggregate_completion_steps, |
@@ -621,6 +622,12 @@ class ResponseRepairer: | ||
| 621 | 622 | inferred_pending_target, |
| 622 | 623 | expect_directory=False, |
| 623 | 624 | ) |
| 625 | + outline_label = infer_output_outline_label( | |
| 626 | + dod, | |
| 627 | + inferred_pending_target, | |
| 628 | + project_root=self.context.project_root, | |
| 629 | + todo_label=next_pending, | |
| 630 | + ) | |
| 624 | 631 | lines = [ |
| 625 | 632 | "Resume with this exact next step: continue " |
| 626 | 633 | f"`{next_pending}` by creating {inferred_label}." |
@@ -628,6 +635,10 @@ class ResponseRepairer: | ||
| 628 | 635 | lines.append( |
| 629 | 636 | f"Prefer one `write(content=...)` call for `{inferred_pending_target}` before more research." |
| 630 | 637 | ) |
| 638 | + if outline_label: | |
| 639 | + lines.append( | |
| 640 | + f"Use the existing outline label `{outline_label}` for that file so it matches the current guide structure." | |
| 641 | + ) | |
| 631 | 642 | if completed_artifacts >= 2: |
| 632 | 643 | lines.append( |
| 633 | 644 | "Follow the same one-file-at-a-time mutation pattern that already " |
@@ -672,6 +683,12 @@ class ResponseRepairer: | ||
| 672 | 683 | next_output_file, |
| 673 | 684 | expect_directory=False, |
| 674 | 685 | ) |
| 686 | + outline_label = infer_output_outline_label( | |
| 687 | + dod, | |
| 688 | + next_output_file, | |
| 689 | + project_root=self.context.project_root, | |
| 690 | + todo_label=next_pending or "", | |
| 691 | + ) | |
| 675 | 692 | if next_pending and _todo_is_mutation_step(next_pending): |
| 676 | 693 | lines = [ |
| 677 | 694 | "Resume with this exact next step: continue " |
@@ -693,6 +710,10 @@ class ResponseRepairer: | ||
| 693 | 710 | lines.append( |
| 694 | 711 | f"Prefer one `write` call for `{next_output_file}` before more research." |
| 695 | 712 | ) |
| 713 | + if outline_label: | |
| 714 | + lines.append( | |
| 715 | + f"Use the existing outline label `{outline_label}` for that file so it matches the current guide structure." | |
| 716 | + ) | |
| 696 | 717 | if not next_output_file.parent.exists(): |
| 697 | 718 | lines.append( |
| 698 | 719 | "The `write` tool can create that file's parent directories " |
src/loader/runtime/workflow.pymodified@@ -54,6 +54,7 @@ __all__ = [ | ||
| 54 | 54 | "effective_pending_todo_items", |
| 55 | 55 | "enrich_clarify_brief_with_grounding", |
| 56 | 56 | "extract_verification_commands_from_markdown", |
| 57 | + "infer_output_outline_label", | |
| 57 | 58 | "infer_pending_todo_output_target", |
| 58 | 59 | "load_brief", |
| 59 | 60 | "load_planning_artifacts", |
@@ -1006,6 +1007,43 @@ def infer_pending_todo_output_target( | ||
| 1006 | 1007 | return matches[0][2] |
| 1007 | 1008 | |
| 1008 | 1009 | |
| 1010 | +def infer_output_outline_label( | |
| 1011 | + dod, | |
| 1012 | + target_path: Path | str, | |
| 1013 | + *, | |
| 1014 | + project_root: Path | None = None, | |
| 1015 | + todo_label: str | None = None, | |
| 1016 | +) -> str | None: | |
| 1017 | + """Infer the existing outline/link label for one concrete output target.""" | |
| 1018 | + | |
| 1019 | + root = project_root or Path.cwd() | |
| 1020 | + target = Path(target_path).expanduser().resolve(strict=False) | |
| 1021 | + normalized_todo = _normalize_pending_output_label(todo_label or "") | |
| 1022 | + best_match: tuple[int, int, str] | None = None | |
| 1023 | + | |
| 1024 | + for html_file in _pending_item_html_sources( | |
| 1025 | + dod, | |
| 1026 | + project_root=root, | |
| 1027 | + ): | |
| 1028 | + try: | |
| 1029 | + content = html_file.read_text() | |
| 1030 | + except OSError: | |
| 1031 | + continue | |
| 1032 | + for href, link_text in _iter_local_html_links(content): | |
| 1033 | + resolved = (html_file.parent / href).resolve(strict=False) | |
| 1034 | + if resolved != target: | |
| 1035 | + continue | |
| 1036 | + normalized_label = _normalize_pending_output_label(link_text) | |
| 1037 | + score = _pending_output_link_match_score(normalized_todo, normalized_label) | |
| 1038 | + candidate = (score, len(link_text.strip()), link_text.strip()) | |
| 1039 | + if best_match is None or candidate > best_match: | |
| 1040 | + best_match = candidate | |
| 1041 | + | |
| 1042 | + if best_match is None: | |
| 1043 | + return None | |
| 1044 | + return best_match[2] | |
| 1045 | + | |
| 1046 | + | |
| 1009 | 1047 | def _select_best_pending_output_path( |
| 1010 | 1048 | paths: list[Path], |
| 1011 | 1049 | *, |
tests/test_repair.pymodified@@ -1081,6 +1081,11 @@ def test_empty_response_retry_maps_title_style_todo_to_html_graph_target( | ||
| 1081 | 1081 | "before more research." |
| 1082 | 1082 | in decision.retry_message |
| 1083 | 1083 | ) |
| 1084 | + assert ( | |
| 1085 | + "Use the existing outline label `Chapter 2: Installation and Setup` for that file " | |
| 1086 | + "so it matches the current guide structure." | |
| 1087 | + in decision.retry_message | |
| 1088 | + ) | |
| 1084 | 1089 | |
| 1085 | 1090 | |
| 1086 | 1091 | def test_empty_response_retry_reminds_model_to_resend_real_write_payload( |