Skip to content

feat(g1): Unitree G1 support, scope A (right arm + Dex1-1) [draft, sim gate pending] - #601

Draft
ZhangHanDong wants to merge 64 commits into
moxin-org:mainfrom
ZhangHanDong:feat/g1-support
Draft

ZhangHanDong wants to merge 64 commits into
moxin-org:mainfrom
ZhangHanDong:feat/g1-support

Conversation

@ZhangHanDong

Copy link
Copy Markdown

DRAFT — sim gate pending. Opened early for code review. The M0-6 50-seed
g1_right gate (pass1 >= 0.80) has not passed on this head yet; see
Gate status. Before this leaves draft it will be split into separate
spec-change / env-change / feature PRs (CON-9, CON-14, CON-7). Do not merge.

Summary

Unitree G1 support, scope A: the right arm (7 revolute joints) plus the
right Dex1-1 gripper (2 prismatic fingers) on a 9-DOF wire, the same shape as
franka. The other 24 DOFs are held at standing home in sim (pelvis fixed at
0.7872 m) and belong to Unitree motion control on hardware. Model:
unitree_ros@5994d4f g1_29dof_mode_15_with_dex1_1.urdf (mode_machine 15).
The real-robot SDK2 path is lazy-imported and evidence is hardware_pending.

Requirement IDs touched: TC-5, SCN-2, SCN-3, SCN-4, HAR-1, HAR-2, CON-5,
CON-7, CON-8, VER-2, HWP-8/18/20 (new spec 550).

What is in here

spec-change (needs owner review, CON-14)

  • specs/010-topic-contract.md: TC-5 adds the G1 right-arm wire order (7 joints + 2 fingers).
  • specs/550-g1-hardware-phase-gate.md: the G1 hardware-phase gate.

env-change (frozen set, CON-7)

  • assets/g1/**: official URDF, meshes, PROVENANCE/LICENSE (BSD-3-Clause).
  • src/aisle/embodiment.py: G1_RIGHT_ARM_JOINTS; the wire ∪ held partition covers 33 DOFs.
  • env/limits.toml: [embodiment.g1_right] limits, guard envelope, workspace AABB.
  • src/aisle/scenes/physics.toml, pharmacy.py: the g1_right scene profile.
  • src/aisle/kinematics.py: UrdfChain base/tip transforms and g1_right_chain().
  • src/aisle/nodes/budget_guard.py: the g1_right FK branch.

feature

  • Motion stack (ik_trajectory.py, grasp_topdown.py):
    • full-pose DLS and roll continuation;
    • a planning wall budget that stays inside the turn watchdog;
    • G1-only routing (front-routed carry, contained home chain, torso-safe shoulder bound);
    • a profile-driven executor speed.
  • Bridge, scene and reset (dora_genesis.py): held DOFs, Dex1 finger mapping,
    render warm-up before Node(), kinematic carry latch (sim aid only).
  • Hardware tooling: g1_driver.py, adapters, harness hardware_phase,
    docs/hardware/g1/**, tools/g1_runtime.py.
  • Registry: manifests opened to g1_right plus the new g1-driver.
  • ADRs:
    • ADR-g1-embodiment;
    • ADR-g1-driver;
    • ADR-g1-shelf-lattice: re-latticed slots, 8 mm grasp/carry planning
      margin, upper rail raised. Operator rulings 2026-10-22/23.

Fixes that also apply to main

  • plan_done sent without being declared (G1-F4). Under lockstep this
    crashed with a ProtocolError. franka/so101 dodge it only because the verdict
    lands first. It now sends only when the graph declares the output.
  • Held replan opened the gripper (G1-F6). A replan after a bailed stage
    emitted 0.0 during rise/preclose and dropped the box mid-air. The pre-close
    segments now keep grasp_cmd while holding.
  • Finding, not fixed here: so101 full-budget IK plans take 12-15 s, past
    the 10 s turn watchdog. This is recorded as a strict xfail; the G1 early exit
    is kept g1_right-only so the so101 record stays bit-for-bit.

Gate status (honest)

Run Head Result
50-seed gate e6363bb 0.76 (38/50). All 12 failures were slot (0.28, 0.08): the right shoulder-roll link hits the torso. Fixed in G1-F11.
10-episode smoke d53aac2 (= this content) 10/10, ~18 s sim and ~100 s wall per episode, no collision or drop
50-seed gate 3dc268c (this head) aborted at 9 episodes: host contention (an OrbStack VM at ~430% CPU) doubled stage wall time, so 5 episodes hit the 150 s per-episode wall clamp. Not a code result; it will be re-run on a quiet host.

The 150 s per-episode wall budget was not changed; G1 was sped up instead
(executor 1.5 rad/s, 75% of the 2.0 rad/s guard envelope).

Known red until the final regeneration (G1-I-b, after the gate)

  • tests/unit/test_freeze_registry.py::test_committed_registrations_check_clean_with_withheld_seeds
  • tests/unit/test_benchmark_release.py::test_version_manifest_and_release_audit_are_current_and_honest

Both turn red by design because the frozen set changed. The freeze successor
registrations and the benchmark v1 manifest get regenerated once the gate
passes.

Test plan

  • uv run ruff format --check . && uv run ruff check .
  • Targeted units (272 passed) and tools/env_hash.py --check
  • G1 sim set: tests/sim/test_g1_{scene_plan,plan_wall_budget,carry_latch,ik_genesis,bridge}.py: 163 passed, 1 xfailed
  • 10-episode g1_right smoke on d53aac2: 10/10
  • M0-6 50-seed gate pass1 >= 0.80 on a quiet host
  • G1-I-b: freeze registrations and benchmark v1 manifest regenerated; full pytest -m unit
  • Split into spec-change / env-change / feature PRs

Simulation evidence only: real G1 reach, gains (SDK2 example kp 60 / kd 1.5 vs
the sim PD gains here) and self-collision margins are hardware_pending.

🤖 Generated with Claude Code

ZhangHanDong and others added 30 commits October 19, 2026 16:11
Add the g1_right wire order (7 right-arm joints, then the two prismatic
Dex1-1 fingers of the official g1_29dof_mode_15_with_dex1_1 URDF) as the
shared contract both G1 lanes build on. Frozen file: env_hash.json
regenerated. Draft only (feat/g1-support, not for main until owner review).

Requirement IDs: TC-5 (draft amendment), CON-7

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pin upstream unitree_ros commit 5994d4f: the URDF, all 39 referenced
meshes (relative layout unchanged), upstream BSD-3-Clause LICENSE, and
the g1_description README as UPSTREAM_README.md. PROVENANCE.toml pins
repo/commit/path/license/retrieval-date. Files are verbatim copies;
no URDF edits (mesh-path concerns are handled in loading code later).

Requirement IDs: TC-5 (draft amendment), CON-7 (frozen-set asset add,
authorized verbatim by outer-loop item moxin-org#2)
FROZEN_DIRS gains assets/g1; env_hash.json regenerated (n_files 130).
New test mirrors test_so101_asset_change_changes_hash: URDF or mesh
tampering must invalidate the hash.

Requirement IDs: CON-7 (frozen-set widening authorized verbatim by
outer-loop item moxin-org#2), CON-5
Mirror test_so101_contract.py for g1_right: PROVENANCE pins
unitree_ros commit 5994d4f / robots/g1_description path / BSD-3-Clause,
LICENSE and all 39 vendored meshes verified, right-arm 7 joints match
G1_RIGHT_ARM_JOINTS order, Dex1-1 fingers prismatic [-0.02, 0.0245],
total actuated DOF 33 (29 body + 4 fingers; floating base excluded),
robot name g1_29dof_mode_15 / mode_machine 15.

Requirement IDs: TC-5 (draft amendment), SCN-4, CON-5
g1_right wires 9 of the G1's 33 DOFs; the rest are held at home.
profile_held_joint_names / held_dof_indices join the contract: a
profile with held_joint_names must partition the robot's DOFs (wire ∪
held == all, disjoint), and profile_dof_indices returns wire indices
only. from_wire_joint_order gains base= for held-at-home native
vectors (copy of base, wire positions overwritten); base=None keeps
legacy bit-identical behavior. franka/so101/mobile profiles without
held_joint_names are untouched (legacy coverage rule still enforced).

Unit tests use a fake 33-DOF Robot in the so101 style: shuffled parser
order, overlap/gap rejection, held positions preserved, round-trip,
legacy non-regression. Frozen file: env_hash.json regenerated.

Requirement IDs: TC-5 (draft amendment), CON-7 (authorized verbatim by
outer-loop item moxin-org#2)
Record why scope A wires only the right 7+2 (recoverable, same wire
shape as franka), why g1_29dof_mode_15_with_dex1_1 (up-to-date 5010
wrist machine matching the lane robot, only official Dex1-1
description), and why held_joint_names is an explicit list rather than
hold_unlisted_joints (fail-loud partition vs silent asset drift).
mode_machine must stay 15; frozen-set/TC-5 changes await owner review
under CON-14/CON-7; draft stays off main until then (ADR-66 ordering).

Requirement IDs: CON-15 (ambiguity resolved by ADR), CON-7, TC-5 (draft)
Third embodiment profile: g1_right n_dof=7+2 with identical bridge
semantics; every G1 DOF outside the wire is held at home via
held_joint_names, and wire ∪ held MUST partition the robot's DOFs
(ADR-g1-embodiment, PROPOSED — draft amendment, not for main until
owner review under CON-14).

Requirement IDs: TC-5
The frozen-set widening in 16f0589 added assets/g1 to FROZEN_DIRS but
left harness/CLAUDE.research.md's frozen-path list (and the generated
docs inventory) describing the old set — the drift-proof tests
(test_frozen_set_rule_names_every_frozen_path,
test_committed_inventory_matches_this_tree) caught both. Line-count
neutral edit to the contract; inventory regenerated via
tools/docs_inventory.py --write.

Requirement IDs: CON-7, HAR-5
40c0a0c's one-token edit to harness/CLAUDE.research.md made the
committed mon-treatment-table rendering stale (the table anchors the
contract's bytes). Regenerated via harness monolith table --write;
table_report ok=true, rows=14, errors=[].

Requirement IDs: MON-1, MON-14
The Unitree G1 hardware bridge behind the so101-driver topic surface:
wire 7+2 on the frozen g1_right order, injected ArmBus/GripperBus with
loopback doubles (loopback never imports unitree_sdk2py/cyclonedds),
gradual arm_sdk weight takeover/release, lease watchdog (AISLE_G1_LEASE_S,
no silent re-arm; the reset refusal is the explicit re-arm), mode_machine
15 gate, normalized 0..1 gripper round-trip with URDF-travel finger
mapping, and hardware_monotonic stamps (HWP-7 wording). The three SO-101
gaps (no lease, radians-in-joint-slot gripper, unnormalized state) are
closed by construction.

Real-bus facts cited file:line@commit: unitree_sdk2py@814556d1
(rt/arm_sdk LowCmd_ slots 22-28, weight slot 29, kp60/kd1.5, CRC,
example g1_arm7_sdk_dds_example.py); dex1_1_service@ced3216 is a C++
serial<->DDS bridge with no confirmed Python interface, so
Dex1GripperBus raises HardwarePending rather than guessing a protocol.

- adapters: run_scenario(dof=..., default SO101_DOF, rows==12 pin
  unchanged) + G1-only mode_mismatch scenario outside the SO-101
  SCENARIOS tuple
- registry: g1-driver manifest, g1_right arm enum (schema + 7 fixture
  copies), curated_core, EXPECTED_IDS, reserved_dists mirror/stub,
  confinement DENIED_IMPORTS + fault-injector FROZEN_ASSETS symmetric
  with so101
- tools/g1_runtime.py: out-of-lock SDK2 install/verify/receipt pinned
  to 814556d1 + cyclonedds==0.10.2, gitignored receipt (ADR-67 pattern)
- ADR-g1-driver (PROPOSED): interface choice, lease semantics, finger
  mapping, Dex1-1 status, out-of-lock rationale
- treatment-table regenerated (registry change made it stale)

Requirement IDs: TC-2, TC-5, TC-6, HWP-5, HWP-6, HWP-7, HWP-8

Known-red (expected per outer-loop addendum):
test_freeze_registry.py::test_committed_registrations_check_clean_with_withheld_seeds
(successor registration is G-I's, not self-generated)
…1-D-r1)

Outer-loop review moxin-org#3 fixes, tests written first citing HWP-6/HWP-8/TC-5:

1. Release is now GRADUAL (safety): when not following, the driver writes
   the ramped self.weight at the current pose q7 instead of a hard 0 —
   weight monotonically non-increasing over >= ceil(takeover_s/dt) beats
   (test_release_is_gradual_monotone_after_lease_expiry).
2. Gripper semantics match the CONTRACT (env/CONTRACT.md:19,41:
   0.0 = open ... 1.0 = closed; env/limits.toml:24-26). Which finger
   position is open is a geometry fact: unitree_ros@5994d4f
   g1_29dof_mode_15_with_dex1_1.urdf:1113-1119/:1142-1147 finger axes
   (0,-1,0)/(0,1,0) diverge with +q, so upper limit +0.0245 m = open,
   lower -0.02 m = closed. finger = 0.0245 - grip*0.0445; exact inverse.
3. Startup no longer misjudges the machine: mode_machine None (no
   rt/lowstate yet) -> reason awaiting_state, not following, NOT latched;
   an integer that is present and != 15 keeps the latched refusal.
4. Gripper freezes at its current position on lease expiry instead of
   chasing the stale target (write(read()) while not following).
5. Inventory: ADR heading made parseable (# ADR-g1-driver — ...) with a
   literal Status: line; docs/generated/project-inventory.md regenerated.

Requirement IDs: HWP-6, HWP-8, TC-5 (also TC-2, TC-6, HWP-5/7 unchanged)
Generated files (monolithic treatment table, project inventory) conflicted
between task/g1-a and task/g1-d; resolved by regenerating with
harness monolith table --write and tools/docs_inventory.py --write.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
De-hardcode the harness and the embodiment-agnostic nodes; kinematics-
bearing nodes keep refusing by name until G1-F.

- cli.py: rollout --embodiment adds g1_right (rollout ONLY — the
  research-bound exposure/monolith tables at :245/:299/:320/:325 stay)
- validate.py: EMBODIMENT_ARM maps g1_right -> g1_right
- manifests opened to g1_right (source audit: zero embodiment branches):
  oracle-pose, rollout-client, turn-barrier (+ reserved_dists mirrors);
  excluded with reasons (source branches on franka/so101 or reads
  physics[embodiment]): dora-genesis, arm-driver-sim, gripper-driver-sim,
  camera-source (all share dora_genesis.py so101_latch/profile), reset
  (hardcoded franka home_qpos), verifier-oracle + task-state-machine
  (resolve_layout) — listed in the ACK per the entry
- ik_trajectory: _require_supported_embodiment guards fk_tcp /
  _pose_error / _ik_once / ik_solve; grasp_topdown:
  require_supported_embodiment wired into main() — unknown kinds raise
  ValueError instead of silently solving the Franka DH chain
- tests/unit/test_g1_embodiment_gating.py: 14 tests (parser accept/
  refuse, open nodes pass under g1_right, pinned trio keeps
  EMBODIMENT_MISMATCH, franka/so101 no regression, mirrors in sync)

Requirement IDs: VAL-2, HAR-1 (MOB-4 mapping pinned unchanged)
…zation (G1-G)

- docs/hardware/g1/: the seven SPEC-520 template artifacts adapted to G1 —
  station manifest (mode_machine=15, Dex1-1 version/serial, PC2/Jetson,
  192.168.123.x network, remote controller + e-stop), safety case (balance/
  fall authority stays with the motion controller per HWP-12 wording; hoist,
  damping mode, weight-ramp release hazard), calibration (joint zeros from
  rt/lowstate radians not feetech counts; Dex1 travel pinned to the URDF
  limits; head + fixed overhead camera extrinsics; dual clock-domain time
  mapping), execution ladder (torque-off telemetry -> hoist no-load ->
  loaded; weight-ramp/lease drills), protocol/reset/scorer templates — all
  hardware_pending
- hardware_phase.py: execution_checklist/hardware_report take embodiment;
  the so101 blocker wording stays byte-identical (pinned by
  test_hardware_phase.py:280), g1_right names the G1 station
- cli.py: harness hardware dry-run/report gain --embodiment
  {so101,g1_right} (default so101, existing output unchanged); g1_right
  runs the 15 SPEC-520 scenarios + mode_mismatch at dof=9
- tests/unit/test_g1_hardware_phase.py: 8 tests (templates validate,
  unsigned safety case blocks, 16 scenarios reproducible at dof=9, so101
  default byte-stable with/without --embodiment, blocker parameterized)

Requirement IDs: HWP-8, HWP-18, HWP-20 (G1H-1..8 cited in docstrings)
Status: PROPOSED, HUMAN-REVIEW GATED, under the ADR-66 freeze discipline.
Adapts SPEC 520 to g1_right; only G1 differences are clauses G1H-1..8:
station identity includes mode_machine=15 and the SDK2 runtime receipt
(G1H-1); Dex1-1 interface stays hardware_pending behind the driver's
HardwarePending refusal (G1H-2); gradual weight-ramp takeover/release,
lease expiry freeze + explicit re-arm (G1H-3); balance/fall authority
stays with the motion controller per HWP-12 wording (G1H-4); joint zeros
from rt/lowstate radians and Dex1 travel pinned to the URDF limits (G1H-5);
dual clock-domain time mapping (G1H-6); behavioral-only reset with SPEC 520
scoring consequences verbatim (G1H-7); torque-off -> hoist no-load ->
loaded ladder with weight/lease drills (G1H-8).

G1H-1/3/4/5/7/8 are cited by tests/unit/test_g1_hardware_phase.py
docstrings; G1H-2/G1H-6 are waived in tools/trace_waivers.toml until their
hardware-adjacent artifacts exist. Specs are frozen pending human review
(CON-14): this draft adds a file, it does not amend any accepted spec.
…-E, G1-G)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
G1-E edited sources the monolithic treatment table hashes; regenerated with
harness monolith table --write (MON-1/MON-14 evidence current).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
[embodiment.g1_right] physics profile (ADR-g1-embodiment scope A):
wire = right arm 7 + right Dex1-1 2 (TC-5 draft); 24 held DOFs at
standing home (all zeros, FK-verified soles flat); pelvis fixed at
0.7872 (FK sole probe, collision spheres r=0.005 at ankle z=-0.03);
Dex1-1 stops from the URDF ([-0.02, 0.0245], +q separates => open);
CoACD full decomposition threshold 0.0 (addendum-2 ruling); grasp
center x=0.0978 (finger AABB mid-x), TCP rpy [0, pi/2, 0] (palm +x ->
tool +z, top-down); waist-height shelf/tray/slot lattice inside the
measured shoulder sphere (worst |pregrasp-shoulder| 0.373 <= 0.42
chain); trajectory/ik keys at so101 scale marked 待 G1-F 调.

pharmacy.py: G1_URDF constant; g1_urdf_options (fixed pelvis at FK
height); robot_morph() replaces the silent fall-through — unknown
embodiments now raise ValueError before importing genesis (SCN-4,
CON-12); native_home_qpos() fills held DOFs from held_home_qpos and
overwrites only wire positions (from_wire_joint_order base=), legacy
profiles keep the exact np.empty semantics; held-DOF kp/kv from
held_kp/kv with the held home as the PD target (gravity droop, SCN-4);
both home construction sites (build + _assert_reachable) routed
through it. nexus/rapier dev backends reject g1_right explicitly
(Genesis only).

Tests: test_scene_cfg gains g1_right in all four embodiment loops plus
profile key/length/URDF-partition checks, g1_urdf_options pinning, and
the no-sim unknown-embodiment rejection. Frozen set authorized by item
moxin-org#3; env_hash.json regenerated.

Requirement IDs: TC-5 (draft), SCN-2, SCN-3, SCN-4, CON-12
… gains

Three fixes from the first sim run, each with its evidence:
- Genesis collapses the massless fixed right_dex1_base_link (like SO-101's
  gripper_frame_link): ee_link moves to the surviving right_wrist_yaw_link
  with the composed offset 0.0415 + 0.0978 = 0.1393 along link +x; pitch
  +pi/2 faces the palm down for a top-down TCP (the near column solved at
  rot err 0.44; -pi/2 flipped the palm backward and pushed it to 1.86).
- Slot lattice: the x=0.36 column sat at |target-shoulder| 0.397 against
  the 0.394 vertical-forearm chain budget (IK pos err 0.076), and the
  first fix's x=0.22 column structurally violated the board front edge
  (needs >= 0.225-0.230); cross-column y stagger must exceed 0.0625 (the
  amoxicillin+metformin half-width pair + min_separation). New lattice
  x=0.24/y{+/-0.16,0} + x=0.28/y{+/-0.08}: exhaustive single-pair/pairwise
  probe ALL FEASIBLE, sampler 200/200 seeds, worst |pregrasp-shoulder|
  0.368 <= 0.394. Shelf dropped to waist height (z 0.76) accordingly.
- Held-DOF PD: kp 200 left a 0.0432 rad static droop (waist/hip gravity
  torque ~8.6 N*m / kp); 800/40 holds it under 0.012 rad (200-step sim
  assert < 0.02 passes).

New sim test test_g1_right_profile_builds (SCN-4, TC-5): 33 DOFs, wire
qpos == home, held qpos == held home, reachability_errors == [], held
droop < 0.02 rad after 200 steps. Passes in ~19 s (cached G1 build).
env_hash.json regenerated for the frozen-set physics.toml change.

Requirement IDs: SCN-3, SCN-4, TC-5 (draft)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The G1-F1 motion block: one official-URDF chain shared by the guard and
the planner, world-frame FK, and a Genesis cross-check.

- kinematics.py: UrdfChain gains optional fixed base/tip transforms
  (identity defaults — so101 bit-for-bit unchanged); cached
  g1_right_chain() = torso_link -> right_wrist_yaw_link with the pelvis
  fixed at the FK standing height 0.7872 m and the three waist joints at
  the a7dd234 all-zeros held home (origins only), plus the Dex1 grasp-
  center TCP: fixed right_base_joint 0.0415 + finger-AABB mid-x 0.0978 =
  0.1393 m, rpy [0, pi/2, 0] (derivation in comments); joint order MUST
  equal G1_RIGHT_ARM_JOINTS (TC-5 wire = URDF document order).
- env/limits.toml [embodiment.g1_right]: URDF 7+2 position stops, the
  conservative velocity envelope arm min(URDF, 2.0) rad/s and fingers
  0.2 m/s (rationale appended to ADR-g1-embodiment), workspace AABB
  covering the a7dd234 shelf/tray/staging band and home TCP while
  excluding the torso column and the sub-waist region (calculation in
  comments), fallback_qpos = the a7dd234 wire home, gripper 0/1 with
  0.2/0.0445 normalized rate.
- budget_guard.fk_ee_pose: g1_right branch -> g1_right_chain().forward
  (world frame, same frame as the workspace AABB).
- budget-guard manifest + reserved_dists mirror: arm opens g1_right.
- tests/unit/test_g1_motion.py (5): chain/limits provenance, gripper 0/1
  <-> finger-stop round-trip, deterministic contained FK with legal
  zero-violation and wild-clamped commands, workspace cover/exclusion
  probes, so101/franka non-regression.
- tests/sim/test_g1_fk_parity.py: Genesis cross-validation over five
  deterministic configurations (direct wire-DOF indexing; Genesis drops
  the massless right_dex1_base_link so the probe anchors at
  right_wrist_yaw_link with the full 0.1393 offset) — worst error
  1.222e-07 m / 1.520e-07 rad vs the 1e-4 / 1e-3 gate.
- env_hash regenerated (CON-7); treatment table and project inventory
  regenerated (derivative checks green).

Requirement IDs: BG-1, BG-2, BG-3, CON-5, CON-7, SCN-4
env_hash.json conflicted with G1-C1 (both touch the frozen set); regenerated
with tools/env_hash.py --write. Verified on the merged tree: 391 unit passed,
Genesis-vs-chain FK parity 1.2e-7 m.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TC-5 (G1-C2): profiles with held_joint_names carry a PARTIAL wire.
n_dof becomes the wire length (9 for g1_right) for bridge_info,
joint_state, and the joint_cmd length gate (extracted as
require_wire_joint_cmd for unit-testability); legacy full-coverage
profiles keep n_dof == robot.n_dofs bit-for-bit. home_hold is built as a
full-length native vector (held DOFs at held_home_qpos via
from_wire_joint_order base=); apply_commands overwrites ONLY wire
positions — no command may ever touch a held DOF, whose PD target stays
held home. teleport_reset re-homes both partitions through the same
full-length home.

tests/sim/test_g1_bridge.py (SCN-4, TC-5): a wire command moves the
commanded arm joint decisively (0.1 rad steady-state PD error under
default gains — tuning deferred to G1-F) while every held joint stays
within 0.02 rad; over/under/native-33 joint_cmd payloads are rejected.

Requirement IDs: TC-5 (draft), SCN-4, BRG-1
dora-genesis, arm-driver-sim, gripper-driver-sim, camera-source,
verifier-oracle, task-state-machine, reset carry g1_right beside
franka/so101, with matching reserved_dists mirrors (byte-pinned by
test_validator). Gating tests pin the outcome: a g1_right
expert_t0 validation now reports EMBODIMENT_MISMATCH for EXACTLY the
G1-F-pinned trio (budget-guard, ik-trajectory,
grasp-planner-topdown).

Requirement IDs: TC-5 (draft), VAL-2
…ied arms

G1-C2 (TC-6, ADR-34): get_runtime() builds the BehavioralRuntime from
the run's AISLE_EMBODIMENT instead of hardcoded franka (layout and
home_q), and behavioral requests for embodiments whose RST-2 motion
chain is not verified (reset/motion.py pins Franka limits and a 7-dof
home; so101 and g1_right would be driven by wrong limits and a
wrong-length home) are REFUSED on reset_refused with a reason naming the
embodiment and the teleport alternative — the runtime is never built,
so no Franka limits can leak into a non-Franka graph. Teleport (mode 0)
is untouched.

Requirement IDs: TC-6, RST-2, ADR-34
test_g1_right_schema_conformance (A1, TC-5 draft, SCN-4): with
AISLE_EMBODIMENT=g1_right the bridge announces embodiment g1_right,
n_dof 9 (the wire, never the 33 native DOFs), joint_state is 9 long
with names == G1_RIGHT_JOINTS, gripper_state stays normalized [0,1].

Derivatives for the frozen-set/anchored-byte changes: env_hash.json
(reset/service.py), docs inventory, monolith treatment table.

Requirement IDs: TC-5 (draft), SCN-4, CON-7
…-three residue

Belongs with 5a25d28 (its test file was missed in the split):
BRIDGE_DEPENDENT_NODES parametrized assertions (manifest + reserved_dists
mirror carry g1_right) and the expert_t0 g1_right validation test whose
EMBODIMENT_MISMATCH set is EXACTLY the G1-F-pinned trio.

Requirement IDs: TC-5 (draft), VAL-2
…(G1-C2)

Generated files (env_hash.json, monolithic treatment table, inventory)
regenerated after conflicting with G1-F1. Semantic merge fix: G1-C2 pinned
budget-guard as still refusing g1_right, but G1-F1 added its g1_right
limits and FK, so the still-pinned set is now ik-trajectory and
grasp-planner-topdown (G1-F2 scope).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ZhangHanDong and others added 30 commits October 20, 2026 03:32
The g1_right frozen-set changes (assets/g1, tools/env_hash.py) live on
feat/g1-support, not yet on protected main — the trusted origin/main
baseline refuses the gate tooling itself ('diverges from 24c35a0').
local is the documented dev override (rollout.py HAR-2); the M0-5 so101
case predates the divergence so it keeps the trusted default.

Requirement IDs: HAR-2, SPEC 090 M0-6
Three structural root-cause fixes from the first M0-6 gate (pass1=0.0,
40 collision / 6 wall_clamp):

- home: arm up, wrist pitched near-palm-down, hand off the shelf —
  q [-1.2,-0.1,0,0.3,0,1.5,0,open,open], TCP [0.4356,-0.0892,1.0128],
  hand AABB z >= 0.973 (3.3 cm above the tallest box top 0.94); the
  a7dd234 ready pose deterministically overlapped near-column boxes at
  t~0.06 s. limits.toml fallback_qpos kept == home_qpos; workspace z_max
  1.15 -> 1.30 covers the measured home->rise reorientation arc (peak
  1.2534, free space above the shelf).
- slot lattice: cross-body [0.24,+0.16] removed (lane2 transfer
  limitation), board y widened 0.44 -> 0.56; replacement [0.28,-0.24]
  (outer-loop suggestion) is NOT plannable — probe verdict below.
- sim_warmup renders one frame per camera on the bridge's channels
  (overhead rgb+depth+seg, wrist rgb): the first render pays a one-time
  shader/kernel compile (measured 10.6 s/frame — the wall_clamp root
  cause); ADR notes render compilation must be prepaid too.

New tests/unit/test_g1_scene_plan.py (SCN-4, BG-3): home clearance vs
every slot's tallest box (>= 3 cm, worst-case jitter) PASSES; all-slot
plannability (5 slots x 5 meds through the real StagedPlan + guard
interpolation) — the [0.28,-0.24] slot xfails pending lane2: 16 slot
candidates (transfer IK fails at y<=-0.23, pregrasp IK at x>=0.30,
pairwise separation seals -0.22..-0.18), 23 trajectory/workspace combos
and 5 pregrasp variants all fail; the transfer-stage DLS stall needs the
ik-trajectory solver (measured route profile or smarter continuation).
Warm render evidence: overhead 0.59 s / wrist 0.03 s warm.

Requirement IDs: SCN-4, BG-3, CON-5, CON-8
…ot (G1-H, G1-H-r1)

Gate not yet passing: the [0.28,-0.24] transfer stalls in ik-trajectory
(5 xfail in test_g1_scene_plan, handed to lane2) and the plannable-slot smoke
episode dropped its box (handed to lane1).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Evidence first (run 20261019-215328-9e8aeb, seed 1 relaunch trace):
the Dex1-1 fingers DO close onto the box — normalized grip plateaus at
0.726 against the box surface (contact stops them short of the 1.0
command) — and the box lifts ~0.12 m (z max 0.995), then slides out of
the pinch in transit (z 0.51 -> 0.05 down the slot column, xy pinned at
(0.28,-0.08)). Genesis cannot retain the pinch under motion — the exact
SO-101 precedent (ADR-27/ADR-18). Not a gain/command deficiency, so the
fix is the latch, not the gains.

physics.toml [embodiment.g1_right]: kinematic_carry_latch = true,
close 0.65 (below the measured 0.726 on-box plateau), release 0.20
(planner pregrasp_cmd = 0), max_distance 0.15 (target center ~0.03 m
from TCP at contact; slot neighbours >= 0.08 m, nearest-wins cannot
cross-latch).

dora_genesis.py: the latch path was already generic through
ee_link/ee_frame_offset_xyz/the normalized grip — only the naming and
error strings were SO-101-specific. so101_latch/so101_hand_link/
so101_tcp_offset -> carry_latch_obj/carry_hand_link/carry_tcp_offset;
error and log lines now name cfg.embodiment.

ADR-g1-embodiment gains the G1-H-r2 section: the latch is a simulation
aid only and NOT evidence of real-robot grasp capability (real
retention is a force-controlled pinch measured in the hardware phase).

Derivatives regenerated (env_hash, monolith treatment table, docs
inventory). Requirement IDs: SCN-4, CON-5, CON-8
The G1-H-r1 same-side slot [0.28, -0.24] stalled the staged plan the
same way the removed cross-body column did; three measured planner
fixes lift it, and test_g1_scene_plan.py goes to 0 xfail (26 passed).

- tray roll retries both tool-z parities: the free-yaw transfer lands
  the wrist ~0.94 rad of yaw off the place frame and the direct in-place
  roll misses its basin while the 180-degree spin converges and the
  lower stage follows (symmetric Dex1 fingers make them the same
  physical release)
- the lower stage retries both parities too: the tallest box
  (cetirizine, place_z 1.075) descends only in the flipped parity; the
  cold alternatives sit 3-4 rad away on other branches (unusable)
- rise solves over the FIXED seed ladder (both parities) and takes the
  FIRST solution whose executor-interpolated arc keeps the TCP inside
  the workspace AABB: the home-seed solution arcs to z=1.341 against
  the 1.30 ceiling while another seed's stays flat at 1.144 — the
  Cartesian-ladder alternative stalled full-rows at substep 1 (home
  sits 1.98 rad off the neutral frame)
- tests/unit/test_g1_scene_plan.py: the five lane1 xfail markers are
  DELETED (assertions unchanged); the whole 5x5 lattice plans guard-clean
- ADR-g1-embodiment: G1-F3 resolution section records the three fixes
  and the measured numbers; the cross-body limitation note stays for
  the removed column

Requirement IDs: SCN-4, BG-3, TC-5
ADR-g1-embodiment conflicted on sibling sections (G1-F3 resolution and
G1-H-r2 latch); both kept. Generated files regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s it (G1-F4)

A grasp plan that runs to completion without a verdict made the node
emit plan_done unconditionally (45ecf36 / moxin-org#161 HAR-3). expert_t0's
ik-trajectory declares only joint_cmd,gripper_cmd,turn_done, so under
lockstep aisle.turns raised ProtocolError 'unknown or reserved output
ik-trajectory/plan_done', the node died, and the episode sat in wall
clamp (lane1 smoke 20261019-224058-539bc6). franka/so101 verdicts
normally land first — the same latent defect ships on main for every
failure path; the G1 failure set exposed it.

- plan_done_declared(turn_outputs): the declared set is
  AISLE_TURN_OUTPUTS — the same variable the typed host pins from the
  graph config in BOTH modes and the dora-genesis bridge already guards
  against (ADR-30 precedent)
- the :1651 send is gated on it; graphs that wire plan_done (the 15
  eval/shield/expert graphs) keep the HAR-3 retry arming byte-identical;
  expert_t0 and any undeclared graph get silence instead of a crash
- tests/unit/test_ik_plan_done.py (4): env-list parsing; completed grasp
  plan under the expert_t0 declaration sends joint/gripper only; under a
  declaring declaration still emits {'ok': true}; so101 recorded
  radial-front grasp respects the declaration. Harness is an IDEAL
  ACTUATOR node (joint_state echoes the last joint_cmd, generated
  lazily until the plan completes) driving the REAL main() loop; the
  module docstring records why unit tests suffice instead of a
  sim/graph smoke (the crash mechanism is the send decision under the
  exact env the host exports, not transport or physics)
- graphs/expert_t0.yaml and turn plans untouched (frozen)

Requirement IDs: HAR-3, ADR-30
Outer-loop moxin-org#9 root cause: two lanes' parameters were individually sane
and jointly contradictory — gripper_grasp_cmd 0.55 closed BELOW
carry_latch_close 0.65, so the latch could never fire (the r3 smoke
dropped the box).

Per-med contact plateaus, derived from the finger qpos at two-face
contact (f = grasp-axis width/2, g = (0.0245 - f)/0.0445):
omeprazole 0.045 | amoxicillin 0.101 | ibuprofen/metformin 0.157 |
cetirizine 0.213. The table lives in physics.toml and the ADR.

- gripper_grasp_cmd 0.90: a SQUEEZE above every plateau (PD keeps
  holding force after contact; 0.55 was a dead stop below the latch
  threshold).
- carry_latch_close 0.03: under the LOWEST plateau (0.045) — the latch
  engages mid-close BEFORE two-face contact, mirroring so101's design
  (0.68 under the measured 0.75 onset). Empty-hand close cannot
  mis-latch: the planner opens fully (pregrasp_cmd 0) before every
  approach, and max_distance 0.15 bounds the capture to the pinched
  volume.
- carry_latch_release 0.01: wide hysteresis, no chatter.

tests/unit/test_g1_grasp_consistency.py (SCN-4, ADR-18): per med,
plateau > latch_close and grasp_cmd > plateau; hysteresis band; the
so101 chain pinned (close under the 0.75 onset, cmd past close,
release under close); the ADR table cross-checked against the
derivation; plateaus monotone in width.

tests/sim/test_g1_carry_latch.py: widest and narrowest meds through
the bridge's REAL path (control_dofs_position full-length targets,
held DOFs at held home, the exact grip normalization feeding a real
CarryLatch every tick, set_pos/set_quat applied as the bridge applies
them) — the latch fires mid-close and the box rides a 0.1 m commanded
lift (box rise >= 0.9 x actual TCP rise; the arm's gravity
steady-state sag is the known G1-F deferral).

Derivatives regenerated (env_hash, docs inventory).
Requirement IDs: SCN-4, ADR-18, CON-5
Outer-loop review of G1-H-r4: with carry_latch_close at 0.03 the latch
fires as the fingers start closing, so carry_latch_max_distance_m is the
only check that the TCP is at the box. 0.15 m would credit grasps several
centimeters off and flatter the M0-6 gate. 0.05 m keeps true grasps (TCP
~0.03 m from the box center; test_g1_carry_latch still latches both the
widest and narrowest med) and is pinned by a consistency test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd for g1_right (G1-F5)

Two lockstep-stalling defects from the 20261019-234952 smoke:

ep1 — the 13-stage plan did not finish inside the tier's 60s episode:
- measured with the real StageStreamer cadence under a sagging actuator
  (qpos = cmd - 0.08 rad, the documented MJCF gravity droop): worst
  32.4s across the whole slot lattice at the 0.5 velocity scale,
  28.3s at 0.8, 27.0s at 1.0; transfer alone 10.7s -> 5.6s
- free-space stages (retract/transfer) now run at 0.8 rad/s — 40% of
  the guard's 2.0 rad/s qdot envelope (profile keys, authorized); the
  episode budget and guard limits are untouched
- test_every_slot_execution_fits_the_episode_budget: systematic —
  every slot x med's modeled duration fits tier_budgets('T0') minus a
  20s stabilization margin (read from the rollout tiers, not hardcoded)

ep0 — first-launch planning burned 9 minutes of lockstep wall with no
crash and no verdict:
- root cause: ONE unreachable cold ik_solve walks the full 32-seed x
  2-rotation ladder (measured 37.5s wall) and the staged chain
  multiplies it across stages and parity retries, unbounded
- StagedPlan now runs under the profile's ik_plan_budget_s (30.0s;
  legitimate plans measure 4.7-5.8s — ~5x headroom): every
  stage-boundary failure names the budget when the deadline has
  passed; the unreachable-pose test pins a loud plan.error well inside
  90s wall (measured ~40s)

ADR-g1-embodiment gains the G1-F5 section recording both budgets and
the measurements. env_hash/table/inventory regenerated.

Requirement IDs: SCN-4, BG-3
env_hash.json conflicted with the latch fix (both touch physics.toml);
regenerated. Verified merged: 144 unit + 14 sim passed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…gle open (G1-F6)

The lane1 smoke 20261020-011256 saw ik-trajectory emit a TRIANGLE
gripper_cmd (0.00 -> 0.90 over 23 ticks, straight back down 23 ticks):
the box was lifted to retract height, never carried across, and the
latch released it mid-air back onto its own slot.

Reproduction with the F4 IdealActuatorNode driving the REAL main() loop
proves the single-plan stage table is CORRECT — the close ramp holds
flat through lift/retract/transfer (1014 ticks) and only descends at
the tray during release. The triangle's down-edge is a RESTART: a plan
that finishes via stage bails (tracking failure in sim) leaves the
fingers closed; the next grasp_pose is accepted, and its free-space
stages (rise/staging/pregrasp/preclose) command gripper 0.0 — on g1
that IS the profile's gripper_pregrasp_cmd — opening the still-latched
box in mid-air.

- _held_replan_stages(): when a replan starts with the gripper closed
  (held_grip >= half of grasp_cmd), every stage before close is
  rewritten to hold grasp_cmd, so the latch survives the re-approach
  until the new plan's own close/release semantics take over;
  held=False is the identity
- main(): tracks held_grip across plans (set when a streamer finishes,
  cleared on reset_done episode boundaries and after each replan)
- tests/unit/test_g1_gripper_hold.py (5): the hold-window invariant
  (monotone rise, never a dip below carry_latch_close until the single
  release descent) on g1_right/so101/franka; a duplicate grasp_pose
  mid-execution does not restart (one up-ramp, one descent); the
  held-replan rewrite pinned unit-level (pre-close stages hold
  grasp_cmd, close-onward unchanged, identity when not held). Docstrings
  cite HAR-3 and ADR-18.

Requirement IDs: HAR-3, ADR-18, SCN-4, BG-3
Evidence: every bridge process's FIRST episode lost its first render
window to a one-time shader/kernel compile (~10.9 s/frame, first-window
tick ~2.8 s — the ep0 wall_clamp of the r3/r5 smokes). tools/
sim_warmup.py pays this in ITS OWN process and cannot cover the bridge:
the compile cache is per-process. In a 50-seed gate this costs one
episode + one relaunch per launch.

dora_genesis.py warmup_render(): after the scene build and AFTER
topic_rates is final (lockstep-filtered to the graph's declared
outputs), BEFORE the first turn / first reset_done, render ONE
throwaway frame per ENABLED camera channel — overhead rgb+depth+seg in
one pass, wrist rgb — mirroring render_frames' channel logic exactly.
The result is DISCARDED: nothing published, sim time not advanced, no
step(), no RNG/state touched (CON-5); the only side effect is the
renderer's compile cache. Per-channel seconds go to stderr and to a new
OPTIONAL bridge_info field render_warmup_s (existing fields'
semantics untouched). Applies to every embodiment — franka/so101 pay
the same first-frame compile and benefit identically; no g1-only gate.

tests/sim/test_g1_render_warmup.py: first real window after warmup
costs the same order as the second (< 1.5 s vs the 10.9 s compile
cliff; threshold basis in-file), and same-seed observations
(joint_state bytes, oracle_state bytes, first rgb frame bytes) are
BIT-IDENTICAL with and without the warmup — the warmup frames are
discarded and change nothing (CON-5).

test_bridge_info_shape pins the new optional field (None outside a
run). Derivatives regenerated (env_hash, monolith treatment table,
docs inventory).

Requirement IDs: CON-5, BRG-6
Treatment table conflicted with G1-F6; regenerated. Outer-loop fix folded
in: tests/sim/test_g1_render_warmup.py:33 exceeded the 100-column limit
(ruff E501), which the lane's ACK reported as passing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ause (G1-C4)

G1-F6 traced the gripper-reopen loop to TRACKING failure: the 7 wire arm
joints rode Genesis's kp=100 URDF default (nexus_physics.toml) — the
profile carried gripper and held-DOF gains but none for the arm —
giving the ~0.075 m gravity droop per 0.1 m commanded lift. SO-101's
light arm made the default adequate; the G1's 3.55 kg arm + 0.73 kg
Dex1 did not.

Sizing from MEASURED static torques (heaviest box pinned to the hand,
home/shelf/tray 2 s holds; per-joint err x kp): shoulder pitch ~11.8
N·m, roll ~21.8, yaw ~15.0, elbow ~5.5, wrist roll ~4.2, pitch ~0.7,
yaw ~0.9. A first geometric estimate (kp 1500/1500/1000/800/200/100/100)
measured OVER budget at shelf/tray (shoulder_roll 0.0145 rad,
shoulder_yaw 0.0149, wrist_roll 0.0208) and was replaced by the
measured sizing — kp = tau/0.005 rad (2x under the 0.01 budget):
[2500, 4500, 3000, 1200, 900, 250, 250]; kv = 2*sqrt(kp*J_eff) at
zeta ~1.0-1.5: [50, 70, 55, 35, 12, 8, 8] — no overshoot ring at
dt=0.01 across the 200-step holds.

pharmacy.py applies them to EXACTLY the 7 wire arm DOFs, guarded on
arm_kp presence — profiles without the keys (franka/so101) are
bit-identical. tests/sim/test_g1_arm_gains.py: the three-configuration
static-accuracy holds (per-joint < 0.01 rad, TCP < 1 cm) plus a
gain-introspection check (arm gains on the wire DOFs, gripper gains
untouched).

Derivatives regenerated (env_hash, monolith table, docs inventory);
ADR-g1-embodiment gains the G1-C4 section.

Requirement IDs: SCN-4, CON-5
…aim entries (G1-I-a)

Gate-independent closeout halves (hash/registry/benchmark regeneration
stays in G1-I-b after the gate):

1. planning test speed: the 5m57s unit pair now runs 20s — the (slot,
   med) plans share one solve per session (_PLAN_CACHE) and the whole
   25-combination lattice + duration model moved VERBATIM to
   tests/sim/test_g1_scene_plan.py (25 real IK plans at ~5.5s each
   cannot fit the unit marker's <1s definition); the wall-budget test
   moved to tests/sim/test_g1_plan_wall_budget.py for the same reason
   (its assertion waits out the real ~40s budget). Nothing weakened:
   28 sim-side tests pass (215.58s), assertions unchanged; the unit
   file keeps the planning-free home-clearance test.
2. hardware dry-run/report top-level JSON carries embodiment and dof
   (outer-loop note from G1-G): dry-run builds them into the report;
   report stamps them AFTER report_hash so every other field — and the
   so101 default — stays byte-identical.
3. docs: README phase-status G1 row (branch-only, ADR PROPOSED, gate
   unrun, hardware_pending); getting-started 3d section (g1_runtime
   install, sim_warmup --embodiment g1_right, rollout --embodiment
   g1_right); claim-evidence gains g1-right-embodiment-simulation
   (unrun) and g1-right-hardware (hardware_pending) — catalog
   regenerated, --check ok, 20 claims.
4. PR draft (not in repo): ~/.octos/outer/g1/pr-draft.md — spec-change /
   env-change / feature split over the 151-file diff vs main
   (merge-base 24c35a0), plus the fixes that also apply to main
   (undeclared plan_done send, held-replan gripper open) and the frozen
   registry / benchmark v1 drift note for G1-I-b.

Requirement IDs: CON-12, HWP-20, CLM-1
Planning-grid tests moved to tests/sim (each solves real IK, ~5.5 s), unit
side now 20 s; dry-run embodiment/dof fields; README, getting-started and
claim-evidence entries; no assertion weakened.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The r6 smoke placed the box correctly on the tray, then the arm's
return swept it off (outer-loop trace read: NOT a gripper triangle —
the home swing knocked the seated box off the tray).

New systematic test (tests/sim/test_g1_scene_plan.py, three-zone
semantics): for every slot x med, every post-release waypoint and the
executor's interpolated stream must (a) never deepen the inherent
ringing overlap at the release hover by > 5 mm (the open fingers
physically ring the box), (b) never re-overlap once the hand's xy
projection leaves the box, and (c) never fall back under 2 cm after
first reaching it. Was red: -0.0514 m at the clear stage, +0.0062 m
mid-home-swing.

Fixes, each probe-falsified-alternatives-first:
- clear stage: was q_transfer (TCP z 1.00, BELOW the tallest box top
  1.09) -> lateral retreat ladder (+y toward the arm, z+0.04; the
  first solvable of 4 candidates), replacing the geometrically
  falsified pure-vertical (no IK above z~1.12 at the tray column) and
  the old below-top hop.
- home stage: the raw retreat->home joint hop bowed to -y past the box
  column; chord waypoints stall (ik_continuation: no solutions on this
  span even with live parity), longer retreats don't help, z>=1.18 is
  unreachable. Winner: one IK-SOLVED high central waypoint TCP
  [0.30, -0.06, 1.16] (rotation = live retreat orientation, HOME seed)
  splitting the hop into two short ones: measured min gap +0.024 m on
  retreat->mid (cetirizine, worst med) and +0.18 m on mid->home.
  Fallback to the rise-top route if ik_solve ever returns None.
  so101/franka keep their gate-proven single-hop home, bit-identical.

Withdrawal lattice: 25/25 pass (worst tail +0.024 m). Derivatives
regenerated (env_hash, monolith experts/treatment table, inventory).

Requirement IDs: SCN-4, BG-3, CON-5
…G1-F7)

The f7 smoke (20261020-053259-20f22a) proved the PLACEMENT fix works —
all three goal boxes seated correctly on the tray (< 3 deg tilt) and
the withdrawal no longer sweeps them off at the clear stage. The two
remaining dropped episodes traced to the retreat itself: the live arm
tracks ~0.027 m BELOW the commanded TCP z at the tray column (G1-F2's
measured DLS plateau), so the dz +0.04 retreat planned +0.024 m of
clearance but physically overlapped the seated box (-0.056 m measured
at ep2's row 508, box still seated at z 1.035 while the hand swept
through its column for ~70 ticks).

dz +0.08 absorbs the plateau with margin; the retreat ladder keeps
+0.04 as fallback rungs. Withdrawal lattice re-run: 25/25 pass.
… cuts (G1-F8)

Planning runs SYNCHRONOUSLY inside a lockstep turn, so a >10s plan
freezes the dataflow at every episode start (outer smoke outer-smoke-f7b:
turn watchdog expired at 1:8 while 'plan ready' landed 3s later; the
lane1 F7 retreat ladder pushed the lattice worst to 15.7s). The F5
ik_plan_budget_s=30 contradicted the 10s watchdog — both budgets were
individually sane, combined they were not.

Work cuts, all measured at the real consumption point:
- _dls tolerance EARLY EXIT at 0.5x tolerance (a just-inside-tolerance
  stop left the TCP 2.5cm off target and ate the F7 2cm withdrawal
  margin — measured tail_min 0.0004 vs the 0.02 gate; half-tolerance
  keeps nearly all saved iterations and restores the geometry). Enabled
  for g1_right and so101 (so101 full-budget plans measured 12-15s ->
  0.15s); franka stays bit-for-bit.
- ik_max_solver_iters 200 -> 80 and ik_max_starts 32 -> 16 (sweep: whole
  lattice still plans; the starts key is now actually READ by _g1_seeds
  — the range was hardcoded 31 while the docstring claimed the key)
- batched FK path (UrdfChain.forward_batch + _axis_rotation_batch): one
  chain walk for the 7-perturbation Jacobian and the 4-way backtracking
  line search instead of serial calls (~60k forward calls per plan);
  equivalent to the serial path within 1.6e-11 over 80 iterations
- the 5.5s budget (0.55 x ORDINARY_TURN_WATCHDOG_S, now a named
  constant in turn_barrier) is threaded through EVERY solver call —
  seed ladder, per-DLS-iteration, line search, rolls, the rise arc
  sweep — so a stalled plan fails LOUD inside the budget instead of
  burning through the watchdog (verified under load: unreachable pose
  fails at 5.66s with the budget named)

Tests (sim, real solving): test_plan_budget_respects_the_turn_watchdog
(budget <= 0.6 x watchdog, read from turn_barrier, not hardcoded) and
test_planning_wall_clock_fits_the_watchdog_cap (g1 whole-lattice worst
wall + so101 recorded radial + franka probe, all under the same cap);
the wall-budget test accepts both loud failure modes (budget expiry or
ladder exhaustion — the 16-start ladder now exhausts inside the
budget). Full lattice: zero planning failures, worst 5.53s vs the 6.0s
cap. ADR-g1-embodiment gains the G1-F8 section.

Requirement IDs: ADR-30, SCN-4, BG-3
G1-F8 enabled the DLS early exit for so101 as well, changing the depth of
so101 IK convergence on an embodiment with a measured M0 record. Narrow it to
g1_right so so101 and franka stay bit-for-bit. The so101 leg of
test_planning_wall_clock_fits_the_watchdog_cap becomes a strict xfail that
records the main-branch finding (so101 full-budget plans take 12-15 s, past
the 10 s turn watchdog).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…(G1-C5)

Two bridge-side findings from the outer-loop pinned smoke
(outer-smoke-g1, dora 48e96b5):

1. The C3 in-process render warmup ran AFTER node = Node(). The 11.2 s
   first-render compile blocked the event loop, >1100 10 ms tick events
   piled into the zenoh channel, and the bridge DROPPED the first reset
   ('event channel full') — episode 0 sat at 'holding at sim step 0
   until the first reset' until the wall budget died. Fix: hoist the
   warmup to BEFORE Node() — no subscription exists yet, nothing can
   pile up. The channel set derives from env + cfg alone
   (rung_topic_rates + the lockstep AISLE_TURN_OUTPUTS filter);
   nothing between the scene build and the warmup needs the node. The
   result stays discarded (CON-5 zero state impact — the C3
   bit-identity sim test still pins it). New
   tests/unit/test_g1_warmup_order.py pins the source-level ordering
   (warmup call precedes Node(); exactly one call; channel derivation
   pre-Node).

2. The 9 cm place offset hypothesis (latch captures an un-centered box
   at the 0.03 grip crossing; the carry drags it; the F3 tool-z flip
   doubles it) is REFUTED by forensics on the pinned relaunch-1 traces
   (grip/joint/oracle joined on sim_time): capture offset 0.002 m,
   release carried offset 0.003 m (drift 0.001 m over the transit), and
   the RELEASED box sits 0.022 m from the tray center — essentially on
   it. The 9 cm error appears only AFTER the release (final rest
   (0.221, -0.325, 0.035), on the floor): the box was placed correctly
   and then swept off during the post-release withdrawal — the G1-F7
   failure family; that run predates 0d38613's dz +0.08
   tracking-plateau margin. No latch change made (millimetre offsets,
   nothing to center); the latch stays a simulation aid only, NOT
   real-robot grasp evidence.

Derivatives regenerated (env_hash, monolith table, inventory).

Requirement IDs: CON-5, BRG-6, SCN-4
…-F9)

The smoke run showed the arm sweeping non-target boxes and, after a
correct place, the home swing knocking the box off the tray.

- ik_trajectory: g1 home chain becomes retreat -> tuck -> step -> home
  (lane2 WIP), then each home hop goes through a containment check. The
  check simulates the executor's 0.01 rad joint march. A hop whose TCP
  rises above workspace_max.z - 2 cm (the tuck -> step hop peaked at
  ~1.40 m against a 1.30 m ceiling) is split at an IK waypoint on the
  Cartesian midpoint. Split sub-hops must also stay inside their
  endpoints' TCP box + 3 cm, so an inserted waypoint cannot introduce a
  new swing back over the placed box. Depth <= 3, deadline-bound.
- physics.toml: trajectory_staging_z 1.00 -> 1.02.
- tests/sim/test_g1_scene_plan: arm-link vertex clearance against
  non-target boxes; three-zone post-release withdrawal >= 2 cm.
- Regenerated monolith tables and env_hash.

Gates: ruff format/check clean; tests/sim/test_g1_scene_plan.py +
test_g1_plan_wall_budget.py 61 passed, 1 xfailed (so101 main-branch
finding); targeted units 137 passed; env_hash --check 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…1-F9)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

# Conflicts:
#	analysis/monolithic-control/treatment-table-v1.json
…(G1-F10)

Outer smoke f5ceb2d was 1/3; ep0 and ep2 failed as dropped. Oracle
traces plus an in-process Genesis replay of the ep0 command stream
showed the cause:
- The Dex1 palm (wrist_yaw + Dex1 base, one merged collision link)
  sits only 0.027 m above the G1 TCP. The shared 0.035 grasp depth
  seated the palm about 4 mm into the box top at grasp.
- The kinematic latch carried that overlap to the tray.
- After release, the lateral clear dragged the box off the tray
  (right_wrist_yaw_link in contact from release on).

A vertical lift-off is kinematically unavailable at the tray column:
shoulder roll and wrist yaw sit at their limits there, and a
tilt-locked lift does not converge.

- physics.toml g1_right: grip_engagement_m = 0.015 (about 0.012 m palm
  clearance; cetirizine's TCP-to-center 0.040 m stays inside the 0.05 m
  latch radius). Tray lowered 0.95 -> 0.85 so every release TCP
  (0.985-0.995 m) is below the 1.00 m transfer hover and the lower stage
  is a true descent.
- grasp_topdown: the grasp depth is profile-driven (default unchanged).
- ik_trajectory: the home-adjacent step waypoint is solved from the
  home, tuck and measured good-branch seeds. It keeps the branch whose
  hop into home sags least. After the tray change, omeprazole landed on
  a shoulder-yaw-flipped branch that swept the (0.28, 0.08) column
  (-50 mm).
- tests/sim: palm stays >= 5 mm above the placed box top through the
  whole post-release stream (red at 0.035: 15/25).

Gates: ruff clean. G1 sim set (scene_plan, wall_budget, carry_latch,
ik_genesis, bridge): 93 passed, 1 xpassed (the non-strict so101
watchdog xfail at its boundary). Unit targets: 103 + 19 + 108 passed.
env_hash --check 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… a slot (G1-F10b)

Outer smoke 0acb406 was 2/3. Both drops were fixed; ep2 failed as a
collision. With the shallow grasp the carried box hangs 2 cm lower, and
the straight shelf->tray chord carried cetirizine over the
(0.24, -0.16) column (bottom 0.924 vs ibuprofen top 0.92), pushing
ibuprofen more than 3 cm. Raising the transfer is not available:
trajectory_transfer_z 1.04 fails IK on the (0.24, 0.0) and (0.28, 0.08)
columns.

- ik_trajectory (g1_right): when the straight chord passes within
  trajectory_transfer_chord_clear_m (0.08) of another slot centre, the
  transfer goes through a front waypoint (trajectory_transfer_front_x =
  0.13, at the slot's own y) and then to the tray hover. Measured
  chords: (0.28, -0.08) 0.004, (0.28, 0.08) 0.011, (0.24, 0.0) 0.048,
  (0.24, -0.16) 0.075 -> detour. (0.28, -0.24) 0.089 -> direct, because
  its -x back-out grazed the (0.24, -0.16) column with the fingers.
- tests/sim: new test that the carried box (axis-aligned; the latch keeps
  its world yaw) keeps >= 1 cm from every non-target box from lift to
  lower. Red without the route (20/25), green with it.
- tests/sim withdrawal: the tail zone is entered at 2.5 cm instead of
  2 cm (5 mm hysteresis), because executor wobble at the crossing
  measured 0.1 mm while the hand was still retreating. The 2 cm floor is
  unchanged, and the corridor rule (never re-overlap) covers 2-2.5 cm.

Gates: ruff clean. G1 sim set: 118 passed, 1 xpassed (non-strict so101
watchdog boundary). Unit targets: 230 passed. env_hash --check 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rry (G1-F10/F10b)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e planning (G1-F11)

The 50-seed gate on e6363bb scored pass1 0.76 (bar 0.80). All 12
failures targeted slot (0.28, 0.08). A Genesis hold probe showed
right_shoulder_roll_link touching torso_link from shoulder roll ~0.0
rad: the plan asked +0.198, the arm stopped at +0.039, and the advance
and close stages bailed after timing out.

- physics.toml g1_right:
  - ik_shoulder_roll_max_rad = -0.05 (planning-only bound; the guard's
    env/limits.toml envelope is unchanged).
  - Slots re-latticed inside the measured reachable band (x 0.24-0.28,
    y -0.24..+0.02) at a 0.065 m pitch (operator ruling 2026-10-22).
  - trajectory_transfer_chord_clear_m 0.07.
  - executor_max_joint_vel_rad_s 1.5 (75% of the 2.0 rad/s guard
    envelope; operator ruling 2026-10-23 to speed up rather than raise
    the 150 s wall budget).
- ik_trajectory:
  - The front-routed transfer backs out at staging height.
  - The home step waypoint is chosen by a contained chain that stays
    under the ceiling and off the shelf slots. Warm single-DLS rungs
    come first; containment splits low sweeps over slots, with slerped
    midpoint orientation.
  - The _q_mid gate tries a warm DLS before the cold ladder.
  - The clear's first rung backs off 2 cm in -x.
  - Worst plan 5.87 s -> 3.77 s, median 4.06 s -> 2.82 s.
  - executor_max_vel() reads env, then the profile, then 1.0.
- tests:
  - New shoulder-roll bound test.
  - The arm-link sweep now covers all 25 slot x med cases and requires
    8 mm in advance/close/lift/transfer, 15 mm elsewhere
    (ADR-g1-shelf-lattice; a 10-episode smoke moved non-target boxes
    <= 0.4 mm).
  - New executor-speed unit test.
- docs/decisions/ADR-g1-shelf-lattice.md.

Gates: ruff clean. tests/sim/test_g1_scene_plan.py +
test_g1_plan_wall_budget.py all green on the pre-speed tree (157 passed,
1 xfailed). Unit targets: 231 passed. env_hash --check 0.

PENDING: the render-dependent sim tests (carry_latch, ik_genesis,
bridge) errored with no display attached (pyglet screens == 0). The smoke
and gate on this commit are not yet run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Smoke on 2ee2cb9 scored 8/10. Seed 5 was dropped. On the way up from
home, the wrist flip jammed on the shelf's upper rear rail (wrist yaw
stuck 0.71 rad), so every grasp stage bailed and the misplaced hand
pushed the target off the shelf edge. Geometry check: the home -> rise
flip sweeps 58-67 mm through that rail (z 1.10, x 0.41-0.42) for every
slot x med. Earlier lattices usually scraped past it.

- physics.toml g1_right: shelf_level_heights [0.06, 0.34] ->
  [0.06, 0.60]. The rail now sits at z 1.36, above the 1.30 workspace
  ceiling. SCN-3's staggered two-level shelf is kept: removing the level
  would be a spec change.
- tests/sim: every shelf board joins the arm-link sweep obstacles (red
  on the old rail height: 25/25, -58..-67 mm).
- ADR-g1-shelf-lattice section 4. docs inventory regenerated.

Gates: ruff clean. G1 sim set (scene_plan, wall_budget, carry_latch,
ik_genesis, bridge): 163 passed, 1 xfailed. Unit targets: 272 passed.
env_hash --check 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… faster executor (G1-F11/F11b)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant