tenseleyflow/loader / 4a458e4

Browse files

Handle directory setup retries

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
4a458e44701049a8cf8c907c20a3d21ec7f8966c
Parents
3cbd1bb
Tree
12c8c4f

2 changed files

StatusFile+-
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
44
 
55
 import json
66
 import re
7
+import shlex
78
 from dataclasses import dataclass, field
89
 from pathlib import Path
910
 
@@ -691,9 +692,10 @@ class ResponseRepairer:
691692
             else None
692693
         )
693694
         if next_pending and inferred_pending_target is not None:
695
+            inferred_is_directory = not bool(inferred_pending_target.suffix)
694696
             inferred_label = self._format_artifact_label(
695697
                 inferred_pending_target,
696
-                expect_directory=False,
698
+                expect_directory=inferred_is_directory,
697699
             )
698700
             outline_label = infer_output_outline_label(
699701
                 dod,
@@ -705,15 +707,23 @@ class ResponseRepairer:
705707
                 "Resume with this exact next step: continue "
706708
                 f"`{next_pending}` by creating {inferred_label}."
707709
             ]
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
+                    )
715726
                 )
716
-            )
717727
             if outline_label:
718728
                 lines.append(
719729
                     f"Use the existing outline label `{outline_label}` for that file so it matches the current guide structure."
@@ -1071,6 +1081,11 @@ class ResponseRepairer:
10711081
             signature = f"write(file_path={normalized_path}, content=\"...\")"
10721082
         return f"Emit this tool shape now: `{signature}`."
10731083
 
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
+
10741089
 
10751090
 def _todo_is_mutation_step(label: str) -> bool:
10761091
     lowered = label.lower()
tests/test_repair.pymodified
@@ -326,6 +326,69 @@ def test_empty_response_retry_mentions_write_can_create_missing_parent_directori
326326
     assert "Do not restart discovery unless one specific missing fact blocks this step." in decision.retry_message
327327
 
328328
 
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
+
329392
 def test_empty_response_retry_recovers_blocked_empty_file_path_to_concrete_target(
330393
     temp_dir: Path,
331394
 ) -> None: