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 export MANDO_OUTPUT=json. Read stdout line-by-line as NDJSON; ignore stderr (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=jsonexact-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 compose chatter stays on stderr; logs/exec streams 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.

EventShapeMeaning
kv{"t":"kv","k":<key>,"v":<val>}a labeled key/value
step{"t":"step","n":<name>,"s":<status>,"d"?:<detail>}a step outcome; spass | 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 warn event type

Warnings are not a top-level {"t":"warn"} event — they surface as a step with s:"warn". An emitterless {"t":"warn"} grammar was documented-but-never-emitted and was removed from the skill / README / spec.

Implementation

  • One pure mappersrc/ui/json.rs element_to_event — covers all 21 commands via the single Element seam. Because everything renders through Element, the mapper is the only place that needs to know about JSON.
  • Events are built with format! so t is guaranteed first in key order. serde_json’s preserve_order is off, so payload keys serialize alphabetically (only t-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 -p flag goes BEFORE the subcommand: mando mock -p N up.

Review Catches (worth remembering)

  1. Fabricated remedy from a test fixture — the implementer invented a “port-conflict” status remedy sourced from a TEST module (src/status/mod.rs test module — there is no production emitter for it). Replaced with the real workspace-lock contention row, using acquire()’s exact wording.
  2. Wrong subcommand sets — the plan’s command table had 3 wrong subcommand sets (migrate / profile / volume); the implementer correctly fixed them against cli.rs.
  3. Emitterless warn grammar removed{"t":"warn"} was documented but had no emitter; removed from skill / README / spec (warnings are step s:"warn").
  4. mock -p position — 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:
    1. stdout is per-line-parseable NDJSON.
    2. result is always last on both success and error paths.
    3. process exit code == result.code.
    4. MANDO_OUTPUT is exact-match ("JSON" uppercase falls through to human mode).
    5. human-mode byte-behavior unchanged (banner, tables, error: on stderr).
    6. stdout purity holds under stderr redirect, and --json works 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; query feature unchanged at its 2 pre-existing errors.