# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [0.34.0] - 2026-07-11

### Added
- **`transition execute` can supply transition-screen fields, resolution, and comment** ([#1115](https://github.com/rust-works/omni-dev/issues/1115)): the transition body was hardcoded to `{"transition":{"id":…}}`, so any workflow transition whose screen required input — the classic "Resolve" needing a `resolution`, or a mandatory comment/assignee on the transition screen — was rejected by JIRA or left the field unset, blocking a very common automation path (auto-resolving issues from CI/scripts). `omni-dev atlassian jira transition execute` now accepts **`--set-field "NAME=VALUE"`** (repeatable; same YAML-scalar semantics as `jira write --set-field`, resolved against the transition's own screen fields), **`--resolution NAME`** (sent as `{"name": …}`), and **`--comment`** (JFM markdown). The comment is delivered **in the transition itself** (`update.comment`, atomic) when the transition screen accepts a comment — the only way to satisfy a mandatory-comment screen — and otherwise falls back to a separate comment post, so screenless transitions are unaffected. Screen-field metadata is fetched via `expand=transitions.fields` and resolved through the same machinery `jira write` uses. The MCP **`jira_transition`** tool mirrors this with new `resolution` and `custom_fields` params, and its existing `comment` now uses the same screen-aware routing. See [docs/user-guide.md](docs/user-guide.md) and [docs/mcp.md](docs/mcp.md).
- **Per-invocation USD cost on `AiClient` responses** ([#818](https://github.com/rust-works/omni-dev/issues/818)): AI calls reported latency but never a dollar cost, because the `AiClient` response surface exposed only a bare `String` — so downstream telemetry consumers (starting with `voice reflect`'s `reflections.log` in the separate `omni-voice` repo) had to record `cost_usd=unknown` even though the `claude-cli` backend already logged `total_cost_usd` and the direct Anthropic backend returns token usage. A new `AiClient::send_request_with_metrics` returns an `AiResponse { text, metrics: InvocationMetrics { cost_usd: Option<f64> } }`, decoupling per-call telemetry from the payload so more fields (token counts, cache hits) can be added later without touching every backend's return type. It is an additive **default** trait method that delegates to `send_request_with_options` and reports `cost_usd: None`, so `send_request`/`send_request_with_options` and the `bedrock`/`openai` backends are unchanged. The direct **Anthropic** backend ([`src/claude/ai/claude.rs`](src/claude/ai/claude.rs)) computes cost from the response's `usage.input_tokens`/`output_tokens` × a new per-model price table, and the **`claude-cli`** backend ([`src/claude/ai/claude_cli.rs`](src/claude/ai/claude_cli.rs)) surfaces the `total_cost_usd` it already parses from the subprocess envelope. The price table adds optional `input_token_price`/`output_token_price` (USD per million tokens) to `ModelSpec` and [`src/templates/models.yaml`](src/templates/models.yaml) for the Claude family; unpriced or unknown models (including the OpenAI/Gemini entries) fall back to `None` with a `tracing::warn!` so unpriced usage is noticed rather than silently zeroed. Per-backend cost for `bedrock`/`openai` and the `omni-voice` `reflections.log` wiring ([rust-works/omni-voice#11](https://github.com/rust-works/omni-voice/issues/11)) are follow-ups the trait surface now unblocks. See [ADR-0028](docs/adrs/adr-0028.md).
- **`SNOWFLAKE_BROWSER_COMMAND` to target a specific browser or Chrome profile for SSO** ([#1229](https://github.com/rust-works/omni-dev/issues/1229)): the client already supported a custom external-browser launch command (`BrowserLaunch::Command` with `{url}` substitution), but nothing populated it — the engine always resolved external-browser SSO to `BrowserLaunch::Auto` (the OS default handler), so the SSO popup could land in the wrong browser or the wrong Chrome profile with no way to redirect it. A new `SNOWFLAKE_BROWSER_COMMAND` env/`settings.json` key, when set, supplies a single command line with a `{url}` placeholder (or the URL is appended as a trailing argument), threaded through `SnowflakeEngineConfig::from_env_and_settings` → `resolve_auth_method` into every pooled session. The value is split with a self-contained POSIX-style tokenizer (single quotes, double quotes, backslash escapes — no new crate dependency) so a program path or argument value may contain spaces (`Google Chrome.app`, `--profile-directory="Profile 1"`); a present-but-malformed value (unterminated quote, or one tokenizing to no words) is a hard error at daemon startup rather than a silent fall back to the default handler. Engine-global and read once at startup, consistent with the other config-only settings; unset or blank keeps `Auto`, so existing setups are byte-for-byte unchanged. Applies only to external-browser SSO — the non-interactive methods (PAT / key-pair JWT) open no browser and ignore it. No CLI-surface change. See [docs/snowflake-service.md](docs/snowflake-service.md#external-browser-sso-default).
- **"Close Worktree" / "Close Window" from the VS Code worktrees tree** ([#1277](https://github.com/rust-works/omni-dev/issues/1277)): the worktrees tree view ([#1268](https://github.com/rust-works/omni-dev/issues/1268), [ADR-0048](docs/adrs/adr-0048.md)) could open a worktree's window but never close one, so tidying up a finished worktree meant switching to a terminal for `git worktree remove`. A new **`close`** op on the `worktrees` daemon service — socket-only, symmetric to `open`, and the service's first **destructive** capability ([ADR-0049](docs/adrs/adr-0049.md)) — closes a worktree's VS Code window and, for a **linked** worktree, deletes it. Right-clicking a leaf offers **Close Worktree** (linked) or **Close Window** (the main working tree, never deleted). The op is two-phase: a side-effect-free **safety check** reports what a removal would lose — modified tracked files, untracked files (ignoring `.gitignore`d), an in-progress rebase/merge/cherry-pick, and commits reachable only from a detached HEAD — so the extension shows a modal confirm **only** when data is actually at risk (or a multi-root window's other folders would close); a clean worktree deletes with no prompt. Unpushed commits on a named branch never block: removal keeps the branch. Deletability keys **solely on the structural `is_main` fact**, never the branch name — a linked worktree checked out on the default branch is fully deletable and its branch survives — and the daemon refuses to remove the main working tree even if a malformed client asks (a `git2`-enforced guard, not just UI gating). Removal is `git2` prune (`valid`+`working_tree`, no shell-out — avoiding the launcher's `PATH` problem — and it refuses a locked worktree rather than forcing past the lock), and it is idempotent for an already-gone worktree. A worktree open in **another** window is closed cross-window by signalling that window through an additive `close` field on its next `heartbeat` reply (the only channel the daemon has to a window it can reply to but never call, riding the `known:false → re-register` precedent, wire-compatible), then waiting bounded for it to unregister before deleting. In the companion, `worktreeContextValue` now encodes `is_main` as `.main`/`.linked` to route the two menu commands, and the heartbeat handler closes this window on the `close` directive. Socket-only, so no CLI-surface or `insta` snapshot change. See [ADR-0049](docs/adrs/adr-0049.md) and [docs/worktrees-service.md](docs/worktrees-service.md).
- **Worktrees daemon service gains repo/worktree enumeration, socket-driven `open`, and push subscriptions** ([#1265](https://github.com/rust-works/omni-dev/issues/1265), [#1266](https://github.com/rust-works/omni-dev/issues/1266), [#1267](https://github.com/rust-works/omni-dev/issues/1267)): the worktrees service ([ADR-0040](docs/adrs/adr-0040.md)) modelled only the *union of currently-open windows* via `register`/`heartbeat`/`unregister`, so it could neither surface a repo's closed/linked worktrees, launch an editor on demand for a socket client, nor push live changes — a tree UI could at best poll a flat open-window list. Three ops close the gap and lay the daemon-side foundation for the tree view ([ADR-0048](docs/adrs/adr-0048.md)). **`tree`** ([#1265](https://github.com/rust-works/omni-dev/issues/1265)) derives the distinct repositories from open-window folders (each open folder → its git common dir → repo root) and enumerates **all** worktrees per repo — open or not — via `git2`, enriching each with branch/ahead/behind/`is_main` and GitHub identity from `origin`, then joining open windows back on by canonicalised path; all git I/O runs on a blocking thread, off the registry lock. It is also exposed as a read-only **`omni-dev worktrees tree [-o table|json]`** CLI subcommand (repo-grouped rows, `*` marks the main working tree). **`open`** ([#1266](https://github.com/rust-works/omni-dev/issues/1266)) lets a socket client focus-or-open an arbitrary worktree folder in VS Code (`code <path>` uniformly focuses an already-open window or opens a new one), reusing the tray's launcher resolution and an absolute-existing-directory guard (which also blocks a `-`-leading path from being parsed as an argument-injection flag) — a deliberate, socket-ownership-and-`0600`-bounded escalation of the previously tray-only focus action. **`subscribe`** ([#1267](https://github.com/rust-works/omni-dev/issues/1267)) is the control socket's first streaming path: a new `ServiceStream` trait plus an optional `DaemonService::subscribe`, driven by `run_stream()`, which sends an initial snapshot then re-samples on a `tokio::sync::watch` change-notify (bumped by `register`/`unregister`/reap **after** the registry lock is released) and a 3 s safety tick, pushing only frames that differ from the last sent (automatic deduplication). Fully backward-compatible — clients that never send `subscribe` keep the classic one-request-one-reply exchange. See [ADR-0048](docs/adrs/adr-0048.md) and [docs/worktrees-service.md](docs/worktrees-service.md).
- **Live worktrees tree view in the VS Code companion** ([#1268](https://github.com/rust-works/omni-dev/issues/1268)): the companion extension previously only *reported* its own window to the daemon and had no UI of its own. It now renders a dedicated activity-bar tree view of every repository and git worktree open across all VS Code windows, fed by the daemon's push `subscribe` stream ([ADR-0048](docs/adrs/adr-0048.md)) with exponential-backoff reconnection (`subscription.ts`), so the view stays live and self-healing across daemon restarts. A vscode-free tree model (`tree.ts`) and a thin `TreeDataProvider` adapter map daemon snapshots to icons/labels/tooltips; double-clicking a worktree opens it through the daemon's `open` op (manual double-click detection, since the TreeView API has no native double-click event), and a title-bar button forces a manual `tree` refresh. The extension is bumped 0.1.0 → 0.2.0. See [ADR-0048](docs/adrs/adr-0048.md) and [docs/worktrees-service.md](docs/worktrees-service.md).
- **Current-window badge in the worktrees tree view** ([#1274](https://github.com/rust-works/omni-dev/issues/1274)): the tree view marked a worktree as merely open-or-not, so with several concurrent windows a user could not tell at a glance which worktree they were *actively* working in. A three-way badge now distinguishes a **blue tick** (the worktree open in *this* window), a **green dot** (open in *another* window), and the branch glyph (no live window). The window's own `windowKey`, assigned at activation, is threaded into `WorktreesTreeDataProvider` and compared against each worktree's registry `window_key` — a pure client-side consumer of a field the `tree` payload already carries, so no daemon or protocol change is needed. `worktreeContextValue()` returns `worktree.current` for the current window's worktree to gate window-specific context-menu items. See [docs/worktrees-service.md](docs/worktrees-service.md).
- **Provider inference recognises OpenAI and Gemini model identifiers** ([#1119](https://github.com/rust-works/omni-dev/issues/1119)): an unknown model from OpenAI or Gemini — one not yet in the embedded catalog — fell through `infer_provider()` to the conservative hard-coded universal fallback (4096/8192 tokens), discarding provider-specific capability. `infer_provider()` in [`src/claude/model_config.rs`](src/claude/model_config.rs) now detects OpenAI models via `gpt-`, `chatgpt-`, and the `o<N>`-reasoning shape (a dedicated `is_openai_reasoning_id()` helper matches `o1`/`o3`/`o4-mini`/… without false positives on other `o`-leading ids) and Gemini models via the `gemini-` prefix, so an unrecognised model resolves to its provider's defaults — OpenAI 16384/128000, Gemini 8192/1048576 — instead of the universal fallback. Purely additive: known models and Claude inference are unchanged.
- **Companion VS Code extension is now publishable to the VS Code Marketplace and Open VSX** ([#1279](https://github.com/rust-works/omni-dev/issues/1279), [#1280](https://github.com/rust-works/omni-dev/issues/1280), [#1282](https://github.com/rust-works/omni-dev/issues/1282), [#1287](https://github.com/rust-works/omni-dev/issues/1287)): the companion extension shipped in 0.33.0 with Marketplace/Open VSX publishing left as an explicit deferred follow-up ([#1111](https://github.com/rust-works/omni-dev/issues/1111) — until now, install the CI-built `.vsix` by hand); that follow-up is closed. A dedicated `vscode-extension-release.yml` workflow, triggered **exclusively** by `vscode-v*` tags (never a routine `main` push), verifies the publisher secrets are present and that the pushed tag matches `package.json`, re-runs the full typecheck/build/test/package, and publishes the *same* `.vsix` bytes to both the VS Code Marketplace (`vsce`) and Open VSX (`ovsx`) ([#1279](https://github.com/rust-works/omni-dev/issues/1279)). A 128×128 raster PNG gallery icon (`media/icon.png`, blue-to-indigo gradient, with an editable `media/icon.svg` source excluded from the package) satisfies the Marketplace's raster-only `"icon"` requirement ([#1280](https://github.com/rust-works/omni-dev/issues/1280)). The extension gains its own Keep-a-Changelog `editors/vscode/CHANGELOG.md` — which the Marketplace and Open VSX render as the listing's Changelog tab — plus release docs, and versions **independently** of the crate (currently 0.2.0) ([#1282](https://github.com/rust-works/omni-dev/issues/1282)). Release hardening then excludes `vscode-*` tags from the crate `release.yml` `v*` trigger (GitHub's `*` glob matched `vscode-v*`, spuriously firing a `cargo publish`) and makes Open VSX optional — only `VSCE_PAT` is required, and a missing `OVSX_PAT` skips that publish with a `::notice::` rather than failing the run ([#1287](https://github.com/rust-works/omni-dev/issues/1287)). See [editors/vscode/CHANGELOG.md](editors/vscode/CHANGELOG.md) and [docs/worktrees-service.md](docs/worktrees-service.md).

### Changed
- **ADF schema snapshot bumped to `@atlaskit/adf-schema` 56.1.1** ([#1231](https://github.com/rust-works/omni-dev/issues/1231)): Refreshes the pinned upstream snapshot from `56.0.9` to `56.1.1` after the weekly `adf-schema-drift` job detected an upstream release. This is a **version bump only** — and another inert one: the upstream `dist/json-schema/v1/full.json` is **byte-for-byte identical** across all of these releases (same `full_json_sha256`), so the vendored `assets/adf-schema/full.json` is unchanged and the regenerated `generated.rs` carries the same `UPSTREAM_ENTRIES` and `UPSTREAM_FULL_JSON_SHA256` as before. The only changes are the provenance constants that pin *which release* the snapshot was taken from: `SCHEMA_VERSION` and `UPSTREAM_TARBALL_SHA256` in `src/atlassian/adf_schema.rs`, the mirrored `UPSTREAM_VERSION` / `UPSTREAM_TARBALL_SHA256` in `generated.rs`, `assets/adf-schema/provenance.json`, and the JFM spec + README version references. No validation behaviour changes. (The drift job flagged `56.0.15`; `56.1.1` was the current `latest` at refresh time and is also content-identical, so the snapshot is pinned to it to keep the drift job green.)

### Documentation
- **ADR-0048 records the worktrees tree view and its daemon push subscription** ([#1269](https://github.com/rust-works/omni-dev/issues/1269)): adds [ADR-0048](docs/adrs/adr-0048.md), documenting the four coordinated design decisions behind the tree view — the `tree` op that inverts the open-window registry into a grouped repo/worktree view, the `open` focus/spawn op (with its absolute-existing-directory guard and threat model), the control socket's first streaming path (`subscribe` with a `tokio::sync::watch` change-notify + 3 s tick + diff-before-push coalescing), and the companion's activity-bar tree UI (three-way open-badge, double-click timer, exponential-backoff reconnect) — and substantially expands [docs/worktrees-service.md](docs/worktrees-service.md) to match: the new ops in the wire-contract table, push-subscription framing/reconnect semantics, the three-way badge iconography, a revised data-flow diagram, and an expanded security section covering the `open` spawn capability and the `subscribe` stream. Documentation only.

## [0.33.0] - 2026-07-08

### Added
- **Companion VS Code extension for the worktrees service** ([#1111](https://github.com/rust-works/omni-dev/issues/1111)): the daemon's worktrees service (engine, `omni-dev worktrees list`, tray submenu, git enrichment) shipped fully but had **no data producer** — the registry only fills via the `register`/`heartbeat`/`unregister` ops that, per [ADR-0040](docs/adrs/adr-0040.md), are driven exclusively by a "first-party companion VS Code extension (separate deliverable)" that did not exist, so the feature was inert end-to-end (`worktrees list` and the tray were always empty). That extension now ships in [`editors/vscode/`](editors/vscode/): a thin TypeScript per-window reporter that, over the daemon's `0600` Unix control socket (newline-delimited JSON), `register`s the window's open folders under a fresh per-`activate()` UUID, `heartbeat`s every ~10s (re-registering when a restarted in-memory daemon replies `known: false`), re-registers on workspace-folder changes, and `unregister`s on deactivation — and no-ops silently when the daemon is not running. It recomputes the daemon's socket path (`<data_dir>/omni-dev/daemon.sock`, matching the Rust `dirs` crate on macOS and Linux) and reports only raw folder paths, leaving branch/ahead-behind enrichment to the daemon (#1186) so the reporter stays thin. It is kept fully isolated from the Rust build: single-crate `cargo` never descends into `editors/`, which is added to `Cargo.toml`'s `exclude` so `cargo publish` never ships it, and a new path-filtered `vscode-extension.yml` workflow type-checks, unit-tests, builds (esbuild), and packages a `.vsix` artifact without touching any Rust job. Publishing to the VS Code Marketplace / Open VSX is a deferred follow-up (it needs a publisher account and CI secrets); until then, install the CI-built `.vsix` with `code --install-extension`. See [docs/worktrees-service.md](docs/worktrees-service.md) and [ADR-0040](docs/adrs/adr-0040.md).
- **JIRA attachment upload and delete — the last read-only CRUD gap closed** ([#1114](https://github.com/rust-works/omni-dev/issues/1114)): JIRA attachments were pull-only — the client had only `get_attachments` and the CLI/MCP surface exposed only `jira attachment download` / `images`, while Confluence already had the full upload/list/download/delete set. You could copy a file *off* an issue but never push a log/screenshot/artifact *onto* one, nor remove a stale attachment — a routine "attach evidence to a ticket" automation was impossible. Two verbs close it, reusing the existing multipart plumbing. **`jira attachment upload <ISSUE> <FILE>...`** accepts one or more files and streams each from disk (`ReaderStream`, never fully buffered) as repeated `file` parts of a single multipart `POST /rest/api/3/issue/{key}/attachments` with the `X-Atlassian-Token: no-check` XSRF header, printing metadata for every created attachment. **`jira attachment delete <ATTACHMENT_ID>`** issues `DELETE /rest/api/3/attachment/{id}` (permanent — JIRA has no trash/purge) behind the same destructive guard the Confluence delete uses: an interactive `[y/N]` confirm, `--dry-run` to preview without an API call, and `--force` to skip the prompt. Matching MCP tools ship too: **`jira_attachment_upload`** (takes `file_paths`, returns the created attachments as YAML) and **`jira_attachment_delete`** (guarded by `confirm: true`, mirroring the sibling `jira_delete` tool rather than deleting silently). See [docs/user-guide.md](docs/user-guide.md#jira-attachments) and [docs/mcp.md](docs/mcp.md).
- **Non-interactive Snowflake auth: programmatic access tokens and key-pair JWT** ([#1108](https://github.com/rust-works/omni-dev/issues/1108)): external-browser SSO was the *only* implemented Snowflake auth method, so the daemon could not authenticate headless — no CI, servers, or display-less containers (the v2 client that would have brought these was declined in [#1003](https://github.com/rust-works/omni-dev/issues/1003), leaving the capability gap untracked). Two non-interactive methods now sit alongside SSO on the same clean-room **v1** REST client, selected by a new `SNOWFLAKE_AUTHENTICATOR` env var (resolved from the daemon's environment, then `settings.json`, once at startup): **`programmatic_access_token`** (alias `pat`) presents a PAT from `SNOWFLAKE_TOKEN` in place of a password (Snowflake requires the user to be covered by a network policy to use one), and **`snowflake_jwt`** (aliases `keypair`/`key_pair`/`jwt`) signs a short-lived RS256 JWT locally from an unencrypted PKCS#8 private key at `SNOWFLAKE_PRIVATE_KEY_PATH` (or inline `SNOWFLAKE_PRIVATE_KEY`), authenticating against the public key registered via `ALTER USER … SET RSA_PUBLIC_KEY`. Unset or `externalbrowser` keeps the interactive default byte-for-byte unchanged. All three finish through one shared `session/v1/login-request` path and yield the same session tokens, so the bounded session pool, transparent token renewal, and the keep-alive heartbeat are auth-method-agnostic and untouched. JWT signing uses `aws-lc-rs` (already in the tree via rustls — deliberately **not** the `rsa` crate, the RUSTSEC-2023-0071 reason `snowflake-connector-rs` was dropped); passphrase-encrypted keys are not yet supported, and setting `SNOWFLAKE_PRIVATE_KEY_PASSPHRASE` fails fast with decrypt guidance. No secret is persisted (in-memory only, wrapped so it stays out of logs); no CLI-surface or daemon wire-protocol change. See [docs/snowflake-service.md](docs/snowflake-service.md#authentication).
- **`SNOWFLAKE_HOST` override for PrivateLink and custom API hosts** ([#1224](https://github.com/rust-works/omni-dev/issues/1224)): the Snowflake API host was always derived as `<account>.snowflakecomputing.com`, so AWS/Azure PrivateLink endpoints and gov/custom hosts were unreachable — the client already honoured a `host` override (`SnowflakeClientConfig.host` / `api_host()`) but nothing populated it. A new `SNOWFLAKE_HOST` env/`settings.json` key, when set, is used verbatim as the API host, resolving through `SnowflakeEngineConfig::from_env_and_settings` (via a `host_override_from` helper that trims and treats blank as unset) and threading into every pooled session in `create_session_with_base`. Engine-global and read once at daemon startup, consistent with the other config-only settings; unset keeps the derived host unchanged. No CLI-surface change. See [docs/snowflake-service.md](docs/snowflake-service.md#configuration).
- **`snowflake disconnect --id` / `--all` over the socket and CLI** ([#1228](https://github.com/rust-works/omni-dev/issues/1228)): the engine already had `disconnect_all()` and `disconnect_by_id()`, but the daemon socket op and `omni-dev snowflake disconnect` exposed only the exact `{account, user}` pair form — the bulk and by-id variants were reachable only from the macOS tray, so non-macOS operators had to restart the daemon to bulk-evict sessions. The `disconnect` op/subcommand is overloaded with two more mutually-exclusive selectors: `--id <ID>` (a single pooled session) and `--all` (every session), guarded by a required clap `ArgGroup` so exactly one selector is given (account/user still require each other for the pair form). The socket reply now carries a `count` alongside `disconnected`, so existing callers of the by-id/pair reply stay wire-compatible. See [docs/snowflake-service.md](docs/snowflake-service.md).
- **CSV/TSV output and `--out-file` for `snowflake query`** ([#1227](https://github.com/rust-works/omni-dev/issues/1227)): `snowflake query` gains `csv` and `tsv` output formats — RFC 4180-compliant field quoting, so embedded quotes, commas, and newlines are escaped — alongside the existing JSON and YAML, plus an `--out-file <PATH>` flag that writes results to a file instead of stdout. Column ordering is preserved and duplicate column names are disambiguated with a numeric suffix (`SELECT 1, 1` yields a `_2` column), and every format now ends with a trailing newline for consistency. This makes query results directly consumable by spreadsheets and data pipelines while leaving the JSON/YAML workflows unchanged. See [docs/snowflake-service.md](docs/snowflake-service.md).
- **`coverage diff --ignore-filename-regex` to exclude files from the delta** ([#1220](https://github.com/rust-works/omni-dev/issues/1220)): a new repeatable (and comma-separated) `--ignore-filename-regex` flag on `coverage diff` excludes files from the coverage-delta computation, matching the semantics of `cargo llvm-cov --ignore-filename-regex`. Patterns are compiled into a `RegexSet` with upfront syntax validation and applied — after strip-prefix normalisation to repo-relative paths — **symmetrically** to both the head and baseline reports before the diff, so the two share the same denominator and `--fail-under-patch` gates plus indirect-change lists stay valid even when the baseline predates the exclusion rule. Wired through to the MCP `coverage_diff` tool as `ignore_filename_regex` (`serde(default)`). See [docs/coverage.md](docs/coverage.md) and [docs/mcp.md](docs/mcp.md).
- **Worktrees service names the parent repo and marks linked worktrees** ([#1249](https://github.com/rust-works/omni-dev/issues/1249)): `worktrees list`, `daemon status`, and the tray "Worktrees" submenu now identify which repository each open window belongs to. `GitStatus` gains `main_repo` (the parent repository name, derived from git's common dir — handling both the normal `.git`-subdirectory and bare `.git`-suffixed layouts) and an `is_worktree` flag, computed even for unborn/detached HEAD so a worktree always names its parent. A linked worktree now displays its parent repo name (not its folder basename) and is visually distinguished with a fork glyph (`⑂`), where a normal checkout uses `·`; the CLI table and tray labels prefer the daemon-computed `main_repo` over the companion-reported name. The tray also collapses to one clickable line per window (the live stats label doubles as the focus action) instead of a stats row plus a separate focus row. Both new fields are `#[serde(skip_serializing_if)]`, so the wire protocol stays backward-compatible. See [ADR-0040](docs/adrs/adr-0040.md) and [docs/worktrees-service.md](docs/worktrees-service.md).
- **Cross-service request-log correlation for daemon-served requests** ([#1198](https://github.com/rust-works/omni-dev/issues/1198)): when the CLI or MCP calls through the daemon (Snowflake queries, browser-bridge requests), the HTTP records the daemon spawned were logged under the *daemon's* own `invocation_id`, so `omni-dev log --id <cli-invocation>` could not surface both halves of one logical operation. The originating client's `invocation_id` is now threaded across the control socket (a new optional `origin_invocation_id` on `DaemonEnvelope`, set via `with_origin()`) and the browser bridge (a new `X-Omni-Bridge-Origin` header), and `request_log::scope_origin_id()` overrides the `invocation_id` for the duration of the daemon-side work — so daemon-triggered HTTP records now correlate back to the caller while still reading `via_daemon: true` (and preserving `source`). The origin id is non-secret (not a token). Backward compatible: older clients omit the field and the daemon skips correlation. Closes the follow-up split out of [#1122](https://github.com/rust-works/omni-dev/issues/1122). See [docs/log.md](docs/log.md).

### Changed
- **Upgrade `rmcp` from 1.7.0 to 2.1.0** ([#1222](https://github.com/rust-works/omni-dev/issues/1222)): bump the MCP SDK (`rmcp` + `rmcp-macros`) to 2.1.0 and migrate to its new API surface across all ten MCP tool modules and the resource handlers — `Content` → `ContentBlock`, the removed `RawContent` / `RawResource` / `RawResourceTemplate` intermediaries replaced by direct variants, and the `Resource::new()` builder with chainable `with_*()` methods. Internal migration only; the advertised tool/resource surface is unchanged (manifest + lockfile bump).
- **Bump the `rust-minor-patch` dependency group** ([#1221](https://github.com/rust-works/omni-dev/issues/1221)): routine dependabot update of `clap_complete` (4.6.5 → 4.6.7) and `rand` (0.10.1 → 0.10.2). Manifest + lockfile bump; no source or behaviour changes.

### Fixed
- **Worktrees tray no longer pegs a core or stalls daemon shutdown** ([#1249](https://github.com/rust-works/omni-dev/issues/1249)): the per-worktree git enrichment added in [#1186](https://github.com/rust-works/omni-dev/issues/1186) ran on the macOS GUI thread on every tray `menu()` call, so its `git2` I/O pegged a core and could stall daemon shutdown. `WorktreesService` now holds the registry behind an `Arc` and caches a menu snapshot rebuilt by a background tokio task every 2s; `menu()` serves the cached snapshot (falling back to an inline compute before the first refresh or outside a tokio runtime), the tray event loop throttles service polling to at most once per second, and `shutdown()` cancels and joins the refresh task. The service is a no-op under unit tests (which run without a runtime). See [ADR-0040](docs/adrs/adr-0040.md).

### Documentation
- **Architecture decision records backfilled and amended for the current design** ([#1211](https://github.com/rust-works/omni-dev/issues/1211), [#1212](https://github.com/rust-works/omni-dev/issues/1212), [#1213](https://github.com/rust-works/omni-dev/issues/1213), [#1214](https://github.com/rust-works/omni-dev/issues/1214), [#1215](https://github.com/rust-works/omni-dev/issues/1215), [#1216](https://github.com/rust-works/omni-dev/issues/1216), [#1218](https://github.com/rust-works/omni-dev/issues/1218), [#1219](https://github.com/rust-works/omni-dev/issues/1219)): a batch of ADRs recording already-shipped behaviour — ADR-0041 (pushed-commit amendment guard), ADR-0042 (the request-log subsystem), ADR-0043 (default-on credential redaction), ADR-0044 (unified AI backend and model resolution), ADR-0045 (isolated named credential profiles), ADR-0046 (unified `-o`/`--output` format convention), and ADR-0047 (remote-first fail-closed base-branch resolution) — plus amendments to ADR-0027 (narrowing the MCP dry-run scope for create/write tools) and ADR-0039 / ADR-0040 (formalising the service engine/adapter split). The `dry_run` helper-scope documentation is also clarified ([#1238](https://github.com/rust-works/omni-dev/issues/1238)), and stale git-worktree convention guidance is removed. Documentation only.

### CI/CD
- **Path-split gate to accelerate docs-only PRs** ([#609](https://github.com/rust-works/omni-dev/issues/609)): a new `gate` job runs `dorny/paths-filter` on pull requests and, for docs-only / skills-only changes (`*.md`, `docs/**`, `.claude/skills/**`; anything else fails safe to full CI), skips the heavy jobs while still publishing every required status context — so a docs PR merges without waiting on the full matrix and without bypassing branch protection (a skipped *step* inside a passing job reports success, not a skipped *conclusion*). Non-PR events (main pushes, release tags) always take the full path. Centralises the decision in one gate rather than a separate `ci-skip.yml` that would silently rot as the matrix or branch-protection rules change. See [docs/ci-path-split.md](docs/ci-path-split.md) ([#598](https://github.com/rust-works/omni-dev/issues/598)).

## [0.32.0] - 2026-07-06

### Added
- **Per-origin `--allow-origin` allowlist for the browser bridge** ([#1185](https://github.com/rust-works/omni-dev/issues/1185)): `browser bridge serve --allow-origin` (and the daemon's `--bridge-allow-origin`) is now **repeatable** and scoped per connecting tab instead of a single global value. Each value is a bare `ORIGIN` (a tab on that origin may reach it) or a `CONNECT=OUTBOUND` mapping (a tab connecting from `CONNECT` may reach `OUTBOUND`); repeats under one connecting origin accumulate its outbound set. The WebSocket-upgrade gate now admits any connecting origin that is a configured **key**, and the per-request outbound-URL check reads the outbound set bound to the **tab a request is routed to** — so a Grafana tab and a Facebook tab each carry only their own grant and neither can borrow the other's scope. Default-closed is unchanged: with no `--allow-origin`, the gate stays open (the session token is the gate) and outbound is relative-URLs-only. The per-request `request --allow-origin` override still wins for a single request. Trust boundary unchanged; see [ADR-0036](docs/adrs/adr-0036.md) and [docs/browser-bridge.md](docs/browser-bridge.md#outbound-request-scope-default-closed).
- **MCP tools for the newer subsystems — Snowflake, request log, transcript, coverage, git amend, browser bridge — plus `jira_search` convenience filters** ([#1120](https://github.com/rust-works/omni-dev/issues/1120)): the MCP server exposed 91 tools for the older subsystems (git/jira/confluence/atlassian/ai/config/datadog) but **zero** for the newer ones — the combined router in [`src/mcp/server.rs`](src/mcp/server.rs) stopped at eight families, so an agent could not run a Snowflake query, search its own request log, fetch a transcript, diff coverage, apply commit-message amendments, or drive an authenticated bridge request even though each already existed as a CLI command with a reusable, non-printing core. Ten tools across five new routers close the gap, each delegating to that same core (MCP is a transport, not a fork): **`snowflake_query`/`snowflake_sessions`/`snowflake_disconnect`** send `query`/`sessions`/`disconnect` ops over the daemon's Unix control socket (so they require a running daemon, and first-time `(account, user)` auth opens external-browser SSO on the daemon host); **`log_search`** reuses the CLI filter matrix + renderers (`oneline`/`json`/`full`), capturing output instead of streaming (`--follow` has no MCP form); **`transcript_youtube_fetch`/`info`/`list_langs`** call the `TranscriptSource` trait (the mutating `sync` is omitted); **`coverage_diff`** reuses `DiffCommand::run`, returning the rendered report + patch percentage + gate result and never failing the call (reports `below_gate` instead); **`git_amend_commits`** is the deterministic apply-messages-from-YAML counterpart to `git_twiddle_commits`, taking the amendments as an inline YAML string (a new `AmendmentFile::from_yaml_str` + non-printing `run_amend` core) and refusing already-pushed commits unless `allow_pushed` is set; **`browser_bridge_request`** drives one non-streaming request through a running authenticated tab via the shared `BridgeClient::send` (not read-only — a non-GET method can mutate remote state; `serve`/`harvest` remain CLI-only). Separately, the MCP **`jira_search`** tool now accepts the `project`/`assignee`/`status` convenience filters the CLI has (with `jql` made optional) by reusing an extracted `build_jql_from_filters` shared with the CLI; its `fields` parameter remains the documented informational no-op it already was. The `sync`, `serve`, `harvest`, and daemon-lifecycle surfaces stay intentionally CLI-only. See [docs/mcp.md](docs/mcp.md#tool-catalog).
- **`daemon start` auto-starts at login on Linux via a systemd user unit** ([#1174](https://github.com/rust-works/omni-dev/issues/1174)): `omni-dev daemon start` gains login persistence on Linux, mirroring the macOS launchd LaunchAgent (the deferred second half of [#1112](https://github.com/rust-works/omni-dev/issues/1112), whose detach fix already landed). On a host with a systemd **user** manager it now writes a socket-activated `omni-dev-daemon.socket` + `omni-dev-daemon.service` pair under `~/.config/systemd/user/` and runs `systemctl --user enable --now omni-dev-daemon.socket`: systemd owns the control socket (created `0600` via `SocketMode`) and demand-spawns `daemon run` on the first client connect, and `WantedBy=sockets.target` re-activates it at login. `daemon run` adopts the inherited descriptor via a hand-rolled `sd_listen_fds` implementation ([`src/daemon/systemd.rs`](src/daemon/systemd.rs); reads `LISTEN_FDS`/`LISTEN_PID`, no libsystemd dependency, and sets `FD_CLOEXEC` so the socket is not leaked into child processes), reusing the same socket-activation shutdown invariant as macOS (the manager owns the inode, so shutdown does not unlink it). `daemon stop` stops and disables the socket unit — the analogue of launchd's `bootout` — so it stays down. When no user manager is available (containers, non-systemd distros, or the new `OMNI_DEV_DAEMON_DISABLE_SYSTEMD=1` escape hatch) `start` falls back to the existing detached `setsid` spawn with no login auto-start, exactly as before. One Linux-specific wrinkle: because systemd re-arms the socket the instant the service goes inactive, `daemon restart` no longer polls the daemon *down* (a probe `connect` would re-activate it) — it shuts down and lets the relaunch plus readiness ping drive a clean handoff. See [ADR-0039](docs/adrs/adr-0039.md) and [docs/browser-bridge.md](docs/browser-bridge.md#running-under-the-daemon).
- **Global `--model` / `--beta-header` flags and `--ai-backend openai|ollama|bedrock`** ([#1118](https://github.com/rust-works/omni-dev/issues/1118)): `--model` and `--beta-header` are now global flags on the root command (propagated as `OMNI_DEV_MODEL` / `OMNI_DEV_BETA_HEADER`), so the documented invocation shape `omni-dev --model claude-opus-4-6 git commit message twiddle …` — which clap previously rejected outright — parses, and the old post-subcommand placement keeps working via clap's `global = true`. The per-subcommand copies on `ai chat`, `git commit message twiddle|check|staged`, and `git branch create pr` are removed (both flag positions still parse; MCP tools keep their explicit `model` parameters, which outrank the env). `--ai-backend` is extended from `{default, claude-cli}` to `{default, claude-cli, openai, ollama, bedrock}`, so the three backends that were previously reachable only through `USE_OPENAI`/`USE_OLLAMA`/`CLAUDE_CODE_USE_BEDROCK` env vars are now first-class flag values. `OMNI_DEV_MODEL` slots in above the per-family model vars and, like every resolver input, is settable via `~/.omni-dev/settings.json` env bundles/profiles. See [docs/ai-backends.md](docs/ai-backends.md#dispatch-order-and-model-selection).
- **`--set-field` accepts comma-separated values for array-typed fields** ([#1172](https://github.com/rust-works/omni-dev/issues/1172)): array-typed JIRA fields (labels, components, fix versions, multi-selects) previously required YAML flow syntax — `--set-field 'Labels=a,b,c'` YAML-parsed to a scalar string and was rejected with `expected a sequence for array field 'Labels'`, and the error never named the `[a, b, c]` form users needed, so most looped through several syntaxes before landing on it. The shared field resolver now treats a scalar on an array-typed field (schema `type: array`) as comma-separated shorthand: split on `,`, each element whitespace-trimmed, empty elements dropped — so `Labels=a,b,c`, `Labels= a , b ,c`, and `Labels=[a, b, c]` all send the identical wire payload, and a lone scalar (`Labels=urgent`) becomes a one-element array. Element shaping and `allowedValues` validation are unchanged (labels stay bare strings, components/versions `{"name": …}`, options `{"value": …}`); sequence elements are never split, so `Labels=["a,b"]` remains the escape hatch for a literal comma inside one element, and `Labels=""` clears the field. Scalar (non-array) fields are untouched — a comma in `Summary=Fix bug, add test` stays literal. Because the split lives in the shared resolver, it covers CLI `jira write --set-field` / `jira create --set-field` and the MCP `jira_edit`/`jira_create` field maps alike; `--set-field` remains **replace**, not append. CLI `--dry-run` still prints the raw pre-conversion value (it deliberately makes no network call, so field schemas are unknown at that point). See [docs/user-guide.md](docs/user-guide.md#jira-reading-and-writing-issues).
- **Per-commit `in_main_branches` is now populated, and amend refuses already-pushed commits** ([#1105](https://github.com/rust-works/omni-dev/issues/1105)): two coupled gaps from the #1103 functionality-gap analysis are closed. First, the documented `commits[].in_main_branches` field ("array of remote main branches that contain this commit") had been a permanent empty stub since its introduction — every commit, including ones on `origin/main`, reported `[]`, so any consumer (human or AI) inferred every commit was unpushed. A new [`src/git/main_branches.rs`](src/git/main_branches.rs) module resolves each remote's main-branch tip **locally and offline** (symbolic `refs/remotes/<remote>/HEAD`, else the `main`/`master`/`develop` common-name fallback; remotes that resolve neither are skipped conservatively rather than guessed) and checks containment per commit via the git2 commit graph — tips are computed once per invocation, not per commit. Second, that containment now gates the amend path: `validate_commit_amendable` (the chokepoint all three amend surfaces — `amend`, `twiddle`, and `check --twiddle` — funnel through) refuses to amend a commit contained in a remote main branch, naming the short hash, the containing branches, and the escape hatch. A new `--allow-pushed` flag on `amend` and `twiddle` overrides the refusal (with a printed warning); the MCP `git_twiddle_commits` tool deliberately does **not** expose the override — an AI caller silently rewriting published history is exactly the failure mode the guard exists for, so overriding requires a human running the CLI flag. (`CommitInfo::from_git_commit` gains a `main_tips` parameter — technically API-breaking for library consumers.) See the [user guide](docs/user-guide.md#safety-already-pushed-commits).
- **MCP `jira_edit` tool — set arbitrary fields on an existing issue by name** ([#1157](https://github.com/rust-works/omni-dev/issues/1157)): once a ticket was created, changing labels or custom fields (Acceptance Criteria, Story Points, …) from MCP forced a fallback to raw `curl -X PUT /rest/api/3/issue/{key}` with a hand-authored payload — `jira_write`'s `fields` escape hatch takes only canonical ids with already-API-shaped values. The new `jira_edit` tool takes an issue key plus a `{field-name-or-id: value}` map, resolves names against the issue's edit screen (`editmeta`), and coerces each value to the API shape with the same shared resolver the CLI uses: select/option strings become `{"value": …}`, multi-selects arrays of those, labels plain string arrays, number/date scalars pass through, issue-link fields (e.g. `Parent`) wrap an issue key as `{"key": …}`, and rich-text fields accept JFM markdown (auto-converted to ADF; `""` clears) or a raw ADF document object (`{"type": "doc", …}`, validated and forwarded as-is). Supports `dry_run: true` (previews the exact `PUT` method/path/body like the other mutating tools) and returns `{status: ok, key, updated_fields}` as YAML. Unlike `jira_write`'s pass-through hatch, an `editmeta` failure is fatal — resolution is the tool's contract. Mirrors the CLI's existing `omni-dev atlassian jira write --set-field`; no new CLI subcommand (the CLI `jira edit` name belongs to the interactive editor). See [docs/mcp.md](docs/mcp.md).
- **`*_path` file alternatives to inline bodies on write-side MCP tools** ([#1093](https://github.com/rust-works/omni-dev/issues/1093)): an AI caller pays an O(size) generation cost to emit a large body inline through a tool call — the model writes every byte through its output stream — so writing a 70 KB Confluence page could take minutes just to *emit the tool call*, versus seconds for the same bytes read from disk. This is the write-side mirror of `confluence_read`/`jira_read`'s `output_file` (which already solved the symmetric read-side cost). Every text body a write tool accepts inline now has an optional sibling parameter the server reads from a filesystem path instead: `confluence_write`/`jira_write` gain `content_path`; `confluence_create` gains `content_path` + `document_path`; `jira_create` gains `description_path` + `document_path`; `confluence_comment_add`/`confluence_comment_add_inline` gain `content_path`; `jira_comment`/`jira_comment_edit` gain `body_path`; and `ai_chat` gains `message_path`. The inline and path forms are **mutually exclusive** (supplying both errors before any network call), and where the body is required exactly one must be given. Binary uploads (`confluence_attachment_upload`) already stream from disk via `file_path`, so no inline-base64 path was ever introduced. A shared [`src/mcp/content_input.rs`](src/mcp/content_input.rs) helper (`resolve_content_input`/`require_content_input`) centralises the either/or resolution and mutual-exclusion check, and each tool's description now says to prefer the path form when the body is already on disk. A diff-style edit tool (the issue's second capability) is intentionally deferred as separate, larger work. See [docs/mcp.md](docs/mcp.md#content_path-on-write-tools).
- **Account-ID → user lookup for JIRA and Confluence** ([#1091](https://github.com/rust-works/omni-dev/issues/1091)): author, assignee, and reporter fields come back as opaque `accountId`s, and the only existing resolver went the other way (display name → id via user search). New `omni-dev atlassian jira user get` / `confluence user get` commands and MCP tools `jira_user_get` / `confluence_user_get` resolve the reverse via `GET /rest/api/3/user?accountId=` (JIRA) and `GET /wiki/rest/api/user?accountId=` (Confluence). Lookups are **batch and failure-tolerant**: an unknown, anonymised, or permission-denied id yields a stub record carrying an `error` field (HTTP status + body snippet) rather than failing the whole batch, while a `401` stays a hard error. The read-side tools whose output surfaces bare account ids (`jira_read`, `jira_comment`, `jira_changelog`, `confluence_read`, `confluence_history`, `confluence_comment`) now cross-reference the new lookup in their descriptions. Table and JSON output for user records. See [docs/user-guide.md](docs/user-guide.md).
- **Inline-comment drift auditing and re-anchoring for Confluence** ([#1092](https://github.com/rust-works/omni-dev/issues/1092)): Confluence anchors an inline comment to a run of text via annotation marks, and when that text is later edited it keeps the marks on whatever original characters survive — leaving comments *torn* across fragments, *drifted* onto unrelated text, or silently dropped. New `confluence comment audit` classifies every inline comment on a page as `Ok`/`Torn`/`MarkLost`/`Drifted` (read-only), and `confluence comment reanchor` moves a comment's anchor to a new run and writes the page back in one ADF update — including anchors split across formatting runs, with 1-based match indexing to disambiguate repeated text. Both ship as CLI commands (confirmation prompts + `--dry-run`) and MCP tools (`confluence_comment_audit` / `confluence_comment_reanchor`); the re-anchor logic operates entirely on typed ADF (never JFM) so it never disturbs surrounding marks, and validates the result before writing. `confluence read` also gains a `--version` flag to fetch a historical snapshot. See [docs/user-guide.md](docs/user-guide.md).
- **Request-log growth bounds: size-capped rotation and `omni-dev log prune`** ([#1121](https://github.com/rust-works/omni-dev/issues/1121)): the append-only `log.jsonl` had no built-in cap. Two opt-in mechanisms now bound it. **Automatic rotation** (unix-only, env-gated): `OMNI_DEV_LOG_MAX_SIZE` (e.g. `10mb`) rotates before an append that would exceed the cap, shifting numbered files (`log.jsonl.1`, `.2`, …) and keeping at most `OMNI_DEV_LOG_KEEP_FILES` (default 3); writers serialize the check-rotate-append on a stable `log.jsonl.lock`, and a rotation failure degrades to a plain append (best-effort). **Explicit pruning**: the new `omni-dev log prune` subcommand takes `--older-than <DUR>` (`7d`, `24h`) and/or `--max-size <SIZE>` (at least one required), rewrites the file atomically via a same-directory temp file + rename (preserving `0600`, so concurrent readers never see a half-written file), conservatively keeps records with missing/unparseable timestamps and always retains the most recent record, and previews with `--dry-run`. The `-f/--follow` reader already tolerates rotation and truncation. See [docs/log.md](docs/log.md).
- **Binary request bodies for the browser bridge** ([#1123](https://github.com/rust-works/omni-dev/issues/1123)): `browser bridge request` could send only UTF-8 text bodies, so uploading an image, protobuf, or gzip payload through an authenticated tab was impossible — the symmetric gap to the binary *response* support the bridge already had. A new `--body-file` flag (mutually exclusive with `--body`) reads a file as raw bytes and base64-encodes them on the wire; the in-page snippet decodes back to a byte array before `fetch()`, so bytes reach the upstream server byte-exact. An optional `encoding` field on the `ControlRequest`/`Command` wire types carries the tag under `#[serde(default, skip_serializing_if = …)]`, so a client that omits it stays byte-identical on the wire (absent = UTF-8 text) and the trust boundary is unchanged; the transparent proxy never tags an encoding, so its behaviour is untouched. See [docs/browser-bridge.md](docs/browser-bridge.md).
- **Per-worktree git enrichment in the worktrees service** ([#1186](https://github.com/rust-works/omni-dev/issues/1186)): `worktrees list`, `daemon status`, and the tray "Worktrees" submenu now show each window's **live** git state — current branch and ahead/behind counts (`main (+2 -1)`) — instead of the companion-reported title, keeping the companion extension thin. The daemon's service adapter computes a `GitStatus` (via `git2`) on every read from each window's primary folder, off the registry lock (on a blocking thread for the control socket, inline for tray rendering), so values are always current and add no error path: a folder that is not a repo, is on a detached HEAD, or has no upstream still appears with only the fields it can supply. Every enrichment field is `#[serde(skip_serializing_if)]`, so the wire protocol stays backward-compatible (older clients ignore the new optional keys). See [ADR-0040](docs/adrs/adr-0040.md) and [docs/worktrees-service.md](docs/worktrees-service.md).
- **Provider-less `omni-dev transcript fetch <url>` auto-detect** ([#1187](https://github.com/rust-works/omni-dev/issues/1187)): a new `transcript fetch <url>` path probes every registered transcript source in priority order and dispatches to the first that recognises the locator, so callers no longer need to know which provider subcommand to use. Detection is **pure string inspection** — an unrecognised locator fails fast with no network call. It mirrors the per-source fetch subcommands' arguments (`--lang`, `--format`, `--auto`, `--translate`, plus an output destination); a new `transcript::detect` module iterates each source's static `matches` method and constructs the first match as `Box<dyn TranscriptSource>`, and registration order sets priority. See [docs/transcript.md](docs/transcript.md).

### Changed
- **Unified machine-readable output selection on `-o`/`--output`; reserved `--out-file` for file destinations** ([#1125](https://github.com/rust-works/omni-dev/issues/1125)): "how do I get JSON?" had four competing answers, and the `-o`/`--output` short flag meant *output format* on list commands but *output file* on `jira read` / `confluence read` / `confluence attachment download` — so `omni-dev atlassian jira read PROJ-1 -o json` silently wrote the issue to a file literally named `json`. Every format selector is now `-o`/`--output <enum>` with a per-command value set: the `--format` flag on `snowflake query`, `coverage diff`, `git commit message check` (also upgraded from a loosely-parsed string that silently fell back to `text` on an invalid value to a validated `ValueEnum` that errors), `log`, and `transcript youtube fetch`; the short-less `--output` on `transcript youtube info` / `list-langs` (now also accepts `-o`); and the boolean `--json` on `daemon status`, `snowflake sessions`, and `worktrees list` (now `-o`/`--output table|json`). File destinations move to a dedicated `--out-file <PATH>` with **no `-o` short**, so `-o` never means "file" again (`jira read PROJ-1 -o json` now errors instead of writing a file). Every old spelling keeps working as a hidden alias that prints `warning: --X is deprecated; use --Y instead` to stderr and will be removed in a future major. **One breaking edge:** on `transcript youtube fetch` the `--output` name is repurposed from the output *file* to the output *format*, so `transcript youtube fetch <id> --output out.srt` now errors (invalid format value) — use `--out-file out.srt`. `confluence compare run` keeps its deliberate `yaml` default; `transcript youtube sync` keeps `--format` (it already has an `--out` directory flag that would collide with `-o`). Unchanged: content-representation `--format jfm|adf` on jira/confluence read/create/comment, `confluence compare section --format`, `--output-dir`, and `ai claude history sync --output-format`. See [docs/user-guide.md](docs/user-guide.md).
- **`--ai-backend` (any value, including `default`) decisively overrides the legacy `USE_*` env soup; unknown `OMNI_DEV_AI_BACKEND` values are now a hard error** ([#1118](https://github.com/rust-works/omni-dev/issues/1118)): `--ai-backend default` previously only *removed* `OMNI_DEV_AI_BACKEND`, so with `USE_OLLAMA=true` exported there was no flag that could force the direct Anthropic API. The flag now always sets `OMNI_DEV_AI_BACKEND` to the chosen value, and when that variable is set it decides the backend outright — the legacy `USE_OLLAMA` → `USE_OPENAI` → `CLAUDE_CODE_USE_BEDROCK` order applies only when it is unset. A set-but-unrecognized value (e.g. a typo like `claudecli`) previously fell through *silently* to the `USE_*`/default chain and dispatched a different backend with different credentials; it now fails fast with an error listing the valid values.
- **claude-cli sandbox-weakened warnings name where the escape hatch was enabled** ([#1143](https://github.com/rust-works/omni-dev/issues/1143)): the per-invocation `WARN` for the tool-access and MCP-access escape hatches previously said only *that* the sandbox was weakened — an intentional one-off `--claude-cli-allow-tools` looked identical to a forgotten `export OMNI_DEV_CLAUDE_CLI_ALLOW_TOOLS=true` in `.zshrc` or a sticky `settings.json` `env` entry. Each warning now ends with an `Enabled via …` clause naming the source: command-line flag, process environment variable (e.g. a shell export), or the base/profile `env` map in `$HOME/.omni-dev/settings.json` — so the sticky cases point at the exact setting to remove. Built on a new sourced resolution seam in [`src/utils/settings.rs`](src/utils/settings.rs) (`EnvValueSource`, `Settings::resolve_with_source`, `get_env_var_sourced`) plus a flag-provenance registry fed by `Cli::propagate_global_flags`, available for any future warning that should name its source. Warning text and docs only — hatch semantics, precedence, and defaults are unchanged. See [docs/ai-backends.md](docs/ai-backends.md#escape-hatch-tool-access) and [ADR-0028](docs/adrs/adr-0028.md).

- **MCP description quality now has an automated backstop; two sibling-tool gaps closed** ([#1072](https://github.com/rust-works/omni-dev/issues/1072)): the v0.31.0 audit brought every MCP tool/parameter description up to the [STYLE-0029](docs/STYLE_GUIDE.md) bar, but nothing prevented the next new tool from silently regressing it — the only test spot-checked three tools. A new `all_tools_advertise_descriptions_and_param_schemas` test in [tests/mcp_integration_test.rs](tests/mcp_integration_test.rs) now asserts that *every* advertised tool **and** every top-level parameter ships a non-empty description (the checklist's "present" floor; prose quality still needs review). It also closes the two remaining "use X vs Y" gaps the audit missed: `jira_board_issues`↔`jira_sprint_issues` and `confluence_read`↔`confluence_download` now cross-reference each other so an agent picks the right one. MCP tool-description text only — no CLI `--help` or `docs/mcp.md` changes.
- **Bump `quick-xml` from 0.40.1 to 0.41.0** ([#1095](https://github.com/rust-works/omni-dev/issues/1095)): routine update of the XML parser used on the Atlassian/ADF paths. Manifest + lockfile bump; no source or behaviour changes.

### Refactored
- **Shared CLI output renderer extracted to `src/cli/format.rs`** ([#1125](https://github.com/rust-works/omni-dev/issues/1125)): the `OutputFormat` enum plus the `write_output`/`output_as` machinery and the `JsonlSerialize` trait — previously duplicated verbatim between [`src/cli/atlassian/format.rs`](src/cli/atlassian/format.rs) and [`src/cli/datadog/format.rs`](src/cli/datadog/format.rs) (the latter carried a standing TODO to extract it) — now live in one shared `crate::cli::format` module. Atlassian keeps only its collection-wrapper `JsonlSerialize` impls; Datadog re-exports the shared module. A new shared `TableOrJson` enum backs the three commands migrated off boolean `--json`. No behaviour change.
- **settings.json env-map writes share one home** ([#1153](https://github.com/rust-works/omni-dev/issues/1153)): `atlassian::auth::save_credentials` inlined the whole "read settings.json → ensure `env` object → merge keys → write pretty" sequence that `datadog::auth` had already factored into private helpers. Those helpers are promoted to [`src/utils/settings.rs`](src/utils/settings.rs) as `Settings::upsert_env_vars` / `Settings::remove_env_vars` (operating on a generic JSON value, so unknown fields and profiles are preserved verbatim), and both auth modules are rewritten over them — giving the write side the same single home the read side already has in `Settings::get_env_var`. The Atlassian module also gains the Datadog-style `save_credentials_to` path-injection seam, so its save test no longer redirects `HOME` (STYLE-0028 / issue #1030). No behaviour change beyond the permission hardening below.
- **Shared HTTP response-handling helpers for the Atlassian and Datadog clients** ([#1151](https://github.com/rust-works/omni-dev/issues/1151)): 50+ inline "check status → extract error body → deserialize JSON" blocks copied across the Atlassian client and the Datadog `*_api.rs` modules are replaced by a small set of helpers — Atlassian's `ensure_success` / `parse_json` and Datadog's `api_url` / `parse_response` / `get_parsed` — so response validation and error diagnostics are consistent and future changes land in one place. No behaviour change.
- **HTTP 429 retry consolidated into one shared driver** ([#1152](https://github.com/rust-works/omni-dev/issues/1152)): six open-coded rate-limit retry loops (GET/PUT/POST/DELETE in Atlassian, GET/POST in Datadog, plus the Claude AI path) are replaced by a single `retry_429` driver in a new `utils::http`, centralising retry scheduling, backoff, and rate-limit header handling (`Retry-After` and `X-RateLimit-Reset`) — which also brings `X-RateLimit-Reset` awareness to the Atlassian client for the first time. A companion `request_log::record_http_result` helper collapses the per-call `match Ok/Err` around `record_http`, and the loop's previously-unreachable `unreachable!()` tail is rewritten as a counted `loop` so every line is covered. Callers are unchanged; the refactor is internal.
- **Worktrees engine split from its daemon adapter** ([#1154](https://github.com/rust-works/omni-dev/issues/1154)): the worktrees registry logic is extracted into a standalone `crate::worktrees` module (`WorktreesRegistry`, `RegisterRequest`, `WindowEntry`), mirroring the engine/adapter split the browser bridge and Snowflake services already use. The registry now owns the `Mutex<HashMap>`, TTL reaping, entry cap/eviction, and the core `register`/`heartbeat`/`unregister`/`list`/`first_folder` ops, while `WorktreesService` shrinks to adapter duties — routing control-socket ops, rendering the tray menu/status, and driving the VS Code launcher. No behaviour change.
- **Last `mod.rs` module roots migrated to named files (STYLE-0004)** ([#1155](https://github.com/rust-works/omni-dev/issues/1155)): the remaining 19 `mod.rs` roots across the daemon, snowflake, browser, atlassian, cli, and coverage subsystems are renamed to the `foo.rs`-beside-`foo/` layout — **no `mod.rs` file remains under `src/`**. Pure file renames (none of the modules use path-relative macros like `#[path]` / `include!`, so Rust resolves them identically), with a stale `mod.rs` pointer in `CLAUDE.md` refreshed.
- **JIRA and Confluence wire types split into dedicated DTO modules** ([#1156](https://github.com/rust-works/omni-dev/issues/1156)): ~2700 lines of REST wire types are lifted out of the monolithic `client.rs` / `confluence_api.rs` into new `src/atlassian/jira_types.rs` (issues, users, comments, search, edit/create-meta, Agile board/sprint/dev-status) and `src/atlassian/confluence_types.rs` (comments, attachments, spaces, users, labels, versions, search), leaving those modules to focus on HTTP transport. All types stay exported through their original modules via re-exports, so no downstream import breaks. No behaviour change.

### Fixed
- **Request log: transcript HTTP now recorded, AI backends split by service, and exit-code/duration/`--until` filters added** ([#1122](https://github.com/rust-works/omni-dev/issues/1122)): four coverage/filter gaps from the [#1103](https://github.com/rust-works/omni-dev/issues/1103) analysis are closed. **(1)** The transcript/YouTube transport made live `reqwest` calls that no `record_http` hook ever saw — every InnerTube `/player`·`/browse` POST, the timedtext GET, the watch-page bootstrap, and the channel resolve/RSS GETs now record a `service: "transcript"` HTTP line (via a shared `record_yt_http` helper mirroring the datadog hook), so `omni-dev log --service transcript` finally shows them. **(2)** All AI backends previously logged as `service: "claude"`, making Bedrock/OpenAI/Ollama traffic indistinguishable and leaving the OpenAI/Ollama context probes unrecorded entirely; `record_ai_http` now takes a `service` + `method`, so the direct Anthropic API tags `anthropic`, and `bedrock`/`openai`/`ollama` (chosen per `is_ollama`) each tag distinctly — the LM Studio (`GET /api/v0/models`) and Ollama (`POST /api/show`) probes included. **(3)** `exit_code`, `duration_ms`, `elapsed_ms`, `hostname`, `system_user`, `cwd`, and `auth_principal` were on every record but reachable only through `--grep`; they're now first-class `--query` fields (with aliases `exit`/`duration`·`dur`/`elapsed`/`host`/`user`/`principal`), and the numeric ones — plus `status` — accept comparison operators (`elapsed:>1000`, `exit_code:>0`, `status:>=400`), so "slow requests" and "failed runs" are expressible without piping to `jq`. **(4)** `--since` was relative-only; it and the new `--until` flag now each accept a relative window, a `YYYY-MM-DD` date, or an RFC3339 timestamp, giving a bounded historical window (`--since 2026-07-01 --until 2026-07-02`). A docs caveat also notes that GitHub traffic issued via the `gh` subprocess runs out of process and is not captured. Cross-socket daemon correlation (a `via_daemon` request carrying the daemon's `invocation_id` instead of the originating CLI's) is split out as a tracked follow-up ([#1198](https://github.com/rust-works/omni-dev/issues/1198)). See [docs/log.md](docs/log.md).
- **`CLAUDE_MODEL` / `CLAUDE_CODE_MODEL` are honoured by every Claude-family backend, via one shared resolver** ([#1118](https://github.com/rust-works/omni-dev/issues/1118)): [docs/ai-backends.md](docs/ai-backends.md) promised every backend resolved `--model` → `CLAUDE_MODEL` → `CLAUDE_CODE_MODEL` → `ANTHROPIC_MODEL` → provider var → default, but only the `claude-cli` backend implemented the chain — the direct Claude API and Bedrock read only `ANTHROPIC_MODEL`, so `export CLAUDE_MODEL=claude-opus-4-6` (a spelling the docs blessed) silently fell through to the default model. Backend, model, and beta-header selection now live in one shared resolver ([`src/claude/backend.rs`](src/claude/backend.rs)) used by both the client factory and preflight, ending the byte-for-byte duplicated switch ADR-0028 required to be updated in lock-step. The chain is deliberately **scoped per family** (and the docs updated to match): Claude-family backends (direct API, Bedrock, claude-cli) read `CLAUDE_MODEL` → `CLAUDE_CODE_MODEL` → `ANTHROPIC_MODEL`; OpenAI reads `OPENAI_MODEL` and Ollama `OLLAMA_MODEL` only — so a Claude model id can never leak into a non-Claude backend. Resolution now also skips *empty* values ("first non-empty value", as documented), and the OpenAI branch's dead `gpt-5` registry-miss fallback literal is corrected to the registry default `gpt-5-mini`.
- **`--profile` now applies to `snowflake query` defaults** ([#1110](https://github.com/rust-works/omni-dev/issues/1110)): `omni-dev --profile work snowflake query …` silently ignored the profile — the `SNOWFLAKE_*` defaults were resolved **once at daemon startup** from the daemon's own environment, while the thin CLI client forwarded only its raw flags (usually absent) over the control socket, so `--profile` set `OMNI_DEV_PROFILE` in a process the resolution never ran in. Following the documented multi-account advice therefore queried the startup profile's tenant with no error. The CLI now resolves the six identity/context defaults (`SNOWFLAKE_ACCOUNT`/`USER`/`WAREHOUSE`/`ROLE`/`DATABASE`/`SCHEMA`) **client-side** via the same profile-aware env-then-`settings.json` chain the Atlassian/Datadog commands use (`QueryRequest::fill_defaults_from`), and sends the resolved values with the query request; precedence is explicit flag > client env > active profile (or base) `settings.json` env > daemon startup defaults. The wire protocol is unchanged (existing fields, now populated; unset fields are omitted from the payload), so bare socket clients that resolve nothing keep the daemon-startup fallback. Pool size and timeouts remain daemon-startup (config-only) settings. [docs/snowflake-service.md](docs/snowflake-service.md#configuration) now states where each setting resolves. Also reported as #1138 in the #1101 security review.
- **Default base-branch resolution is unified across `check` / `branch info` and prefers `origin/main` over a stale local `main`** ([#1106](https://github.com/rust-works/omni-dev/issues/1106)): when no commit range was given, `git commit message check` and `git branch info` each resolved the default base independently (local `main` → local `master`), and diverged when neither existed — `check` silently guessed `HEAD~5..HEAD` (an arbitrary window that could include mainline commits the user didn't write, and which hard-failed anyway in repos with fewer than 6 commits) while `branch info` errored. Worse, both bound the range to the **local** branch, so a stale local `main` made the default range include commits already merged into `origin/main`. Both commands now share one resolver ([`GitRepository::resolve_default_base_branch`](src/git/repository.rs)) that prefers remote-tracking refs: `origin/main` → `origin/master` → local `main` → local `master` — matching the remote-first behaviour `create pr` and `coverage diff` already had, and the `origin/main..HEAD` form the README examples always spelled explicitly. When none of the four refs exist, both commands now fail with the same actionable error (`No default base branch found (checked origin/main, origin/master, main, master)`) instead of guessing — a small behaviour break for `check` in mainline-less repos, which previously fell back to `HEAD~5`. Explicit ranges and `branch info <BASE_BRANCH>` are unchanged. See [docs/troubleshooting.md](docs/troubleshooting.md#error-remote-branch-not-found).
- **Snowflake keep-alive heartbeat actually runs — idle pools no longer re-prompt browser SSO** ([#1107](https://github.com/rust-works/omni-dev/issues/1107)): `SnowflakeSession::heartbeat()` existed but was never called in production, so the `CLIENT_SESSION_KEEP_ALIVE = true` set at login was inert — the server only extends the master token (~4h TTL) when periodic `session/heartbeat` calls arrive. A pool idle past that TTL lost its session and the next query popped a fresh SSO browser window, undercutting the service's "authenticate once, query many times" promise. The engine now owns a background tokio task (`SnowflakeEngine::start_heartbeat`, spawned by the daemon's `SnowflakeService`, cancelled and awaited on shutdown — the `BridgeServer` CancellationToken pattern) that every `SNOWFLAKE_HEARTBEAT_INTERVAL` (default 900s, ¼ of the 3600s session-token validity; `0` disables) visits each pool's **idle** sessions: each is borrowed through the existing permit + checkout machinery (so a query arriving mid-heartbeat briefly waits instead of authenticating a duplicate session), proactively renewed if its session token would lapse before the next tick, heartbeated, and restored without touching `last_used`/context bookkeeping (a keep-alive is not a query). Busy sessions are skipped — the query path already renews inline. Only a session whose master token has expired anyway is discarded (the next query lazily re-authenticates); transient errors keep the session for the next tick. See [docs/snowflake-service.md](docs/snowflake-service.md#reliability).
- **Non-macOS `daemon start` now actually detaches the daemon** ([#1112](https://github.com/rust-works/omni-dev/issues/1112)): the non-macOS launcher was a bare `Command::spawn()` — the daemon stayed in the launching shell's session and process group and inherited its terminal, so closing that terminal could `SIGHUP`-kill it (and its tracing output interleaved with the user's shell), while docs and help text described the spawn as "detached". The launcher now detaches for real: a `pre_exec` hook calls `setsid(2)` so the daemon gets its own session (surviving terminal exit and its `SIGHUP`), stdin comes from `/dev/null`, and stdout/stderr are appended to a `0600` `daemon.log` beside the control socket (`paths::log_path_for_socket`) so panics and tracing output remain inspectable. Defense-in-depth, the daemon also treats `SIGHUP` as a graceful shutdown signal alongside `SIGTERM`/`SIGINT` — a manual `kill -HUP` now drains services and unlinks the socket instead of hard-killing. Auto-start at login remains macOS-only (the launchd LaunchAgent); a systemd user unit is tracked in [#1174](https://github.com/rust-works/omni-dev/issues/1174). See [ADR-0039](docs/adrs/adr-0039.md).
- **`auth login` / `logout` now write to the active profile — profiles are populatable from the CLI** ([#1116](https://github.com/rust-works/omni-dev/issues/1116)): `atlassian auth login` and `datadog auth login`/`logout` always wrote credentials into the **base** `env` map of `~/.omni-dev/settings.json`, but profile resolution deliberately never falls back to base `env` when a profile is active — so `omni-dev --profile work atlassian auth login` stored credentials somewhere `--profile work` would never read, and hand-editing `settings.json` was the only way to populate a profile (undermining the multi-tenant feature from [#1089](https://github.com/rust-works/omni-dev/issues/1089)). The settings write side now mirrors the read side's profile targeting: new `Settings::upsert_env_vars_in` / `Settings::remove_env_vars_in` take an explicit `Option<&str>` profile and write to `profiles.<name>.env` when one is given (the existing base-`env` helpers delegate with `None`, so external callers are unaffected), and all three auth flows resolve the active profile (`--profile` / `OMNI_DEV_PROFILE`) at the production boundary and thread it down as a plain parameter (issue #1030 style). A profile-targeted logout removes only that profile's keys, leaving the base map and other profiles intact, and the login/logout confirmation messages name the profile they wrote to (`… settings.json (profile 'work')`). See the new [Populating a profile](docs/configuration-best-practices.md#populating-a-profile) section.
- **`confluence attachment delete` gains `--dry-run` and joins the shared destructive-command guard** ([#1126](https://github.com/rust-works/omni-dev/issues/1126)): the attachment-delete CLI command carried `--force` + `--purge` but not `--dry-run`, while every sibling removal (`confluence delete`, `confluence label remove`, `jira link remove`, `jira watcher remove`) offered all of them — so the ADR-0027 trick of sanity-checking a scripted `--force` invocation by appending `--dry-run` silently didn't exist for attachments. The command now accepts `--dry-run` (prints `Would delete attachment <ID>.` — or `Would permanently purge attachment <ID>.` with `--purge` — and exits without any API call; takes precedence over `--force`) and is migrated off its private `confirm_with_reader` copy onto the shared `guard_destructive_with_io` helper in [`src/cli/atlassian/confirm.rs`](src/cli/atlassian/confirm.rs), the third duplicated prompt copy ADR-0027 set out to eliminate. Like label removal, the preview echoes the ID without a lookup, keeping the no-API-call contract strict. Prompt wording, `--force`, and `--purge` behaviour are unchanged; the `confluence_attachment_delete` MCP tool intentionally stays confirm-free/`--force`-shaped per ADR-0027's MCP convention. The broader #1126 question — a uniform `--dry-run`/`--force` rule across *all* mutating CLI commands — remains open. See [ADR-0027](docs/adrs/adr-0027.md) and the destructive-commands section of [docs/user-guide.md](docs/user-guide.md).
- **Custom-field resolver: labels, issue-link (`Parent`), raw-ADF passthrough, and system-field ids** ([#1157](https://github.com/rust-works/omni-dev/issues/1157)): four gaps in the shared field resolver behind `jira create --set-field` / `jira write --set-field` / MCP `jira_create.custom_fields` (and the new `jira_edit`) are closed. **Labels**: array fields were always coerced to option objects (`[{"value": …}]`), which the API rejects for `labels` and labels-type custom fields — the array arm now dispatches on the schema's `items` type (`string` → plain string array, `component`/`version` → `[{"name": …}]`, options unchanged), so `Labels=[a, b]` finally works. **Issue-link fields**: schema type `issuelink` (e.g. `Parent`) was rejected outright ("Unsupported field type 'issuelink'"), forcing a create-then-`jira_link_parent` two-step when bulk-creating under one parent — an issue key string now coerces to `{"key": …}` at create and edit time. **Raw ADF**: rich-text fields only accepted JFM strings; a mapping with `type: doc` is now validated against the ADF content model and forwarded verbatim, the escape hatch for structured content JFM can't express. **System ids**: field lookup only honoured ids matching `customfield_<digits>`, so canonical system ids like `labels`/`parent`/`priority` never resolved — the id lookup is now unconditional (an exact id match shadows an identically spelled display name). Under the hood, `EditMetaSchema` now captures the `items` and `system` properties from editmeta/createmeta, and `is_adf_rich_text` also recognises the system `description`/`environment` fields, so a string sent to those via any `fields` map converts JFM→ADF instead of drawing a 400.
- **Credential structs no longer derive a plaintext `Debug` (latent secret leak)** ([#1131](https://github.com/rust-works/omni-dev/issues/1131)): `AtlassianCredentials.api_token`, `DatadogCredentials.api_key`/`app_key` (and `DatadogClient`'s stored copies), and the Snowflake session/master tokens all derived a `Debug` that printed the secret verbatim, so any future `tracing::debug!("{x:?}")` or `.context(format!("… {x:?}"))` would have leaked it into logs or error chains. Those fields are now wrapped in a new redacting newtype, [`utils::secret::Secret`](src/utils/secret.rs), whose hand-written `Debug` prints `<redacted>` and which implements neither `Display` nor serde — the value can only leave the wrapper through an explicit, greppable `expose_secret()` call at the HTTP-header/settings-write boundaries. The Debug-formatting tests, which previously only asserted the type *name* appeared (and so would have passed even while leaking), now assert the secret value is absent and `<redacted>` is present. Found in the #1101 security review. (Field type change on the two `pub` credential structs is technically API-breaking for library consumers.)
- **Daemon runtime files are created with tight permissions atomically — no write-then-chmod window** ([#1132](https://github.com/rust-works/omni-dev/issues/1132), [#1139](https://github.com/rust-works/omni-dev/issues/1139)): two low-severity findings from the [#1101](https://github.com/rust-works/omni-dev/issues/1101) security review shared one root pattern — creating a path under the ambient umask and only *then* tightening it, leaving a brief window where the inode was group/world-readable. The bridge token file was written with `std::fs::write` then `chmod`'d `0600` (#1132), and `ensure_dir_0700` did `create_dir_all` then `chmod 0700` (#1139). Both now pass the mode to the creating syscall itself: directories are born `0700` via `DirBuilder::mode`, and a new `paths::write_file_0600` opens the token file `O_CREAT` with mode `0600` (matching the `0600`-from-birth posture the daemon socket has had since [#995](https://github.com/rust-works/omni-dev/issues/995)). Additionally, modes set only at creation are now re-applied to pre-existing looser-perm files: `write_token` re-tightens a stale loose `bridge.token` *before* the new secret lands in it, and the request log re-tightens a loose `log.jsonl` (an older version's, or a user-set `OMNI_DEV_LOG_FILE` target) on every open — both via handle-based `fchmod` (`paths::ensure_handle_0600`), so there is no path race and no umask interference. Defense-in-depth only: the parent directories were already `0700`, so no privilege boundary was crossable in practice.
- **JFM→ADF validation failures now point at the source line, column, and offending text** ([#1087](https://github.com/rust-works/omni-dev/issues/1087)): when a JFM body converts to ADF that violates the content model — most commonly bold-wrapping-inline-code (`` **`/api/v1/example`** ``, which maps to a text node carrying both `strong` and `code`, a combination ADF forbids) — the write was aborted with only an opaque ADF index path (e.g. `at /38/4/0/1`), leaving the author to count siblings under an unrendered tree to find the culprit in a large document. The [`AdfValidationError`](src/atlassian/adf_validated.rs) is now enriched: it resolves the index path against the produced ADF tree to name the **offending run's text** (`in text: "/api/v1/example"`) and, when the JFM source is available, reports the **1-based `line:column`** of that run in the original markdown (columns counted in characters, so multi-byte text lines up with an editor cursor). Every validation error gains the text excerpt via [`ValidatedAdfDocument::try_new`](src/atlassian/adf_validated.rs); the JFM-sourced paths additionally gain `line:column` through a new shared [`markdown_to_validated_adf`](src/atlassian/adf_validated.rs) helper (and `try_new_with_source` / `validate_with_source` variants) now used by the MCP `jira`/`confluence` create/write/comment tools, the matching CLI commands and their `--dry-run` / `convert to-adf` preflights, and custom-field rich-text sections. This is the diagnostics improvement from #1087 option 2; silently splitting the `strong`+`code` marks (option 1) remains a separate follow-up.

- **`claude-cli` budget cap no longer fails open silently on an invalid value** ([#1135](https://github.com/rust-works/omni-dev/issues/1135)): the `--claude-cli-max-budget-usd` / `OMNI_DEV_CLAUDE_CLI_MAX_BUDGET_USD` spending cap dropped non-positive, non-finite, or unparseable values without a trace — a typo like `--claude-cli-max-budget-usd 0` ran the invocation with **no cap at all** and no signal that the guardrail was gone. A set-but-unusable value now logs a `WARN` naming the offending value and the env var before running uncapped (an unset cap stays silent, as before). The behaviour is still fail-open by design — see [ADR-0028](docs/adrs/adr-0028.md). [docs/ai-backends.md](docs/ai-backends.md#spending-cap) now also documents the cap's two inherent limitations from the #1101 security review: enforcement is delegated to `claude -p` (omni-dev has no independent kill switch), and because a turn's cost is only known after it completes, a single invocation can overshoot the cap by up to one model turn.
- **launchd socket activation no longer leaks extra file descriptors** ([#1145](https://github.com/rust-works/omni-dev/issues/1145)): when launchd hands over the activated socket, the kernel can supply more descriptors than the single one declared in the plist; the daemon adopted the first as its Tokio listener and silently left any extras open for the process lifetime (an fd leak). `daemon run` now detects `cnt > 1`, logs a warning, and closes each extra descriptor. macOS-only, on the launchd socket-activation path.
- **`estimate_lines_changed` no longer subtracts on a malformed diff-stat token** ([#1151](https://github.com/rust-works/omni-dev/issues/1151)): the diff-stat estimator parsed the count after `|` as `i32` and summed it, so a malformed line with a negative token (e.g. `|-1`, reachable because a vertical tab counts as `split_whitespace` whitespace) drove the running total negative — the case a property test shrank to. `git diff --stat` counts are always non-negative, so the token is now parsed as `u32`: a stray negative or non-numeric token fails to parse and is skipped instead of subtracting. A pre-existing latent bug surfaced during the #1151 refactor; two deterministic tests pin the previously proptest-only fall-through branches.

### Documentation
- **DEBUG traces and `claude-cli` subprocess errors contain prompt bodies** ([#1142](https://github.com/rust-works/omni-dev/issues/1142)): the request log's redaction posture in [docs/log.md](docs/log.md#redaction-posture) now spells out what redaction does *not* cover — prompt **content**. AI prompts carry whatever the invocation was about (repo diffs, commit messages, JIRA/Confluence data), and that content surfaces on three paths: `OMNI_DEV_LOG_BODIES=1` records the request/response bodies (which *are* the prompts), `RUST_LOG=debug` tracing emits the full system/user prompt and full response from every AI backend to stderr, and a `claude-cli` subprocess failure embeds the child's stdout/stderr verbatim in the returned error. No API keys or tokens appear on any of these paths — a data-sensitivity note, not a credential leak. Documentation-only per the [#1101](https://github.com/rust-works/omni-dev/issues/1101) security review (findings-only disposition); no behavior changed.
- **Bridge session token on stdout documented as an accepted risk** ([#1148](https://github.com/rust-works/omni-dev/issues/1148)): a foreground `bridge serve` prints the session token to stdout by design — the operator must paste it into the DevTools snippet ([ADR-0036](docs/adrs/adr-0036.md)). The residual exposure (terminal scrollback, tmux logs, CI transcripts) is now recorded as an accepted risk in the operator guide and the ADR, with restart-to-rotate guidance. Found in the #1101 security review; no code change.
- **SO_REUSEADDR fail-closed binding prose corrected** ([#1150](https://github.com/rust-works/omni-dev/issues/1150)): [ADR-0036](docs/adrs/adr-0036.md) and [docs/browser-bridge.md](docs/browser-bridge.md) still claimed the bridge planes bind without `SO_REUSEADDR`, but `bind_loopback_reuse` has set it since [#990](https://github.com/rust-works/omni-dev/issues/990) so a restart can rebind the fixed ports through a lingering `TIME_WAIT`. Both docs now describe the actual behaviour and why fail-closed still holds: `SO_REUSEADDR` (unlike the unused `SO_REUSEPORT`) does not admit a second live listener on the same address+port, so a live squatter still yields `EADDRINUSE` and the process exits.
- **Docs drift sweep** ([#1124](https://github.com/rust-works/omni-dev/issues/1124)): a batch of confirmed documentation-drift fixes from the #1103 functionality gap analysis. The shipped `worktrees` command is now visible — a README feature bullet and `### Worktrees` section plus a docs-index entry linking the previously orphaned [docs/worktrees-service.md](docs/worktrees-service.md). Twiddle's Phase 3 (contextual intelligence) is relabeled COMPLETED in [docs/plan/twiddle.md](docs/plan/twiddle.md) and the docs index — it shipped long ago (`--use-context`, `collect_context()`). Ten twiddle flags absent from the README/user-guide option tables are documented (`--fresh`, `--refine`, `--no-ai`, `--check`, `--work-context`, `--branch-context`, `--model`, `--beta-header`, `--allow-pushed`, `--quiet`), with a new "Refine, Inspect, or Validate" user-guide example. Stale pointers corrected: ADR-0020 inventory row Proposed→Accepted; `#1003` references repointed (v2 client declined; PAT/key-pair auth tracked in [#1108](https://github.com/rust-works/omni-dev/issues/1108)); the getting-started `#765` TODO replaced with a link to the shipped [`.omni-dev/` Directory Contract](docs/omni-dev-directory.md); ADR-0028's `--beta-header` no longer called a "global flag" (it is per-subcommand); the snowflake client module doc no longer claims external-chunk results are unsupported (only Arrow remains); and [docs/snowflake-service.md](docs/snowflake-service.md) now documents the full `sessions` reply (`sessions`/`max_sessions`/`members[]` with per-member context and running query). Three honest-but-untracked follow-up mentions are now tracked and cited in place: per-origin bridge allowlist ([#1185](https://github.com/rust-works/omni-dev/issues/1185)), per-worktree git enrichment ([#1186](https://github.com/rust-works/omni-dev/issues/1186)), and generic `transcript fetch <url>` auto-detect ([#1187](https://github.com/rust-works/omni-dev/issues/1187)).

### Security
- **`claude-cli` tool-access escape hatch scrubs well-known secret env vars; untrusted-input caveat documented** ([#1144](https://github.com/rust-works/omni-dev/issues/1144)): enabling `--claude-cli-allow-tools` removes the two mitigations that normally contain the `claude-cli` backend — tools-off and prompt-is-just-data — while the nested `claude -p` session inherits the parent environment and is prompted with attacker-influenceable content (diffs, commit messages, JIRA text), so a prompt-injection payload could in principle drive Bash/Read tool calls that exfiltrate inherited credentials. As a compensating control, the tool-access hatch now widens the existing env scrub (`CLAUDE_PROJECT_DIR`, `CLAUDE_CODE_*`, `CLAUDE_PROJECT_*`) to well-known secret vars: suffixes `_API_KEY`/`_TOKEN`/`_SECRET`/`_PASSWORD`/`_CREDENTIALS` plus `AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY`. `ANTHROPIC_API_KEY`/`ANTHROPIC_AUTH_TOKEN` are always kept (the nested `claude` may authenticate through them; omni-dev passes it no credentials of its own), and a new `OMNI_DEV_CLAUDE_CLI_KEEP_ENV` (comma-separated exact names) exempts credentials a tool-enabled workflow legitimately needs, e.g. `GITHUB_TOKEN` for env-authenticated `gh`. The scrub is inactive on the default (tools-off) path, secret-var removals are logged at `DEBUG` (names only), and the per-invocation `WARN` now spells out the untrusted-input risk — which is also documented next to the escape hatch in [docs/ai-backends.md](docs/ai-backends.md#escape-hatch-tool-access) and [ADR-0028](docs/adrs/adr-0028.md). Info severity, from the #1101 security review.
- **JFM→ADF conversion now bounds nesting depth instead of overflowing the stack** ([#1130](https://github.com/rust-works/omni-dev/issues/1130)): the hand-written markdown parser behind `to-adf` recursed once per nesting level with no limit, and a Rust stack overflow is an uncatchable abort (guard page, not a panic) — so a few KB of malicious input (`> > > …` at ~2 bytes per level, or equivalently nested lists, `:::panel`/`:::expand` directives, `:::td` table cells, or layout columns) killed the whole process. Reachable from the MCP `atlassian_convert` tool with fully caller-supplied content (a prompt-injection scenario that takes down the long-lived MCP server/daemon), `omni-dev atlassian convert to-adf`, and every JFM write path. A `depth` counter is now threaded through every nested `MarkdownParser` and checked in `parse_blocks` against a 64-level cap, returning a clean "maximum depth" error through the existing fallible pipeline (the cap is below serde_json's 128 — which already bounds the opposite `from-adf` direction — so that at-cap parsing also fits debug-build stacks; real documents nest under 20 levels). The independent **inline** recursion vector (nested emphasis/strikethrough/link text/bracketed spans/inline `:span` directives, ~2–3 bytes per level) is capped at the same depth but degrades gracefully — past the cap the remaining run is emitted as literal text instead of erroring, since inline parsing is infallible by design. Found during the #1101 security review.
- **`worktrees list` strips terminal control bytes from registry strings** ([#1137](https://github.com/rust-works/omni-dev/issues/1137)): a same-user process that can write the daemon socket can `register` a worktrees entry whose `title`/`repo`/`folders` contain ANSI/terminal control sequences, and the `omni-dev worktrees list` table printed those fields verbatim — letting a registered entry move the cursor, clear the screen, retitle the terminal, or forge extra table rows on the operator's TTY. The table renderer now strips control characters (C0 including ESC/CR/LF/BEL, DEL, and C1) from `repo`, `title`, and folder strings before formatting. Display-layer only: the wire payload and `worktrees list --json` stay byte-verbatim per the companion contract (JSON encoding already escapes control bytes), and native tray menus never interpreted ANSI. Found during the [#1101](https://github.com/rust-works/omni-dev/issues/1101) security review; see [docs/worktrees-service.md](docs/worktrees-service.md#security).
- **Argv is scrubbed before it reaches the request log** ([#1129](https://github.com/rust-works/omni-dev/issues/1129)): the invocation record wrote the full process argv to `log.jsonl` verbatim — unredacted and not gated by `OMNI_DEV_LOG_BODIES`/`OMNI_DEV_LOG_HEADERS` — so a secret passed on the command line (e.g. `browser bridge request --header "Authorization: Bearer <secret>"` or `--body "<secret>"`) persisted in plaintext, in telling contrast to the `OMNI_DEV_*` env snapshot on the same record, which was already redacted. `command_line` is now scrubbed centrally in `record_invocation` before writing, in both `--flag value` and `--flag=value` forms: `--header` values naming a sensitive header (the same list `redact_headers` uses) keep the name but lose the value (`Authorization: REDACTED`), inline `--body` values are redacted (`@file` references are kept — a path, not a secret), and the value of any flag whose name contains a `token`/`secret`/`password`/`passwd`/`key` segment is redacted as future-proofing (flags ending in `-file`/`-path` carry paths and are exempt, e.g. `--token-file`). Write-side scrubbing means the on-disk line is clean, so `--grep`/`--query` matching and every output format are covered with no reader changes. Found during the #1101 security review; see the redaction posture in [docs/log.md](docs/log.md#redaction-posture).
- **Attachment downloads sanitize server-controlled filenames (path traversal / arbitrary file write)** ([#1127](https://github.com/rust-works/omni-dev/issues/1127)): the single-attachment download paths joined the remote-supplied filename/title onto the output directory verbatim, and `Path::join` both replaces the base on an absolute component and leaves `..` segments intact — so an attachment named `/Users/<victim>/.zshrc` or `../../../../.ssh/authorized_keys` on a page/issue the victim downloads would write attacker-controlled bytes to an attacker-chosen path, even in the default temp-dir configuration. Four surfaces were affected: the MCP tools `confluence_attachment_download`, `jira_attachment_download`, and `jira_attachment_images`, plus the CLI commands `jira attachment download|images` and `confluence attachment download` (the bulk `confluence download` command was already safe). All now route the remote name through the same sanitizer that command used — [`utils::path::attachment_filename`](src/utils/path.rs) reduces it to its final path component and falls back to `attachment-<id>` when no usable name remains — so the file always lands inside the intended directory. Explicitly user-supplied output *file* paths are untouched; YAML/console metadata still reports the original remote title, while `path` reports the sanitized on-disk location. Found during the #1101 security review.
- **Request log no longer records secret-bearing URL query/fragment values** ([#1133](https://github.com/rust-works/omni-dev/issues/1133)): the request log wrote the request URL verbatim, so a browser-bridge request to an operator-supplied target carrying credentials in the URL — a presigned S3/GCS URL, `?access_token=…`, an Azure SAS `?sig=…`, or an OAuth implicit-flow `#access_token=…` fragment — landed in `log.jsonl` unredacted, violating the log's "no secret material is ever written" contract (first-party clients were unaffected: Datadog/Atlassian/Snowflake/Claude authenticate via headers, which were already redacted). [`record_http_with`](src/request_log.rs) now centrally redacts, always-on, covering every current and future caller: query and fragment parameter values whose keys look secret-bearing (suffixes `token`/`secret`/`password`/`passwd`/`signature`/`api_key`; exact keys `sig`/`sas`/`jwt`/`auth`; prefixes `X-Amz-*`/`X-Goog-*`) are replaced with `REDACTED`, matching segments only — host, path, parameter keys, and all benign segments are preserved byte-verbatim, so `omni-dev log --url <SUBSTR>` filtering keeps working and clean URLs round-trip unchanged. Found in the [#1101](https://github.com/rust-works/omni-dev/issues/1101) security review; see the updated redaction posture in [docs/log.md](docs/log.md#redaction-posture).
- **Invocation-record argv also redacts secret-bearing URL query/fragment values** ([#1162](https://github.com/rust-works/omni-dev/issues/1162)): closing the #1133 URL leak on `kind:"http"` records surfaced the same secret reaching `log.jsonl` through a second field — the `kind:"invocation"` record's full argv. The #1129 argv scrub redacts secret-bearing *flag names* and `--header`/`--body` values, but a URL passed as `--url /api/export?access_token=…&sig=…` slipped through: `--url` is not a secret-bearing flag name, and its value is a whole URL, not a bare secret. `scrub_argv` now runs every resulting element through the same `redact_url` machinery `record_http_with` uses (a second layer after the flag-value scrub), so a credential carried in a URL argument's query or fragment — most naturally the browser bridge's operator-supplied target, or a presigned URL — is redacted (`access_token=REDACTED&sig=REDACTED`) while benign argv (paths, ports, non-secret params) passes through byte-identical, keeping `--grep`/`--query` matching intact. Found during end-to-end verification of the #1133 fix; see the redaction posture in [docs/log.md](docs/log.md#redaction-posture).
- **Worktrees registry capped at 256 entries** ([#1140](https://github.com/rust-works/omni-dev/issues/1140)): the daemon's worktrees service inserted `register` requests into an unbounded in-memory map, so a misbehaving same-user process flooding `register` with distinct keys could grow daemon memory until the 30 s TTL reaped the entries (TTL × request rate). The registry now holds at most 256 windows: where the TTL bounds how *stale* an entry can get, the cap bounds how *many* can exist. A `register` of a new key at the cap evicts the longest-silent entry (oldest `last_seen`, ties broken by key) instead of rejecting — `register` stays infallible so the companion-extension contract gains no error path, and an evicted live window self-heals through the existing `heartbeat → {known: false} → re-register` resync; upserts of an existing key never evict. Robustness hardening within the same-user trust domain (Info severity, from the #1101 security review) — no trust boundary changes. See [ADR-0040](docs/adrs/adr-0040.md) and [docs/worktrees-service.md](docs/worktrees-service.md).
- **settings.json credential store is no longer world-readable** ([#1128](https://github.com/rust-works/omni-dev/issues/1128), from the #1101 security review): `auth login` wrote the plaintext `ATLASSIAN_API_TOKEN` / Datadog keys (and, since the profile system shares the file, every profile's secret `env` bundle) to `~/.omni-dev/settings.json` with umask permissions — `0644` inside a `0755` directory on a typical host, readable by any local user on a shared machine. Every settings write now goes through the single hardened write site in [`src/utils/settings.rs`](src/utils/settings.rs): the parent directory is created/tightened to `0700`, the file is created through a `0600` handle (no window where a fresh file is world-readable), and a pre-existing looser-permission file is re-tightened to `0600` on every write — the same owner-only posture as the daemon's runtime state (`src/daemon/paths.rs`). Run any `omni-dev atlassian auth login` / `omni-dev datadog auth login` once to tighten an existing file, or `chmod 600 ~/.omni-dev/settings.json && chmod 700 ~/.omni-dev` by hand.
- **Request log redacts secret-bearing headers by name substring, not just an exact list** ([#1134](https://github.com/rust-works/omni-dev/issues/1134)): header redaction was an exact-name denylist, so an off-list auth header (`x-auth-token`, `x-amz-security-token`, `x-goog-api-key`, a custom `x-<vendor>-token`) would be written in the clear once a caller starts populating `HttpExtra` headers. A `SENSITIVE_HEADER_MARKERS` substring denylist — mirroring the `SECRETISH` env-var convention — is added alongside the exact list, so any header whose lowercased name is on the list or contains a marker is redacted (false positives redact harmlessly). Found in the #1101 security review; see [docs/log.md](docs/log.md#redaction-posture).
- **Facebook harvester no longer panics on a multi-byte char boundary** ([#1136](https://github.com/rust-works/omni-dev/issues/1136)): the fixed-size search windows in `parse_session_from_me` (anchor + 4000) and `refetch_doc_in_bundle` (at + 2000) sliced at an arbitrary byte offset that could land inside a multi-byte UTF-8 sequence, panicking with "byte index is not a char boundary" instead of returning the staged error — reachable because timeline HTML embeds other users' Unicode (emoji, non-ASCII names), i.e. semi-trusted input. A `search_window` helper now walks the end back to the nearest char boundary (shrinking the window by at most 3 bytes), used at both sites; regression tests straddle a 4-byte scalar across each window edge. Found during the #1101 security review.

## [0.31.0] - 2026-06-30

### Added
- **Worktrees daemon service + `omni-dev worktrees list`** ([#1011](https://github.com/rust-works/omni-dev/issues/1011)): the daemon's third service (after the browser bridge and Snowflake) tracks the repos and git worktrees open across **every** VS Code window — something no single extension can do, since the extension host is sandboxed per-window and each window sees only its own `workspace.workspaceFolders`. A resident daemon is the rendezvous point that aggregates per-window registrations into one consistent, liveness-aware view served back to the CLI and tray. `src/daemon/services/worktrees.rs` holds an in-memory registry behind a `std::Mutex` (never held across `.await`) supporting four ops — `register` (idempotent upsert), `heartbeat` (returns `{known}` so a window re-registers after a daemon restart), `unregister`, and `list`. Liveness is **reap-on-read**: entries carry `last_seen` and a 30s TTL (three missed ~10s heartbeats) ages out a window that crashed without a clean `unregister`, run inline on every `list`/`status`/`menu` with no background task. A "Worktrees" tray submenu lists open windows with a best-effort `focus:<key>` action per window that shells out `code <folder>` (resolved via `OMNI_DEV_VSCODE_BIN` → well-known paths → bare `code`). The new read-only client is `omni-dev worktrees list [--json]`; the register/heartbeat/unregister ops are spoken directly over the daemon's existing `0600` NDJSON socket by a companion VS Code extension (a separate deliverable). No new trust boundary and nothing is persisted. See [ADR-0040](docs/adrs/adr-0040.md) and [docs/worktrees-service.md](docs/worktrees-service.md).
- **MCP `jira_bulk_create` tool for epic decomposition** ([#1055](https://github.com/rust-works/omni-dev/issues/1055)): a new tool that creates multiple JIRA issues **and** wires the dependency links between them in a single call, eliminating the per-call round-trips of sequential creation. It takes a batch of issue specs (each with a local `alias`) and link specs whose endpoints are resolved **alias-first** (to the freshly-minted keys) then as literal keys for pre-existing issues — so a whole epic + stories + sub-task links land at once. Each issue create mirrors the `jira_create` JFM→ADF path; execution is **continue-on-error** with an optional `fail_fast` to stop at the first failure, and the tool returns a structured YAML report (per-issue `alias`/`ok`/`key`/`self_url` or `error`, per-link outcomes, and summary tallies). An empty issues array allows linking existing keys only. **No rollback** — partial success leaves the created issues in place; see [docs/mcp.md](docs/mcp.md).
- **`custom_fields` on the MCP `jira_create` tool** ([#1052](https://github.com/rust-works/omni-dev/issues/1052)): `jira_create` gains an optional `custom_fields` map (field name **or** canonical `customfield_NNNNN` id → value). When present, the tool fetches `createmeta` to resolve human-readable names to API ids and shapes each value to its schema (e.g. wrapping a select as `{"value": …}`), mirroring the CLI's `--set-field`. This lets MCP clients satisfy fields a JIRA project marks **required at create time**, preventing the HTTP 400 (`customfield_NNNNN is required`) rejection. The `createmeta` fetch is skipped when no custom fields are supplied (fast path).
- **Client-side option-value validation for `jira create --set-field`** ([#1053](https://github.com/rust-works/omni-dev/issues/1053)): supplied values for enumerated option fields (single/multi-select) are now validated against the field's `allowedValues` from the JIRA edit meta **before** the API call, so an out-of-range value is rejected locally with a field-named, actionable error instead of an opaque server 400. Fields that don't enumerate values (free text, numbers, user pickers, cascading selects) skip local validation and defer to the API. `jira create --help` now documents the full `--set-field "NAME=VALUE"` semantics: splitting on the first `=` only, VALUE kept verbatim, exact case-sensitive matching, `customfield_<digits>`-or-exact-display-name resolution, the create-screen requirement, and that `--set-field` overrides frontmatter `custom_fields:`.
- **MCP `jira_create` / `confluence_create` accept a full JFM `document`** ([#1058](https://github.com/rust-works/omni-dev/issues/1058)): `jira_read` / `confluence_read` emit a full JFM document (frontmatter + body), but the create tools previously took only structured params with no way to accept a document — so the natural *read → edit → create* round-trip an agent expects was broken at the MCP layer (the CLI side was handled by #1050/#1051). Both create tools gain an optional **`document`** parameter carrying the JFM document: `project`/`summary`/`issue_type`, labels and **custom fields** (JIRA, via the same `createmeta` resolution the CLI uses) or `space_key`/`title`/`parent_id` (Confluence) are taken from its frontmatter — the JIRA project derives from `key:` when no `project:` is set — and the body becomes the description/page body. The existing structured params still **override** their frontmatter counterparts (precedence: parameter/flag > frontmatter); when an override shadows a value present in the frontmatter a `warning:` is surfaced — **prepended in-band** to the MCP tool result (whose caller cannot see stderr) and printed to **stderr** by the CLI. Passing `document` together with `description`/`content` is an error. The frontmatter resolution and the createmeta-aware create path are extracted into a shared `src/atlassian/create.rs` used by **both** the CLI and MCP surfaces, so they cannot drift. See [docs/specs/jfm.md](docs/specs/jfm.md#creating-from-frontmatter).
- **MCP tools `jira_link_create` and `jira_link_parent`** ([#1049](https://github.com/rust-works/omni-dev/issues/1049)): complete the per-operation JIRA link tool family so every `omni-dev atlassian jira link` subcommand has a 1:1 MCP tool. `jira_link_create` creates a typed relationship link (`link_type`/`inward`/`outward`); `jira_link_parent` sets the system `parent` field for Epic → Story / Story → Sub-task hierarchy (`parent`/`child`). Both return YAML `{status: ok}` and join the existing `jira_link_list` / `jira_link_types` / `jira_link_remove` / `jira_link_remote_list` tools.
- **`jira create --instance <URL>`** ([#1051](https://github.com/rust-works/omni-dev/issues/1051)): new flag on `omni-dev atlassian jira create` to target a specific Atlassian instance, overriding the `ATLASSIAN_INSTANCE_URL` environment variable / `settings.json` for that invocation (email and token are still resolved from the configured credentials, so the override works even when no instance is configured in the environment). Applies to both `--format jfm` and `--format adf`.
- **JIRA create-screen introspection (`jira project create-meta`)** ([#1057](https://github.com/rust-works/omni-dev/issues/1057)): a new CLI command `omni-dev atlassian jira project create-meta --project <P> --issue-type <T>` and matching MCP tool `jira_project_create_meta` that answer "what does it take to create a `<Issue Type>` in `<Project>`?" up front, instead of forcing the create→HTTP 400 (`customfield_NNNNN is required`)→`field list`→`field options` recovery loop. For each field on the create screen it reports `field_id`, `name`, `required`, `schema_type`, any `custom` plugin URI, the `allowed_values` (resolving option / select / cascading-select, including nested `children`), and a `default_value` when the screen defines one — with required fields sorted first in the table view and `table`/`json`/`yaml`/`yamls`/`jsonl` output. Lets an agent prompt the user for a required field's value *before* attempting the create. Backed by a new `AtlassianClient::get_project_create_meta` (the existing lean `get_createmeta`/`EditMeta` path used by `jira create` is unchanged); see [docs/user-guide.md](docs/user-guide.md#jira-projects).
- **`dry_run` on mutating Atlassian MCP tools** ([#1048](https://github.com/rust-works/omni-dev/issues/1048)): the CLI has long offered `--dry-run` on `jira create`/`write` and `confluence create`/`write` to preview a mutation without contacting the API, but the MCP surface exposed no equivalent — an AI agent uncertain about required fields or JFM→ADF formatting had no safe way to validate a call before committing it, and a failed call could leave a real, un-undoable ticket. The mutating Atlassian tools (`jira_create`, `jira_write`, `jira_link_create`, `jira_link_parent`, `jira_link_remove`, `confluence_create`, `confluence_write`) now accept an optional `dry_run: bool`. When `true`, the tool runs all local resolution and ADF validation (so malformed input still errors) but stops short of the network call, returning the would-be HTTP request as YAML — `dry_run: true`, the `method` and `path`, and the request `body` (for JIRA, the exact `{"fields": {…}}` payload; for Confluence, the caller-supplied fields plus the resolved ADF, with server-resolved values like the numeric space id and next version number filled in at send time). For `jira_link_remove`, a dry-run previews the `DELETE` without requiring the destructive `confirm` guard. Each tool's description now points to the flag as the first thing to try when unsure. New module `src/mcp/dry_run.rs`; see [docs/mcp.md](docs/mcp.md).

### Changed
- **ADF schema snapshot bumped to `@atlaskit/adf-schema` 56.0.9** ([#1084](https://github.com/rust-works/omni-dev/issues/1084)): Refreshes the pinned upstream snapshot from `56.0.2` to `56.0.9` after the weekly `adf-schema-drift` job detected an upstream release. This is a **version bump only** — and a particularly inert one: the upstream `dist/json-schema/v1/full.json` is **byte-for-byte identical** across these patch releases, so the vendored `assets/adf-schema/full.json` is unchanged and the regenerated `generated.rs` carries the same `UPSTREAM_ENTRIES` and `UPSTREAM_FULL_JSON_SHA256` as before. The only changes are the provenance constants that pin *which release* the snapshot was taken from: `SCHEMA_VERSION` and `UPSTREAM_TARBALL_SHA256` in `src/atlassian/adf_schema/mod.rs`, the mirrored `UPSTREAM_VERSION` / `UPSTREAM_TARBALL_SHA256` in `generated.rs`, `assets/adf-schema/provenance.json`, and the JFM spec + README version references. No validation behaviour changes. (The drift job flagged `56.0.8`; `56.0.9` was the current `latest` at refresh time and is also content-identical, so the snapshot is pinned to it to keep the drift job green.)
- **ADF schema snapshot bumped to `@atlaskit/adf-schema` 56.0.2** ([#1046](https://github.com/rust-works/omni-dev/issues/1046)): Refreshes the pinned upstream snapshot from `54.0.4` to `56.0.2` after the weekly `adf-schema-drift` job detected an upstream release. This is a **version bump only** — the allowed-children content model is byte-for-byte unchanged, so the code-generated `UPSTREAM_ENTRIES` and the hand-maintained `CONTENT_ENTRIES` are untouched and no validation behaviour changes. Updates the provenance constants (`SCHEMA_VERSION`, `UPSTREAM_TARBALL_SHA256` in `src/atlassian/adf_schema/mod.rs`; `UPSTREAM_VERSION`, tarball/JSON SHA-256s in the regenerated `generated.rs`), the vendored `assets/adf-schema/full.json` + `provenance.json`, and the JFM spec + README version references. The only upstream content change is additive attribute-only metadata not modelled by the content-model drift detector (a `valign` enum — `top`/`middle`/`bottom` — added to `tableCell`/`tableHeader` attributes).
- **JIRA link surfaces aligned across MCP and CLI** ([#1049](https://github.com/rust-works/omni-dev/issues/1049)): the MCP and CLI shapes for managing JIRA links had diverged — the same set-parent operation was named `action: "parent"` on MCP but `jira link epic` on the CLI — so an agent moving between surfaces hit a `parent`↔`epic` naming mismatch. The CLI `jira link epic` subcommand is renamed to **`jira link parent`** (flags `--parent`/`--child`), with the subcommand alias `epic` and the flag aliases `--epic`/`--issue` retained so existing scripts keep working unchanged. Each tool/subcommand description now cross-references its counterpart on the other surface.
- **`jira create --help` documents the JFM frontmatter fields; `instance:` clarified as informational** ([#1051](https://github.com/rust-works/omni-dev/issues/1051)): `jira create --help` gained an epilogue listing the fields read from JFM frontmatter (`project`, `summary`, `issue_type`, `labels`, `custom_fields`, and `instance`), so the flag/frontmatter split is discoverable from `--help` alone — closing the agent-facing discoverability gap that motivated the issue (an agent reading `--help` had no signal these existed, and hit a runtime parse error as the first hint). The frontmatter `instance:` field is documented as informational only: it never routes requests — the target instance comes from the new `--instance` flag (see above), otherwise from `ATLASSIAN_INSTANCE_URL` / `settings.json`. Builds on #1050, which made create-path frontmatter optional; the strict read/write/edit round-trip (`JfmDocument::parse`) still requires `instance`.
- **macOS daemon switched to launchd socket activation** ([#1081](https://github.com/rust-works/omni-dev/issues/1081)): the `RunAtLoad`/`KeepAlive` LaunchAgent is replaced by a socket-activated one — launchd now **owns** the control socket (a `Sockets`→`Listener` dict, created `0o600`) and demand-spawns `omni-dev daemon run` on the first client connect, handing it the listening file descriptor; `daemon run` adopts that fd via the `launch_activate_socket(3)` FFI (`launchd::launchd_listener`) and falls back to binding the socket itself when not launchd-activated (a manual `daemon run`, CI, or any non-macOS platform). This makes the on-demand-only-mode start failure **architecturally impossible** and retires the tactical `launchctl kickstart` workaround from [#1078](https://github.com/rust-works/omni-dev/issues/1078): there is nothing for `RunAtLoad` to fail to do because the daemon is no longer run-at-load. Knock-on wins: cold-start latency for the first CLI invocation after login drops to ~0 (launchd is already listening), and a crashed daemon is re-activated on the next connect for free (no `KeepAlive` race). Behavioural notes: `daemon start` is now effectively a one-time install (after it, any CLI call re-activates the daemon — and login re-bootstraps the agent); `daemon stop` still boots the agent out for a real, observable stop (the demand socket is removed, so it stays down until the next `start` or login); and `daemon status` reporting "not running" now means "no process is resident right now", not "unavailable", since any connect re-activates it. Users upgrading from an older `RunAtLoad` daemon should run `omni-dev daemon restart` once to pick up the new plist. This change introduces the crate's first (narrowly-scoped, macOS-only) `unsafe` block for the FFI. See [ADR-0039](docs/adrs/adr-0039.md) and [docs/browser-bridge.md](docs/browser-bridge.md).

### Removed
- **BREAKING (MCP) — monolithic `jira_link` tool removed** ([#1049](https://github.com/rust-works/omni-dev/issues/1049)): the `jira_link` tool (the `action: list|types|create|remove|parent|remote_list` discriminator) is removed in favour of the per-operation `jira_link_*` tools, leaving exactly one shape that mirrors the CLI subcommands. MCP callers using `jira_link` with an `action` must switch to the matching tool (e.g. `action: "parent"` → `jira_link_parent`, `action: "create"` → `jira_link_create`).
- **BREAKING — `parent` dropped from the JIRA *write* surface; the dedicated link tool is the one canonical way to set hierarchy** ([#1056](https://github.com/rust-works/omni-dev/issues/1056)): two paths set the same system `parent` field — the link surface (`jira_link_parent` tool / CLI `jira link parent`) and the write surface (`jira_write parent` / CLI `jira write --parent`) — and the `jira_write` descriptions wrongly claimed the link path only made "relationship links", forcing an AI agent to read both surfaces and infer they were equivalent. The redundant write path is removed: the `parent` parameter no longer exists on the `jira_write` MCP tool, and `--parent` is gone from `omni-dev atlassian jira write`. Set an issue's parent (Epic → Story, Story → Sub-task) via the `jira_link_parent` tool (MCP) or `omni-dev atlassian jira link parent --parent <E> --child <I>` (CLI) — both already converge on the same `PUT /issue/{key}` `{"fields":{"parent":{"key":…}}}` request via `set_issue_parent`. The now-vestigial `parent` argument is also stripped from the internal `AtlassianClient::update_issue_with_custom_fields` helper chain.

### Fixed
- **`jira create` no longer demands frontmatter when CLI flags are supplied** ([#1050](https://github.com/rust-works/omni-dev/issues/1050)): creating an issue previously required JFM frontmatter with `type`, `instance`, and `summary` even when the equivalent values were already on the command line, so a first invocation hit a chain of sequential parse errors (`Document must start with '---'…` → `missing field instance` → `missing field summary`), each needing an edit-and-retry — and `instance` had to be typed into the frontmatter only to be thrown away, since the create path builds its client from auth config, not the document. An agent that started with the MCP `jira_create` tool (which takes a plain description with no frontmatter) and switched to the CLI hit this wall immediately. Frontmatter is now **optional**: a file with no `---` block is treated entirely as the issue body with all metadata taken from flags — `omni-dev atlassian jira create --project P --summary "…" --type Story body.md` just works, matching MCP `jira_create`. When frontmatter *is* present, any field it omits falls back to the matching flag (extending the existing `--project`/`--type`/`--summary` override behaviour to the whole resolution), and `instance` is no longer required at all. Resolution precedence — **flags → frontmatter → derived/default** — is documented at the top of `create --help` and in [docs/user-guide.md](docs/user-guide.md). The strict read/write/edit round-trip (`JfmDocument::parse`) is unchanged; leniency is localized to the create path via a new `split_frontmatter` helper and a create-only `JiraCreateFrontmatter`.
- **Forbidden ADF mark combinations are rejected pre-flight with an actionable message** ([#1047](https://github.com/rust-works/omni-dev/issues/1047)): a JFM body like `` **`text`** `` (bold + monospace) maps to a text node carrying both `strong` and `code` marks, which ADF forbids — but Atlassian only signalled it as an opaque `HTTP 400: {"errorMessages":["INVALID_INPUT"],"errors":{}}` with no field, path, or offending node, leaving callers (and AI agents) to bisect the body by hand. The ADF mark validator now models the upstream schema's two mutually-exclusive inline-mark groups (the `code_inline_node` group `{annotation, code, link}` vs the `formatted_text_inline_node` group `{annotation, backgroundColor, em, link, strike, strong, subsup, textColor, underline}`, transcribed from the pinned `@atlaskit/adf-schema`) and emits a new `ForbiddenMarkCombination` violation when a text node mixes marks across groups — most commonly `code` paired with any styling mark. Because every create/write path (`jira create`, `confluence create`, `confluence write`) and `--dry-run` already run this validator before sending, the bad combination is now caught locally with a `cannot be combined with` message naming the two conflicting marks plus a remediation hint, instead of the opaque API error. Separately, `jira create` now routes its non-success responses through the same error handler `jira update` uses, so a field that reports it requires ADF surfaces as the actionable `JiraAdfFieldRequired` rather than a bare `ApiRequestFailed`. (Note: the issue also listed `link`+`textColor` as forbidden, but the pinned upstream schema permits both in the formatted group, so that pairing is intentionally **not** flagged.)
- **Close a process-global env race in config discovery** ([#821](https://github.com/rust-works/omni-dev/issues/821)): the env-reading functions in `claude::context::discovery` are split into thin public boundary wrappers and pure inner seams that take the environment as an injected dependency (STYLE-0028), so the 18 tests formerly serialised by a module-local `ENV_MUTEX` now inject a `MapEnv` and temp dirs instead of mutating process env — the mutex is deleted and the module is lock-free and fully parallel. This removes the last writer of `XDG_CONFIG_HOME` that raced a *different* lock (`AUTH_ENV_MUTEX`) over the same process-global var (the cross-module `EACCES` flake seen on PR #1014); public signatures are unchanged thin wrappers, so callers and the CLI surface are untouched. No user-facing behaviour change.

### Documentation
- **Surface the daemon, request log, and Snowflake in the README and docs index** ([#1043](https://github.com/rust-works/omni-dev/issues/1043)): these v0.30.0 features shipped with substantive reference docs (`docs/log.md`, `docs/snowflake-service.md`, and the daemon coverage in `docs/browser-bridge.md` / [ADR-0039](docs/adrs/adr-0039.md)) but weren't discoverable — the top-level README never mentioned them and the docs index left `log.md` and `snowflake-service.md` unlinked. Adds README feature sections for the **Daemon**, **Snowflake**, and the **Request log** (`omni-dev log`), and docs-index subsections linking those guides plus a daemon pointer.
- **Document `coverage diff`** ([#1045](https://github.com/rust-works/omni-dev/issues/1045)): the `omni-dev coverage diff` command (added in v0.29.0) had no user documentation at all. Adds a [docs/coverage.md](docs/coverage.md) reference (what it computes, the accepted report formats, output formats, the `--fail-under-patch` gate, diff scoping, and the full flag table), a README feature section, and a docs-index entry.
- **Audit every MCP tool and parameter description for AI-agent clarity** ([#1072](https://github.com/rust-works/omni-dev/issues/1072)): a tool/parameter description is the only thing an AI agent reads to decide *how* to call a tool, so vague ones cause wrong-but-silent calls (inverted link direction, a display name where an `accountId` is wanted, the wrong body shape). Brings all descriptions across the git, jira, confluence, datadog, and ai/config/atlassian families up to a new **STYLE-0029** bar — concrete example values, `accountId`-vs-name and JFM-vs-ADF wire formats, JQL/CQL and date/duration formats, enum value lists, directional link semantics, use-vs-sibling pointers, and `dry_run`/`confirm` notes — adds the previously-missing `Mirrors omni-dev …` CLI cross-references to the Datadog tools, cross-references MCP tools from the matching CLI subcommand `--help`, and syncs the [docs/mcp.md](docs/mcp.md) catalog with the audited surface.

### Security
- **Bump `anyhow` to 1.0.103 for RUSTSEC-2026-0190**: the advisory reports unsoundness in `anyhow::Error::downcast_mut()` — adding context via `Error::context` and later calling `downcast_mut` on the returned `Error` violates borrow rules (undefined behaviour), fixed in 1.0.103. Lockfile-only bump (the manifest already allowed `1.0`); surfaced by the `cargo-deny` dependency-policy CI job.
- **Bump `quinn-proto` to 0.11.15 for RUSTSEC-2026-0185**: resolves a high-severity remote memory-exhaustion advisory (unbounded out-of-order stream reassembly) that began failing the Security Audit CI job after publication on 2026-06-22. Transitive patch-level lockfile bump (0.11.14 → 0.11.15); no manifest or source changes.

### CI/CD
- **Use a prebuilt `cargo-audit` in the Security Audit job** ([#1071](https://github.com/rust-works/omni-dev/issues/1071)): the job ran `cargo install cargo-audit --locked`, recompiling it from source on every run (rust-cache can't help — it caches the registry/target dir, not installed binaries). Switches to `taiki-e/install-action`, which fetches a prebuilt binary in seconds, mirroring the prebuilt `cargo-deny-action` already used by the dependency-policy job. The `cargo audit` run step is unchanged.

## [0.30.0] - 2026-06-22

### Added
- **Extensible daemon for long-lived services** ([#987](https://github.com/rust-works/omni-dev/issues/987), [#988](https://github.com/rust-works/omni-dev/issues/988)): A new `omni-dev daemon` command tree (`run`/`start`/`stop`/`restart`/`status`) that supervises long-lived **services** inside one process behind a private per-user Unix-domain control socket. Single-instance supervision falls out of the exclusive socket bind itself (with a `ping`-probe + stale-socket reclaim), JSON/NDLINE operations route to registered services via a `ServiceRegistry`, and `SIGTERM`/`SIGINT` trigger graceful shutdown; on macOS, `daemon start` installs a launchd LaunchAgent for auto-start at login. The **browser bridge is the first service migrated onto it**: `BridgeService` wraps the same `BridgeServer` planes verbatim — standalone `omni-dev browser bridge serve` keeps working unchanged — persists its session token to a `0600` file so the thin clients (`request`, `harvest`) transparently discover a daemon-hosted bridge, and the bridge now also accepts WebSocket connections on IPv6 (`::1`) loopback alongside IPv4. An optional macOS **menu-bar tray** (behind the off-by-default `menu-bar` feature) renders one submenu per service from `DaemonService::menu()`, refreshes ~1 Hz, and offers copy-snippet / copy-bridge-key / copy-request-command actions; enabling it inverts the runtime so tokio runs on background threads and the main thread hosts the macOS event loop (all other paths unchanged). `daemon status` prints a per-service table (`--json` for machines). New modules under `src/daemon/`; hosting the bridge does **not** change its trust boundary. See [ADR-0039](docs/adrs/adr-0039.md), [ADR-0036](docs/adrs/adr-0036.md), and [docs/browser-bridge.md](docs/browser-bridge.md).
- **Graceful connection draining on daemon shutdown** ([#992](https://github.com/rust-works/omni-dev/issues/992)): accepted control-socket connections that have begun dispatch now always finish their reply before the daemon tears down, instead of being abandoned mid-request. Handlers are tracked in a `JoinSet` (reaped during normal operation so a long-lived daemon's set can't grow unbounded) and drained on shutdown under a 5-second `DRAIN_TIMEOUT`.
- **Local invocation + HTTP request log (`omni-dev log`)** ([#1025](https://github.com/rust-works/omni-dev/issues/1025)): A local, append-only NDJSON log (`log.jsonl`) records exactly one `kind:"invocation"` line per `omni-dev` run — the argv-derived subcommand path, exit code, duration, the rendered error chain on failure, and a whitelisted `OMNI_DEV_*` env snapshot — plus one `kind:"http"` line per outbound request, hooked **inside** each client's existing retry loop (Atlassian, Datadog, Snowflake, the direct-Anthropic/Bedrock/OpenAI-compatible Claude backends and the `claude-cli` subprocess, and the browser bridge) so retries and transport errors are captured too. HTTP records inherit their parent run's `invocation_id`/`source`/`mcp_tool` through a process-global context plus a `tokio` task-local override (the MCP server scopes `source=mcp` + the tool name around each `call_tool`), so every request correlates to the run that issued it. The new read-only `omni-dev log` subcommand searches and pretty-prints it: a filter matrix (`--since`/`--method`/`--status`/`--service`/`--command`/`--url`/`--grep`/`--fuzzy`/`--id`), a `--query` mini-language (AND/OR/NOT, parentheses, `field:value` terms, bare fuzzy tokens, status-class matching like `5xx`), `oneline`/`json`/`full` renderers (`json` emits the on-disk line verbatim so it composes with `jq`), and `-n/--limit` + `-f/--follow` streaming. Best-effort and default-on — a write failure never changes the caller's exit code — and **no secret is ever written**: auth headers are redacted centrally, request/response bodies are opt-in via `OMNI_DEV_LOG_BODIES` (headers via `OMNI_DEV_LOG_HEADERS`), and `OMNI_DEV_LOG_DISABLE` is an absolute opt-out. The path resolves via `OMNI_DEV_LOG_FILE` → `state_dir`/`data_dir` `omni-dev/log.jsonl` (dir `0700`, file `0600`). New module `src/request_log.rs` plus `src/cli/log.rs`; see [docs/log.md](docs/log.md).
- **Confluence attachment download** ([#1036](https://github.com/rust-works/omni-dev/issues/1036)): The Confluence side gains the attachment-download capability the JIRA side has had since #309, closing an asymmetry — previously the only ways to pull an attachment binary off a page were dropping out to `curl` with the `api_token` read from `~/.atlassian/config.yaml` (leaking credentials into shell pipelines the wrapper exists to avoid) or `confluence download`, which fetches page text but skips attachment binaries entirely. New CLI `omni-dev atlassian confluence attachment download <ATTACHMENT_ID> [--output <PATH>]` resolves the attachment's `downloadLink` from its ID (v2 `GET /wiki/api/v2/attachments/{id}`) and streams the binary to disk via the same authenticated, request-logged transport as the rest of the client (following the media-CDN redirect); `--output` defaults to the attachment's filename in the cwd, treats an existing directory as a target dir, and otherwise writes verbatim. New MCP tool `confluence_attachment_download` (`{attachment_id, output_path?}`) mirrors it, returning YAML `{id, title, media_type, bytes, path}` and writing to a fresh temp dir when `output_path` is omitted so the assistant can read the file back. Finally, `confluence download` (and the `confluence_download` MCP tool) gain `--include-attachments` / `include_attachments`, which fans out per page after the content write to fetch every attachment binary into an `attachments/` subdirectory beside its content file — making full-page snapshots possible in one command. The fan-out is best-effort (listing or per-binary failures are logged and swallowed so a broken attachment can't fail an otherwise-successful page) and strips remote-controlled attachment titles to a single path component to prevent directory traversal. New `ConfluenceApi::{get_attachment, download_attachment, download_attachment_bytes}`; see [docs/user-guide.md](docs/user-guide.md#confluence-attachments).
- **Per-video metadata sidecars for `transcript youtube sync`** ([#976](https://github.com/rust-works/omni-dev/issues/976)): `sync` now writes a metadata sidecar `<out>/<channel-id>/<video-id>.meta.yaml` alongside each transcript — title, channel, channel URL, category, `published_at`, duration, description, keywords, point-in-time `view_count`/`like_count`, `is_live_content`/`is_unlisted`, thumbnail, a `fetched_at` snapshot timestamp, and `schema: 1`. The metadata it already received per video was previously parsed and discarded. Sidecars are language-independent (one per video) and written atomically (temp file + rename). They are planned by a **filesystem scan** of the channel directory rather than channel re-enumeration, so **missing sidecars are backfilled on every run** for the full set of already-synced videos, without `--full` and without touching the bot-gated transcript path. The new **`--refresh-metadata-older-than <date-spec>`** flag re-fetches sidecars whose `fetched_at` predates the cutoff — `<date-spec>` accepts an absolute date (`YYYY-MM-DD`, midnight UTC), a full RFC 3339 timestamp, or a relative spec like `"2 days ago"` (units: minute, hour, day, week, month, year). Metadata is fetched with a single **un-gated WEB-client `/player` call** (no `visitorData` bootstrap), which carries the `microformat` block — publish date, like count, category — that the `ANDROID_VR` transcript path lacks; that independence is what makes standalone backfill/refresh possible. `microformat`-sourced fields are omitted when absent (ratings disabled, or private/removed videos written from `videoDetails` alone). Metadata fetch/write failures are tallied separately (`metadata synced`/`refreshed`/`failed` in the `SyncReport`) and **never block or fail transcript syncing**. New module `src/transcript/sources/youtube/metadata.rs`; see [docs/transcript.md](docs/transcript.md).
- **Snowflake query service on the daemon** ([#999](https://github.com/rust-works/omni-dev/issues/999)): A second daemon-hosted service (after the browser bridge) that authenticates Snowflake sessions via external-browser SSO and reuses them for **concurrent arbitrary SQL** across **any** account — solving the "SSO popup on every query" problem (a live Snowflake session has no serialization API, so a resident process is the only way to reuse one auth across invocations). New engine `src/snowflake/`: each `(account, user)` keeps a **bounded session pool** (`SNOWFLAKE_POOL_SIZE`, default 4) so queries run **concurrently** on one authentication identity while still honoring per-query `warehouse`/`role`/`database`/`schema` — applied as a `USE`-diff on checkout against each session's captured base context, with a `tokio::Semaphore` capping concurrency *and* the live-session/auth count, a shared auth gate serializing browser popups, lazy re-auth on session expiry, and arbitrary-schema → self-describing `{ columns, rows }` JSON conversion. Plus a thin `DaemonService` adapter `src/daemon/services/snowflake.rs` (routes `query`/`sessions`/`disconnect`, contributes a "Snowflake" tray submenu, surfaces in `daemon status`) and a thin client `omni-dev snowflake {query,sessions,disconnect}` — `query` takes SQL from an argument or stdin and supports per-query `--warehouse/--role/--database/--schema` plus `--format json|yaml`. Account/user/context default from `SNOWFLAKE_*` env vars then `~/.omni-dev/settings.json` (the Atlassian pattern); **no hardcoded accounts**. No new socket or persisted secret — it rides the daemon's existing `0600` Unix control socket and keeps sessions in memory only. The engine runs on an in-tree **clean-room Snowflake v1 REST client** (`src/snowflake/client/`, no third-party connector) that adds **session-token renewal** (so a session survives the ~1h token lifetime instead of re-authenticating), transparent renew-and-retry on expiry, **async-result polling** so heavy long-running queries complete (bounded by `SNOWFLAKE_QUERY_TIMEOUT`) instead of being killed by a per-request timeout, per-request HTTP timeouts (`SNOWFLAKE_HTTP_TIMEOUT`) and transient-error retries, an overall sign-in deadline (`SNOWFLAKE_AUTH_TIMEOUT`) so a hung auth can't block others, and external-chunk download for large results. No third-party Snowflake connector is used (an early `snowflake-connector-rs` dependency, which pulled `rsa`/RUSTSEC-2023-0071, was removed). See [docs/snowflake-service.md](docs/snowflake-service.md).

### Changed
- **Expanded crates.io discovery metadata and `--help` banner** ([#835](https://github.com/rust-works/omni-dev/issues/835)): three discovery surfaces still described omni-dev as merely a git-commit toolkit, hiding its AI/MCP/multi-backend scope. The `Cargo.toml` `keywords` (now `git`, `ai`, `claude`, `mcp`, `conventional-commits`) and one-line `description` — what crates.io indexes and displays — the clap top-level `about` (the first line of every shell user's `--help`), and the `src/lib.rs` crate-root rustdoc (the docs.rs landing page) now all lead with the broader one-line pitch and a working public-API example. crates.io re-indexes the new keywords on this release's version bump.
- **ADF schema snapshot bumped to `@atlaskit/adf-schema` 54.0.4** ([#986](https://github.com/rust-works/omni-dev/issues/986)): Refreshes the pinned upstream snapshot from `52.9.5` to `54.0.4` after the weekly `adf-schema-drift` job detected an upstream release. This is a **version bump only** — the allowed-children content model is byte-for-byte unchanged, so the code-generated `UPSTREAM_ENTRIES` and the hand-maintained `CONTENT_ENTRIES` are untouched and no validation behaviour changes. Updates the provenance constants (`SCHEMA_VERSION`, `UPSTREAM_TARBALL_SHA256` in `src/atlassian/adf_schema/mod.rs`; `UPSTREAM_VERSION`, tarball/JSON SHA-256s in the regenerated `generated.rs`), the vendored `assets/adf-schema/full.json` + `provenance.json`, and the JFM spec reference. The upstream diff is additive attribute-only metadata not modelled by the content-model drift detector (`codeBlock.hideLineNumbers`/`wrap`, `tableCell`/`tableHeader.valign`).
- **`jira dev --summary` provider output** ([#924](https://github.com/rust-works/omni-dev/issues/924)): each entry in a category's `providers` list now carries both the `instance_type` identifier (the `applicationType` round-trip key, e.g. `stash`) and the display `name` (e.g. `Bitbucket Server`). The human table shows `Bitbucket Server (stash)` when the two differ and a plain `GitHub` when they match. **Output shape change**: under `--output json`/`yaml`, `providers` is now a list of `{instance_type, name}` objects instead of a list of strings.

### Removed
- **BREAKING — `omni-dev voice …` command tree removed** ([#980](https://github.com/rust-works/omni-dev/issues/980), [#977](https://github.com/rust-works/omni-dev/issues/977)): All voice / audio / speech-to-text functionality has been extracted to the dedicated [rust-works/omni-voice](https://github.com/rust-works/omni-voice) repository and removed from omni-dev. The entire `omni-dev voice …` command tree (`capture`, `transcribe`, `reflect`, `review`, `enroll`, `install-model`) no longer exists — users who need it should use omni-voice. This drops the `src/voice/` and `src/cli/voice/` modules, their tests, snapshots, and ~10 MB of audio fixtures, plus the voice-only dependency stack (`cpal`, `rubato`, `hound`, `signal-hook`, `ringbuf`, `ulid`, `candle-core`, `candle-nn`, `candle-transformers`, `hf-hub`, `tokenizers`, `byteorder`, `tract-onnx`, `rustfft`) and the `libasound2-dev`/ALSA CI steps that existed solely for `cpal`. The result is a measurably smaller dependency tree and release binary. The voice ADRs (0031–0035, 0037) are marked superseded by the new [ADR-0038](docs/adrs/adr-0038.md), which records the extraction; they remain as historical context.

### Fixed
- **omni-dev compiles on Windows again — the daemon is gated to Unix** ([#1041](https://github.com/rust-works/omni-dev/issues/1041)): the new daemon (#987, #988) is built on Unix-domain sockets (`tokio::net::UnixListener`/`UnixStream`), which don't exist on Windows, so `cargo build` and `cargo install omni-dev` failed to compile on Windows MSVC (`error[E0432]: unresolved import` in `src/daemon/{server,client,single_instance}.rs`). The daemon runtime and its `daemon` / `snowflake` CLI subcommands are now gated behind `#[cfg(unix)]` and compiled out on Windows — running the daemon there remains future work — while `daemon::paths` stays cross-platform (the request log and the browser thin-client token discovery depend on it), so the rest of omni-dev builds and installs on Windows as before. A Windows build job was added to CI so this class of portability regression is caught on every PR rather than only when a release tag is pushed.
- **Editor-sized images with pixel widths above 100 round-trip without ADF validation failure** ([#1037](https://github.com/rust-works/omni-dev/issues/1037)): real Confluence emits `mediaSingle` pixel widths well above 100 for editor-sized images, but the pinned ADF schema validated `width` against a fixed `[0, 100]` range (the percentage cap), so a read→write round-trip of such an image failed validation. The validator now models the upstream schema's sibling-conditional range via a new `CondNumRange` attribute type resolved at validation time, before shape-checking: pixel widths (`widthType="pixel"`) are unbounded `[0, ∞)`, while percentage widths (the default when `widthType` is absent) keep the `[0, 100]` cap.
- **Git command spawning no longer races concurrent environment mutation** ([#1022](https://github.com/rust-works/omni-dev/issues/1022)): git subprocesses are now spawned through a `git_command()` helper that snapshots the environment (`env_clear()` + `envs(std::env::vars_os())`) under `std`'s lock before `exec`, eliminating intermittent failures where a concurrent `std::env::set_var` raced a child process's environment capture. The helper also sets `commit.gpgsign=false` on spawned git to avoid flaking on hosts with commit signing enabled.
- **Daemon control socket is owner-private from birth** ([#995](https://github.com/rust-works/omni-dev/issues/995)): the socket was created and then `chmod`-ed to `0600`, leaving a brief window where it could be group/world-accessible — security-critical for a socket carrying privileged daemon operations. Binding now tightens the process umask to `0o177` for the synchronous bind only (restored via an RAII guard), so the socket inode is never created with broader permissions.
- **Bridge session token is masked in the menu-bar tray** ([#997](https://github.com/rust-works/omni-dev/issues/997)): `BridgeService::menu` rendered the full session token as a visible `Key: <token>` label — needless exposure to shoulder-surfing, screen-sharing, and screenshots when the value is already reachable via the "Copy bridge key" action. The label now reveals only a masked tail (`Key: ••••<last 4>`, or fully masked when the token is too short to keep most of its entropy hidden); the full value stays behind the copy action.
- **`launchctl bootout` spawn failures are surfaced on `stop`/`restart`** ([#996](https://github.com/rust-works/omni-dev/issues/996)): the pre-bootstrap bootout and `unload`'s bootout both discarded spawn errors *and* non-zero status via `let _ = …output()`, so if `launchctl` was missing or the GUI domain unreachable the teardown silently did nothing yet `unload` returned `Ok` — making `stop`/`restart` claim to have disabled auto-start when they may not have. Both call sites now share a `bootout` helper that logs a spawn failure via `tracing::warn!` while still treating "already not loaded" as the benign no-op it should be.
- **Duplicate daemon service registrations are rejected** ([#998](https://github.com/rust-works/omni-dev/issues/998)): `ServiceRegistry::register` pushed unconditionally, so a second service sharing a `name()` was dead code for routing (`get`/`dispatch` use `.find`) yet still double-counted in `status`/`menu`/`shutdown` iteration. A duplicate is now dropped with a warning, keeping the documented first-wins contract consistent everywhere.
- **`daemon restart` no longer fails with "Bootstrap failed: 5: Input/output error"** ([#1004](https://github.com/rust-works/omni-dev/issues/1004)): `launchctl bootout` is asynchronous and returns before launchd has finished tearing the job down, so an immediate re-bootstrap raced the in-flight teardown into EIO (the socket-ping `wait_until_down` never waited for the launchd job itself to unload). `install_and_load` now polls `launchctl print` until the job leaves the domain before bootstrapping, and retries bootstrap a few times to absorb any residual transient EIO.
- **`daemon start` no longer times out when launchd downgrades `RunAtLoad`** ([#1078](https://github.com/rust-works/omni-dev/issues/1078)): `launchctl bootstrap` returned success but launchd intermittently downgraded the plist's `RunAtLoad` to on-demand activation, leaving the freshly-bootstrapped job parked at `state = not running` (`pended nondemand spawn = speculative`) indefinitely — so `start`/`restart` burned their 5s readiness budget and errored with `daemon did not become ready within 5s`, even though the plist was valid and `daemon run` worked in the foreground. The downgrade is non-deterministic (depends on launchd's internal scheduling, not the plist), so depending on `RunAtLoad` to spawn the process was inherently racy. `install_and_load` now issues an explicit `launchctl kickstart -p <domain>/<label>` immediately after a successful bootstrap; `-p` is idempotent (returns the existing pid if `RunAtLoad` already fired, spawns otherwise), making the immediate-start path deterministic while remaining a no-op on the happy path. The kickstart is best-effort (logged, not fatal) — the socket-ping readiness poll stays the authoritative gate.
- **Browser-bridge restart reliably rebinds its fixed ports** ([#990](https://github.com/rust-works/omni-dev/issues/990)): a daemon- or tray-initiated `BridgeService::restart` tears the old planes down and rebinds the *same* fixed ports (default `9998`/`9999`), but a just-closed connection could leave a port in `TIME_WAIT`, so `BridgeServer::start` failed with `EADDRINUSE` — leaving the bridge wedged not-running with no automatic recovery (`status`/`menu` reported not-running until another restart). The bridge now binds its loopback control and WebSocket listeners with `SO_REUSEADDR`, so the rebind succeeds immediately despite a lingering `TIME_WAIT`; standalone `omni-dev browser bridge serve` restarts benefit too. Fail-closed against a *live* listener on the port is unchanged (`SO_REUSEADDR` ≠ `SO_REUSEPORT`), and the loopback-only, token-authenticated trust boundary (ADR-0036) is untouched.
- **launchd plist no longer breaks on paths containing XML metacharacters** ([#991](https://github.com/rust-works/omni-dev/issues/991)): `daemon start` rendered the LaunchAgent plist by interpolating the executable path (`current_exe()`) and the `--socket` path straight into `<string>…</string>`. `&`, `<`, and `>` are all legal in filenames (e.g. a home or socket dir named `A&B`), so such a path produced malformed plist XML and `launchctl bootstrap` failed with an opaque parse error. The two paths are now XML-escaped before interpolation. A non-UTF-8 path — previously corrupted silently via `Path::display()` — is now rejected with a clear error instead, since it cannot be faithfully represented in the UTF-8 plist.
- **Daemon control socket bounds NDJSON line length (local memory-exhaustion DoS)** ([#989](https://github.com/rust-works/omni-dev/issues/989)): the control-socket framing used `LinesCodec::new()`, whose `max_length` defaults to `usize::MAX`, so any local process that connected and streamed bytes **with no newline** grew the per-connection read buffer without bound — and since each accepted connection is a detached `tokio::spawn`, many such connections could run in parallel. The codec is now capped at 1 MiB per line (`MAX_LINE_BYTES`) on both the server and the thin client; an over-limit request gets a single `DaemonReply::err` and the connection is closed (rather than looping forever in the codec's post-overflow discard mode). Found during the post-merge review of #988; see [ADR-0039](docs/adrs/adr-0039.md).
- **Daemon shutdown no longer makes stray pings hang, and `stop`/`restart` honour their ~5s budget** ([#993](https://github.com/rust-works/omni-dev/issues/993)): after the accept loop broke on shutdown, the listener fd stayed open until process exit, so a `connect`+`ping` arriving during `shutdown_all().await` connected at the kernel level but was never accepted — it blocked the caller until the (potentially slow) drain finished. The control socket is now closed **and unlinked before** draining, so such connects fail fast (`ECONNREFUSED`); unlinking up-front also closes a restart race where a replacement daemon could reclaim the stale socket and rebind its own listener mid-drain only to have the old daemon's late unlink delete it. Separately, `wait_until_ready`/`wait_until_down` polled `50×100 ms` with **no per-attempt timeout**, so a single hung `ping` could blow the documented "~5s" budget and couple `stop`/`restart` latency to drain time; each `ping` is now individually bounded (250 ms) and the poll loop enforces a real 5s deadline.
- **`daemon restart` no longer risks leaving the daemon stopped *and* unregistered from launchd** ([#994](https://github.com/rust-works/omni-dev/issues/994)): on macOS, `restart` booted the launchd agent out (`launchd::unload`) *before* re-bootstrapping it via `install_and_load` — but `install_and_load` already boots out any prior agent first, so the explicit pre-bootout was redundant and merely widened a failure window: if the process died or `launchctl bootstrap` failed between that bootout and a successful re-bootstrap, the daemon was left both stopped and unregistered from auto-start (worse than before `restart` ran). The redundant pre-bootout is removed, so the unavoidable bootout→bootstrap gap is now the tight, retrying sequence inside `install_and_load`. This also fixes a second instance of the same bug, where a slow shutdown could make `wait_until_down`'s timeout bail *after* the agent had already been booted out. `stop` still boots out via `unload` to disable auto-start.
- **Confluence attachment upload returned HTTP 405** ([#1005](https://github.com/rust-works/omni-dev/issues/1005)): `confluence attachment upload` (and the `confluence_attachment_upload` MCP tool) POSTed to the Confluence Cloud v2 path `/wiki/api/v2/pages/{id}/attachments`, which has no attachment-creation handler and returns `405 METHOD_NOT_ALLOWED`. Attachment creation is v1-only; uploads now use `POST /wiki/rest/api/content/{id}/child/attachment` and parse the v1 response shape, so the returned attachment metadata (media type, file size, download URL, page id, file id, version) is populated. Listing and deletion (already on supported v2 endpoints) are unchanged.
- **Large (multi-chunk) Snowflake results decode correctly** ([#999](https://github.com/rust-works/omni-dev/issues/999)): the clean-room client parsed each downloaded result chunk as a self-contained JSON array, but Snowflake serves chunks as **bare comma-separated row arrays** (`[r1],[r2],…`) meant to be concatenated — so any query whose result spanned a multi-row chunk failed with `invalid result chunk JSON: trailing characters`. Chunk bytes are now framed with `[` … `]` before parsing (matching the reference connector), so heavy queries returning large result sets decode correctly. (Found via the #999 field test once the inner error was surfaced.)
- **Daemon errors surface the underlying cause** ([#999](https://github.com/rust-works/omni-dev/issues/999)): a control-socket reply collapsed any handler error to its top-level wrapper (e.g. just `Snowflake query failed`), hiding the real reason. Replies now carry the full error chain (`{:#}`), so the actual Snowflake server error code/message — or a client-side polling-deadline timeout (`query did not finish within …`) — reaches the caller, and the engine also logs the inner cause server-side. (`SNOWFLAKE_QUERY_TIMEOUT`, default 3600s, bounds the async-result polling for long-running queries.)
- **Menu-bar tray no longer closes on refresh** ([#999](https://github.com/rust-works/omni-dev/issues/999)): the daemon tray rebuilt its whole menu (`set_menu`) whenever any item's text changed — and on macOS that closes an open menu, so a Snowflake submenu showing live per-session activity would snap shut roughly once a second. The tray now updates item text/enabled **in place** and only rebuilds when the menu *structure* changes (a session added or removed), so an open menu stays open while its live state refreshes. Each authenticated session now also shows **what it's doing** — the running query (a truncated preview) and how long it's been running, or its idle time and query count — across the tray submenu, `daemon status`, and `omni-dev snowflake sessions`.
- **`jira dev` auto-discovery for Bitbucket Server** ([#924](https://github.com/rust-works/omni-dev/issues/924)): provider auto-discovery now queries the DevStatus detail endpoint with the instance-type identifier (e.g. `applicationType=stash`) reported by the summary's `byInstanceType` map, rather than the human-readable display name (`Bitbucket Server`). Previously, any provider whose display name differed from its identifier — Bitbucket Server being the canonical case — was missed, returning empty results.
- **Inline `code` in JFM headings no longer fails the ADF mark validator** ([#1005](https://github.com/rust-works/omni-dev/issues/1005)): A heading authored with backticks — e.g. `` ### `GET /api/services/example` `` — previously produced a `heading` node carrying a `code` mark, which ADF's heading content model forbids, so the document was rejected at write time with an opaque JSONPath-only error. The JFM→ADF converter now strips the `code` mark from heading content (keeping the text as plain — the safe lossy direction, since Atlassian renders no inline-code styling on headings) and emits a warning naming the affected heading. The validator remains the safety net for any other path that produces a code-marked heading.

### Documentation
- **Daemon architecture documented** ([#987](https://github.com/rust-works/omni-dev/issues/987)): [ADR-0039](docs/adrs/adr-0039.md) records the daemon design (service abstraction, Unix-socket protocol, paths/permissions, lifecycle, macOS menu-bar); [ADR-0036](docs/adrs/adr-0036.md) is amended for the browser-bridge daemon integration, token-file discovery, and security-model preservation; `CLAUDE.md` and [docs/browser-bridge.md](docs/browser-bridge.md) gain daemon sections, and the ADR index is updated.
- **Facebook browser-bridge recipe** ([#922](https://github.com/rust-works/omni-dev/issues/922)): a step-by-step guide in [docs/browser-bridge.md](docs/browser-bridge.md) documenting the manual three-step procedure (`fb_dtsg`/`lsd`/`USER_ID` harvest → cross-origin `doc_id` discovery → the paginating GraphQL drain) that `harvest facebook posts` automates, with a worked `bash`/`jq` drain loop, a field map of where post data lives in the response, and caveats on re-harvesting volatile values.
- **Website tutorial examples re-themed** ([#982](https://github.com/rust-works/omni-dev/issues/982)): the project website's tutorial worked examples were re-themed away from the (now-extracted) voice workflow to the `coverage diff` workflow.

### CI/CD
- **Coverage pipeline delegated to a reusable composite action** ([#1024](https://github.com/rust-works/omni-dev/issues/1024)): the inline coverage workflow (cargo-llvm-cov setup, baseline computation, PR-comment generation, threshold enforcement — 165+ lines) is replaced by a single invocation of the new `action-works/omni-dev-coverage-check@v1` action. Coverage behaviour (patch coverage, per-file deltas, merge-base baseline) is unchanged; the threshold and worktree system deps are passed as inputs.
- **commit-check workflow gets a `github-token`** ([#1034](https://github.com/rust-works/omni-dev/issues/1034)) so its GitHub API calls run under higher rate limits.
- **Dependency bumps**: `rand` 0.9.4→0.10.1 ([#984](https://github.com/rust-works/omni-dev/pull/984)), the `rust-minor-patch` group ([#983](https://github.com/rust-works/omni-dev/pull/983)), and `actions/checkout` upgraded to v7 across all workflows ([#1033](https://github.com/rust-works/omni-dev/pull/1033)).

## [0.29.0] - 2026-06-12

### Added
- **`transcript youtube sync`** ([#931](https://github.com/rust-works/omni-dev/pull/931), [#928](https://github.com/rust-works/omni-dev/issues/928)): A new `omni-dev transcript youtube sync` subcommand for incrementally syncing transcripts from one or more YouTube channels to the filesystem. Builds on the existing per-video fetcher with channel enumeration, filesystem-as-state deduplication, and concurrent fetching (`futures::stream::buffer_unordered`). Flags: `--out`, `--lang`, `--format`, `--auto`, `--full`, `--since` (accepts `YYYY-MM-DD` midnight-UTC or RFC 3339), `--concurrency`, and `--dry-run`. Incremental mode stops at the first already-present file (newest-first ordering); `--full` examines every ID to fill gaps. Each transcript is written atomically via a sibling `.tmp` file + rename, and a `SyncReport` (synced / already present / no transcript / failed / would fetch / channel errors) is recorded and printed. Videos without a usable transcript (age-gated, region-locked, no captions) are skipped rather than aborting the run. New module `src/cli/transcript/youtube/sync.rs`.
- **`git branch create pr --from-commits`** ([#926](https://github.com/rust-works/omni-dev/pull/926), [#899](https://github.com/rust-works/omni-dev/issues/899)): A commit-message-driven PR generation path that sends only commit metadata to the AI instead of full diff content — faster, and it avoids large diff payloads when the branch's commits already convey intent. The new `--from-commits` flag on `CreatePrCommand` selects the path at runtime; `ClaudeClient::generate_pr_content_with_context_from_commits` builds a `RepositoryViewForAiFromCommits` payload (commit messages + detected type/scope only, no diff files read from disk), falls back to per-commit split dispatch when the combined payload exceeds the token budget, and bails with a clear error if even the single-commit slim view overflows. `try_full_diff_budget` is made generic over the view type so the diff-driven and commit-driven paths share one budget check.
- **`coverage diff` — diff/patch coverage analysis** ([#952](https://github.com/rust-works/omni-dev/pull/952), [#964](https://github.com/rust-works/omni-dev/pull/964), [#949](https://github.com/rust-works/omni-dev/issues/949)): A complete coverage-attribution pipeline that ingests per-line coverage reports (lcov, llvm-cov JSON, Cobertura XML — line coverage only, branch data ignored) plus a git diff and produces PR-attributable metrics: patch coverage (fraction of added lines covered), uncovered new lines (actionable `file:line` list), per-file project deltas, and indirect coverage flips on unchanged code. The `omni-dev coverage diff` command supports report-format auto-detection, base/head refs, baseline reports, output formats (markdown/YAML/JSON), coverage gates, and rendering options; the markdown renderer is a drop-in replacement for the legacy shell renderer used in PR comments. New CLI surface plus core analysis in `src/lib`.

- **`browser bridge harvest facebook posts`** ([#923](https://github.com/rust-works/omni-dev/issues/923)): A new `omni-dev browser bridge harvest <platform> <object>` command tree, with `harvest facebook posts` as the first target: it downloads the signed-in user's **own** Facebook timeline through the bridge, encapsulating the previously manual three-step recipe (issue [#922](https://github.com/rust-works/omni-dev/issues/922)) — harvest session tokens (`fb_dtsg`/`lsd`/`USER_ID` + the initial query's variables) from the `/me` shell, discover the pagination persisted-query `doc_id` from a cross-origin `static.xx.fbcdn.net` script bundle, then replay the refetch GraphQL query feeding `end_cursor` back until the timeline is exhausted. It reuses the existing `bridge request` dispatch/auth/cross-origin path via a shared `BridgeClient` (`src/browser/client.rs`) rather than opening its own socket — the cross-origin discovery step relies on `--allow-origin` ([#918](https://github.com/rust-works/omni-dev/issues/918)) and `--credentials omit` ([#920](https://github.com/rust-works/omni-dev/issues/920)). Each post is `{ id, creation_time, text, url, shared_link }`. Options: `--output` (file or stdout), `--format <jsonl|json>` (default `jsonl`; streamed and append-friendly), `--since <unix-ts|ISO8601>` (incremental archive), `--limit <N>` (sampling), `--resume <PATH>` (continue a run interrupted by a 504 / token rotation from its last saved cursor), plus the `--target` / `--token-file` / `--control-port` auth surface shared with `bridge request`. **Best-effort contract**: it drives reverse-engineered, undocumented Facebook internals, re-harvests every volatile `doc_id`/token/provider flag on each run (nothing hardcoded), fails with staged actionable errors naming the step that drifted (not panics), and only ever uses the connected tab's own session (your own account only). The stable alternative — Facebook's official "Download Your Information" export — is documented alongside. New modules `src/browser/harvest/` and `src/cli/browser/harvest.rs`; see [docs/browser-bridge.md](docs/browser-bridge.md).

### Changed
- **`coverage diff` scopes deltas to changed files by default** ([#973](https://github.com/rust-works/omni-dev/pull/973)): Coverage is measured by two independent instrumented runs (baseline vs head), so lines in files the PR never touched can flip covered↔uncovered purely from run-to-run variance, surfacing as phantom per-file deltas and "indirect coverage changes" for unrelated files. The project-delta and indirect-change sections now default to `DiffScope::DiffOnly` (report only files the diff touches). Real cross-file effects (e.g. a PR that removes a test, dropping a whole module's coverage) still surface via a magnitude-gated "notable unchanged" note (≥ 10 net covered lines), so only the small measurement-noise flips are hidden. Patch coverage and the total are unchanged. The new `--all-files` flag restores the unscoped (noisier) report.

### Removed
- **`git://repo/commits/{range}` MCP resource** ([#972](https://github.com/rust-works/omni-dev/pull/972), [#971](https://github.com/rust-works/omni-dev/issues/971)): Removes the `git://repo/commits/{range}` resource URI and all associated infrastructure from the MCP server and CLI, consolidating the resource layer on Atlassian and omni-dev resource types. The `ResourceUri::GitCommits` variant and its parsing/matching logic are gone, `read_resource()` loses its now-unused `repo_root` parameter, git-specific error messaging is dropped from `UriParseError::UnknownScheme`, and the server boundary in `src/mcp/server.rs` no longer resolves the CWD for git. The advertised resource count drops from 6 to 5.

### Fixed
- **Repo-relative operations anchored to the injected repo path** ([#970](https://github.com/rust-works/omni-dev/pull/970), [#967](https://github.com/rust-works/omni-dev/issues/967)): Resolves a category of bugs where commands resolved configuration, scratch directories, and git operations relative to the process CWD instead of the user-provided (or inferred) repository root — fixing inconsistencies when the MCP server or CLI injects an alternate repo path via `--repo`. Relative `.` defaults are converted to absolute paths via `current_dir()` in the twiddle and staged entry points; AI scratch-directory resolution is anchored to the opened repository's workdir in `CommitAnalysis`; new `_at` variants of the config-dir resolution functions anchor to an explicit repo path; and the preflight/remote checks (`check_ai_command_prerequisites`, `check_pr_command_prerequisites`, `check_github_cli`, `remote.rs::get_github_default_branch`) verify the injected repo via `check_git_repository_at` and `.current_dir(repo_root)` rather than the ambient CWD. The obsolete `CwdGuard` and no-arg repo opens are retired.
- **Voice model-installation test HOME race** ([#979](https://github.com/rust-works/omni-dev/pull/979), [#978](https://github.com/rust-works/omni-dev/issues/978)): Eliminates a `HOME` environment-variable mutation race that could make model-installation tests flaky when run in parallel.
- **Drift-report connection-test port race** ([#943](https://github.com/rust-works/omni-dev/pull/943), [#861](https://github.com/rust-works/omni-dev/issues/861)): Resolves a port race condition in the Atlassian drift-report connection test, and unifies the test environment mutex ([#953](https://github.com/rust-works/omni-dev/pull/953), [#950](https://github.com/rust-works/omni-dev/issues/950)) to fix flaky credential races.
- **Cobertura XML end-tag pattern for clippy 1.96**: Resolves a Cobertura XML end-tag match pattern flagged by clippy 1.96.
- **Exclude large assets from published crate**: The published crate no longer bundles large assets, keeping the crates.io package within size limits.

### Documentation
- **ADR-0037 — pure-C native ASR backends via Rust FFI** ([#938](https://github.com/rust-works/omni-dev/pull/938), [#933](https://github.com/rust-works/omni-dev/issues/933)): Records a coupled four-part decision enabling the native Voxtral backend without reopening the C++/cmake door ADR-0033 closed: a scoped relaxation permitting pure-C deps compiled with `cc`/`make` behind a Rust FFI boundary on `cfg(not(target_os = "windows"))` (C++ and cmake remain forbidden on all platforms), a third ADR-0035 integration shape (in-process native-C FFI behind a target-cfg Cargo dependency), and Windows graceful fallback (the Voxtral backend is cfg-excluded on Windows; the auto-upgrade hierarchy falls through to candle).
- **README leads with the demo** ([#945](https://github.com/rust-works/omni-dev/pull/945), [#833](https://github.com/rust-works/omni-dev/issues/833)): Moves the "See It In Action" demo and the 30-second command examples to the top of the README, immediately after the introduction, so new users see concrete value before the feature lists and installation instructions.
- **Browser bridge surfaced across docs and website** ([#925](https://github.com/rust-works/omni-dev/pull/925), [#927](https://github.com/rust-works/omni-dev/pull/927), [#929](https://github.com/rust-works/omni-dev/pull/929)): Surfaces the browser bridge in the README, `CLAUDE.md`, and ADR-0036, and adds browser-bridge, YouTube-transcript, and Claude-history demos plus a tutorial to the project website.

### CI/CD
- **PR coverage reporting pipeline** ([#948](https://github.com/rust-works/omni-dev/pull/948), [#946](https://github.com/rust-works/omni-dev/pull/946), [#944](https://github.com/rust-works/omni-dev/pull/944)): Renders the PR coverage comment via `omni-dev coverage diff` with a merge-base baseline strategy, per-file tracking, commit references, and artifact links; rebuilds the lcov baseline when the merge-base lacks a coverage file, and shares/reuses the coverage build env so the tool build is cached. Coverage tests were made deterministic ([#966](https://github.com/rust-works/omni-dev/pull/966)).
- **Haiku model added to commit-check workflow** ([#940](https://github.com/rust-works/omni-dev/pull/940), [#939](https://github.com/rust-works/omni-dev/issues/939)): Adds `claude-haiku-4-5` to the commit-check workflow.
- **Dependency bumps**: `tokio-tungstenite` 0.24→0.29 ([#959](https://github.com/rust-works/omni-dev/pull/959)), `cpal` 0.17→0.18 ([#960](https://github.com/rust-works/omni-dev/pull/960)), `rand` 0.8→0.9 ([#961](https://github.com/rust-works/omni-dev/pull/961)), `tract-onnx` 0.22.1→0.23.0 ([#962](https://github.com/rust-works/omni-dev/pull/962)); GitHub Actions `actions/upload-artifact` 4→7 ([#955](https://github.com/rust-works/omni-dev/pull/955)), `codecov/codecov-action` 6→7 ([#956](https://github.com/rust-works/omni-dev/pull/956)), `dawidd6/action-download-artifact` v6→v21 ([#954](https://github.com/rust-works/omni-dev/pull/954)), `marocchino/sticky-pull-request-comment` v2→v3 ([#957](https://github.com/rust-works/omni-dev/pull/957)).

## [0.28.0] - 2026-06-02

### Added
- **Browser WebSocket bridge** ([#902](https://github.com/rust-works/omni-dev/issues/902)): A new `omni-dev browser` command tree drives HTTP requests **through an authenticated browser tab**, borrowing the browser's SSO/OAuth/cookie session without exfiltrating credentials (a confused-deputy by design). `omni-dev browser bridge serve` runs a long-lived local process exposing two planes — a WebSocket plane the browser connects to via a pasted DevTools snippet (default `127.0.0.1:9999`), and an HTTP control plane the operator drives (default `127.0.0.1:9998`) — joined by an `id`-keyed correlator. Requests reach the page origin via a transparent proxy (any non-`/__bridge/` path), `GET /__bridge/status`, or `POST /__bridge/request`; `omni-dev browser bridge request --url … --method …` is a thin client that injects auth headers itself. The security model is core (see [ADR-0036](docs/adrs/adr-0036.md)): both planes are authenticated and default-closed, with a startup-generated session token never read from argv, control-plane `Host` allowlist + `Origin`/`Sec-Fetch-Site` rejection + mandatory `X-Omni-Bridge: 1` header + no CORS (anti-CSRF / anti-DNS-rebinding), WS token subprotocol, server-side relative-URL-only outbound scope (`--allow-origin` to widen), per-request `504` timeout, and bounded body size/concurrency. Both ports accept `0` to bind an OS-assigned random free port. New modules under `src/browser/` and `src/cli/browser/`; new dependencies `axum`, `tokio-tungstenite`, `rand`. Documented in [docs/browser-bridge.md](docs/browser-bridge.md).

- **Binary Response Bodies in the Browser Bridge** ([#906](https://github.com/rust-works/omni-dev/issues/906)): The browser bridge now round-trips **non-text** response bodies (images, gzipped/encoded blobs, file downloads) intact, closing the v1 gap where the snippet read every response via `resp.text()` and corrupted anything that wasn't UTF-8 text. The pasted snippet now inspects the response `Content-Type`: bodies that aren't `text/*`, JSON, XML, or JavaScript (and bodies with no type) are read via `arrayBuffer()`, base64-encoded, and tagged with `"encoding": "base64"` on the WebSocket frame; text/JSON bodies are unchanged and omit `encoding` (back-compat). The wire protocol gains an optional `encoding` field on `BrowserReply` / `ResponseEnvelope` (`src/browser/protocol.rs`) defaulting to plain text. The transparent proxy (`src/browser/bridge.rs`) base64-decodes before writing the HTTP response body so `curl` receives the raw bytes; `POST /__bridge/request` returns the envelope as-is for the caller to decode. `--max-body-bytes` is now accounted against the **decoded** size, and a malformed or unsupported encoding fails closed with `502`. Covered by new protocol unit tests, a snippet-content assertion, and three integration tests (proxy decodes to raw bytes, request endpoint returns the base64 envelope, oversized decoded body rejected). See [docs/browser-bridge.md](docs/browser-bridge.md) and [ADR-0036](docs/adrs/adr-0036.md).

- **Streaming / Chunked Response Bodies in the Browser Bridge** ([#907](https://github.com/rust-works/omni-dev/issues/907)): The browser bridge can now consume **streaming / long-lived** endpoints (Grafana Live, Server-Sent Events, chunked APIs) that never deliver a final buffered body, closing the v1 gap where every response was buffered under `--request-timeout` (so streaming endpoints timed out with `504` or returned nothing). Streaming is **caller opt-in** — `--stream` on `omni-dev browser bridge request`, `"stream": true` on `POST /__bridge/request`, or `?__stream=1` on the transparent proxy — and buffered (text + binary) behaviour is the unchanged default. The wire protocol (`src/browser/protocol.rs`) gains a `stream` flag on the `Command`, a server→browser `CancelCommand`, a `BrowserFrame` superset that classifies into head / `{seq,chunk}` / `done` / error `StreamItem`s, and an untagged `StreamLine` for NDJSON output. The pasted snippet reads `response.body.getReader()`, emits a head frame then one base64 chunk frame per read then a `done` frame, and cancels its reader on a `cancel` frame. The correlator (`src/browser/bridge.rs`) replaces the single `oneshot` per id with a per-id channel; the transparent proxy streams the decoded bytes as a native chunked HTTP body (curl-friendly) while `POST /__bridge/request` streams an `application/x-ndjson` body (head line, `{seq,chunk}` lines, `{done}` line). For streams, `--request-timeout` is reinterpreted as an **inter-chunk idle timeout** and `--max-body-bytes` as a **cumulative** ceiling; exceeding either limit — or the control-plane consumer disconnecting mid-stream — sends the browser a `cancel` frame so it stops its in-page reader. There is no socket-level backpressure to the browser's reader (memory is bounded by the cumulative cap, an accepted limitation). Covered by new protocol/correlator unit tests, a snippet-content assertion, and integration tests spanning the happy paths (proxy reassembles chunks to a raw body, request endpoint returns ordered NDJSON, `--stream` thin-client round-trip), the limit paths (cumulative cap aborts and cancels, idle timeout ends the stream), and the error/edge paths (no browser → 503, cross-origin → 403, invalid header → 400, error head / chunk-before-head → 502, start idle timeout → 504, undecodable chunk truncates, stray head ignored, unparseable frame survived). See [docs/browser-bridge.md](docs/browser-bridge.md) and [ADR-0036](docs/adrs/adr-0036.md).
- **Per-request `--allow-origin` on the browser bridge `request`** ([#918](https://github.com/rust-works/omni-dev/issues/918)): `omni-dev browser bridge request` gains a `--allow-origin <URL>` flag carrying a **request-scoped** outbound-origin override, so a single request can target a cross-origin URL **without** restarting `serve` and **without** breaking the extension's WebSocket connection. Previously cross-origin was gated only by the `serve --allow-origin` global, which feeds two checks at different times — `ws_origin_allowed` at WebSocket **upgrade** (the extension's `Origin`) and `validate_outbound_url` **per request** (the target URL's origin) — so widening it to permit a cross-origin target simultaneously caused the connection-time check to reject the page's own tab. The new override reaches **only** `validate_outbound_url`, never `ws_origin_allowed`: it takes precedence over the `serve` global for that request's outbound-URL check and leaves the WS upgrade gate untouched. The wire protocol (`src/browser/protocol.rs`) gains an optional `allow_origin` field on `ControlRequest` (`#[serde(default, skip_serializing_if = "Option::is_none")]`, so pre-feature clients are byte-identical on the wire); `dispatch` and `start_stream` (`src/browser/bridge.rs`) resolve the per-request value over the global and emit a WARN at the dispatch site whenever the override is exercised. The override is **per-request and explicit — never a default**: blast radius is bounded by the browser's own CORS (response body readable only if the target sends permissive CORS), and a token holder can already issue authenticated same-origin requests. Covered by protocol round-trip/back-compat tests and bridge unit tests (override wins over and falls back to the global; the WS gate is unaffected). See [docs/browser-bridge.md](docs/browser-bridge.md) and [ADR-0036](docs/adrs/adr-0036.md).
- **Multi-tab routing on the browser bridge via `X-Omni-Bridge-Target`** ([#908](https://github.com/rust-works/omni-dev/issues/908)): The bridge now accepts **multiple authenticated browser tabs concurrently**, replacing the single `Option<WsConn>` connection slot with a `conn_id`-keyed `HashMap<u64, WsConn>` registry (`src/browser/bridge.rs`); each tab authenticates independently via the token subprotocol and a new connection never evicts an existing one. A request selects its target tab by connection id or by unique `Origin` through the new `X-Omni-Bridge-Target` header (or the `target` body field on `POST /__bridge/request`, with the header taking precedence and being stripped before forwarding), surfaced on the thin client as `omni-dev browser bridge request --target <ID|ORIGIN>`. `resolve_target()` routes the request, rejecting ambiguous matches with `409` and unknown targets with `404`; with a single tab connected the target is optional (v1 back-compat) and only becomes required once more than one tab is connected. The protocol (`src/browser/protocol.rs`) gains a `TabInfo { id, origin }` struct and `StatusResponse` grows a `tabs: Vec<TabInfo>` listing every connected tab (`browser_origin` retained for back-compat). See [docs/browser-bridge.md](docs/browser-bridge.md) and [ADR-0036](docs/adrs/adr-0036.md).
- **Per-request `--credentials` on the browser bridge `request`** ([#920](https://github.com/rust-works/omni-dev/issues/920)): `omni-dev browser bridge request` gains a `--credentials <include|omit|same-origin>` flag controlling the browser `fetch()` credentials mode per request. A new `Credentials` enum (`ValueEnum` for clap, with an `as_fetch_value()` wire helper) threads an optional `credentials` field through `ControlRequest` and `Command` (`#[serde(skip_serializing_if)]`, so v1 clients stay byte-identical on the wire) into `dispatch` and `start_stream`; the `browser-bridge.js` snippet reads `cmd.credentials || 'include'` instead of the hard-coded `'include'`. The default stays `include` (send cookies/auth) so existing callers are unaffected. The primary use case is reading wildcard-CORS (`Access-Control-Allow-Origin: *`) cross-origin assets the browser refuses to expose to a credentialed request — pair `--credentials omit` with `--allow-origin`. Covered by unit tests for defaults, all three modes, invalid-value rejection, wire mapping, serialization round-trips, and field omission. See [docs/browser-bridge.md](docs/browser-bridge.md).
- **Project website** ([#875](https://github.com/rust-works/omni-dev/issues/875)): A complete [Zola](https://www.getzola.org/) static site for omni-dev, deployed to GitHub Pages at [omni-dev.john-ky.io](https://omni-dev.john-ky.io) via a new `website.yml` workflow that builds and publishes on pushes to `main` touching `website/**`. The landing page carries a four-tab demo (Jira, Confluence, Create PR, Twiddle commits) pairing reproducible asciicasts with screenshots of the rendered Atlassian artefacts, an expanded install section distinguishing the CLI-only and `--features mcp` paths, and a link to a self-contained [tutorial page](https://omni-dev.john-ky.io/tutorial.html) walking through the four core workflows and doubling as a JFM reference (nested expanders, layout sections, decision/task lists, directive tables, status pills, mentions, inline cards, and the `adf-unsupported` escape hatch). A new `website` project scope is registered for the promo site. Cast/demo sources live at [rust-works/omni-dev-demo](https://github.com/rust-works/omni-dev-demo).

### Changed
- **Browser bridge commands nested under `bridge` (breaking)** ([#916](https://github.com/rust-works/omni-dev/issues/916)): The `omni-dev browser` command tree now groups both the long-lived server and the thin client under a `bridge` parent: `omni-dev browser bridge` becomes `omni-dev browser bridge serve`, and `omni-dev browser request` becomes `omni-dev browser bridge request`. `bridge` was previously doing double duty as both the noun (the server) and the verb (run it) while `request` — which only makes sense against a running bridge — sat as its sibling; nesting `serve` and `request` under `bridge` makes the grouping read correctly and leaves room for future bridge-scoped subcommands (e.g. `status`, `stop`). In `src/cli/browser/bridge.rs` the former server struct is renamed `ServeCommand` and a new parent `BridgeCommand` holds a `Serve` / `Request` subcommand split; `src/cli/browser.rs` drops the top-level `Request` variant. This is a **breaking CLI change** — scripts invoking `omni-dev browser bridge` or `omni-dev browser request` must move to the nested paths. No deprecated aliases are kept: the feature shipped only in this `[Unreleased]` cycle, so a hard break is cleanest. Docs ([docs/browser-bridge.md](docs/browser-bridge.md), [ADR-0036](docs/adrs/adr-0036.md)), the startup banner, and the help snapshot are updated to match.
- **Actionable Over-`--max-body-bytes` Error + Canonical Pagination Loop** ([#909](https://github.com/rust-works/omni-dev/issues/909)): When a browser response exceeds `--max-body-bytes` (default 8 MiB), the bridge's `502` now names the observed decoded size and the configured limit and steers the operator toward both remedies — page the request to fetch less per call (narrow the time range or lower a `limit`/page size) or raise `--max-body-bytes` — instead of the bare `"browser response body exceeds --max-body-bytes"`. The message is produced in `dispatch()` (`src/browser/bridge.rs`) where the decoded length and configured cap are already in scope, and reaches the `omni-dev browser bridge request` user verbatim via its `bridge returned {status}: {text}` relay. The body cap and its `502` status are unchanged — this is ergonomics, not a loosening of the bound. [docs/browser-bridge.md](docs/browser-bridge.md) gains a runnable canonical drain loop (the Grafana/Loki `direction=backward` worked example), using the `request` thin client + `jq` to take the oldest returned timestamp as the next `end` and repeat until a page is empty, with a note tying a tripped over-limit error back to lowering the page size or raising the cap. No new CLI surface — pagination conventions are per-API, so the bridge stays a thin generic proxy and orchestration is left to the caller. Covered by extending the existing `oversized_decoded_binary_body_is_rejected` integration test to assert the new guidance text.

### Fixed
- **SSH Remotes Work for `git branch create pr`** ([#903](https://github.com/rust-works/omni-dev/issues/903)): Remote git operations no longer depend on libgit2's network transport, which the vendored libgit2 inside released binaries lacks a reliable SSH implementation for — causing `omni-dev git branch create pr` (and any remote operation) to fail on SSH remotes (`git@github.com:…`) with `unsupported URL protocol; class=Net (12)` even though the user's `git`, SSH keys, and network were all fine. Both affected operations in `src/git/repository.rs` now shell out to the user's `git` CLI via a new private `GitRepository::run_git` helper: `push_branch` runs `git push --set-upstream <remote> <branch>` (recording the tracking branch in the same step) and `branch_exists_on_remote` runs `git ls-remote --heads <remote> <branch>`, comparing the returned ref column exactly against `refs/heads/<branch>` to avoid the tail-glob false positives `git ls-remote`'s pattern matching would otherwise produce (e.g. `team/<branch>` matching `<branch>`). This delegates all URL-scheme and authentication handling (ssh-agent, `~/.ssh/config`, credential helpers) to `git`, matching the project's existing "favour the git CLI" guidance and the user's manual workflow. The now-obsolete libgit2 credential-callback machinery (`make_auth_callbacks`, `get_ssh_identity_for_host`, `format_auth_error`, `extract_hostname_from_git_url`, the `MAX_AUTH_ATTEMPTS` constant) and the `ssh2-config` dependency are removed. Covered by four new unit tests exercising push + presence/absence detection and the exact-ref-match guard against a local bare remote.

## [0.27.0] - 2026-05-23

### Added
- **JFM→ADF Auto-Conversion for Rich-Text Custom Fields** ([#866](https://github.com/rust-works/omni-dev/issues/866)): String values supplied for rich-text/textarea JIRA custom fields are now auto-converted from JFM markdown to ADF — both via the MCP `jira_write` `fields` escape hatch and via the CLI's `--set-field NAME=VALUE`. Callers no longer need to hand-craft `{"version":1,"type":"doc",…}` payloads for fields like Acceptance Criteria (`customfield_19300`); a plain `"- bullet\n- bullet"` is converted using the same pipeline that already serves `content`/description and body sections. An empty string (or YAML null on the CLI side) clears the field. JSON object values are passed through unchanged, preserving the raw-ADF path for callers who need it. Implemented as a new `convert_textarea_string_values` helper in `src/atlassian/custom_fields.rs` (shared with a relaxed `resolve_custom_fields` that now accepts string scalars for rich-text fields) and wired into `run_jira_write` in `src/mcp/jira_core_tools.rs` with a lazy prefilter — editmeta is only fetched when at least one `fields` value is a string. Per-issue editmeta is cached via a new `editmeta` slot on `CatalogueCache` (`src/mcp/catalogue_cache.rs`) with a 60-second TTL (separate `EDITMETA_TTL` constant) and the same single-flight semantics as the other catalogue slots. Metadata-lookup failures fall back to passthrough — the API surfaces its own error rather than failing the write. Covered by 7 unit tests on the conversion helper, 7 wiremock integration tests on `run_jira_write` (string→ADF, object passthrough, empty→null, non-textarea passthrough, editmeta-failure passthrough, invalid-nesting short-circuit, unknown-field passthrough), 3 CLI tests on `--set-field` for textareas, and 5 editmeta-cache unit tests.
- **Actionable Error for JIRA ADF-Required Fields** ([#867](https://github.com/rust-works/omni-dev/issues/867)): When `jira_write` submits a plain string to a JIRA rich-text custom field, JIRA returns HTTP 400 with `errors.<field_id>: "Operation value must be an Atlassian Document (see the Atlassian Document Format)"`. The error is now enriched with the offending field ID(s), a `To fix:` hint pointing at JFM markdown and the `omni-dev://specs/jfm` resource, and the verbatim original API message — instead of being relayed as an opaque body. Implemented via a new `jira_write_error` helper in `src/atlassian/client.rs` (paralleling `confluence_write_error` in `src/atlassian/confluence_api.rs`) and a new `AtlassianError::JiraAdfFieldRequired { fields, original_message, body }` variant in `src/atlassian/error.rs`. Multi-field 400s are joined into a single header line. Non-ADF 400s and all other status codes fall back to the existing `ApiRequestFailed` format. Scoped to the JIRA update path (`update_issue_with_custom_fields`); the create path is intentionally out of scope. Covered by three Display unit tests in `error.rs` and two integration tests (`run_jira_write_enriches_adf_required_error`, `run_jira_write_falls_back_for_non_adf_400`) in `src/mcp/jira_core_tools.rs`.
- **Confluence Inline (Anchored) Comments** ([#830](https://github.com/rust-works/omni-dev/issues/830)): omni-dev's Confluence comment surface now distinguishes footer comments (page-level) from inline comments (anchored to a text selection), closing the gap with the official Atlassian MCP server. Three additions: (1) new `omni-dev atlassian confluence comment add-inline <PAGE-ID> [FILE] --anchor-text "..." [--match-index N]` CLI subcommand and matching `confluence_comment_add_inline` MCP tool create inline comments by posting to `/wiki/api/v2/inline-comments` with the `inlineCommentProperties` payload Confluence requires (`textSelection`, `textSelectionMatchCount`, `textSelectionMatchIndex`); the index/count are resolved automatically by fetching the page via the existing `get_content` and counting occurrences of `--anchor-text` in the body (flattened to plain text via the new `adf_to_plain_text` helper in `src/atlassian/convert.rs`), with explicit errors for "not found", "ambiguous (specify --match-index 1..=N)", and out-of-range overrides. (2) `confluence comment list` (CLI) and `confluence_comment_list` (MCP) gain a `--kind {footer,inline,all}` filter defaulting to `all`, which issues both `/footer-comments` and `/inline-comments` v2 GETs and merges the results sorted by creation time. (3) New `omni-dev atlassian confluence comment replies <COMMENT-ID> --kind {footer,inline}` CLI subcommand and `confluence_comment_replies` MCP tool fetch a comment's reply thread via `GET /wiki/api/v2/{footer|inline}-comments/{id}/children` — the caller must commit to a kind because Confluence stores replies on kind-specific endpoints. `ConfluenceComment` gains a `kind: CommentKind` field (serialised as lower-case `"footer"` / `"inline"`) so a merged listing identifies each row; the table printer adds the kind to each comment's header line. Footer behaviour is unchanged — `confluence comment add` still posts a footer comment, the v2 footer endpoints still back it, and existing scripts that never pass `--kind` to `list` will see inline comments included by default (the intentional behaviour change). Implemented across `src/atlassian/confluence_api.rs` (new `CommentKind` enum, `InlineAnchor` struct, `get_page_inline_comments`, `get_comment_replies`, `add_inline_page_comment`, `resolve_anchor`, refactored pagination into a shared `fetch_comments_paginated` helper), `src/atlassian/convert.rs` (`adf_to_plain_text`), `src/cli/atlassian/confluence/comment.rs` (new `AddInlineCommand`, `RepliesCommand`, shared `parse_comment_input` helper), and `src/mcp/confluence_tools.rs` (extended list params, new add-inline / replies tool handlers, registration). Covered by anchor-resolution unit tests (0/1/N-match branches, `--match-index` bounds), wiremock integration tests for each new HTTP path (`POST /inline-comments`, `GET /{kind}-comments/{id}/children`, `list --kind all` issuing both GETs), and the registration test in `tool_router_registers_all_confluence_tools`.
- **`confluence space pages` Command + `confluence_space_pages` MCP Tool** ([#829](https://github.com/rust-works/omni-dev/issues/829)): New `omni-dev atlassian confluence space pages <KEY>` CLI subcommand and matching `confluence_space_pages` MCP tool enumerate pages within a Confluence space against `GET /wiki/api/v2/spaces/{id}/pages`. The space key is resolved to a space ID via the existing `resolve_space_id` helper, so callers pass the human-readable key (e.g. `ENG`) rather than an opaque numeric ID. Optional `--status` (common values: `current`, `archived`, `draft`, `trashed`) and `--sort` (common values: `id`, `-id`, `title`, `-title`, `created-date`, `-created-date`, `modified-date`, `-modified-date`) are passed through to the v2 API verbatim — no client-side allow-list, matching the `confluence_space_list` philosophy. Output records are summary-only — `id`, `title`, `status`, `parentId`, `authorId`, `createdAt` — with no page body, so listings of large spaces stay bounded in size. Pagination is explicitly cursor-driven (`--cursor` / `--limit`, default 25): the MCP tool returns a `next_cursor` callers thread back to fetch the next page, mirroring `list_spaces` rather than the auto-drained pattern in `get_space_root_pages`, because spaces with thousands of pages must not buffer entirely in memory on the MCP path. The `body-format` filter suggested in the issue is intentionally omitted — we never emit a body, so accepting it would only waste server bytes. Pairs with #828: agents can now walk from "what spaces exist?" → "what pages are in this space?" without prior knowledge of keys or IDs.
- **`confluence space list` Command + `confluence_space_list` MCP Tool** ([#828](https://github.com/rust-works/omni-dev/issues/828)): New `omni-dev atlassian confluence space list` CLI subcommand and matching `confluence_space_list` MCP tool list Confluence spaces against `GET /wiki/api/v2/spaces`. Filters `--keys <K1,K2>` (comma-separated), `--type <TYPE>`, and `--status <STATUS>` combine as AND on the wire; `--cursor` + `--limit` (default 25) page explicitly — the MCP tool returns a `next_cursor` callers pass back to fetch the next page so large space inventories never have to buffer in memory. Output fields are `id`, `key`, `name`, `type`, `status`, `homepageId`. `--type` / `--status` are passed through verbatim rather than restricted to a clap enum; Atlassian's `SpaceTypeEnum` includes template-derived values (`onboarding`, `xflow_sample_space`, `system`, `app`, …) that extend the four documented types, so a strict client-side allow-list would silently drift from reality. Invalid values surface as a clear `400 INVALID_REQUEST_PARAMETER` from Atlassian listing the current accepted set. Internally, the existing `resolve_space_id` helper was refactored to delegate to the new `list_spaces` method against the same endpoint, removing the duplicated query-building path.
- **`voice enroll` Subcommand** ([#805](https://github.com/rust-works/omni-dev/issues/805)): New CLI command at `src/cli/voice/enroll.rs` captures a microphone sample, computes a wespeaker speaker embedding, and persists it to `~/.omni-dev/voice/speakers/<name>.json`. Reuses `voice::capture::run_capture` in-process exactly as `voice capture` does — no shell-out, no duplicated pipeline. `--name <name>` chooses the JSON filename stem (default `default`); `--idle-after <secs>` stops on trailing silence (default 2 s); `--max-secs <secs>` enforces a hard upper bound (default 30 s, `0` disables); `--device <name>` selects the audio input; `--speaker-model <path>` overrides the wespeaker ONNX location; `--force` overwrites an existing enrolment. The deadline cap is enforced by a background watchdog thread that flips the same stop signal Ctrl-C uses, so capture terminates on the first of: idle silence, deadline reached, or user interrupt. The captured WAV is staged at `~/.omni-dev/voice/captures/.enroll-<UTC-timestamp>.wav` and deleted on success. The persisted `EnrolledSpeaker` JSON shape (`name`, `model`, `dim`, `vector`, `samples_used`, `enrolled_at`) matches the original #805 spec and is forward-compatible with extra fields via serde defaults. Covered by 9 unit tests for clap parsing, the install-hint error path, and WAV-format validation; the end-to-end embedding path is validated by the `#[ignore]`-gated `voice_enroll_speaker_test`.
- **`voice transcribe --speaker <name>` Filter** ([#805](https://github.com/rust-works/omni-dev/issues/805)): `omni-dev voice transcribe` now accepts `--speaker <name>` plus `--threshold <f32>` (default `0.5` — calibrated against `tests/fixtures/voice/two_speakers.wav` where within-speaker mean cosine is ≈ 0.91 and cross-speaker mean ≈ 0.07, leaving ~0.4 margin on both sides) and `--speaker-model <path>`. When `--speaker` is set, the transcribe command loads the enrolled embedding from `~/.omni-dev/voice/speakers/<name>.json`, re-reads the source WAV in parallel with the transcriber, and for each `Final` event embeds the corresponding PCM window via `WespeakerEmbedder` and drops the event when cosine similarity to the enrolled vector falls below the threshold. Surviving events are tagged with `speaker: Some(name)`. Windows shorter than 0.5 s (the minimum for stable cepstral-mean normalisation) are dropped conservatively rather than risk false positives. `Partial` and `Endpoint` events pass through untouched; the `Transcriber` trait is **not** extended — the filter is a separate composable layer over the existing event stream so future backends with native diarisation can bypass it. Implementation in `src/cli/voice/transcribe.rs::SpeakerFilter` (~140 LOC).
- **`voice install-model --variant <variant>` Flag** ([#805](https://github.com/rust-works/omni-dev/issues/805)): The install-model subcommand now accepts `--variant {whisper-tiny.en, speaker-wespeaker-en}` (default `whisper-tiny.en` for backwards compatibility — bare `voice install-model` continues to install the ASR model). Selecting `speaker-wespeaker-en` downloads [`wespeaker_en_voxceleb_resnet34_LM.onnx`](https://github.com/k2-fsa/sherpa-onnx/releases/download/speaker-recongition-models/wespeaker_en_voxceleb_resnet34_LM.onnx) (26.5 MB, SHA-256 `e9848563da86f263117134dfd7ad63c92355b37de492b55e325400c9d9c39012`) from the sherpa-onnx `speaker-recongition-models` release into `~/.omni-dev/voice/models/wespeaker-en-voxceleb-resnet34-LM/` via a direct `ureq` GET with SHA-256 verification and the same `.part` + atomic rename install pattern as the Whisper path. Both variants share the same `--dest`, `--force`, and idempotency semantics; the download transport bifurcates on the `ModelSource` carried by each variant's `ModelSpec`.
- **`WespeakerEmbedder` Speaker-Embedding Runtime** ([#805](https://github.com/rust-works/omni-dev/issues/805) / [ADR-0034](docs/adrs/adr-0034.md)): New `src/voice/speaker.rs` wraps `tract-onnx` v0.21 loading the wespeaker `voxceleb_resnet34_LM` ONNX model. `WespeakerEmbedder::new(path)` loads + optimises the ONNX graph and builds the FBANK mel filterbank + FFT plan once; `WespeakerEmbedder::embed(&[i16]) -> Vec<f32>` takes 16 kHz mono signed-PCM samples and returns a 256-dimensional L2-normalised embedding suitable for cosine similarity. Refuses windows shorter than 0.5 s (`MIN_EMBED_SAMPLES`) for which cepstral-mean normalisation would be unstable. `Send + Sync` because `SimplePlan::run` takes `&self`, allowing one embedder to be shared across worker threads without internal mutexing. Companion `EnrolledSpeaker` struct in the same module handles atomic load/save against the JSON storage convention, with `dim`/`vector.len()` parity verified on load. Free functions `cosine(&[f32], &[f32]) -> f32` and `l2_normalise(Vec<f32>) -> Vec<f32>` round out the public surface. Runtime choice documented in [SPIKE.md on `issue-805-spike-tract-speaker`] and [ADR-0034](docs/adrs/adr-0034.md). The `tract-linalg` build-dep on `cc` (compiling platform-conditional SIMD assembly stubs, not C++) is the same C-toolchain footprint already required by [ADR-0033](docs/adrs/adr-0033.md)'s `onig_sys`/`ring`.
- **`voice features` Kaldi-Style FBANK Module** ([#805](https://github.com/rust-works/omni-dev/issues/805)): New `src/voice/features.rs` implements 80-dim Kaldi-style FBANK feature extraction matching sherpa-onnx's `kaldi-native-fbank` defaults for the wespeaker recipe — 16 kHz mono, 25 ms / 10 ms framing, pre-emphasis 0.97, Hamming window, mel scale `1127·ln(1 + f/700)` (Slaney/Kaldi), low-freq 20 Hz / high-freq 8000 Hz, 512-point FFT (smallest power of two ≥ 400-sample window), log applied to mel energies with `1e-10` numerical floor, cepstral mean normalisation across the supplied window's frames. Pure-Rust DSP — only non-`std` dep is `rustfft` for the FFT. Public surface: `compute_fbank(pcm: &[f32], filters: &[Vec<f32>]) -> Result<Vec<Vec<f32>>>` and `build_mel_filterbank(num_bins, fft_size, sample_rate) -> Result<Vec<Vec<f32>>>`, plus the parameter constants (`SAMPLE_RATE`, `FRAME_LENGTH_MS`, `FRAME_SHIFT_MS`, `NUM_MEL_BINS`, `FFT_SIZE`, `LOW_FREQ_HZ`, `HIGH_FREQ_HZ`, `PREEMPHASIS`). Covered by 8 unit tests including filterbank dimensionality, non-negativity, monotonic centre-frequency ordering, frame-count formula correctness, CMN-zeros-mean-per-bin invariant, and `hz<->mel` round-trip stability.
- **`omni_dev::voice::paths` Helpers** ([#805](https://github.com/rust-works/omni-dev/issues/805)): New `src/voice/paths.rs` centralises `~/.omni-dev/voice/...` layout so capture, transcribe, install-model, and enroll all derive paths from one source of truth. Public functions: `omni_dev_voice_root() -> Result<PathBuf>`, `captures_dir() -> Result<PathBuf>`, `speakers_dir() -> Result<PathBuf>`, `speaker_file(name: &str) -> Result<PathBuf>`. `speaker_file` rejects empty names, path separators (`/` or `\`), null bytes, and `.`/`..` as a defence-in-depth guard against accidental directory traversal. `src/cli/voice/capture.rs::default_output_path` now routes through `captures_dir()` rather than open-coding the home-dir join.
- **`#[ignore]`-Gated `voice_enroll_speaker_test` Integration Test** ([#805](https://github.com/rust-works/omni-dev/issues/805)): New `tests/voice_enroll_speaker_test.rs` exercises `WespeakerEmbedder` end-to-end against the committed `tests/fixtures/voice/two_speakers.wav` fixture (two LibriVox readers, 24.5 s, public-domain). Asserts the same spike-aligned separability gates that ADR-0034 was accepted on: within-speaker cosine ≥ 0.70 (spike measured ≈ 0.91) and all six cross-speaker cosines ≤ 0.40 (spike measured ≈ 0.07). A second test verifies the default 0.5 threshold correctly accepts a same-speaker query window and rejects both other-speaker windows — the runtime contract `voice transcribe --speaker` depends on. Both tests are `#[ignore]`-by-default because they need the wespeaker ONNX file on disk; run locally with `omni-dev voice install-model --variant speaker-wespeaker-en` followed by `cargo test --test voice_enroll_speaker_test -- --ignored`, or point at a pre-staged install via `OMNI_DEV_VOICE_SPEAKER_MODEL`.
- **`whisper-candle` ASR Backend** ([#802](https://github.com/rust-works/omni-dev/issues/802)): New pure-Rust Whisper backend at `src/voice/backends/candle.rs` selected via `--backend whisper-candle` (or `OMNI_DEV_VOICE_BACKEND=whisper-candle`) on `omni-dev voice transcribe`. Built on `candle-core` / `candle-nn` / `candle-transformers` 0.10.x with `openai/whisper-tiny.en` (revision `refs/pr/15`), greedy decode (temperature 0), English-only, no timestamps; RTF ≈ 0.10 on CPU per the spike in #813. Emits one `TranscriptEvent::Final` per decoded mel segment (`event_id = Ulid::new()`, `confidence` from per-token average log-probability mapped via `avg_logprob.exp().clamp(0.0, 1.0)`, `words = None`, `speaker = None`, `revisable = false`) followed by a terminal `Endpoint { kind: StreamEnd }`. The `m::model::Whisper` handle is held behind a `Mutex` because its encoder and decoder methods take `&mut self` and the decoder owns a per-segment KV cache that concurrent calls would corrupt. Mel-filter coefficients (80-bin, 64 KB) are vendored as `src/voice/backends/candle_melfilters.bytes` and embedded via `include_bytes!`. Model files are loaded from a directory resolved with priority `--model <path>` → `OMNI_DEV_VOICE_WHISPER_MODEL` → `~/.omni-dev/voice/models/whisper-tiny.en/`; missing-model errors carry an actionable install hint pointing at `voice install-model`. Default backend remains `mock` until the model storage convention has been through at least one release cycle. Recorded in [ADR-0033](docs/adrs/adr-0033.md). Covered by unit tests for `Send + Sync`, mel-filter shape, the missing-model error path, factory dispatch, and a `#[ignore]`-by-default end-to-end test at `tests/voice_transcribe_candle_test.rs` that exercises the backend against the committed `short_en.wav` fixture with case-insensitive substring matching on the seven content words from the #813 baseline.
- **`omni-dev voice install-model` Subcommand** ([#802](https://github.com/rust-works/omni-dev/issues/802)): New one-time CLI command at `src/cli/voice/install_model.rs` downloads the Whisper tiny.en `config.json`, `tokenizer.json`, and `model.safetensors` from `openai/whisper-tiny.en` (revision `refs/pr/15`) into `~/.omni-dev/voice/models/whisper-tiny.en/` via `hf-hub`. `--dest <path>` overrides the install directory; `--force` re-downloads when files are already present. Idempotent — skips when all three required files exist and are non-empty. Atomic — each file is staged as a `.<name>.part` sibling and renamed into place, so partial downloads never leave half-written model artifacts at the destination. Progress lines (`fetching <file>... done (N bytes in M.Ms)`) are emitted to stderr so stdout stays free for future machine-readable additions. Bumps the model-download cost to install time rather than transcribe time so network failures surface explicitly when the user opts in to installing rather than silently on first `voice transcribe`.
- **`--model <path>` Flag on `voice transcribe`** ([#802](https://github.com/rust-works/omni-dev/issues/802)): `omni-dev voice transcribe` now accepts `--model <path>` to override the backend's model directory (for `whisper-candle`, this overrides `OMNI_DEV_VOICE_WHISPER_MODEL` and the default at `~/.omni-dev/voice/models/whisper-tiny.en/`). The flag is plumbed through to `VoiceOpts.model` (previously hardcoded `None`) and is ignored by the `mock` backend. The shared `src/voice/models.rs` helper centralises three-tier path resolution so the backend's load path and `install-model`'s download path can't diverge.

### Changed
- **`jira_field_list` Exposes `schema.custom` and Maps Rich-Text Fields** ([#865](https://github.com/rust-works/omni-dev/issues/865)): The `omni-dev atlassian jira field list` CLI and matching `jira_field_list` MCP tool now report `schema_type: "richtext"` for ADF-required custom fields (e.g. *Acceptance Criteria*) and expose the raw plugin URI from `schema.custom` as a new `schema_custom` field. Callers — including AI agents — can detect rich-text fields up front instead of discovering them via a `400 "Operation value must be an Atlassian Document"` on the first write attempt. Previously the JIRA API's `schema.custom` value (e.g. `com.atlassian.jira.plugin.system.customfieldtypes:textarea`) was deserialised and discarded; `schema_type` reported `"string"` indistinguishably from plain string fields. Implementation extends `JiraFieldSchema` to deserialise `custom`, adds `JiraField.schema_custom: Option<String>` (`#[serde(skip_serializing_if = "Option::is_none")]` so non-custom fields stay shape-compatible), and routes `(schema.type, schema.custom)` through a new file-private `map_schema_type` helper in `src/atlassian/client.rs::get_fields` that returns `"richtext"` when `schema.custom == "...customfieldtypes:textarea"` and passes everything else through unchanged. The textarea URI is consolidated into a `pub(crate) const TEXTAREA_CUSTOM_TYPE` shared with the existing `EditMetaField::is_adf_rich_text` discriminator. The CLI table's TYPE column picks up the new value transparently; the MCP tool description is extended to document the new contract for agent enumeration. Behaviour change for consumers that compared `schema_type` against the literal `"string"` for textarea custom fields — they now see `"richtext"`, which is more accurate.
- **`src/voice/models.rs` Generalised to `ModelSpec` Shape** ([#805](https://github.com/rust-works/omni-dev/issues/805)): The Whisper-only constants from #802 are now exposed through a `ModelSpec` struct (with a sibling `ModelSource` enum carrying either `HfHub { repo_id, revision }` or `HttpReleaseAsset { url, sha256, bytes }`) so the same registry hosts both ASR (Whisper, HfHub-sourced) and speaker (wespeaker, GitHub-release-asset-sourced) models. Two canonical instances are registered: `WHISPER_TINY_EN` and `SPEAKER_WESPEAKER_EN`. `ModelSpec` exposes `default_dir()`, `resolve_dir(override)`, `required_files_in(dir)`, and `ensure_present(dir)` as methods; the last carries the variant-specific install hint baked into the spec (`run \`omni-dev voice install-model\` or pass --model <path>` for Whisper, `run \`omni-dev voice install-model --variant speaker-wespeaker-en\` or pass --speaker-model <path>` for wespeaker). All pre-existing Whisper helpers (`MODEL_ID`, `REVISION`, `REQUIRED_FILES`, `DEFAULT_VARIANT_DIR`, `default_whisper_model_dir`, `resolve_whisper_model_dir`, `ensure_model_present`, `required_files_in`) are retained as thin shims over `WHISPER_TINY_EN`, so existing callers in `src/voice/backends/candle.rs`, `src/voice/factory.rs`, and `src/cli/voice/install_model.rs` keep compiling with no signature change.

### CI/CD
- **Release Workflow Ships `omni-dev-mcp` Binary** ([#874](https://github.com/rust-works/omni-dev/pull/874)): The `release.yml` workflow now builds with `--features mcp --bin omni-dev --bin omni-dev-mcp` and includes both binaries in the per-platform release archives (Linux/macOS/Windows). The MCP server binary was previously buildable from source but missing from GitHub release assets. Both binaries are stripped on linux and macos.

## [0.26.0] - 2026-05-11

### Added
- **ADF Validator: Per-Context Mark Allow-Lists & Per-Mark Attribute Schemas** ([#733](https://github.com/rust-works/omni-dev/issues/733)): `validate_document` now flags two more violation classes — `DisallowedMark { mark_type, parent_type, inline_index, path }` (e.g. a `code` mark on text inside a `heading`, a `border` block mark on a `paragraph`) and `InvalidMarkAttr { mark_type, attr_name, problem, inline_index, path }` (e.g. `link.href: "not a url"`, `subsup.type: "side"`, `border.size: 5`). Inline-mark allow-lists are keyed by the *parent* container (since `code` is fine on `paragraph` text but not on `heading` text); block-mark allow-lists are keyed by the node's *own* type (since `border`/`backgroundColor` are valid on `tableCell` and `tableHeader` only, while `alignment`/`indentation` are valid on `paragraph` and `heading` only). Per-mark attribute schemas reuse the `AttrSchema` / `AttrType` / `AttrProblem` machinery from the attribute slice — `link` requires an absolute-URL `href`, `subsup` requires `type ∈ {sub, sup}`, `alignment` requires `align ∈ {start, end, center, right, left}`, `indentation` requires `level ∈ 1..=6`, `breakout` requires `mode ∈ {wide, full-width}`, `border` requires hex `color` and `size ∈ 1..=3`, `annotation` requires `id` and `annotationType`. `unsupportedMark` and `unsupportedNodeAttribute` are accepted under any context as round-trip preservation wrappers (matching `unsupportedBlock`/`unsupportedInline`'s treatment in #717). Per-mark and per-context hints are added to `adf_hints` (`headings cannot carry the code mark — use a paragraph if you need code styling`, `link.href must be an absolute URL`, etc.). New module: `src/atlassian/adf_mark_schema.rs`. Modified: `src/atlassian/adf_schema.rs` (new `DisallowedMark` / `InvalidMarkAttr` enum variants + walker calls `validate_marks` for every visited child), `src/atlassian/adf_attr_schema.rs` (`check_value` made `pub` so the mark module can reuse type-checking), `src/atlassian/error.rs` (Display arms for the new variants), `src/atlassian/adf_hints.rs` (per-mark hint tables). Covered by 22 unit tests in `adf_mark_schema` (allow-list per parent context, block-mark contexts, mark-attr happy/sad path per mark type), 2 new integration tests in `tests/adf_schema_test.rs` exercising `code`-on-heading and malformed `link.href` through `validate_document`, and a new `print_dry_run_fails_on_disallowed_mark` end-to-end test in the CLI helper.
- **ADF Validator: Per-Node Attribute Schemas** ([#733](https://github.com/rust-works/omni-dev/issues/733)): `validate_document` now validates each node's `attrs` against a per-node-type schema and reports two new violation variants — `MissingAttr { node_type, attr_name, path }` and `InvalidAttr { node_type, attr_name, problem, path }`. The `problem` field carries an `AttrProblem` enum (`NotInEnum { allowed, actual }`, `OutOfRange { lo, hi, actual }`, `OutOfRangeF { … }`, `WrongType { expected }`, `BadFormat { reason }`) so display wording is centralised. Schemas are encoded in a new `src/atlassian/adf_attr_schema.rs` module for the user-visible mistakes called out in the issue (`panel.panelType ∈ {info, note, warning, success, error, custom}`, `heading.level ∈ 1..=6`) and for the rest of the typed attrs across the snapshot: `media.type` (file/link/external), `mediaSingle.layout`, `taskItem`/`decisionItem` `state` and `localId`, `taskList`/`decisionList.localId`, `status.color` and `text`, `extension.extensionType`/`extensionKey`, `mention.id`, `date.timestamp`, `emoji.shortName`, `embedCard.url` (URL-parseable), `expand.title`, `nestedExpand.title`, `orderedList.order` (positive integer), `layoutColumn.width` (0–100), `codeBlock.language`, plus more. Forward-compatibility preserved per ADR-0023's permissive philosophy: unknown node types and unknown attribute names trigger no violation; explicit `null` is treated as absent. Per-attribute hints are added to the `adf_hints` module (`set panelType to one of: info, note, warning, success, error, custom`, `heading.level must be an integer in 1..=6`, etc.). Reuses the shared `AttrSchema` / `AttrType` machinery — designed to be reused by mark-attribute validation in the third sub-PR. Modified: `src/atlassian/adf_schema.rs` (new `MissingAttr` / `InvalidAttr` enum variants + walker calls `validate_attrs` for every visited child), `src/atlassian/error.rs` (Display arms for the new variants), `src/atlassian/adf_hints.rs` (per-attr hint table), `src/atlassian/adf_attr_schema.rs` (new module). Covered by 21 unit tests in `adf_attr_schema`, 9 integration tests in `tests/adf_schema_test.rs`, and a new `print_dry_run_fails_on_invalid_attribute` end-to-end test exercising `panelType: "purple"` through the CLI helper.
- **ADF Validator: Quantifier / Arity Checks** ([#733](https://github.com/rust-works/omni-dev/issues/733)): `validate_document` now flags arity violations alongside the existing disallowed-child checks. `mediaSingle` with two `media` children, an empty `bulletList`, an empty `mediaGroup`, an empty `tableRow`, and a `layoutSection` with the wrong number of columns (must be 2 or 3) are all reported as `AdfSchemaViolation::Arity { parent_type, atoms, expected, actual, path }`. The `expected` field carries a new `Quantifier` enum (`ZeroOrOne`, `ZeroOrMore`, `OneOrMore`, `Exactly(n)`, `Range(min, max)`); `path` points at the parent whose count is wrong (whereas `DisallowedChild`'s `path` points at the child). Disallowed children do **not** count toward parent arity — an empty panel containing only an `expand` produces both a `DisallowedChild` (for the expand) and an `Arity` (for the panel needing ≥1 valid child). `unsupportedBlock` / `unsupportedInline` continue to be accepted under any known parent **and** count toward the parent's current term arity, so round-tripped documents carrying preservation wrappers still satisfy `+` / `Exactly(n)` requirements. Two upstream rules are intentionally relaxed for compatibility with real-world inputs: `doc` is `block*` instead of `block+` (so `AdfDocument::new()` — the canonical empty value for missing JIRA descriptions — does not trigger an arity violation) and `tableCell` / `tableHeader` are `block*` instead of `block+` (visibly-empty cells in real Confluence tables are common and accepted by the renderer). The `confluence write --dry-run` output and the HTTP-500 diagnosis in `confluence_write_error` both pick up the new variant via the existing `Display` impl, with arity-specific wording (`'bulletList' must contain at least one 'listItem' (found 0)`). Per-arity hints (empty list, two-media mediaSingle, layoutSection out-of-range, …) are added to the new `adf_hints` module. New module: `src/atlassian/adf_hints.rs`. Modified: `src/atlassian/adf_schema.rs` (full content-model rewrite + new `Quantifier` / `ContentTerm` types + new walker), `src/atlassian/error.rs` (Display switches per variant), `src/atlassian/confluence_api.rs` (uses `adf_hints::hint_for(&violation)`). Covered by 41 unit tests in `adf_schema` plus 6 unit tests in `adf_hints` plus 6 integration tests in `tests/adf_schema_test.rs`.

### Changed
- **`AdfSchemaViolation` Struct → Enum (Breaking)** ([#733](https://github.com/rust-works/omni-dev/issues/733)): The single concrete `AdfSchemaViolation` struct from #717 is now an enum so each new class of violation lands as its own variant (callers can opt in to strictness). The first variant `DisallowedChild { child_type, parent_type, path }` carries the existing fields verbatim — same `Display` wording, same path semantics — so the only migration is updating constructions and pattern-matches in callers. The second variant `Arity { … }` is added in the same release for the quantifier slice. Public `validate_document(&AdfDocument) -> Vec<AdfSchemaViolation>` signature is unchanged. The `hint_for` helper formerly inside `confluence_api.rs` moves to the new dedicated `src/atlassian/adf_hints.rs` module and now takes `&AdfSchemaViolation` so it can dispatch per variant. New `pub fn content_model(parent: &str) -> Option<&'static [ContentTerm]>` exposes the full per-parent term sequence (with quantifiers) for callers that want richer schema introspection than `allowed_children` provides.
- **OpenAI/Ollama Backends Enforce JSON Schema Response Format** ([#702](https://github.com/rust-works/omni-dev/issues/702)): `OpenAiAiClient` (used by `USE_OLLAMA=true`, `USE_OPENAI=true`, and OpenAI-compatible servers like LM Studio) now honours `RequestOptions::response_schema` by populating `response_format: {type: "json_schema", json_schema: {name, strict, schema}}` on `/v1/chat/completions`. Previously the option was silently dropped and the backend reported `supports_response_schema = false`, so structured calls (twiddle, check, PR generation) received hard-validated JSON only on `claude-cli` and prose-instructed YAML everywhere else — the most visible symptom being invented commit scopes that were not in `.omni-dev/scopes.yaml`. Capability flips to `true` on both OpenAI and Ollama paths; the existing capability-driven dispatch in `client.rs::send_with_optional_schema` and the `ResponseFormat::JsonSchema` system-prompt swap pick up the change with no further wiring. Honoured by OpenAI ≥2024-08-06 (gpt-4o, gpt-5, o-series), LM Studio (decoder-constrained), and Ollama ≥0.5; older servers either 400 with a clear "model does not support structured output" error (a surface change for those users — schema enforcement was never available there before, and the error names the cause) or silently ignore the field and emit prose (matching today's behaviour). The wire body is byte-identical to today's when no schema is attached, guarded by `#[serde(skip_serializing_if = "Option::is_none")]` on the new `OpenAiRequest::response_format` field. The schema registry's existing types (`AmendmentFile`, `PrContent`, `AiCheckResponse`) were tightened to satisfy OpenAI's strict-subset rule that every property in `properties` must also appear in `required`: `Amendment.summary` becomes a non-optional `String` (defaulting to empty for graceful YAML loading), `AiCommitCheck` and `AiIssue` use `#[schemars(extend("required" = [...]))]` to force `Option<T>` and serde-defaulted fields into `required` while preserving nullability via `type: ["...", "null"]`. A new CI test (`schemas_satisfy_openai_strict_subset` in `src/claude/response_schema.rs`) walks each schema and asserts the invariant, catching drift the moment any new optional field is added without an `extend(...)` override. Implementation localised to `src/claude/ai/openai.rs` (~150 LOC including tests) plus the schema-shape adjustments; covered by serde-level body-shape tests, wiremock round-trip tests for both schema-bearing and schemaless paths, and the new strict-subset CI check.

### Added
- **TTL-Bounded In-Memory Cache for JIRA Catalogue MCP Tools** ([#719](https://github.com/rust-works/omni-dev/issues/719)): New `src/mcp/catalogue_cache.rs` module wraps the four near-static JIRA catalogue endpoints (`jira_link_types`, `jira_field_list`, `jira_project_list`, `jira_board_list`) so repeated MCP invocations within a server-process lifetime collapse to a single HTTP request per catalogue per hour, instead of rebuilding an `AtlassianClient` and hitting JIRA every time. `CatalogueCache` lives as `Arc<CatalogueCache>` on `OmniDevServer`; each catalogue has its own `tokio::sync::RwLock<Option<CacheEntry<T>>>` slot, with single-flight via the read-fast-path / write-on-miss / double-check pattern (no extra mutex). Entries are tagged with the issuing `instance_url` and refetched on drift. Failed fetches do not poison the cache — they propagate the error and leave the slot untouched. Default TTL is 1 hour (`DEFAULT_TTL`), constructible with arbitrary `Duration` for tests. Parameterised methods (`get_projects(limit)`, `get_boards(project, board_type, limit)`) cache the *unfiltered, unbounded* result; the YAML helpers (`project_list_yaml`, `board_list_yaml`, `field_list_yaml`) apply limit/filter to the cached `Arc<…>` at serve time so the hit rate is independent of caller arguments. CLI commands remain uncached (one-shot per process; nothing to amortise). Recorded in [ADR-0024](docs/adrs/adr-0024.md). Covered by 7 wiremock tests in `catalogue_cache.rs` (per-catalogue cache hit, TTL expiry, instance-url drift, error propagation) plus updated `*_yaml` helper tests for the four affected helpers.
- **ADF Schema Validation in `confluence write --dry-run`** ([#718](https://github.com/rust-works/omni-dev/issues/718)): `omni-dev atlassian confluence write --dry-run` now runs the ADF schema validator from [#717](https://github.com/rust-works/omni-dev/issues/717) over the converted ADF after printing the JSON output, prints a `Validation:` section that reports either `OK` or one line per violation (using the `AdfSchemaViolation` `Display` impl, e.g. `✗ ADF schema violation at /0/0: 'expand' is not permitted inside 'panel'`), and exits non-zero when any violation is found. Acts as a CI pre-flight check that surfaces what Confluence would otherwise reject silently or with HTTP 500. First wiring point for the soft-launched validator; the converter fix and non-dry-run / MCP wiring are tracked in [#730](https://github.com/rust-works/omni-dev/issues/730). Implemented as a ~20-line addition to `print_dry_run` in `src/cli/atlassian/helpers.rs`; covered by a new `print_dry_run_fails_on_schema_violation` unit test that exercises the `panel`-containing-`expand` case.
- **Data-Driven ADF Content-Model Schema and Validator** ([#717](https://github.com/rust-works/omni-dev/issues/717)): New `src/atlassian/adf_schema.rs` module encodes the full allowed-children content model from the upstream `@atlaskit/adf-schema` npm package, transcribed faithfully from `json-schema/v1/full.json` of the pinned tarball (version `52.9.5`, SHA-256 `90b9b26f5cdf6f0850cebe5cf2df7662601b249322d6bcbeead712ca018e0b56`, recorded in `SCHEMA_VERSION` and `UPSTREAM_TARBALL_SHA256` constants). Storage is a static `LazyLock<HashMap<&str, &[&str]>>` lookup; public helpers are `allowed_children(parent)`, `permits_child(parent, child)`, and a depth-first walker `validate_document(&AdfDocument) -> Vec<AdfSchemaViolation>` that reports nesting violations in document order with `parent_type`, `child_type`, and an index `path` from the document root. Covers every container node type in the upstream schema: `doc`, `paragraph`, `heading`, `panel`, `expand`, `nestedExpand`, `blockquote`, `bulletList`/`orderedList`/`listItem`, `tableCell`/`tableHeader`/`tableRow`/`table`, `layoutSection`/`layoutColumn`, `taskList`/`taskItem`/`blockTaskItem`, `decisionList`/`decisionItem`, `mediaSingle`/`mediaGroup`/`caption`, `codeBlock`, `bodiedExtension`, `bodiedSyncBlock`. Forward-compat preserved per [ADR-0020](docs/adrs/adr-0020.md) via two complementary mechanisms: unknown parent node types are treated permissively (their subtrees are not walked, so future Atlassian schema additions don't fail validation), and `unsupportedBlock`/`unsupportedInline` are accepted under any known parent via a walker-level short-circuit (the upstream JSON schema does not list these wrappers in any parent's allowed-children set, so faithfulness to upstream and round-trip survival can only coexist via the escape hatch). Unknown *children* under known parents are still flagged. Soft launch — the validator is a public library function, not wired into `markdown_to_adf` or any write path; the existing converter still emits structurally invalid ADF (the `nested_expand_inside_panel` and `nested_expand_inside_table_cell` tests document this) and fixing the converter to produce only schema-valid output is tracked as a separate effort. Recorded in [ADR-0023](docs/adrs/adr-0023.md). Covered by 20 unit tests (issue-#717 examples, sortedness invariants, walker depth-first ordering, permissiveness on unknown parents *and* unknown root doc_type, `unsupportedBlock`/`unsupportedInline` walker escape hatch with positive *and* negative assertions on the underlying allowed-children sets) plus 4 integration tests exercising the public API through `AdfDocument::from_json_str`; 100% line and region coverage on the new module. Deferred to follow-ups: converter fix + write-path enforcement ([#730](https://github.com/rust-works/omni-dev/issues/730)), CI drift detection against upstream ([#731](https://github.com/rust-works/omni-dev/issues/731)), code-generation from npm ([#732](https://github.com/rust-works/omni-dev/issues/732)), validator coverage beyond nesting (quantifiers, marks, attribute schemas — [#733](https://github.com/rust-works/omni-dev/issues/733)).
- **ADF Nesting Validation Wired Into API Send Path** ([#714](https://github.com/rust-works/omni-dev/issues/714), follow-up to [#717](https://github.com/rust-works/omni-dev/issues/717)): The schema validator from [#717](https://github.com/rust-works/omni-dev/issues/717) / [ADR-0023](docs/adrs/adr-0023.md) is now enforced at every API send site, so JFM → ADF conversions that produce structurally invalid ADF (e.g., `expand` inside `panel`) abort locally with an actionable message rather than surfacing as opaque HTTP 500s from Confluence. A new `src/atlassian/adf_validated.rs` module exposes a `ValidatedAdfDocument` newtype whose only fallible constructor (`try_new`) runs `adf_schema::validate_document`; the type system enforces "validated once before send" by giving every API send signature (`AtlassianApi::update_content`, `AtlassianClient::{update_issue, update_issue_with_custom_fields, create_issue, create_issue_with_custom_fields, add_comment, update_comment}`, `ConfluenceApi::{create_page, add_page_comment, update_content}`, plus per-section validation in `custom_fields.rs` for rich-text JIRA custom fields) a `&ValidatedAdfDocument` parameter rather than `&AdfDocument`. Errors are surfaced via a new `AtlassianError::InvalidAdfNesting` variant wrapping `AdfValidationError`, formatted as ``invalid ADF nesting — `expand` cannot be a child of `panel` at /0/0. hint: invert the nesting (put the panel inside the expand) or use siblings.`` — the path comes from the upstream walker, the per-(parent, child) hint table covers the high-traffic combinations called out in #714 (panel↔expand, table-cell-or-header↔expand, layout-section nesting, blockquote / list-item containment) with a generic ``"restructure the document so X is not a direct child of Y."`` fallback for any other forbidden pair. All violations are collected so the user can fix multiple issues in one editor pass. The `omni-dev atlassian convert to-adf` CLI also validates by default and accepts a new `--no-validate` flag for users inspecting invalid ADF for debugging. The existing `nested_expand_inside_panel()` and `nested_expand_inside_table_cell()` regression tests now assert that validation rejects the converted document, replacing assertions that the converter still emits these constructs (the converter fix is tracked in [#730](https://github.com/rust-works/omni-dev/issues/730)); `nested_expand_inside_layout_column()` is preserved as a positive case (`expand` inside `layoutColumn` is legitimate per the schema). Architecture decision recorded in [ADR-0025](docs/adrs/adr-0025.md).
- **Discrete `jira_link_list` / `jira_link_types` / `jira_link_remove` MCP Tools** ([#711](https://github.com/rust-works/omni-dev/issues/711)): Three new MCP tools mirror the existing `omni-dev atlassian jira link {list,types,remove}` CLI subcommands by re-exposing the existing `AtlassianClient::{get_issue_links,get_link_types,remove_issue_link}` methods individually, alongside the existing dispatch-style `jira_link` tool (which stays for back-compat). `jira_link_list(key)` returns inward + outward links with link type and target issue summary; `jira_link_types()` returns the configured link-type catalogue (id, name, inward, outward) — global per JIRA instance, not the types used in any particular issue; `jira_link_remove(link_id)` returns YAML `{status: ok}`. Pattern mirrors the existing `jira_watcher_{list,add,remove}` triplet. No new client methods. Implemented in `src/mcp/jira_tools.rs` (params structs, `*_yaml` helpers, three `#[tool]` handlers) with six wiremock unit tests covering each helper's happy path plus a 404-on-remove acceptance test, and registration tests updated in `src/mcp/server.rs` and `tests/mcp_integration_test.rs`. Caching of the link-types catalogue (which is near-static) is deferred to [#719](https://github.com/rust-works/omni-dev/issues/719); confirmation-flag conventions for destructive CLI operations are deferred to [#720](https://github.com/rust-works/omni-dev/issues/720).
- **Confluence Page Version History** ([#708](https://github.com/rust-works/omni-dev/issues/708)): New `omni-dev atlassian confluence history <id>` CLI subcommand and matching `confluence_history` MCP tool list version metadata for a Confluence page (number, ISO timestamp, author account ID, edit message, minor-edit flag), newest-first. Pure metadata — no body fetch — so the structural-diff feature in [#706](https://github.com/rust-works/omni-dev/issues/706) can resolve `from`/`to` references without inlining version-list logic. `--since` accepts either a numeric version (`5`) or an ISO 8601 date (`2026-01-01T00:00:00Z`); newest-first ordering means a date or version cutoff terminates pagination early. `--limit` defaults to 20 with `0 = unlimited`, matching the `comment list` / `label list` convention. Output supports `-o table|json|yaml|yamls|jsonl` and emits `{ page: { id, title, current_version }, versions: [...], truncated }`. Backed by a new `ConfluenceApi::list_page_versions` (auto-paginates `/wiki/api/v2/pages/{id}/versions?limit=&cursor=`, applies the `since` filter client-side, returns a `truncated` flag when `limit` cuts the listing short) plus a lightweight `get_page_metadata` that skips the body and space-key lookup. Tolerates null `authorId`/`createdAt`/`message` per the issue's risk #1. Implemented in `src/atlassian/confluence_api.rs`, `src/cli/atlassian/confluence/history.rs`, and `src/mcp/confluence_tools.rs`; covered by wiremock tests for pagination, numeric/ISO `since` filters, early termination at the cutoff, missing-field tolerance, `limit` truncation, empty results, and API-error propagation across the API, CLI, and MCP layers.
- **`jira_transition_list` MCP Tool & `transition list` CLI Subcommand** ([#710](https://github.com/rust-works/omni-dev/issues/710)): New single-purpose MCP tool `jira_transition_list(key)` and CLI subcommand `omni-dev atlassian jira transition list <KEY>` expose JIRA workflow transitions without forcing a full `jira_read` round-trip. Returns YAML with `{id, name, to_status: {id, name, category}, has_screen}` for each transition — the richer shape mirrors what sooperset/mcp-atlassian publishes, so agent prompts trained against that surface work unchanged. Equivalent to `jira_transition` with `list = true` (which is preserved for backward compatibility), but the dedicated tool is easier for LLM agents to discover. Implemented in `src/mcp/jira_core_tools.rs` (new `JiraTransitionListParams`, `run_jira_transition_list`, `jira_transition_list` tool method) and `src/cli/atlassian/jira/transition.rs`, with `JiraTransition` extended in `src/atlassian/client.rs` to expose the `to_status` and `has_screen` fields previously discarded by the deserialiser. Covered by new wiremock tests for happy path, 404 issue-not-found, empty transitions array, and a richly-populated response body.
- **Destructive CLI Commands: `--dry-run` Flag** ([#720](https://github.com/rust-works/omni-dev/issues/720)): All five destructive Atlassian subcommands (`omni-dev atlassian jira delete`, `omni-dev atlassian confluence delete`, `omni-dev atlassian jira watcher remove`, `omni-dev atlassian jira link remove`, `omni-dev atlassian confluence label remove`) now accept `--dry-run`, which prints `Would <verb> <target>.` and returns without making any API calls. `--dry-run` takes precedence over `--force`, so a script author can add `--dry-run` to an existing scripted `--force` invocation as a temporary safety check without removing the force flag. The `confluence label remove --dry-run` preview is purely client-side: labels are echoed back verbatim without verifying they exist on the page (documented trade-off in [ADR-0027](docs/adrs/adr-0027.md)). A new shared helper `src/cli/atlassian/confirm.rs` exposes `guard_destructive(opts) -> GuardOutcome { Proceed | Cancelled | DryRun }` and replaces the two duplicated `confirm_with_reader` copies that previously lived inline in `jira/delete.rs` and `confluence/delete.rs`. The decision is recorded in [ADR-0027](docs/adrs/adr-0027.md).
- **CI Drift Detection vs Upstream `@atlaskit/adf-schema`** ([#731](https://github.com/rust-works/omni-dev/issues/731)): A new `adf-schema-drift` binary plus a weekly scheduled GitHub Actions workflow (`.github/workflows/adf-schema-drift.yml`, also manual-dispatch) detect drift between the locally-encoded schema snapshot and the upstream npm package, opening or updating an issue labelled `adf-schema-drift` when drift is detected. The Rust implementation lives in a new `src/atlassian/adf_schema/drift.rs` module exposing `DriftReport` / `ParentDrift` value types, an HTTP-backed upstream tarball fetcher with `reqwest` (overridable per `OMNI_DEV_ADF_SCHEMA_LATEST_URL` for test injection) that verifies the response SHA-256 via `sha2`, a `.tgz` extraction routine using `flate2` + `tar`, and a JSON-schema parser tolerant of `anyOf` / `allOf` alias chains and marks-overlay definitions (refs into marks subtrees are filtered out so they do not leak into content children). The `adf-schema-drift` CLI binary (`src/bin/adf_schema_drift.rs`) takes `--format markdown|json|both` and `--output-dir`, writes `drift-report.md` / `drift-report.json`, and emits `drift=<bool>` / `version_changed=<bool>` to stdout and `$GITHUB_OUTPUT`. Adds `flate2`, `tar`, and `sha2` to `Cargo.toml`. Covered by wiremock-backed integration tests that spawn the compiled binary against synthesised upstream responses (success, 4xx, 5xx, connection refused, output-path failure modes).
- **`adf-schema-codegen` Binary and Vendored Upstream Schema Assets** ([#732](https://github.com/rust-works/omni-dev/issues/732)): The hand-transcribed `CONTENT_ENTRIES` allowed-children table is no longer the sole source of truth for the ADF nesting model. A new `adf-schema-codegen` binary (registered in `Cargo.toml`, sourced from `src/bin/adf_schema_codegen.rs`) reads `assets/adf-schema/full.json` plus its `provenance.json` sidecar (npm package, version, tarball URL, tarball SHA-256, `full.json` SHA-256), verifies the input via SHA-256 before generating, parses the upstream JSON schema via the existing `parse_upstream_full_json` function from `drift.rs` (now `pub` for reuse), and emits a deterministic `src/atlassian/adf_schema/generated.rs` formatted through `rustfmt`. A `--check` mode exits non-zero if the committed `generated.rs` is stale, suitable as a CI staleness gate. The vendored tarball assets (`assets/adf-schema/full.json`, `provenance.json`, and a refresh-workflow `README.md`) are checked into the repository. Two new tests assert (a) the generated `UPSTREAM_ENTRIES` slice agrees with the hand-maintained `CONTENT_ENTRIES` modulo a documented leniency allowlist, and (b) the vendored provenance SHA and npm version match the runtime `SCHEMA_VERSION` / `UPSTREAM_TARBALL_SHA256` constants.
- **Confluence HTTP 500 ADF Diagnosis** ([#715](https://github.com/rust-works/omni-dev/issues/715)): When a Confluence write returns HTTP 500, the error now includes a structured diagnosis pointing at the offending ADF nesting and an actionable hint, rather than surfacing as opaque `ApiRequestFailed`. A new helper `confluence_write_error` runs `adf_schema::validate_document` over the submitted payload at error time, returning a new `AtlassianError::ApiRequestFailedWithDiagnosis` variant whose `Display` impl produces a multi-line message — header, `Diagnosis:` line naming the offending parent/child, and an optional `Hint:` line — and intentionally omits the raw response body (which is logged at `debug!` by the call site). A `hint_for(parent, child)` table maps known bad pairs (`expand` inside `panel`, nested `expand` inside `expand`, etc.) to fix suggestions. Wired into `ConfluenceApi::{update_content, create_page, add_page_comment}`. Covered by integration tests for both the 500-with-violation and 500-without-violation paths across all three write operations.
- **Confluence Page Version Comparison** ([#706](https://github.com/rust-works/omni-dev/issues/706)): New `omni-dev atlassian confluence compare run|section` CLI surface and matching `confluence_compare` / `confluence_compare_section` MCP tools perform a structural diff between two versions of a Confluence page using an ADF-tree-aware engine rather than raw text diffing. The engine in `src/atlassian/diff.rs` splits documents into heading-delimited sections identified by stable slug paths (e.g. `/h2#background`), pairs nodes via a three-tier matcher (natural-key via `localId` / `url` / `id` attrs → content-hash → positional), then dispatches per-block: paragraphs and code blocks get word-level inline deltas via the `similar` crate, tables get cell-level pairing, lists get item-level pairing, and everything else falls back to opaque tree-equality. Three output detail levels — `summary` (counts only), `outline` (per-section change kind + drill-in cursors), `full` (embedded deltas, byte-budget-truncated via binary search to fit ~16 KiB / ≈4000 tokens) — let agents request progressively more detail; the `compare_section` drill-in tool decodes a stateless base64url cursor encoding `{page_id, from_v, to_v, section_path}` to re-fetch a single section in `unified`, `side-by-side`, or `markdown-inline` text format. Section-level filters (path, change kind, `min_change_chars`) are honoured by both surfaces. `ConfluenceApi` gains `get_page_at_version` and `resolve_version` (accepting `latest`, `previous`, `v-N`, numeric, or ISO 8601 date references). Adds `similar = "2.7"` to dependencies.
- **Confluence Page Move** ([#707](https://github.com/rust-works/omni-dev/issues/707)): New `omni-dev atlassian confluence move <id> --position append|before|after [--target <id>]` CLI subcommand and matching `confluence_move` MCP tool reparent or reorder a page within its current space via the v1 `PUT /wiki/rest/api/content/{id}/move/{position}/{target}` endpoint. Cross-space moves are not supported by the v2 API and are out of scope. `ConfluenceApi::move_page` returns a `MovedPage` whose ancestor chain is freshly fetched via `fetch_page_with_ancestors` (`GET /wiki/api/v2/pages/{id}?include-ancestors=true`) so callers see the post-move parent. Case-insensitive `parse_move_position` allows agents to pass the position as a string. Explicit 403 / 404 error messages distinguish permission-denied from missing-target.
- **Confluence Attachment Management** ([#709](https://github.com/rust-works/omni-dev/issues/709)): New `omni-dev atlassian confluence attachment upload|list|delete` CLI subcommands and matching `confluence_attachment_upload` / `confluence_attachment_list` / `confluence_attachment_delete` MCP tools cover end-to-end management of page attachments. Uploads are streamed via a new `AtlassianClient::post_multipart` helper with `X-Atlassian-Token: no-check` and no auto-429-retry (streamed bodies are non-replayable); the file is never fully buffered in memory. List is explicitly cursor-paginated — callers control page traversal, unlike the auto-draining label helpers — and `delete` honours an optional `purge: bool` flag that hard-deletes the attachment row rather than tombstoning it. The CLI `delete` subcommand prompts for confirmation by default (`--force` skips). Adds `mime_guess` to deps and enables `multipart` + `stream` features on `reqwest`; `tokio-util` becomes unconditional with `codec` + `io` features.
- **JIRA Project Version Management** ([#712](https://github.com/rust-works/omni-dev/issues/712)): New `omni-dev atlassian jira version list|create` CLI subcommands and matching `jira_version_list` / `jira_version_create` MCP tools manage JIRA project release versions. `list` returns a `JiraProjectVersionList` filterable by `--released true|false` and `--archived true|false` (tri-state — flag absent means "any"). `create` validates the `YYYY-MM-DD` release date up-front via a new `validate_iso_date` helper so callers see a clear error rather than JIRA's opaque 400. The slice also extracts `EnvGuard` and `AUTH_ENV_MUTEX` from the existing `atlassian::auth::tests` into a new `pub(crate) test_util` module so every Atlassian-touching test shares a single process-wide env-serialisation mutex, eliminating cross-test flakiness when several tests mutate `ATLASSIAN_TOKEN` / `ATLASSIAN_USER` / `ATLASSIAN_DOMAIN` concurrently; pre-existing inline guards in `confluence_tools`, `jira user`, and `confluence download` were migrated to the shared mutex.
- **JIRA Comment Edit** ([#713](https://github.com/rust-works/omni-dev/issues/713)): New `omni-dev atlassian jira comment edit <ISSUE> <COMMENT-ID> [--from-file|--message] [--visibility-type group|role] [--visibility-value <name>]` CLI subcommand and matching `jira_comment_edit` MCP tool send `PUT /rest/api/3/issue/{key}/comment/{id}` with the supplied JFM rendered to ADF plus optional visibility restriction. New `JiraVisibility` / `JiraVisibilityType` types serialise to JIRA's `{type, identifier}` wire format; `JiraComment` gains an `updated` timestamp field. A shared `parse_comment_input` helper is extracted from the existing `add` subcommand so the two share argument parsing. Covered by wiremock tests for success, 403 forbidden, 404 not-found, visibility payload round-trip, and rejection of unknown visibility types.
- **Resolve Duplicate Amendments in `commit twiddle`** ([#697](https://github.com/rust-works/omni-dev/issues/697)): The model occasionally emits duplicate amendments (same commit hash listed twice), which previously caused the apply path to fail because the first amendment rewrites the commit, leaving subsequent duplicates pointing at a now-nonexistent hash. A new `resolve_duplicate_amendments` step runs immediately after scope refinement in both the single-commit and batch-commit flows: when stdin is a TTY it prompts interactively (showing each candidate message, looping until a valid choice is made), otherwise it silently keeps the first occurrence and emits a warning to stderr. `--auto-apply` always takes the silent path. Order of non-duplicate amendments is preserved. The slice also tightens the system-prompt instructions for amendment cardinality so the duplicates show up less frequently in the first place.
- **Probe Local Server Context Length at Startup** ([#696](https://github.com/rust-works/omni-dev/issues/696)): The `OpenAiAiClient` (used by `USE_OLLAMA=true`, `USE_OPENAI=true`, and LM Studio) now queries the running server's actual loaded context length at client construction and feeds it into `AiClientMetadata`, so token-budget calculations reflect the real-time limits of the model that is actually loaded rather than the static registry estimate. LM Studio is probed via `/api/v0/models`; Ollama is probed via `/api/show`, scanning the response for an architecture-specific `*.context_length` key. `create_default_claude_client` becomes async to accommodate the probe; failures (network error, missing endpoint, unparseable response) fall back to the registry default with a warning. Covered by wiremock tests for both server shapes plus fallback / error paths.
- **YouTube Transcript Subcommand and Source-Agnostic Library Module** ([#687](https://github.com/rust-works/omni-dev/issues/687)): New top-level `omni-dev transcript` command tree fetches captions and metadata from media platforms, launching with full YouTube support. The CLI surface is `transcript youtube fetch|info|list-langs` with `--format srt|vtt|txt|json`, language selection, auto-generated caption opt-in, and translation. The underlying `src/transcript/` library is clap-free and structured around an async `TranscriptSource` trait (`fetch`, `list_languages`, `info`) so future platforms can be added without touching format converters; first-class `Cue`, `Transcript`, `LanguageInfo`, `MediaInfo`, `TrackKind`, `Format` (SRT / WebVTT / plain / JSON), and a `TranscriptError` enum covering invalid locator / parse failure / missing language / auto-caption-opt-in-required / playability refusal / HTTP error live under that module. The YouTube source uses the ANDROID_VR InnerTube client to bypass bot-detection gating that refuses WEB-context sessions on most videos: a per-session `visitorData` token is scraped from the watch-page HTML on first call and cached in `tokio::sync::OnceCell` (concurrent first-callers serialise on a single in-flight fetch rather than double-fetching), then forwarded under `context.client` on every InnerTube `/player` POST. The InnerTube envelope uses ANDROID_VR device-fingerprint fields (`deviceMake`, `deviceModel`, `osName`, `osVersion`, `androidSdkVersion`) and moves the API key from `?key=` to `X-Goog-Api-Key`. URL normalisation accepts watch / share / shorts / embed forms across www / m subdomains plus bare 11-character video IDs; the timedtext URL builder replaces (not appends) `fmt=` and `tlang=` params so YouTube serves `json3` rather than the legacy SRV3 format. Adds `async-trait` to deps. Documentation lives in `docs/transcript.md` (CLI reference, library architecture, error variants, recipe for adding a new source) with README and `docs/README.md` index entries.
- **Layered Model Catalog with User and Project Overrides** ([#684](https://github.com/rust-works/omni-dev/issues/684)): The previously single-source embedded model registry is replaced by a three-layer YAML loader so model specifications and provider configurations can be customised without a rebuild. Precedence (highest wins): explicit `--models-yaml <PATH>` / `OMNI_DEV_MODELS_YAML` env var → `./.omni-dev/models.yaml` (project) → `~/.omni-dev/models.yaml` (user) → `src/templates/models.yaml` (embedded — always present, hard error if malformed). Deep-merge happens at YAML value level: the `models` sequence is keyed by `api_identifier`, the `providers` mapping is deep-merged per provider, other keys use last-writer-wins. A new `ModelSource` enum (`Embedded` / `User` / `Project` / `Override`) is populated by the loader (never read from YAML) and tracked on each merged `ModelSpec` / `ProviderConfig`. `version: "1"` is added to the embedded catalog with advisory mismatch warnings for user / project files declaring a different version. Missing user / project files fall through silently; malformed files log an error and are skipped; an explicit override path that doesn't exist emits a warning. `config models show` renders the merged catalog with per-entry `source:` annotations and a layer-count header; `--embedded-only` falls back to a verbatim embedded dump. Recorded in [ADR-0022](docs/adrs/adr-0022.md), which supersedes [ADR-0011](docs/adrs/adr-0011.md). Covered by recovery / edge-case tests for scalar-replacement of top-level / `models` / `providers` and for empty-env-var filtering.
- **Breaking-Change Detection in Commit-Twiddle System Prompt** ([#744](https://github.com/rust-works/omni-dev/issues/744)): The AI system prompt now contains a mandatory `BREAKING CHANGE DETECTION` section that lists the diff signals which require breaking-change markers (new mandatory parameters, removed or renamed public APIs, CLI flag changes, MCP schema changes, serialisation format changes, default-behaviour changes) and instructs the model to emit **both** the `!` suffix on type/scope and a `BREAKING CHANGE:` footer with concrete migration instructions whenever any signal is detected. An "Additive-Looking-But-Breaking Trap" subsection calls out the case that originally regressed (adding a mandatory `confirm: bool` to an existing MCP params struct without `#[serde(default)]` — addition-of-required-field is breaking even though the type signature looks superset-compatible) with the `WatcherRemoveParams` confirm-field example. A `BREAKING_CHANGE_FINAL_PASS` constant appended to both the basic and contextual user-prompt variants exploits recency bias as a hedge against the system-prompt-only rule being ignored on long inputs. Covered by prompt-assembly tests asserting the new block is present, lists the expected signals, requires both markers, and is inherited into the contextual prompt.

### Changed
- **`omni-dev atlassian jira transition` CLI Restructure (Breaking)** ([#710](https://github.com/rust-works/omni-dev/issues/710)): The flat `transition <KEY> <TRANSITION>` form has been replaced with subcommands. Migrate `omni-dev atlassian jira transition PROJ-1 Done` → `omni-dev atlassian jira transition execute PROJ-1 Done`, and `omni-dev atlassian jira transition PROJ-1 --list` → `omni-dev atlassian jira transition list PROJ-1`. Mirrors the `comment list`/`comment add` shape used elsewhere in the JIRA CLI tree. The MCP `jira_transition` tool is unchanged.
- **Destructive CLI Commands: Confirmation by Default (Breaking)** ([#720](https://github.com/rust-works/omni-dev/issues/720)): `omni-dev atlassian jira watcher remove`, `omni-dev atlassian jira link remove`, and `omni-dev atlassian confluence label remove` now prompt for confirmation by default and accept `--force` to skip the prompt — bringing them into line with the existing behaviour of `jira delete` and `confluence delete`. Previously these three commands executed the API mutation immediately with no guard. Non-interactive callers (CI scripts, automation) must add `--force` to keep current behaviour, or feed `y\n` on stdin.
- **MCP Destructive Tools: Mandatory `confirm: true` (Breaking)** ([#720](https://github.com/rust-works/omni-dev/issues/720)): The `jira_watcher_remove`, `jira_link_remove`, and `confluence_label_remove` MCP tools now require a mandatory `confirm: bool` parameter and refuse with an explanatory error when called without `confirm: true`. Brings them into line with the existing `jira_delete` and `confluence_delete` tools. The shared `WatcherMutateParams` is split: the `add` tool keeps the original two-field struct, the `remove` tool gets a new `WatcherRemoveParams` with the `confirm` field. `LinkRemoveParams` and `ConfluenceLabelRemoveParams` gain `confirm` in place. There is no `dry_run` parameter on the MCP surface — assistants can preview by reading the relevant resource (`jira_read`, `confluence_read`, `jira_watcher_list`, `jira_link_list`) before mutating.
- **`sha2` 0.10 → 0.11** ([#757](https://github.com/rust-works/omni-dev/pull/757)): Dependabot bump. `sha2` 0.11 changes the digest output type from `GenericArray` (which implements `LowerHex`) to `hybrid_array::Array` (which does not), breaking the previous `format!("{:x}", Sha256::digest(...))` idiom at three call sites in `src/atlassian/adf_schema/drift.rs` and `src/bin/adf_schema_codegen.rs`. A new `pub fn hex_encode(bytes: &[u8]) -> String` helper in `drift.rs` folds bytes into a lowercase hex string and replaces all three sites. Transitive bumps: `block-buffer` 0.10→0.12, `cpufeatures` 0.2→0.3, `crypto-common` 0.1→0.2, `digest` 0.10→0.11; adds `hybrid-array` 0.4 and `const-oid` 0.10; removes `generic-array` 0.14.
- **`similar` 2.7 → 3.1** ([#758](https://github.com/rust-works/omni-dev/pull/758)): Dependabot bump. No call-site changes required.
- **`tokio` 1.52.1 → 1.52.3** ([#756](https://github.com/rust-works/omni-dev/pull/756)): Dependabot bump. No call-site changes required.
- **Subject-Length Rule Replaced With Imperative-Mood in Commit Examples**: The 72-character subject-line limit was documented in `commit-guidelines.md` and `default-commit-guidelines.md` but never enforced by the checker, while the `imperative-mood` rule was checked but absent from the human-readable docs. The mismatch is removed: the length limit is dropped from both guideline files and the imperative-mood rule replaces it in the prompt example output and test fixtures (`prompts.rs`, `client.rs`, `check.rs`).

### Fixed
- **`decisionItem` ADF Inline Content** ([#753](https://github.com/rust-works/omni-dev/issues/753)): The ADF schema requires `decisionItem` to contain inline nodes (`text`, etc.) directly, not wrapped in a `paragraph`. `parse_decision_items` was inserting a paragraph wrapper before passing inline content to `AdfNode::decision_item`, producing schema-invalid output. The wrapper is removed and a regression test asserts the first child of a constructed `decisionItem` is a `text` node, not a `paragraph`.
- **`claude-cli` `--json-schema` Inline Argument**: `claude -p --json-schema` silently returns empty output when given a filesystem path; the value must be an inline JSON string on argv. `build_command_with_schema` is changed from `Option<&Path>` to `Option<&str>` and `run_with_options` now serialises the schema with `serde_json::to_string` and passes it verbatim as the argument instead of writing a temp file and passing the path. A new `build_command_inline_schema_is_passed_verbatim_not_as_path` test asserts the argv value starts with `{` and is not an absolute path; an integration test using a new argv-capture shim asserts the inline JSON appears verbatim in the subprocess argv.
- **Multi-Scope Subject Allows Single Trailing Space After Comma**: Pre-validation previously rejected `feat(a, b): …` (one space after the comma) and only accepted `feat(a,b): …`. The check is relaxed to permit a single trailing space (`!scope.contains(",  ") && !scope.contains(" ,")`) while still rejecting two or more spaces after the comma and any whitespace before the comma. `commit-guidelines.md` now shows both `scope1,scope2` and `scope1, scope2` as accepted forms and explicitly calls out the rejected variants.
- **Breaking-Change Detection: Additive-Looking-But-Breaking Trap** ([#744](https://github.com/rust-works/omni-dev/issues/744)): Companion to the [#744](https://github.com/rust-works/omni-dev/issues/744) feature entry above — adds the `WatcherRemoveParams` example and the diff-signal expansion that catches new mandatory MCP params, new mandatory struct fields without `#[serde(default)]`, and new confirmation prompts on previously-unattended operations as breaking even when the type signature looks superset-compatible.

### Documentation
- **Comprehensive Documentation Coverage Refresh**: Audited the user-facing docs against the current CLI/MCP surface and closed the major gaps that had accumulated through the v0.23–v0.25 cycle. New `docs/mcp.md` is the canonical MCP tool/resource reference (~70 tools across git, JIRA, Confluence, Atlassian shared, Datadog, AI/config domains plus the `omni-dev://specs/{name}` resource scheme), and the README MCP section is rewritten to defer detail there. The README Datadog section drops "subsequent slices" wording in favour of present-tense subcommand examples, and a new `## Datadog Integration` section in `docs/user-guide.md` covers auth, metrics, monitors, dashboards, logs, events, SLOs, hosts, and downtimes. The user guide also gains backfills for JIRA `read --fields`/`--all-fields`, `write --parent`/`--assignee`/`--reporter`/`--no-content`/`--set-field`, `create --set-field`, `sprint create`/`update`, watcher, worklog, user search, and dev-info subcommands; Confluence comments, labels, children, user search, and bulk download (with `--space`/`--title-filter`/`--resume`/`--on-conflict`/`--concurrency`); `git branch create pr` flag table (adds `--ready`/`--draft`/`--base`/`--model`/`--context-dir`/`--no-push`); a full `git commit message check` reference; and new `ai chat` / `ai claude history sync` / `ai claude skills` / `ai claude cli model resolve` / `commands generate` sections. `docs/configuration.md` gains an "AI Backend Selection" matrix covering the five-way dispatch (`OMNI_DEV_AI_BACKEND` claude-cli → Ollama → OpenAI → Bedrock → Anthropic), model resolution precedence, and the claude-cli sandbox/escape-hatch/budget-cap envelope. `docs/troubleshooting.md` grows new sections for Atlassian auth (404, trailing slashes, MCP env inheritance), Datadog auth (region/site mismatch, 429), MCP server (`failed to open git repository`, missing `mcp` feature), and `claude-cli` backend failures (tools blocked, MCP blocked, budget cap). `ARCHITECTURE.md` module map adds `mcp/`, `atlassian/`, `datadog/`, `claude/ai/claude_cli.rs`, the `omni-dev-mcp` second binary (per ADR-0021), and an "AI backend dispatch" subsection. Drift fixes: removed the non-existent `--edit` flag from twiddle's option tables in `README.md` and `docs/user-guide.md`; bumped the Rust MSRV claim from 1.70+ to 1.80+ to match `Cargo.toml`. `docs/README.md` Key Features table adds rows for Datadog and the MCP server; `CONTRIBUTING.md` gains the `.work/` worktree convention.
- **ADF Content Model Section in JFM Specification** ([#716](https://github.com/rust-works/omni-dev/issues/716)): A new "Content Model Constraints" section in `docs/specs/jfm.md` documents the public ADF validator helpers (`allowed_children`, `permits_child`, `validate_document`, `content_model`), the common per-container pitfalls and recommended workarounds (expand↔panel, nested-expand in table cells, decision / task items, blockquote / list-item containment, layoutSection nesting), forward-compatibility behaviour on unknown and `unsupported*` node types, and the write-path enforcement path via `ValidatedAdfDocument` plus HTTP-500 re-diagnosis via `AtlassianError::ApiRequestFailedWithDiagnosis`. The "Coverage and limits" subsection lists what the validator currently covers (allowed-children sets, per-term quantifiers) and what remains out of scope (mark whitelists and attribute-value schemas — the [#733](https://github.com/rust-works/omni-dev/issues/733) follow-ups).
- **Architecture Decision Records**:
  - [ADR-0022](docs/adrs/adr-0022.md) accepted — layered model catalog with user and project overrides, supersedes [ADR-0011](docs/adrs/adr-0011.md).
  - [ADR-0023](docs/adrs/adr-0023.md) accepted — data-driven ADF content-model schema and validator transcribed from the upstream JSON schema.
  - [ADR-0025](docs/adrs/adr-0025.md) accepted — wire ADF schema validator into the API send path via `ValidatedAdfDocument` newtype enforcement.
  - [ADR-0026](docs/adrs/adr-0026.md) accepted — ADF validator extensions covering quantifier / arity checks, per-node attribute schemas, and per-context mark allow-lists.

### CI/CD
- **`cargo clippy --all-targets` in Lint Workflow** ([#690](https://github.com/rust-works/omni-dev/issues/690)): The clippy step in `.github/workflows/ci.yml` now passes `--all-targets`, surfacing lints on test / bench / example targets that were previously checked only for the default target set. The slice also includes batch lint fixes across `src/atlassian/` produced when `--all-targets` first surfaced them, plus `#[allow(clippy::unwrap_used, clippy::expect_used)]` opt-ins in test modules where assertion-style panics are idiomatic.
- **`rustfmt` Component Pinned in Rust Toolchain Setup**: The Rust toolchain setup action in CI now explicitly installs the `rustfmt` component so `cargo fmt --check` runs reliably in environments where the default profile does not include it.

## [0.25.0] - 2026-05-05

### Added
- **Set JIRA Parent Field via `jira write` and `jira_link`** ([#670](https://github.com/rust-works/omni-dev/issues/670)): Closes the gap that previously forced callers to fall back to raw `curl` for setting the JIRA `parent` field. `omni-dev jira write` accepts a new `--parent <KEY>` flag (independent of `--set-field`, which only handles custom fields); when supplied alone it performs a parent-only update without overwriting the existing description, when supplied with a body it sends both in one PUT, and the parent key is validated up-front via `validate_issue_key`. The MCP `jira_write` tool gains a matching `parent` parameter and makes `content` optional — at least one of `content`/`parent` is required. The MCP `jira_link` tool gains a `parent` action (`key` = child issue, `target` = parent issue) for callers that prefer the link-management surface; this is distinct from `create`, which produces relationship links (Blocks, Composition, etc.) rather than the system parent field. Internally, `AtlassianClient::link_to_epic(epic_key, issue_key)` was renamed to `set_issue_parent(issue_key, parent_key)` (argument order flipped to match other update helpers); the CLI `jira link epic` subcommand is unchanged for back-compat. `update_issue_with_custom_fields` now accepts `description_adf: Option<&AdfDocument>` and a new `parent: Option<&str>` so a parent-only PUT does not overwrite the description, with a guard rejecting empty payloads. Implemented across `src/atlassian/client.rs`, `src/cli/atlassian/helpers.rs`, `src/cli/atlassian/jira/{write.rs,link.rs,mod.rs}`, and `src/mcp/jira_core_tools.rs`. Covered by new wiremock-backed tests for `set_issue_parent`, parent-only updates, content+parent updates, the `--parent` CLI flag (dry-run, parent-only, body+parent, invalid-key rejection), the `jira_link` `parent` action (success, missing-key, missing-target, API error), and the `jira_write` MCP tool's content/parent variants.
- **`jira_write` Field Updates** ([#669](https://github.com/rust-works/omni-dev/issues/669)): The `jira_write` MCP tool and `omni-dev atlassian jira write` CLI now accept typed `assignee` and `reporter` parameters (taking an Atlassian `accountId`) plus an escape-hatch `fields` map for arbitrary canonical JIRA fields. The empty string `""` clears assignee/reporter (Atlassian's `null` payload); `"-1"` triggers automatic assignment. The description body is now optional — `content`/`--no-content` lets a caller flip an assignee or set a priority without re-posting the description, removing the previous `jira_read` → `jira_write` round-trip. A typed `assignee`/`reporter` parameter that collides with the same key inside `fields` (or `--set-field`) is rejected with a hard error rather than silently overriding, so the caller's intent is unambiguous. Implemented in `src/mcp/jira_core_tools.rs`, `src/cli/atlassian/jira/write.rs`, and a shared `user_field_value` / `apply_user_field_overrides` helper in `src/atlassian/custom_fields.rs` (used by both MCP and CLI paths).
- **`jira_user_search` MCP Tool & CLI** ([#669](https://github.com/rust-works/omni-dev/issues/669)): New `jira_user_search` MCP tool and `omni-dev atlassian jira user search` CLI subcommand resolve a display name or email substring to an Atlassian `accountId` so callers can populate `jira_write`'s new `assignee`/`reporter` parameters. Returns YAML/table/json/yaml/jsonl output with `account_id`, `display_name`, `email_address` (often GDPR-redacted), `active`, and `account_type` for each match. The MCP tool description explicitly tells the model to call this tool first when only a name or email is available, keeping disambiguation visible to the agent rather than hidden inside `jira_write`. Backed by a new `AtlassianClient::search_jira_users` method that paginates `GET /rest/api/3/user/search` until a short page is returned (the endpoint has no `isLast`/`next` envelope).
- **`omni-dev://specs/{name}` MCP Resource** ([#672](https://github.com/rust-works/omni-dev/issues/672)): New MCP resource URI scheme serves reference specifications embedded into the binary at compile time. The initial spec (`omni-dev://specs/jfm`) is the JIRA-Flavoured Markdown reference, embedded from `docs/specs/jfm.md` via `include_str!` so installed builds can serve it without reading from disk and the resource cannot drift from the human-readable docs. The resource template is advertised through the server's `resources` capability with a description guiding AI clients to fetch it before writing JIRA or Confluence content; server instructions and the `jira_write` tool description point at the same resource. `content` field doc comments on `ConfluenceCreateParams`, `ConfluenceWriteParams`, `JiraCreateParams`, `JiraWriteParams`, and `JiraCommentParams` reference the spec resource so the model surfaces it during schema introspection. New `ResourceUri::Specs` variant adds `EmptyIdentifier` and `Malformed` parsing errors. Implemented in `src/mcp/specs.rs`, `src/mcp/resources.rs`, and `src/mcp/server.rs`; covered by new unit tests for spec lookup, URI parsing, and `read_resource` dispatch, plus integration tests for listing and reading the resource.

### Changed
- **`jira_transition` MCP Tool Discoverability** ([#671](https://github.com/rust-works/omni-dev/issues/671)): The `jira_transition` tool description now leads with the most common usage (executing by name, e.g. `transition: "In Progress"`), shows the numeric-id form (`transition: "31"`), and explicitly tells the assistant to call with `list = true` first when the available transitions are not yet known. The `transition` field doc carries the same examples into the JSON schema. When the underlying `POST /rest/api/3/issue/{key}/transitions` call fails, the error chain now includes a hint that the workflow may require additional fields (assignee, resolution, screen-driven field) or that the transition may not be valid from the current status, suggesting `list = true` to confirm — the original `AtlassianError::ApiRequestFailed { status, body }` stays in the chain so the HTTP status and response body remain visible. No API surface change.
- **rmcp 1.5.0 → 1.6.0**: Bumped the `rmcp` dependency (and `rmcp-macros`) to the upstream 1.6.0 release.

### Documentation
- **JFM Markdown Description in Confluence Tool Annotations**: Clarified the JFM markdown wording in the `confluence_*` MCP tool descriptions so the model surfaces the correct format expectations.
- **ADR-0021 Accepted**: Marked the "MCP server via second binary" architectural decision as accepted.

### CI/CD
- **Stop Hook Enforces Snapshot Updates** ([#667](https://github.com/rust-works/omni-dev/issues/667)): Added a Claude Code stop hook (`.claude/hooks/check-snapshots.sh`) that blocks session completion when files under `src/cli/` or `src/main.rs` have changed but `tests/snapshots/` has not been updated, directing the AI to run the `update-snapshots` skill before stopping. Guards against infinite loops via the `stop_hook_active` flag, registered with a 30-second timeout in `.claude/settings.json`. Permissions also broadened to allow all `omni_dev` debug/release binaries instead of a single hardcoded hash-suffixed path.

## [0.24.0] - 2026-05-03

### Added
- **Claude History Sync — Markdown Output** ([#665](https://github.com/rust-works/omni-dev/issues/665)): `omni-dev ai claude history sync` accepts `--output-format <jsonl|markdown>` (comma-separated, e.g. `--output-format jsonl,markdown` writes both side-by-side). Markdown output produces an LLM-friendly `<target>/<slug>/<uuid>.md` alongside (or instead of) the lossless `.jsonl`, with YAML frontmatter (session metadata) followed by `## User` / `## Assistant` turns, `### Tool call: <name>` blocks for tool invocations, `<details>` blocks for thinking, and 4-backtick fences when tool output contains embedded triple-backtick blocks. Agent-to-user interactions are surfaced as first-class structured events: `AskUserQuestion` calls render as `### Agent question: <header>` with the question text and option list (with descriptions and a `(multi-select)` marker where applicable), and the paired answer renders as `## User response`; tool denials are detected via the "The user doesn't want to proceed" sentinel and labelled `denied by user` rather than a generic `error`; user interrupts (escape mid-execution) are labelled `interrupted by user`. This means the rendered transcript distinguishes "the agent asked, the user picked B" and "the agent tried X, the user denied" from plain tool runs — the signal a coaching LLM needs. Sub-agent (`Agent`) calls render only the `prompt` argument; sub-agent internal turns are not captured (matches the v1 sync invariant from #658). The existing `<persisted-output>` "(persisted, NN KB)" envelope from large tool outputs is preserved verbatim. System-side events (system reminders inside user text, attachments, permission-mode changes, summary events, generic system events) are **included by default** — pass `--exclude-system` to drop them; `--exclude-system` only affects markdown output, the jsonl is byte-identical regardless. Markdown idempotency uses source mtime alone (the rendered length differs from the source length, so size cannot participate in the freshness key); the source jsonl is append-only, making mtime alone sufficient. `--prune` scopes deletion to whichever extensions are listed in `--output-format`, so artifacts the run was not asked to manage survive. Implemented in `src/cli/ai/claude/history/markdown.rs` (renderer) with new clap surface in `src/cli/ai/claude/history/sync.rs` and a `FileFormat` enum in `src/cli/ai/claude/history/common.rs`. Covered by 130+ unit tests in the history module including `insta` snapshots pinning the rendered shape of one of each event type both with and without `--exclude-system`, plus end-to-end tests for both-formats-side-by-side, prune scoping per format, partial-jsonl-prefix tolerance, and markdown idempotency on re-run.
- **Claude History Sync** ([#658](https://github.com/rust-works/omni-dev/issues/658)): New `omni-dev ai claude history sync --target <DIR>` exports Claude Code conversation history to a target directory as one `.jsonl` per chat, grouped by encoded project slug (`<target>/<slug>/<uuid>.jsonl`). Re-running is idempotent — sessions whose source `(size, mtime)` match the target are skipped, modified sessions are overwritten via a sibling tempfile + rename, and source `mtime` is preserved on the target so downstream tooling can sort sessions chronologically. In-progress chats produce a valid jsonl prefix via a snapshot-EOF read (the source size is captured up front and exactly that many bytes are copied — never more). Flags: `--source` (defaults to `~/.claude/projects`), `--project NAME_OR_PATH` (matches encoded slug or decoded cwd path), `--since DURATION_OR_DATE` (`30s`, `5m`, `2h`, `7d`, `4w`, or RFC 3339), `--prune` (deletes target files for sessions removed upstream — only files matching `<slug>/<uuid>.jsonl` are eligible; anything else inside the target survives regardless), `--dry-run`, and `--format text|yaml`. Refuses to run when the target is the source root or a descendant. The export is a **behavioural transcript** — prompts, responses, thinking, tool calls, and tool-result metadata sufficient for analyst use cases (behavioural coaching, work-log generation). Sub-agent internal turns, tool-result `*.txt` sidecars, PDF rasters, and auto-memory are deliberately excluded; see the issue for the rationale and planned follow-ups (memory sync, full-fidelity archive mode, restore, redact, MCP wrapper). Implemented in `src/cli/ai/claude/history/{mod.rs, common.rs, sync.rs}` and wired through `src/cli/ai/claude/mod.rs`. Covered by 48 unit tests (line coverage 95–100% across the new module).
- **`--claude-cli-allow-mcp` Escape Hatch** ([#634](https://github.com/rust-works/omni-dev/issues/634)): New global flag and `OMNI_DEV_CLAUDE_CLI_ALLOW_MCP` environment variable opt the `claude-cli` backend out of `--strict-mcp-config`, letting the nested `claude -p` session load MCP servers from `~/.claude/settings.json`. Independent of `--claude-cli-allow-tools` so MCP access can be granted without enabling built-in tools, and vice-versa. A WARN log fires on every invocation when the flag is active, mirroring the existing tool escape hatch. Wired through `src/cli.rs` (flag → env propagation) and `src/claude/ai/claude_cli.rs` (`ClaudeCliAiClient::with_allow_mcp` builder, `allow_mcp_from_env`, conditional argv assembly). Honours the README forward reference promised by the [#608](https://github.com/rust-works/omni-dev/issues/608) lockdown work.
- **MCP `output_file` for Read Tools** ([#631](https://github.com/rust-works/omni-dev/issues/631)): `confluence_read` and `jira_read` accept an optional `output_file` parameter. When set, the rendered content is written to that path and the tool returns a short YAML summary (`path`, `bytes`, `format`) instead of the inline body. This avoids blowing past the assistant's context window on large pages — the assistant can then page through the file with offset/limit using its filesystem read tool. Mirrors the on-disk pattern already established by `confluence_download` and `jira_attachment_download`. Implemented in `src/mcp/output_file.rs` and wired through `confluence_tools::run_confluence_read` and `jira_core_tools::run_jira_read`.
- **Datadog Phase 2 Endpoints** ([#639](https://github.com/rust-works/omni-dev/issues/639)): New `omni-dev datadog events`, `slo`, `hosts`, `downtime`, and `metrics catalog` subcommands wrap the read-only Phase 2 Datadog endpoints called out in the original [#619](https://github.com/rust-works/omni-dev/issues/619) design. `events list` calls `GET /api/v2/events` (single-page; cursor pagination preserved on `meta.page.after` for callers that need to iterate manually); `slo list` and `slo get <id>` call `GET /api/v1/slo` (auto-paginating, capped at 10000 SLOs) and `GET /api/v1/slo/{id}`; `hosts list` calls `GET /api/v1/hosts` with `start`/`count` pagination; `downtime list [--active-only]` calls `GET /api/v1/downtime` (the API returns the full set in one response — no pagination); `metrics catalog list` calls `GET /api/v1/metrics` (distinct from the Phase 1 metrics *query* endpoint at `/api/v1/query`). Built on five new façades — `EventsApi`, `SloApi`, `HostsApi`, `DowntimesApi`, `MetricsCatalogApi` — under `src/datadog/` plus matching wire types (`Event`, `EventsResponse`, `Slo`, `SloListResponse`, `SloGetResponse`, `Host`, `HostsResponse`, `Downtime`, `MetricCatalogResponse`) in `src/datadog/types.rs`. Output supports `-o json|yaml|yamls|jsonl|table`; bespoke tables render `TIMESTAMP | TITLE | SOURCE | HOST | TAGS` (events), `ID | NAME | TYPE | TAGS` (SLOs), `NAME | UP | LAST REPORTED | APPS` (hosts), `ID | SCOPE | START | END | MONITOR | MESSAGE` (downtimes), and `METRIC` (metrics catalog). Six new MCP tools (`datadog_events_list`, `datadog_slo_list`, `datadog_slo_get`, `datadog_hosts_list`, `datadog_downtime_list`, `datadog_metrics_catalog_list`) mirror the CLI surface so AI assistants can issue the same queries over the MCP transport. No new crate dependencies. Phase 2 follow-ons (logs cursor pagination, 429 retry tuning) remain deferred.
- **Datadog MCP Tools** ([#640](https://github.com/rust-works/omni-dev/issues/640)): The Phase 1 Datadog CLI surface is now also exposed over MCP so AI assistants (Claude Desktop, Claude Code) can invoke Datadog read-only queries over stdio. New tools registered on `omni-dev-mcp`: `datadog_auth_status` (boolean credential-presence flags only — never emits `DATADOG_API_KEY` or `DATADOG_APP_KEY` values), `datadog_metrics_query`, `datadog_monitor_list`, `datadog_monitor_get`, `datadog_monitor_search`, `datadog_dashboard_list`, `datadog_dashboard_get`, and `datadog_logs_search`. Each tool builds a fresh `DatadogClient` per invocation via the existing `cli::datadog::helpers::create_client` (per-call construction mirrors the Atlassian tools and lets credential changes take effect without restarting the MCP server) and serialises the typed response struct as YAML, matching the CLI `-o yaml` output. Implemented in `src/mcp/datadog_tools.rs` and registered through `Self::datadog_tool_router()` in `OmniDevServer::new()`. Gated behind the existing `mcp` Cargo feature; non-MCP builds are unaffected. No new crate dependencies.
- **Datadog Logs Search** ([#638](https://github.com/rust-works/omni-dev/issues/638)): New `omni-dev datadog logs search` subcommand wraps Datadog's read-only logs search endpoint via `POST /api/v2/logs/events/search` (the only Phase 1 endpoint that uses POST). Accepts `--filter <q>`, `--from` (relative shorthand like `15m`/`1h`, `now`, RFC 3339, or Unix epoch seconds — converted to RFC 3339 before sending), `--to` (default `now`), `--limit` (per-page; default 100, max 1000 — rejected client-side above the cap), and `--sort timestamp-asc|timestamp-desc` (serialised as `timestamp` / `-timestamp` on the wire). Built on a new `LogsApi` façade in `src/datadog/logs_api.rs` plus `LogEvent`, `LogEventAttributes`, `LogSearchResult`, `LogSearchMeta`, `LogSearchPage`, and `SortOrder` types in `src/datadog/types.rs`. Datadog v2 logs search uses cursor pagination (`meta.page.after`), not offset; Phase 1 ships single-page only and the cursor token is preserved on the response so a Phase 2 follow-up can iterate without changing the wire types. Output supports `-o json|yaml|yamls|jsonl|table`; the bespoke table renders `TIMESTAMP | SERVICE | STATUS | MESSAGE`. No new crate dependencies. Closes [#619](https://github.com/rust-works/omni-dev/issues/619).
- **Datadog Dashboard List/Get** ([#637](https://github.com/rust-works/omni-dev/issues/637)): New `omni-dev datadog dashboard` subcommands wrap Datadog's read-only dashboard endpoints. `dashboard list [--filter-shared]` calls `GET /api/v1/dashboard` (which returns every dashboard in a single response — no server-side pagination); `dashboard get <id>` calls `GET /api/v1/dashboard/{id}` and renders the full definition. Built on a new `DashboardsApi` façade in `src/datadog/dashboards_api.rs` plus `DashboardSummary`, `DashboardListResponse`, and `Dashboard` types in `src/datadog/types.rs`. Per-widget schemas are deeply heterogeneous, so `Dashboard.widgets` is preserved as raw `serde_json::Value` (mirroring how ADF is handled in the Atlassian integration). Output supports `-o json|yaml|yamls|jsonl|table`; the bespoke table renders `ID | TITLE | AUTHOR | URL`. No new crate dependencies.
- **Datadog Monitor List/Get/Search** ([#636](https://github.com/rust-works/omni-dev/issues/636)): New `omni-dev datadog monitor` subcommands wrap Datadog's read-only monitor endpoints. `monitor list [--name <substr>] [--tags k:v,...] [--monitor-tags k:v,...] [--limit N]` filters via `GET /api/v1/monitor`; `monitor get <id>` fetches a single monitor; `monitor search --query <str> [--limit N]` runs the faceted `GET /api/v1/monitor/search`. `--limit 0` auto-paginates and is hard-capped at 10,000 monitors per invocation (per the decisions in #619). Built on a new `MonitorsApi` façade in `src/datadog/monitors_api.rs` plus `Monitor`, `MonitorSearchResult`, `MonitorSearchItem`, and `MonitorSearchMetadata` types in `src/datadog/types.rs`. Output supports `-o json|yaml|yamls|jsonl|table`; the bespoke table renders `ID | NAME | STATUS | TAGS`. No new crate dependencies.
- **Datadog Metrics Query** ([#635](https://github.com/rust-works/omni-dev/issues/635)): New `omni-dev datadog metrics query` subcommand runs point-in-time Datadog timeseries queries via `GET /api/v1/query`. Accepts `--query`, `--from`, and optional `--to` (relative shorthand like `15m`/`1h`, the literal `now`, RFC 3339 timestamps, or Unix epoch seconds via the shared `src/datadog/time.rs` parser), plus `-o json|yaml|yamls|jsonl|table`. Table output is bespoke: a `TIMESTAMP` column followed by one column per series, with the union of per-series timestamps sorted ascending and `-` rendered for gaps and missing samples. Built on a new `MetricsApi` façade in `src/datadog/metrics_api.rs` that percent-encodes the query string via the existing `url` crate. No new crate dependencies.
- **Datadog Integration — Scaffold & Auth** ([#619](https://github.com/rust-works/omni-dev/issues/619)): New `omni-dev datadog` command tree exposing read-only Datadog API access. This first slice ships the authentication surface: `datadog auth login` (interactive credential prompt), `datadog auth logout` (removes credentials from `~/.omni-dev/settings.json`), and `datadog auth status` (verifies credentials via `/api/v1/validate`). Credentials live in the shared `env` map under `DATADOG_API_KEY`, `DATADOG_APP_KEY`, and `DATADOG_SITE`; environment variables override stored settings. The underlying `DatadogClient` in `src/datadog/client.rs` handles 429 responses with `Retry-After` and Datadog-specific `X-RateLimit-Reset` awareness, and surfaces `X-RateLimit-*` headers in error output when retries are exhausted. No new crate dependencies.

## [0.23.1] - 2026-04-23

### Fixed
- **AI Issue Output Field Order** ([#627](https://github.com/rust-works/omni-dev/issues/627)): Require the `reasoning` field to appear before `severity` in AI-generated issue output. The prompt now instructs the model to write `reasoning` before `severity`, and the deserializer rejects payloads whose field order is reversed so that callers see a clear parse error rather than silently accepting malformed output
- **Multi-Scope Whitespace Trimming** ([#626](https://github.com/rust-works/omni-dev/issues/626)): `feat(a, b): …` style multi-scope subjects are now valid. Scope validation trims whitespace around each comma-separated part before checking allowed scopes, so spaces after commas no longer cause spurious validation failures

### Security
- **RUSTSEC-2026-0104** ([#623](https://github.com/rust-works/omni-dev/pull/623)): Bump `rustls-webpki` to 0.103.13 to address RUSTSEC-2026-0104

### Documentation
- **Worktree Convention** ([#622](https://github.com/rust-works/omni-dev/pull/622)): Document the project convention that new git worktrees live under `.work/` (gitignored) so worktrees stay scoped to the project rather than scattered across sibling directories

## [0.23.0] - 2026-04-22

### Added
- **MCP Server (Foundation)** ([#575](https://github.com/rust-works/omni-dev/issues/575)): New `omni-dev-mcp` binary exposes omni-dev's git analysis as MCP tools for AI assistants (Claude Desktop, Claude Code). Gated behind the `mcp` Cargo feature so default builds are unaffected. Initial tool: `git_view_commits`. See [ADR-0021](docs/adrs/adr-0021.md) for the architectural rationale.
- **MCP Resource System** ([#606](https://github.com/rust-works/omni-dev/issues/606)): Content is now addressable over MCP via three URI schemes — `git://repo/commits/{range}`, `jira://issue/{key}[.adf]`, and `confluence://page/{id}[.adf]` — so AI clients can fetch commits, JIRA issues, and Confluence pages as MCP resources without issuing tool calls. Resource templates, listings, and reads are advertised through the server's `resources` capability. JIRA and Confluence resources return JFM markdown by default or raw ADF JSON when the URI ends in `.adf`. See `STYLE-0026` in [docs/STYLE_GUIDE.md](docs/STYLE_GUIDE.md) for MCP tool/resource authoring conventions.
- **JIRA Custom Field Read** ([#594](https://github.com/rust-works/omni-dev/issues/594)): `jira read` accepts `--fields <name>` (repeatable) and `--all-fields` to request custom fields alongside the default set. Scalar custom fields are rendered into the frontmatter `custom_fields:` map; ADF rich-text custom fields are rendered as tagged body sections (`<!-- field: Name (id) -->`) in the JFM document
- **JIRA Custom Field Write** ([#594](https://github.com/rust-works/omni-dev/issues/594)): `jira write` and `jira create` now support writing custom fields. Frontmatter `custom_fields:` entries and body sections delimited by `<!-- field: Name (id) -->` are resolved through `/editmeta` (write) and `/createmeta` (create), dispatching by schema type (option, radio, array, textfield, number, date, and rich-text ADF). A new `--set-field NAME=VALUE` CLI flag allows inline overrides; CLI values take precedence over frontmatter scalars for the same name. Rejected when combined with `--format adf`
- **MCP Tool: `git_branch_info`** ([#600](https://github.com/rust-works/omni-dev/issues/600)): Mirrors `omni-dev git branch info`. Returns repository information as YAML for commits between a base branch and `HEAD`.
- **MCP Tool: `git_check_commits`** ([#600](https://github.com/rust-works/omni-dev/issues/600)): Mirrors `omni-dev git commit message check`. Returns the full check report plus pass/fail summary and exit code (honouring `strict`) as a structured payload so the assistant can act on validation failures without parsing exit codes.
- **MCP Tool: `git_twiddle_commits`** ([#600](https://github.com/rust-works/omni-dev/issues/600)): Mirrors `omni-dev git commit message twiddle --auto-apply`. Forces non-interactive semantics — never starts an editor. Accepts a `dry_run` flag that returns the proposed amendments as YAML without applying them.
- **MCP Tool: `git_create_pr`** ([#600](https://github.com/rust-works/omni-dev/issues/600)): Mirrors the content-generation phase of `omni-dev git branch create pr`. Returns the AI-drafted PR title and description as YAML; does not push the branch or invoke `gh pr create`.
- **MCP JIRA Sprint Tools** ([#602](https://github.com/rust-works/omni-dev/issues/602)): `jira_sprint_list`, `jira_sprint_issues`, `jira_sprint_add`, `jira_sprint_create`, and `jira_sprint_update` — list, inspect, populate, create, and transition agile sprints from MCP-aware AI assistants
- **MCP JIRA Watcher Tools** ([#602](https://github.com/rust-works/omni-dev/issues/602)): `jira_watcher_list`, `jira_watcher_add`, `jira_watcher_remove` — manage watchers on a JIRA issue by Atlassian account ID
- **MCP JIRA Worklog Tools** ([#602](https://github.com/rust-works/omni-dev/issues/602)): `jira_worklog_list` and `jira_worklog_add` — list worklog entries and log time on an issue (accepts JIRA's `1h 30m` duration format)
- **MCP JIRA Field Tools** ([#602](https://github.com/rust-works/omni-dev/issues/602)): `jira_field_list` (with optional substring filter) and `jira_field_options` (auto-discovers context when not specified) — discover custom fields and their permitted option values
- **MCP JIRA Board Tools** ([#602](https://github.com/rust-works/omni-dev/issues/602)): `jira_board_list` and `jira_board_issues` — enumerate agile boards and their issues, with optional project/type/JQL filters
- **MCP JIRA Attachment Tools** ([#602](https://github.com/rust-works/omni-dev/issues/602)): `jira_attachment_download` (filename substring filter) and `jira_attachment_images` (PNG/JPEG/GIF/SVG/WebP only) — download attachments to disk and return YAML metadata including the on-disk path; defaults to a fresh temp dir so the assistant can read files via its filesystem tool
- **MCP JIRA Project & Changelog Tools** ([#602](https://github.com/rust-works/omni-dev/issues/602)): `jira_project_list` and `jira_changelog` — enumerate projects and inspect issue change history
- **MCP JIRA Delete Tool (Destructive)** ([#602](https://github.com/rust-works/omni-dev/issues/602)): `jira_delete` deletes an issue irreversibly. The tool requires the assistant to explicitly pass `confirm: true`; without it, the call is rejected before any API contact. The required-confirmation guard was preferred over a Cargo feature flag to keep the MCP build surface uniform while still preventing accidental deletions through MCP.
- **MCP JIRA Tools — Read/Search** ([#601](https://github.com/rust-works/omni-dev/issues/601)): `jira_read` fetches an issue as JFM markdown (default) or raw ADF JSON; `jira_search` runs a JQL query and returns matching issues as YAML.
- **MCP JIRA Tools — Create/Write** ([#601](https://github.com/rust-works/omni-dev/issues/601)): `jira_create` creates a new JIRA issue from `project`/`summary`/`description`/`issue_type`; `jira_write` updates an existing issue's description from JFM markdown or raw ADF.
- **MCP JIRA Tools — Transition/Comment** ([#601](https://github.com/rust-works/omni-dev/issues/601)): `jira_transition` lists workflow transitions (or executes one by name/id with an optional comment); `jira_comment` lists or posts comments on a JIRA issue.
- **MCP JIRA Tools — Link/Dev** ([#601](https://github.com/rust-works/omni-dev/issues/601)): `jira_link` lists issue links, lists available link types, creates, or removes links; `jira_dev` returns linked pull requests, branches, and repositories as YAML.
- **MCP Confluence Tools** ([#603](https://github.com/rust-works/omni-dev/issues/603)): Six Confluence tools wired through the MCP server — `confluence_read`, `confluence_search`, `confluence_create`, `confluence_write`, `confluence_delete`, and `confluence_download`. Each mirrors its CLI counterpart and reuses the same `AtlassianClient` / `ConfluenceApi` implementation. `confluence_delete` requires an explicit `confirm: true` parameter to prevent accidental deletion; `confluence_download` writes page trees to a caller-provided directory (or a tempdir) and returns a YAML manifest summary.
- **MCP `atlassian_convert` Tool** ([#603](https://github.com/rust-works/omni-dev/issues/603)): Stateless JFM ↔ ADF conversion tool exposed over MCP. Takes `content` and `direction` (`"to-adf"` or `"from-adf"`) plus optional `compact` / `strip_local_ids` flags. No API client required.
- **MCP Confluence Children Tool** ([#604](https://github.com/rust-works/omni-dev/issues/604)): `confluence_children` MCP tool lists child pages of a Confluence page or top-level pages in a space, with optional `recursive` and `max_depth` parameters mirroring the `atlassian confluence children` CLI command
- **MCP Confluence Comment Tools** ([#604](https://github.com/rust-works/omni-dev/issues/604)): `confluence_comment_list` (auto-paginated, with a `limit` parameter where `0` means unlimited) and `confluence_comment_add` (markdown body, converted to ADF before posting) MCP tools mirror the `atlassian confluence comment` CLI subcommands
- **MCP Confluence Label Tools** ([#604](https://github.com/rust-works/omni-dev/issues/604)): `confluence_label_list`, `confluence_label_add`, and `confluence_label_remove` MCP tools mirror the `atlassian confluence label` CLI subcommands
- **MCP Confluence User Search Tool** ([#604](https://github.com/rust-works/omni-dev/issues/604)): `confluence_user_search` MCP tool searches Confluence users by display name or email, with a `limit` parameter (`0` = unlimited, default 25)
- **MCP AI Tools** ([#605](https://github.com/rust-works/omni-dev/issues/605)): Adds `ai_chat` (single-turn chat with the configured AI model) and `claude_skills_sync` / `claude_skills_clean` / `claude_skills_status` (worktree-aware skill distribution) as MCP tools. `ai_chat` is single-response (no streaming); skills tools accept a `worktrees` flag and a `format` selector (`"text"` or `"yaml"`). The skill-mutation tools operate relative to the MCP server process's current working directory.
- **MCP Config & Auth Tools** ([#605](https://github.com/rust-works/omni-dev/issues/605)): Adds `config_models_show` (returns the embedded `models.yaml` listing every AI model the CLI knows about) and `atlassian_auth_status` (reports Atlassian credential presence per scope as boolean flags, never leaking secret values). A new `atlassian::auth::status()` helper provides the read-only check.
- **MCP Server Hardening** ([#607](https://github.com/rust-works/omni-dev/issues/607)): Production hardening for the MCP server — input validators at the tool boundary (`range`, `repo_path`, JIRA key, Confluence id) return clear `invalid_params` errors on malformed input; a 100 KB default response cap with a `truncated`/`original_bytes`/`limit_bytes` marker for oversized payloads; cancellation support via the rmcp `CancellationToken` that returns early from blocking git work when a client cancels; per-tool `debug!` logs on invocation and a startup `info!` event listing compiled-in feature flags; an integration test that drives the binary and asserts every stdout frame is a valid JSON-RPC message. CI now runs `cargo build`/`test`/`clippy` with and without `--features mcp`.
- **Claude Skills Status** ([#558](https://github.com/rust-works/omni-dev/issues/558)): New `ai claude skills status` subcommand reports the symlinks and managed exclude-block entries left behind by prior `sync` runs, with `--worktrees` to aggregate across every worktree and `--format {text,yaml}` for machine-readable output
- **Claude Skills YAML Output** ([#558](https://github.com/rust-works/omni-dev/issues/558)): `ai claude skills sync` and `ai claude skills clean` now accept `--format {text,yaml}` (default `text`), producing a structured YAML report when invoked with `--format yaml`

### Changed
- **Claude Skills Exclude File Format** ([#558](https://github.com/rust-works/omni-dev/issues/558)): Entries in `.git/info/exclude` written by `sync` are now wrapped in a managed `# BEGIN omni-dev-skills (managed — do not edit)` / `# END omni-dev-skills` block so that `clean` can reverse the operation precisely. Clean break: bare `.claude/skills/*/` lines written by earlier builds are not recognised by the new `clean` and must be removed manually

## [0.22.0] - 2026-04-19

### Added
- **Confluence Bulk Space Download** ([#556](https://github.com/rust-works/omni-dev/issues/556)): `confluence download` now accepts `--space <KEY>` to recursively download every page in a Confluence space, plus `--title-filter` to download only pages whose title contains a substring (case-insensitive)
- **Confluence Children Command** ([#557](https://github.com/rust-works/omni-dev/issues/557)): New `confluence children` command for traversing page hierarchies, with `--space` and `--recursive` options for whole-space and recursive subtree listing
- **JIRA Comment Pagination** ([#543](https://github.com/rust-works/omni-dev/issues/543)): `jira comment list` now auto-paginates the JIRA comments REST API (previously only the first page was returned) and accepts a `--limit` flag to cap the total number of comments (use `0` for unlimited)
- **Confluence Comment Pagination** ([#544](https://github.com/rust-works/omni-dev/issues/544)): `confluence comment list` now auto-paginates the underlying API, returning every comment rather than just the first page
- **YAML Stream Output** ([#546](https://github.com/rust-works/omni-dev/issues/546)): New `-o yamls` output format emits results as `---`-separated YAML documents (YAML multi-document streams). For sequence results, each item becomes its own document; other values emit as a single `---`-prefixed document. Streams parse with standard YAML tooling (`yaml.safe_load_all()`, `yq`) and enable future per-page streaming for auto-paginated commands.
- **JSONL Output Format** ([#545](https://github.com/rust-works/omni-dev/issues/545)): New `-o jsonl` output format emits results as newline-delimited JSON (one JSON value per line), suitable for streaming consumption by `jq`, `fx`, and other line-oriented JSON tools
- **Claude Skills Sync/Clean** ([#558](https://github.com/rust-works/omni-dev/issues/558)): New `claude skills sync` and `claude skills clean` subcommands for worktree-aware skill distribution, allowing skills to be propagated consistently across multiple git worktrees of the same project

### Changed
- **CI Coverage**: Switched coverage tooling from `cargo-tarpaulin` to `cargo-llvm-cov` ([#589](https://github.com/rust-works/omni-dev/issues/589)) to eliminate `.await?` false negatives and improve accuracy for async code, macros, and generics
- **Atlassian Command Refactor** ([#281](https://github.com/rust-works/omni-dev/issues/281)): Extract `run_*` handler functions from the `execute` methods across Atlassian CLI commands, consolidating parameter structs (`DownloadParams`) and expanding wiremock coverage for every handler

### Fixed
- **Null ADF Input** ([#591](https://github.com/rust-works/omni-dev/issues/591)): `convert from-adf` no longer errors on `null` JSON input (empty Jira descriptions); null is now interpreted as an empty ADF document
- **CommonMark Code Span Handling** ([#578](https://github.com/rust-works/omni-dev/issues/578)): `convert to-adf` no longer splits code-marked text containing backtick characters. Code-span rendering now picks the minimum backtick delimiter length that avoids collisions and applies the CommonMark space-padding rule; the parser matches multi-backtick delimiters with exact-length matching and strips padding spaces symmetrically
- **Integer vs Float Table Width** ([#577](https://github.com/rust-works/omni-dev/issues/577)): ADF→JFM→ADF round-trips no longer coerce integer table width values to floats. A shared `parse_numeric_attr`/`fmt_numeric_attr` helper pair preserves integer-vs-float JSON type based on whether the source had a decimal point
- **Integer/Float in Numeric Attributes** ([#555](https://github.com/rust-works/omni-dev/issues/555)): `layoutColumn`, `mediaSingle`, and file-attachment `media` width/height/`mediaWidth` attributes now preserve the original integer-vs-float JSON number type on round-trip, preventing byte-level JSON inequalities and potential schema validation failures
- **Combined Emoji ShortNames** ([#576](https://github.com/rust-works/omni-dev/issues/576)): Emoji nodes with combined shortNames like `:slightly_smiling_face::bow:` are no longer split into multiple nodes during ADF→JFM→ADF round-trips. The inline parser now extends the match through adjacent `:name:` shortcodes before checking for a trailing attribute directive
- **Pipe Characters in Table Cells** ([#579](https://github.com/rust-works/omni-dev/issues/579)): Literal `|` characters inside pipe-table cell content (including inside inline code spans) are now escaped as `\|` on render and parsed as literal pipes on input, preventing table cells from being split at non-separator pipes
- **Code-Block & Unicode Emoji Shortcodes** ([#552](https://github.com/rust-works/omni-dev/issues/552)): Two ADF↔Markdown round-trip bugs fixed: hardBreak continuation no longer consumes 2-space-indented fenced code blocks or `:::` container directives as continuation lines, and emoji shortcode escaping now uses Unicode `is_alphanumeric` (matching the parser) rather than ASCII-only, so patterns like `:Café:` and `:配置:` are escaped and survive round-trips intact
- **Card URL Brackets** ([#553](https://github.com/rust-works/omni-dev/issues/553)): `::card[...]` / `:card[...]` directives now round-trip correctly even when the URL contains unbalanced `]` or newline characters. URLs that cannot be safely embedded in bracket content are emitted using the quoted `url` attribute form. Additionally, bare URLs inside emphasis, strikethrough, link labels, bracketed spans, or span directives are no longer incorrectly promoted to `inlineCard` nodes (which was silently dropping the enclosing marks)
- **Spurious order=1 on orderedList** ([#547](https://github.com/rust-works/omni-dev/issues/547)): Converting an ADF `orderedList` with no attrs through JFM and back no longer adds a spurious `{"order": 1}` attrs field to the node
- **ADF Mark Ordering** ([#549](https://github.com/rust-works/omni-dev/issues/549)): Inline mark ordering is now preserved across ADF→JFM→ADF round-trips. Markdown wrappers are emitted in mark-array order (outermost first) rather than a fixed priority, so every permutation of `em`, `strong`, `strike`, `underline`, `link`, `annotation`, `textColor`, `backgroundColor`, and `subsup` survives with its original ordering intact
- **Media `occurrenceKey` & Attribute Quoting** ([#550](https://github.com/rust-works/omni-dev/issues/550)): The `occurrenceKey` attribute on `mediaSingle` and `mediaInline` nodes is now preserved on round-trip. Attribute values containing spaces, quotes, closing braces, or backslashes are now quoted and escaped correctly, so arbitrary `id`, `collection`, `url`, `alt`, `localId`, and `occurrenceKey` values round-trip losslessly
- **Annotation-Link Round-Trip Test URL** ([#574](https://github.com/rust-works/omni-dev/pull/574)): Correct the URL used in the annotation-link round-trip test (typo fix)
- **Confluence User Search** ([#542](https://github.com/rust-works/omni-dev/issues/542)): `confluence user search` no longer fails with `missing field 'accountId'` deserialization errors. The response parser now reads the nested `user` object returned by `/wiki/rest/api/search/user` and tolerates user records (such as app users or deactivated users) that omit `accountId`.
- **Empty Task Checkboxes** ([#548](https://github.com/rust-works/omni-dev/issues/548)): Recognise `- [ ]` and `- [x]` markdown task markers even when the checkbox is not followed by a trailing space. Fixes ADF round-trip drift where empty `taskItem` nodes were parsed as `listItem` nodes containing literal `[ ]` text
- **Literal Checkbox Text in Bullet Lists** ([#548](https://github.com/rust-works/omni-dev/issues/548)): Escape the leading `[` when rendering a `bulletList` item whose literal text begins with a sequence that looks like a task checkbox marker (`[ ]`, `[x]`, or `[X]` followed by space, newline, or end), preventing `to-adf` from falsely promoting these bullet items to `taskList`/`taskItem` on round-trip
- **ADF Round-Trip URL Brackets** ([#551](https://github.com/rust-works/omni-dev/issues/551)): Preserve square brackets in URLs embedded in link-marked text so ADF→JFM→ADF round-trips no longer leak `\[`/`\]` escapes or split the text into corrupted `inlineCard` nodes
- **ADF Mark Combinations** ([#554](https://github.com/rust-works/omni-dev/issues/554)): `textColor`, `backgroundColor`, `subsup`, `underline`, and `annotation` marks were silently dropped when combined with a `code` mark or with each other. Marks are now preserved by nesting `:span[…]{…}` and `[…]{…}` wrappers based on the original mark order
- **Underscore boundary in span directives** ([#554](https://github.com/rust-works/omni-dev/issues/554)): `from-adf` now escapes underscores at text-node boundaries (per the CommonMark intraword rule). Previously, a plain text node ending with `_ ` followed by a textColor span whose text started with `_` produced JFM that the parser saw as italic, destroying the span directive and losing the textColor mark

## [0.21.0] - 2026-04-17

### Added
- **Confluence Label Commands**: Label management commands for adding, removing, and listing page labels
- **Confluence User Search**: Search for Confluence users by query
- **Confluence Page Comments**: List and add comments on Confluence pages
- **JIRA Watcher Commands**: List, add, and remove watchers on JIRA issues
- **JIRA Worklog Commands**: Time tracking with worklog list and add commands
- **JIRA Sprint Management**: Create and update sprint commands
- **JIRA Dev Status**: Dev status command for viewing development information on issues
- **PR Auto-Push**: `--no-push` flag to skip branch push on PR creation
- **ADF Node Support**: Added `mediaInline`, `placeholder`, `table caption`, `mediaSingle caption`, and `expand localId/parameters` node support
- **ADF Annotation Marks**: Full annotation mark support for ADF/markdown conversion
- **ADF localId Round-Trip**: Comprehensive `localId` round-trip support with `--strip-local-ids` option
- **ADF Border Mark**: Border mark support for media and table cell/header nodes
- **ADF Breakout Width**: Optional width parameter for breakout marks

### Fixed
- **ADF Round-Trip Fidelity** (50+ fixes): Extensive improvements to ADF/markdown round-trip conversion:
  - Preserve mark ordering (link, annotation, strong/em/strike, code) across conversions
  - Prevent consecutive paragraphs from merging in blockquotes, list items, and task items
  - Preserve `localId` on caption, listItem/mediaSingle, layout columns, media, table, and paragraph nodes
  - Handle nested taskList, taskItem, and ordered list nodes correctly
  - Preserve hardBreak nodes in paragraphs, headings, list items, and table cells
  - Preserve trailing/leading whitespace in headings, list items, table cells, and text nodes
  - Escape backticks, backslashes, asterisks, and underscores in plain text to prevent misinterpretation
  - Preserve NBSP content in list item paragraphs and NBSP-only paragraphs
  - Preserve empty language attrs in codeBlock, empty attrs on tableCell, and integer colwidth values
  - Handle parentheses in link/image URLs and bracket-link ambiguity
  - Prevent bare URLs and URL link text from becoming inlineCard nodes
  - Preserve emoji shortName with/without colons, date timestamps, and mention localId attribution
  - Preserve parameters block in bodiedExtension and extension layout/localId attrs
  - Preserve multiple annotation marks, table cell attrs, embedCard attrs, and mediaSingle mode
  - Preserve content after directive table blocks and whitespace-only text after hardBreak
  - Reject pipe syntax for tableCell-only first rows and intraword underscores as emphasis
  - Escape trailing double-spaces to prevent hardBreak misinterpretation
  - Ensure blank lines between consecutive block nodes in table cells
- **Deterministic AI Scopes**: Make AI-generated commit scopes deterministic by post-processing with file-pattern logic
- **CI Scope Guidance**: Improve CI scope guidance in type selection rules

### Changed
- **Function Naming**: Rename `execute_*` helper functions to `run_*` across Atlassian CLI commands
- **Function Extraction**: Extract `run_download`, `run_images`, `run_transition` into standalone functions

### Documentation
- **ADR-0020**: Add ADR for JFM markdown dialect for ADF interchange
- **JFM Spec**: Expand JFM spec with table, media, localId, escaping, and annotation mark sections
- **Style Guide**: Update style guide to clarify `run_*` function extraction rules and add testing style rules

### CI/CD
- Bump `EmbarkStudios/cargo-deny-action` to 2.0.17
- Bump `codecov/codecov-action` from 5 to 6
- Bump `cachix/cachix-action` from 16 to 17
- Bump Rust minor patch dependencies

### Security
- Update `rustls-webpki` to 0.103.12 for RUSTSEC-2026-0098

## [0.20.0] - 2026-04-12

### Added
- **Confluence Download**: Recursive page tree download with concurrent workers
  - BFS tree traversal via Confluence children API
  - Bounded parallel downloads with configurable concurrency (`--concurrency`)
  - Directory tree mirroring with `{id}-{slug}/index.{md,json}` structure
  - Manifest-based resume (`--resume`) with ID-aware page tracking
  - Per-page `meta.json` with untruncated titles and parent IDs
  - Backup-before-clobber with `--on-conflict backup|skip|overwrite`
  - Append-mode `download.log` recording all actions per run
  - Configurable max depth (`--max-depth`)
- **Structured Output**: `--output json|yaml|table` flag on all list/table commands
  - Added `Serialize` derives to all public Atlassian data types
  - `OutputFormat` enum with `output_as()` helper
- **HTTP 429 Rate Limiting**: Automatic retry with `Retry-After` header support
  - All transport methods (`get_json`, `post_json`, `put_json`, `delete`, `get_bytes`) retry on 429
  - Exponential backoff fallback when no `Retry-After` header is present
  - Configurable max retries (default: 3)

### Fixed
- **Nested Container Directives**: Container directives (`:::expand`, `:::panel`) inside table cells, layout columns, and other containers are now correctly parsed with depth tracking
- **hardBreak in Table Cells**: Tables containing `hardBreak` nodes now fall back to directive form instead of pipe tables, preventing row corruption on round-trip
- **Multi-Paragraph Containers**: Panels, expands, layout columns, and extensions with multiple paragraphs now render with blank-line separators, preserving paragraph boundaries on round-trip
- **Commit Message Generator**: Added type selection rules to align generator with checker expectations — prevents incorrect type selection (e.g., `docs` for source code changes)

## [0.19.0] - 2026-04-10

### Added
- **Atlassian Integration**: Comprehensive JIRA and Confluence CLI commands via JFM (JIRA-Flavored Markdown) format
  - Read and write JIRA and Confluence content as JFM markdown
  - JIRA issue create, delete, search (JQL), and transition commands
  - JIRA comment list and add commands
  - JIRA issue link management and link list commands
  - JIRA issue attachment download commands
  - JIRA issue changelog command
  - JIRA agile board and sprint management commands
  - JIRA project list and field listing/options commands
  - Auto-discover field context when fetching JIRA field options
  - Confluence search (CQL), create, and delete commands with purge flag
  - `post_json` and `delete` methods on `AtlassianClient`
- **Auto-Pagination**: Automatic pagination for all Atlassian API methods
- **Claude CLI Model Resolve**: `claude cli model resolve` command for model resolution diagnostics

### Changed
- **CLI Help Text**: Improved help text and converted key arguments to positional for Atlassian commands

### Fixed
- **Confluence Delete Error**: Improved 404 error message for confluence delete command
- **JIRA Search Endpoint**: Updated search endpoint and handle missing `total` field in response

### Security
- **CI Hardening**: Pin `cargo-deny-action` version and update vulnerable dependencies

### Testing
- **Field Context Error Handling**: Added test for `get_field_contexts` 404 error response

### Documentation
- **Atlassian User Guide**: Comprehensive command reference for all Atlassian CLI commands
- **JFM Specification**: Moved JFM spec from plan to specs directory with revised content
- **Atlassian Scope**: Added `atlassian` scope to scope list
- **v0.18.0 Retrospective**: Added release retrospective document

## [0.18.0] - 2026-02-26

### Added
- **Split Dispatch for Large Diffs**: Intelligent per-file diff splitting when commits exceed token budgets
  - Per-file and per-hunk unified diff parser for granular diff handling
  - Per-file diff storage with `FileDiffRef` struct tracking byte lengths
  - Greedy file-packing algorithm for token-budget-constrained splitting
  - Split dispatch across amendment, check, and multi-commit operations
  - Per-hunk diff override support for partial commit views
  - Placeholder substitution for oversized diffs instead of hard failures
- **File-Level Context Analysis**: Hook-based file analyzer adds semantic context to commit pipelines
- **Walk-Up Config Directory Discovery**: Config resolution walks up the directory tree to find `.omni-dev/` directories
- **XDG Base Directory Compliance**: Config resolution follows XDG standards for fallback locations
- **`OMNI_DEV_CONFIG_DIR` Environment Variable**: Explicit config directory override for all commands
- **Config Source Tracking**: Diagnostic output shows where each config file was loaded from
- **`--quiet` Flag**: Suppress interactive retry prompts in twiddle
- **`--context-dir` Option**: Explicit context directory for create-pr command
- **`--refine` Flag**: Opt-in to refine mode (fresh mode is now the default for twiddle)
- **Amendment Parse Retry Logic**: Automatic retry on amendment parse and AI request failures
- **Claude Sonnet 4.6 Model**: Added to model registry and set as default
- **Preflight Checks Expanded**: Applied consistently to amend, view, and info commands
- **Configurable Mock AI Client**: Shared test utility for integration testing

### Changed
- **Fresh Mode Default**: Twiddle now generates messages from scratch by default; use `--refine` to amend existing messages
- **Default AI Model**: Claude Sonnet 4.6 replaces previous default
- **Registry Default Model**: Uses model registry default instead of hardcoded model strings

### Removed
- **`--batch-size` Flag**: Removed deprecated flag from check and twiddle commands (use `--concurrency` instead)
- **Progressive Diff Reduction**: Removed fallback strategy in favor of split dispatch
- **Dead Utils Module**: Removed unused `utils/general` module and its re-exports

### Fixed
- **Batch Processing Failure Tracking**: Track failed commit indices when batch processing errors occur
- **Progress Counter**: Increment only on success path to avoid inflated counts
- **Split Dispatch Overhead**: Correctly subtract prompt overhead from chunk capacity
- **Token Estimation**: Use conservative estimation for code diffs
- **Error Chain Display**: Print full error chain for failed commits in twiddle
- **Tracing Output**: Write tracing output to stderr instead of stdout
- **Stdin EOF Loop**: Prevent infinite loop on EOF in interactive prompts
- **Provider String Matching**: Use exact string matching in prompt_style selection
- **Field Presence Tracking**: Add missing `branch_prs[].base` field
- **Batch-Size Deprecation**: Proper deprecation warning for `--batch-size` flag

### Security
- **Dependency Advisories**: Update `bytes` and `git2` to resolve security advisories
- **CI Hardening**: Add security audit, dependency policy, and secret scanning workflows

### Refactored
- **Generic Repository View**: Make `RepositoryView` and `CommitInfo` generic over inner types
- **Single Async Runtime**: Migrate command execution to single tokio runtime
- **Consolidated Config Resolution**: Unified config resolution into discovery module
- **Panic-Free Operations**: Replace panicking operations with proper error handling across all modules
- **Pure Logic Extraction**: Extract and test pure logic from twiddle, create-pr, and check commands
- **Interactive Retry Extraction**: Extract interactive retry loop and `read_interactive_line` helper into testable methods
- **Shared Formatting Utilities**: Extract formatting module for reuse across commands
- **AI Client Helpers**: Extract shared helpers for AI client implementations
- **Deduplicated Models Embed**: Shared constant for `models.yaml` embedding

### Testing
- **Property-Based Tests**: Added proptest-based tests across 7 modules
- **Comprehensive Unit Tests**: Added tests for 6+ previously untested modules
- **Split Dispatch Integration Tests**: Integration tests with prompt recording
- **Test Directory Isolation**: Relocated temp directories to project-local `tmp/` folder
- **Parallel Test Safety**: Fix process-wide CWD mutation causing parallel test failures

### Documentation
- **Architecture Decision Records**: Added ADRs 0004–0019 covering embedded templates, hierarchical config resolution, two-view data model, preflight validation, deterministic pre-validation, token-budget batch planning, multi-layer retry, model registry, severity levels, self-describing YAML, provider-specific prompts, dual error handling, hierarchical CLI, per-file diff splitting, context detection, and ecosystem scope auto-detection
- **Architecture Documentation**: Comprehensive architecture docs for the codebase
- **Style Guide**: Broadened scope, added STYLE-0022 (ADR format) and STYLE-0023 (commit validation)
- **Config Resolution Docs**: Four-tier config resolution with walk-up discovery and XDG support

### CI/CD
- **Code Coverage Enforcement**: Enforce minimum coverage threshold and fail on codecov errors
- **GitHub Actions Updates**: Bump actions/checkout to v6, codecov-action to v5, cachix/install-nix-action to v31, cachix/cachix-action to v16
- **Clippy Pedantic Lints**: Enable pedantic and nursery lint groups

### Dependencies
- `crossterm` 0.28 → 0.29
- `dirs` 5.0 → 6.0
- `ssh2-config` 0.6 → 0.7
- `thiserror` 1.x → 2.x
- `reqwest` 0.12 → 0.13
- Rust minor-patch group with 13 updates

## [0.17.0] - 2026-02-13

### Added
- **Ecosystem Default Scopes**: Automatic scope detection based on project ecosystem
  - Detects Rust, Node.js, Python, Go, and Java projects from marker files (Cargo.toml, package.json, etc.)
  - Merges ecosystem-specific default scopes (e.g., `cargo`, `lib`, `core`, `test` for Rust)
  - Skips defaults that conflict with existing custom scopes in `scopes.yaml`
  - Works consistently across twiddle, check, and PR creation commands
- **Scope Pre-Validation**: Deterministic scope checks before AI processing
  - Validates scope format (e.g., multi-scope comma separation without spaces)
  - Verifies scope validity against the merged scope list before sending to AI
  - Passing checks recorded in `pre_validated_checks` field so the AI skips re-checking them
  - Prevents AI from contradicting deterministic validations

### Fixed
- **Config Loading**: Always load `.omni-dev/` configuration regardless of directory existence
  - Previously skipped config loading when the context directory didn't exist as a directory
  - Now correctly resolves individual config files even when the parent directory is absent
  - Fixes scope and guideline loading in projects without an explicit `.omni-dev/` directory

### Refactored
- **Scope Loading Consolidation**: Unified scope loading across all commands
  - Extracted `load_project_scopes()` as a single entry point for scope resolution
  - Consistent config file priority (local override → project → home fallback) everywhere
  - Eliminated duplicated scope loading logic between twiddle and check commands

### Documentation
- **Configuration Best Practices**: New guide for `.omni-dev/` configuration
  - Scope definition patterns, file pattern matching, and local override workflows
  - Troubleshooting guide for common configuration issues
- **Configuration Internals**: New technical reference for configuration resolution
  - Detailed explanation of config file priority, ecosystem detection, and scope merging
  - Architecture diagrams for the discovery pipeline

## [0.16.0] - 2026-02-12

### Added
- **Parallel Map-Reduce Processing**: Replaced sequential batch processing with concurrent commit processing
  - Each commit processed individually in parallel using semaphore-based concurrency control
  - New `--concurrency` flag (default: 4) replaces deprecated `--batch-size`
  - Real-time progress feedback with atomic completion counters
  - Graceful failure handling continues processing remaining commits
- **Cross-Commit Coherence Pass**: Optional AI refinement for consistency across commit messages
  - Ensures consistent scope usage, terminology, and message quality across a commit set
  - New `--no-coherence` flag to skip the coherence pass when not needed
  - Automatically skipped when all commits fit in a single batch
- **Token-Budget-Aware Commit Batching**: Intelligent grouping using first-fit-decreasing bin-packing
  - Groups commits into batches that fit within the AI model's token budget
  - Estimates tokens from file metadata without reading full content
  - Split-and-retry fallback for oversized batches with progressive diff reduction
  - Reduces API calls from O(n) to O(batches) while maintaining quality
- **Progressive Diff Reduction**: Four-level fallback for token budget optimization
  - Automatically reduces diff detail when prompts exceed model limits: Full → Truncated → StatOnly → FileListOnly
  - Precise truncation calculations with tokens-to-chars conversion
  - Maximizes context sent to AI while respecting model constraints
- **Token Budget Validation**: Pre-flight token estimation and budget check before all AI requests
  - Estimates prompt token count using a character-based heuristic with 10% safety margin
  - Validates prompts fit within the model's input context window minus reserved output tokens
  - Returns a clear `PromptTooLarge` error instead of letting the API reject oversized requests
  - Covers all AI call paths: twiddle, check, PR creation, and raw message sending
- **HTTP Request Timeout Configuration**: Configurable timeout for AI client HTTP requests
- **Enhanced YAML Formatting**: Improved multi-line commit message formatting in YAML output

### Changed
- **Deprecated `--batch-size`**: Replaced by `--concurrency` flag with clearer semantics; `--batch-size` remains as a hidden backward-compatible alias

### Refactored
- **Module Structure Flattening**: Converted `mod.rs` files to direct module files across claude, cli, data, and git modules
- **Git CLI Split**: Split monolithic git module into focused subcommand modules
- **YAML Payload Reduction**: Reduced per-commit YAML payload size for more efficient AI analysis
- **Dead Code Removal**: Removed unused core module scaffolding

### Fixed
- **Error Handling**: Improved error handling and configuration parsing in AI client

### Documentation
- **Architecture Decision Records**: Introduced ADR framework with ADR-0001 (YAML as primary data exchange format)
- **Style Guide Enhancements**: Added tag-based categorization system, task-to-tag lookup table, and STYLE-0020 single-purpose commit guidelines
- **Commit Guidelines**: Enhanced with multi-scope support and practical examples
- **Module Layout Guidance**: Refined examples and guidance for module organization
- **Documentation Updates**: Updated all docs to reflect `--concurrency` replacing `--batch-size`

## [0.15.0] - 2026-02-08

### Added
- **Beta Header Support**: New `--beta-header` flag for twiddle and check commands
  - Enables enhanced model capabilities like 1M context window and 128K output tokens
  - Format: `--beta-header key:value` (e.g., `--beta-header anthropic-beta:context-1m-2025-08-07`)
  - Validates beta headers against the model registry with helpful error messages
  - Beta-aware token limits automatically applied to API requests and display
  - Debug logging for active beta headers sent with API requests
- **Interactive Chat Command**: New `omni-dev chat` command for conversational AI interaction
  - Interactive Claude AI chat session with streaming-style responses
  - Configurable system prompts and model selection
  - Multi-line input support and conversation history
- **Interactive Twiddle Mode for Check**: New `--twiddle` flag on check command
  - Automatically runs twiddle to fix failing commit messages after check identifies issues
  - Streamlined workflow for validating and correcting commits in one step
- **Intelligent Retry Mechanism**: Smart retry for twiddle commit validation
  - Automatically retries failed commit message generation with refined prompts
  - Configurable retry limits with exponential backoff
  - Improved success rates for challenging commit messages
- **Deterministic Scope Pre-Validation**: Rule-based validation before AI processing
  - Catches common scope formatting issues (e.g., extra spaces) without API calls
  - Reduces unnecessary AI requests for deterministic formatting rules

### Changed
- **Model Catalog Update**: Updated AI model registry to February 2026
  - Added Claude Opus 4.6 as current flagship model
  - Added beta header definitions for models supporting extended context and output
  - Updated model specifications and tier classifications

### CI/CD
- **Enhanced Commit-Check Workflow**: Improved CI validation pipeline
  - Added concurrency control to prevent redundant workflow runs
  - Updated GitHub Actions to latest versions

### Documentation
- **Context Window Documentation**: Added documentation for context window limitations and fallback behavior

## [0.14.0] - 2026-02-08

### Added
- **Scope Refinement via File Patterns**: Intelligent scope detection that matches changed file paths against configured scope patterns from `.omni-dev/scopes.yaml`
  - Pattern matching using globset for project-specific scope rules
  - Specificity-based matching prioritizes more specific patterns
  - Support for negation patterns and multi-scope matching
  - Fallback to original detection when no patterns match
  - Applied across twiddle, check, and validation commands
- **Preflight Validation System**: Comprehensive early failure detection for AI and GitHub commands
  - AI provider detection and credential validation for Claude, Bedrock, OpenAI, and Ollama
  - GitHub CLI availability and authentication checks
  - Clear, actionable error messages with resolution guidance
  - Integrated into twiddle, create-pr, and check commands
- **Working Directory Validation**: Early cleanliness check before expensive twiddle operations
  - Detects staged changes, unstaged modifications, and untracked files
  - Provides detailed error messages showing specific uncommitted files
  - Prevents wasted AI processing time on dirty working directories
- **Model Parameter for create-pr**: Added `--model` flag to create-pr command for model selection

### Changed
- **Scope Definitions Loading**: Simplified and consolidated scope definitions loading logic in twiddle command
  - Scope refinement now works consistently with or without contextual intelligence
  - Same logic pattern applied to both full and batch processing modes

## [0.13.1] - 2025-01-07

### Fixed
- **Bedrock Client Selection Logic**: Fixed inverted conditional that prevented Bedrock from being used
  - Setting `CLAUDE_CODE_USE_BEDROCK=true` now correctly uses Bedrock client
  - Removed confusing `CLAUDE_CODE_SKIP_BEDROCK_AUTH` requirement
  - Users only need `CLAUDE_CODE_USE_BEDROCK=true`, `ANTHROPIC_AUTH_TOKEN`, and `ANTHROPIC_BEDROCK_BASE_URL`
- **CI Publish Ordering**: Publish to crates.io only after all platform builds succeed

### Added
- **Scope Definitions**: Added `release` and `workflows` scopes for better commit categorization
  - `release`: Version bumps, changelog updates, release preparation
  - `workflows`: GitHub Actions and CI/CD pipeline changes

### Changed
- **CI Commit Check**: Trigger commit validation on push to main branch
- **CI Workflow**: Removed version pinning from commit-check workflow

## [0.13.0] - 2025-12-27

### Added
- **Post-Twiddle Validation**: New `--check` flag for twiddle command
  - Automatically validates commit messages after applying amendments
  - Runs full AI-powered analysis against project guidelines
  - Supports batched processing for large commit ranges
  - Single-step workflow: improve and validate in one command
- **Guidance File Diagnostics**: Enhanced diagnostic output for loaded configuration
  - Shows status of commit guidelines, scopes, and other guidance files
  - Clear visibility into which configuration files are being used
  - Helps troubleshoot configuration issues
- **Scope Validation in Check**: Enhanced commit message checking with scope awareness
  - Validates commit scopes against project-defined scope list
  - Reports invalid or missing scopes as warnings

### Changed
- **CI Workflow Enhancement**: Added commit message validation for pull requests
  - New GitHub Actions workflow validates PR commit messages
  - Automatic quality enforcement on all pull requests

### Documentation
- **Release Process Restructure**: Comprehensive overhaul of release documentation
  - Reorganized for automated CI/CD workflow with clear manual vs automated steps
  - Added documentation review phase before version updates
  - Enhanced with CI monitoring commands and verification steps
  - Improved release skill with complete automation guidance
- **README Updates**: Added documentation for check command and new twiddle options
  - New section for commit message validation command
  - Updated options table with `--fresh` and `--check` flags

## [0.12.0] - 2025-12-25

### Added
- **Commit Message Validation Command**: New `check` command for validating commit messages against project guidelines
  - AI-powered analysis with configurable severity levels (error, warning, info)
  - Multiple output formats (text, JSON, YAML) for CI/CD integration
  - Batch processing support for large commit ranges
  - Smart exit codes for pipeline integration (0=pass, 1=errors, 2=warnings in strict mode)
  - Optional suggestion generation for improved commit messages
  - Color-coded severity indicators in text output
- **Fresh Mode for Twiddle**: Generate commit messages from scratch ignoring existing messages
  - New `--fresh` flag for twiddle command
  - Forces AI to analyze only diff content for completely fresh suggestions
  - Useful for poorly-written or misleading original messages
- **Base Branch Support**: Explicit base branch selection for PR creation and updates
  - New `--base` flag for `create pr` command
  - Intelligent base branch resolution with fallback logic
  - Interactive confirmation when changing base branch on updates
  - Better visibility of target branches in PR operations
- **Comprehensive Gemini Model Support**: Full Google Gemini model catalog
  - Gemini 3.0 Pro and Flash (preview models)
  - Gemini 2.5 series (Pro, Flash, Flash-Lite)
  - Legacy support for Gemini 2.0 and 1.5 series
  - Three-tier system (flagship, balanced, fast) for model selection

### Changed
- **AI Model Registry Update**: Updated to latest model releases (December 2025)
  - Added Claude 4.5 series (Opus, Sonnet, Haiku) as current generation
  - Updated default Claude model to claude-sonnet-4-5-20250929
  - Added OpenAI GPT-5.2, o3/o4 reasoning models, and GPT-4.1 series
  - Marked legacy models appropriately for deprecation visibility

### Refactored
- **Commit Guidelines Template**: Extracted default guidelines to shared template file
  - Single source of truth in `src/templates/default-commit-guidelines.md`
  - Consistent guidelines between twiddle and check commands
  - Easier maintenance and editing as markdown

### Documentation
- **Enhanced Commit Guidelines**: Comprehensive guidelines with severity levels
  - Detailed type and scope tables with clear use cases
  - Subject line rules with imperative mood requirements
  - Accuracy requirements section for truthful descriptions
  - Severity level mapping for CI/CD integration
- **Commit Message Check Plan**: Detailed implementation specification
  - Design philosophy for guideline-driven validation
  - Command structure and output format examples
  - CI integration patterns and exit code behavior

## [0.11.0] - 2025-12-10

### Added
- **Draft PR Support**: New draft PR functionality with configurable defaults
  - Added `--draft` flag to PR creation command for creating draft pull requests
  - Configurable default draft status via `.omni-dev/pr-config.yaml`
  - Enhanced PR workflow with draft mode for work-in-progress changes
- **No-AI Mode for Twiddle**: Direct YAML output without AI processing
  - Added `--no-ai` flag to twiddle command for direct YAML generation
  - Enables manual editing workflows without AI-powered amendment
  - Better integration with custom automation pipelines

### Documentation
- **AI-Generated PR Guidelines**: Comprehensive documentation for PR description generation
  - Detailed guidelines for AI-powered PR description creation
  - Best practices and examples for effective PR generation
  - Enhanced documentation for team collaboration

### Fixed
- **PR Creation Branch Handling**: Improved head branch parameter handling
  - Fixed explicit head branch parameter in `gh pr create` command
  - Better handling of upstream branch configuration
  - More reliable PR creation workflow

## [0.10.0] - 2025-09-30

### Added
- **Branch Information in Twiddle**: Enhanced twiddle repository view with branch information
  - Branch context now included in commit analysis and AI-powered amendments
  - Better understanding of current branch status for more targeted suggestions
  - Improved repository view completeness for AI assistants

### Enhanced
- **AI Model Configuration**: Updated default models to Claude Opus 4.1
  - Latest AI model specifications for improved performance
  - Enhanced model registry with updated token limits and capabilities
  - Better AI response quality and accuracy
- **PR Command User Experience**: Improved PR command UX by showing context early
  - Faster feedback for users during PR creation process
  - Better progress indicators and context display
  - Enhanced user interface clarity
- **PR Template Integration**: Enhanced PR template location exposure in repository views
  - PR template location now visible in repository analysis
  - Better integration between template system and PR creation workflow
  - Improved AI understanding of project PR standards

### Documentation
- **Comprehensive Scope Documentation**: Added detailed scope documentation and usage examples
  - Complete guide for scope usage patterns and best practices
  - Real-world examples and configuration scenarios
  - Enhanced developer documentation for project customization

## [0.9.0] - 2025-09-18

### Added
- **AI-Powered Pull Request Creation**: New `git create pr` command with intelligent PR generation
  - Automatically generates PR titles and descriptions using AI analysis of commits and diffs
  - Supports both interactive creation and save-only modes for review
  - Integrates with GitHub CLI for seamless PR creation and updates
  - Context-aware analysis using project-specific guidelines and branch information
- **PR Guidelines System**: Project-specific PR description guidelines support
  - New `.omni-dev/pr-guidelines.md` configuration file for PR generation guidance
  - Separate from commit guidelines to allow different standards for PRs vs commits
  - Local override support with priority: local > project > global
  - Integration with AI prompts for project-consistent PR descriptions
- **Enhanced PR Template**: Significantly improved `.github/pull_request_template.md`
  - Added comprehensive sections for testing, performance, security, and deployment
  - Better structure and guidance for thorough PR descriptions
  - Includes examples and best practices for different types of changes
- **YAML Output Format**: New structured output format for PR details
  - `pr-details.yaml` replaces `pr_description.md` for better structured data
  - Complete PR content serialization including title and description
  - Better integration with automation and tooling workflows

### Enhanced
- **Context-Aware AI Generation**: PR creation now uses full project context
  - Leverages branch analysis, work patterns, and architectural understanding
  - Project-specific scope validation and suggestions for PR organization
  - Enhanced prompts that incorporate both commit analysis and PR best practices
- **Command-Specific Guidance Display**: Improved user interface clarity
  - Twiddle command shows only commit guidelines (focused on commit messages)
  - PR creation command shows only PR guidelines (focused on PR descriptions)
  - Eliminates confusion about which guidelines are being used for each operation
- **Comprehensive Documentation**: Updated user guides and README
  - Added complete workflow documentation for PR creation feature
  - Enhanced examples and usage patterns for both commit and PR workflows
  - Better organization of feature documentation and command references

### Fixed
- **YAML Parsing Robustness**: Improved Claude API response processing
  - Better handling of markdown-wrapped YAML responses from AI
  - Consistent parsing logic across commit amendments and PR generation
  - Enhanced error diagnostics for malformed AI responses

## [0.8.0] - 2025-09-17

### Added
- **AI Model Configuration System**: New `config models show` command to view available AI models
  - Complete model registry with token limits and specifications
  - Support for both standard Claude and AWS Bedrock identifier formats
  - Model information display in twiddle command output
- **Interactive Amendment Editing**: `--edit` option for twiddle command
  - Integration with `OMNI_DEV_EDITOR` and `EDITOR` environment variables
  - Manual review and editing of AI-generated amendments before applying
- **Build Automation Script**: New `scripts/build.sh` for standardized builds
  - Combines cargo build, format checking, and clippy analysis
  - Comprehensive error handling and progress indicators

### Enhanced
- **Contextual Intelligence System**: Significantly improved commit message generation
  - Home directory fallback support for all `.omni-dev` configuration files
  - Literal template reproduction ensures AI follows project formats exactly
  - Enhanced diagnostic output showing guidance file status and sources
- **AI Client Logging**: Improved debugging and observability
  - Enhanced logging for API requests and responses
  - Better error handling and diagnostics for troubleshooting

### Removed
- **Commit Template System**: Removed template functionality to simplify configuration
  - Projects should use commit guidelines instead of templates
  - Eliminates conflicts between templates and guidelines
  - **BREAKING**: `.gitmessage` and commit template files are no longer loaded

## [0.7.0] - 2025-09-14

### Added
- **AWS Bedrock AI Client**: Complete integration with AWS Bedrock for Claude AI model access
  - Implemented `BedrockAiClient` with full AWS API support
  - Added comprehensive logging and diagnostics for troubleshooting
  - Support for AWS credentials and region configuration
  - Integration with existing `AiClient` trait architecture
- **AI Client Architecture**: Extensible AI provider system
  - New `AiClient` trait for pluggable AI providers
  - Provider selection and configuration management
  - Support for multiple AI service backends
- **Settings Management System**: Enhanced configuration handling
  - New settings management utilities for AI provider configuration
  - Environment-based configuration support
  - Structured settings validation and loading

### Improved
- **Code Quality**: Resolved clippy warnings for better maintainability
  - Fixed `vec_init_then_push` patterns with `vec![]` macro usage
  - Improved code consistency and performance

## [0.6.0] - 2025-09-09

### Added
- **File-based Amendment Workflow**: Complete overhaul of the twiddle command user experience
  - Save amendments to temporary YAML files instead of printing to stdout
  - Interactive menu system with [A]pply/[S]how/[Q]uit options 
  - File content preview functionality for reviewing changes before applying
  - Better user feedback and more granular control over amendment process
  - Preserved backward compatibility with `--auto-apply` and `--save-only` options

- **Local Configuration Overrides**: Personal workflow customization system
  - Support for `.omni-dev/local/` directory to override shared project settings
  - Local override capability for all configuration files (scopes, guidelines, templates)
  - Priority system: local overrides take precedence over shared project configuration
  - Automatic `.gitignore` exclusion to keep personal settings private
  - Comprehensive documentation for setup and usage patterns

- **Structured Debug Logging**: Professional logging system using `RUST_LOG`
  - Integration with `tracing` and `tracing-subscriber` for structured logging
  - Module-specific debug control (e.g., `RUST_LOG=omni_dev::claude=debug`)
  - Detailed diagnostic information for troubleshooting configuration and API issues
  - Comprehensive documentation in troubleshooting guide
  - Replaced custom verbose flag with industry-standard logging approach

### Improved
- **YAML Output Formatting**: Enhanced readability of amendment files
  - Automatic conversion of multiline commit messages to YAML literal block scalars (`|`)
  - Proper formatting instead of escaped newlines in quoted strings
  - Better preserved indentation and structure in generated files
  - Improved user experience when reviewing amendment content

### Removed
- **Verbose Flag**: Removed `--verbose`/`-v` CLI option in favor of `RUST_LOG` environment variable
  - More flexible and powerful debugging control through standard Rust logging
  - Better performance with zero overhead when logging is disabled
  - Industry-standard approach familiar to Rust developers

### Documentation
- **Comprehensive RUST_LOG Documentation**: Added detailed logging guides
  - Basic usage examples and log level explanations
  - Module-specific targeting for focused debugging
  - Common troubleshooting scenarios with specific commands
  - Updated README.md with debugging section
- **Local Override Documentation**: Complete guide for personal configuration management
  - Setup instructions and best practices
  - Real-world usage examples and patterns
  - Integration with team workflows

## [0.5.0] - 2025-09-01

### Changed
- **Diff Output Format**: Modified YAML output to write diff content to external files instead of embedding in YAML
  - Changed `diff_content` field to `diff_file` in `CommitAnalysis` struct for improved memory usage
  - Diff content now written to temporary files in AI scratch directory
  - Enables AI assistants to access detailed diff information through file reads
  - Maintains backward compatibility with similar data structure
  - Updated field documentation for AI assistant guidance

## [0.4.1] - 2025-08-29

### Fixed
- **Rebase Operations**: Fixed short commit hash ambiguity in interactive rebase operations
  - Modified rebase sequence generation to use full commit hashes instead of 7-character truncated hashes
  - Eliminates "short object ID is ambiguous" errors when multiple git objects share the same hash prefix
  - Ensures reliable commit amendment operations regardless of repository size

## [0.4.0] - 2025-08-29

### Added
- **Command Template Management**: New command template system for enhanced CLI experience
  - Added `pr-update` command template generation for pull request workflow automation
  - Implemented comprehensive command template management system
  - Enhanced Claude slash command integration with structured templates
- **AI Scratch Directory Support**: Added AI scratch directory configuration support
  - Integrated AI_SCRATCH environment variable support for enhanced AI assistant workflows
  - Added scratch directory path handling in command templates
- **Version Information Enhancement**: Added version information to command outputs
  - Commands now include version context for better debugging and support
  - Enhanced output format with version tracking
- **Documentation Improvements**: Enhanced slash command documentation structure
  - Improved Claude command file organization and documentation
  - Added comprehensive AI assistant guide and release documentation
  - Better structured troubleshooting information in slash commands

## [0.3.0] - 2025-08-26

### Added
- **Field Presence Tracking**: Enhanced YAML output with explicit field presence indicators
  - Added `present: bool` field to `FieldDocumentation` struct for AI assistant guidance
  - Implemented `update_field_presence()` method on `RepositoryView` to dynamically track available fields
  - Added comprehensive AI assistant guidance in field explanation text
  - Included git command mappings for better field documentation
- **Enhanced Command Structure**: Reorganized Claude command files with improved analysis instructions
  - Added commit-twiddle commands for debug, release, and standard modes
  - Added pr-create commands with enhanced PR workflow decision guidance
  - Standardized command structure across all variants with detailed field checking instructions

### Changed
- **Data Structure Improvements**: Reordered RepositoryView fields to place commits last for better readability
  - Summary fields (explanation, working_directory, remotes, branch_info, pr_template, branch_prs) now appear before detailed commit analysis
  - Improved YAML output organization and user experience

### Fixed
- **Code Quality**: Resolved clippy warnings for better code quality
  - Replaced deprecated `map_or(false, |prs| !prs.is_empty())` patterns with `is_some_and(|prs| !prs.is_empty())`
  - Maintained proper borrowing semantics with `.as_ref()` calls

## [0.2.0] - 2025-08-26

### Added
- **Git Branch Analysis**: New `omni-dev git branch info` command for comprehensive branch analysis
  - Branch-aware commit analysis with automatic range calculation
  - Current branch detection and validation
  - Base branch comparison (defaults to main/master)
  - Enhanced YAML output including branch context
- **GitHub Integration**: GitHub CLI integration for enhanced functionality
  - Accurate main branch detection using GitHub API
  - Pull request information retrieval and display
  - PR template support with conditional YAML output
  - GitHub repository URI parsing and validation
- **Git Commit Analysis**: Comprehensive commit analysis with YAML output
  - Commit metadata extraction (hash, author, date)
  - File change analysis and diff statistics
  - Conventional commit type detection
  - Remote branch tracking and main branch detection
  - Working directory status reporting
- **Commit Message Amendment**: Safe and reliable commit message modification
  - HEAD commit amendment using `git commit --amend`
  - Multi-commit amendment via individual interactive rebases
  - Shell-script-inspired strategy for reliable rebase operations
  - YAML-based amendment file format with validation
- **Safety Features**: Comprehensive safety checks and error handling
  - Working directory cleanliness validation (ignoring build artifacts)
  - Commit existence and accessibility validation
  - Automatic rebase abort and error recovery
  - Prevention of amendments to potentially problematic commits
- **CLI Interface**: Full-featured command-line interface
  - `omni-dev git commit message view [range]` - Analyze and view commits
  - `omni-dev git commit message amend <yaml-file>` - Amend commit messages
  - `omni-dev git branch info [base-branch]` - Analyze branch commits
  - Rich help system and error reporting
- **Testing Infrastructure**: Comprehensive test suite
  - Integration tests with temporary git repositories
  - Amendment functionality validation
  - YAML parsing and validation tests
  - Error handling and edge case testing

### Changed
- Complete rewrite of core functionality to focus on git commit operations
- Updated CLI interface to provide git-specific commands
- Enhanced error handling with detailed context and recovery options
- Remote information now uses `uri` field instead of `url` for consistency

### Fixed
- Working directory safety checks now properly ignore git-ignored files
- Multi-commit amendment reliability improved with individual rebase strategy
- Clippy linting warnings resolved (needless_borrows_for_generic_args)
- Compilation warnings eliminated through dead code cleanup

## [0.1.0] - 2024-08-24

### Added
- Initial release of omni-dev
- Basic project structure and configuration
- CLI application with version and help commands
- Core application framework with configuration support
- Utility functions for input validation and byte formatting
- Comprehensive test suite
- GitHub Actions CI/CD pipeline
- Documentation and community files (README, CONTRIBUTING, CODE_OF_CONDUCT)
- BSD 3-Clause license

[Unreleased]: https://github.com/rust-works/omni-dev/compare/v0.34.0...HEAD
[0.34.0]: https://github.com/rust-works/omni-dev/compare/v0.33.0...v0.34.0
[0.33.0]: https://github.com/rust-works/omni-dev/compare/v0.32.0...v0.33.0
[0.32.0]: https://github.com/rust-works/omni-dev/compare/v0.31.0...v0.32.0
[0.31.0]: https://github.com/rust-works/omni-dev/compare/v0.30.0...v0.31.0
[0.30.0]: https://github.com/rust-works/omni-dev/compare/v0.29.0...v0.30.0
[0.29.0]: https://github.com/rust-works/omni-dev/compare/v0.28.0...v0.29.0
[0.28.0]: https://github.com/rust-works/omni-dev/compare/v0.27.0...v0.28.0
[0.27.0]: https://github.com/rust-works/omni-dev/compare/v0.26.0...v0.27.0
[0.26.0]: https://github.com/rust-works/omni-dev/compare/v0.25.0...v0.26.0
[0.25.0]: https://github.com/rust-works/omni-dev/compare/v0.24.0...v0.25.0
[0.24.0]: https://github.com/rust-works/omni-dev/compare/v0.23.1...v0.24.0
[0.23.1]: https://github.com/rust-works/omni-dev/compare/v0.23.0...v0.23.1
[0.23.0]: https://github.com/rust-works/omni-dev/compare/v0.22.0...v0.23.0
[0.22.0]: https://github.com/rust-works/omni-dev/compare/v0.21.0...v0.22.0
[0.21.0]: https://github.com/rust-works/omni-dev/compare/v0.20.0...v0.21.0
[0.20.0]: https://github.com/rust-works/omni-dev/compare/v0.19.0...v0.20.0
[0.19.0]: https://github.com/rust-works/omni-dev/compare/v0.18.0...v0.19.0
[0.18.0]: https://github.com/rust-works/omni-dev/compare/v0.17.0...v0.18.0
[0.17.0]: https://github.com/rust-works/omni-dev/compare/v0.16.0...v0.17.0
[0.16.0]: https://github.com/rust-works/omni-dev/compare/v0.15.0...v0.16.0
[0.15.0]: https://github.com/rust-works/omni-dev/compare/v0.14.0...v0.15.0
[0.14.0]: https://github.com/rust-works/omni-dev/compare/v0.13.1...v0.14.0
[0.13.1]: https://github.com/rust-works/omni-dev/compare/v0.13.0...v0.13.1
[0.13.0]: https://github.com/rust-works/omni-dev/compare/v0.12.0...v0.13.0
[0.12.0]: https://github.com/rust-works/omni-dev/compare/v0.11.0...v0.12.0
[0.11.0]: https://github.com/rust-works/omni-dev/compare/v0.10.0...v0.11.0
[0.10.0]: https://github.com/rust-works/omni-dev/compare/v0.9.0...v0.10.0
[0.9.0]: https://github.com/rust-works/omni-dev/compare/v0.8.0...v0.9.0
[0.8.0]: https://github.com/rust-works/omni-dev/compare/v0.7.0...v0.8.0
[0.7.0]: https://github.com/rust-works/omni-dev/compare/v0.6.0...v0.7.0
[0.6.0]: https://github.com/rust-works/omni-dev/compare/v0.5.0...v0.6.0
[0.5.0]: https://github.com/rust-works/omni-dev/compare/v0.4.1...v0.5.0
[0.4.1]: https://github.com/rust-works/omni-dev/compare/v0.4.0...v0.4.1
[0.4.0]: https://github.com/rust-works/omni-dev/compare/v0.3.0...v0.4.0
[0.3.0]: https://github.com/rust-works/omni-dev/compare/v0.2.0...v0.3.0
[0.2.0]: https://github.com/rust-works/omni-dev/compare/v0.1.0...v0.2.0
[0.1.0]: https://github.com/rust-works/omni-dev/releases/tag/v0.1.0
