# ADR-0005: Hierarchical Configuration Resolution with Walk-Up Discovery

## Status

✅ Accepted

## Context

omni-dev resolves configuration files (`commit-guidelines.md`, `pr-guidelines.md`,
`scopes.yaml`, feature contexts) through a three-tier priority chain implemented in
`resolve_config_file()` in `src/claude/context/discovery.rs`:

| Priority    | Location                     | Purpose                       |
|-------------|------------------------------|-------------------------------|
| 1 (highest) | `{dir}/local/{filename}`     | Personal overrides (gitignored) |
| 2           | `{dir}/{filename}`           | Shared project config         |
| 3 (lowest)  | `$HOME/.omni-dev/{filename}` | Global user defaults          |

Where `{dir}` defaults to `.omni-dev` relative to the current working directory but can be
overridden via the `--context-dir` CLI flag. The home directory is resolved via the `dirs`
crate (`dirs::home_dir()`). Resolution is per-file: each config file is resolved
independently through the chain, so a project can override `scopes.yaml` locally while
inheriting `commit-guidelines.md` from the shared tier.

This design works well for single-project repositories but has three limitations:

1. **No XDG compliance.** The global tier hardcodes `$HOME/.omni-dev/`. Users on Linux and
   macOS who organise their dotfiles according to the XDG Base Directory Specification
   expect global config at `$XDG_CONFIG_HOME/omni-dev/` (defaulting to
   `$HOME/.config/omni-dev/`). Tools like `rustup`, `starship`, and `bat` follow XDG.

2. **No environment variable for config directory.** The only way to override the config
   directory is via `--context-dir` on every invocation. Users who always want a non-default
   directory (e.g., in CI or when managing multiple projects from a shared config) must pass
   the flag every time. An environment variable would allow persistent overrides without CLI
   flags.

3. **No walk-up discovery.** In monorepos with multiple packages, each subdirectory may need
   different commit guidelines. Currently, running omni-dev from `repo/packages/frontend/`
   looks for `.omni-dev/` only in the CWD. Users must either maintain identical config in
   every subdirectory or use `--context-dir` to point at the right location. Tools like
   eslint, prettier, and `.gitignore` solve this with walk-up: search from CWD upward
   through parent directories until a config is found.

Additionally, the three-tier resolution logic is duplicated across multiple call sites:
`resolve_config_file()` in `discovery.rs` is the canonical implementation, but `check.rs`,
`twiddle.rs`, and `create_pr.rs` each contain inline reimplementations of the
local/project/home chain for guidelines and status display. Any extension to the resolution
system must consolidate these paths.

Three alternative approaches were considered for each extension:

**XDG:** (a) Replace `$HOME/.omni-dev/` with XDG unconditionally — breaks existing users.
(b) Use the `directories` crate for platform-specific standard paths — introduces a heavier
dependency for what amounts to a single `std::env::var` check. (c) Check XDG first, fall
back to legacy — non-breaking and minimal.

**Env var:** (a) Env var as highest priority, overriding even `--context-dir` — violates the
convention that explicit CLI flags beat environment. (b) CLI flag and env var mutually
exclusive with an error if both set — unnecessarily strict; cargo, git, and docker all allow
both with clear precedence. (c) CLI flag wins over env var, both disable walk-up — standard
and predictable.

**Walk-up:** (a) Walk up to filesystem root — risks picking up config from unrelated parent
directories outside the repository. (b) Walk up to repository root (`.git/` boundary) —
matches `.gitignore` semantics, confines discovery to the repo. (c) Merge configs from all
directories in the walk-up chain — powerful but complex, with no precedent in the project's
ecosystem.

In each case the third option (c for XDG, c for env var, b for walk-up) was selected.

## Decision

We will extend the configuration resolution system with three capabilities: XDG-compliant
global config, an `OMNI_DEV_CONFIG_DIR` environment variable, and walk-up discovery from
CWD to repository root. The full resolution chain, applied per-file, will be:

| Priority    | Source                              | Condition                                   |
|-------------|-------------------------------------|---------------------------------------------|
| 1 (highest) | `--context-dir` CLI flag            | If provided; disables walk-up               |
| 2           | `OMNI_DEV_CONFIG_DIR` env var       | If set and no CLI flag; disables walk-up     |
| 3           | Walk-up: nearest `.omni-dev/`       | Walk from CWD upward to repo root           |
| 4           | `$XDG_CONFIG_HOME/omni-dev/`        | Defaults to `$HOME/.config/omni-dev/` if unset |
| 5 (lowest)  | `$HOME/.omni-dev/`                  | Legacy fallback                             |

Within whichever project-level directory is selected (by flag, env var, or walk-up), the
existing local override sub-chain applies:

| Sub-priority | Location                 |
|--------------|--------------------------|
| A (higher)   | `{dir}/local/{filename}` |
| B (lower)    | `{dir}/{filename}`       |

If no file is found at any tier, the function returns the project-level path as a
non-existent default, preserving current behaviour for callers that check `.exists()`.

### Consolidation

We will centralise all resolution logic in `src/claude/context/discovery.rs`. A new
`resolve_context_dir()` function will encapsulate directory selection (CLI flag → env var →
walk-up → default), and the existing `resolve_config_file()` will handle per-file resolution
within the selected directory plus the global fallback. The inline reimplementations in
`check.rs`, `twiddle.rs`, and `create_pr.rs` will be replaced by calls to these central
functions.

### Walk-up boundary

Walk-up discovery stops at the repository root, identified by the presence of a `.git/`
directory (or file, for worktrees). This prevents config from escaping the repository
boundary. The walk-up is skipped entirely when `--context-dir` or `OMNI_DEV_CONFIG_DIR` is
set, providing a deterministic escape hatch.

### XDG resolution

The global tier will check `$XDG_CONFIG_HOME/omni-dev/{filename}` before
`$HOME/.omni-dev/{filename}`. If `$XDG_CONFIG_HOME` is not set, it defaults to
`$HOME/.config` per the XDG specification. Resolution uses `std::env::var("XDG_CONFIG_HOME")`
directly rather than a platform-specific crate — on macOS, `dirs::config_dir()` returns
`~/Library/Application Support/`, which is not the expected location for a CLI tool's config
files.

### Environment variable naming

The environment variable is named `OMNI_DEV_CONFIG_DIR`, following the project's existing
naming convention (`OMNI_DEV_EDITOR`, `OMNI_DEV_DEFAULT_DRAFT_PR`) and the common pattern
of `{TOOL}_CONFIG_DIR` (cf. `CARGO_HOME`, `RUSTUP_HOME`).

## Consequences

**Positive:**

- **XDG compliance without breakage.** Existing `$HOME/.omni-dev/` installations continue
  to work. Users who prefer XDG can move their config to `$XDG_CONFIG_HOME/omni-dev/` and
  it will be found automatically. No migration step is required.

- **Environment-based override.** CI pipelines and users with non-standard layouts can set
  `OMNI_DEV_CONFIG_DIR` once in their shell profile instead of passing `--context-dir` on
  every invocation.

- **Monorepo support.** Walk-up discovery lets monorepo subdirectories carry their own
  `.omni-dev/` config. Running omni-dev from `packages/frontend/` finds
  `packages/frontend/.omni-dev/` if it exists, falling back to the root `.omni-dev/`.
  No `--context-dir` flag needed.

- **Consolidation.** Centralising resolution eliminates the parallel inline implementations
  in `check.rs`, `twiddle.rs`, and `create_pr.rs`, reducing the maintenance surface and
  ensuring all commands resolve config identically.

**Negative:**

- **Walk-up adds filesystem I/O on every invocation.** Walking from CWD to repo root
  involves one `stat()` per directory level for `.omni-dev/`. For a typical repository with
  3–5 levels of nesting this adds ~5 `stat()` calls, well under 1 ms and negligible compared
  to network I/O for AI calls.

- **Two global config locations.** Users may be unsure whether to use
  `$XDG_CONFIG_HOME/omni-dev/` or `$HOME/.omni-dev/`. Documentation must clearly state
  the priority and recommend XDG for new installations.

- **Walk-up may surprise users.** A stale `.omni-dev/` directory in an intermediate
  subdirectory could silently override the root config. Diagnostic output must clearly show
  which directory was selected, and `--context-dir` provides an explicit override.

**Neutral:**

- **No merging across walk-up levels.** When walk-up finds a `.omni-dev/` in an
  intermediate directory, the root-level `.omni-dev/` is not consulted (only the global
  XDG/home tier remains). This matches eslint and prettier semantics: nearest config wins.
  Subdirectory configs must be self-contained; partial inheritance is handled within a single
  directory via the `{dir}/local/` tier.

- **The `dirs` crate is retained for `home_dir()`.** XDG resolution uses `std::env::var`
  directly, but `dirs::home_dir()` is still used for the `$HOME` fallback. This handles
  cross-platform edge cases (Windows `USERPROFILE`) without adding a new dependency.
