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"]
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 --simdaemon driving a duck that stands, holds 50 Hz and runs skills — plus a--fakebody for the unit suite. - It cannot walk yet.
duck movereaches the daemon and selects the walk network, but the joint targets come back static. Details below. - It cannot load policies on this daemon.
robot.loadPolicyand 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:
-
Bring up a duck.
--simis the path the verification records below cover — the real daemon driving the MuJoCo body. Drop--headlessto watch it in the viewer, or use--fakefor a body that needs no simulator at all.microduck env up --sim --headless
-
Ask the robot for its own verdict.
microduck duck health
-
Stand it up, then hand it its policy. Both are gated, hence
--apply.microduck duck init --apply microduck duck enable --apply
-
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
-
Inject one intent through the same admission registry a rule fires through.
microduck rules intent stop
-
Tear it down. Never kill by name —
env downis 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 MuJoCo — Spark, 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 daemon — Thor, 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
--fakeand 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'sdrive), the generated params, the keyframe, the real-time factor (1.00) and the viewer.test_sim_body_walks_forward_on_moveis kept as a non-strictxfailsentinel — 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_87kernels 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"]
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.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file microduck_cli-0.9.4.tar.gz.
File metadata
- Download URL: microduck_cli-0.9.4.tar.gz
- Upload date:
- Size: 698.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
uv/0.12.9 {"installer":{"name":"uv","version":"0.12.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ff435081ebf06e3814fff3b08cad7e92a6ddee961439e93ca01233531234d1bd
|
|
| MD5 |
cafd582e1700f069b232bea4940bcadd
|
|
| BLAKE2b-256 |
0a722e652593d32a58a1c9478c012924355407574c295307ae6db65716ec5f3b
|
File details
Details for the file microduck_cli-0.9.4-py3-none-any.whl.
File metadata
- Download URL: microduck_cli-0.9.4-py3-none-any.whl
- Upload date:
- Size: 254.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
uv/0.12.9 {"installer":{"name":"uv","version":"0.12.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
81c4862839a433b0fe592e73121b0f580dd37b7a9af0d4d32498d1353c5a84d4
|
|
| MD5 |
89353a679a00edb6bde6b0b9f1c24150
|
|
| BLAKE2b-256 |
3bdfb30ed67d27afd04fb66eba38a09bd989754da4e8503b6fa0e222991a9815
|