mando --json <cmd> (or MANDO_OUTPUT=json) switches all Element-rendered output to terse NDJSON on stdout, and a token-minimal agent skill (.claude/skills/mando/SKILL.md) teaches agents to drive the CLI machine-first. Landed in the mando-cli working tree 2026-07-23, uncommitted.
For Agents
To consume mando programmatically: pass
--json(flag beats env) or exportMANDO_OUTPUT=json. Readstdoutline-by-line as NDJSON; ignorestderr(child/compose chatter, logs, exec streams). Trust the exit code — the LAST stdout line is always{"t":"result","ok":<bool>,"code":<int>}. Never parse the human tables.
Activation
- Flag:
mando --json <cmd>— the flag wins over the env var. - Env:
MANDO_OUTPUT=json— exact-match"json"only (no other value activates it). - Human mode is byte-identical to before the feature (zero regression for interactive use).
- Spinners are suppressed in json mode; child process /
docker composechatter stays on stderr;logs/execstreams pass through raw by design (not wrapped as events).
NDJSON Grammar
One JSON object per line on stdout. t (type) is always the first key.
| Event | Shape | Meaning |
|---|---|---|
kv | {"t":"kv","k":<key>,"v":<val>} | a labeled key/value |
step | {"t":"step","n":<name>,"s":<status>,"d"?:<detail>} | a step outcome; s ∈ pass | fail | warn | skip | info |
err | {"t":"err","m":<message>} | an error line |
data | {"t":"data","v":<payload>} | status / query payloads |
result | {"t":"result","ok":<bool>,"code":<int>} | ALWAYS LAST, emitted from main.rs’s single exit path |
No
warnevent typeWarnings are not a top-level
{"t":"warn"}event — they surface as astepwiths:"warn". An emitterless{"t":"warn"}grammar was documented-but-never-emitted and was removed from the skill / README / spec.
Implementation
- One pure mapper —
src/ui/json.rselement_to_event— covers all 21 commands via the singleElementseam. Because everything renders throughElement, the mapper is the only place that needs to know about JSON. - Events are built with
format!sotis guaranteed first in key order.serde_json’spreserve_orderis off, so payload keys serialize alphabetically (onlyt-first is hand-guaranteed).
Agent Skill
Checked in at .claude/skills/mando/SKILL.md (64 lines, token-minimal). Contents:
- Setup (how to activate json mode).
- Grammar table (the events above).
- Exact minimal invocations per command.
- Error → remedy map.
- Frugality rules: status before logs, trust exit codes, never parse human tables.
Command-shape facts pinned in the skill:
mock’s-pflag goes BEFORE the subcommand:mando mock -p N up.
Review Catches (worth remembering)
- Fabricated remedy from a test fixture — the implementer invented a “port-conflict” status remedy sourced from a TEST module (
src/status/mod.rstest module — there is no production emitter for it). Replaced with the real workspace-lock contention row, usingacquire()’s exact wording. - Wrong subcommand sets — the plan’s command table had 3 wrong subcommand sets (
migrate/profile/volume); the implementer correctly fixed them againstcli.rs. - Emitterless
warngrammar removed —{"t":"warn"}was documented but had no emitter; removed from skill / README / spec (warnings aresteps:"warn"). mock -pposition — corrected to precede the subcommand.
Process Note
Two fixer subagents died on API connection errors mid-run; the orchestrator applied the four review fixes inline and re-verified the gate: 401 lib tests + 8 integration tests, clippy zero, release warnings zero. The optional query feature was unchanged (still at its 2 pre-existing errors).
Validation
Two-judge validation pass — verdict PASS. Report in repo: .agents/council/2026-07-23-vibe-json-output.md. Status: awaiting user commit.
- Live-behavior judge ran the actual debug binary with side-effect-free commands and confirmed 6/6 observable contracts:
- stdout is per-line-parseable NDJSON.
resultis always last on both success and error paths.- process exit code ==
result.code. MANDO_OUTPUTis exact-match ("JSON"uppercase falls through to human mode).- human-mode byte-behavior unchanged (banner, tables,
error:on stderr). - stdout purity holds under stderr redirect, and
--jsonworks position-independently (global=true).
- Fix-closure judge confirmed all 4 inline fixes CLOSED with line evidence, and a clean standards census: 0 comments, 0 non-test unwraps, SKILL.md 64 lines. Gate green including release-profile 0 warnings;
queryfeature unchanged at its 2 pre-existing errors.