omni-dev 0.41.0

AI-powered git commit rewriter, PR generator, and MCP server for Jira, Confluence, Datadog, Gmail, and Drive.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
# `.omni-dev/` Directory Contract

This document is the canonical reference for the `.omni-dev/` directory: the
inventory of recognised files, their formats, the precedence rules that decide
which copy wins when more than one exists, and the validation behaviour
omni-dev exhibits when a file is missing or malformed.

If you came here from a passing mention of `.omni-dev/commit-guidelines.md` (or
any other `.omni-dev/<file>`), this is the spec.

## Overview

`.omni-dev/` is the configuration directory omni-dev uses to learn about a
project. It is normally placed at the repository root, but discovery walks up
from the current working directory so omni-dev keeps working from anywhere
inside a repository tree.

Two distinct precedence systems are at work, depending on which file is being
loaded:

- **Chain A** — for `commit-guidelines.md`, `pr-guidelines.md`, `scopes.yaml`,
  and feature contexts. Resolves through `local/` overrides, project scope,
  XDG, and a legacy `~/.omni-dev/` fallback. Discussed in
  [Chain A — hierarchical resolution]#chain-a--hierarchical-resolution.
- **Chain B** — for `models.yaml`. A layered merge with deep-merge semantics
  driven by ADR-0022. Discussed in
  [Chain B — layered model catalog]#chain-b--layered-model-catalog.

Credentials in `~/.omni-dev/settings.json` are home-only and use neither
chain — see [settings.json](#settingsjson).

## Recognised files

| File | Purpose | Format | Scope | Precedence | Source |
|---|---|---|---|---|---|
| `commit-guidelines.md` | Commit-message rules consumed by `git commit message check` / `twiddle` | Markdown | project / user / XDG / `~/.omni-dev/` | Chain A | [`src/claude/context/discovery.rs:456`]../src/claude/context/discovery.rs#L456 |
| `pr-guidelines.md` | PR title / body rules consumed by `git pr` flows | Markdown | same as above | Chain A | [`src/claude/context/discovery.rs:471`]../src/claude/context/discovery.rs#L471 |
| `scopes.yaml` | Commit/PR scope vocabulary; merged with ecosystem defaults | YAML | same as above | Chain A | [`src/claude/context/discovery.rs:486`]../src/claude/context/discovery.rs#L486 |
| `gmail-sync.yaml` | The account list + output dirs `gmail sync-all` fans out to concurrently ([ADR-0068]adrs/adr-0068.md) | YAML | project / user / XDG / `~/.omni-dev/` | Chain A (loaded strictly — see [Validation behaviour]#gmail-syncyaml-1) | [`src/cli/gmail/sync_all.rs:70`]../src/cli/gmail/sync_all.rs#L70 |
| `models.yaml` | AI model catalog overrides | YAML | project / user / embedded | Chain B | [`src/claude/model_config.rs:178`]../src/claude/model_config.rs#L178 |
| `context/feature-contexts/*.yaml` | Per-feature AI prompt context fragments | YAML | inside the active `.omni-dev/` (plus `local/` override) | Chain A (variant) | [`src/claude/context/discovery.rs:502`]../src/claude/context/discovery.rs#L502 |
| `local/<any>` | Gitignored personal overrides for any of the above | follows the underlying file | personal | top of Chain A | [`src/claude/context/discovery.rs:44`]../src/claude/context/discovery.rs#L44 |
| `~/.omni-dev/settings.json` | API credentials and env-var fallbacks (Atlassian / Datadog / etc.); written `0600` inside a `0700` dir | JSON | user (home) only | none — single path | [`src/utils/settings.rs:130`]../src/utils/settings.rs#L130 |

Missing files are not an error. Each loader falls through to a lower-precedence
tier (or to the embedded default, where one exists) and omni-dev continues —
except `gmail-sync.yaml`, whose absence is a hard error; see its own
[Validation behaviour](#gmail-syncyaml-1) entry.

## Precedence

### Chain A — hierarchical resolution

Used for `commit-guidelines.md`, `pr-guidelines.md`, `scopes.yaml`, and the
`context/feature-contexts/` files. Implemented by `resolve_config_file` in
[`src/claude/context/discovery.rs:43-72`](../src/claude/context/discovery.rs#L43-L72).
The first existing file wins:

| Priority | Location | Purpose |
|---|---|---|
| 1 | `{dir}/local/{filename}` | Gitignored personal override |
| 2 | `{dir}/{filename}` | Shared project config |
| 3 | `$XDG_CONFIG_HOME/omni-dev/{filename}` | XDG global config (defaults to `~/.config/omni-dev/`) |
| 4 | `$HOME/.omni-dev/{filename}` | Legacy global fallback |

`{dir}` is itself resolved by `resolve_context_dir_with_source` in
[`src/claude/context/discovery.rs:128-147`](../src/claude/context/discovery.rs#L128-L147):

| Priority | Source | Description |
|---|---|---|
| 1 | `--context-dir` CLI flag | Explicit override; disables walk-up |
| 2 | `OMNI_DEV_CONFIG_DIR` env var | Environment override; disables walk-up |
| 3 | Walk-up discovery | Nearest `.omni-dev/` from CWD up to the repo root (`.git` boundary) |
| 4 | `.omni-dev` | Default fallback relative to CWD |

Walk-up stops at the first directory containing a `.git` entry (file or
directory) — discovery does not escape the repository. See
[ADR-0005](adrs/adr-0005.md).

### Chain B — layered model catalog

Used **only** for `models.yaml`. Implemented by
`ModelRegistry::load_layered_from_paths` in
[`src/claude/model_config.rs:195-227`](../src/claude/model_config.rs#L195-L227).

Unlike Chain A, layers are **deep-merged** rather than first-match. The
embedded catalog is always present, and higher-precedence layers override
individual model entries (matched by `api_identifier`) and provider settings
without forcing the user to redeclare the whole file.

| Priority | Layer | Notes |
|---|---|---|
| 1 (highest) | `OMNI_DEV_MODELS_YAML` env override | Short-circuits both project and user layers. Missing file falls back to embedded with a warning. |
| 2 | `./.omni-dev/models.yaml` (project, **CWD-relative**) | No walk-up; resolved by `default_project_path` at [`src/claude/model_config.rs:494-498`]../src/claude/model_config.rs#L494-L498. |
| 3 | `~/.omni-dev/models.yaml` (user) | Resolved by `default_user_path` at [`src/claude/model_config.rs:501-503`]../src/claude/model_config.rs#L501-L503. |
| 4 (lowest) | Embedded [`src/templates/models.yaml`]../src/templates/models.yaml | Compile-time include via `include_str!`; cannot be removed. |

> **Caveat — no walk-up for `models.yaml`.** Chain B resolves the project
> layer from the current working directory only. If you `cd` into a
> sub-directory that does not itself contain `.omni-dev/models.yaml`, the
> project-layer overrides will not apply, even though Chain A's walk-up would
> have found the same `.omni-dev/`. Run omni-dev from the project root, or
> use `OMNI_DEV_MODELS_YAML` to point at the file explicitly.

See [ADR-0022](adrs/adr-0022.md) for the rationale behind the layered-merge
design.

### Settings (`~/.omni-dev/settings.json`)

`Settings::get_settings_path` in
[`src/utils/settings.rs:130`](../src/utils/settings.rs#L130) returns a
single path: `$HOME/.omni-dev/settings.json`. There is no walk-up, no
project-scoped equivalent, and no XDG fallback. This is intentional:
credentials are personal and should never be checked into a project's
`.omni-dev/`.

All writes go through `Settings::upsert_env_vars_in` /
`Settings::remove_env_vars_in` (the Atlassian and Datadog `auth login` /
`logout` flows). When a profile is active they target that profile's `env`
map, mirroring read-side resolution (issue #1116); otherwise they target the
base `env` map. Because the `env` maps hold credentials, the file is created
`0600` inside a `0700`
`~/.omni-dev/` and re-tightened on every write
([`src/utils/settings.rs:330`](../src/utils/settings.rs#L330), issue #1128) —
the same owner-only posture as the daemon's runtime state
([`src/daemon/paths.rs`](../src/daemon/paths.rs)).

## File specs

### `commit-guidelines.md`

Markdown describing the commit-message conventions the project enforces. Read
verbatim into the AI prompt; omni-dev does not parse it for structure. The
spec-by-example lives at
[`src/templates/default-commit-guidelines.md`](../src/templates/default-commit-guidelines.md);
the default is used when no project, XDG, or home copy exists.

A minimally useful file declares severity levels and a list of accepted types:

```markdown
## Severity Levels

| Severity | Sections                              |
|----------|---------------------------------------|
| error    | Commit Format, Types, Subject Line    |
| warning  | Body Guidelines                       |
| info     | Subject Line Style                    |

## Commit Format

Use conventional commit format: `<type>(<scope>): <description>`

## Types

| Type    | Use for                |
|---------|------------------------|
| `feat`  | New features           |
| `fix`   | Bug fixes              |
| `docs`  | Documentation changes  |

## Subject Line

- Imperative mood ("add", not "added")
- No trailing period
```

The `## Severity Levels` table is the single source of truth for whether a
section is `error`, `warning`, or `info` — see
[ADR-0012](adrs/adr-0012.md).

### `pr-guidelines.md`

Markdown describing PR title and body conventions. Same loading semantics as
`commit-guidelines.md`. There is no embedded default — the file is optional;
when absent, omni-dev falls back to behaviour driven solely by
`commit-guidelines.md` and ecosystem defaults. See this repository's own
[`.omni-dev/pr-guidelines.md`](../.omni-dev/pr-guidelines.md) for a
worked example.

### `scopes.yaml`

YAML enumerating valid commit/PR scopes for the project. Loaded into
`ScopesConfig` in
[`src/claude/context/discovery.rs:245`](../src/claude/context/discovery.rs#L245),
then merged with ecosystem defaults via
`merge_ecosystem_scopes` (see [ADR-0019](adrs/adr-0019.md)).

Minimal valid example:

```yaml
scopes:
  - name: "auth"
    description: "Authentication and authorization"
    examples:
      - "auth: add OAuth2 login"
      - "auth: fix session timeout"
    file_patterns:
      - "src/auth/**"
      - "auth/**"
```

All four fields (`name`, `description`, `examples`, `file_patterns`) are
required per scope. Extra fields are ignored.

### `gmail-sync.yaml`

YAML listing the named Gmail accounts `omni-dev gmail sync-all` archives
concurrently, and where. `account` is a lookup key into
`~/.omni-dev/settings.json`'s `gmail.accounts` map (see
[Settings](#settingsjson)) — this file only says *which* accounts to sync
and *where*, never a second credential store. Loaded into
`GmailSyncAllConfig` by
[`load_gmail_sync_config`](../src/cli/gmail/sync_all.rs#L70). See
[ADR-0068](adrs/adr-0068.md) for the full design.

Minimal valid example:

```yaml
concurrency: 20
accounts:
  - account: jky.greens
    output_dir: emails/jky.greens/
  - account: newhoggy
    output_dir: emails/newhoggy/
    query: "-in:spam"
    extract_attachments: true
```

`accounts` (a non-empty list) and each entry's `account`/`output_dir` are
required; top-level `concurrency` and each entry's `query`/
`extract_attachments` are optional. `output_dir` resolves relative to the
project root (the parent of the discovered `.omni-dev/`) unless absolute.
Unlike every other Chain A file, a missing, empty, or malformed
`gmail-sync.yaml` is a hard error, not a silent fallback — see
[Validation behaviour](#gmail-syncyaml-1).

### `models.yaml`

YAML overriding the embedded model catalog. The schema version is currently
`"1"` (constant `MODELS_SCHEMA_VERSION` at
[`src/claude/model_config.rs:26`](../src/claude/model_config.rs#L26)). Two
top-level keys are treated specially during merge:

- `models:` — a sequence; entries are matched by `api_identifier` and
  deep-merged with the corresponding embedded entry, or appended if new.
- `providers:` — a mapping; merged per provider name, so a user file can
  override just `default_model` without redeclaring every tier.

All other top-level keys are last-writer-wins.

Minimal valid example — overrides Anthropic's default model and adds one
provider-specific entry:

```yaml
version: "1"

providers:
  claude:
    default_model: "claude-opus-4-7"

models:
  - provider: "claude"
    model: "Claude Opus 4.7"
    api_identifier: "claude-opus-4-7"
    max_output_tokens: 64000
    input_context: 200000
    generation: 4.7
    tier: "flagship"
```

`api_identifier` is the only strictly-required field per model entry — entries
without it are skipped (see [Validation behaviour](#validation-behaviour)).
The full schema, including provider defaults and tier descriptions, is
documented by example in
[`src/templates/models.yaml`](../src/templates/models.yaml).

See [ADR-0022](adrs/adr-0022.md) for the design rationale.

### `settings.json`

JSON file at `~/.omni-dev/settings.json` containing an `env` map. Each key
corresponds to an environment variable name that omni-dev (or one of its
sub-clients) consults; values are used **as a fallback** for the actual
environment — real environment variables always win. See
[`src/utils/settings.rs:63-74`](../src/utils/settings.rs#L63-L74).

Minimal valid example:

```json
{
  "env": {}
}
```

Recognised keys written by built-in flows:

| Key | Written by | Read by |
|---|---|---|
| `ATLASSIAN_INSTANCE_URL` | `omni-dev atlassian auth login` ([`src/atlassian/auth.rs:152`]../src/atlassian/auth.rs#L152) | [`load_credentials`]../src/atlassian/auth.rs#L40 |
| `ATLASSIAN_EMAIL` | same | same |
| `ATLASSIAN_API_TOKEN` | same | same |
| `DATADOG_API_KEY` | `omni-dev datadog auth login` ([`src/datadog/auth.rs:195`]../src/datadog/auth.rs#L195) | [`load_credentials`]../src/datadog/auth.rs#L85 |
| `DATADOG_APP_KEY` | same | same |
| `DATADOG_SITE` | same | same |

Any other environment variable consulted via `Settings::get_env_var` can also
be set under the same `env` map (including API keys for `CLAUDE_API_KEY`,
`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, etc.).

### `local/`

`{dir}/local/` is a gitignored sub-directory that mirrors the layout of
`{dir}/` itself: any file under `local/` (e.g.
`.omni-dev/local/commit-guidelines.md`,
`.omni-dev/local/scopes.yaml`,
`.omni-dev/local/context/feature-contexts/*.yaml`) shadows the file at the
same relative path one level up. The repo root
[`.gitignore`](../.gitignore) contains a `.omni-dev/local/` entry that you
should mirror in any project that adopts this convention.

This is the intended escape hatch for personal overrides that should never
ship to teammates — for example, a per-developer override of
`commit-guidelines.md` for experimentation, or a `models.yaml` that points
the project at a different model provider locally. (Note that `models.yaml`
uses Chain B, not Chain A, so `local/models.yaml` is **not** consulted —
use `OMNI_DEV_MODELS_YAML` for a personal model override instead.)

### `context/feature-contexts/*.yaml`

Per-feature prompt context fragments. Files are loaded from both
`{dir}/context/feature-contexts/` and `{dir}/local/context/feature-contexts/`;
local entries override standard entries with the same filename. Implemented
by `load_feature_contexts` at
[`src/claude/context/discovery.rs:570-610`](../src/claude/context/discovery.rs#L570-L610).
Only `.yaml` and `.yml` files are picked up; the filename (minus extension)
becomes the feature key.

## Validation behaviour

omni-dev favours silent fallback over hard failure: missing files are
expected (defaults exist); malformed files log a warning and fall through to
the next tier. The strings below are the actual messages emitted by the
current source — you can grep your logs against them verbatim.

### `models.yaml`

| File:line | Level | Trigger | Message |
|---|---|---|---|
| [`src/claude/model_config.rs:207-210`]../src/claude/model_config.rs#L207-L210 | `warn!` | `OMNI_DEV_MODELS_YAML` points at a missing or unreadable file | `{OMNI_DEV_MODELS_YAML_ENV} points at {} but the file is missing or unreadable; falling back to embedded catalog` |
| [`src/claude/model_config.rs:246-248`]../src/claude/model_config.rs#L246-L248 | hard error (`anyhow!`) | Embedded YAML is malformed at compile time (compile-time invariant — only triggers if a build ships a broken `src/templates/models.yaml`) | `Embedded models.yaml is malformed at compile time: {e}` |
| [`src/claude/model_config.rs:250-252`]../src/claude/model_config.rs#L250-L252 | `error!` | User or project `models.yaml` has invalid YAML — non-fatal, falls through | `Malformed {source} models.yaml: {e}. Falling through to lower-precedence layers.` |
| [`src/claude/model_config.rs:514-517`]../src/claude/model_config.rs#L514-L517 | `error!` | Read error (permissions, I/O) on user or project `models.yaml` | `Failed to read {}: {e}. Falling through to lower-precedence layers.` |
| [`src/claude/model_config.rs:600-602`]../src/claude/model_config.rs#L600-L602 | `warn!` | A model entry lacks the required `api_identifier` field | `` Skipping model entry without `api_identifier` from {source} models.yaml `` |
| [`src/claude/model_config.rs:685-687`]../src/claude/model_config.rs#L685-L687 | `warn!` | User or project `models.yaml` has no `version:` field | `` {source} models.yaml has no `version:` field; assuming compatibility with schema version {MODELS_SCHEMA_VERSION}. Add `version: "{MODELS_SCHEMA_VERSION}"` to silence this warning. `` |
| [`src/claude/model_config.rs:691-693`]../src/claude/model_config.rs#L691-L693 | `warn!` | Declared schema version differs from `MODELS_SCHEMA_VERSION` | `{source} models.yaml declares schema version {v}; this build understands {MODELS_SCHEMA_VERSION}. Continuing — unrecognised fields may be ignored.` |

### `scopes.yaml`

| File:line | Level | Trigger | Message |
|---|---|---|---|
| [`src/claude/context/discovery.rs:241`]../src/claude/context/discovery.rs#L241 | `warn!` | File exists but cannot be read (permissions, I/O) — `load_project_scopes` returns `vec![]` | `Cannot read scopes file {}: {e}` |
| [`src/claude/context/discovery.rs:248-251`]../src/claude/context/discovery.rs#L248-L251 | `warn!` | File exists but is malformed YAML — `load_project_scopes` returns `vec![]` | `Ignoring malformed scopes file {}: {e}` |
| [`src/claude/context/discovery.rs:494-497`]../src/claude/context/discovery.rs#L494-L497 | `warn!` | Same condition, but encountered while loading the wider `.omni-dev/` config — `load_omni_dev_config` skips the scopes update | `Ignoring malformed scopes file {}: {e}` |

### `gmail-sync.yaml`

Deliberately the one Chain A file that never silently falls back — a
silently-empty account list would look like a successful no-op for a
command whose entire job is fanning out to every configured account (see
[ADR-0068](adrs/adr-0068.md)). Every case below is a hard `Result::Err`,
propagated before any Gmail client is built.

| File:line | Trigger | Message |
|---|---|---|
| [`src/cli/gmail/sync_all.rs:73`]../src/cli/gmail/sync_all.rs#L73 | No `gmail-sync.yaml` found at any Chain A tier | `no gmail-sync.yaml found (looked under {}); add one with an ``accounts:`` list, or point --context-dir/OMNI_DEV_CONFIG_DIR at a directory containing one` |
| [`src/cli/gmail/sync_all.rs:84`]../src/cli/gmail/sync_all.rs#L84 | File exists but its `accounts:` list is empty | `{} has no accounts configured; add entries to its ``accounts:`` list` |
| — (`serde_yaml::from_str`, wrapped with `anyhow::Context`) | File exists but is malformed YAML | `Failed to parse {}` |
| [`src/cli/gmail/sync_all.rs:105`]../src/cli/gmail/sync_all.rs#L105 | An `account` entry doesn't match any key in `~/.omni-dev/settings.json`'s `gmail.accounts` map — batches every unknown name into one error | `gmail-sync.yaml references unknown Gmail account(s): {names}. Run ``gmail account list`` to see configured accounts.` |

### Feature contexts

| File:line | Level | Trigger | Message |
|---|---|---|---|
| [`src/claude/context/discovery.rs:578-581`]../src/claude/context/discovery.rs#L578-L581 | `warn!` | Feature contexts directory is unreadable — directory is skipped | `Cannot read feature contexts dir {}: {e}` |
| [`src/claude/context/discovery.rs:600-603`]../src/claude/context/discovery.rs#L600-L603 | `warn!` | A `.yaml` / `.yml` file in the directory fails to deserialise as `FeatureContext` — that one file is skipped | `Ignoring malformed feature context {}: {e}` |

### `settings.json`

| File:line | Level | Trigger | Message |
|---|---|---|---|
| [`src/utils/settings.rs:122-123`]../src/utils/settings.rs#L122-L123 | `Result::Err` (propagated via `anyhow::Context`) | File exists but cannot be read | `Failed to read settings file: {}` |
| [`src/utils/settings.rs:125-126`]../src/utils/settings.rs#L125-L126 | `Result::Err` (propagated via `anyhow::Context`) | File exists but is not valid JSON or does not match the `Settings` schema | `Failed to parse settings file: {}` |

Missing `~/.omni-dev/settings.json` is silent (see
[`src/utils/settings.rs:116-119`](../src/utils/settings.rs#L116-L119)) — `load`
returns an empty `Settings { env: {} }`.

### `commit-guidelines.md` / `pr-guidelines.md`

No file-specific validation. Read errors during
`fs::read_to_string` are propagated as `Result::Err` via the standard
`anyhow` chain. Missing files are silent; the context loader simply leaves
`context.commit_guidelines` / `context.pr_guidelines` as `None` and the AI
prompt falls back to
[`src/templates/default-commit-guidelines.md`](../src/templates/default-commit-guidelines.md)
(`commit-guidelines.md` only — `pr-guidelines.md` has no embedded default).

## See also

- [ADR-0005]adrs/adr-0005.md — Hierarchical Configuration Resolution with Walk-Up Discovery (Chain A).
- [ADR-0018]adrs/adr-0018.md — Automatic Context Detection for Adaptive AI Prompts.
- [ADR-0019]adrs/adr-0019.md — Ecosystem-Aware Scope Auto-Detection (`scopes.yaml` merge).
- [ADR-0022]adrs/adr-0022.md — Layered Model Catalog with User and Project Overrides (Chain B).
- [ADR-0068]adrs/adr-0068.md — Concurrent Multi-Account Gmail Sync via `gmail-sync.yaml` and a Shared Fetch Semaphore.
- [Configuration Guide]configuration.md — narrative walkthrough with worked examples.
- [User Guide]user-guide.md — end-to-end setup including `.omni-dev/` bootstrap.
- [Style Guide]STYLE_GUIDE.md — commit-message-authoring conventions for this repository.