Skip to content

CLI

Part of the OpenRAL public-symbol inventory. Hand-curated; (LNN) markers are refreshed by tools/refresh_methods_linenos.py.

python/cli/src/openral_cli/main.py

openral CLI entry point — openral command (typer-based).

  • _root(ctx) — Top-level callback. Picks the trace sample_ratio from _SAMPLE_RATIO_BY_MODE (hardware → 0.1, others → None → ALWAYS_ON), calls configure_observability(service_name="ral", sample_ratio=...), and opens a cli.command root span via cli_command_span(ctx.invoked_subcommand, mode=…), registered on the click Context so the span closes after the subcommand returns. Mode is derived from _RUN_MODE_BY_SUBCOMMAND (sim → sim, benchmark → benchmark, deploy/connect → hardware). (L377)
  • _RUN_MODE_BY_SUBCOMMAND: dict[str, str] — Subcommand → openral.run.mode mapping consumed by _root. (L359)
  • _SAMPLE_RATIO_BY_MODE: dict[str, float]openral.run.mode → trace sample ratio. Hardware: 0.1 per ADR-0010 (2026-05-17 amendment). (L371)
  • class CheckResult(NamedTuple) — One row in openral doctor output. (L408) fields: check, status, details
  • _check_python() -> CheckResult (L425)
  • _check_platform() -> CheckResult (L430)
  • _check_openral_core() -> CheckResult (L434)
  • _check_ros2() -> list[CheckResult] — ROS 2 binary, distro, RMW. (L442)
  • _check_colcon() -> CheckResult (L483)
  • _check_gpu() -> list[CheckResult] — One row per GPU. (L488)
  • _check_usb() -> list[CheckResult] — Candidate robot USB serial devices. (L527)
  • _check_just() -> CheckResult (L545)
  • _check_reasoner_llm() -> list[CheckResult] — Returns a Reasoner LLM summary row plus follow-up rows naming each missing variable (MODEL / API_KEY) when PROVIDER is set but the config is incomplete. TCP-probes the Ollama / local endpoint when BASE_URL is loopback and emits an Ollama row with the just bootstrap-ollama remediation hint when unreachable. Never prints the API key value (only set / unset). Pure env read, no SDK import, no cloud round-trip. (L582)
  • _REASONER_PROVIDERS_REQUIRING_KEY: frozenset[str]{anthropic, openrouter}; bare openai-compatible is the exception because a local Ollama / llama-server doesn't enforce auth. (L556)
  • _REASONER_PROVIDER_DEFAULT_BASE_URL: dict[str, str] — Provider → default base URL surfaced in the doctor summary when BASE_URL isn't set. Kept local to main.py to avoid forcing the CLI to import openral_reasoner on every invocation. (L562)
  • _is_local_base_url(url) -> bool — True when host resolves to a loopback name. (L569)
  • _probe_tcp(host, port, *, timeout_s=0.2) -> bool — Fast non-blocking TCP probe used to diagnose the Ollama daemon. (L575)
  • _gather_checks() -> list[CheckResult] (L691)
  • doctor(--json) — Diagnose host: Python, OS, ROS 2, GPU, USB. Delegates GPU enumeration to probe_gpus. (L714)
  • detect(--output, --report, --dds-timeout, --include, --no-write, --yes) — Probe + assemble + write a complete RobotDescription robot.yaml. (replaces the old ral init)
  • _render_detection_summary(detection) — Print per-probe Rich table for openral detect.
  • connect(--robot, --port) — Open HAL, read state, disconnect.
  • _connect_so100(port: str) -> None
  • calibrate_camera(--sensor, --topic, --chessboard-size, --square-size, --dry-run) — Run ROS 2 camera_calibration.
  • skill_install(HUB_ID, --revision, --force, --non-commercial, --yes) — Download rSkill, validate, register. An org-less HUB_ID (no /) fails fast with an OpenRAL/<name> suggestion + rskill search hint instead of a raw Hub 404; a 404 on a qualified id appends the same search hint.
  • rskill_search(QUERY?, --kind, --role, --embodiment, --license, --limit, --json) — Search the OpenRAL HF Hub org (HfApi.list_models(author="OpenRAL", search=QUERY)) for installable rSkills (ADR-0055 D4). Each hit's rskill.yaml is fetched + validated; repos without a valid manifest are skipped (count surfaced), survivors filtered client-side by the facet flags and rendered as a paste-able repo_id table.
  • _load_hub_rskill_manifest(repo_id) -> RSkillManifest | None — Fetch + validate one Hub repo's rskill.yaml; None (skip) when absent or invalid.
  • _rskill_matches_filters(m, *, kind, role, embodiment, license_) -> bool — Whether a manifest passes every non-empty rskill search facet filter.
  • _render_rskill_search_results(rows, skipped, query) -> None — Print the rskill search table or the no-results notice.
  • skill_list(--json) — List installed rSkills.
  • skill_check(rskill_id?, --robot, --rskills-dir, --json) — Two modes. With a positional id, resolves it via load_rskill_manifest and renders a per-section breakdown via check_single_rskill. Without an id, falls back to the legacy walk-all path (check_installed_rskills). --rskills-dir defaults to rskills/ and is silently skipped when the directory does not exist. Exits 1 on any blocking failure.
  • rskill_new(ID, --out-dir, --owner, --license, --embodiment-tag, --family, --from-hf, --yes, --overwrite) — Scaffold a new local rSkill from rskills/template/ via _rskill_scaffolder.scaffold_rskill. Three modes: (1) --from-hf <repo> introspects the Hub config to auto-fill policy_id / chunk_size / sensors / state_contract / aliases / weights_uri; (2) --family <act|smolvla|pi05|xvla|diffusion> overlays family-aware defaults; (3) interactive prompts for any missing flag (skipped under --yes). (L1616)
  • _resolve_or_prompt(value, *, prompt, default, skip_prompt) -> str — Drives the owner / license / embodiment prompts only when the flag was not provided and --yes is off.
  • _resolve_family_and_patch(*, family, from_hf, yes) -> tuple[RSkillFamily | None, RSkillPatch | None] — Resolves --family / --from-hf into a family + manifest patch for scaffold_rskill. Prompts for family in interactive mode; bails non-zero with a clear message when --from-hf introspection fails or --family is unrecognized.
  • _display_license_banner(name, license_value, version, con) -> None
  • sensor_list(--vendor, --modality, --kind, --json) — List entries in sensor catalog. (L1912)
  • sensor_show(SENSOR_ID, --name, --parent-frame, --json) — Resolve catalog entry to a SensorSpec/SensorBundle. (L2003)
  • benchmark_report(--rskills-dir, --json) — Aggregate rskills/*/eval/*.json benchmark blocks into a rich-table or JSON dump. Validates every JSON against RSkillEvalResult. --rskills-dir defaults to rskills/. (L2630)
  • benchmark_run(--suite, --rskill, --out, --device, --save-dir, --benchmarks-dir, --n-episodes, --dry-run, --update-manifest/--no-update-manifest, --dashboard, --dashboard-port) — Resolve --suite (built-in id or path) to a bare list[BenchmarkScene] via openral_core.load_benchmark_suite + raise_on_invalid_suite (ADR-0042), parse the rSkill reference from --rskill, dispatch to openral_sim.run_benchmark(scenes, vla, suite_id=<id>), and write a validated RSkillEvalResult JSON. --n-episodes overrides every BenchmarkScene.n_episodes in the suite for smoke runs (mirrors benchmark scene --n-episodes). Default output path is rskills/<rskill-dir>/eval/<suite_id>.json. With --update-manifest (default on), also writes avg_success_rate back into the manifest's benchmarks.<suite_id> field via update_rskill_benchmarks. ADR-0009 PR D. (L2096)
  • benchmark_scene(--config, --rskill, --out, --device, --save-dir, --n-episodes, --view/--no-view, --dry-run, --update-manifest/--no-update-manifest, --dashboard, --dashboard-port) — Single-scene sibling of benchmark_run (ADR-0009 + scene-hierarchy refactor). --view/--no-view (default unset = headless) mirrors sim run --view for parity — opens a live mujoco.viewer per episode; threaded into run_benchmark_scene(view=…). Strictly accepts a BenchmarkScene YAML via load_scene_strict (DeployScene/SimScene rejected with a redirect), optionally overrides n_episodes for smoke runs, dispatches to openral_sim.run_benchmark_scene, writes rskills/<dir>/eval/scene_<scene_id>.json, and surgically updates the rSkill manifest's benchmarks.<scene_id> field. --dry-run short-circuits before rSkill resolution so it never touches the Hub. (L2369)
  • _default_benchmark_scene_out_path(vla_spec, scene) -> Path — Mirrors _default_benchmark_out_path but for single-scene JSONs; the scene_ prefix distinguishes per-scene outputs from multi-task suite outputs under the same rSkill directory. (L2613)
  • deploy(--config, --max-ticks, --rate-hz) — Sibling of openral sim run for hardware deployments (ADR-0010 PR G). Loads a RobotEnvironment YAML, calls openral_runner.build_runner, drives the skill lifecycle (configure / activate / deactivate / shutdown) around runner.activate / run / deactivate, prints a Rich summary table (n_ticks, mean / p99 timings, budget violations, trace id). CLI overrides for --max-ticks and --rate-hz. Heavy runtime deps (lerobot, opencv) are deferred so openral --help stays sub-second.
  • deploy sim(--config, --robot, --dashboard-port, --foxglove/--no-foxglove, --foxglove-port, --reset-to-pose-service, --hal, --dry-run) — Boot the full ROS graph (dashboard + C++ safety_kernel + reasoner + prompt_router + runtime + HAL) against a digital-twin HAL by sharding ros2 launch openral_rskill_ros sim_e2e.launch.py (one generic launch — no per-robot launch files). --foxglove (default off, ADR-0059) also spawns the read-only foxglove_bridge live-scene surface on ws://127.0.0.1:<foxglove-port> (view-only — cannot actuate; see packages/openral_foxglove_bringup). Loads a DeployScene YAML (mirrors openral sim run --config); picks the HAL package/executable/node-name from _ROBOT_HAL_REGISTRY[robot_id]; asserts the registered HAL's supported_robot_names matches the manifest's name field (mismatch fails loud at resolution time). No envelope YAML is written or read: the launch's OpaqueFunction loads robot.yaml, calls openral_safety.envelope_loader.compute_intersection(robot, skill=None), and forwards each EnvelopeIntersection field as a ROS parameter on the kernel node (the C++ kernel grew a parameter-based loader in ADR-0020 PR-K alongside the legacy file path). --robot overrides the YAML's robot_id. --hal key=value (repeatable) overrides per-robot HAL defaults (JSON-parsed where possible). No --rskill flag: the reasoner picks the active rSkill dynamically from the in-tree rskills/ palette at on_configure. --dry-run prints the resolved argv without writing the HAL params temp file. Defined in openral_cli.deploy_sim.
  • dashboard(--host, --port, --log-level, --inprocess)openral dashboard (ADR-0017, closes #44). Boot a live debug pane that doubles as an OTLP/HTTP receiver on the same port; lazy-imports openral_observability.dashboard.run_dashboard so openral --help stays sub-second. --inprocess takes a single shell-quoted string (shlex-tokenised) and spawns it as a child workload with OTEL_EXPORTER_OTLP_ENDPOINT + OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf pre-set so a one-keystroke demo doesn't need a second shell. Defaults: bind 127.0.0.1:4318 (OTLP/HTTP standard; was 8000 until issue #132), uvicorn at warning. Works without Jaeger/Tempo (the dashboard is its own receiver). Inverse path: openral sim run --dashboard (sim spawns the dashboard).
  • replay(BAG, --trace, --frame, --dataset-root, --dashboard, --out) — ADR-0018 F7 + ISSUE-109. Read a .mcap file or rosbag2 directory, join with OTel spans from --dashboard (http://host:port), and emit a chronological JSON timeline keyed by trace_id. --frame <repo_id>/<episode>/<frame> (with --dataset-root) pivots from a written LeRobotDataset frame: it resolves that frame's trace_id via openral_dataset.read_frame_trace and uses it as the join key (mutually exclusive with --trace). --out writes to a file; otherwise prints to stdout. Bag-only when --dashboard is omitted.
  • _resolve_frame_trace_id(frame_spec, dataset_root) -> str — Parse a <repo_id>/<episode>/<frame> spec (rsplit('/', 2); repo_id keeps its own slash) and return that frame's stored trace_id via read_frame_trace. typer.Exit(2) on a malformed spec, missing frame, or a frame with no trace.
  • record(--out, --profile, --storage, --extra-topic, --extra-regex, --dry-run) — ADR-0018 F7. Spawn ros2 bag record with slim (default) or full profile presets; --dry-run prints the composed argv without forking.
  • profile session ACTION (start | stop | view) (--output, --name) — ADR-0018 F9. Drive an LTTng session for the realtime hot path; surfaces LttngSessionError cleanly when lttng is missing on PATH. Set OPENRAL_ROS2_TRACING=1 on the agent process to emit tracepoints; without the gate every tracepoint is a no-op.
  • _resolve_benchmark_suite(suite: str, benchmarks_dir: Path) -> tuple[list[BenchmarkScene], str] — Accept either a built-in id (looked up at benchmarks/<id>.yaml) or a direct YAML path; raise typer.BadParameter listing catalogue entries on a typo.
  • _parse_rskill_cli_arg(raw) — Parse --rskill <ref> into a VLASpec. Accepts bare names (smolvla-libero), paths (rskills/smolvla-libero), or HF repo ids; validates via openral_rskill.loader._validate_skill_ref so VLASpec.weights_uri rejects explicit URI schemes. The adapter id is read from the manifest's model_family. Raises typer.BadParameter on an invalid scheme or empty input.
  • _summarize_results(results: dict[str, object]) -> str — Headline-line picker for free-form results blocks (*_avg → numeric → status fallback). (L2720)
  • _path_completer(text: str, state: int) -> str | None — Stdlib readline-shaped Tab completer wired into _run_repl. Globs text* (with ~ expansion), adds trailing / to directory matches, and rewrites a leading $HOME back to ~ so a user who typed ~/foo keeps their literal tilde. Lets the REPL complete filesystem paths after sim run --config, --rskill rskills/, etc. Installed by _run_repl together with readline.set_completer_delims(" \t\n=;|&><") (shell-shaped delims so /, ., -, ~ are not word boundaries) and parse_and_bind("tab: complete") (or libedit's equivalent on macOS). Returns None past the last match per readline's state contract.
  • render_banner(version_str: str, *, width: int | None = None) -> RenderableType — Build the interactive-REPL welcome box as a rich Panel (Claude-Code style), content-sized (expand=False) and with the layout chosen to fit width: a white-bordered rounded box with OPENRAL v<version> inline (left-aligned) in the top border. Wide (>= _WIDE_MIN): two columns split by a MINIMAL-box vertical divider — left holds the logo mark beside the OPENRAL wordmark (_logo_wordmark/_identity) over the tagline + capability strip; right holds the community links (_LINKS) above a Rule above the quick-start commands (_COMMANDS). Narrow: a single stacked column keeping every section, with the logo beside the wordmark while it fits (>= _SIDE_BY_SIDE_MIN) and stacked above it below that. The box is sized to its content rather than stretched to the terminal, so it stays compact on wide terminals (already-printed output cannot reflow if the window is later dragged narrower than the box). Returns a renderable (not a print) so it exports to plain text in tests independent of TTY/colour state.
  • _logo_wordmark(*, stacked: bool) -> RenderableType — Logo mark + OPENRAL wordmark side by side (vertical-middle grid) or stacked.
  • _identity(*, stacked: bool) -> RenderableType_logo_wordmark above the tagline + capability strip.
  • _kv_grid(rows: tuple[tuple[str, str], ...], key_style: str) -> Table — Borderless two-column key value grid (styled key, dim value) used for the links and commands cells.
  • _LOGO_ART: str / _WORDMARK_ART: str — White (single-weight, no gradient) OpenRAL logo mark — a 6-row block icon (horns flaring out and down into a rounded head, eyes below) matching the wordmark height — and the OPENRAL block-letter wordmark.
  • _LINKS / _COMMANDS: tuple[tuple[str, str], ...] — Community links (Discord / GitHub / Hugging Face / Website) and quick-start commands (doctor, rskill search, help, exit) shown in the right cell.
  • _WIDE_MIN / _SIDE_BY_SIDE_MIN: int — Minimum terminal columns each content-sized layout occupies (127 / 82), measured from the rendered box so the richest layout that fits is chosen and the box never overflows.
  • _cli_version() -> str — Best-effort openral-cli package version for the banner title; suppresses PackageNotFoundError and falls back to "0.0.0".
  • _print_banner() -> None — Print render_banner(_cli_version(), width=console.width) to the REPL console at _run_repl startup, sizing to the live terminal.

python/cli/src/openral_cli/_hf_publish.py

ADR-0019 PR5 — shared HF Hub publishing helpers, de-duped from tools/rskill_publisher.py.

  • resolve_token(token_arg: str | None = None) -> str — Resolve the HF token from arg → HF_TOKENHUGGINGFACE_HUB_TOKEN. Raises ROSConfigError with actionable hint when missing. (L55)
  • ensure_private(api: HfApi, repo_id: str, *, repo_type: str = "model") -> None — Re-fetch repo metadata and raise ROSConfigError if the repo is public. Critical safety gate; repo_type supports "model" / "dataset" / "space". (L88)
  • IGNORE_PATTERNS: Final[list[str]] — Glob patterns excluded from every upload (.env, __pycache__, *.key, etc.). (L43)

python/cli/src/openral_cli/_rskill_doc_validator.py

rSkill README + manifest publish-readiness validator (CLAUDE.md §6.4 publish gate).

Hard gate consumed by tools/rskill_publisher.py (and printed in dry-run mode): refuses publish when the README is missing / too short / missing canonical sections / contains template sentinels, or when the manifest still has the template-default description, lacks both paper_url and source_repo, or carries a TEMPLATE substring in name / weights_uri / source_repo. Supports single-hop delegation via the <!-- openral:rskill-readme-delegates-to: <path> --> marker so RLDX-1-family stubs can share one canonical README.

  • class DocValidationIssue(BaseModel) — one problem record: severity ("error" / "warning"), field (readme.section.License / manifest.description / …), message.
  • class DocValidationReport(BaseModel)skill_dir + manifest_name + issues; .is_valid / .errors / .warnings derived properties.
  • validate_rskill_docs(skill_dir: Path, manifest: RSkillManifest) -> DocValidationReport — Public entry point; composes README and manifest checks into one report. (L~175)
  • _validate_readme(skill_dir) -> list[DocValidationIssue] — Presence, min-length, required-sections-via-heading, placeholder-sentinel scan. Honors single-hop delegation.
  • _resolve_delegation(skill_dir, body) -> (list[str] | None, list[DocValidationIssue]) — Resolve a openral:rskill-readme-delegates-to marker; rejects double-hop and missing-target.
  • _validate_manifest_content(manifest) -> list[DocValidationIssue] — Description / provenance (paper_urlsource_repo) / name / weights_uri / source_repo checks beyond what the Pydantic schema catches.
  • format_report(report) -> str — Human-readable summary used by the publisher dry-run.
  • Module-level constants: README_REQUIRED_SECTIONS, PLACEHOLDER_SENTINELS, PLACEHOLDER_MANIFEST_DESCRIPTION_MARKERS, DELEGATION_MARKER_NAME.

python/cli/src/openral_cli/deploy_sim.py

openral deploy sim — boot the full ROS graph against a digital-twin HAL via ros2 launch openral_rskill_ros sim_e2e.launch.py (one generic launch, no --rskill).

  • deploy_sim_command(--config, --robot, --dashboard-port, --foxglove/--no-foxglove, --foxglove-port, --reset-to-pose-service, --hal, --dry-run) — Typer callback registered under deploy sim. Forwards --foxglove/--foxglove-port (ADR-0059) into resolve_launch_invocationenable_foxglove:=/foxglove_port:= on sim_e2e.launch.py. Calls resolve_launch_invocation, runs assert_ros2_packages_discoverable on openral_rskill_ros + the resolved HAL package (catches an un-sourced overlay or stale build before ros2 launch returns its terse "Package not found" error), runs _preflight_palette_deps (advisory: warn-and-drop rSkills blocked on missing extras and boot the rest, OPENRAL_AUTO_INSTALL_DEPS=1 auto-installs, hard-fail only when the palette would be empty), writes the synthesised envelope + HAL params to two tempfile.NamedTemporaryFiles (lifetime = subprocess), substitutes their paths into the argv template, and runs ros2 launch via _run_launch with PATH prepended to .venv/bin so spawned #!/usr/bin/env python3 node shebangs resolve to the venv interpreter (the only one that processes editable .pth files). _run_launch spawns the launch in its own session (start_new_session=True) and tears it down in three escalating stages on exit so nothing orphans onto /tf_static or the GPU: (1) forward SIGINT/SIGTERM to the launch's group for ros2 launch's graceful shutdown; (2) _terminate_launch_group SIGKILLs the group after a grace; (3) _kill_orphan_openral_graph_processes sweeps by argv signature — the bulletproof backstop, since ros2 launch spawns nodes in their own process groups (and the rldx sidecar in its own session) that killpg can't reach directly. The same signature reaper runs at startup via _reap_orphans_with_log, matching _cmdline_is_openral_graph_process against _ORPHAN_GRAPH_NEEDLES (now covering the static_transform_publisher / robot_state_publisher TF chain + the rldx-sidecar, closing the stale-z=0.4 /tf_static poisoning hole that made the rldx-rc365 arm reach 40 cm high). Cleanup unlinks both temp files in finally.
  • assert_ros2_packages_discoverable(packages, *, prefix_lookup=_ros2_pkg_prefix) -> None — Raise ROSConfigError listing every pkg ros2 pkg prefix cannot resolve. Catches the most common openral deploy sim failure (operator sourced /opt/ros/jazzy/setup.bash but not the OpenRAL workspace overlay; or the local just ros2-build is stale and never installed the requested openral_hal_<X>). prefix_lookup is injectable so unit tests drive the path with a deterministic fake (process-boundary fake, CLAUDE.md §1.11). Error message names the missing packages, points at just ros2-build && source install/setup.bash, and disambiguates "is the package called rskill now?" — it is not.
  • _preflight_palette_deps(*, repo_root, robot_yaml, commercial_deployment=False) -> NoneAdvisory (not a gate) policy-extras preflight. Mirrors ReasonerNode._maybe_seed_palette_from_search_paths: globs <repo_root>/rskills/*/rskill.yaml, runs build_tool_palette against the robot's RobotCapabilities, then probes each capability-matching manifest's model_family via policy_deps.can_import_policy_family. The palette is robot-WIDE (a franka_panda config matches six model families), so a partially-installed venv is the common case and the reasoner already drops unimportable rSkills at on_configure — this mirrors that contract instead of blocking. When ≥1 matching skill is blocked: with OPENRAL_AUTO_INSTALL_DEPS=1 (the unattended-consent env var openral_sim._assets / _deps also honor) it runs just sync --all-packages --group … (union of model_family_install_groups, cwd=repo_root), re-probes, and continues — a non-zero sync surfaces its exit code; on a TTY it instead typer.confirms the same install. Otherwise (declined / non-TTY / no install cmd / partial install) it drops the blocked skills from the palette and proceeds, printing the enable hint — UNLESS that leaves the palette empty, in which case it typer.Exit(1)s with the just sync --all-packages --group … command (--all-packages is required so the workspace members survive the install — without it the next ros2 launch fails with No module named 'openral_core'). Silent return when nothing blocked, when no rSkills are installed, or when the palette is empty for capability/role/license reasons (the reasoner's domain to surface at on_configure).
  • run_launch_invocation(invocation, *, run_preflight=True) -> int — Shared shelling path for deploy sim + deploy run (ADR-0032): runs _preflight_palette_deps, writes the ephemeral HAL params YAML, exports OPENRAL_VENV_SITE + prepends the venv bin/PATH, and shells the resolved argv via _run_launch; returns the launch exit code. Defined in openral_cli.deploy_sim.
  • resolve_launch_invocation(*, config=None, robot_override, dashboard_port, reset_to_pose_service, approach_skill_id=None, hal_param_overrides=None, hal_mode="sim", enable_slam=None, enable_nav2=None, enable_octomap=None, enable_dashboard=True) -> LaunchInvocation — Pure resolver shared by deploy sim (hal_mode="sim", a DeployScene config) and deploy run (hal_mode="real", robot_override from a RobotEnvironment, config=None). ADR-0032: real mode skips the sim digital-twin / scene-attach injections (so the so100/so101 node opens its serial bus) and forwards hal_mode="real" to manifest_driven nodes, and fast-fails with ROSCapabilityMismatch when a simulation-only robot (hal.real null) is asked for real mode. Loads DeployScenerobot_id only when config is given; otherwise robot_override is required. Looks up robots/<robot_id>/robot.yaml and validates it via RobotDescription.validate_for_e2e_pipeline(), asserts the loaded manifest's name is in _ROBOT_HAL_REGISTRY[robot_id].supported_robot_names (mismatch raises ROSConfigError), merges per-robot HAL defaults with operator overrides, and builds the ["ros2", "launch", "openral_rskill_ros", "sim_e2e.launch.py", …] argv template. enable_slam auto-on tracks description.capabilities.has_lidar; enable_nav2 auto-on tracks enable_slam (ADR-0025 amendment: lidar-equipped mobile robots auto-co-enable both); enable_octomap (ADR-0030) auto-on when the manifest declares a depth SensorSpec — forwards enable_octomap:=true, which brings up octomap_server + the openral_octomap_bridge and turns on the kernel's capsule-vs-voxel world-collision check. The template carries a HAL_PARAMS_FILE_PLACEHOLDER sentinel the dispatcher fills in after writing the temp HAL params YAML. No envelope file path exists — the launch reads robot_yaml and feeds the kernel via ROS params.
  • _parse_hal_overrides(raw: list[str] | None) -> dict[str, object] — Parse repeated --hal key=value flags. Values are JSON-decoded where possible (so --hal viewer_enabled=false parses as bool); fall back to raw string for --hal port=/dev/ttyUSB0.
  • class LaunchInvocation — Frozen dataclass: robot_id, robot_yaml, envelope_payload (the synthesised dict), hal: _HalSpec, hal_params, hal_mode (ADR-0036 — "sim"|"real"; forwarded as hal_mode:=… so the reasoner's action-mode palette gate matches the HAL the graph brings up), reset_to_pose_service, approach_skill_id (ADR-0053 — MoveIt approach rSkill URI, e.g. rskills/rskill-moveit-joints; when set the runner dispatches it retargeted at each skill's starting_pose and aborts the goal on a plan failure, vs. the best-effort snap. Empty default = legacy snap; forwarded as approach_skill_id:=… only when non-empty), argv_template. Object-detector fields: enable_object_detector, object_detector_onnx, object_detector_manifest (ADR-0037 2026-06-09 — a kind:detector manifest path; runtime:pytorch selects the LocateAnything VLM sidecar and auto-enables the leg with no ONNX file), object_detector_query (initial open-vocab query), object_detector_locators (ADR-0056 — tuple of resolved on-demand locator manifest paths; the launch builds one namespaced /openral/perception/<alias>/locate_in_view lifecycle node per entry; forwarded comma-joined as object_detector_locators:=… only when non-empty). resolve_launch_invocation gains object_detector_onnx/object_detector_manifest/object_detector_query/object_detector_locators kwargs forwarded as object_detector_*:=… launch args. The object-detection leg is on by default (--object-detector/--no-object-detector, default on); when no explicit --object-detector-manifest/--object-detector-onnx is given the default backend resolves to the open-vocab omdet-turbo-indoor manifest via _omdet_runtime_available(), gracefully falling back to the in-tree RT-DETR COCO ONNX when the omdet deps are absent, and auto-downgrading the leg to off (with a console notice) when neither backend is available. On-demand locators default to omdet-turbo-locator (when omdet deps import) and accept a repeatable --object-detector-locator <manifest|alias> (LocateAnything opt-in). openral deploy sim exposes --object-detector-manifest / --object-detector-query / --object-detector-locator. Reward-monitor (ADR-0057): resolve_launch_invocation also gains enable_reward_monitor/reward_monitor_manifest/reward_monitor_task, forwarded as enable_reward_monitor:=… (+ the optional overrides only when set); openral deploy sim exposes --enable-reward-monitor/--no-enable-reward-monitor / --reward-monitor-manifest (a kind:reward YAML; weights_uri may be hf://org/repo or local:///abs/dir) / --reward-monitor-task. When on, the launch brings up reward_monitor_node parallel to the VLA and sets the reasoner's task_progress_available:=true.
  • _omdet_runtime_available() -> bool — Probe (importlib.util.find_spec for transformers + timm) deciding whether resolve_launch_invocation's default object detector is the open-vocab omdet-turbo-indoor continuous backend or the in-tree RT-DETR COCO ONNX fallback. Patched in unit tests to exercise both branches deterministically. Defined in openral_cli.deploy_sim.
  • class _HalSpec — Frozen dataclass with package, executable, node_name, default_params, and opt-in flags supports_sim_env_yaml (scene-attach injection), manifest_driven (ADR-0032 — nodes built via make_lifecycle_main_from_manifest; the resolver forwards robot_yaml + hal_mode), and bare_twin_sim (issue #191 — a manifest arm that builds its OWN sim MJCF rather than scene-attaching: so100/so101 derive a bare MujocoArmHAL twin, openarm composes a tabletop MJCF from scene_defaults.composition; suppresses the sim_env_yaml injection). Every robot is now manifest_driven — Phase 3 migrated the last two bespoke nodes (panda_mobile, openarm), so no openral_hal_* package ships a node subclass. One entry per supported robot lives in _ROBOT_HAL_REGISTRY.
  • _ROBOT_HAL_REGISTRY: dict[str, _HalSpec] — Robot-id → HAL spawn descriptor (openarm(openral_hal_openarm, lifecycle_node.py, openral_hal_openarm, {viewer_enabled, robot_lift_z, robot_forward_x, scene_white_background}), so100_follower(openral_hal_so100, lifecycle_node, openral_hal_so100, {port})). Add a new robot by extending this dict; no per-robot launch file is needed because sim_e2e.launch.py is generic.

python/cli/src/openral_cli/dataset.py

ADR-0019 PR5 — openral dataset Typer app + push subcommand.

  • dataset_app: typer.Typer — Public Typer group mounted under openral at name="dataset". (L43)
  • push_command(root, *, repo_id, yes, dry_run, token, commit_message) -> Noneopenral dataset push <root>. Reads meta/info.json, resolves the repo_id, runs the PII consent prompt (skippable via --yes or OPENRAL_DATASET_CONSENT=1), then HfApi.create_repo(private=True) → ensure_private → upload_folder. (L296)
  • from_bag_command(bag_path, *, robot, output, repo_id, license, fps) -> Noneopenral dataset from-bag <bag.mcap> --robot robots/<x>/robot.yaml --output <ds-root>. Calls Rosbag2ToLeRobotConverter.from_bag; produces a v3 dataset ready for openral dataset push. (L61)
  • _read_info_json(root: Path) -> dict[str, object] — Parse meta/info.json; raises ROSConfigError on missing / malformed file. (L187)
  • _camera_keys_from_info(info) -> list[str] — Extract observation.images.* feature keys for the consent prompt's camera disclosure. (L214)
  • _confirm_consent(repo_id, root, info, yes) -> None — Render the PII / regulatory disclosure Panel, accept --yes / env-var overrides, refuse non-TTY without override. (L222)
  • _resolve_repo_id(root, info, cli_repo_id) -> str — CLI override → info.json → error. Validates <org>/<name> format. (L268)

python/cli/src/openral_cli/collision.py

ADR-0030 — openral collision lower|check Typer app: offline URDF/SRDF → manifest self-collision model. Defers openral_safety.urdf_lowering.lower_robot (yourdfpy/trimesh) so openral --help stays fast.

  • collision_app: typer.Typer — Public Typer group mounted under openral at name="collision".
  • lower(robot, *, write, acm_only, geometry_only) -> Noneopenral collision lower --robot <yaml>. Prints a unified diff of the regenerated collision_geometry / allowed_collision_pairs block(s) and mutates the manifest only with --write (a regenerated ACM is a safety input — never silent; CLAUDE.md §3). --acm-only / --geometry-only are mutually exclusive.
  • check(robot, *, all_robots, acm_only, geometry_only) -> Noneopenral collision check (--robot <yaml> | --all). Exits 1 if any manifest drifts from its lowered model (the fleet-wide ACM drift guard).
  • splice_collision_blocks(text, *, geometry_block=None, acm_block=None) -> str — Replace only the two collision blocks in a manifest's text, preserving every other line + comment (absorbs the block's own header comment so repeated lowers stay idempotent).
  • render_blocks(model) -> tuple[str, str] — Render a LoweredCollisionModel to (geometry_block, acm_block) YAML text with a generated-provenance header; floats rounded to 4 dp for a stable diff.
  • inject_joint_fk(text, joint_fk) -> str — Inject origin_xyz/origin_rpy/axis_xyz into the named manifest joint blocks (matched by name), dropping any pre-existing FK lines. Used when onboarding a robot onto self-collision (the kernel needs joint FK to place capsules). Idempotent; preserves all other lines/comments.

python/cli/src/openral_cli/robot.py

ADR-0058 — openral robot vendor-urdf <id>: expand an upstream xacro to a flat, committed URDF so end users need no xacro tooling at runtime. Defers robot_descriptions/xacrodoc/yourdfpy inside the command so openral --help stays fast.

  • vendor_urdf(robot_id, *, upstream, out_dir, rename=None, raw_text=False) -> Path — Load upstream (rd:<robot_descriptions module> → xacro expanded via xacrodoc, or file:<path> → already-flat URDF), serialize to a flat URDF, apply rename (a (pattern, repl) pair or a sequence of them, re.sub in order; default per-robot from _RENAME / _RAW_RENAMES), and write <robot_id>.urdf with an ADR-0058 provenance header after the XML declaration. raw_text=True copies an already-flat upstream URDF's text verbatim (no yourdfpy round-trip) and applies the renames to the raw XML, preserving package:// / relative mesh paths and CRLF byte-for-byte — used for joint-name-only patches (h1 strips _joint). Returns the written path.

python/cli/src/openral_cli/_rskill_scaffolder.py

Scaffolder helper backing openral rskill new and tools/rskill_scaffolder.py. Copies rskills/template/ into a target directory, rewrites manifest sentinels (name / license / embodiment_tags / weights_uri / source_repo) plus README sentinels, then re-validates the result through RSkillManifest.from_yaml + rSkill.from_yaml so a malformed scaffold fails at scaffold-time. Partial scaffolds are cleaned up on validation failure.

  • _default_template_dir() -> Path — Walks parents of this file until a rskills/template/ directory is found. (L41)
  • scaffold_rskill(rskill_id, *, out_dir, owner, license_, embodiment_tag, family=None, patch=None, template_dir=None, overwrite=False) -> Path — Public entry point; copies the template, applies family defaults + introspection patch, rewrites placeholders, re-validates the manifest. (L60)
  • _rewrite_manifest(manifest_path, *, rskill_id, owner, license_, embodiment_tag, family, patch) -> None — Layered rewrite: (1) template baseline → (2) family defaults from _rskill_intel.family_defaults → (3) explicit patch → (4) CLI rename/license/embodiment_tags. (L173)
  • _apply_patch(raw, patch) -> None — Overlay patch keys onto the raw manifest dict; a None value removes the key (so e.g. ACT family clears min_vram_gb).
  • _rewrite_readme(readme_path, *, rskill_id, owner) -> None — Replace TEMPLATE_ORG / TEMPLATE_ID sentinels in README.md.
  • _validate_scaffold(scaffold_dir) -> None — Round-trip through RSkillManifest.from_yaml + rSkill.from_yaml.

python/cli/src/openral_cli/_rskill_intel.py

Per-family scaffold defaults + HF Hub config introspection for openral rskill new.

  • RSkillFamilyLiteral["act", "smolvla", "pi05", "xvla", "diffusion"]; mirrors the keys of openral_sim.registry.POLICIES minus the mock entries.
  • RSKILL_FAMILIES: tuple[RSkillFamily, ...] — Tuple form of the above for menu rendering / validation.
  • RSkillPatch(TypedDict, total=False) — Subset of manifest fields the scaffolder overlays: model_family, policy_id, chunk_size, quantization, latency_budget, min_vram_gb, n_action_steps, image_preprocessing, state_contract, sensors_required, weights_uri, source_repo, description.
  • family_defaults(family: RSkillFamily) -> RSkillPatch — Per-family manifest baseline mirroring the in-tree reference manifests (act-aloha, smolvla-libero, pi05-libero-nf4, xvla-libero, diffusion-pusht). (L67)
  • introspect_hf(repo_id, *, default_family=None) -> tuple[RSkillFamily, RSkillPatch] — Fetches config.json from a HF Hub repo, infers the policy family from type, and derives chunk_size / sensors / state_contract / image_preprocessing.aliases / weights_uri from input_features. (L168)
  • _fetch_hf_json(repo_id, filename) -> Anyhuggingface_hub.hf_hub_download + json.load; raises ValueError on network / parse error.
  • _sensors_from_input_features(input_features) -> list[dict] — One SensorRequirement-shaped dict per observation.images.* feature, with min_width / min_height pulled off the CHW shape.
  • _state_dim_from_input_features(input_features) -> int | None — Reads observation.state.shape[0].
  • _aliases_from_input_features(input_features) -> dict[str, str] — Pairs camera<N> source keys with the checkpoint's image-feature names; empty when names already match.

python/cli/src/openral_cli/autodetect.py

USB VID/PID enumeration and DDS topic discovery for openral detect.

  • class UsbDevice(NamedTuple) — A USB serial device on the host. (L45) fields: port, vid, pid, description
  • class KnownDevice(NamedTuple) — A known USB adapter/controller from the VID/PID table. (L61) fields: chip, driver_hint, embodiment_tag, bh_robot_type
  • class UsbMatch(NamedTuple) — A detected device matched against the table. (L79) fields: device, known
  • class DdsTopic(NamedTuple) — A ROS 2 topic observed during DDS scan. (L91) fields: name, type_name
  • _enumerate_linux_pyudev() -> list[UsbDevice] (L217)
  • _enumerate_macos_system_profiler() -> list[UsbDevice] (L250)
  • _enumerate_glob_fallback() -> list[UsbDevice]/dev/tty* glob. (L295)
  • enumerate_usb_devices() -> list[UsbDevice] — OS-routed enumerator. (L318)
  • match_known_devices(devices) -> list[UsbMatch] (L350)
  • scan_dds_topics(timeout_s=5.0) -> list[DdsTopic]ros2 topic list -t. (L377)
  • infer_robot_from_topics(topics) -> str | None (L425)