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 bylist,log,search,blame,checklist,show, and theinitsummary.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 indiagnose,new --classify, andpatterns --synthesize.unwrap— every interactive clack prompt routes through it; Ctrl-C prints a clean cancel line and exits130without writing to the store.
--json and --quiet output is byte-identical to before the polish — only
human-mode rendering changed.