cfbf4a0267
The launcher can now spawn two `ShepherdDog` robots, each masked to a single axis of motion, so the herding workload is split orthogonally. Mechanic: * `HERDING_NDOGS=2` (default 1) tells `tools/run_webots.sh` to replace the single-dog node in the generated test world with two copies: - `ShepherdDogX` at (-4, -10), `customData "axis=x"` - `ShepherdDogY` at (+4, -10), `customData "axis=y"` Each spawn position sits south of the field interior so the pair doesn't collide with starting sheep. * `controllers/shepherd_dog/shepherd_dog.py` reads `getCustomData()` at startup; when `axis=x|y` it zeroes the off-axis component of every action *after* speed modulation and *before* EMA smoothing. With `customData` empty the controller behaves identically to single-dog mode, so all existing launches are unaffected. * The dog's emitter line now carries the robot's name (`dog:ShepherdDogX:x:y`), and `controllers/sheep/sheep.py` keeps a `dogs` dict keyed by name, picking the closest one each step for its flee target. Single-dog runs still use the legacy two-field `dog:x:y` format thanks to a length check. * `HERDING_NDOGS` is written into `herding_runtime.cfg` and exported to subprocesses so future tooling can read it. Verified behaviour in Webots smoke tests (HERDING_NDOGS=2, strombom, diff/field, 5 sheep): both dogs spawn with the expected names and axis tags, the dual-dog status print appears, each dog acts only on its assigned axis early in the trial, and the masking is internally consistent. The pair stalls before penning under pure axis-split because each dog reaches its drive standoff and then has only one degree of freedom — useful research finding for the write-up; coordination strategy (shared CoM, role-switching, etc.) is future work. 126 pytest cases still pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>