## API Design

#### `cqs --json model swap <bad-preset>` emits plain-text error, not envelope JSON
- **Difficulty:** easy
- **Location:** `src/cli/commands/infra/model.rs:cmd_model_swap` (the `Unknown preset 'X'.` `anyhow::anyhow!` and the `No index at ...` `bail!`) and the parallel `cmd_model_show` `bail!` for missing-index.
- **Description:** `cqs --json model swap nonexistent-preset` prints `Error: Unknown preset 'nonexistent-preset-zzz'. ...` to stderr and exits non-zero — no JSON envelope. Verified live. Compare to `cqs --json ref remove nonexistent-zzz` which emits `{"data":null,"error":{"code":"not_found","message":"..."}, "version":1}` via `json_envelope::emit_json_error`. Same `--json` global flag, two different error contracts. Agents driving model swaps from CI can't differentiate "bad preset" from "transient I/O failure" because the surface looks identical (stderr text + non-zero exit). `cmd_model_show`'s `No index at ...` `bail!` has the same shape problem on the read path. The v1.30.0 audit fixed this for `ref/project/telemetry`; `model` was missed.
- **Suggested fix:** Wrap the `anyhow::anyhow!`/`bail!` sites in `cmd_model_swap` and `cmd_model_show` so that when `json: bool` is true they route through `json_envelope::emit_json_error` with codes `unknown_preset`, `no_index`, `already_on_target` (the no-op short-circuit), and `swap_failed`. Pattern is already established in `cmd_ref_remove` (`src/cli/commands/infra/reference.rs`).

#### `cqs init`, `cqs index`, `cqs convert` lack `--json` despite being the headline mutation surface
- **Difficulty:** medium
- **Location:** `src/cli/commands/infra/init.rs:cmd_init`; `src/cli/commands/index/build.rs:cmd_index` (and `IndexArgs` in `src/cli/args.rs:567+` has no `json` field); `src/cli/commands/index/convert.rs` (no `json` flag).
- **Description:** `cqs init`, `cqs index`, and `cqs convert` are the only path for an agent to bootstrap or refresh an index. None of the three accept `--json`. `cqs index --json` errors with `unexpected argument '--json'`; `cqs init --json` likewise. Every other long-running command (`cqs gc`, `cqs model swap`, `cqs index` peer commands) already supports `--json`. The result is that an automation pipeline that wraps `cqs init && cqs index --llm-summaries` can't capture timing/byte/chunk-count metrics structurally — it has to scrape colored stdout. Particularly painful for `cqs index --llm-summaries --improve-docs` which spends real money and has no machine-readable summary on completion. The CONTRIBUTING "Dry-Run vs Apply" comment in the code already lumps `index` and `convert` as "side-effect commands"; both should also be the `--json`-emitting commands.
- **Suggested fix:** Add `#[arg(long)] pub json: bool` to `IndexArgs`, the new `InitArgs` (currently no args struct — need to introduce one), and `ConvertArgs`. Thread `cli.json || args.json` through `cmd_init` / `cmd_index` / `cmd_convert` and emit a final `{indexed_files, indexed_chunks, took_ms, model, summaries_added?, docs_improved?}` (or analog) envelope via `json_envelope::emit_json` after the work completes. Suppress the per-step progress lines when `json` is set (or route them to stderr — doctor already does this with "Colored human-readable check progress is routed to stderr in this mode").

#### Global `--slot` is silently ignored by every `cqs slot` and `cqs cache` subcommand
- **Difficulty:** easy
- **Location:** `src/cli/definitions.rs` (`pub slot: Option<String>` declared with `#[arg(long, global = true)]`); `src/cli/commands/infra/slot.rs` (`SlotCommand::Create/Promote/Remove` take a positional `name` and never read `cli.slot`); `src/cli/commands/infra/cache_cmd.rs:resolve_cache_path` (no `cli.slot` reference, cache is project-scoped, not per-slot).
- **Description:** `--slot <NAME>` is `global = true` on the top-level `Cli`, so it appears in every subcommand's `--help` (verified: `cqs slot create --help`, `cqs slot promote --help`, `cqs slot remove --help`, `cqs cache stats --help` all advertise `--slot <SLOT>`). For `slot create/promote/remove/active` and the entire `cache` subtree it's a no-op: `slot create foo --slot bar` creates `foo` and ignores `bar` (verified live), `cache stats --slot foo` opens the project-wide cache regardless. The help text actively lies about supported behaviour — agents reading `--help` will conclude they can scope cache ops to a slot. This is the worst kind of API drift: the flag works, parses, and is silently dropped.
- **Suggested fix:** Move `--slot` off `global = true` and onto only the subcommands that actually consume it (every `Commands::*` that ends up calling `cqs::slot::resolve_slot_name` or threads slot into `CommandContext`). For `slot create/promote/remove/active` and `cache *`, keep `--slot` off the surface entirely. Alternative: enforce at dispatch time — `if cli.slot.is_some() && !subcommand_uses_slot { bail!("--slot has no effect on `cqs {subcommand}`") }` — louder than silent-drop, cheap to implement.

#### `cqs refresh` has no `--json`; every other infra command does
- **Difficulty:** easy
- **Location:** `src/cli/definitions.rs:755-761` (`Commands::Refresh` — no JSON arg), and `src/cli/registry.rs` dispatch arm.
- **Description:** `cqs refresh` (alias `invalidate`) is the user-facing surface for the daemon's cache-drop verb. It returns no JSON output — verified, `cqs --json refresh` prints plain text. Compare to `cqs ping --json`, `cqs gc --json`, `cqs cache compact --json`, `cqs telemetry --reset --json`, `cqs slot promote foo --json` — every other infra-mutation/healthcheck supports `--json`. An agent that just promoted a slot needs `refresh` followed by a query; without `--json` it can't confirm "cache invalidated, re-open succeeded" structurally. Was caught in v1.30.0 audit as missing CLI surface (added) but the new entry shipped without the standard JSON contract.
- **Suggested fix:** Add `#[command(flatten)] output: TextJsonArgs` to `Commands::Refresh`, thread into the dispatch arm, and emit `{"refreshed": true, "daemon_running": bool, "caches_invalidated": [...]}` when the flag is set. The daemon-side `dispatch_refresh` (`src/cli/batch/handlers/refresh.rs` or wherever it lives) likely already returns a JSON object — surface it on the CLI path too.

#### List-shape JSON envelopes are inconsistent across `*list` commands
- **Difficulty:** easy
- **Location:** `src/cli/commands/infra/reference.rs:cmd_ref_list` (raw array); `src/cli/commands/infra/model.rs:cmd_model_list` (raw array); `src/cli/commands/infra/project.rs:124-140` (object `{projects: [...]}`); `src/cli/commands/infra/slot.rs:slot_list` (object `{active, slots: [...]}`); `src/cli/commands/io/notes.rs:cmd_notes_list` (object `{notes: [...]}`); `src/cli/commands/search/query.rs` (object `{query, results, total}`).
- **Description:** Verified live: `cqs ref list --json` emits `{"data":[{...},{...}], "error":null, "version":1}` (raw array under `data`). `cqs project list --json` emits `{"data":{"projects":[...]}, ...}` (object under `data`). `cqs slot list --json` emits `{"data":{"active":"default","slots":[...]}, ...}`. `cqs model list --json` emits `{"data":[{...}], ...}` (raw array). Five `*list --json` surfaces, three different shape conventions. Agents writing a generic `data.{slots,projects,refs,models,notes}` accessor have to special-case ref and model. The right convention is the wrapping-object form — it leaves room for sibling fields like the slot-list `active` summary or the search `total` count without breaking the schema.
- **Suggested fix:** Standardize on `{"data": {"<plural-name>": [...]}}` for every list-emitting subcommand. Concretely: change `cmd_ref_list` to emit `{"references": [...]}` and `cmd_model_list` to emit `{"models": [...], "current": "<name>"}` (the latter folds the `current: bool` per-row flag into a top-level `current` field — cleaner). Hard rename, no-external-users; ship it as a single PR that touches both call sites + their tests.

#### `cqs cache stats` JSON mixes byte and megabyte units; `cache compact` uses bytes only
- **Difficulty:** easy
- **Location:** `src/cli/commands/infra/cache_cmd.rs:cache_stats` (emits both `total_size_bytes` AND `total_size_mb`) vs `cache_compact` (emits `reclaimed_bytes`, `size_after_bytes`, `size_before_bytes`).
- **Description:** Verified: `cqs cache stats --json` emits `total_size_bytes: 1662976, total_size_mb: 1.5859375` — same number, two units. `cqs cache compact --json` emits `reclaimed_bytes / size_after_bytes / size_before_bytes` — bytes only. An agent computing "free space saved" has to special-case which fields are present per command. Mixed-unit fields are also fragile: a future SI-vs-binary correction (1024 vs 1000) silently shifts `*_mb` while `*_bytes` stays correct, producing two-source-of-truth bugs.
- **Suggested fix:** Drop `total_size_mb` from `cache_stats` JSON (keep it in the human text path where the caller wants a digestible number). Bytes are the canonical unit across the rest of cqs (`size_*_bytes` already in `compact`, `chunks` are counts not sizes). For human stdout, format-on-display.

#### `pub use nl::*` leaks dead `generate_nl_with_call_context` wrapper into the lib API
- **Difficulty:** easy
- **Location:** `src/lib.rs:165` (`pub use nl::*`); `src/nl/mod.rs:43-59` (`generate_nl_with_call_context` — five-arg wrapper around the seven-arg `_and_summary` variant).
- **Description:** `generate_nl_with_call_context` exists only as a thin wrapper that hard-codes `summary = None, hyde = None` and calls `generate_nl_with_call_context_and_summary`. Production code has zero callers — verified with `grep -rn 'generate_nl_with_call_context\b' src/ | grep -v '/nl/mod.rs:'` returns empty. Only the tests in `src/nl/mod.rs` use it. It's `pub` and re-exported via `pub use nl::*`, so it's part of the lib's public API surface. Not just dead code — it's dead public API, which is worse because removing it later is a "breaking" change in any consumer that picked it up. Same anti-pattern likely lurks under the other ten `pub use foo::*` glob re-exports in `src/lib.rs`.
- **Suggested fix:** Drop `generate_nl_with_call_context` outright (only the test sites need to be updated to call `_and_summary` with `None, None`). Then audit all eleven `pub use foo::*` lines in `src/lib.rs` (`diff`, `gather`, `impact`, `nl`, `onboard`, `project`, `related`, `scout`, `search`, `task`, `where_to_add`) — replace each with explicit `pub use foo::{...}` lists naming the items the lib actually wants to publish. Glob re-exports are a footgun for surface control: every new `pub fn` in any of those modules joins the public API automatically.

#### `cqs gather --expand <N>` and `cqs --expand-parent` collide on a high-traffic flag name
- **Difficulty:** easy
- **Location:** top-level `src/cli/definitions.rs:Cli::expand_parent` (flag `--expand-parent`, parent-context expansion, `bool`); `src/cli/args.rs:GatherArgs::expand` (flag `--expand`, call-graph BFS depth, `usize`); the v1.30.0 fix renamed `SearchArgs::expand` to `expand_parent` with `--expand` as a visible alias for back-compat.
- **Description:** `cqs gather foo --expand 3` (graph depth) and `cqs foo --expand-parent` (parent-context `bool`) both accept the substring `--expand`. The v1.30.0 fix made `SearchArgs::expand` an alias for `expand_parent`, but `GatherArgs::expand` is still a distinct usize-typed flag. `cqs --expand 2 gather foo` parses `--expand 2` against the *top-level* `Cli` first — and there's no top-level `--expand`, so it errors, but the error doesn't say "did you mean `--expand-parent` (top-level) or did you mean to pass `--expand 2` after `gather`?" The dual semantic (bool parent-context vs usize call-graph depth) under similar flag names is the original API-V1.22-3 footgun that motivated the rename — it's only half-fixed.
- **Suggested fix:** Rename `GatherArgs::expand` to `--graph-depth` (or `--depth` to match `onboard`/`impact`/`test-map` which all already use `--depth` for the same call-graph concept post-v1.30.0). That eliminates the `--expand` collision entirely and aligns the four call-graph-depth-taking commands under one flag name. Ship as hard rename — no external users.

#### `cqs eval` `--save` accepts a path with no `.json` validation; output JSON is hard-coded
- **Difficulty:** easy
- **Location:** `src/cli/commands/eval/mod.rs:EvalCmdArgs::save` (`Option<PathBuf>`).
- **Description:** `cqs eval queries.json --save report` writes a JSON report to a file literally named `report` (no extension). The flag is `Option<PathBuf>` with no validation, no extension default, no warning. `cqs eval` is the eval harness — a sloppy filename in a release-comparison workflow turns into "did I overwrite my baseline?" guessing. Compare `cqs eval --baseline report` which requires the path exist, contrasted with `--save` which just overwrites. Asymmetric input/output validation on a command meant for release-gate comparisons.
- **Suggested fix:** When `--save <path>` lacks an extension, append `.json` automatically (with a one-line stderr note). When it has the wrong extension (`.txt`, `.yaml`), error out — eval reports are JSON-only. Mirrors the validation `cqs train-data --output <path>` should also have but doesn't.

Summary: 0 findings carried forward (all 10 archived findings were fixed in v1.30.0 — verified via direct source reads of `src/cli/commands/infra/{project,reference,telemetry_cmd,model}.rs`, `src/cli/args.rs:NotesListArgs`, `src/cli/batch/handlers/misc.rs`, `src/cli/dispatch.rs:try_daemon_query`, `src/cli/definitions.rs:Refresh`, `src/cli/commands/eval/mod.rs:EvalCmdArgs::limit`); 9 new findings, 10 archived findings dropped as fixed.
