@@ -2,6 +2,7 @@ |
| 2 | 2 | |
| 3 | 3 | from __future__ import annotations |
| 4 | 4 | |
| 5 | +import json |
| 5 | 6 | import re |
| 6 | 7 | from dataclasses import dataclass, field |
| 7 | 8 | from pathlib import Path |
@@ -375,6 +376,12 @@ class ResponseRepairer: |
| 375 | 376 | return [ |
| 376 | 377 | target_line, |
| 377 | 378 | "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.", |
| 378 | 385 | ] |
| 379 | 386 | if attempt.tool_name == "edit": |
| 380 | 387 | target_line = ( |
@@ -385,6 +392,12 @@ class ResponseRepairer: |
| 385 | 392 | return [ |
| 386 | 393 | target_line, |
| 387 | 394 | "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.", |
| 388 | 401 | ] |
| 389 | 402 | if attempt.tool_name == "patch": |
| 390 | 403 | target_line = ( |
@@ -395,19 +408,32 @@ class ResponseRepairer: |
| 395 | 408 | return [ |
| 396 | 409 | target_line, |
| 397 | 410 | "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.", |
| 398 | 417 | ] |
| 399 | 418 | if attempt.tool_name == "write": |
| 400 | | - target_line = ( |
| 419 | + lines = [ |
| 420 | + ( |
| 401 | 421 | f"Last tool failure: resend `write` for `{target}` with real `content`, not just summary fields." |
| 402 | 422 | if target |
| 403 | 423 | 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 | + ), |
| 408 | 425 | ] |
| 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 |
| 409 | 435 | if attempt.tool_name == "edit": |
| 410 | | - return [ |
| 436 | + lines = [ |
| 411 | 437 | ( |
| 412 | 438 | f"Last tool failure: resend `edit` for `{target}` with the real text payload." |
| 413 | 439 | if target |
@@ -415,8 +441,16 @@ class ResponseRepairer: |
| 415 | 441 | ), |
| 416 | 442 | f"Do not use {invalid} in place of `old_string`/`new_string`.", |
| 417 | 443 | ] |
| 444 | + if target: |
| 445 | + lines.append( |
| 446 | + self._mutation_tool_scaffold( |
| 447 | + Path(target), |
| 448 | + tool_name="edit", |
| 449 | + ) |
| 450 | + ) |
| 451 | + return lines |
| 418 | 452 | if attempt.tool_name == "patch": |
| 419 | | - return [ |
| 453 | + lines = [ |
| 420 | 454 | ( |
| 421 | 455 | f"Last tool failure: resend `patch` for `{target}` with real patch text or structured hunks." |
| 422 | 456 | if target |
@@ -424,6 +458,14 @@ class ResponseRepairer: |
| 424 | 458 | ), |
| 425 | 459 | f"Do not use {invalid} in place of the real patch payload.", |
| 426 | 460 | ] |
| 461 | + if target: |
| 462 | + lines.append( |
| 463 | + self._mutation_tool_scaffold( |
| 464 | + Path(target), |
| 465 | + tool_name="patch", |
| 466 | + ) |
| 467 | + ) |
| 468 | + return lines |
| 427 | 469 | return [] |
| 428 | 470 | |
| 429 | 471 | def _todo_refresh_retry_line(self, dod: DefinitionOfDone) -> str | None: |
@@ -666,6 +708,12 @@ class ResponseRepairer: |
| 666 | 708 | lines.append( |
| 667 | 709 | f"Prefer one `write(content=...)` call for `{inferred_pending_target}` before more research." |
| 668 | 710 | ) |
| 711 | + lines.append( |
| 712 | + self._mutation_tool_scaffold( |
| 713 | + inferred_pending_target, |
| 714 | + tool_name="write", |
| 715 | + ) |
| 716 | + ) |
| 669 | 717 | if outline_label: |
| 670 | 718 | lines.append( |
| 671 | 719 | f"Use the existing outline label `{outline_label}` for that file so it matches the current guide structure." |
@@ -741,6 +789,12 @@ class ResponseRepairer: |
| 741 | 789 | lines.append( |
| 742 | 790 | f"Prefer one `write` call for `{next_output_file}` before more research." |
| 743 | 791 | ) |
| 792 | + lines.append( |
| 793 | + self._mutation_tool_scaffold( |
| 794 | + next_output_file, |
| 795 | + tool_name="write", |
| 796 | + ) |
| 797 | + ) |
| 744 | 798 | if outline_label: |
| 745 | 799 | lines.append( |
| 746 | 800 | f"Use the existing outline label `{outline_label}` for that file so it matches the current guide structure." |
@@ -791,8 +845,10 @@ class ResponseRepairer: |
| 791 | 845 | "for a separate mkdir." |
| 792 | 846 | ) |
| 793 | 847 | 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 | + ) |
| 796 | 852 | ) |
| 797 | 853 | if completed_artifacts >= 3: |
| 798 | 854 | lines.append( |
@@ -1001,6 +1057,20 @@ class ResponseRepairer: |
| 1001 | 1057 | return first_line or None |
| 1002 | 1058 | return None |
| 1003 | 1059 | |
| 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 | + |
| 1004 | 1074 | |
| 1005 | 1075 | def _todo_is_mutation_step(label: str) -> bool: |
| 1006 | 1076 | lowered = label.lower() |