Back to blog
2026-07-0918 min readLocoMuJoCorobot policiesUnitree G1MuJoCo

A Vibe Coder's Guide to Robot Yoga

The final fix was not a bigger network. It was a slower return to neutral, after the policy learned the right 29-DOF body, the right MuJoCo version, and the right contact choreography.

The first policy did not fail with dignity.

It learned for hours, exported cleanly, passed observation parity, loaded into the simulator, and then the Unitree G1 calmly threw itself into the floor.

This is the part of robotics that makes vibe coding useful and ridiculous at the same time. You can start with a sentence that feels alive. Make the humanoid teach yoga. Give it mountain, chair, warrior, tree, namaste. Put the code in an IDE. Let agents write the harness. Let the simulator tell you what happened.

Then the robot gets the last word.

The final win was not a grand reward breakthrough. It was not a larger model. It was not a new simulator stack. The final fix was a slower return to neutral after loaded poses.

That sounds small until you know the history. Before that one timing change, we had already learned the hard lessons. The first pose demo was only a smoke test. The first trained policy learned the wrong body. A clean observation parity check did not prove transfer. A MuJoCo version mismatch broke a controller that was otherwise fine. A trajectory bug kept disguising itself as policy weakness. The robot did not need inspiration. It needed the exact body, exact contacts, exact schedule, exact engine, and enough patience to stop punishing itself between poses.

By the end, the bundle mounted in the Cybernetic IDE simulator had 29 actions, 474 observations, 15 mimic sites, a 100 Hz policy loop, a 7300 frame reference flow, nine named poses, and zero falls in the local yoga-studio run.

This is the captain's log of how we got there.

A Unitree G1 in MuJoCo holding a warrior one yoga pose under the learned policy.
A policy-run frame from the MuJoCo yoga flow. This is the kind of evidence we needed after every training claim.

The Prompt Hit Physics

The original product idea was simple enough to say out loud.

Make the G1 a yoga teacher.

Not a cinematic clip. Not a keyframed mannequin. A simulated humanoid with physics on, moving through a small class. Mountain. Upward salute. Forward fold. Chair. Warrior one. Warrior two. Goddess. Tree. Namaste.

The first version used the machinery we already had in Cybernetic IDE. A local MuJoCo runtime. A Unitree-shaped Python surface. A pose registry in the simulator. A script called examples/yoga_teacher.py that could move through the poses, take snapshots, and tell us what the robot was doing.

That first loop was worth building because it made failure visible. It also proved that visible is not the same thing as solved.

Gravity-compensated PD hold could manage the easy poses. Mountain, upward salute, and namaste were fine. The harder poses toppled. Chair wanted torso lean. Warrior poses wanted real planted feet. Forward fold wanted pelvis pitch. Tree wanted single-support balance, which is exactly where a mannequin pose stops being a controller.

At that point the problem changed shape.

We were no longer asking whether an agent could write a script that named yoga poses. It could. The real question was whether we could turn a vibe-coded intent into a closed-loop policy that survives contact physics.

LocoMuJoCo Was the Right Kind of Boring

LocoMuJoCo is an imitation-learning benchmark for whole-body control. It ships MuJoCo and MJX environments for humanoids, quadrupeds, and skeletal human models. It includes retargeted motion data, trajectory utilities, domain randomization, and clean JAX examples for PPO, GAIL, AMP, and DeepMimic-style training.

The flashy description is less important than the useful shape of the code.

LocoMuJoCo already had a Unitree G1 environment. It already had a Trajectory object. It already had ImitationFactory, which can build an environment and attach a trajectory. It already had TrajectoryHandler, which filters, extends, reorders, and interpolates trajectory data so the current MuJoCo model sees the right joints, bodies, and sites. It already had MimicReward, which compares the current body to a reference motion through joint positions, joint velocities, relative site positions, relative site orientations, and relative site velocities.

That is the whole trick in less romantic words.

Do not ask a model to "do yoga" as a wish. Give it a reference body state, a simulated body, a reward that notices when those two diverge, and a terminal condition that ends the episode when the root drifts too far.

The local machine mattered too. We were on an M5 Mac, not a CUDA box. Big Isaac or GPU-first training stacks were the wrong first move for this loop. LocoMuJoCo's MJX path could run PPOJax on CPU at roughly useful speed. The benchmark run plateaued around five thousand environment steps per second. Not magic. Enough.

The point was not to find the most glamorous robotics stack. The point was to keep the shortest truthful loop.

The Small Workflow That Became the Project

The first useful command sequence looked like this.

g1-yoga-project-poses \
  --output .runtime/g1-yoga-rl/yoga_pose_projection.json

g1-yoga-make-trajectory \
  --output .runtime/g1-yoga-rl/yoga_trajectory.npz

g1-yoga-check-imitation \
  --traj .runtime/g1-yoga-rl/yoga_trajectory.npz

g1-yoga-train \
  --traj .runtime/g1-yoga-rl/yoga_trajectory.npz \
  --out .runtime/g1-yoga-rl/run1 \
  --total-timesteps 20000000 \
  --num-envs 64 \
  --chunks 10

g1-yoga-export \
  --agent .runtime/g1-yoga-rl/run1/PPOJax_saved.pkl \
  --out .runtime/g1-yoga-rl/run1/policy.npz

g1-yoga-pack \
  --policy .runtime/g1-yoga-rl/run1/policy.npz \
  --traj .runtime/g1-yoga-rl/yoga_trajectory.npz \
  --out .runtime/g1-yoga-rl/run1/deploy_bundle.npz

g1-yoga-validate-deploy \
  --bundle .runtime/g1-yoga-rl/run1/deploy_bundle.npz \
  --traj .runtime/g1-yoga-rl/yoga_trajectory.npz

g1-yoga-sim2sim \
  --bundle .runtime/g1-yoga-rl/run1/deploy_bundle.npz

That is the workflow I wish every vibe-coded robotics project reached early.

There is a command to project the human idea into robot joints. There is a command to make the trajectory. There is a command to check that LocoMuJoCo accepts it. There is a trainer. There is an exporter. There is a deploy packer. There is an observation parity gate. There is a sim-to-sim gate before the bundle gets mounted into the Docker runtime.

The agents helped write a lot of this. Codex sessions moved between source inspection, package scaffolding, training logs, policy export, runtime mounting, and viewer checks. Claude Code sessions did their own stretches of handoff research and package buildout. The important thing is not that an agent typed the code. The important thing is that the loop had places where reality could push back.

A vibe without a judge becomes a demo.

A vibe with a simulator, reward, parity check, and viewer becomes engineering.

The Pose Demo Lied Politely

The PD baseline was useful because it failed in a readable way.

PD hold means the simulator pulls each joint toward a target position with spring-damper control. It is a good smoke test. It is not a yoga teacher.

The easy poses passed because they were close to standing. The hard poses asked for whole-body balance while pretending that joint targets were enough. A chair pose with an upright torso and arms overhead pushed the center of mass toward the heels. A forward fold without pelvis pitch turned into a geometry problem. A warrior pose could look plausible while one foot was quietly floating. Tree was a single-support pose, which means the policy has to make peace with a missing support polygon.

This was the first robotics lesson in the project.

Pretty keyframes are not the same thing as feasible motion.

The yoga registry had to become more physical before the policy could be fairly blamed. We added a base_pitch pseudo-key for the floating base. We worked through sign conventions in the G1 model. Forward is positive x. Hip pitch, knee, and ankle pitch rotate around positive y. Positive hip pitch swings the segment backward, which is the kind of fact that humbles a beautiful prompt very quickly.

After retuning, the static margins made sense. Mountain had room. Upward salute had room. Chair had a narrow but real margin. Warrior poses had planted feet. Goddess was marginal. The original tree stayed intentionally difficult, and later the flow moved to a toe-touch beginner version instead of pretending the final policy was a full single-leg yoga master.

This is where the work started feeling less like "make the robot do yoga" and more like "design a reference that a controller can learn without being tricked into nonsense."

Run1 Learned and Then Forgot

The first serious training attempt used LocoMuJoCo's reduced Unitree G1 model.

It had 23 actions and a 450-dimensional observation. PPOJax ran for 20 million steps. The return climbed above 100. Then around update 1700 it collapsed. The final return was around 10.9.

This is a very robotics way to lose a day.

The policy did learn something. We just failed to keep the useful checkpoint. PPO gave us a good intermediate controller and then wandered away from it. The trainer only kept the final state, so the run ended with a weak artifact and a strong lesson.

After run1, the trainer moved to chunked execution. Each chunk saved the agent. The script kept the best tail return, not merely the final checkpoint. Long local runs got logs. Later they got caffeinate -is, because a training job killed by laptop sleep is not a deep research failure. It is just embarrassing in a way that teaches fast.

Run2 Learned the Wrong Body

Run2 was the first result that could fool a busy person.

The same reduced 23-DOF setup trained much better. Best tail return landed around 729. In the training environment it held six of nine poses. That doubled the PD baseline.

Then we deployed it into the 29-DOF simulator and got zero of nine.

The observation builder matched to machine precision. The bundle loaded. The export parity looked clean. The policy was not obviously broken. It had simply learned a controller for a different body.

The reduced LocoMuJoCo G1 welded or lumped pieces that our deploy simulator kept as actuated joints. The wrist chain differed. The waist differed. The mass distribution differed. Total mass was close, but close was not enough. The policy had learned closed-loop dynamics, not a slogan.

This was the first big rule that survived the rest of the project.

Train where you deploy.

For language models, a schema mismatch can make a tool call fail. For robots, a small body mismatch can make a policy fall down while every static checker smiles.

The 29-DOF Pivot

The fix was not to make the 23-DOF policy tougher. The fix was to stop asking it to transfer across the wrong boundary.

We built g1_yoga_rl/cyber_env.py around the exact deploy scene, scene_29dof.xml.

That file became one of the most important pieces of the project. It loads the Cybernetic IDE G1 scene as a MuJoCo spec. It injects the mimic sites LocoMuJoCo needs. It names the foot capsules. It renames the free joint to the root name the environment expects. It generates observation and actuation specs from the model itself.

The new environment had 29 actions and 474 observations.

The action set now matched the deploy model. The mimic sites were in the same body tree. The contact reduction matched the training expectation, with explicit foot-capsule to floor contact. The policy would still have plenty of ways to fail, but at least it was now failing in the right universe.

Run3 reached a best tail return around 406 before stopping. Run4 resumed and reached around 658 before another interruption. These were not the final wins, but they proved the pivot was sound.

The work also pushed a design boundary into the open.

The policy trainer should not own the Unitree SDK abstraction. The SDK layer owns session shape, transport, command surfaces, and safety boundaries. The policy is a controller that can be mounted behind that surface. Keeping those things separate made it possible to train a policy, pack it, mount it in the simulator, and still talk to the robot through the same IDE-facing status and command loop.

Most of the Work Was Choreography

Run4 looked good on paper and still fell in the live cycle.

The holds were often fine. The transitions were not. Some paths dragged loaded feet sideways. Some went through contact states that were hostile to the policy. Some asked the robot to leave a hard pose and snap back through neutral too quickly.

This is the section where "reward design" becomes too small a phrase.

For a whole-body controller, the reference motion is part of the environment. The policy is not just learning nine poses. It is learning the route through those poses, the foot contacts on that route, the root motion, the timing, and the recovery envelope when small errors accumulate.

Run5 changed the flow. Transitions routed back through standing. Tree became a toe-touch beginner version instead of a full single-leg stunt. The cycle lengthened to roughly 73 seconds. The evaluation harness stopped silently testing the wrong schedule. The run finished with a best tail return around 810.9 and an export parity around 2.68e-06.

This gave us one of the best lessons of the whole build.

A trajectory bug can look exactly like a policy bug.

If the reference asks for a contact transition the body cannot physically afford, the policy gets blamed for being weak. The policy may be fine. The dance may be wrong.

The Reward Was a Contract

The LocoMuJoCo reward was not mystical.

MimicReward compares the simulated body with the reference trajectory. Joint positions and velocities matter. Relative mimic-site positions and orientations matter. Relative site velocities matter. Costs can punish action bounds, acceleration, torque, and action rate.

For our task, this had a clean interface.

The generated trajectory says where the body should be. The environment says what the body is doing now. The reward measures the gap. The terminal handler ends an episode when the root drifts too far from the reference. PPOJax trains a small actor-critic policy on that loop.

The policy itself was not a giant foundation model. The exported actor was a compact MLP. That is part of why the project is interesting. A tiny policy can look smart when the environment, reference, and deployment path are honest.

The deploy bundle did one extra important thing. g1-yoga-pack precomputed the reference half of the goal observation with LocoMuJoCo's own math. The simulator runtime only had to compute the current-state half and run the NumPy MLP. That removed a large class of cross-runtime observation mismatches by construction.

This is the kind of boring detail that determines whether a robotics demo is repeatable.

The Version Mismatch Was Not a Footnote

At one point the same policy behaved like two different policies.

Training used MuJoCo 3.10.0. The container runtime had MuJoCo 3.3.6. The same loop that held seven of nine in one version fell apart in the other.

This was not a vibes problem. It was not a reward problem. It was simulator drift.

The container got pinned to the training version. The schedule reader got fixed so examples/yoga_teacher.py --policy used the bundle timing instead of a hardcoded segment duration. The policy runtime, training environment, and viewer loop had to agree on the boring facts.

Engine version. Model XML. Solver options. Timestep. Control rate. Observation construction. Action mapping. Flow schedule.

All of them counted.

This is why "sim-to-sim" is not a magic spell. It is an audit checklist.

Run8 and the Last Dumb Bug

Run6 became the stable intermediate base. Run7 tried observation noise, but it was not the final path. Run8 resumed from run6 on the tuned 29-DOF trajectory and became the winning policy.

Best tail return was about 951.5. Export parity was around 4.35e-06.

The first live run8 bundle still had two resets. That was annoying because the policy was clearly strong enough. The diagnostic showed the pattern. The resets happened on fast returns to neutral after loaded poses.

So the last fix was not to train longer.

We repacked run8 with a slower neutral return. The artifact was deploy_bundle_return25.npz. It had 7300 frames. It mounted as .runtime/unitree-g1-mujoco/policy/g1_yoga_policy.npz.

Then the live policy held all nine poses with zero topples.

That is the part I keep coming back to. The final bug was small, mechanical, and completely physical. The body knew what to do. We were rushing it through the hallway between poses.

What the Agents Were Good For

The agent sessions were most useful when they behaved like impatient lab assistants with notebooks.

One session audited the Unitree SDK and MuJoCo surfaces. Another turned the pose demo into a policy-training handoff. Another built the first package and watched run1 collapse. Another pushed the 29-DOF pivot. Another chased runtime parity. The final Codex pass reconstructed the run history, diagnosed the return-to-neutral failures, mounted the winning bundle, and verified the yoga studio run.

The agents were not good because they made physics disappear.

They were good because they kept producing the next check.

Does the generated trajectory load in LocoMuJoCo. Does the MJX path step with finite observations. Does the exporter match the Flax actor. Does the deploy observation builder match training. Does the sim-to-sim loop survive. Does the Docker runtime expose policy status. Does the viewer show the same pose label the bundle thinks it is running.

That is the productive version of vibe coding in robotics.

Let the model write code quickly. Then make every layer answer to a sensor, simulator, log, or screenshot.

The Recipe I Would Reuse

Start with the smallest visible behavior.

For us, that was a PD-held yoga flow in the simulator. It gave us the pose names, the failure set, and the first evidence trail.

Turn the visible behavior into a trajectory.

The trajectory is where vague intent becomes robot state. This is also where you catch bad sign conventions, floating feet, impossible folds, and support margins that live too close to the edge.

Train on the body you will deploy.

The 23-DOF policy taught us a real controller for the wrong body. It was not useless. It paid for the lesson. The 29-DOF pivot won.

Keep the reward boring.

Mimic the reference body state. Penalize the kinds of thrash you actually care about. Terminate when the root leaves the reference envelope. Make the reward readable enough that a failed rollout has somewhere to point.

Save best checkpoints.

Run1 learned and forgot. That should only happen once.

Validate the boring boundaries.

Observation parity, export parity, action mapping, engine versions, solver options, timestep, control rate, and bundle schedule are all part of the policy. They just do not have gradients.

Watch the body.

A return curve cannot tell you that a foot is sliding sideways through a transition. A log cannot make you feel how fast the robot is being pulled back through neutral. The viewer is not decoration. It is one of the instruments.

What This Is Not

This was a simulator win.

It does not mean you should send the policy to a physical G1 tomorrow. Real hardware needs a harder safety boundary, official SDK2 paths, command limits, fall handling, emergency stop behavior, and a much more conservative deployment review.

It also does not mean LocoMuJoCo is a turnkey yoga-teacher factory. The useful part of LocoMuJoCo was that it gave us clean interfaces for environments, trajectories, rewards, and training. We still had to make the exact deploy model trainable. We still had to generate physical references. We still had to build the export and runtime path. We still had to debug timing and version drift.

That is the actual story.

Vibe coding got us moving fast enough to discover the real problems.

Robotics made us earn the win.

The Part Worth Keeping

I like this project because the final lesson is not cynical.

The agents were useful. The simulator was useful. LocoMuJoCo was useful. The IDE was useful. The vibe was useful. The first prompt mattered because it gave the work a shape people could care about.

But the win only happened when the vibe became a loop.

Pose registry. Trajectory. Training environment. Reward. Export. Deploy bundle. Runtime policy mode. Viewer. Status endpoint. Failure log. Repeat.

That is where vibe coding becomes a real robotics tool. Not when it skips the hard parts, but when it makes the hard parts small enough to inspect.

The robot yoga teacher did not become graceful because we told it to be graceful.

It became stable because we stopped lying to ourselves about what the body was doing.

Field notes

Get the next research note.

Occasional dispatches on agent control loops, simulator infrastructure, robot development systems, and the practical details behind what ships.

We will store your email only to send Cybernetic Physics research and product updates. Analytics records only form surface, outcome, and email domain.