Files
project-work/worker-toolkit-potion-polyglot/scripts/validate_task_dir.py
Eric Bell e55ccea018 Loaded up for the 3rd redo
Still on potion-voice
2026-09-26 14:57:10 -04:00

89 lines
3.5 KiB
Python
Executable File

#!/usr/bin/env python3
"""validate_task_dir.py — say WHY harbor will not accept a task directory.
``harbor run -p <dir>`` silently reinterprets a directory that fails task
validation as a *dataset* of tasks, finds none inside, and dies with
``ValueError: Either datasets or tasks must be provided.`` — a message naming
neither the path nor the missing file. ``scripts/harbor-run`` calls this first so
the author reads "tests/test.sh is missing" instead.
Must run under HARBOR'S interpreter (its uv-tool venv), not any python3.11+: it
imports harbor to reuse ``Task.is_valid_dir``, the exact predicate the CLI
branches on, so the two cannot drift.
Usage: validate_task_dir.py <task-dir> [--disable-verification]
Prints ``verdict=valid`` or ``verdict=invalid`` on stdout; the reason goes to
stderr. Callers must gate on the stdout verdict, never on the exit code alone —
an interpreter that cannot run this file at all also exits non-zero.
Exit 0 = valid, 1 = invalid, 2 = the check could not run.
"""
import sys
from pathlib import Path
def reason(task_dir: Path, disable_verification: bool) -> str | None:
"""Return why harbor rejects task_dir, or None if it accepts it."""
from harbor.models.task.config import TaskConfig
from harbor.models.task.paths import TaskPaths
from harbor.models.task.task import Task
if Task.is_valid_dir(task_dir, disable_verification=disable_verification):
return None
paths = TaskPaths(task_dir)
if not paths.config_path.exists():
return f"{paths.config_path} is missing."
if not paths.environment_dir.exists():
return f"{paths.environment_dir} is missing."
try:
config = TaskConfig.model_validate_toml(paths.config_path.read_text())
except Exception as exc:
return f"{paths.config_path} does not parse as a task config: {exc}"
# A stepped task carries no root instruction.md, so only the shape harbor
# checks may be asserted here — hence steps first, root instruction last.
if disable_verification:
for step in config.steps or []:
if not paths.step_dir(step.name).exists():
return f"{paths.step_dir(step.name)} is missing."
if not paths.step_instruction_path(step.name).exists():
return f"{paths.step_instruction_path(step.name)} is missing."
if not config.steps and not paths.instruction_path.exists():
return f"{paths.instruction_path} is missing."
else:
# Private, but it owns the instruction/test diagnostics is_valid_dir discards.
try:
Task._validate_tests(config, paths)
except FileNotFoundError as exc:
return str(exc)
except AttributeError:
pass
return f"{task_dir} is not a task directory harbor recognizes."
def main() -> int:
args = sys.argv[1:]
disable_verification = "--disable-verification" in args
positional = [a for a in args if not a.startswith("-")]
if len(positional) != 1:
print(
f"usage: {sys.argv[0]} <task-dir> [--disable-verification]", file=sys.stderr
)
return 2
try:
why = reason(Path(positional[0]), disable_verification)
except Exception as exc:
print(f"validate_task_dir: check did not run ({exc})", file=sys.stderr)
return 2
if why is None:
print("verdict=valid")
return 0
print("verdict=invalid")
print(why, file=sys.stderr)
return 1
if __name__ == "__main__":
sys.exit(main())