Skip to content

Rebuild Odin as an OSMO-only benchmark dispatcher - #6786

Draft
AntoineRichard wants to merge 33 commits into
isaac-sim:developfrom
AntoineRichard:antoiner/feat/odin-v2
Draft

Rebuild Odin as an OSMO-only benchmark dispatcher#6786
AntoineRichard wants to merge 33 commits into
isaac-sim:developfrom
AntoineRichard:antoiner/feat/odin-v2

Conversation

@AntoineRichard

Copy link
Copy Markdown
Collaborator

Description

Rebuilds Odin as an OSMO-only benchmark dispatcher on top of Isaac Lab's own
benchmarking infrastructure. The previous harness was 38.5k LOC across an SSH
fleet dispatcher and an OSMO dispatcher, built when Isaac Lab had no benchmark
framework of its own. Most of what it invented is now upstream, so this cuts it
to 4.1k LOC and keeps only the OSMO half.

The fleet stack is preserved untouched on antoiner/feat/odin and is not
carried over.

What Odin does now

tools/odin/ discovers tasks from the Gym registry, pins a commit into a
container image, submits one OSMO task per row, polls, and pulls results back.
Every task runs uv run isaaclab benchmark training directly — Odin owns no
runner, no schema, and no metrics code.

Changes outside tools/odin/

Three, each independently reviewable:

1. packaging cap widened to <27 (root pyproject.toml, uv.lock).
ovphysx 0.5.9 caps packaging<24 in its own wheel metadata while
isaacsim-core pins ==26.0, which made the two extras impossible to install
together and forced a [tool.uv].conflicts entry. Both caps are stricter than
the code needs. The override collapses them onto 26.0, so the
isaacsim/ovphysx conflict entry is removed. Validated at runtime, not just
at resolution: import ovphysx succeeds against packaging 26.0 with
isaacsim 6.0.0.1 installed alongside. The two published override copies
(tools/wheel_builder/, install-CI) are synced.

2. --video on the four play benchmark adapters.
PlayBundle.video_path and build_play_bundle(video_path=...) already existed,
but no adapter could populate them: the video CLI arguments lived in
add_common_train_args and wrap_record_video hardcodes a videos/train
folder with a periodic trigger. Camera tasks were therefore benchmarked
headless. Recording starts at the first step rather than on an interval, since a
play run is one bounded rollout; --video_interval stays training-only.

3. Shared video helpers in isaaclab_rl.entrypoints.common so training and
play cannot drift apart.

Notable findings

  • physics=physx does not mean Kit PhysX. Measured across the registry it
    resolves to kitless OvPhysX on 39 tasks and to Kit PhysX on 5;
    isaacsim_physx is the reliably-Kit one. Selecting uv extras from the preset
    token was therefore wrong, which the single full profile now sidesteps.
  • 35 of 87 dispatchable tasks declare no physics preset and hard-fail on any
    physics= token, so physics is optional throughout.
  • OSMO datasets are retiredoutputs: - dataset: is rejected server-side
    with "Bucket isaac mode is read-only". Publishing is declarative via
    outputs: - url: swift://…, so OSMO performs the upload after exec regardless
    of exit code and no storage credentials enter the container.

Type of change

  • New feature (tools/odin/)
  • Bug fix (play video; packaging conflict)

Validation

  • 207 Odin tests, offline: no simulator, no Docker daemon, no live OSMO calls.
  • 199 upstream benchmark tests, 29 new play-video tests.
  • scripts/benchmarks/test + source/isaaclab/test/cli: 256 passed. The 6
    failures are environmental — skrl, stable_baselines3 and rl_games are
    absent from the base venv, confirmed by re-running one with --extra skrl
    (passes), plus a prebundled-torch check needing a full Isaac Sim install.
  • Rendered workflows validate against live OSMO
    (osmo workflow validate → succeeded).
  • The benchmark image builds and uv sync completes with every backend in one
    virtualenv.
  • uv run isaaclab -f clean; tools/changelog/cli.py check develop passes.

Not done

  • Image not yet pushed to NGC. Built and validated locally; the push needs a
    faster uplink than the current VPN (~111 KB/s for a 33.8 GB layer).
  • No live dispatch yet. Everything up to submission is verified.
  • No compare front-end. Odin produces paired, provenance-tagged bundles;
    regression methodology belongs with the Perf team.
  • --resume / --retry_failed are not wired.
  • Task discovery lives in tools/odin/discover.py rather than
    isaaclab_tasks.utils; promoting it is a follow-up.

Checklist

  • I have read and understood the contribution guidelines
  • I have run the pre-commit checks
  • I have added tests
  • I have added the required changelog fragments
  • My name already exists in CONTRIBUTORS.md

Cuts the Odin harness down to its OSMO half. The fleet stack (asgard,
valhalla, hugin, munin, common) stays on antoiner/feat/odin and is not
carried over; a layout test asserts those sub-packages never reappear.

Keeps the osmo CLI wrapper and trims the task-name sanitiser down to the
one function OdinV2 still needs. Everything else is rewritten against
Isaac Lab's own benchmark entrypoints.
Validates odin.yaml with key-path error messages. Rejects booleans where
integers are required, since YAML would otherwise read 'gpu: true' as a
request for one GPU.
Schema 2.0, keyed by row_key rather than a synthesised run_id: upstream's
capture.synth_run_id() is timestamp-derived, so the controller cannot
pre-compute it.

Sizing fields are optional so a first dispatch can omit them and let each
task run at its shipped agent-config default; those resolved values are
later harvested back out of the emitted bundles.

Records an image reference per A/B side, digest-pinned, so a retag cannot
change what a comparison compared.
Expands a seed list of (task_id, rl_library, physics) triples across seeds
into deterministic rows. Sizing is optional and omitted by default so each
task runs at its shipped agent-config default on the first dispatch.

Selects uv extras per row instead of using the 'all' aggregate:
[tool.uv].conflicts declares isaacsim incompatible with all, test, mimic,
teleop, ovphysx, and viser, so a physx row and an ovphysx row can never
share a virtualenv.

Chunks cheapest-first, with unbudgeted rows last so they cannot size a
chunk shared with a known-short task.
Each task runs 'uv run isaaclab benchmark training' directly; the hugin
and munin wrappers are gone. Sizing flags are omitted when unset so the
task falls back to its shipped agent config.

Keeps the one-group-per-task wrapping: OSMO's exec_timeout clock starts
at each group's RUNNING transition, so tasks at the workflow root get
killed for other tasks' queue time.

The entry script captures the benchmark exit code before publishing
results and re-raises it afterwards, so a failed run still uploads
whatever it produced.
Replaces dataset download and delete with 'osmo data download' against a
bucket URI: OSMO datasets are retired. A test asserts the dataset helpers
stay gone so they cannot creep back in.

Drops the rsync dev path OdinV2 does not use and forwards --priority on
submit.
Renames the failure kinds off their Hugin provenance: benchmark_crash,
malformed_bundle, timeout, infrastructure.

Unrecognised FAILED_* states fall back to infrastructure so OSMO version
drift cannot wedge the loop or be misread as a benchmark crash, and a
failed status query is skipped for the tick rather than aborting the
dispatch.
OSMO datasets are retired and the replacement is not settled. This
isolates storage behind publish_command and fetch_results so swapping the
mechanism is a one-file change.

fetch_results returns the row directory even when the bundle is
unreadable, so one bad row is reclassified rather than aborting the
dispatch. Ships a bucket-URI implementation over 'osmo data' as the
placeholder.
Pins a commit by cloning a git bundle into a CUDA image and running
uv sync --frozen against that commit's committed uv.lock, so a commit
that changes a dependency is tested with the changed dependency.

Clones from a bundle rather than copying a checkout because
capture_versions() records git_commit and git_branch; without a real .git
every emitted benchmark bundle would lose its provenance. The Dockerfile
asserts the checked-out HEAD matches the requested commit.

Bundles a single temporary ref rather than --all to keep the build
context small, and removes that ref even when bundling fails.
Wires planning, rendering, submission, polling, and fetching into four
subcommands. A/B mode submits the same row set against two digest-pinned
images, disambiguating row keys and OSMO task names by side so both can
share one dispatch.

A COMPLETED task whose bundle does not parse is reclassified
malformed_bundle rather than counted as a success.
Derives per-task sizing, a wall-clock budget, and a reward baseline from
a completed dispatch's bundles, so the sizing table is measured rather
than guessed. A first dispatch runs every task at its shipped
agent-config default; harvest reads the resolved values back out.

plan.apply_metadata overlays the result onto the seed list, with
seed-list values winning so a hand-set override survives a later harvest.
Crashed runs are excluded: their duration describes the failure, not the
task.
Records the build, dispatch, harvest, status, and fetch flows, the
on-disk layout, the four failure kinds, and the three known gaps:
provisional result retrieval, the interim seed task list, and missing
play-side video support upstream.
Enumerating the registry showed the physics presets are physx (44),
newton_mjwarp (45), ovphysx (32), newton_kamino (27), isaacsim_physx
(10), newton_mjwarp_vbd (2), newton_mjwarp_vbd_proxy (1).

isaacsim_physx is a distinct preset, not an alias of physx, and was
falling through to the newton profile — which would have built a
virtualenv without Isaac Sim and failed at run time.

Also normalises the legacy aliases (newton, kamino, ovrtx_renderer,
isaacsim_rtx_renderer) before profile selection. The separate 'rtx'
renderer preset is left out deliberately: it is not confirmed to require
Isaac Sim.
The previous publish path never worked. publish_command() emitted
'osmo data upload', but the image installs only ca-certificates, curl,
git and uv — no osmo binary, no credentials. The deliberate '|| echo'
that stops a failed upload masking the benchmark exit code would have
swallowed 'command not found', so every row of a first real dispatch
would have returned malformed_bundle.

Publishing is now declarative: each task carries an 'outputs: - url:'
block and OSMO uploads the output directory itself, after exec and
regardless of exit code. No osmo binary and no storage credentials are
needed in the container, a crashed run still returns what it produced,
and the benchmark's verdict is structurally unmaskable.

Validating a rendered workflow against the live service also caught
'pull_credential' being rejected outright; OSMO's task 'credentials:'
block maps a credential name to the registry host. Pool and platform in
the shipped config did not exist either.

Adds a submit-time preflight — 'osmo data check -a WRITE' plus
'osmo workflow validate' on every chunk — so schema drift and bucket-mode
changes fail before any GPU time is spent, rather than after.
A preset token does not determine the physics backend. Measured across
every task that declares them: physics=physx resolves to kitless OvPhysX
on 39 tasks and to Kit PhysX on 5, while isaacsim_physx resolves to Kit
PhysX on all 10. Extras were being chosen from the token, so all three
physx rows in the seed list would have been given --extra isaacsim when
they actually need ovphysx — and uv refuses to install both.

Profiles are now full installs of everything that co-resolves within
them, so a row picks a profile rather than reasoning about extras. The
two-way split is forced by a dependency clash, not by preference:
isaacsim-core pins packaging==26.0 and ovphysx pins packaging<24, so a
single virtualenv holding both is unresolvable. Renames the newton
profile to kitless, since it covers OvPhysX and OVRTX as well.

Makes physics optional: 35 of the 87 usable tasks declare no physics
preset and hard-fail on any physics= token, and the template emitted one
unconditionally.

Rows carry their profile explicitly, defaulting to kitless. Resolving it
per task belongs to task discovery.
ovphysx 0.5.9 caps packaging at <24 in its own wheel metadata while
isaacsim-core 6.0.0.1 pins ==26.0, which made the two extras impossible
to install together and forced [tool.uv].conflicts to declare them
incompatible. Both caps are stricter than the code needs.

A packaging>=20,<27 override collapses the forked resolution onto 26.0,
so the isaacsim/ovphysx conflict entry is no longer required. teleop,
viser, mimic, test and the 'all' aggregate still conflict and stay.

For OdinV2 this removes a whole class of failure. Its two profiles
existed only because of this clash, and picking between them required
resolving each row's physics preset to a Kit or kitless runtime — which
a preset token does not determine (physics=physx resolves to OvPhysX on
39 tasks and to Kit PhysX on 5). One 'full' profile removes the routing
problem entirely.

Lockfile regenerated with uv 0.12.0; it also picks up the workspace
version bumps the nightly changelog commit left unlocked.
The first real build spent 480s failing apt DNS lookups and reported
nothing, because every subprocess ran with capture_output=True. Docker
output now streams, so a wedged step looks different from a slow one, and
_check reports the exit code when there is no captured stderr.

uv sync pulls tens of GB from PyPI in a single layer, so a dropped
connection discarded the lot and restarted from zero. It now writes to a
BuildKit cache mount, which makes a retry resume from what already
landed. That also keeps the wheel cache out of the image: nothing needs
it at run time, since the venv built here already matches what tasks
request and 'uv run --frozen' cannot re-resolve.

Raises UV_HTTP_TIMEOUT to 180s, well above the 30s default, because large
wheels behind a VPN stall longer than that while still alive, and builds
with --network=host to use the host resolver rather than BuildKit's
bridge.
Replaces the hand-typed six-entry seed list with discovery over the Gym
registry: 87 dispatchable tasks, 163 rows, 102 core and 61 contrib. The
old list covered about 5% of the tree.

Discovery is the default source; a hand-written file remains possible via
'dispatch --tasks-yaml'. Rows carry an explicit scope field rather than
re-deriving core-vs-contrib from the task-id prefix at each call site.

Four named policies with composable post-filters: standard (163 rows),
core-only (102), all-libraries (348), cross-backend (75).

Two selection rules are worth stating. physx is dropped when ovphysx is
also declared, because headless they resolve to the same backend and
running both is an exact duplicate. Tasks declaring no physics preset
emit one row with the field absent, since they reject any physics= token.

Sizing is never invented here -- it is harvested from a real run and
applied as an overlay.
AGENTS.md mandates snake_case CLI arguments and upstream follows it
(--rl_library, --num_envs, --max_iterations), but sixteen Odin flags used
kebab-case, so 'odin' read inconsistently with the 'isaaclab benchmark'
command it wraps.

--policy is renamed --selection. It names the row-set shape, but 'policy'
means the learned agent everywhere else in this codebase, so
'--policy standard' read like it selected a network. The RL library axis
is --library.

Documents the discovery flow, including that physics=physx resolves to
kitless OvPhysX on 39 tasks and to Kit PhysX on only 5 -- so sweeping the
PhysX family needs all three tokens.
Renderers were discovered and then silently dropped, so a camera task was
expanded across physics only and benchmarked headless -- measuring
everything except the thing under test. All four renderers on
Isaac-Cartpole-Camera-Direct were being discarded.

Discovery now returns a task's legal modes directly rather than raw axes,
so nothing downstream reconstructs the cross product or reasons about
which pairings are rejected. The cross product is not all legal: OVRTX is
kitless and cannot share a process with Kit physics, so
isaacsim_physx + ovrtx is refused and 15 of 16 pairings survive. Each
pairing is checked against the runtime validator, which costs about seven
seconds for the whole registry.

Row keys gain a renderer segment. Without it the renderer variants of one
task collapse onto a single key -- caught by the existing duplicate
guard. Headless keys are unchanged.

87 tasks now yield 429 rows, 109 of them carrying a renderer across the
five camera-capable tasks.
Domain presets -- depth, rgb, albedo, semantic_segmentation, shading and
scene variants -- were a third axis the tool ignored entirely, so depth on
cartpole was unreachable. Discovery now expands them and --presets
whitelists them, alongside --physics and --renderer.

Backend names are filtered out of that axis. 31 tasks list physx or
newton_mjwarp under DOMAIN as well, and a per-task check is not enough: a
task can list a backend under DOMAIN without declaring it under PHYSICS.
Uses the same global mirror-name guard as tools/environ_docs.py.

Discovery emits one preset at a time and never combines them, which is a
documented limitation rather than a property of the runtime: presets
targeting the same field conflict outright, and no rule is available for
which pairs compose. The executor imposes no such limit -- a hand-written
list may give a token, a comma-separated string, or a YAML list, and it
renders as one presets=a,b token. dispatch.json records what ran.

Row keys gain a preset segment, without which the variants of one task
collide. 87 tasks now yield 1663 rows, so whitelisting is the normal
path rather than an optimisation.
The header pointed at --tasks-yaml, which was renamed --tasks_yaml when
the CLI moved to snake_case.
PlayBundle.video_path and build_play_bundle(video_path=...) existed, but
no play adapter could populate them: the video CLI arguments lived in
add_common_train_args and were training-only, and wrap_record_video
hardcodes a videos/train folder with a periodic step trigger. Camera
tasks were therefore benchmarked headless, measuring everything except
the thing under test.

Adds --video and --video_length to the RSL-RL, RL-Games, SKRL and SB3
play adapters, which now enable cameras, request an rgb_array render
mode, wrap the env before the RL vec wrapper, and report the directory on
the bundle.

Recording starts at the first step rather than on an interval, because a
play run is one bounded rollout with no later interval to catch;
--video_interval stays training-only. The shared arguments move to
add_video_args so training and play cannot drift apart.
Widening the packaging cap touched only the root pyproject, but the
override list is published in two more places that must mirror it:
tools/wheel_builder/uv-overrides.txt and the uv_pip install-CI copy.
Without them, wheel-based installs would still hit the isaacsim/ovphysx
conflict while uv run worked -- a split-brain failure visible only in the
wheel path. test_wheel_builder_uv_overrides_match_root_pyproject exists
to catch exactly that, and did.

Updates test_uv_run_isaacsim_extra_is_conflict_forked, which asserted the
isaacsim/ovphysx conflict that was deliberately removed. The pair is now
asserted absent, with the reason, so re-adding it fails loudly rather
than silently reinstating the split.
@github-actions github-actions Bot added documentation Improvements or additions to documentation isaac-lab Related to Isaac Lab team infrastructure labels Jul 29, 2026
parent_dispatch_id and reset_in_flight_to_pending sat in the persisted
schema promising a capability nothing implemented. A controller that died
mid-poll could only be recovered by hand, and failed rows could not be
re-dispatched at all -- painful on a 1663-row sweep.

--resume re-attaches to a dispatch whose workflows are already submitted,
flipping in-flight rows back to pending and re-entering the poll loop. It
never re-submits: the workflow ids are recorded, and submitting again
would duplicate every task. It refuses a dispatch that never got past
planning rather than silently doing nothing.

--retry_failed starts a new dispatch containing only the failed rows of
another, linked by parent_dispatch_id, reusing the parent's seeds. Retries
stay explicit because a benchmark crash usually repeats.

Both accept LATEST. --image and --seeds become conditionally optional,
with an explicit error rather than an argparse failure that could not
explain which path made them optional.
Goal #2 needs videos and checkpoints for the Core tasks. --play runs the
rollout in the same task as training, so it reads the checkpoint just
written with no cross-task handoff, no shared storage round trip, and no
dependency graph to get wrong.

TrainingBundle.checkpoint_path was hardcoded None in the RSL-RL,
RL-Games, and SKRL training benchmarks -- the same dead-field shape as
video_path -- so there was nothing for a chained step to read. It now
reports the checkpoint that actually landed on disk, found by
latest_checkpoint_path since the libraries name and place them
differently and none return the path.

The chained step is skipped when training failed or wrote no checkpoint,
and the task exits with the training code: a play failure must not turn a
good training run into a red row. Both steps receive the same physics,
renderer and preset tokens.

Verified beyond rendering: the entry script passes bash -n, the embedded
extractor parses and returns the right path on a real bundle shape and
nothing on a null field, and the workflow validates against live OSMO.
The chained entry script invoked python3 to read the checkpoint out of
the training bundle, but the image is nvidia/cuda plus uv, and uv manages
its own Python -- there is no python3 on PATH. The call would have failed
with command not found, the substitution would have yielded an empty
string, and the row would have reported 'training wrote no checkpoint':
a wrong diagnosis for a missing interpreter. Uses the venv interpreter
uv sync created instead.

Every row now writes odin-steps.json recording the training exit code,
whether a checkpoint was found and where, why play did or did not run
(not_requested, skipped_training_failed, skipped_no_checkpoint, ran) and
its exit code, plus an artifact listing. The bundles say what a run
measured; this says what happened, which is what a row returning less
than expected needs. Writing it can never fail the task.

Verified by executing the rendered script inside the real image on both
paths -- training-failed and checkpoint-found-play-ran -- not just with
bash -n, which is what missed the interpreter in the first place.
Two bugs found while drafting the data-side design doc.

Checkpoints were never uploaded. RL libraries write them under logs/,
but OSMO collects only {{output}}, and nothing copied them across -- so
the stated goal of producing videos and checkpoints delivered videos and
silently lost every checkpoint, while odin-steps.json reported a path
that died with the container. --keep_checkpoints copies it into the
uploaded output and the step record reports where it landed.

Checkpoint resolution now happens whether or not play is chained:
retaining a policy and rolling it out are separate concerns, and a
training-only sweep should still be able to keep its weights.

harvest grouped by (task, library, physics) alone, so an A/B dispatch
averaged both commits into one baseline, and a depth rollout was pooled
with an RGB one on the same task and backend. It now excludes side B
using dispatch.json and segments by renderer and presets.

Also drops a stale README claim that play video was not yet possible, and
documents that only --output_path survives the container.

Verified in the real image: the checkpoint copy lands and the step record
reads retained_as checkpoint/model_500.pt.
Each of these silently changes the verdict of a real dispatch:

- The entry script's `find "$OUT" -type f | head -40` runs under
  `set -o pipefail`. `head` closes the pipe, `find` takes SIGPIPE, and
  the pipeline exits 141, ending the script before `exit $rc`. It only
  triggers once a run produces enough files to fill head's buffer, so
  small tests pass and production rows fail.

- `latest_checkpoint_path` globbed `model_*.pt`, which matches RSL-RL
  only. SKRL writes `agent_<tag>.pt`, `best_agent.pt` and
  `<name>_<tag>.pt`, so every SKRL row reported no checkpoint and any
  chained play step took the `skipped_no_checkpoint` branch.

- `JobEntry.physics` is legitimately None for tasks that declare no
  physics preset, but `_job_from_dict` coerced it with `str()`. Reload
  produced the string "None", which passes every `is not None` guard and
  renders `physics=None` into a retry's workflow.

- A COMPLETED row whose bundle does not parse is reclassified as failed
  by the fetch hook. OSMO keeps reporting COMPLETED, so the next tick
  attempted an illegal failed -> completed transition, and the
  uncaught ValueError abandoned every still-running row.

- `osmo workflow query` takes `-t/--format-type`, not `--output`, so the
  JSON path had never once run: every query fell through to the table
  parser, which reports no exit code at all. Worse, the JSON parser read
  a top-level `tasks` key that OSMO does not emit -- tasks are nested
  under `groups[].tasks[]` and the workflow is named by `name`. Both are
  now matched against a real query response, and the table fallback is
  gone.
Eight places where the code did something narrower or broader than what
it claims:

- A retry collapsed failures to (identities, union-of-seeds) and let
  plan_rows re-expand the cross product, so a seed that passed was
  re-run. Failures are now grouped by the seed they occurred on.

- A retry plans one row per identity and runs it against --image, so an
  A/B dispatch's side-B failures would silently be re-run on side A's
  image. Retrying an A/B dispatch is now refused outright.

- apply_metadata keyed on three fields while harvest groups on five, so
  a camera row could inherit a headless row's timeout. Both now use the
  same key.

- sync_once's query-failure handler had no ceiling, so bad credentials
  looped forever. poll_until_terminal now aborts after a bounded run of
  ticks in which every query failed, pointing at --resume.

- _mode_resolves caught bare Exception, so an Isaac Lab import
  regression marked every mode illegal and the CLI blamed the operator's
  filters. Import and API failures now surface as DiscoveryError.

- harvest swallowed an unreadable dispatch.json to {}, which empties
  side_b_rows and averages an A/B dispatch's two commits together --
  precisely what excluding side B exists to prevent.

- chunk_rows raised only after dispatch.json was written, stranding an
  orphan dispatch that LATEST resolves to. chunk_size is now validated
  at config load and before the dispatch directory is created.

- The row sort key omitted renderer and presets, so a task's preset
  variants tied and their layout followed input order rather than being
  identical across reruns.
The workflow YAML and the Dockerfile were each asserted by a few dozen
substring checks, which pass while saying nothing about the rest of the
artefact. They are now compared whole against committed goldens under
tools/odin/tests/golden/, regenerable with ODIN_UPDATE_GOLDEN=1. The
invariants a golden diff would show but not explain -- one group per
task, the credential-to-host mapping, the exit-code verdict, the venv
interpreter -- stay as named tests.

Also removed machinery that only ever had one value or no caller:

- plan.PROFILE_EXTRAS and image.PROFILES held the same 8-tuple verbatim
  in two modules, each with a single "full" key. They collapse to one
  plan.UV_EXTRAS that image.py imports, taking with them
  uv_extras_for_profile, PlannedRow.profile, --profile, and the
  Dockerfile's per-profile loop. Tags lose their "-full" suffix. A test
  now asserts the image and the rows agree on the extras, which nothing
  checked before.

- OsmoClient.logs/_stream_logs/cancel had no callers and no tests;
  data_check's access argument was only ever "WRITE".

- JobEntry.attempts was written and never read.

- discover_tasks' validate_modes was never passed.

- state's major-version comparison had only ever seen one version.

- read_bundle and validate_bundle were the same glob-parse-check written
  twice; harvest now imports results.read_bundle.

Golden files are byte-exact renderer output, so pre-commit's whitespace
and license hooks skip that directory.
Comments should say what the code does and why, in the present tense.
Removed the ones that instead recount how a bug was found or what the
code used to be, the speculative paragraphs about where an API might one
day move, and the exact task counts that go stale the next time the
registry changes without anything failing to say so.

The README also still described the per-profile build that this branch
collapsed: --profile, the profile table, and the -full tag suffix are
gone, and the retry section now states the per-seed and A/B rules. Added
the ODIN_UPDATE_GOLDEN workflow for re-rendering the golden files.

Kept the comments that record something the code cannot: the
config_file module-name clash, bool subclassing int, BUNDLE_REF's effect
on recorded provenance, why pending may go straight to terminal, why
each OSMO task gets its own group, and why the image clones a bundle
rather than copying a checkout.
tools/odin/config/tasks.yaml is produced by `odin discover` in seconds
and goes stale against the registry the moment a task changes. Committing
it added 9059 lines -- 55 percent of the branch diff -- of content nobody
should read line by line, and every regeneration was a diff.

It is now gitignored, and dispatch says how to produce one when it is
absent rather than reporting an unreadable path. Dropping the
generated_at timestamp from the header removes the remaining source of
churn for anyone who does keep a local copy.

The CLI tests wrote their own three-row list instead of copying the
1663-row file into every case, which takes the suite from 21s to 1s.
Marks the rendered test goldens as generated so they collapse in review.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation infrastructure isaac-lab Related to Isaac Lab team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant