tenseleyflow/loader / 3842320

Browse files

Scaffold empty mutation retries

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
38423207c7aa50e1091a2b101fb7729eadd52b17
Parents
28de433
Tree
897f3a4

2 changed files

StatusFile+-
M src/loader/runtime/repair.py 82 12
M tests/test_repair.py 12 0
src/loader/runtime/repair.pymodified
@@ -2,6 +2,7 @@
22
 
33
 from __future__ import annotations
44
 
5
+import json
56
 import re
67
 from dataclasses import dataclass, field
78
 from pathlib import Path
@@ -375,6 +376,12 @@ class ResponseRepairer:
375376
                 return [
376377
                     target_line,
377378
                     "Do not leave `file_path` empty; point it at the concrete next output file.",
379
+                    self._mutation_tool_scaffold(
380
+                        Path(target),
381
+                        tool_name="write",
382
+                    )
383
+                    if target
384
+                    else "Emit the `write(file_path=..., content=\"...\")` call with the real target path now.",
378385
                 ]
379386
             if attempt.tool_name == "edit":
380387
                 target_line = (
@@ -385,6 +392,12 @@ class ResponseRepairer:
385392
                 return [
386393
                     target_line,
387394
                     "Do not leave `file_path` empty; point it at the concrete file you already know needs the edit.",
395
+                    self._mutation_tool_scaffold(
396
+                        Path(target),
397
+                        tool_name="edit",
398
+                    )
399
+                    if target
400
+                    else "Emit the `edit(file_path=..., old_string=\"...\", new_string=\"...\")` call with the real target path now.",
388401
                 ]
389402
             if attempt.tool_name == "patch":
390403
                 target_line = (
@@ -395,19 +408,32 @@ class ResponseRepairer:
395408
                 return [
396409
                     target_line,
397410
                     "Do not leave `file_path` empty; point it at the concrete file you already know needs the patch.",
411
+                    self._mutation_tool_scaffold(
412
+                        Path(target),
413
+                        tool_name="patch",
414
+                    )
415
+                    if target
416
+                    else "Emit the `patch(file_path=..., patch=\"...\")` call with the real target path now.",
398417
                 ]
399418
         if attempt.tool_name == "write":
400
-            target_line = (
419
+            lines = [
420
+                (
401421
                     f"Last tool failure: resend `write` for `{target}` with real `content`, not just summary fields."
402422
                     if target
403423
                     else "Last tool failure: resend `write` with real `content`, not just summary fields."
404
-            )
405
-            return [
406
-                target_line,
407
-                f"Do not use {invalid} in place of the actual file body.",
424
+                ),
408425
             ]
426
+            lines.append(f"Do not use {invalid} in place of the actual file body.")
427
+            if target:
428
+                lines.append(
429
+                    self._mutation_tool_scaffold(
430
+                        Path(target),
431
+                        tool_name="write",
432
+                    )
433
+                )
434
+            return lines
409435
         if attempt.tool_name == "edit":
410
-            return [
436
+            lines = [
411437
                 (
412438
                     f"Last tool failure: resend `edit` for `{target}` with the real text payload."
413439
                     if target
@@ -415,8 +441,16 @@ class ResponseRepairer:
415441
                 ),
416442
                 f"Do not use {invalid} in place of `old_string`/`new_string`.",
417443
             ]
444
+            if target:
445
+                lines.append(
446
+                    self._mutation_tool_scaffold(
447
+                        Path(target),
448
+                        tool_name="edit",
449
+                    )
450
+                )
451
+            return lines
418452
         if attempt.tool_name == "patch":
419
-            return [
453
+            lines = [
420454
                 (
421455
                     f"Last tool failure: resend `patch` for `{target}` with real patch text or structured hunks."
422456
                     if target
@@ -424,6 +458,14 @@ class ResponseRepairer:
424458
                 ),
425459
                 f"Do not use {invalid} in place of the real patch payload.",
426460
             ]
461
+            if target:
462
+                lines.append(
463
+                    self._mutation_tool_scaffold(
464
+                        Path(target),
465
+                        tool_name="patch",
466
+                    )
467
+                )
468
+            return lines
427469
         return []
428470
 
429471
     def _todo_refresh_retry_line(self, dod: DefinitionOfDone) -> str | None:
@@ -666,6 +708,12 @@ class ResponseRepairer:
666708
             lines.append(
667709
                 f"Prefer one `write(content=...)` call for `{inferred_pending_target}` before more research."
668710
             )
711
+            lines.append(
712
+                self._mutation_tool_scaffold(
713
+                    inferred_pending_target,
714
+                    tool_name="write",
715
+                )
716
+            )
669717
             if outline_label:
670718
                 lines.append(
671719
                     f"Use the existing outline label `{outline_label}` for that file so it matches the current guide structure."
@@ -741,6 +789,12 @@ class ResponseRepairer:
741789
                     lines.append(
742790
                         f"Prefer one `write` call for `{next_output_file}` before more research."
743791
                     )
792
+                    lines.append(
793
+                        self._mutation_tool_scaffold(
794
+                            next_output_file,
795
+                            tool_name="write",
796
+                        )
797
+                    )
744798
                     if outline_label:
745799
                         lines.append(
746800
                             f"Use the existing outline label `{outline_label}` for that file so it matches the current guide structure."
@@ -791,8 +845,10 @@ class ResponseRepairer:
791845
                         "for a separate mkdir."
792846
                     )
793847
                 lines.append(
794
-                    "Shape the next response as one concrete `write(file_path=..., "
795
-                    "content=...)` tool call for that exact path."
848
+                    self._mutation_tool_scaffold(
849
+                        target,
850
+                        tool_name="write",
851
+                    )
796852
                 )
797853
             if completed_artifacts >= 3:
798854
                 lines.append(
@@ -1001,6 +1057,20 @@ class ResponseRepairer:
10011057
                 return first_line or None
10021058
         return None
10031059
 
1060
+    @staticmethod
1061
+    def _mutation_tool_scaffold(path: Path, *, tool_name: str) -> str:
1062
+        normalized_path = json.dumps(str(path.expanduser().resolve(strict=False)))
1063
+        if tool_name == "edit":
1064
+            signature = (
1065
+                f"edit(file_path={normalized_path}, old_string=\"...\", "
1066
+                'new_string="...")'
1067
+            )
1068
+        elif tool_name == "patch":
1069
+            signature = f"patch(file_path={normalized_path}, patch=\"...\")"
1070
+        else:
1071
+            signature = f"write(file_path={normalized_path}, content=\"...\")"
1072
+        return f"Emit this tool shape now: `{signature}`."
1073
+
10041074
 
10051075
 def _todo_is_mutation_step(label: str) -> bool:
10061076
     lowered = label.lower()
tests/test_repair.pymodified
@@ -316,6 +316,10 @@ def test_empty_response_retry_mentions_write_can_create_missing_parent_directori
316316
         f"Prefer one `write(content=...)` call for `{index_path}` before more research."
317317
         in decision.retry_message
318318
     )
319
+    assert (
320
+        f'Emit this tool shape now: `write(file_path="{index_path.resolve(strict=False)}", content="...")`.'
321
+        in decision.retry_message
322
+    )
319323
     assert (
320324
         "Do not restart discovery unless one specific missing fact blocks that file write."
321325
         in decision.retry_message
@@ -385,6 +389,10 @@ def test_empty_response_retry_recovers_blocked_empty_file_path_to_concrete_targe
385389
         in decision.retry_message
386390
     )
387391
     assert "Do not leave `file_path` empty" in decision.retry_message
392
+    assert (
393
+        f'Emit this tool shape now: `write(file_path="{second_chapter.resolve(strict=False)}", content="...")`.'
394
+        in decision.retry_message
395
+    )
388396
 
389397
 
390398
 def test_empty_response_retry_respects_discovery_first_pending_step(
@@ -1146,6 +1154,10 @@ def test_empty_response_retry_maps_title_style_todo_to_html_graph_target(
11461154
         "before more research."
11471155
         in decision.retry_message
11481156
     )
1157
+    assert (
1158
+        f'Emit this tool shape now: `write(file_path="{(chapters / "02-installation.html").resolve(strict=False)}", content="...")`.'
1159
+        in decision.retry_message
1160
+    )
11491161
     assert (
11501162
         "Use the existing outline label `Chapter 2: Installation and Setup` for that file "
11511163
         "so it matches the current guide structure."