# ADR-0046: Unified `-o/--output <format>` Convention with Dedicated `--out-file` for File Destinations

## Status

✅ Accepted (2026-07-07)

## Context

omni-dev's CLI had grown four independent, mutually inconsistent answers to the
single question "how do I get machine-readable output from this command?":

- a boolean `--json` flag (`daemon status`, `snowflake sessions`,
  `worktrees list`);
- a free-form `--format <string>` parsed by hand (`git commit message check`,
  `snowflake query`, `log`, `transcript youtube fetch`), which silently fell
  back to a default on an unrecognised value;
- `-o/--output <PATH>` meaning **a file destination** (`jira read`,
  `confluence read`, `confluence attachment download`, `transcript youtube
  fetch`);
- and, on the many Atlassian list/get commands, `-o/--output <format>` already
  meaning a **format enum**.

The two `-o/--output` idioms directly contradict each other: on `jira read` the
short `-o` wrote a file, while on `confluence search` the same `-o` chose a
format. A user who learned `-o json` on one command and typed it on the other
either silently wrote a file literally named `json` or selected a format — with
no error either way. There was no shared renderer, so each command re-hand-rolled
serialization and its own value set.

[Issue #1125](https://github.com/rust-works/omni-dev/issues/1125) (commit
`ce9d282e`, `feat(cli)!`) collapsed all four idioms onto a single convention.
This is a **repo-wide, breaking** CLI change whose most load-bearing decision is
a deliberate semantic *reversal* — `-o` must **never** mean "file" again — so it
is recorded here rather than left to be reverse-engineered from the diff.

This decision is about the **flag convention and its shared renderer**, which is
orthogonal to three existing ADRs and not captured by any of them:
[ADR-0001](adr-0001.md) (YAML as the primary human data-exchange *format*) and
[ADR-0013](adr-0013.md) (self-describing YAML field-presence in the *payload*)
concern what is serialized; [ADR-0016](adr-0016.md) (clap derive with
hierarchical subcommands) is the structural substrate this convention rides on.
None of them governs which flag selects a format, or what that flag's short name
may mean.

## Decision

### One flag selects format everywhere: `-o/--output <format>`

Every command that emits machine-readable output binds format selection to the
same clap flag — `-o/--output` — carrying a **strict `ValueEnum`** (invalid
values are rejected by clap, never silently defaulted). The short `-o` is
reserved for this meaning across the entire CLI surface, and for nothing else.

The convention is the **flag and its enum-typed value**, not a single global
enum. Commands whose value set is the common one use the shared
[`OutputFormat`](../../src/cli/format.rs) (`Table`, `Json`, `Yaml`, `Yamls`,
`Jsonl`); commands with a different natural value set keep a command-specific
`ValueEnum` bound to the same flag — e.g. `git commit message check` keeps
`crate::data::check::OutputFormat` (`Text`/`Json`/`Yaml`, `src/data/check.rs:188`,
default `Text`), `transcript youtube fetch` uses a subtitle-oriented `CliFormat`
(`src/cli/transcript/youtube/fetch.rs:26`, default `Srt`), and `log` uses
`Format` (`src/cli/log.rs:76`, default `Oneline`). Per-command **defaults** also
differ deliberately (`snowflake query` defaults to `Json`,
`src/cli/snowflake.rs:101`; `confluence compare` to `Yaml`,
`src/cli/atlassian/confluence/compare.rs:71`; most list commands to `Table`).
Uniform *flag*, per-command *vocabulary*.

### Shared renderer contract in `src/cli/format.rs`

The common serialization is centralised so commands do not re-implement it:

- [`OutputFormat`](../../src/cli/format.rs) — the five-variant machine-format
  enum above.
- [`TableOrJson`](../../src/cli/format.rs) — a two-variant selector for commands
  that only ever render a human table **or** JSON. It exists specifically to
  give the ex-`--json`-boolean commands a typed `-o/--output` without offering
  YAML/JSONL variants they have no rendering for.
- [`JsonlSerialize`](../../src/cli/format.rs) — a trait for writing JSON Lines
  (one object per line), with a blanket `impl` for `Vec<T>` and helpers
  (`write_items_jsonl`/`write_scalar_jsonl`); container-specific impls for the
  Atlassian wrapper types live in `src/cli/atlassian/format.rs`.
- `write_output` / `output_as` — render `data` in the requested format and
  return **`Ok(false)` when the format is `Table`**, signalling the caller to
  fall through to its own human-readable rendering, and `Ok(true)` when the
  machine format was written. This `Ok(false)`-means-"you render the table"
  contract is the seam that lets one renderer serve every command without owning
  each command's bespoke table layout.

### `--out-file <PATH>` — a *separate* flag for file destinations, with no `-o` short

Writing output to a file is a genuinely different axis from choosing a format
(you can want JSON on stdout, or a table into a file). The two are split onto
two flags. File destinations move to `--out-file <PATH>` with **no `-o` short**,
on exactly the commands that previously conflated destination into `-o/--output`:
`jira read` (`src/cli/atlassian/jira/read.rs:21`), `confluence read`
(`.../confluence/read.rs:20`), `confluence attachment download`
(`.../confluence/attachment.rs:236`), `transcript youtube fetch`
(`src/cli/transcript/youtube/fetch.rs:44`), and `snowflake query`
(`src/cli/snowflake.rs:107`). Reserving `-o` for format *globally* is what makes
the convention learnable: `-o` is always a format and never a path, on every
command.

### Migration via hidden, warning deprecated aliases

Every retired spelling is preserved as a `#[arg(hide = true)]` alias that, when
used, rewrites itself to the new flag and prints a one-line `warning: … is
deprecated; use … instead` to **stderr** (so it never corrupts machine-readable
stdout). The three alias families:

- `--format <string>` → `-o/--output` on `git check` (`src/cli/git/check.rs:89`),
  `snowflake query` (`src/cli/snowflake.rs:117`), `log` (`src/cli/log.rs:105`),
  `coverage diff` (`src/cli/coverage/diff.rs:173`), and `transcript youtube
  fetch` (`src/cli/transcript/youtube/fetch.rs:52`).
- boolean `--json` → `-o/--output json` on `daemon status`
  (`src/cli/daemon/status.rs:37`), `snowflake sessions`
  (`src/cli/snowflake.rs:177`), and `worktrees list` (`src/cli/worktrees.rs:68`).
- `--output <PATH>` (file) → `--out-file` on `jira read`
  (`src/cli/atlassian/jira/read.rs:47`), `confluence read`
  (`.../confluence/read.rs:42`), and `confluence attachment download`
  (`.../confluence/attachment.rs:248`).

The aliases are hidden from `--help` (so the documented surface shows only the
new convention) but remain functional, giving existing scripts a warning-guarded
grace period. They are slated for removal in a **future major version**.

## Consequences

**Positive**

- One rule to learn and to apply: adding output to any new command means binding
  `-o/--output` to a `ValueEnum` and, if the values are the common set,
  delegating to `write_output`/`output_as`. `-o` is a format everywhere; a path
  is `--out-file` everywhere.
- Invalid format values now **error** (strict `ValueEnum`) instead of silently
  falling back to a default, so a typo like `-o jsonl` on a command without that
  variant fails loudly rather than emitting the wrong format.
- Serialization is written once. Commands render their own tables and defer
  json/yaml/yamls/jsonl to the shared renderer via the `Ok(false)` fall-through.
- The documented help surface is uniform; the drift is caught by the help
  golden-snapshot ([ADR-0030](adr-0030.md)), whose
  `integration_test__help_all_output.snap` was updated in the same change.

**Negative — breaking, and deliberately so**

- **`-o` is reversed on the read commands.** `jira read PROJ-1 -o json` no
  longer silently writes a file named `json`; `-o` is reserved for format, and
  the destination is now `--out-file file.json`. This is a surprising reversal
  for anyone with the old muscle memory — accepted as the cost of a single
  consistent meaning for `-o`.
- **`--output` is repurposed on `transcript youtube fetch`** from an output
  *file* to an output *format*; file destinations move to `--out-file`.
- **Boolean `--json` is removed** as a first-class flag on `daemon status`,
  `snowflake sessions`, and `worktrees list`; use `-o json`.
- **`--format` on `git commit message check` is now a strict enum**; a value
  outside `text|json|yaml` errors instead of silently falling back to `text`.
- The hidden aliases are extra surface to carry until the next major, and every
  one is a small ongoing maintenance and test cost.

The tradeoff recorded: we chose a **breaking semantic reversal of `-o`** over
preserving back-compat, betting that long-term, repo-wide consistency (and the
removal of a silent-file-write foot-gun) outweighs the one-time migration churn,
which the warning-guarded deprecation aliases are there to soften.

**Neutral**

- Complements rather than competes with [ADR-0001](adr-0001.md) (YAML remains
  the default human format; this ADR only standardises the *selector*) and
  [ADR-0013](adr-0013.md) (payload shape is unchanged). It rides on
  [ADR-0016](adr-0016.md)'s clap-derive structure, adding a cross-command flag
  convention on top of it.
- It shares the spirit of [ADR-0027](adr-0027.md) — a uniform, predictable CLI
  contract applied consistently across commands — without sharing its mechanism
  or surface.
