Skip to content

Repository files navigation

microduck-cli

One CLI for the MicroDuck robot — any agent, any human, the same verbs.

flowchart LR
  A["an agent<br/>(Claude, Codex, ...)"] --> C
  H["a human<br/>(a terminal)"] --> C
  C["<b>microduck-cli</b><br/>env · duck · policy · rules"]
  C -- "JSON-RPC over<br/>~/.cache/duck-sim/duck-a.sock" --> D["<b>robotd</b><br/>API 16"]
  D --> F["--fake body"]
  D --> S["MuJoCo body<br/>(microduck_rl duck-body)"]
  D -. "not driven yet" .-> R["a real MicroDuck"]
Loading

One unix socket, the daemon's own JSON-RPC, no robotics SDK — dependencies = [] is the whole runtime dependency list. Everything below was checked at microduck-cli 0.9.4 against its own --help, the real daemon, and the MuJoCo simulation on three machines.

Install

uv tool install microduck-cli     # or: uvx microduck-cli whoami
microduck --version

microduck and microduck-cli are the same entry point. From a checkout, prefix with uv run — the form docs/operating-the-duck.md uses:

git clone https://github.com/agentculture/microduck-cli && cd microduck-cli
uv sync && uv run microduck whoami

Python ≥ 3.12, Linux on aarch64 — the only platform on record (see Proof).

What it is

Five noun groups. Every verb takes --json; results go to stdout, diagnostics to stderr, never mixed. Exit 0 success, 1 your error, 2 the environment's.

Noun What it does Verbs
env start, check and stop the stack overview doctor up down status hosts
duck operate one duck, in robotctl's words overview health version monitor init relax enable do mode look stop move quack configure record
policy the policy lifecycle and the microduck_rl train lane overview list load reset add remove search check update pad smoke train play export publish infer install
rules the data-only rules layer and its 50 Hz tick engine overview list check engine intent
cli introspection — agents start at microduck learn / explain overview

What it is not

  • It has never driven a real duck. Every verb is exercised against the MuJoCo simulation — the real robotd --sim daemon driving a duck that stands, holds 50 Hz and runs skills — plus a --fake body for the unit suite.
  • It cannot walk yet. duck move reaches the daemon and selects the walk network, but the joint targets come back static. Details below.
  • It cannot load policies on this daemon. robot.loadPolicy and friends need API ≥ 18; the pinned build answers API 16, and those verbs say so and exit 2.

If you do have a duck: motion is gated — no --apply, no movement — and duck relax drops torque, which makes the duck fall over.

Try it in simulation

Prerequisites. Two upstream clones at the pinned commits (docs/upstream-pins.md) and a Rust toolchain. Without them env doctor fails seven of its thirteen checks — that is the box, not the CLI. Point MICRODUCK_CLONE and DUCK_SIM_RL at the clones and ask:

microduck env doctor      # 13 checks: clone pins, cargo, daemons built, RL venv, port, state dir

Then, in order — this numbered walkthrough is the plain-text equivalent of the diagrams above and below, for readers whose renderer shows a mermaid fence as code:

  1. Bring up a duck. --sim is the path the verification records below cover — the real daemon driving the MuJoCo body. Drop --headless to watch it in the viewer, or use --fake for a body that needs no simulator at all.

    microduck env up --sim --headless
  2. Ask the robot for its own verdict.

    microduck duck health
  3. Stand it up, then hand it its policy. Both are gated, hence --apply.

    microduck duck init --apply
    microduck duck enable --apply
  4. Run the tick engine briefly. Connect, hello, health, init, enable, armed — each step logged, then 50 ticks at 50 Hz.

    microduck rules engine run --max-ticks 50 --apply
  5. Inject one intent through the same admission registry a rule fires through.

    microduck rules intent stop
  6. Tear it down. Never kill by name — env down is the supported path.

    microduck env down

docs/operating-the-duck.md walks the same six commands with each one's exact output and what to do when a check fails; the first-party operate-microduck skill is the same ground for an agent, including the screenshot recipe for watching the MuJoCo window from a headless session.

Proof — three boxes

Everything in this section is copied from the verification records in docs/verification/, not retyped. Home directories are shortened to ~; nothing else is changed. Each record names the box, the upstream pins, the daemon API and the CLI commit it was recorded at — and nothing re-runs them, so a re-pin ages them silently. Check the record's date against the pins table before trusting a number here.

Box Reached Result Caveat
DGX Spark (GB10, aarch64) all six checks + train smoke pass; live suite 12 passed, 0 failed walking xfail
Jetson AGX Thor (JetPack 7) all six checks, three tiers, headless pass; 12 passed, 1 xfailed ran on an uncommitted local torch override; the upstream fix is still open as microduck_rl#39 (issue #38), so env doctor's rl_pinned_commit fails there by design until it merges and this repo re-pins
Jetson AGX Orin (L4T R39) checks 1–4 pass the SBSA torch wheel carries no sm_87 kernels — GPU training is not available on Orin at this pin

A duck standing up in MuJoCoSpark, CLI 420dc5c:

$ microduck env up --sim --headless --skip-build
waiting for duck-a to report healthy (~/.cache/duck-sim/duck-a.sock)...
microduck-cli env up: healthy (sim)
  duck-a: ~/.cache/duck-sim/duck-a.sock
$ microduck duck init --apply --json
{... "summary": "init accepted: ramping to the home pose", "result": {"accepted": true}}
$ microduck duck monitor --frames 2 --json     # 8 s later
{'policy': 'held', 'fallen': False, 'gravity': [-0.028, -0.00004, -0.9996], 'z': 0.0687, 'loop': {'hz': 50.03, 'missed': 0}}

The same trunk height, to four decimals, on a different box — Orin, CLI 3c09fb0 (0.9.1):

$ microduck duck health --json
{"healthy": true, "degraded": false, "health": {"control_loop": {"target_hz": 50.0,
 "achieved_hz": 49.999974411777806, "ticks": 900, "missed": 0, "last_tick_age_ms": 17}, ...}}

A rule firing, and a drop that says why — Spark, one overlay rule (fallen is false → look, cooldown 5 s) over a 300-tick run:

$ microduck rules engine run --duck duck-a --rules /tmp/duck-rules-test.toml --apply --max-ticks 300 --json
{'ticks': 300, 'achieved_hz': 50.0, 'overruns': 0}
[SENSE stage=rule source=verify-look event=fired] look -> look-1
[SENSE stage=rule source=verify-look event=cooldown] dropped reason=cooldown: fired 0.020s ago, cooldown_s is 5.0

229 cooldown drops over the run, every one named on the microduck.sense logger — stderr only, so JSONL on stdout stays pure. A layer whose drops are invisible is indistinguishable from one that silently does nothing.

The live suite against a real daemonThor, CLI 2b00480, MuJoCo body:

$ MICRODUCK_LIVE=1 MICRODUCK_LIVE_BODY=sim MICRODUCK_LIVE_SIM=1 ... uv run pytest -m live -n0 -v tests/live
(the eleven above) PASSED
test_sim_body_stands_the_duck_up PASSED
test_sim_body_walks_forward_on_move XFAIL
======================== 12 passed, 1 xfailed in 26.65s ========================

Those twelve drive the CLI as subprocesses against the real socket. The unit suite (1101 tests at 0.9.4; the records above were taken at 998) does not — it runs against the in-process Python fake.

Not verified

Stated plainly, because a record that only lists passes is a brochure:

  • No physical duck. The --fake and MuJoCo bodies only.
  • Walking. Sampled at 25 Hz during duck move --vx 0.15: policy: walk, move.applied [0.15, 0, 0], fallen: false — and the left-knee target moves between −0.09 and −0.05 rad over 97 frames while odometry goes 0.065 → 0.072 m in 4.4 s. The twist arrives, the network is selected, the joint targets are static. Ruled out: our command shape (identical to upstream's drive), the generated params, the keyframe, the real-time factor (1.00) and the viewer. test_sim_body_walks_forward_on_move is kept as a non-strict xfail sentinel — an XPASS after a re-pin means walking arrived.
  • Upstream's own torch routing on Thor — every tier there ran on the local override, not as shipped.
  • GPU training on Orin — no sm_87 kernels in the SBSA wheel.
  • A real Hugging Face Jobs submission — the dry run proves the command shape and the tarball, nothing was submitted or billed.
  • Multi-duck, the ether, cameras and ToF in sim — upstream marks them "designed and measured but not built" on this branch.

The tick engine

One process owns the control socket. robotd arbitrates nothing between clients, so a second process would be two authors fighting over every channel — hence one loop, one seam, riders composed onto it:

flowchart TB
  subgraph read ["1 · read — ONE snapshot per tick"]
    direction LR
    P["sense providers"] --> SN["Sense"]
  end
  subgraph decide ["2 · decide — pure, no I/O"]
    direction LR
    B["behaviours + rules<br/>one contribution each"] --> AR["arbitrate<br/>one owner per channel"] --> CO["compose the pose"]
  end
  subgraph write ["3 · write — exactly once"]
    direction LR
    HG{"human<br/>driving?"} -- "yes" --> WH["MOTION withheld"]
    HG -- "no" --> SK["TargetSink → robotd"]
  end
  read --> decide --> write --> TS["4 · tick_seam riders — after the write"]
Loading

Per tick, in this order: read one Sense; ask each live behaviour once; arbitrate a single owner per channel; compose; write through the sink exactly once; run the tick seam after the write; expire finished lifetimes; sleep to an absolute deadline. No wall-clock read anywhere in the loop — clock and sleep are injected, which is what makes a 500-tick run bit-for-bit reproducible in a test. A provider that raises degrades to None; a rider that raises is caught, counted and logged as a named drop while its siblings still run.

CLAUDE.md has the rest: the seam rules, how to add a verb or a noun, the error and output contracts, and the agent-first rubric CI enforces.

Cited from / built on

Nothing from these repositories is copied into this one. The CLI implements their documented commands and wire protocol and links to their docs — cite, don't import.

Upstream — the exact commits every verb is validated against are in docs/upstream-pins.md; re-pinning is one PR that moves all rows and re-runs the on-box verification.

Repo What this CLI takes
pollen-robotics/microduck robotd, robotctl and the duck-ipc-proto JSON-RPC contract that microduck_cli/ipc/proto.py is transcribed from.
pollen-robotics/microduck_rl duck-body (the MuJoCo body) and the train / play / export / publish / infer lane the policy noun drives.

AgentCulture siblings — this CLI is composed from three of them rather than inventing a fourth architecture.

Repo Role here
reachy-mini-cli The architecture: noun groups with engine logic in sibling packages, ONE tick seam for every sense, the single-SDK-owner model.
arm101-cli The hardware-safety patterns: gated motion (dry-run / TTY confirm / --apply), release-on-abnormal-exit, hardware deps behind an extra.
teken The agent-first rubric (teken cli doctor . --strict) that gates CI.
devague The spec → plan → delivery method this repo builds by; see docs/specs/ and docs/deliveries/.

Vendored skills under .claude/skills/ carry their provenance in docs/skill-sources.md.

Contributing

CLAUDE.md is the working agreement: CLI contracts, what to take from each sibling, version-bump-every-PR, the cicd PR lane, worktree layout, memory discipline.

uv sync
uv run pytest -n auto                          # 1101 tests
uv run teken cli doctor . --strict             # the agent-first rubric gate CI runs
markdownlint-cli2 "**/*.md" "#node_modules" "#.local" "#.claude/skills" "#.teken"

License

Apache 2.0 — see LICENSE.

About

Agent-agnostic CLI for controlling the MicroDuck robot. Any agent, any human, one CLI. Built on the neurosymbolic-system runtime and following the reachy-mini-cli architecture.

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages