# ADR-0016: Clap Derive Macros with Hierarchical Subcommand Structure

## Status

✅ Accepted

## Context

omni-dev's command surface spans several distinct domains — git commit analysis,
git branch operations, AI interaction, configuration display, and command template
generation. Each domain has sub-operations that form a natural hierarchy: a git
commit operation is meaningfully different from a git branch operation, and a commit
message amendment is meaningfully different from a commit message view.

Three CLI definition approaches were evaluated:

- **Manual argument parsing.** `std::env::args()` is read and matched by hand.
  Argument parsing, help text, shell completion, and error messages must all be
  implemented from scratch. This is impractical for a command surface of this
  complexity and produces an inconsistent user experience compared with
  standard Rust tooling.

- **Flat command structure with clap.** All leaf commands appear at the top level
  (e.g., `omni-dev commit-view`, `omni-dev commit-amend`, `omni-dev branch-info`).
  The command surface is easy to enumerate but does not reflect the domain model.
  Related commands have no structural grouping, making the tool harder to discover
  and document. As the command count grows, the flat list becomes difficult to
  navigate.

- **Builder-pattern clap.** Commands are constructed programmatically via
  `Command::new().subcommand(…)` calls at runtime. The command tree is defined
  procedurally, which gives flexibility but separates the command definition from
  its argument types. Argument extraction requires manual `ArgMatches` lookups that
  the compiler cannot type-check. Adding a command requires edits in at least two
  places: the builder and the dispatch match.

- **Derive-macro clap with hierarchical subcommands.** Each level of the hierarchy
  is expressed as a pair: a `#[derive(Parser)]` struct that owns a
  `#[command(subcommand)]` field, and a `#[derive(Subcommand)]` enum whose variants
  name the next level. The command tree is defined entirely in Rust types; clap
  generates help text, shell completions, and error messages from the derive
  annotations. Dispatch is a chain of exhaustive `match` expressions that the
  compiler verifies for completeness.

## Decision

We will define the CLI using clap derive macros with a hierarchical subcommand
structure up to four levels deep.

### Command tree

The top-level struct `Cli` (`src/cli.rs`) owns a `Commands` subcommand enum with
five variants. The full tree is:

```
omni-dev
├── ai
│   └── chat
├── git
│   ├── commit
│   │   └── message
│   │       ├── view    [4 levels deep]
│   │       ├── amend   [4 levels deep]
│   │       ├── twiddle [4 levels deep]
│   │       └── check   [4 levels deep]
│   └── branch
│       ├── info
│       └── create
│           └── pr      [4 levels deep]
├── commands
│   └── generate
│       ├── commit-twiddle
│       ├── pr-create
│       ├── pr-update
│       └── all
├── config
│   └── models
│       └── show
└── help-all
```

Leaf commands for the `git` subtree are implemented in separate files under
`src/cli/git/`; all intermediate nodes for that subtree live in `src/cli/git.rs`.
All other subtrees follow the same layout within `src/cli/`.

### Struct and enum pairing

Every intermediate node is a matched pair:

```rust
#[derive(Parser)]
pub struct FooCommand {
    #[command(subcommand)]
    pub command: FooSubcommands,
}

#[derive(Subcommand)]
pub enum FooSubcommands {
    Bar(BarCommand),
}
```

Intermediate structs hold only a subcommand field. Leaf structs hold the actual
arguments and flags for that command.

### Async dispatch

`main` uses `#[tokio::main]` and calls `cli.execute().await`. Async propagates
only through subtrees that contain async leaves:

- **`git` and `ai` subtrees** — all intermediate `execute` methods are
  `async fn execute(self) -> Result<()>` and propagate to their child with `.await`.
  Sync leaves (`view`, `amend`, `info`) are called directly; async leaves
  (`twiddle`, `check`, `git branch create pr`, `ai chat`) are called with `.await`.

- **`commands`, `config`, and `help-all` subtrees** — all `execute` methods are
  synchronous `fn execute(self) -> Result<()>`. `Cli::execute` calls these without
  `.await`.

This gives a single tokio runtime for the process lifetime, created by the
`#[tokio::main]` macro in `main.rs`. Async is applied only where the subtree
requires it.

## Consequences

**Positive:**

- **Compile-time completeness.** The `match` at each dispatch level is exhaustive.
  Adding a new subcommand variant without handling it is a compiler error, not a
  runtime omission.

- **Type-safe argument access.** Each leaf command receives a typed struct rather
  than untyped `ArgMatches`. There are no string-keyed lookups and no runtime
  panics from mismatched argument names.

- **Help text and shell completions generated automatically.** Doc comments on
  structs and variants become help strings. Shell completion scripts can be
  generated via clap's `generate` API without additional code.

- **Command definition and handler are co-located.** The struct that defines a
  command's arguments is in the same file as the `impl` that executes it. There is
  no separate registration or routing table to keep in sync.

- **Domain structure is reflected in the command surface.** The four-level hierarchy
  (`git → commit → message → view`) mirrors the domain decomposition. Users who
  understand the domain can predict command paths; help text at each intermediate
  level guides discovery.

- **Single runtime.** `#[tokio::main]` creates one tokio runtime for the entire
  process. There are no per-command `Runtime::new()` calls to forget or duplicate
  when adding new async commands.

**Negative:**

- **Intermediate-node boilerplate.** Every level of nesting requires a paired struct
  and enum that exist solely to hold a subcommand field. `CommitCommand`,
  `MessageCommand`, `BranchCommand`, and `CreateCommand` each contain exactly one
  field. This is unavoidable with clap's derive approach for deep hierarchies.

- **Async propagates through all intermediate nodes in async subtrees.** Within the
  `git` and `ai` subtrees, every intermediate node must be declared `async fn` even
  when it has no async work of its own. This is a consequence of Rust's async model:
  an async leaf cannot be called with `.await` from a sync parent. The alternative —
  per-command `Runtime::new()` at each async dispatch site — was used previously and
  was worse: it created multiple runtimes and required each new async command author
  to remember the pattern independently.

- **Adding a command touches multiple files.** A new leaf requires: the leaf struct
  and impl (one file), a variant in the parent enum, and a match arm in the parent
  `execute`. For a new intermediate level, a new paired struct and enum are also
  needed. This is more than a flat structure would require.

**Neutral:**

- **Four levels is the current maximum; the structure is open to extension.** The
  nesting depth reflects the current domain model. A new domain area would add a
  new top-level variant to `Commands`; a deeper sub-operation would add another
  intermediate pair. Neither change affects existing subtrees.

- **`help-all` traverses the derive-generated command tree.** `HelpGenerator` in
  `src/cli/help.rs` calls `Cli::command()` (a clap derive method) to obtain the
  `Command` tree and recurse through it. This provides comprehensive help output
  without maintaining a separate list of commands, and sorts subcommands
  lexicographically for deterministic output.
