tenseleyflow/loader / c3b0cbc

Browse files

Surface outline labels

Authored by espadonne
SHA
c3b0cbc3ff49da229f256bf604eef5ceca9d5899
Parents
a53d8d8
Tree
045ccc0

3 changed files

StatusFile+-
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 (
1717
 from .parsing import parse_tool_calls
1818
 from .recovery import detect_missing_mutation_payload
1919
 from .workflow import (
20
+    infer_output_outline_label,
2021
     infer_pending_todo_output_target,
2122
     preferred_pending_todo_item,
2223
     reconcile_aggregate_completion_steps,
@@ -621,6 +622,12 @@ class ResponseRepairer:
621622
                 inferred_pending_target,
622623
                 expect_directory=False,
623624
             )
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
+            )
624631
             lines = [
625632
                 "Resume with this exact next step: continue "
626633
                 f"`{next_pending}` by creating {inferred_label}."
@@ -628,6 +635,10 @@ class ResponseRepairer:
628635
             lines.append(
629636
                 f"Prefer one `write(content=...)` call for `{inferred_pending_target}` before more research."
630637
             )
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
+                )
631642
             if completed_artifacts >= 2:
632643
                 lines.append(
633644
                     "Follow the same one-file-at-a-time mutation pattern that already "
@@ -672,6 +683,12 @@ class ResponseRepairer:
672683
                         next_output_file,
673684
                         expect_directory=False,
674685
                     )
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
+                    )
675692
                     if next_pending and _todo_is_mutation_step(next_pending):
676693
                         lines = [
677694
                             "Resume with this exact next step: continue "
@@ -693,6 +710,10 @@ class ResponseRepairer:
693710
                     lines.append(
694711
                         f"Prefer one `write` call for `{next_output_file}` before more research."
695712
                     )
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
+                        )
696717
                     if not next_output_file.parent.exists():
697718
                         lines.append(
698719
                             "The `write` tool can create that file's parent directories "
src/loader/runtime/workflow.pymodified
@@ -54,6 +54,7 @@ __all__ = [
5454
     "effective_pending_todo_items",
5555
     "enrich_clarify_brief_with_grounding",
5656
     "extract_verification_commands_from_markdown",
57
+    "infer_output_outline_label",
5758
     "infer_pending_todo_output_target",
5859
     "load_brief",
5960
     "load_planning_artifacts",
@@ -1006,6 +1007,43 @@ def infer_pending_todo_output_target(
10061007
     return matches[0][2]
10071008
 
10081009
 
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
+
10091047
 def _select_best_pending_output_path(
10101048
     paths: list[Path],
10111049
     *,
tests/test_repair.pymodified
@@ -1081,6 +1081,11 @@ def test_empty_response_retry_maps_title_style_todo_to_html_graph_target(
10811081
         "before more research."
10821082
         in decision.retry_message
10831083
     )
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
+    )
10841089
 
10851090
 
10861091
 def test_empty_response_retry_reminds_model_to_resend_real_write_payload(