## API Design

#### `cqs --json project list` / `project remove` silently emit text, ignoring --json
- **Difficulty:** easy
- **Location:** `src/cli/commands/infra/project.rs:90-108,110-118` (`ProjectCommand::List` and `ProjectCommand::Remove` — no `--json` field, no `TextJsonArgs` flatten)
- **Description:** `cqs --json project list` and `cqs --json project remove foo` print plain text ("No projects registered.", "Removed 'foo'") even when the top-level `--json` flag is set. Verified with `cqs --json project list`: emits `No projects registered.` on stdout. Breaks programmatic consumers that assume `cqs --json <anything>` emits JSON. `cqs ref list` was already fixed to take `TextJsonArgs` (see `src/cli/commands/infra/reference.rs:54-58`); `project list`/`remove` were missed by the sweep. Same class of bug as post-v1.27.0 P1 #8 (`cqs --json project search` ignored `--json`), but on the sibling subcommands.
- **Suggested fix:** Change `ProjectCommand::List` and `ProjectCommand::Remove` to carry `#[command(flatten)] output: TextJsonArgs`, route `cli.json || output.json` into the handlers, and emit an envelope JSON payload (`{projects: [...]}` for list, `{status: "removed"|"not_found", name}` for remove) via `crate::cli::json_envelope::emit_json`. Mirrors what `cqs ref list` already does.

#### `cqs --json ref {add, remove, update}` silently emit text, ignoring --json
- **Difficulty:** easy
- **Location:** `src/cli/commands/infra/reference.rs:42-69` (`RefCommand::Add`, `RefCommand::Remove`, `RefCommand::Update` have no `TextJsonArgs`)
- **Description:** `cqs --json ref add foo /path` and `cqs --json ref remove bar` emit plain-text status (or a text `anyhow::Error`) even with `--json`. Verified: `cqs --json ref remove nonexistent-reference` prints `Error: Reference 'nonexistent-reference' not found in config.` on stderr. An agent driving config via JSON can't differentiate "already absent" (idempotent success) from "real error" (bad args) because there is no envelope. Same class as the `project list`/`remove` finding but on the reference-index mutations.
- **Suggested fix:** Add `#[command(flatten)] output: TextJsonArgs` to each of the three variants. Convert the handlers (`cmd_ref_add`, `cmd_ref_remove`, `cmd_ref_update`) to emit `{status, name, weight?}` envelopes when `--json` is set, and return a typed `not_found` error code via the shared `json_envelope::emit_json_error` path.

#### `cqs telemetry --reset --json` silently drops --json; handler prints text
- **Difficulty:** easy
- **Location:** `src/cli/commands/infra/telemetry_cmd.rs:520-578` (`cmd_telemetry_reset`) and dispatch at `src/cli/dispatch.rs:147-158`
- **Description:** `cqs telemetry --reset --json` prints `Archived 44912 events to telemetry_20260423_231842.jsonl` on stdout — plain text, not JSON. Verified live. The dispatch in `dispatch.rs:153-157` routes around the `--json` flag entirely: `if reset { cmd_telemetry_reset(&cqs_dir, reason.as_deref()) } else { cmd_telemetry(..., cli.json || output.json, all) }`. The reset path gets no json arg, and `cmd_telemetry_reset` has no json parameter. Same top-level `--json` swallow as post-v1.27.0 P1 #8 but on a mutating action.
- **Suggested fix:** Thread `cli.json || output.json` into `cmd_telemetry_reset` as a `json: bool` parameter. When true, emit `{archived_events, archive_path, lock_path}` via `emit_json`. Keep the existing text output for `json = false`. Update `dispatch.rs:153-157` to pass the flag.

#### `cqs notes list --check` is accepted by the CLI but dropped by the daemon batch dispatch
- **Difficulty:** easy
- **Location:** `src/cli/commands/io/notes.rs:51-64` defines `--check` on `NotesCommand::List`; `src/cli/args.rs:527-540` defines `NotesListArgs` (shared with batch) WITHOUT `--check`; `src/cli/batch/handlers/misc.rs:85-118` (`dispatch_notes`) has no `check` parameter and never calls `suggest::check_note_staleness`.
- **Description:** `cqs notes list --check` on the CLI populates a `stale_mentions` field per note by running `check_note_staleness` (see `src/cli/commands/io/notes.rs:544-593`). The daemon path — selected automatically when the daemon is running — parses into `NotesListArgs` which has only `warnings` and `patterns`. `BatchCmd::Notes` accepts `--check` as a parsed-but-unused flag via the `TextJsonArgs` carrier? No — `NotesListArgs` has no `check` field, so `echo 'notes --check' | cqs batch` will error "unexpected argument `--check`", AND `cqs notes list --check` routed to daemon produces a response without `stale_mentions` populated. Agents who rely on staleness info silently lose it when the daemon is up. Cross-refs the `NotesCommand::List` vs `NotesListArgs` split documented at `src/cli/args.rs:527-531`.
- **Suggested fix:** Add `pub check: bool` (with `#[arg(long)]`) to `NotesListArgs`. Extend `dispatch_notes` to accept the flag and call `cqs::suggest::check_note_staleness` when set, producing the same `stale_mentions: Option<Vec<String>>` shape as the CLI. Mirrors the wave-1 fix for `cqs stale --count-only` missing on batch (API-V1.25-2).

#### Daemon `dispatch_drift` / `dispatch_diff` emit `file` via `.display().to_string()` — CLI uses `normalize_path`
- **Difficulty:** easy
- **Location:** `src/cli/batch/handlers/misc.rs:277, 353, 365, 377` (all four uses of `e.file.display().to_string()`) vs `src/cli/commands/io/drift.rs:53` (`build_drift_output` uses `normalize_path`)
- **Description:** The daemon-side drift/diff handlers emit divergent file-path shapes compared to the CLI. CLI `cmd_drift` uses `normalize_path(&e.file)` (normalizes backslashes to forward slashes on Windows, strips UNC `\\?\` prefixes — the same invariant `cqs stale --json` and `cqs search --json` apply after P1 #19 in v1.27.0). Daemon `dispatch_drift` and `dispatch_diff` both use `e.file.display().to_string()`, which emits OS-native path separators. On Windows / WSL mixed projects, a consumer comparing `drift[].file` or `diff[].{added,removed,modified}[].file` to file paths from other `--json` commands gets mismatches (`src\\foo.rs` from daemon, `src/foo.rs` from CLI). Four call sites affected, same bug class.
- **Suggested fix:** Replace all four `e.file.display().to_string()` with `cqs::normalize_path(&e.file)` in `src/cli/batch/handlers/misc.rs`. Then factor the shared `DriftEntryOutput` / diff-entry serializers into `src/cli/commands/io/` so future field additions stay single-source.

#### `BatchCmd::Refresh` / `invalidate` has no CLI surface; agents can't invalidate daemon caches
- **Difficulty:** medium
- **Location:** `src/cli/batch/commands.rs:302-304` (`BatchCmd::Refresh`, visible alias `invalidate`) — no matching `Commands::Refresh` variant in `src/cli/definitions.rs:287+`
- **Description:** The daemon batch surface exposes a `Refresh` / `invalidate` verb that drops cached `hnsw`, `splade_index`, `call_graph`, `test_chunks`, `notes`, and refs — useful when a user edits the index out-of-band (e.g. `cqs model swap` that crashed mid-run, a manual SQLite patch, or a reference that got reindexed in another shell). It's only reachable via `echo 'refresh' | cqs batch` or direct socket send. There's no `cqs refresh` subcommand, no `cqs --invalidate` flag. Agents that know they mutated the index under the daemon have no first-class way to tell the daemon to re-read, so they fall back to `systemctl --user restart cqs-watch` or `CQS_NO_DAEMON=1`. This is a CLI/batch asymmetry directly relevant to agent ergonomics.
- **Suggested fix:** Add `Commands::Refresh` (and a clap `visible_alias = "invalidate"`) to the CLI definitions. Classify as `BatchSupport::Daemon` so it forwards to the daemon when present (its whole purpose is cache invalidation on the running daemon) and no-ops cleanly otherwise. Update `src/cli/dispatch.rs` with a one-line arm that forwards through `try_daemon_query` or bails cleanly ("no daemon running, nothing to refresh").

#### `cqs eval --limit` diverges from every other query command's `-n, --limit`
- **Difficulty:** easy
- **Location:** `src/cli/commands/eval/mod.rs:46-48` (`--limit` without `short = 'n'`)
- **Description:** Every query-returning subcommand uses `#[arg(short = 'n', long, default_value = "5")]` for its result cap (`search`, `callers`, `similar`, `scout`, `gather`, `explain`, `test-map`, `related`, `where`, `deps`, `onboard`, `plan`, `task`, `drift`, `blame`'s `-n, --commits`, `neighbors`, `project search`). `cqs eval --limit 20` works but `cqs eval -n 20` errors. `cqs train-pairs --limit` has the same gap. Agents that build commands from a template (`--limit N`) don't notice but humans doing `cqs eval -n 10` hit a surprise. Minor footgun.
- **Suggested fix:** Add `short = 'n'` to `EvalCmdArgs::limit` and `TrainPairs::limit`. One-token change per file, zero breakage since no caller uses `-n` today.

#### Pretty/compact JSON output drifts between CLI path and daemon path
- **Difficulty:** easy
- **Location:** `src/cli/dispatch.rs:786-790` (daemon response passed through `serde_json::to_string(other)` — compact) vs `src/cli/json_envelope.rs:242-251` (CLI `emit_json` uses `to_string_pretty` — multi-line)
- **Description:** `cqs --json impact-diff` emits compact single-line JSON when the daemon is running, pretty multi-line JSON when `CQS_NO_DAEMON=1`. Verified: `CQS_NO_DAEMON=1 cqs --json impact-diff` starts with `{\n  "data":...` while `cqs --json impact-diff` (daemon path) starts with `{"data":...`. Every other CLI subcommand has the same split. Agents / humans rely on output shape for diffing eval logs across runs; the two surfaces produce byte-different outputs for the same inputs. Not a bug for `jq` consumers but is a bug for snapshot tests and diff-review workflows.
- **Suggested fix:** In `try_daemon_query` (`src/cli/dispatch.rs:786-790`), use `serde_json::to_string_pretty` instead of `to_string` on the non-string `Value` arm, so the CLI-side output is pretty in both modes. Alternatively, switch `emit_json` to compact in both and pretty-print only when stdout is a TTY (matches `git`/`gh` conventions). Former is the zero-risk one.

#### `--expand` on `cqs search` vs `--expand-parent` on top-level `cqs <QUERY>` — same semantic, two names
- **Difficulty:** easy
- **Location:** `src/cli/args.rs:122-124` (`SearchArgs::expand`, flag `--expand`) vs `src/cli/definitions.rs:234-235` (`Cli::expand_parent`, flag `--expand-parent`)
- **Description:** The top-level `Cli` struct renamed the parent-context flag from `--expand` to `--expand-parent` to avoid colliding with `gather --expand <N>` (see the API-V1.22-3 comment). But `SearchArgs` (used by `BatchCmd::Search` AND by the flattened search args inside `Commands::Search` if it existed — actually `search` is not a named subcommand on CLI, only on batch) still exposes `--expand`. That means `echo 'search foo --expand' | cqs batch` works, `cqs search foo --expand` works in batch-mode only, but `cqs --expand-parent foo` is required at the top level. Three words for one concept across three slightly-overlapping surfaces. An agent batching `search foo --expand-parent` gets an error; batching `search foo --expand` works.
- **Suggested fix:** Rename `SearchArgs::expand` to `SearchArgs::expand_parent` with `long = "expand-parent"`, matching the top-level. Zero-risk rename — the tests in `src/cli/definitions.rs:1016-1038` already pin the top-level spelling; update the single batch-surface callsite. If backcompat is wanted, add `visible_alias = "expand"` and remove on v2.

#### `--depth` short-flag `-d` present on `cqs onboard`, absent on `cqs impact` and `cqs test-map`
- **Difficulty:** easy
- **Location:** `src/cli/args.rs:349-351` (`OnboardArgs::depth` has `short = 'd'`) vs `src/cli/args.rs:174-176` (`ImpactArgs::depth` — no short) and `src/cli/args.rs:322-324` (`TestMapArgs::depth` — no short)
- **Description:** Three commands expose `--depth <N>` with exactly the same semantic (search / BFS depth in the call graph) but only `onboard` has the `-d` short flag. Humans muscle-memory-typing `cqs impact foo -d 3` get "unexpected argument '-d' found". Post-v1.27.0 already caught the parallel issue of `--depth` having three different meanings (blame commits, onboard callee depth, test-map BFS); that was fixed on blame with the rename to `--commits`. The short-flag is the remaining inconsistency across the three that legitimately share semantics (all three are "call-graph search depth").
- **Suggested fix:** Either (a) add `short = 'd'` to both `ImpactArgs::depth` and `TestMapArgs::depth` for symmetry with onboard, or (b) remove `short = 'd'` from onboard for unified `--depth`-only. Option (a) is easier for humans; (b) is stricter for the no-short-flag-collisions stance elsewhere. Neither costs backcompat since all three are internal-only.

Summary: 10 API-design findings, all verified against live `cqs v1.29.0`. Dominant classes: (1) top-level `--json` silently dropped on non-flattened subcommands (`project list/remove`, `ref add/remove/update`, `telemetry --reset`), (2) CLI↔daemon-batch shape drift (`dispatch_drift` path separators, `NotesListArgs` missing `--check`, pretty-vs-compact JSON), (3) missing CLI surface for a daemon-only verb (`refresh`/`invalidate`), (4) flag-name/short-flag inconsistencies across commands that share semantics (`eval --limit` no `-n`, `--expand` vs `--expand-parent`, `--depth` short-flag asymmetry).
