Handle directory setup retries
Authored by
mfwolffe <wolffemf@dukes.jmu.edu>
- SHA
4a458e44701049a8cf8c907c20a3d21ec7f8966c- Parents
-
3cbd1bb - Tree
12c8c4f
4a458e4
4a458e44701049a8cf8c907c20a3d21ec7f8966c3cbd1bb
12c8c4f| Status | File | + | - |
|---|---|---|---|
| M |
src/loader/runtime/repair.py
|
24 | 9 |
| M |
tests/test_repair.py
|
63 | 0 |
src/loader/runtime/repair.pymodified@@ -4,6 +4,7 @@ from __future__ import annotations | ||
| 4 | 4 | |
| 5 | 5 | import json |
| 6 | 6 | import re |
| 7 | +import shlex | |
| 7 | 8 | from dataclasses import dataclass, field |
| 8 | 9 | from pathlib import Path |
| 9 | 10 | |
@@ -691,9 +692,10 @@ class ResponseRepairer: | ||
| 691 | 692 | else None |
| 692 | 693 | ) |
| 693 | 694 | if next_pending and inferred_pending_target is not None: |
| 695 | + inferred_is_directory = not bool(inferred_pending_target.suffix) | |
| 694 | 696 | inferred_label = self._format_artifact_label( |
| 695 | 697 | inferred_pending_target, |
| 696 | - expect_directory=False, | |
| 698 | + expect_directory=inferred_is_directory, | |
| 697 | 699 | ) |
| 698 | 700 | outline_label = infer_output_outline_label( |
| 699 | 701 | dod, |
@@ -705,15 +707,23 @@ class ResponseRepairer: | ||
| 705 | 707 | "Resume with this exact next step: continue " |
| 706 | 708 | f"`{next_pending}` by creating {inferred_label}." |
| 707 | 709 | ] |
| 708 | - lines.append( | |
| 709 | - f"Prefer one `write(content=...)` call for `{inferred_pending_target}` before more research." | |
| 710 | - ) | |
| 711 | - lines.append( | |
| 712 | - self._mutation_tool_scaffold( | |
| 713 | - inferred_pending_target, | |
| 714 | - tool_name="write", | |
| 710 | + if inferred_is_directory: | |
| 711 | + lines.append( | |
| 712 | + f"Prefer one concrete directory-creation step for `{inferred_pending_target}` before more research." | |
| 713 | + ) | |
| 714 | + lines.append( | |
| 715 | + self._directory_creation_scaffold(inferred_pending_target) | |
| 716 | + ) | |
| 717 | + else: | |
| 718 | + lines.append( | |
| 719 | + f"Prefer one `write(content=...)` call for `{inferred_pending_target}` before more research." | |
| 720 | + ) | |
| 721 | + lines.append( | |
| 722 | + self._mutation_tool_scaffold( | |
| 723 | + inferred_pending_target, | |
| 724 | + tool_name="write", | |
| 725 | + ) | |
| 715 | 726 | ) |
| 716 | - ) | |
| 717 | 727 | if outline_label: |
| 718 | 728 | lines.append( |
| 719 | 729 | f"Use the existing outline label `{outline_label}` for that file so it matches the current guide structure." |
@@ -1071,6 +1081,11 @@ class ResponseRepairer: | ||
| 1071 | 1081 | signature = f"write(file_path={normalized_path}, content=\"...\")" |
| 1072 | 1082 | return f"Emit this tool shape now: `{signature}`." |
| 1073 | 1083 | |
| 1084 | + @staticmethod | |
| 1085 | + def _directory_creation_scaffold(path: Path) -> str: | |
| 1086 | + command = f"mkdir -p {shlex.quote(str(path.expanduser().resolve(strict=False)))}" | |
| 1087 | + return f"Emit this tool shape now: `bash(command={json.dumps(command)})`." | |
| 1088 | + | |
| 1074 | 1089 | |
| 1075 | 1090 | def _todo_is_mutation_step(label: str) -> bool: |
| 1076 | 1091 | lowered = label.lower() |
tests/test_repair.pymodified@@ -326,6 +326,69 @@ def test_empty_response_retry_mentions_write_can_create_missing_parent_directori | ||
| 326 | 326 | assert "Do not restart discovery unless one specific missing fact blocks this step." in decision.retry_message |
| 327 | 327 | |
| 328 | 328 | |
| 329 | +def test_empty_response_retry_uses_directory_creation_for_setup_targets( | |
| 330 | + temp_dir: Path, | |
| 331 | +) -> None: | |
| 332 | + context = build_context( | |
| 333 | + temp_dir=temp_dir, | |
| 334 | + use_react=False, | |
| 335 | + ) | |
| 336 | + repairer = ResponseRepairer(context) | |
| 337 | + | |
| 338 | + guide_root = temp_dir / "guides" / "nginx" | |
| 339 | + chapters_path = guide_root / "chapters" | |
| 340 | + index_path = guide_root / "index.html" | |
| 341 | + | |
| 342 | + implementation_plan = temp_dir / "implementation.md" | |
| 343 | + implementation_plan.write_text( | |
| 344 | + "\n".join( | |
| 345 | + [ | |
| 346 | + "# Implementation Plan", | |
| 347 | + "", | |
| 348 | + "## File Changes", | |
| 349 | + f"- `{chapters_path}/`", | |
| 350 | + f"- `{index_path}`", | |
| 351 | + "", | |
| 352 | + ] | |
| 353 | + ) | |
| 354 | + ) | |
| 355 | + | |
| 356 | + dod = create_definition_of_done("Create a multi-file nginx guide.") | |
| 357 | + dod.implementation_plan = str(implementation_plan) | |
| 358 | + dod.pending_items.extend( | |
| 359 | + [ | |
| 360 | + "Create the nginx directory structure", | |
| 361 | + "Create the main index.html file for nginx guide", | |
| 362 | + ] | |
| 363 | + ) | |
| 364 | + | |
| 365 | + decision = repairer.handle_empty_response( | |
| 366 | + task="Create a multi-file nginx guide.", | |
| 367 | + original_task=None, | |
| 368 | + empty_retry_count=1, | |
| 369 | + max_empty_retries=2, | |
| 370 | + dod=dod, | |
| 371 | + ) | |
| 372 | + | |
| 373 | + assert decision.should_continue is True | |
| 374 | + assert decision.retry_message is not None | |
| 375 | + assert ( | |
| 376 | + "Resume with this exact next step: continue `Create the nginx directory structure` " | |
| 377 | + "by creating `chapters/`." | |
| 378 | + in decision.retry_message | |
| 379 | + ) | |
| 380 | + assert ( | |
| 381 | + f"Prefer one concrete directory-creation step for `{chapters_path}` before more research." | |
| 382 | + in decision.retry_message | |
| 383 | + ) | |
| 384 | + expected_command = f"mkdir -p {chapters_path.resolve(strict=False)}" | |
| 385 | + assert ( | |
| 386 | + f'Emit this tool shape now: `bash(command="{expected_command}")`.' | |
| 387 | + in decision.retry_message | |
| 388 | + ) | |
| 389 | + assert f'write(file_path="{chapters_path.resolve(strict=False)}"' not in decision.retry_message | |
| 390 | + | |
| 391 | + | |
| 329 | 392 | def test_empty_response_retry_recovers_blocked_empty_file_path_to_concrete_target( |
| 330 | 393 | temp_dir: Path, |
| 331 | 394 | ) -> None: |