# ADR-0007: Preflight Validation Pattern

## Status

✅ Accepted

## Context

omni-dev commands span a spectrum of cost: `view` and `info` perform local git reads in
milliseconds, while `twiddle`, `check`, and `create-pr` make AI API calls that take seconds
and consume billable tokens. The `amend` command mutates repository history through
interactive rebase — a destructive operation whose failure mid-way leaves the repository in
an unclean state requiring manual recovery.

When prerequisites are missing — no API key, no `gh` CLI, not inside a git repository, or
uncommitted changes that would block a rebase — the user experience depends heavily on
*when* the failure occurs:

- **Early failure** (before any work begins): the user sees a clear, actionable error message
  and can fix the problem immediately. No time, money, or repository state has been wasted.

- **Late failure** (mid-operation): the user has already waited for AI responses, consumed
  API credits, or entered a rebase state. The error message may be a cryptic subprocess
  failure rather than a targeted diagnostic. Recovery may require manual intervention
  (`git rebase --abort`, re-running the command after fixing credentials).

The cost asymmetry makes early validation the right default: a few redundant `stat()` or
environment variable reads at startup are negligible compared to the cost of an avoidable
mid-operation failure.

Three alternative validation strategies were considered:

- **Lazy validation** (fail when the resource is first needed). This minimises upfront cost
  but maximises the blast radius of missing prerequisites. A missing API key discovered after
  20 seconds of diff computation wastes the user's time. A dirty working directory discovered
  after loading and parsing an amendment file wastes I/O and provides a confusing error
  context.

- **Partial validation** (check some prerequisites but not others). This offers no coherent
  guarantee — the user cannot reason about which failures are caught early and which are not.

- **Preflight validation** (check all prerequisites before starting any work). This
  front-loads a small, bounded amount of validation in exchange for a strong guarantee: if the
  command starts its main work, all prerequisites are met.

## Decision

We will validate all required prerequisites at the top of each command's `execute()` method
before any significant work begins. Validation functions are centralised in a single module,
`src/utils/preflight.rs`, and re-exported via `src/utils.rs` for convenient access.

### Individual checks

The preflight module provides four atomic checks, each validating a single prerequisite:

| Function                         | Validates                                              |
|----------------------------------|--------------------------------------------------------|
| `check_git_repository()`         | CWD is inside a valid git repository                   |
| `check_working_directory_clean()`| No staged, unstaged, or untracked changes (excl. ignored files) |
| `check_ai_credentials()`         | AI provider credentials are configured (env vars)      |
| `check_github_cli()`             | `gh` CLI is installed and authenticated for the current repo |

Each function returns `anyhow::Result<()>` (or `Result<AiCredentialInfo>` for
`check_ai_credentials`) with a targeted, actionable error message that tells the user exactly
what is missing and how to fix it.

### Composite checks

For commands that share the same prerequisite profile, composite functions combine the
individual checks in a single call:

| Function                           | Combines                                          |
|------------------------------------|---------------------------------------------------|
| `check_ai_command_prerequisites()` | `check_git_repository` + `check_ai_credentials`   |
| `check_pr_command_prerequisites()` | `check_git_repository` + `check_ai_credentials` + `check_github_cli` |

### Application to commands

Each command calls the appropriate check(s) at the top of its `execute()` method:

| Command     | Preflight applied                                             | Rationale                                  |
|-------------|---------------------------------------------------------------|--------------------------------------------|
| `twiddle`   | `check_ai_command_prerequisites` + `check_working_directory_clean` | AI calls + potential commit amendment |
| `check`     | `check_ai_command_prerequisites`                              | AI calls for message validation            |
| `create-pr` | `check_pr_command_prerequisites`                              | AI calls + GitHub CLI for PR creation      |
| `chat`      | `check_ai_credentials`                                        | AI calls only, no git dependency           |
| `amend`     | `check_git_repository` + `check_working_directory_clean`      | Destructive history mutation               |
| `view`      | `check_git_repository`                                        | Git read; consistent early error           |
| `info`      | `check_git_repository`                                        | Git read; consistent early error           |

Commands that are pure dispatchers (`git`, `commit`, `message`, `branch`, `create`), display
help (`help`), or manage configuration (`config`, `commands`) do not require preflight checks
— they either delegate to subcommands that perform their own checks, or have no external
dependencies.

### Optional capabilities

Some commands use external tools in a best-effort manner. The `info` command queries GitHub
PRs via `gh pr list`, but wraps the call in `.ok()` so that missing or unauthenticated `gh`
CLI degrades gracefully (PR info is omitted rather than causing an error). Preflight
validation is not applied to optional capabilities — only to prerequisites whose absence
would cause a hard failure or wasted work.

### Belt-and-suspenders for destructive operations

The `amend` command performs preflight validation at its entry point *and* retains the
working-directory-clean check inside `AmendmentHandler::perform_safety_checks()`. This
double check guards against TOCTOU issues: the working directory could become dirty between
the preflight check and the actual rebase, particularly if the user runs concurrent git
operations. The preflight check provides the user-facing fast feedback; the internal check
provides the safety-critical guard.

## Consequences

**Positive:**

- **Immediate, actionable feedback.** Users learn about missing prerequisites within
  milliseconds of running a command, before any expensive work begins. Error messages name
  the specific missing prerequisite and suggest a fix (e.g., "Set ANTHROPIC_API_KEY",
  "Run `gh auth login`", "Commit or stash your changes").

- **No wasted API credits.** AI-dependent commands validate credentials before sending any
  requests. A missing API key never results in a partial diff computation followed by a
  connection error.

- **Safe repository state.** The `amend` command validates the working directory before
  entering a rebase. Users never encounter a mid-rebase failure caused by a prerequisite that
  could have been checked upfront.

- **Single source of truth.** All validation logic lives in `src/utils/preflight.rs`. There
  are no duplicate implementations in command modules or in the git layer. Changes to
  validation logic (e.g., supporting a new AI provider) are made in one place.

- **Composability.** Individual checks compose into profiles for different command types.
  Adding a new command requires selecting the appropriate composite check or combining
  individual checks — not reimplementing validation logic.

**Negative:**

- **Redundant repository opens.** Commands that call `check_git_repository()` and then
  immediately call `GitRepository::open()` open the repository twice. The overhead is a
  single `stat()` for `.git/` — negligible compared to any subsequent git operation.

- **Preflight may diverge from runtime.** A credential that passes preflight (env var is set)
  could still fail at runtime (key is expired or revoked). Preflight catches the common case
  (missing config) but does not guarantee runtime success. This is inherent to any
  early-validation approach and is acceptable because the alternative (no early check) is
  strictly worse.

**Neutral:**

- **Commands choose their own profile.** There is no enforced mechanism (trait, macro) that
  requires a command to call preflight. Each command's `execute()` method calls the
  appropriate checks explicitly. This is a deliberate choice: a compile-time requirement
  would add abstraction without proportional benefit, given the small number of commands and
  the self-documenting nature of the call at the top of `execute()`.
