Skip to content

CLI

Source: packages/cli/src/ · Spec: §5

Global flags on every command: --json (stable machine output — the agent contract, snapshot-tested), --quiet, --cwd <path>, --yes, --actor <human:username | agent:tool[:model]>. Actor resolution: --actor flag → ~/.beetl/config.json identity → git config user.name$USER.

M0 commands:

Command Notes
init interactive prompts (privacy mode, config format, global layer — default yes, agent install offer, devDependency offer when package.json exists — M-03); --yes defaults: json, committed, global off, no offers; non-interactive --agent-install / --save-dev. Establishes user identity from git config. --private = local-only.
new interactive when TTY without --title; flags or --json-input file|- otherwise
list --status (incl. open/terminal), --type --category --surface --tag --since --author; enum/date flags validated (INVALID_INPUT), --since normalized to UTC (M-02)
show <ref> detail view; --md renders the markdown communication format
update <ref> --set field=value (dotted paths, JSON-typed values) or --json-input
move <ref> <status> gate-satisfying flags (--symptoms --reliability --root-cause --files --fix-summary --approach --tests --no-test-reason --reason…) applied first, then chained transition
resolve <ref> move … resolved with the same flags
close <ref> --as <wontfix|duplicate|cannot-reproduce> --reason terminal close
file-complete full record via --json-input (default stdin); all gates atomically
checklist <ref> unmet gates + missing high-value fields, paste-able into agent context
doctor store validation (incl. private-file lifecycle + held locks); exit 2 when unhealthy
unlock <ref|registry> [--force] remove a crashed holder’s lock; refuses live owners; --force only for unknown liveness

Session refs everywhere: canonical ID, unique fragment (a4f2), or #ordinal.

Exit codes: 0 ok · 1 user/validation (incl. gate unmet, USAGE, INTERNAL) · 2 store/registry corruption · 3 runner failure · 130 prompt cancelled (Ctrl-C). --json errors (ADR-0006): every failure — parse/usage failures and unexpected internal errors included — is exactly one JSON object on stderr, { "error": { "code": "GATE_UNMET", "gates": [...] } }; stdout is reserved for success payloads (doctor’s report stays on stdout even when unhealthy). Unexpected failures map to { "error": { "code": "INTERNAL", correlationId } } with no stack in JSON mode.

The mirror rule holds for every successful command, human renderers included: the payload goes to stdout and stderr stays empty, so beetl search foo | grep … and stdout-capturing agents work. The only things beetl ever writes to stderr are error envelopes/usage text and the TTY-only progress spinner below. Both halves are regression-tested — failures in test/json-errors.test.ts, successes in test/streams.test.ts.

When measuring output, capture the streams separately (>out 2>err); folding them with 2>&1 hides which stream a command actually used, and a placeholder like show <session-id> is a shell redirect rather than an argument, so it yields an empty stdout that looks like a stream bug.

Human output (TUI) — src/tui.ts + src/render.ts

Section titled “Human output (TUI) — src/tui.ts + src/render.ts”

Human-facing rendering shares one helper layer (M4 TUI polish):

  • alignRows — ANSI-aware two-space column alignment; used by list, log, search, blame, checklist, show, and the init summary.
  • GLYPH (✓ ⚠ ✗ ↺ ○) — plain-text status glyphs, colored via picocolors (auto-disabled when not a TTY, so piped output stays greppable).
  • progress — clack spinner on stderr, TTY-only, silent under --json/--quiet; wraps runner calls in diagnose, new --classify, and patterns --synthesize.
  • unwrap — every interactive clack prompt routes through it; Ctrl-C prints a clean cancel line and exits 130 without writing to the store.

--json and --quiet output is byte-identical to before the polish — only human-mode rendering changed.