tenseleyflow/loader / bea3236

Browse files

Sanitize concatenated verification commands

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
bea32360e48caddbb7ba6e8654b09c6907fb8678
Parents
c0bdb3d
Tree
6583ee7

2 changed files

StatusFile+-
M src/loader/runtime/dod.py 38 7
M tests/test_dod.py 41 0
src/loader/runtime/dod.pymodified
@@ -408,16 +408,47 @@ def sanitize_verification_commands(
408408
         normalized = str(command or "").strip()
409409
         if not normalized:
410410
             continue
411
-        detected_paths = _extract_verification_command_paths(normalized)
412
-        if detected_paths and any(
413
-            not _path_is_within_allowed_verification_roots(path, allowed_roots)
414
-            for path in detected_paths
415
-        ):
416
-            continue
417
-        _append_unique(sanitized, normalized)
411
+        for candidate in _split_concatenated_verification_command(normalized):
412
+            candidate = _normalize_directory_test_command(candidate)
413
+            detected_paths = _extract_verification_command_paths(candidate)
414
+            if detected_paths and any(
415
+                not _path_is_within_allowed_verification_roots(path, allowed_roots)
416
+                for path in detected_paths
417
+            ):
418
+                continue
419
+            _append_unique(sanitized, candidate)
418420
     return sanitized
419421
 
420422
 
423
+def _split_concatenated_verification_command(command: str) -> list[str]:
424
+    """Split common accidental command concatenations from model-authored plans."""
425
+
426
+    if not command.strip():
427
+        return []
428
+    if len(re.findall(r"(?<!\S)(?:ls|test)\s+", command)) < 2:
429
+        return [command]
430
+    pieces = [
431
+        piece.strip()
432
+        for piece in re.split(r"\s+(?=(?:ls|test)\s+)", command)
433
+        if piece.strip()
434
+    ]
435
+    return pieces or [command]
436
+
437
+
438
+def _normalize_directory_test_command(command: str) -> str:
439
+    try:
440
+        tokens = shlex.split(command)
441
+    except ValueError:
442
+        return command
443
+    if len(tokens) != 3 or tokens[0] != "test" or tokens[1] != "-f":
444
+        return command
445
+    target = tokens[2]
446
+    target_path = Path(target).expanduser()
447
+    if target.endswith("/") or target_path.is_dir():
448
+        return f"test -d {shlex.quote(target)}"
449
+    return command
450
+
451
+
421452
 def build_verification_summary(evidence: list[VerificationEvidence]) -> str:
422453
     """Create a short evidence summary for the final user response."""
423454
 
tests/test_dod.pymodified
@@ -17,6 +17,7 @@ from loader.runtime.dod import (
1717
     determine_task_size,
1818
     ensure_active_verification_attempt,
1919
     record_successful_tool_call,
20
+    sanitize_verification_commands,
2021
 )
2122
 
2223
 
@@ -376,6 +377,46 @@ def test_derive_verification_commands_flags_insufficient_pages_for_broad_thoroug
376377
     assert any("insufficient HTML page count" in command for command in commands)
377378
 
378379
 
380
+def test_sanitize_verification_commands_splits_concatenated_ls_and_directory_test(
381
+    tmp_path: Path,
382
+) -> None:
383
+    guide = tmp_path / "guides" / "nginx"
384
+    chapters = guide / "chapters"
385
+    chapters.mkdir(parents=True)
386
+    index = guide / "index.html"
387
+    index.write_text("<html></html>\n")
388
+    implementation_plan = tmp_path / "implementation.md"
389
+    implementation_plan.write_text(
390
+        "\n".join(
391
+            [
392
+                "# Implementation Plan",
393
+                "",
394
+                "## File Changes",
395
+                f"- `{index}`",
396
+                f"- `{chapters}/`",
397
+                "",
398
+            ]
399
+        )
400
+    )
401
+    dod = create_definition_of_done("Create a multi-page HTML guide.")
402
+    dod.implementation_plan = str(implementation_plan)
403
+
404
+    commands = sanitize_verification_commands(
405
+        [
406
+            f"ls -la {guide}/ ls -la {chapters}/",
407
+            f"test -f {chapters}/",
408
+        ],
409
+        dod=dod,
410
+        project_root=tmp_path,
411
+    )
412
+
413
+    assert commands == [
414
+        f"ls -la {guide}/",
415
+        f"ls -la {chapters}/",
416
+        f"test -d {chapters}/",
417
+    ]
418
+
419
+
379420
 def test_collect_planned_artifact_targets_ignores_prose_path_fragments_in_refreshed_plan(
380421
     tmp_path: Path,
381422
 ) -> None: