tenseleyflow/loader / 75beb49

Browse files

Resume missing declared outputs

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
75beb496a915bcb50031e2a869a43ac9e774c5b5
Parents
c10dbd1
Tree
0f46f75

2 changed files

StatusFile+-
M src/loader/runtime/repair.py 102 7
M tests/test_repair.py 70 0
src/loader/runtime/repair.pymodified
@@ -12,6 +12,7 @@ from ..llm.base import ToolCall
1212
 from .context import RuntimeContext
1313
 from .dod import (
1414
     DefinitionOfDone,
15
+    collect_missing_declared_html_output_files,
1516
     collect_planned_artifact_targets,
1617
     infer_next_output_file,
1718
     planned_artifact_target_satisfied,
@@ -695,7 +696,7 @@ class ResponseRepairer:
695696
     def _should_compact_empty_retry_message(self, dod: DefinitionOfDone) -> bool:
696697
         completed_artifacts, missing_artifacts = self._planned_artifact_counts(dod)
697698
         if completed_artifacts >= 3:
698
-            return missing_artifacts > 0
699
+            return missing_artifacts > 0 or self._has_concrete_next_output_step(dod)
699700
         return self._has_concrete_next_output_step(dod)
700701
 
701702
     def _planned_artifact_counts(self, dod: DefinitionOfDone) -> tuple[int, int]:
@@ -718,14 +719,15 @@ class ResponseRepairer:
718719
         return completed, missing
719720
 
720721
     def _has_concrete_next_output_step(self, dod: DefinitionOfDone) -> bool:
722
+        planned_targets = collect_planned_artifact_targets(
723
+            dod,
724
+            project_root=self.context.project_root,
725
+            max_paths=12,
726
+        )
721727
         next_missing_artifact = next(
722728
             (
723729
                 artifact
724
-                for artifact in collect_planned_artifact_targets(
725
-                    dod,
726
-                    project_root=self.context.project_root,
727
-                    max_paths=12,
728
-                )
730
+                for artifact in planned_targets
729731
                 if not planned_artifact_target_satisfied(
730732
                     dod,
731733
                     target=artifact[0],
@@ -735,6 +737,10 @@ class ResponseRepairer:
735737
             ),
736738
             None,
737739
         )
740
+        if next_missing_artifact is None:
741
+            next_missing_artifact = self._next_missing_declared_output_artifact(
742
+                planned_targets
743
+            )
738744
         next_pending = self._preferred_resume_pending_item(
739745
             dod,
740746
             missing_artifact=next_missing_artifact,
@@ -816,6 +822,13 @@ class ResponseRepairer:
816822
                 project_root=self.context.project_root,
817823
             )
818824
         ]
825
+        if not missing_labels and preferred_missing_artifact is not None:
826
+            missing_labels = [
827
+                self._format_artifact_label(
828
+                    preferred_missing_artifact[0],
829
+                    expect_directory=preferred_missing_artifact[1],
830
+                )
831
+            ]
819832
         if not missing_labels:
820833
             return []
821834
 
@@ -1110,6 +1123,63 @@ class ResponseRepairer:
11101123
                 )
11111124
             return lines
11121125
 
1126
+        if next_missing_artifact is not None and not next_missing_artifact[1]:
1127
+            concrete_target = next_missing_artifact[0]
1128
+            planned_target_keys = {
1129
+                str(target.expanduser().resolve(strict=False))
1130
+                for target, expect_directory in collect_planned_artifact_targets(
1131
+                    dod,
1132
+                    project_root=self.context.project_root,
1133
+                    max_paths=12,
1134
+                )
1135
+                if not expect_directory
1136
+            }
1137
+            if str(concrete_target.expanduser().resolve(strict=False)) not in planned_target_keys:
1138
+                outline_label = infer_output_outline_label(
1139
+                    dod,
1140
+                    concrete_target,
1141
+                    project_root=self.context.project_root,
1142
+                    todo_label=next_pending or "",
1143
+                )
1144
+                lines = [
1145
+                    f"Resume with this exact next step: create `{concrete_target.name}`.",
1146
+                    "It is the next missing declared output in the current artifact graph.",
1147
+                    "Prefer one `write(content=...)` call for "
1148
+                    f"`{display_runtime_path(concrete_target)}` before more research.",
1149
+                    self._mutation_tool_scaffold(
1150
+                        concrete_target,
1151
+                        tool_name="write",
1152
+                    ),
1153
+                ]
1154
+                if outline_label:
1155
+                    lines.append(
1156
+                        f"Use the existing outline label `{outline_label}` for that file so it matches the current guide structure."
1157
+                    )
1158
+                self._append_concrete_html_write_cues(
1159
+                    lines,
1160
+                    target=concrete_target,
1161
+                    outline_label=outline_label,
1162
+                    retry_number=retry_number,
1163
+                    has_confirmed_output_file_progress=has_confirmed_output_file_progress,
1164
+                    has_confirmed_substantive_output_file_progress=has_confirmed_substantive_output_file_progress,
1165
+                )
1166
+                if has_confirmed_substantive_output_file_progress:
1167
+                    lines.append(
1168
+                        "Follow the same full-payload one-file-at-a-time write pattern that "
1169
+                        "already created the confirmed output files."
1170
+                    )
1171
+                if retry_number >= 2:
1172
+                    lines.append(
1173
+                        "Do not return another working note or empty response; emit the "
1174
+                        "concrete mutation tool call now."
1175
+                    )
1176
+                else:
1177
+                    lines.append(
1178
+                        "Do not restart discovery unless one specific missing fact blocks "
1179
+                        "that file write."
1180
+                    )
1181
+                return lines
1182
+
11131183
         for target, expect_directory in collect_planned_artifact_targets(
11141184
             dod,
11151185
             project_root=self.context.project_root,
@@ -1398,7 +1468,7 @@ class ResponseRepairer:
13981468
             None,
13991469
         )
14001470
         if first_missing is None:
1401
-            return None
1471
+            return self._next_missing_declared_output_artifact(planned_targets)
14021472
 
14031473
         next_pending = self._preferred_resume_pending_item(
14041474
             dod,
@@ -1432,6 +1502,31 @@ class ResponseRepairer:
14321502
                 return normalized_target, False
14331503
         return first_missing
14341504
 
1505
+    def _next_missing_declared_output_artifact(
1506
+        self,
1507
+        planned_targets: list[tuple[Path, bool]],
1508
+    ) -> tuple[Path, bool] | None:
1509
+        seen_scopes: set[str] = set()
1510
+        for planned_target, expect_directory in planned_targets:
1511
+            if expect_directory:
1512
+                scope_target = planned_target
1513
+            elif planned_target.suffix.lower() in {".html", ".htm"}:
1514
+                scope_target = planned_target
1515
+            else:
1516
+                continue
1517
+
1518
+            scope_key = str(scope_target.expanduser().resolve(strict=False))
1519
+            if scope_key in seen_scopes:
1520
+                continue
1521
+            seen_scopes.add(scope_key)
1522
+            missing_outputs = collect_missing_declared_html_output_files(
1523
+                target=scope_target,
1524
+                project_root=self.context.project_root,
1525
+            )
1526
+            if missing_outputs:
1527
+                return missing_outputs[0], False
1528
+        return None
1529
+
14351530
     def _preferred_retry_target(self, dod: DefinitionOfDone | None) -> str:
14361531
         if dod is None:
14371532
             return ""
tests/test_repair.pymodified
@@ -2023,6 +2023,76 @@ def test_empty_response_retry_points_at_declared_child_file_within_incomplete_ou
20232023
     )
20242024
 
20252025
 
2026
+def test_empty_response_retry_names_missing_declared_child_after_directory_exists(
2027
+    temp_dir: Path,
2028
+) -> None:
2029
+    context = build_context(
2030
+        temp_dir=temp_dir,
2031
+        use_react=False,
2032
+    )
2033
+    repairer = ResponseRepairer(context)
2034
+
2035
+    guide_root = temp_dir / "guides" / "nginx"
2036
+    chapters = guide_root / "chapters"
2037
+    chapters.mkdir(parents=True)
2038
+    index_path = guide_root / "index.html"
2039
+    chapter_four = chapters / "04-reverse-proxy.html"
2040
+    chapter_five = chapters / "05-load-balancing.html"
2041
+    chapter_six = chapters / "06-security.html"
2042
+    index_path.write_text(
2043
+        "\n".join(
2044
+            [
2045
+                "<html>",
2046
+                '<a href="chapters/04-reverse-proxy.html">Reverse Proxy</a>',
2047
+                '<a href="chapters/05-load-balancing.html">Load Balancing</a>',
2048
+                '<a href="chapters/06-security.html">Security</a>',
2049
+                "</html>",
2050
+            ]
2051
+        )
2052
+        + "\n"
2053
+    )
2054
+    chapter_four.write_text("<html><h1>Reverse Proxy</h1></html>\n")
2055
+    chapter_five.write_text("<html><h1>Load Balancing</h1></html>\n")
2056
+
2057
+    implementation_plan = temp_dir / "implementation.md"
2058
+    implementation_plan.write_text(
2059
+        "\n".join(
2060
+            [
2061
+                "# Implementation Plan",
2062
+                "",
2063
+                "## File Changes",
2064
+                f"- `{guide_root}/`",
2065
+                f"- `{chapters}/`",
2066
+                f"- `{index_path}`",
2067
+                "",
2068
+            ]
2069
+        )
2070
+    )
2071
+
2072
+    dod = create_definition_of_done("Create a multi-file nginx guide.")
2073
+    dod.implementation_plan = str(implementation_plan)
2074
+    dod.touched_files.extend([str(index_path), str(chapter_four), str(chapter_five)])
2075
+    dod.completed_items.append("Create load balancing chapter")
2076
+    dod.pending_items.extend(["Complete the requested work", "Collect verification evidence"])
2077
+
2078
+    decision = repairer.handle_empty_response(
2079
+        task="Create a multi-file nginx guide.",
2080
+        original_task=None,
2081
+        empty_retry_count=2,
2082
+        max_empty_retries=2,
2083
+        dod=dod,
2084
+    )
2085
+
2086
+    assert decision.should_continue is True
2087
+    assert decision.retry_message is not None
2088
+    assert "Resume with this exact next step: create `06-security.html`." in decision.retry_message
2089
+    assert "It is the next missing declared output" in decision.retry_message
2090
+    assert (
2091
+        f'Emit this tool shape now: `write(file_path="{display_runtime_path(chapter_six)}", content="...")`.'
2092
+        in decision.retry_message
2093
+    )
2094
+
2095
+
20262096
 def test_empty_response_retry_infers_concrete_file_from_pending_todo_after_broad_artifacts_exist(
20272097
     temp_dir: Path,
20282098
 ) -> None: