Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions docs/v6/reference/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -110,19 +110,24 @@ For a platform taskset, pass its name or id directly: `hud eval "My Tasks" claud

## Run a packaged image

`hud task start` / `hud task grade` attach to an env already serving locally (e.g. inside a built image, or alongside `hud serve`), or load one from source with `--source`. `hud task list` always reads from source (default `.`) - it doesn't attach.
`hud task start` / `hud task grade` attach to an env already serving locally (e.g. inside a built image, or alongside `hud serve`), or load one from source with `--source`. Both verify that the live environment exposes the requested task before running it.

```bash
hud task list # what tasks are exposed
hud task list --env browser-v2 # boot a deployed env and read its live manifest
hud task start fix_bug # -> the prompt (stdout)
hud task grade fix_bug --answer "..." # -> the reward (stdout)
hud task grade fix_bug --dry-run # start + empty-answer grade, no agent rollout
hud task grade task/fix_bug --env env/browser-v2 --dry-run # check a deployed env
```

`--dry-run` reports the environment, task, grader, and reward-shape phases separately. A valid low reward passes: the check verifies that grading works and returns finite values in the supported range, not that an empty answer solves the task.

| Command | Key options |
|---------|-------------|
| `hud task start <task>` | `--source`/`-s`, `--args` (JSON), `--url`/`-u`, `--out`/`-o` |
| `hud task grade <task>` | `--answer`, `--answer-file`, `--source`, `--args`, `--url`, `--out` |
| `hud task list` | `--source`/`-s` |
| `hud task start <task>` | `--source`/`-s`, `--args` (JSON), `--url`/`-u`, `--env`, `--out`/`-o` |
| `hud task grade <task>` | `--answer`, `--answer-file`, `--dry-run`, `--timeout`, `--source`, `--args`, `--url`, `--env`, `--out` |
| `hud task list` | `--source`/`-s`, `--env`, `--url`/`-u` |

## Platform

Expand All @@ -131,6 +136,8 @@ hud sync tasks my-taskset # publish tasks as a named taskset
hud sync env # sync environment metadata
```

Before uploading tasks, `hud sync tasks` rejects macOS archive metadata, broken links, and references to deployed environments or task ids that do not exist. `hud deploy` runs the same source-fixture checks before building.

External benchmark formats can be adapted into runnable `Taskset`s through the
experimental [Harbor integration](/v6/experimental/harbor).

Expand Down
45 changes: 1 addition & 44 deletions hud/cli/eval.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

from __future__ import annotations

import ast
import asyncio
import logging
import os
Expand All @@ -23,6 +22,7 @@
from rich import box
from rich.table import Table

from hud.cli.task_runtime import spawn_target as _spawn_target
from hud.cli.utils.api import require_api_key
from hud.cli.utils.config import parse_key_value
from hud.settings import settings
Expand Down Expand Up @@ -686,49 +686,6 @@ def _build_agent(cfg: EvalConfig) -> Any:
return cast("Any", cfg.agent_type.cls)(config=config)


def _python_defines_environment(path: Path) -> bool:
"""Return True when ``path`` constructs a v6 :class:`~hud.environment.Environment`."""
try:
tree = ast.parse(path.read_text(encoding="utf-8"))
except (OSError, SyntaxError):
return False
for node in ast.walk(tree):
if not isinstance(node, ast.Call):
continue
callee = node.func
callee_name = (
callee.id
if isinstance(callee, ast.Name)
else callee.attr
if isinstance(callee, ast.Attribute)
else None
)
if callee_name == "Environment":
return True
return False


def _spawn_target(source: Path) -> Path:
"""The path the ``SubprocessRuntime`` provider serves.

Directories and env-defining ``.py`` files are served as-is. Task-only
sources (``tasks.py`` importing from ``env.py``) resolve to a sibling
``env.py`` or the containing directory. JSON/JSONL data files use the
surrounding directory (the env source lives next to the tasks file).
"""
resolved = source.resolve()
if resolved.is_dir():
return resolved
if resolved.suffix != ".py":
return resolved.parent
if _python_defines_environment(resolved):
return resolved
env_py = resolved.parent / "env.py"
if env_py.is_file():
return env_py
return resolved.parent


def _resolve_placement(cfg: EvalConfig, source_path: Path | None, taskset: Any) -> Any:
"""Map the config's ``runtime`` onto a placement for ``Taskset.run``.

Expand Down
81 changes: 81 additions & 0 deletions hud/cli/sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,85 @@ def _warn_on_linked_environment_mismatch(
)


def _resolve_registry_environment_detail(
platform: PlatformClient,
ref: str,
) -> tuple[RegistryEnvironment | None, str | None]:
matches = [
env
for env in resolve_registry_environments(platform, ref)
if env.name == ref or env.id == ref
]
if not matches:
return None, f"environment {ref!r} is not deployed"
if len(matches) > 1:
return None, f"environment name {ref!r} is ambiguous"
registry_env = get_registry_environment(platform, matches[0].id)
if registry_env is None:
return None, f"environment {ref!r} is not deployed"
return registry_env, None


def _validate_task_manifests(
taskset: Taskset,
platform: PlatformClient,
console: HUDConsole,
) -> None:
"""Fail before upload when a deployed environment cannot expose a task."""
errors: list[str] = []
for env_name in sorted(taskset.environment_names()):
try:
registry_env, error = _resolve_registry_environment_detail(platform, env_name)
except (HudException, ValueError) as exc:
errors.append(f"could not validate environment {env_name!r}: {exc}")
continue
if error is not None:
errors.append(error)
continue
assert registry_env is not None
manifest = registry_env.manifest
raw_tasks = manifest.get("tasks") if manifest is not None else None
if not isinstance(raw_tasks, list):
errors.append(f"environment {env_name!r} has no successful build manifest")
continue
exposed = {
task["id"]
for task in raw_tasks
if isinstance(task, dict) and isinstance(task.get("id"), str)
}
required = {task.id for task in taskset if task.env == env_name}
required.update(
task.verifier.id
for task in taskset
if task.verifier is not None and task.verifier.env == env_name
)
missing = sorted(required - exposed)
if missing:
errors.append(f"environment {env_name!r} does not expose task(s): {', '.join(missing)}")

if errors:
console.error("Task validation failed:")
for error in errors:
console.error(f" {error}")
console.hint("Deploy the environment or fix the task ids before syncing.")
raise typer.Exit(1)
console.success("Task environment manifests validated")


def _validate_source_fixtures(source: str, console: HUDConsole) -> None:
errors = [
issue
for issue in EnvironmentSource.open(source).validate_fixture_quality()
if issue.severity == "error"
]
if not errors:
return
console.error("Source fixture validation failed:")
for issue in errors:
console.error(f" {issue.message} ({issue.file})")
raise typer.Exit(1)


def _fetch_remote_taskset(
platform: PlatformClient,
target_ref: str,
Expand Down Expand Up @@ -310,6 +389,8 @@ def sync_tasks_command(
exclude=exclude,
console=hud_console,
)
_validate_source_fixtures(source, hud_console)
_validate_task_manifests(local_taskset, platform, hud_console)
_warn_on_linked_environment_mismatch(local_taskset, platform, hud_console)

# Creating a new taskset is only allowed when targeting an explicit name
Expand Down
Loading
Loading