# AI Consultants - Claude Code Instructions

## Project Overview

AI Consultants is a multi-model coverage system that queries up to 10 AI consultants (Gemini, Codex, Mistral, Kimi, Claude, Qwen3, GLM, Grok, DeepSeek, MiniMax) to obtain diverse perspectives on coding problems.

**Self-Exclusion**: The invoking agent is automatically excluded from both the panel and synthesis. Claude Code won't query or synthesize with Claude, Codex CLI won't query or synthesize with Codex, etc.

**Version**: 5.1.1

## Distribution

Two distribution methods are supported:

1. **npx** (recommended): `npx ai-consultants "question"` - uses npm as distribution mechanism only (zero dependencies)
2. **curl | bash**: `curl -fsSL .../install.sh | bash` - git clone into `~/.claude/skills/`

### npm Architecture

`bin/ai-consultants` is a bash wrapper that npm registers as the CLI entry point. npm creates a symlink in `node_modules/.bin/` pointing to this file. The wrapper:

1. Resolves its own path through symlinks (portable `readlink` loop for macOS/Linux)
2. Computes `PROJECT_ROOT` and `SCRIPTS_DIR` from the resolved path
3. Fixes `chmod +x` on first run (npm can strip execute permissions)
4. Routes subcommands (`doctor`, `install`, `version`, `help`) or delegates to `consult_all.sh` via `exec`

**Key insight**: Because the wrapper resolves symlinks before calling scripts, `BASH_SOURCE[0]` in every script points to the real file. This means **zero modifications** to the 28 existing scripts.

## Structure

```
ai-consultants/
├── bin/
│   └── ai-consultants          # npm/npx entry point (bash wrapper)
├── package.json                # npm distribution metadata (zero dependencies)
├── .npmignore                  # Excludes dev artifacts from npm package
├── scripts/
│   ├── consult_all.sh          # Main orchestrator - entry point
│   ├── config.sh               # Centralized configuration
│   ├── doctor.sh               # Diagnostic and auto-fix tool (v2.2)
│   ├── update_clis.sh          # Check/update installed consultant CLIs (v2.21)
│   ├── install.sh              # One-liner installer (v2.2)
│   ├── query_*.sh              # Wrapper for each consultant
│   ├── query_claude.sh         # Claude consultant (v2.2)
│   ├── synthesize.sh           # Coverage-union synthesis of responses
│   ├── classify_question.sh    # Question classifier
│   ├── followup.sh             # Follow-up queries
│   ├── preflight_check.sh      # DEPRECATED v2.10.9 (thin wrapper -> doctor.sh)
│   └── lib/
│       ├── common.sh           # Shared utilities (logging, quorum grading)
│       ├── personas.sh         # Consultant persona definitions
│       ├── schema.json         # JSON output schema
│       ├── routing.sh          # Smart routing (category affinity) + cost-aware routing
│       ├── session.sh          # Session management
│       ├── costs.sh            # Cost tracking + response limits
│       ├── cache.sh            # Semantic caching (v2.3)
│       ├── api_query.sh        # API mode query execution (v2.6)
│       └── progress.sh         # Progress bars
├── references/
│   ├── configuration.md      # Full configuration reference
│   └── details.md            # Presets, strategies, best practices
├── docs/
│   ├── releases/             # Release notes (one per version)
│   ├── SETUP.md              # Installation guide
│   ├── RECIPES.md            # Copy-paste workflow configurations
│   ├── COST_RATES.md         # Model pricing
│   ├── SMART_ROUTING.md      # Affinity matrix
│   └── JSON_SCHEMA.md        # Output schema
└── templates/
    └── synthesis_prompt.md     # Synthesis prompt
```

## Claude Skills Compliance

**IMPORTANT**: This project is a Claude Skill following the [agentskills.io](https://agentskills.io) open standard.

**Key Requirements:**
- SKILL.md `name`: max 64 chars, lowercase letters/numbers/hyphens only
- SKILL.md `description`: max 1024 chars, must include WHAT and WHEN to use
- Keep SKILL.md body under 500 lines
- Use progressive disclosure: reference separate files for detailed content
- Scripts are executed via bash, not loaded into context
- Test with all models (Haiku, Sonnet, Opus)

## Language Policy

**IMPORTANT**: The entire codebase MUST remain in English. This includes:
- All code comments
- All user-facing messages (log_info, log_error, echo, etc.)
- All documentation (README, CLAUDE.md, docs/, etc.)
- All prompt templates
- Variable names and function names

Do NOT introduce Italian or other languages in any part of the codebase.

## Code Conventions

### Bash Scripts
- Always use `set -euo pipefail` at the beginning
- Source `lib/common.sh` for logging (`log_info`, `log_error`, `log_success`, `log_warn`)
- Source `config.sh` for configuration
- JSON output must follow `lib/schema.json`
- Use environment variables for configuration override

### Logging
```bash
log_info "Informational message"
log_success "Operation completed"
log_warn "Warning"
log_error "Critical error"
log_debug "Debug message"  # Only shown when LOG_LEVEL=DEBUG
```

### Shared Response Processing
Query scripts should use `process_consultant_response()` from `lib/common.sh` for DRY response handling:
```bash
source "$SCRIPT_DIR/lib/common.sh"
process_consultant_response "$raw_output" "$consultant_name" "$model" "$persona" "$output_file"
```

### JSON Output
Each consultant must produce JSON with this minimum structure:
```json
{
  "consultant": "ConsultantName",
  "model": "model-used",
  "persona": "The Architect|Pragmatist|Devil's Advocate|Innovator",
  "response": {
    "summary": "TL;DR",
    "detailed": "Full response",
    "approach": "Approach name",
    "pros": ["advantage 1", "advantage 2"],
    "cons": ["disadvantage 1"],
    "caveats": ["important note"]
  },
  "confidence": {
    "score": 1-10,
    "reasoning": "Justification",
    "uncertainty_factors": ["what could affect this"]
  },
  "metadata": {
    "tokens_used": 1234,
    "latency_ms": 5600,
    "timestamp": "ISO-8601"
  }
}
```

## Main Flow

1. `consult_all.sh` receives query, optional files, and flags (`--preset`, `--strategy`)
2. Applies preset if specified (`apply_preset()` in config.sh) — sets the consultant set + model tier
3. Classifies the question (`classify_question.sh`)
4. Selects consultants (smart routing by category affinity, or all)
5. Launches parallel queries (`query_*.sh`) — one shot per consultant, no serial rounds
6. Grades quorum (`grade_quorum()` in `lib/common.sh`) — surfaces a degraded panel
7. Generates synthesis with the selected strategy (`synthesize.sh`); the default `coverage` produces the deduplicated UNION of every distinct point across the panel
8. Produces final report

## v2.2 Features

### Self-Exclusion
The invoking agent is automatically excluded from the panel:

```bash
# From Claude Code slash commands (automatic)
# Claude is excluded, all others participate

# Manual bash usage
INVOKING_AGENT=claude ./scripts/consult_all.sh "question"   # Claude excluded
INVOKING_AGENT=codex ./scripts/consult_all.sh "question"    # Codex excluded
./scripts/consult_all.sh "question"                          # No exclusion
```

Functions in `lib/common.sh`:
- `get_self_consultant_name()` - Maps invoking agent to consultant name
- `should_skip_consultant()` - Returns true if consultant should be excluded
- `log_self_exclusion_status()` - Debug logging for exclusion
- `resolve_synthesis_cli()` - Refuses the invoking agent as synthesis provider

Agent hosts must set `INVOKING_AGENT` explicitly and run `consult_all.sh`
instead of individual adapters. A Claude-hosted run must produce no
`claude.json`, and its `synthesis_provider` must not be `claude`; the equivalent
invariant applies to Codex and Gemini. See `SKILL.md` for the full execution
contract.

### Configuration Presets
```bash
# Quality Tier Presets (v2.5)
./scripts/consult_all.sh --preset max_quality "question"  # all 10 consultants + maximum tier/max effort
./scripts/consult_all.sh --preset medium "question"       # 3 consultants + standard models
./scripts/consult_all.sh --preset fast "question"         # 2 consultants + economy models

# Use Case Presets
./scripts/consult_all.sh --preset minimal "question"    # Gemini + Codex
./scripts/consult_all.sh --preset balanced "question"   # + Mistral
./scripts/consult_all.sh --preset high-stakes "question" # Broad premium panel
```

Presets are defined in `config.sh` via `apply_preset()` function.

### Synthesis Strategies
```bash
./scripts/consult_all.sh --strategy coverage "question"      # Default: union of distinct points
./scripts/consult_all.sh --strategy majority "question"      # Blended recommendation
./scripts/consult_all.sh --strategy risk_averse "question"   # Conservative
./scripts/consult_all.sh --strategy security_first "question" # Security focus
./scripts/consult_all.sh --strategy compare_only "question"  # No recommendation
```

Strategies are implemented in `synthesize.sh` via `get_strategy_instructions()`.

### Doctor Command
```bash
./scripts/doctor.sh              # Full diagnostic
./scripts/doctor.sh --fix        # Auto-fix issues
./scripts/doctor.sh --json       # JSON output
./scripts/doctor.sh --verbose    # Detailed output
```

## v2.9 Features

### Kimi CLI Support
Kimi (MoonshotAI) is now supported as a CLI-based consultant with "The Eastern Sage" persona.

```bash
# Enable Kimi consultant
export ENABLE_KIMI=true
./scripts/consult_all.sh "question"
```

**CLI Installation:**
```bash
curl -L code.kimi.com/install.sh | bash
```

**Environment Variables:**
- `ENABLE_KIMI` - Enable Kimi consultant (default: false)
- `KIMI_CMD` - CLI command (default: kimi)
- `KIMI_TIMEOUT` - Timeout in seconds (default: 180)
- `KIMI_MODEL` - Kimi CLI model alias (default: kimi-code/k3; passed explicitly with `--model`)

**Persona:** The Eastern Sage - Focuses on holistic understanding, balance of perspectives, and wisdom from diverse viewpoints.

## v2.7 Features

### Qwen CLI Support (qwen-code)
Qwen3 now supports CLI/API mode switching using the qwen-code CLI.

```bash
# CLI mode (new in v2.7)
export QWEN3_USE_API=false
./scripts/consult_all.sh "question"

# API mode (opt-in)
export QWEN3_USE_API=true
export QWEN3_API_KEY="your-dashscope-key"
./scripts/consult_all.sh "question"
```

**CLI Installation:**
```bash
npm install -g @qwen-code/qwen-code@latest
```

**Note:** `QWEN3_USE_API` defaults to `false` to use the qwen CLI by default.

## v2.6 Features

### CLI/API Mode Switching
Seven consultants can switch between CLI and API mode: **Gemini, Codex, Claude, Mistral, Qwen3, Grok, MiniMax**.

When API mode is enabled for an agent, CLI mode is disabled (mutual exclusivity).

```bash
# Enable API mode for individual consultants
export GEMINI_USE_API=true
export GEMINI_API_KEY="your-google-ai-key"
./scripts/consult_all.sh "question"

export CODEX_USE_API=true
export OPENAI_API_KEY="sk-..."
./scripts/consult_all.sh "question"

export CLAUDE_USE_API=true
export ANTHROPIC_API_KEY="sk-ant-..."
./scripts/consult_all.sh "question"

export MISTRAL_USE_API=true
export MISTRAL_API_KEY="your-mistral-key"
./scripts/consult_all.sh "question"

export QWEN3_USE_API=true  # Enable API mode (CLI is default)
export QWEN3_API_KEY="your-dashscope-key"
./scripts/consult_all.sh "question"
```

### API Mode Configuration
New environment variables in `config.sh`:

| Variable | Default | Description |
|----------|---------|-------------|
| `GEMINI_USE_API` | false | Use Google AI API instead of the agy CLI |
| `CODEX_USE_API` | false | Use OpenAI API instead of codex CLI |
| `CLAUDE_USE_API` | false | Use Anthropic API instead of claude CLI |
| `MISTRAL_USE_API` | false | Use Mistral API instead of vibe CLI |
| `QWEN3_USE_API` | false | Use DashScope API instead of qwen CLI (v2.7) |
| `GROK_USE_API` | auto | Use xAI API when Grok Build is unavailable |
| `MINIMAX_USE_API` | false | Use MiniMax API instead of the mmx CLI (v2.21) |
| `GEMINI_API_URL` | https://generativelanguage.googleapis.com/v1beta/models | Google AI endpoint |
| `CODEX_API_URL` | https://api.openai.com/v1/chat/completions | OpenAI endpoint |
| `CLAUDE_API_URL` | https://api.anthropic.com/v1/messages | Anthropic endpoint |
| `MISTRAL_API_URL` | https://api.mistral.ai/v1/chat/completions | Mistral endpoint |

### API Keys for API Mode

| Agent | API Key Variable | Notes |
|-------|------------------|-------|
| Gemini | `GEMINI_API_KEY` | Google AI API key |
| Codex | `OPENAI_API_KEY` | Same as existing OpenAI key |
| Claude | `ANTHROPIC_API_KEY` | Anthropic API key |
| Mistral | `MISTRAL_API_KEY` | Same as existing Mistral key |
| Qwen3 | `QWEN3_API_KEY` | DashScope API key |

### Mode Checking Functions
New functions in `lib/common.sh`:
- `is_api_mode()` - Check if agent is in API mode
- `validate_api_mode()` - Validate API key is set
- `get_api_key_var()` - Get API key variable name
- `get_api_url()` - Get API endpoint URL
- `get_api_format()` - Get response format (openai, anthropic, google_ai)

### Doctor Diagnostics
The `doctor.sh` script now shows CLI/API mode status:
```bash
./scripts/doctor.sh --verbose
# Shows:
#   ✓ Gemini: API mode (key: AIza...1234)
#   ○ Codex: CLI mode
#   ○ Claude: CLI mode
#   ○ Mistral: CLI mode
#   ✓ Qwen3: API mode (key: sk-...1234)
```

## v2.5 Features

### Model Quality Tiers
Three normal tiers plus a `maximum` tier are configurable via `apply_model_tier()`:

| Tier | Description | Example Models |
|------|-------------|----------------|
| **premium** | Latest flagship models | claude-fable-5-1, Gemini 3.7 Flash (High), gpt-6-astra |
| **maximum** | All 10 consultants; maximum targets and highest provider effort | claude-fable-5-1, K3-256k, Qwen3.8-Max, MiniMax M3; Grok xhigh, GLM/DeepSeek max |
| **standard** | Good quality at reasonable cost | claude-opus-5, Gemini 3.7 Flash (High), gpt-5.6-terra |
| **economy** | Optimized for speed and low cost | claude-haiku-4-5, Gemini 3.7 Flash (Low), gpt-5.6-luna |

**Default models are now premium tier** for maximum quality.

```bash
# Programmatic usage
source scripts/config.sh
apply_model_tier "premium"   # Set all consultants to premium models
apply_model_tier "maximum"   # max_quality-only / separate-plan models
apply_model_tier "standard"  # Set all consultants to standard models
apply_model_tier "economy"   # Set all consultants to economy models

# Get model for a specific consultant and tier (v2.8.1)
get_model_for_tier "gemini" "premium"   # → Gemini 3.7 Flash (High) on agy
get_model_for_tier "claude" "economy"   # → claude-haiku-4-5
```

### Quality Tier Presets
Three new presets leverage the model tiers:

```bash
# Maximum quality - all consultants + maximum models and provider effort
./scripts/consult_all.sh --preset max_quality "critical architecture decision"

# Balanced quality - standard models, 3 consultants
./scripts/consult_all.sh --preset medium "general coding question"

# Super fast - economy models, 2 consultants
./scripts/consult_all.sh --preset fast "quick syntax question"
```

### Premium Model Defaults (September 2026)
All consultants now use premium models by default:

| Consultant | Default Model |
|------------|---------------|
| Claude | claude-fable-5-1 |
| Gemini | Gemini 3.7 Flash (High) (via agy CLI); API: gemini-3.1-pro-preview |
| Codex | gpt-6-astra |
| Mistral | CLI: mistral-medium-3.5; API: mistral-large-3 |
| DeepSeek | deepseek-flash (DeepSeek-V4.1-Flash) |
| GLM | glm-5.3-flash |
| Grok | grok-4.6 |
| Qwen3 | qwen3.7-max |
| Kimi | kimi-code/k3 |
| MiniMax | MiniMax-M2.7 |

Override with environment variables: `CLAUDE_MODEL`, `GEMINI_MODEL`, `CODEX_MODEL`, `KIMI_MODEL`, etc.

Grok is CLI-first through the official Grok Build `grok` command, with
`grok-4.6` passed explicitly in headless mode. The prompt is delivered with
`--prompt-file`; HOME and CWD are isolated, built-in/MCP tools are denied, and
the strict sandbox prevents user or project extensions from joining the
consultation. The xAI Chat Completions path is retained only when the CLI is
missing, cannot launch, or lacks usable authentication and `GROK_API_KEY` is
available. A timeout, model error, empty response, or other post-launch failure
is surfaced and never silently rerouted to the API.

The live full-panel smoke defines the `max_quality` runtime envelope, not just
the model names. Mistral and Grok receive four bounded advisory turns plus an
explicit no-tools/workspace instruction. Qwen3.8-Max gets a 600-second Token
Plan timeout; DeepSeek V4 Pro/max gets 600 seconds and 16,384 completion tokens;
GLM gets 16,384; MiniMax M3 gets 16,384 plus a compact Markdown contract in
mmx's native system channel. OpenAI-compatible
`finish_reason=length` is an error. Every
response records `metadata.response_quality=structured|fallback|error`:
malformed JSON-looking output fails closed, while real markdown/prose is kept
as an explicit fallback. Synthesis filters error envelopes and includes bounded
detail from structured and fallback responses. Claude CLI consultations are
preflighted, stateless (`--no-session-persistence`), setting-source-free,
tool-free, and MCP-free; a hung/help/auth probe fails before a paid dispatch.

## v2.4 Features

### Budget Enforcement (Opt-in)
Optional budget limits to prevent consultations from exceeding configurable cost limits.

```bash
# Enable budget enforcement
ENABLE_BUDGET_LIMIT=true
MAX_SESSION_COST=1.00
BUDGET_ACTION=warn  # or "stop"
```

**BUDGET_ACTION options:**
- `warn` - Log warning but continue consultation
- `stop` - Halt consultation and return partial results

**Enforcement Points:**
1. Before Round 1 - Check estimated cost vs budget
2. After Round 1 - Check actual cost vs warning threshold
3. Before Synthesis - Check cumulative + synthesis estimate

Functions in `lib/costs.sh`:
- `is_budget_enabled()` - Check if budget enforcement is enabled
- `enforce_budget()` - Check budget and take action based on BUDGET_ACTION
- `get_remaining_budget()` - Get remaining budget
- `format_budget_status()` - Format budget status for display
- `estimate_phase_cost()` - Estimate cost for a specific phase

Configuration via environment variables or natural language.

## v2.3 Features

### Semantic Caching
Reduces redundant API calls by caching responses based on query + context fingerprints.

```bash
# Configuration in config.sh
ENABLE_SEMANTIC_CACHE=true      # Enable caching (default: true)
CACHE_TTL_HOURS=24              # Cache expiration
CACHE_DIR=/tmp/ai_consultants_cache
```

Functions in `lib/cache.sh`:
- `generate_fingerprint()` - Creates hash from query + category + context
- `check_cache()` - Returns cached response if valid
- `store_cache()` - Stores response with metadata
- `cleanup_cache()` - Removes expired entries
- `get_cache_stats()` - Returns cache statistics as JSON

### Response Length Limits (Opt-in)
Limits output tokens by question category to reduce costs.

```bash
ENABLE_RESPONSE_LIMITS=false    # Default: false (opt-in per quality review)
MAX_RESPONSE_TOKENS_BY_CATEGORY="QUICK_SYNTAX:200,CODE_REVIEW:800,ARCHITECTURE:1000,SECURITY:1000,GENERAL:500"
```

Functions in `lib/costs.sh`:
- `get_max_response_tokens()` - Returns limit for category
- `is_response_limits_enabled()` - Check if enabled

### Cost-Aware Routing
Routes simple queries to cheaper models, complex queries to premium models.

```bash
ENABLE_COST_AWARE_ROUTING=false # Enable cost-based routing
USE_ECONOMIC_MODELS_FOR_SIMPLE=true
COMPLEXITY_THRESHOLD_SIMPLE=3   # Score 1-3 = simple
COMPLEXITY_THRESHOLD_MEDIUM=6   # Score 4-6 = medium, 7-10 = complex
```

Functions in `lib/routing.sh`:
- `select_consultants_cost_aware()` - Selects consultants based on complexity
- `get_cost_aware_model()` - Returns economic model for simple queries
- `calculate_query_complexity()` - Scores query 1-10

Functions in `lib/costs.sh`:
- `get_economic_model()` - Maps consultant to cheaper model
- `get_model_tier()` - Returns economy/standard/premium

### Fallback Escalation
Automatically re-queries with premium models if confidence is too low.

```bash
FALLBACK_CONFIDENCE_THRESHOLD=7  # Escalate if confidence < 7
```

Functions in `lib/routing.sh`:
- `needs_escalation()` - Returns true if response needs premium model
- `get_premium_model()` - Returns premium model for consultant
- `get_escalation_summary()` - Returns escalation info as JSON

### Quality Monitoring
Logs optimization metrics and saves to output directory.

```bash
# In LOG_LEVEL=DEBUG mode, shows optimization status
# Always saves optimization_metrics.json to output directory
```

Output file `optimization_metrics.json`:
```json
{
  "optimization_settings": {
    "cache_enabled": true,
    "cache_hits": 2,
    "response_limits_enabled": false,
    "cost_aware_routing": false,
    "compact_report": true
  },
  "quality_metrics": {
    "successful_responses": 4,
    "total_consultants": 4,
    "category": "ARCHITECTURE"
  },
  "timestamp": "2026-01-17T10:30:00Z"
}
```

### Compact Reports
Generates shorter reports by default (summaries only, no full JSON).

```bash
ENABLE_COMPACT_REPORT=true       # Default: true
REPORT_MAX_JSON_LINES=50         # Max JSON lines in full report
```

## Testing

```bash
# Full regression suite (required before committing/releasing)
npm test

# ShellCheck lint
npm run lint

# Full diagnostic
./scripts/doctor.sh

# Basic test
./scripts/consult_all.sh "How to optimize a SQL query?"

# Test with preset
./scripts/consult_all.sh --preset minimal "Quick question"

# Test with strategy
./scripts/consult_all.sh --strategy risk_averse "Security question"

# Syntax validation (all scripts)
for f in scripts/*.sh scripts/lib/*.sh; do bash -n "$f" && echo "OK: $f"; done
```

## Key Environment Variables

| Variable | Default | Description |
|----------|---------|-------------|
| `INVOKING_AGENT` | unknown | Agent invoking the skill (for self-exclusion) |
| `ENABLE_CLAUDE` | true | Enable Claude consultant (auto-excluded under Claude Code) |
| `ENABLE_SYNTHESIS` | true | Coverage-union synthesis of responses |
| `ENABLE_SMART_ROUTING` | false | Intelligent routing by category |
| `ENABLE_COST_TRACKING` | true | Track costs |
| `MAX_SESSION_COST` | 1.00 | Max budget ($) |
| `CLAUDE_MODEL` | claude-fable-5-1 | Claude model (use claude-opus-5 for lower-cost standard) |
| `CLAUDE_API_MAX_TOKENS` | 16384 | Claude API thinking + visible-output budget |
| `GEMINI_MODEL` | Gemini 3.7 Flash (High) | Gemini agy CLI model |
| `GEMINI_API_MODEL` | gemini-3.1-pro-preview | Gemini API-mode model ID (v2.15) |
| `CODEX_MODEL` | gpt-6-astra | Codex model (v2.5) |
| `MISTRAL_MODEL` | mistral-large-3 | Mistral model (v2.5) |
| `MISTRAL_CLI_MODEL` | mistral-medium-3.5 | Vibe CLI model alias |
| `SYNTHESIS_STRATEGY` | coverage | Synthesis strategy (coverage=union of distinct points) |
| `ENABLE_SEMANTIC_CACHE` | true | Semantic response caching (v2.3) |
| `CACHE_TTL_HOURS` | 24 | Cache expiration in hours (v2.3) |
| `ENABLE_RESPONSE_LIMITS` | false | Response token limits (v2.3, opt-in) |
| `ENABLE_COST_AWARE_ROUTING` | false | Cost-based model routing (v2.3) |
| `ENABLE_COMPACT_REPORT` | true | Compact report format (v2.3) |
| `ENABLE_BUDGET_LIMIT` | false | Budget enforcement (v2.4, opt-in) |
| `BUDGET_ACTION` | warn | Action on budget exceeded: warn/stop (v2.4) |
| `QWEN3_USE_API` | false | Use DashScope API instead of qwen CLI (v2.7) |
| `QWEN3_CMD` | qwen | Qwen CLI command (v2.7) |
| `ENABLE_KIMI` | true | Enable Kimi consultant (v2.9) |
| `KIMI_CMD` | kimi | Kimi CLI command (v2.9) |
| `KIMI_MODEL` | kimi-code/k3 | Kimi CLI model alias |
| `ENABLE_MINIMAX` | true | Enable MiniMax consultant (v2.10; CLI via mmx v2.21) |
| `MINIMAX_USE_API` | false | Use MiniMax API instead of the mmx CLI (v2.21) |
| `MINIMAX_CMD` | mmx | MiniMax CLI command (v2.21) |
| `MINIMAX_API_KEY` | - | MiniMax API key (API mode only) (v2.10) |
| `MINIMAX_MODEL` | MiniMax-M2.7 | MiniMax model (v2.10) |

## External Dependencies

- `agy` CLI - Antigravity CLI (Gemini consultant; successor to the deprecated Gemini CLI, v2.15)
- `codex` CLI - OpenAI Codex
- `vibe` CLI - Mistral Vibe
- `kimi` CLI - Kimi Code (v2.9)
- `claude` CLI - Claude (v2.2, also used for synthesis)
- `qwen` CLI - Qwen via qwen-code (v2.7, optional)
- `mmx` CLI - MiniMax via mmx-cli (v2.21, optional; `npm i -g mmx-cli`, auth `mmx auth login`)
- `jq` - JSON parsing

## Error Handling and Retry

The system handles errors with:

- **Automatic retry**: `MAX_RETRIES` attempts (default: 2)
- **Delay between retries**: `RETRY_DELAY_SECONDS` (default: 5s)
- **Cross-platform timeout**: Supports Linux (`timeout`), macOS (`gtimeout`), and POSIX fallback
- **Exit codes**: 0 = success, 1 = error, 124 = timeout

```bash
# Retry configuration
MAX_RETRIES=3
RETRY_DELAY_SECONDS=10

# Per-consultant timeout
GEMINI_TIMEOUT=240
CODEX_TIMEOUT=180
```

## Extended Documentation

For detailed information, see:
- [docs/SETUP.md](docs/SETUP.md) - Installation and authentication
- [docs/COST_RATES.md](docs/COST_RATES.md) - Rates and budget management
- [docs/SMART_ROUTING.md](docs/SMART_ROUTING.md) - Affinity matrix and routing
- [docs/JSON_SCHEMA.md](docs/JSON_SCHEMA.md) - Complete output schema

## Development Notes

- Scripts in `lib/` are libraries, not standalone executables
- Output goes to `$XDG_CACHE_HOME/ai-consultants/consultations/TIMESTAMP/` (normally `~/.cache/ai-consultants/consultations/`)
- Session state lives in `$XDG_STATE_HOME/ai-consultants/sessions/` (normally `~/.local/state/ai-consultants/sessions/`)
- All timeouts are configurable in `config.sh`
- Consultants can be disabled individually (`ENABLE_GEMINI=false`, etc.)
- Use `.env.example` as template for environment configuration
- Run `./scripts/doctor.sh` to verify configuration

## Git Conventions

Use [Conventional Commits](https://www.conventionalcommits.org/): `feat:`, `fix:`, `chore:`, `docs:`, `refactor:`, `test:`, `perf:`, `ci:`. Scope is optional (e.g., `feat(routing): add fallback escalation`).

### Pre-commit hook (v2.14.1+)

A git pre-commit hook runs `shellcheck` on staged `.sh` files using the exact CI invocation. Install it once per checkout:

```bash
npm run install-hooks    # copies scripts/hooks/pre-commit into .git/hooks/
```

Manual lint of the full repo: `npm run lint`. Bypass the hook: `git commit --no-verify` (use sparingly — CI will catch the same warnings).

## Release Process

Every version bump **must** include a release note in `docs/releases/v<VERSION>.md`. Use the template below.

**The whole flow is automated by the maintainer's `ai-consultants-release` skill — run `/ai-consultants-release <VERSION>` from anywhere inside this checkout.** It orchestrates the steps below and refuses to proceed when a surface is missing.

That skill deliberately lives **outside this repository**, at `~/.claude/skills/ai-consultants-release/`, and is not distributed. `scripts/install.sh` git-clones this entire repo into `~/.claude/skills/ai-consultants`, so anything under a `.claude/skills/` directory here reaches every `curl | bash` user — and release tooling that pushes tags and deploys the showcase site must not. The skill's own `references/surfaces.md` is the authoritative map of every release surface; this section is the summary.

Consequence worth knowing: the skill's `finalize.sh` re-verifies the same version surfaces `scripts/release.sh` owns, and the two now live in different places with nothing making them drift together. `finalize.sh` compares its arity against that script's `SURFACES` array on every run and aborts if they disagree — so **adding or removing a surface in `scripts/release.sh` will correctly fail the next release until the skill is updated to match.** That is the tradeoff for not shipping release tooling to users.

### Steps

1. **Preflight**: `~/.claude/skills/ai-consultants-release/scripts/preflight.sh <VERSION>` — read-only gate on both repos (branch, cleanliness, origin sync, forward semver, free tag, tooling)
2. **Bump the version**: `scripts/release.sh <VERSION>` — rewrites 9 anchored surfaces across 7 files (`package.json`, `scripts/config.sh`, `SKILL.md` ×2, `README.md` ×2, `CLAUDE.md`, `docs/cost_rates.json`, `docs/COST_RATES.md`), validates them, and runs `npm test` + `npm run lint`. Never commits, tags, or publishes.
3. **Write the three changelogs** (hand-written — see "Why three changelog surfaces?" below): `CHANGELOG.md` entry at the top, `## Changelog` entry in this file, and `docs/releases/v<VERSION>.md`
4. **Update workspace sync surfaces**: `../CLAUDE.md` workspace guide ("Latest at time of last sync" + Recent release line)
5. **Finalize**: `~/.claude/skills/ai-consultants-release/scripts/finalize.sh <VERSION> --message-file … --tag-message-file …` — re-verifies all 9 version surfaces *and* the 3 doc surfaces, re-runs the gate, then commits, creates the annotated tag, and pushes both
6. **Publishing is automatic**: pushing the `v*` tag triggers `.github/workflows/publish.yml`, which re-runs the gate, verifies the tag matches `package.json`, publishes to npm via **Trusted Publishing (OIDC — no `NPM_TOKEN`)** with provenance, and creates the GitHub release from the release note. Both terminal steps are idempotent, so a failed run can be re-run after a fix — **never move or delete a pushed tag**, and never `npm publish` from a workstation.
7. **Sync the showcase site last**: `~/.claude/skills/ai-consultants-release/scripts/sync_site.sh <VERSION> --message-file …` waits for npm to actually serve the version, bumps `index.html` (`softwareVersion` schema + badge), then commits and pushes. Editorial copy (roster, feature cards, presets table, install commands) is hand-edited before running it. The wait enforces the npx timing rule: a subcommand advertised as `npx ai-consultants <cmd>` before publication does not error — `bin/` routes an unknown argument to `consult_all.sh`, starting a real **billable** consultation.

**Why three changelog surfaces?** They serve different audiences:
- `CLAUDE.md ## Changelog` — what the *next maintainer* needs (file/line references, rationale, latent bugs uncovered)
- `CHANGELOG.md` — what *users on a specific version* need (concise, categorized, scannable)
- `docs/releases/v<VERSION>.md` — what *people deciding to upgrade* need (highlights, breaking changes, upgrade guide)

Drift between these is the most common release-process bug. Keep them in sync per release.

### Release Note Template

```markdown
# Release v<VERSION>

**Date:** <YYYY-MM-DD>
**Type:** <Major | Minor | Patch> — <one-line summary>
**Previous:** v<PREVIOUS_VERSION>

## Highlights

- <3-5 bullet points with the most impactful changes>

## What's New

### <Feature/Area 1>

<Description of what changed and why. Include tables for multi-item changes.>

### <Feature/Area 2>

<Description.>

## Breaking Changes

<List breaking changes, or "None" if backwards-compatible.>

## Upgrade Guide

\`\`\`bash
# If installed via git clone
cd ~/.claude/skills/ai-consultants && git pull

# If installed via curl | bash
curl -fsSL https://raw.githubusercontent.com/matteoscurati/ai-consultants/main/scripts/install.sh | bash
\`\`\`

<Note any configuration changes required, or "No configuration changes required.">

## Commits

- \`<hash>\` <commit message>

## Contributors

- <list>
```

### Guidelines

- **Audience**: Developers who use the skill. Write for someone who hasn't seen the commits.
- **Highlights first**: Lead with impact, not implementation. "Provider failures are now diagnosed" > "Added an error branch".
- **Breaking changes prominent**: If any exist, they go in a dedicated section — never buried in a bullet list.
- **Quantify when possible**: Token savings percentages, issue counts, file counts.
- **Upgrade guide always present**: Even if the answer is "just pull", make it explicit.
- **No internal jargon**: Avoid referencing issue tracker IDs or internal codenames without context.

## Changelog

### v5.1.1 (2026-09-10)

- **DeepSeek V4.1 Flash policy.** `scripts/config.sh` selects `deepseek-flash` for the default and every tier. `scripts/configure.sh` migrates only the two exact historical unpinned defaults; durable pins and explicit overrides remain intact. No credentials are imported or personal configuration rewritten by the upgrade.
- **Conservative cost accounting.** `docs/cost_rates.json` and the catalog-independent fallbacks use $0.30/M input and $1.20/M output for Flash, including retired Flash aliases now served by the new model. `format_cost_caveats` discloses the peak/cache-miss assumption and excluded discounts; estimates are not provider invoices. Other legacy rates are retained as historical estimates.
- **Verification and scope.** PR #27 passed all 32 CI suites, plus local API 101, configure 138 and core 451 checks. One authorized API request on `96816b5` returned HTTP 200 and a correct structured expiry-boundary answer, with `deepseek-flash` provider-reported, max effort, 119 input / 602 output measured tokens. The shared transport is unchanged; provider identity is not rewritten into a version label. No frozen P1.6 input or live benchmark claim changes.


### v5.1.0 (2026-09-09)

- **Fable and Astra model policy.** `scripts/config.sh` and `scripts/configure.sh` align default/premium/maximum with Fable 5.1 and Astra, retain Opus 5/Terra for standard and Haiku/Luna for economy, and migrate exact historical unpinned Codex defaults while recording their origin. `resolve_codex_effort` applies implicit high per request so a later tier cannot inherit an automatically exported value.
- **Completion and identity are independent checks.** `scripts/lib/claude_stream.jq` derives content identity from assistant text events, keeps billing participants separate, rejects incomplete/error/max-token streams and preserves available accounting. `query_codex.sh` reads usage from JSON events while keeping the final-message file as the answer. Requested-only Codex identity remains truthful and is not an invented promotion blocker.
- **Grok sandbox checks precede fallback decisions.** `scripts/lib/grok_sandbox.sh` supplies passive socket and diagnostic checks shared with doctor/OAuth. Hidden `--no-memory` is supported by the installed parser even though omitted from help: the earlier diagnosis of an unsupported flag was incorrect. It remains mandatory in dispatch. Metadata annotation cannot replace the adapter's status when its same-directory temporary file fails.
- **Accounting and configuration.** Fable and Astra both have catalog-independent rate fallbacks. Astra estimates disclose long-context multipliers and excluded cache/service adjustments; no estimate is called a provider invoice. Explicit `AI_CONSULTANTS_CONFIG_DIR` selects private configuration; project dotenv discovery and credential import are not added.
- **Validation scope.** CLI-only acceptance was selected explicitly. The three provider smoke results retain their actual commit and identity provenance; API paths retain automated tests without a live-promotion claim. The maintainer Breadth v1 harness is included in the repository but excluded from npm, and its frozen inputs are unchanged. No completed P1 benchmark or coverage-gain claim is made.


### v5.0.0

- **Coverage claims are now locally auditable instead of resting on synthesis prose.** `scripts/lib/coverage_integrity.sh` normalizes successful structured envelopes into deterministic `<consultant-slug>:<index>` findings, overwriting any provider-supplied IDs. `scripts/synthesize.sh` asks the synthesizer to cite those IDs, then independently compares expected and represented IDs. Missing/duplicate/non-normalizable sources produce `DEGRADED`; unknown IDs and structural failures produce `FAILED`; invented IDs are removed from published coverage. The legacy `coverage`, `weighted_recommendation`, risk, action, and individual-response fields remain present.
- **Truncation can no longer hide evidence loss.** Coverage/union prompts receive complete atomic findings. Only non-normalizable fallback context is capped, at Unicode code-point boundaries, and local `coverage_input_truncated` / `truncated_consultants` values overwrite model output in normal, local-fallback, and failed-closed artifacts. The report renders status, audited fields, truncation, and the affected consultants. The first Ubuntu CI run exposed an jq-expression compatibility gap; the final form uses jq 1.6-compatible string slicing and passed the release runner.
- **Presets are host-aware and fail closed.** `get_effective_preset_panel_size` and `select_preset_consultants` preserve the advertised panel after self-exclusion by filling from the canonical roster using static transport evidence only. `max_quality` is ten in the catalog and nine for a canonical host. Insufficient capacity now aborts with promised/selected/missing counts, and health-gate pruning is rechecked before Round 1. Custom agents can satisfy capacity; explicit per-consultant false values remain fallback opt-outs.
- **Public modes and their documentation have one executable source of truth.** `scripts/public_registry.sh` owns `fast-check -> fast/coverage`, `coverage-review -> balanced/coverage`, `max-coverage -> max_quality/coverage`, every existing preset/alias, and every strategy/default. `scripts/generate_public_docs.sh --check` renders and verifies README, SKILL, and reference tables. CLI conflict/override/unknown diagnostics run before user config, output directories, health checks, or adapters. `doctor --suggest-preset` downgrades to runnable `minimal` when only two effective transports remain.
- **The v5 release path was exercised end to end within the preregistered cap.** One no-retry `coverage-review` smoke dispatched Gemini, Mistral, and Kimi once each, then synthesized once with Claude: four dispatches total, 3/3 consultant responses, `coverage_integrity=MET` with 12/12 source IDs, and no truncation. The release gate passed 27/27 suites, ShellCheck, Bash 3.2 syntax, generated-doc drift, packaging, and sentinel isolation.
- **Development safety record.** An early P0.5 mode-registry lookup returned only the mode name, left the preset empty, and caused a local test to invoke ambient adapter auth/capability gates. Gemini/Codex were fixtures; the other adapters returned missing-key/auth/capability diagnostics. The retained transcript does not establish a network/provider request and no successful provider response was observed. The lookup was fixed, all later local release gates used provider/curl sentinels, and the final stub-only mode smoke recorded zero non-fixture adapter calls.

  Deliberately retained: every v4 preset/alias, advanced configuration key, smart routing, custom agents, self-exclusion, and legacy JSON fields. Not claimed: semantic verification that a synthesized sentence faithfully paraphrases the cited atom; v5.0 verifies attribution-set integrity, not natural-language entailment.

### v4.0.4

- **Housekeeping now removes dead implementation without removing public compatibility.** The cleanup deletes uncalled API, cache, optimizer, common, cost, persona, reliability, routing-summary, and session-display helpers, plus the stale README changelog and redundant npm ignore file. The documented `get_routing_mode`, `get_recommended_count`, and `get_category_timeout` helpers stay available and are regression-tested. Six historic config keys that had no runtime reader remain accepted as explicitly deprecated no-ops through v4.x: `SESSION_CLEANUP_DAYS`, `ENABLE_PROGRESS_BARS`, `ENABLE_EARLY_TERMINATION`, `USE_COMPACT_PROMPTS`, `ENABLE_SELECTIVE_CONTEXT`, and `MAX_FILES_PER_CONSULTANT`.
- **All Bash test suites now share one assertion framework.** `scripts/lib/test_helpers.sh` owns counters, pass/fail/skip rendering, regex/string/numeric/JSON/exit assertions, section rendering, and both concise and detailed summaries. `test_suite.sh` retains compatibility adapters for its established assertion call sites while delegating all mechanics; its summary fails on zero assertions or any failure. The previously excluded `test_functions.sh` is replaced by auto-discovered `test_common.sh`, which keeps regression coverage for diagnosed failures, quorum, health probes, feature-flag recognition, Kimi stream extraction, fenced/bare JSON normalization, confidence preservation, and both detailed-summary failure gates.
- **The package and active guidance are smaller and truthful.** `package.json#files` is the sole npm allowlist; the package excludes the GitHub release-note archive, tests, fixtures, and maintainer release tooling while retaining active docs. The manifest moves 107 → 61 files, packed bytes 340,662 → 268,831, and unpacked bytes 1,145,406 → 929,330. Setup/CI instructions use `doctor` and `configure`, examples use XDG consultation paths, and routing documentation describes real affinity selection rather than an inert routing-mode/timeout path.
- **Verification is wholly offline for this patch.** No consultant adapter, model, host transport, roster, cost rate, or synthesis behavior changed, so no billable live smoke was needed. Hermetic `npm test` passed 24/24 suites; core 447 assertions, configure 94 checks, installer/package 51 checks, common-helper 38 checks, Bash 3.2 syntax, ShellCheck, `npm ci --dry-run`, markdown links, and two rounds of cross-family cleanup review are green. PR #16 merged with its merge commit and all final CI checks passed.

### v4.0.3

- **GLM's active catalog target moves from `glm-5.3` to `glm-5.3-flash` without changing transport or tier semantics.** `scripts/config.sh:189` changes the clean-install default, and the maximum/premium/standard branches at `scripts/config.sh:626-656` select the same exact provider ID. `max_quality` still supplies `GLM_REASONING_EFFORT=max`; economy deliberately remains `glm-4-flash`.
- **Existing managed installs advance without rewriting explicit choices.** `scripts/configure.sh:310-318` migrates the two exact historical generated defaults, `glm-5.2` and `glm-5.3`, to Flash and records `# ai-consultants:default`. A `# ai-consultants:pin` survives, including an explicit old `glm-5.3`; `scripts/test_configure.sh:449-461` covers both current-default migration and pin preservation.
- **Identity and pricing stay honest.** The OpenAI-compatible adapter sends `glm-5.3-flash`, `reasoning_effort=max`, and the 16,384-token GLM budget; the synthetic transport test requires provider-reported identity and requested-model metadata (`scripts/test_api_transport.sh:253-263`). `docs/cost_rates.json` marks Flash unpriced and retains a zero/unpriced entry for legacy `glm-5.3` pins, avoiding the generic fallback rate.
- **The exact ai-consultants transport completed one authorized no-retry live smoke.** Z.AI returned HTTP 200 and provider-reported `glm-5.3-flash` in about 12 seconds with 37 measured input tokens and 308 output tokens. The provider accepted max effort but returned near-structured text rather than the requested JSON; the adapter preserved `PONG` through its explicit unstructured fallback with confidence 5, so strict schema adherence is not claimed. PR #15 CI, the post-bump 23-suite gate, 452 core checks, configure 94/94, API transport 88/88, and ShellCheck are green.

  Deliberately unchanged: `delegation-kit` remained read-only; its Claude-to-Z.AI gate and runner were not copied into this API-only consultant. Installation does not silently edit personal configuration, so an existing unpinned `.env` advances when the user runs `configure`; explicit pins remain user-owned. The separate `agent/model-defaults-4.1.0-wip` branch is not part of this patch release.

### v4.0.2

- **The Grok adapter used to copy a rotating OAuth credential into a disposable HOME and delete any refreshed token at exit.** Parallel calls amplified the race: both could seed from one refresh token, one rotation vanished with its temp directory, and the ambient session could be left stale. `scripts/lib/grok_oauth.sh` now owns a durable generation under the XDG data root and `scripts/query_grok.sh` defaults to `GROK_OAUTH_MODE=shared`.
- **Shared means credential state only.** Every invocation still has a private HOME, workspace, prompt file, output, capability-probe HOME, permission mode, strict sandbox, empty tool surface, model, and effort. Concurrent processes use one runner-owned `GROK_HOME`, allowing Grok Build's native auth lock to coordinate refresh while inference remains outside the ai-consultants lock.
- **Publication is optimistic and atomic.** A recoverable directory lock bounds seed, bootstrap, inventory, and publication; valid rotation is staged mode 600 and renamed into the ambient `auth.json`. The marker binds generation and last-synced SHA-256. If a concurrent external `grok login` changes the ambient digest, that login wins, the consultation fails temporarily, and no API fallback occurs.
- **Malformed and obsolete state fails closed but recovers forward.** Corrupt credentials never replace ambient auth; dead locks are reclaimed; unpersistible refreshes publish no answer. A later run reseeds from valid ambient state. Generations carrying the exact legacy runner-owned `config.toml` signature are superseded without modification or deletion.
- **Grok 1.0.4 required exact-transport fixes beyond the reference implementation.** Its pristine-home bootstrap lazily creates metadata/cache/DB state; capability probes must use a separate credential-free HOME; authenticated inventory is bounded and coordinated. Provider-owned `config.toml` cannot be replaced before each inventory, and the display sentence “You are logged in with grok.com.” is not stable enough to be an auth gate. Exit 0, a still-valid credential, and the requested inventory model are the attestation.
- **Secrets remain private.** The prompt and tokens never enter argv. Grok dispatch uses redacted `run_query` errors, so token-shaped provider stderr is absent from stdout, normal logs, error envelopes, and artifacts. Forced API mode remains stateless; missing/auth-unavailable CLI fallback and post-dispatch fallback suppression are unchanged.
- **Verification closes both concurrency layers.** The adapter suite grows to 88 checks and covers simultaneous fake dispatch, rotation/reuse, external-login CAS, corrupt state, dead locks, atomic policy reconciliation, legacy migration, stateless API mode, serialized mode, and token leakage. A real two-process `grok-4.6`/`xhigh` smoke returned structured `PONG` twice with exit 0, no workspace changes, no API fallback, and fully aligned ambient/generation/marker state. Full gate: 23/23 suites, 452/452 core checks, ShellCheck and PR #14 CI green.

  Deliberate residuals: superseded generations are retained because deleting one that an active process still references would be unsafe. Real forced refresh rotation and a real concurrent `grok login` were not induced; those conflict paths are verified with controlled fakes. The separate local 4.1.0 model-default WIP is not part of this release.

### v4.0.1

- **The installer's first line of user-visible output was six major/minor generations stale.** `scripts/install.sh:53` printed `AI Consultants v2.10 - Installation` even while the public binary, package metadata, npm registry, active checkout, and doctor all reported v4.0.0. That did not change installed bytes or runtime behavior, but it made a successful v4 install look wrong at the exact moment users were deciding whether to trust it.
- **The banner is now deliberately version-neutral, not dynamically coupled to a tenth version surface.** The curl-piped installer prints before it has cloned or updated the checkout, so sourcing `scripts/config.sh` there would either be impossible on a fresh install or would report the old local version during an update. A static `AI Consultants - Installation` label stays truthful throughout both paths and does not need another release-time rewrite.
- **The failure is regression-tested through rendered output.** `scripts/test_install.sh` sources the existing define-only path, calls `print_header`, requires the installer label, and rejects `AI Consultants v<major>.<minor>` in the banner. The focused installer suite grows to 47 checks; the full 23-suite gate and ShellCheck pass on the 4.0.1 surfaces.

  Deliberately unchanged: no runtime, model, preset, credential, host command, or minisite copy changes. The showcase site already reflects the ten-consultant v4 panel; after npm serves 4.0.1 it needs only its two mechanical version surfaces bumped.

### v4.0.0

- **`max_quality` now means the full ten-consultant panel at each exact transport's strongest supported effort and response budget.** GLM and DeepSeek run at `max`; Grok uses its highest supported setting. K3-256k, MiniMax M3, and Qwen3.8-Max remain confined to the maximum tier, with Qwen promoted only when the authenticated Token Plan transport is already configured.
- **Gemini 3.7 Flash High completed an authenticated exact-adapter `agy` smoke and is promoted only on that CLI transport.** It serves maximum, premium, and standard; Low serves economy. The unverified Google API transport remains on 3.1 Pro, while Fable 5 and the new Mistral API IDs remain opt-in.
- **Cursor is no longer a consultant.** The ambiguous `agent`/`cursor-agent` runtime, model tiers, presets, doctor/configuration fields, personas, costs, tests, and website card were removed. Cursor remains a supported host through SkillPort; the panel itself has 10 consultants.
- **Managed migrations are exact and pins survive.** `configure` upgrades only the historical managed Gemini 3.1 Pro CLI, GLM 5.2, and Grok 4.5 values, while removing obsolete Cursor settings. Environment/`--set` model values and any `# ai-consultants:pin` entry remain user-owned. The catalog-parity gate checks all 40 consultant/tier cells and all 16 Gemini/Mistral transport cells against `docs/cost_rates.json`, requiring every automatic target to be priced or explicitly unpriced.
- **Claude can launch the skill as a real host, not merely describe it.** `SKILL.md` carries an imperative execution contract; all host commands use a private query-file handoff, preserve preset/strategy options, pass the caller project through `--context-root`, and set `INVOKING_AGENT`; self-exclusion covers consultation, full-roster targeted follow-ups, and synthesis for Claude/Codex/Gemini. Offline E2E proves a Claude host dispatches Gemini/Codex/Mistral with no Claude artifacts. A real Claude Code/Sonnet run started from an external temp project, used `mktemp`, mode 600, a quoted heredoc, trap cleanup, `--context-root`, and `src/context.txt@CONTEXT`; Gemini 3.7 High and Codex 5.6 Sol returned (2/2), the external marker/tag reached both, no Claude artifact appeared, and the private staging directory was removed. The live run disabled synthesis; symmetric regressions prove each host is excluded there. Current runtime prompts, schemas, templates, help, and instructions are coverage-only; stale Codex/Gemini debate and roster-audit commands are removed.
- **Cross-family host review went `NO_SHIP` → `NO_SHIP` → `SHIP`.** The review found the Bash 3.2 empty-array abort, query-file/context parser conflict, caller-project path loss, unsafe/host-self follow-ups, stale live consensus output, broad uninstall glob, and active v3.0 residue. Each was fixed with executable coverage. After the `SHIP` verdict, its remaining low-severity findings were also resolved: staged files retain their original project-relative identity, fallback synthesis reports the requested strategy, follow-up context no longer rides in argv and respects disabled consultants, and remaining contributor/cost/test residue was removed.

  Deliberate residuals: Vibe isolates the workspace but retains ambient HOME for its authenticated CLI state; unpriced credit/subscription models are disclosed but cannot be enforced by a dollar budget; provider/requested model substitutions are available in raw JSON but not yet summarized in the human report.

### v3.5.0

- **The catalog now distinguishes an advisory panel's normal premium tier from subscription-only maximum targets.** `get_model_for_tier()` accepts an explicit transport, while `apply_model_tier maximum` selects K3-256k and MiniMax M3 and promotes Qwen3.8-Max only when `QWEN3_USE_API=true`, the wire is OpenAI-compatible, the endpoint is a Token Plan `/chat/completions` URL, and a key is present (`scripts/config.sh:581-737`). The Qwen effort is marked tier-managed, cleared when leaving maximum, and never overwrites a user pin. Gemini 3.7, Fable 5, and the new Mistral API IDs remain catalogued opt-ins after their exact transports failed authentication or timed out; normal runs do not inherit delegation-kit's routing gates.
- **Mistral CLI and API names are no longer conflated.** `MISTRAL_CLI_MODEL=mistral-medium-3.5` reaches Vibe through `VIBE_ACTIVE_MODEL`; `MISTRAL_MODEL` remains API-only. Vibe verifies its bounded read-only interface, runs `plan` for one turn in a mode-700 temporary workspace, and treats empty or post-launch failures as failures (`scripts/query_mistral.sh:47-143`). The current machine passed the exact Vibe smoke; no Mistral API key was available, so the new API IDs were not promoted.
- **Cursor now means Cursor.** The old `CURSOR_CMD=agent` resolved to Grok on this machine. Runtime, configure, and doctor now default to `cursor-agent`, validate the Cursor help surface, bound model-inventory calls, require Composer 2.5, and invoke `--mode ask --trust` only against an ephemeral workspace (`scripts/query_cursor.sh:33-132`, `scripts/configure.sh:322-363`). The local Cursor account is not logged in, so runtime authentication remains a user gate rather than something this release works around.
- **Model identity evidence is additive and honest.** Every envelope carries the requested ID and one of `provider-reported`, `capability-probed`, or `requested-only`; API model strings are syntax-validated before adoption (`scripts/lib/common.sh:935-1068`, `scripts/lib/api_query.sh:162-194`). `modelUsage` remains billing participation, not content identity. The shared billing resolver prefers a known effective ID, then the explicit requested ID, and uses consultant fallback only for legacy responses that lack the new metadata (`scripts/lib/costs.sh:311-430`).
- **The reliability boundary is finite.** Cursor/Gemini/Mistral capability probes and Cursor configure/doctor probes have explicit time limits. An HTTP 200 with an empty body increments the retry counter, including at the shipped `MAX_RETRIES=2`, instead of spinning indefinitely (`scripts/lib/api.sh`, `scripts/test_api_transport.sh`). Google `thinkingConfig` is emitted only for Gemini 3.7; the proven 3.1 API body remains byte-compatible.
- **Managed migrations are exact and pins survive.** `configure` upgrades only the historical managed Cursor command, GLM 5.2, and Grok 4.5 values. Environment/`--set` model values and any `# ai-consultants:pin` entry remain user-owned. The new catalog-parity gate checks all 44 consultant/tier cells and all 16 Gemini/Mistral transport cells against `docs/cost_rates.json`, requiring every automatic target to be priced or explicitly unpriced.
- **Promotion evidence is recorded without overstating it.** Exact ai-consultants smokes passed for Mistral Vibe Medium 3.5, GLM 5.3, Grok 4.6, K3-256k, Qwen3.8-Max Token Plan, and MiniMax M3. Gemini 3.7 lacked agy/API auth, Mistral API lacked a key, Fable 5 timed out, and Cursor lacked login; those stay opt-in/unverified. Cross-family Opus 5/max review first returned `NO-SHIP` with four blockers, then `SHIP` after the hermeticity, trust, probe, thinking, billing, retry, and parity findings were fixed. Final gate: 22/22 suites, 443/443 core checks, ShellCheck and npm tarball green.

  Deliberate residuals: Cursor and Vibe isolate the workspace but retain ambient HOME for their authenticated CLI state; unpriced credit/subscription models are disclosed but cannot be enforced by a dollar budget; provider/requested model substitutions are available in raw JSON but not yet summarized in the human report.

### v3.4.0
- **Gemini was unreachable from an SSH session, and no amount of re-authenticating helped.** `agy` picks its credential store from the environment: seeing `SSH_CLIENT`, `SSH_CONNECTION`, or `SSH_TTY` it switches to a file-based token store and never consults the macOS Keychain. A user who signed in locally therefore lost Gemini entirely over SSH, and signing in again landed the new credential in the Keychain the CLI had already decided to ignore. Reproduced live before fixing (`agy models` → "Please sign in"; the same command under `env -u SSH_CLIENT -u SSH_CONNECTION -u SSH_TTY` → full inventory), and confirmed again end to end after. Found by reading delegation-kit's 0.13.1 fix and checking whether the same failure applied here — it did, in the very session doing the work.
- **The fix ships in two shapes on purpose** (`lib/common.sh:163-164`). `AGY_ENV_PREFIX` is an array of `env -u …` for anything that reaches `agy` through `run_with_timeout`, because GNU `timeout` execs a binary and **cannot invoke a shell function**; `agy_env` is the same-shell wrapper, defined *from* that array so the two cannot drift. Applied at every executing call site — `query_gemini.sh`, `doctor.sh`, `update_clis.sh`, and `build_synthesis_args` (which is executed directly by `synthesize.sh:304`, not through timeout, so either shape would work there — it uses the array for consistency). The remaining `${GEMINI_CMD:-agy}` hits are `command -v` probes and config-dump strings, which execute nothing.
- **The Codex adapter was the least isolated in the panel, contradicting our own v3.1.0 policy.** That release isolated Grok specifically so "local plugins, hooks, skills, memories, and project instructions stay outside the consultation", then left Codex running `codex exec --skip-git-repo-check -m … "$FULL_QUERY"` in the ambient environment: the user's `~/.codex/config.toml`, the repo's `AGENTS.md`, their hooks, MCP servers, and execpolicy rules all reached an advisory consultation, the sandbox was unpinned, and the prompt rode in argv — the same `ARG_MAX` and process-list exposure v3.1.0 had already fixed once. CLI mode now uses an ephemeral mode-700 HOME/CWD with `--ephemeral --ignore-user-config --ignore-rules -s read-only`, prompt on stdin, answer from a private `-o` payload (`query_codex.sh:86-160`).
- **`CODEX_HOME` is resolved before HOME is overridden** (`query_codex.sh:89`) and passed through unchanged: `--ignore-user-config` skips `config.toml` but auth still resolves through `CODEX_HOME`, so repointing it at the isolated home would break authentication outright. Note the coupling recorded at `query_codex.sh:123`: `--ignore-user-config` is safe **only** because `-m` pins the model — it also makes Codex ignore any `-p <profile>`, so a profile must never be added here.
- **Review caught the `-o` payload rewriting real failures into successes.** The first implementation adopted the payload whenever it was non-empty, *regardless of how `run_query` finished* — so a timeout (124), an auth error, or exhausted retries that left a partial answer on disk all became `exit_code=0` and entered synthesis as a healthy consultation. That is strictly worse than the bug it replaced and defeats the v2.18/v2.19 diagnosed-failure machinery. Adoption is now gated on success; exit 0 with an empty payload remains a failure. Both directions are regression-tested, and both tests were confirmed non-vacuous by reintroducing the defect and watching them fail.
- **Correction, on the record: the justification for that override was measured and found false.** It was defended as avoiding "false empty-stdout failures", since `run_query` treats an empty stdout as a failure *and retries* — which would have meant every Codex consultation making `MAX_RETRIES` billable calls. Measured against the real CLI, stdout carries the answer (11 bytes), the `-o` file mirrors it, and only token telemetry goes to stderr. There was never a retry storm. `-o` is kept as defence in depth — and the isolation is what makes stdout clean in the first place, since `--ignore-user-config` plus an ephemeral HOME leaves no hooks to emit chatter.
- **`*_REASONING_EFFORT` had been a documented no-op on the default path.** `warn_effort_ignored_in_cli` announced the setting as API-mode only, but CLI-first transport has been mandatory since v2.21 — so in the shipped configuration the knob did nothing at all, while three CLIs did expose the control. Now wired: Codex `-c model_reasoning_effort=` (`query_codex.sh:148`), Claude `--effort` (`query_claude.sh:78-86`), Gemini `--effort` (`query_gemini.sh:97-105`). This is not cosmetic — cheap models do not degrade gracefully, so a consultant silently inheriting an unknown effort is not a controlled panel member.
- **Deliberately no per-CLI effort allowlist.** `agy` accepts only `low|medium|high`, but the accepted enum is a provider fact, not ours; an unsupported value comes back as a diagnosed 400 rather than being pre-empted by a table in shell. Same stance v2.25.0 took for Qwen, and for the same reason: hardcoding per-model facts is what produced the stale `MODEL_USED` bugs.
- **The parameter-contract test did its job on code it knew nothing about.** `test_configure.sh:91-121` scans every non-test `*.sh` for `${VAR:-}` and requires each name to be accepted by `configure --set`; the three new effort reads and then `CODEX_HOME` each failed it in turn. The effort variables are now declared in `.env.example` (commented out, matching the `QWEN3_REASONING_EFFORT` convention). `CODEX_HOME` went to the **exclusion list** instead (`test_configure.sh:105-107`), because it is owned by the Codex CLI: we read it to locate the user's auth store and must never persist it — declaring it would advertise `configure --set CODEX_HOME=…`, writing another tool's private auth path into our config. It joins `HOME`, `TMPDIR`, `XDG_CONFIG_HOME`, and `XAI_API_KEY`, and the distinction is now stated inline so nobody "fixes" it by moving it into `.env.example`.
- **The model catalog refresh would have been invisible to every existing installation — and only a live smoke test caught it.** The full gate was green (19/19, shellcheck clean, both new regression tests proven non-vacuous) and a real consultation still reported `model=gpt-5.5`. `init` copies `.env.example` verbatim, so the then-current Codex default was persisted as an ordinary unmarked value, and `configure` reads any unmarked value as a deliberate user pin. The proof sits side by side in an installed config: `CODEX_MODEL=gpt-5.5` beside `CLAUDE_MODEL=claude-opus-5 # ai-consultants:default`, where only the marked line can migrate. Fixed with the direct analogue of v3.2.0's Claude migration (`configure.sh:288-295`) — one exact historical value, one target, skipped on the pin marker, rewritten with the default marker. Verified against real config files: unmarked `gpt-5.5` migrates, a pin recorded via `--set` survives, an unrelated `gpt-5.4` is untouched.
- **Deliberately not generalised.** A table-driven "upgrade anything that looks old" rule would silently rewrite genuine user pins. Two adjacent narrow blocks is the intended shape; a third model needs a third block.
- **Model ids were verified by querying the installed binaries, never from documentation.** `agy models` (with the SSH markers cleared) returns slugs — `gemini-3.6-flash-high`, `gemini-3.1-pro-high` — but the display names we ship still resolve, checked by round-tripping both forms. Gemini premium stays `Gemini 3.1 Pro (High)` because the inventory has **no 3.6 Pro**; only standard/economy move to 3.6 Flash. Codex `gpt-5.6-sol|terra|luna` and `claude-sonnet-5` each answered a live probe.
- **Cost rates ship with the ids, not after them.** `calculate_session_cost` never calls `resolve_model_for_cost`, so a model absent from the catalog falls to the `*)` arm of `get_input_cost_per_1k` at 0.005/0.015 and is reported as a confident, wrong number — the trap documented at v2.25.0. Sonnet 5 is entered at its standard $3/$15 rate rather than the introductory $2/$10, which expires 2026-08-31; that slightly overstates for a few weeks and is exactly right afterwards.
- **Qwen: two ids, two deployments, not a rename.** delegation-kit's snapshot said `GET /models` lists both `qwen3.8-max` and `qwen3.8-max-preview`. Verified live against a real Token Plan account: both are served **and each completion reports its own requested id back in `.model`**, so `-preview` is not an alias and is not silently served by `qwen3.8-max`. Both stay catalogued and both stay in `unpriced_models` — removing the preview entry is precisely what would drop an existing pin onto `default_rate`. Recorded in `docs/RECIPES.md`; the shipped premium Qwen model remains `qwen3.7-max` and the negative assertion in `test_suite.sh` keeps that contract self-documenting.
- **Process note for the next stacked release.** `ci.yml` triggers on `pull_request: branches: [main]` with the default activity types, and a base-branch change fires `edited`, which is not among them — so a PR stacked on another branch never gets a server-side check until it is retargeted *and* reopened. Separately, `gh pr merge --delete-branch` on a stack base **closes** the child PR rather than retargeting it, and GitHub will neither retarget a closed PR nor reopen one whose base is gone; recovery meant pushing the base ref back from a commit still reachable in `main`. Adding `edited` to the workflow's activity types would close the first hole.

### v3.3.0
- **Grok and Kimi compatibility is now established from capabilities, not inferred from an installed binary or its version string.** `grok_cli_supports_required_interface` validates the complete headless parser surface inside the same isolated HOME/CWD boundary used for dispatch, while `grok_cli_exposes_requested_model` verifies both usable authentication and the exact `GROK_MODEL` in the CLI inventory (`scripts/query_grok.sh:85-154`). Kimi similarly checks prompt mode, `stream-json`, the provider inventory, and `KIMI_MODEL` before any request (`scripts/query_kimi.sh:45-79`). Compatible future and vendor-custom versions therefore continue to work without a release-specific allowlist; an interface that cannot enforce the adapter's contract fails before a model call.
- **The observed CLI version is provenance only.** Successful Grok and Kimi responses add `metadata.cli_version` and `metadata.cli_compatibility="capability-probed"`; the shared schema documents both fields without making either required for other transports (`scripts/query_grok.sh:319-336`, `scripts/query_kimi.sh:126-138`, `scripts/lib/schema.json:169-180`). This deliberately does not introduce a blessed-version range: the version is useful for diagnosis, while compatibility is decided by the exercised command, output, and model surfaces.
- **Optional capabilities remain optional.** Grok's `--no-auto-update` is used when the help surface advertises it, but its absence does not reject an otherwise compatible CLI. The capability probe itself uses `--help` and model/provider inventory commands; it does not send a consultation. Grok preserves its existing narrowly classified API fallback when the required CLI route is unavailable, while Kimi continues to fail closed because it has no API transport.
- **Regression coverage pins the compatibility contract rather than today's binaries.** Grok's suite accepts an alternate compatible version, rejects an incomplete interface before dispatch, checks requested-model availability, and preserves the post-launch/API-fallback boundary (32 checks). Kimi's suite covers an alternate compatible version, an incomplete interface, and a missing requested model (8 checks). Full release gate: 17/17 suites; core suite: 299/299; ShellCheck green. Installed Grok 0.2.114 and Kimi 0.27.0 also passed no-model-call probes.

### v3.2.0
- **Claude premium upgraded from Opus 4.8 to Opus 5.** The default `CLAUDE_MODEL`, premium/max/best tier, standalone adapter fallback, configuration examples, model table, and cost catalog now resolve to the canonical `claude-opus-5` ID (`scripts/config.sh:274`, `scripts/query_claude.sh:29`). Pricing remains $5/$25 per million tokens; `claude-opus-4-8` stays in the legacy catalog for historical responses and intentional pins.
- **Opus 5 adaptive thinking is accounted for end to end.** Claude CLI mode now requests the JSON result envelope, extracts visible `.result`, aggregates provider usage across `modelUsage`, and persists the provider's exact `costUSD` (`scripts/query_claude.sh:72-126`, `scripts/lib/common.sh:904-980`, `scripts/lib/costs.sh:307-354`). This fixes the review finding that pricing only the visible answer undercounted hidden thinking and cache charges. The implementation deliberately prefers provider cost over reconstructing cache pricing from token totals.
- **Anthropic API responses no longer disappear when thinking precedes text.** `parse_anthropic_response` selects every `type=text` block rather than assuming `.content[0].text` (`scripts/lib/api.sh:325-343`). The API token budget is now the configurable `CLAUDE_API_MAX_TOKENS=16384`, shared by adaptive thinking and visible output; `stop_reason=max_tokens` fails closed instead of admitting a partial answer into synthesis (`scripts/lib/api_query.sh:94-153`).
- **Historical generated defaults migrate with provenance.** Before this release, `init` copied `CLAUDE_MODEL=claude-opus-4-8` without distinguishing a generated default from a user pin, so the new runtime fallback was unreachable for existing installations. `configure` now upgrades that exact historical value to Opus 5 and writes `# ai-consultants:default`; model values supplied by environment, interactive input, or `--set` receive `# ai-consultants:pin` and survive later rewrites (`scripts/configure.sh:183-291`, `scripts/configure.sh:357-420`). An intentionally retained 4.8 value should be reasserted once with `--set CLAUDE_MODEL=claude-opus-4-8`.
- **Verification expanded around all four review findings.** `test_query_claude.sh` exercises CLI JSON extraction, measured usage, provider cost persistence, and session billing. `test_api_transport.sh` covers thinking-before-text, the 16,384-token request default, invalid budgets, and truncation rejection; `test_configure.sh` covers managed migration and persistent pins. Full gate: 17/17 suites; core suite: 299/299; shellcheck green. The real maintainer config was not rewritten during implementation and remained on 4.8 until the release installation step.

### v3.1.1
- **`doctor` no longer aborts at the first missing enabled consultant.** The v3.1.0 tag workflow exposed the pre-existing `set -e` interaction when Grok became CLI-first and the clean Ubuntu runner had no `grok` binary: the main JSON diagnostic exited before emitting output. Missing CLI/API configuration checks now add their issue and continue to the final summary, which still exits unhealthy. `test_doctor.sh` stubs the complete CLI roster, rejects empty JSON output, and includes a dedicated missing-Grok regression. Full gate: 16/16 suites; shellcheck green.

### v3.1.0
- **Grok is now a first-class CLI/API-switchable consultant on `grok-4.5`.** `scripts/query_grok.sh` uses the official Grok Build headless interface, `scripts/config.sh` resolves CLI-first while preserving API-only installations, and configure/doctor/update-clis/docs all classify Grok with the other switchable consultants. `XAI_API_KEY` is accepted as the official alias for the historical `GROK_API_KEY` contract. The route that actually answered is recorded as `metadata.transport = cli|api|api_fallback`.
- **The CLI boundary is enforced rather than documented.** The first implementation disabled plan/subagents/memory/web but still inherited the current checkout and the maintainer's Grok environment; `grok inspect --json` showed 1 MCP server, 13 plugins, 11 hooks, 79 skills, and 3 project instructions available to an advisory consultation. Each run now creates an ephemeral mode-700 HOME/CWD, copies only a regular non-symlink `auth.json` at mode 600, supplies `--tools ""`, `--permission-mode dontAsk`, `--sandbox strict`, and explicit deny rules, then validates the temp-prefix before recursive cleanup. A live Grok 4.5 smoke succeeded through this isolated path.
- **Prompts use `--prompt-file`, not argv.** `FULL_QUERY` includes context files; passing it through `-p "$FULL_QUERY"` exposed the prompt in the process list and failed before Grok launched once the combined context crossed `ARG_MAX`. Reproduced locally with a 1.1 MiB context (`timeout: Argument list too long`). The private prompt file removes both failure modes, and the regression suite drives the same 1.1 MiB case.
- **API fallback means transport unavailable, not "any non-zero".** The initial adapter sent every CLI error to the API when a key existed — including timeouts, model errors, empty responses, and a generic tested `exit 42` — which hid CLI regressions and could add an unexpected API charge. `grok_cli_is_unavailable` now recognizes missing/unexecutable binaries and authentication/startup failures only; post-launch failures remain `transport=cli` and surface as errors. Missing-command and authentication fallback paths are tested separately.
- **Regression and release coverage.** New `test_query_grok.sh` carries 26 assertions across headless flags/model pinning, HOME/CWD/auth isolation, tool denial, transport metadata, API fallback boundaries, and large contexts. The master runner discovers it automatically, taking `npm test` to 16 suites; the v3.1.0 release gate and shellcheck pass. README, SKILL, setup/recipes/configuration references, changelog, and the showcase roster are synchronized.

### v3.0.0
- **Phase 2 of the panel-premise investigation: the deliberation/consensus machinery is removed and the default is now the coverage union.** Two independent reviews (Codex `gpt-5.6-sol`, then Fable-5) had judged the panel premise weak — voting/consensus measure *agreement*, not correctness, and no held-out comparison against one strong model existed. A held-out A/W/C coverage experiment (maintainer instrumentation on the `experiment/coverage-metric` branch, not shipped) resolved it: **task-dependent.** On convergent single-answer defect-finding a single strong model (Gemini 3.1 Pro) caught **19/19** hard bugs (repo-internal + external CVEs/concurrency incl. the Rust `Arc` bug found by RustBelt formal verification) → the panel has no coverage headroom. On **breadth/enumeration** (deep 60-point rubrics across JWT/webhook/cache), rubric coverage was **A 51% · self-consistency C 70% · panel W 93%, W>C>A on every item** → diversity, not volume, and specifically the raw **union** (deliberation OFF). The durable value is the diverse fan-out + union; the averaging machinery adds nothing measurable. Directional (n=3 breadth, hand-validated; n=19 defect).
- **Removed end to end** (six test-green commits on `refactor/remove-deliberation`, merged via PR #6): `lib/voting.sh` (voted recommendation, 1-10 score, consensus score, dissenters), `lib/orchestration.sh` (shape planner + convergence loop) + `debate_round.sh`, `lib/stance.sh`, `peer_review.sh`, and the capability-calibration cluster `roster_audit.sh`/`roster_calibrate.sh`/`run_calibration.sh`/`taste_elo.sh` + `references/calibration_benchmark.json` + the `capabilities`/`category_axis` blocks in `affinity.json`. Panic functions cut from `common.sh`; capability functions (`get_capability`/`get_category_axis`) and the `ENABLE_CAPABILITY_ROUTING` branch cut from `routing.sh`. Config: `ENABLE_DEBATE`/`DEBATE_ROUNDS`, `ORCHESTRATION_MODE`/`CONVERGENCE_*`, `ENABLE_DEBATE_OPTIMIZATION`/`DEBATE_*`, `ENABLE_PANIC_MODE`/`PANIC_*`, `ENABLE_ADVERSARIAL_VERIFY`, `ENABLE_PEER_REVIEW`/`PEER_REVIEW_MIN`, `ENABLE_CAPABILITY_*`/`CAPABILITY_*`, `ENABLE_STANCE_CONSENSUS`/`STANCE_*`. Presets no longer force debate/peer-review. The `debate` and `roster-audit` slash commands are gone (installers prune them). 19 files deleted, ~-4600 lines. 6 test suites removed.
- **`consult_all.sh` pipeline** is now classify → route → parallel fan-out → synthesis (the orchestration-planning, panic, Round-2+ deliberation, and voting/consensus blocks are gone; `optimization_metrics.json` and the report no longer emit consensus/orchestration/capability fields). `QUERY_COMPLEXITY` is retained (from `costs.sh`) for opt-in cost-aware routing. `KNOWN_FEATURE_FLAGS` synced (27 → 19).
- **`synthesize.sh`** default strategy is now `coverage` (alias `union`): the deduplicated union of every distinct point, crediting what only one model raised; it does NOT collapse to a winner. `majority` reworded (no consensus language); `compare_only`/`risk_averse`/`security_first`/`cost_capped` kept. `DEFAULT_STRATEGY` default `majority` → `coverage`. `doctor --suggest-preset` now recommends `coverage`.
- **`npm test` = 15 suites** (was 22), hermetic (`AI_CONSULTANTS_CONFIG_DIR=<empty>`). **Release-gate note (recurring):** `scripts/release.sh` runs `npm test` against the maintainer's real `~/.config` (it sources `config.sh` → `load_user_config` first), and under the real config `test_context_optimization.sh` hung the gate; the tree is green under the documented hermetic condition (`AI_CONSULTANTS_CONFIG_DIR=<empty> npm test </dev/null`). Same hermeticity class as the v2.23.0/v2.25.1 gate incidents.
- **Docs swept** (README/SKILL/CLAUDE.md/consult.md/references/docs) to the coverage-first framing; the reference-doc sweep was delegated and verified. CLAUDE.md's historical `## v2.X Features` sections are left as archive — this entry recontextualizes them. **Not statistically binding**: breadth is n=3; a binding claim needs n≥15–20 deep-rubric items (the harness exists on `experiment/coverage-metric`).

### v2.25.2
- **`doctor`'s static ✓ was a false all-clear.** `check_cli_consultant` verifies `command -v` (installed) or key-present; neither confirms the consultant answers. So `✓ All systems healthy!` and JSON `status: "healthy"` were printed on installation state alone — a consultant with an expired key or spent quota passes every static check and fails the first real query. Surfaced concretely: static doctor called all 11 healthy; `doctor --live` showed 8/11 (Cursor usage limit, Qwen3 401, Grok bad xAI key returned as HTTP 400). Fix gates the word "healthy" on `LIVE_MODE`: a static-only clean run now prints "Static checks passed: CLIs installed, keys present … verify with doctor --live", and JSON reports `status: "static_ok"` + a new `verified: live|static` field; "healthy" now implies a live check backed it. `test_doctor.sh` asserts the invariant directly (a static-only run must never report `healthy`) rather than pinning a specific status, so it holds regardless of the test host's own consultant health. v2.18.0 added `--live` but left the default over-claiming; this closes that. `npm test` = 22 suites.
- **Note (not shipped, local only)**: the 3 down consultants were all credentials, not code — Grok/Qwen3 keys refreshed and Cursor disabled in the maintainer's own `~/.config`, taking the local live panel to 10/10 (Cursor off by choice). Qwen locally points at Token Plan `qwen3.8-max-preview`; the shipped default stays `qwen3.7-max`.

### v2.25.1
- **Six defects found by an independent review of the released v2.25.0** (Codex `gpt-5.6-sol`, effort high, read-only via the `sol-reviewer` profile). Five were reproduced locally by executing the shipped functions before accepting them; the bash 3.2 crash was verified by the reviewer against `/bin/bash 3.2.57`.
- **`calculate_final_score` scored every consultant as a dissenter.** `calculate_weighted_recommendation` emitted `_map_sanitize_key`'s output (spaces and punctuation stripped) as the winner, while `calculate_final_score` compared the raw `.response.approach` against it. Unanimous multiword panels scored 2/10, single-word panels 10/10. Voting now keeps original strings in bash 3.2-compatible indexed arrays. **The shipped voting tests used single-word approaches, which is exactly why this survived every prior review** — when writing a voting fixture, use a realistic multiword approach.
- **v2.25.0's recursive billing scan was too wide.** It was made recursive to reach `round_N/`, which also captured `peer_review/` — anonymized copies carry the original `tokens_used`, bookkeeping files take the `// 1000` fallback. Now scoped to root + `round_<digits>/`. `_is_consultant_response_file` became a shape check (object, non-empty `.consultant`, object `.response`) instead of a six-name denylist; it gates 16 call sites across voting/synthesis/peer-review/costs, and was verified to still accept error responses, which must stay counted for quorum.
- **`select_consultants` returned disabled and self-excluded consultants.** Eligibility lived only in `consult_all.sh`'s non-smart branch. Now applied in `routing.sh` where the output is consumed, with `declare -f` guards; custom-agent discovery was extracted to `_list_custom_api_agents` (common.sh) and is shared with `_discover_custom_api_agents`.
- **Consensus used connected components**, so similarity being non-transitive produced 100% for `alpha beta`/`beta gamma`/`gamma delta`. Replaced with an exact maximum-clique search with branch-and-bound. Worst case is exponential; measured 1s at 11 consultants and 4s at 30, and the panel is bounded by the roster.
- **The CI caught a test that passed locally for the wrong reason — read this before trusting a local green.** `test_suite.sh` asserted DeepSeek is selected for QUICK_SYNTAX without enabling it; `ENABLE_DEEPSEEK` defaults to false, so once routing honoured `ENABLE_*` the assertion only held for someone whose user config enables it. It passed on the maintainer's machine and failed on Ubuntu. **`npm test` from your own shell is not a clean signal**: `load_user_config` exports `~/.config/ai-consultants/.env` into every suite. Verify with `AI_CONSULTANTS_CONFIG_DIR=<empty dir> npm test`, which is the condition CI runs in. Note `env -i` is NOT the right harness — it strips variables the tests legitimately need and makes `test_context_optimization.sh` fail on unmodified HEAD.
- **`estimate_tokens ""` read stdin, and it hung the release gate for hours.** The function chose stdin-vs-argument by emptiness (`[[ -z "$text" ]]`), so an explicit empty string was indistinguishable from no argument and fell through to `cat`. `test_suite.sh` calls `estimate_tokens ""`; with stdin closed (CI, background tasks piping from /dev/null) `cat` gets EOF and returns 0 — the "empty string: 0 tokens" assertion passes by accident. With stdin an open pipe (an interactive-ish local gate run) it blocks forever. `release.sh`'s gate wedged for ~10h on exactly this, and an earlier run had left a second orphaned tree hung ~16h. Fixed at the source: decide by `$#`, not emptiness — `estimate_tokens ""` now returns 0 without touching stdin, and only a genuinely arg-less call reads it. Regression test drives it under a never-closing fifo with `run_with_timeout`, so a re-hang is a FAIL not an infinite run. **This is a third hermeticity axis after `LOG_LEVEL` and the user config: a suite can pass or hang depending on whether stdin is closed.** The lesson compounds — several "22/22 passed" reports earlier in this work were real only because their stdin happened to be closed; the gate was never as green as it read.
- **Deliberately unchanged**: peer review still runs after voting and synthesis, so its scores cannot alter the recommendation the docs call a "refutation gate". That is an architectural decision, not a bug fix. The same review also judged the panel premise itself weak — it measures agreement rather than correctness, and no held-out comparison against a single strong model exists in the repo.

### v2.25.0
- **`tokens_used` was hardcoded to 0, so the cost report priced pipeline metadata and nothing else.** `build_response_metadata` (`lib/common.sh`) emitted a literal `tokens_used: 0` and was the only writer of that field in the repo; `calculate_session_cost` (`lib/costs.sh:248`) multiplies it by the model rate. Structurally zero for all 11 consultants, both transports, since the field existed. **Do not describe this as "every session reported $0.00"** — I did, in three changelogs, and it is wrong: the pipeline files caught by the same unfiltered glob have no `tokens_used` key at all, so they took the `// 1000` fallback at the default rate (~$0.009 each). Measured against HEAD on a realistic three-consultant directory, v2.24 reports **$0.0270**, entirely phantom. The reported number tracked how many pipeline stages ran. Worse in API mode: `run_api_mode_query` (`lib/api_query.sh:154`) called `extract_token_usage`, which computes the right number (verified: extracts 85 from a live Token Plan body), `log_debug`'d it, and dropped it on the floor.
- **Four paths write response metadata**, which is why the fix is spread out: `process_consultant_response` (codex/cursor/gemini/kimi/minimax/qwen3), direct builder calls (claude/mistral), `run_api_consultant` (glm/deepseek/grok), and the error path. All converge on `build_response_metadata`, so that is where the new `tokens`/`tokens_source` parameters land; the builders thread them through and default to `0`/`unknown` so pre-existing 5-arg calls still work.
- **The measured split travels in a module variable, `_API_TOKEN_SPLIT`.** `run_api_mode_query` publishes it via `set_api_token_split`; `resolve_response_tokens` reads it; the CALLER clears it via `clear_api_token_split`. The asymmetry is the whole subtlety: `resolve_response_tokens` runs inside `$(...)`, so it can *read* the variable (subshells inherit) but an assignment there dies with the subshell — it cannot clear it. Its callers are plain statements and can. **An earlier cut used a temp-file sidecar justified by the claim that some callers of `run_api_mode_query` run in a command substitution. That claim was false** — all six call sites (`query_{claude,codex,gemini,mistral,qwen3}.sh` and `run_api_consultant`) are plain statements — and it bought a cross-function file lifecycle with five cleanup sites for nothing. Caught by `/code-review max`, which also noted the false comment had been promoted to binding policy in this very file. The real subshell constraint applies to `calculate_session_cost` / `format_cost_caveats`, which ARE always `$(...)` and therefore take their input as arguments.
- **The provider's prompt/completion SPLIT is kept, not just the total.** `extract_token_split` (api.sh) preserves it and `metadata.tokens_input`/`tokens_output` record it; `calculate_session_cost` prefers them over the 60/40 fallback. Collapsing to a total and re-splitting it 60/40 overstated API-mode cost ~2.7x on a realistic 20000-prompt/500-completion gpt-5.5 call ($0.3075 vs $0.1150) — while still labeling it `measured`, the one value that is supposed to mean "the provider's own figure". The 60/40 guess survives only for CLI mode, which has no split to record.
- **CLI mode estimates, and says so.** No CLI exposes token counts, and CLI-first is mandatory since v2.21, so leaving CLI at 0 would have fixed almost nothing. `resolve_response_tokens` falls back to `estimate_tokens` (the pre-existing 4-chars-per-token helper at `common.sh:864`, previously unused for this) over prompt + reply. The six `process_consultant_response` call sites pass `"${#FULL_QUERY}"`; claude/mistral/`run_api_consultant` resolve inline. `metadata.tokens_source` records `measured|estimated|unknown` so a consumer can tell the difference — added to both `lib/schema.json` and `docs/JSON_SCHEMA.md`.
- **Budget impact is narrower than it first looks.** `enforce_budget` is called four times in `consult_all.sh`. The first two (`:345`, `:451`) pass literal `0` as current cost and project from `ESTIMATED_COST`, derived from `CONTEXT_SIZE` — those always worked. Only `:755` and `:862` pass `CURRENT_COST` from `calculate_session_cost`, and that was the zero. Do not describe this as "budget enforcement never triggered"; it did, off estimates.
- **Five `*_FORMAT` flags were dead since v2.6.** `QWEN3_`/`GLM_`/`GROK_`/`DEEPSEEK_`/`MINIMAX_FORMAT` are declared in `config.sh:174,185,195,205,216` and documented in `.env.example:178-182`, but `get_api_format()` (`common.sh:396`) hardcoded the mapping and `api_query.sh:50` is its only consumer — ten occurrences, all definitions, zero reads. Same shape as the `ENABLE_REFLECTION` removal in v2.23.0, but the opposite call: that capability was covered elsewhere, this one is not — there is no other route to a non-DashScope Qwen endpoint, so deleting the flag would have forced a Qwen-only hardcode. Now honored, with validation against `google_ai|anthropic|qwen|openai` and fallback-with-warning on anything else.
- **Note the deliberate asymmetry**: a bad `*_FORMAT` falls back to the default; a bad `*_REASONING_EFFORT` fails. Format has a correct per-consultant default to fall back to. Effort does not — silently substituting the model's default when the user asked for `xhigh` *is* the silent-no-op bug the knob exists to avoid.
- **The effort enum came from the API, not the docs, and the docs were wrong.** Every public write-up on `qwen3.8-max-preview` reported `low|high|xhigh`. A live 400 reports `'reasoning_effort' must be one of: 'none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'`. The first implementation allowed five values and would have rejected `minimal`, which the model accepts. The per-model subset is intentionally NOT enforced in shell — `none` is rejected by this model at a second layer (`The value of the enable_thinking parameter is restricted to True`), which is a model fact that belongs to the provider, not to us. Hardcoding per-model facts is what produced the stale `MODEL_USED` bugs also fixed here.
- **Qwen 3.8 is opt-in and stays that way.** It needs a Token Plan subscription (`https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1`, its own key, credit billing) and is not on DashScope, so swapping `config.sh:668` would break the Qwen consultant for every user without one. `test_suite.sh` now asserts the negative explicitly (`premium != qwen3.8-max-preview`) so the contract is self-documenting. `references/affinity.json` is untouched **by construction**: affinity and capability axes are keyed on the consultant name, never the model id, so changing `QWEN3_MODEL` implies no affinity change and `test_routing_parity.sh` needed no edit.
- **No invented cost rate.** `qwen3.8-max-preview` is credit-billed with no published per-token price, so it is catalogued at 0/0 plus a new `unpriced_models` list. Doing nothing was NOT neutral: `calculate_session_cost` never calls `resolve_model_for_cost`, so an absent model falls to the `*)` arm of `get_input_cost_per_1k` (`costs.sh:163`) at 0.005/0.015 — ~4x the real qwen3.7-max input rate, reported confidently. The same fallback sits in `roster_calibrate.sh:34-35`, so a measured cost axis would have been corrupted by a model that is not billed per token at all. Run `run_calibration.sh` only after this entry exists.
- **Verified live against Token Plan**, not just fixtures: endpoint 200, structured response through the real adapter (confidence 9/10, persona applied — not the confidence-5 stub the Gemini fence bug produced in v2.15.0), `622 measured` tokens in API mode, `311 estimated` in CLI mode via Codex, and a full `consult_all.sh` run reporting `$0.0550 [token counts estimated for 3 of 3]` where it would previously have said `0¢`.
- **Making `tokens_used` real turned five latent mis-billings into money, all found by `/code-review max` (55 agents, 52 verified findings → 15 defects).** Every one was harmless while the field was hardcoded 0. `calculate_session_cost` globbed `*.json` unfiltered, so `voting.json`/`orchestration.json`/`synthesis.json` were each priced at the `// 1000` fallback times the default rate (measured 2.8x inflation); a cache hit replayed the original response's metadata and billed at full price ($0.52 for zero API calls); debate rounds live in `round_N/` subdirectories the non-recursive glob never saw; and `<consultant>_escalated.json` was left beside the file it had just been copied over, counting one query twice. New `_billable_response_files` is the single gate for all four — **any future loop over the responses dir should use it**, not a bare glob. `format_cost_caveats` additionally counted `unknown` (i.e. failed) responses in the "estimated for N of M" denominator, which made failures read as provider-measured.
- **The reasoning-effort knob was a silent no-op on the exact path the docs steer users to.** `${AGENT}_REASONING_EFFORT` was resolved inside the `openai` arm of the format switch, but `config.sh:174` defaults `QWEN3_FORMAT=qwen` — so `QWEN3_USE_API=true` + `QWEN3_REASONING_EFFORT=high` against DashScope built a body with no effort field, emitted no warning, and never validated the value. Resolution is now hoisted **above** the switch so every wire format sees it, and the three formats that cannot carry it (`qwen`, `google_ai`, `anthropic`) say so. Related: `validate_reasoning_effort`'s `return 1` propagated to a bare `run_api_mode_query` call under `set -euo pipefail`, killing the query script before `exit_code=$?` — no error envelope, a 0-byte output file, and the consultant silently vanishing from the panel instead of being reported as failed. All five API-capable query scripts now use the explicit `if ! …; then` guard `run_query` already used; this also fixes the same pre-existing hole for network failures.
- **Cataloguing the credit-billed model at 0/0 fixed the estimator and broke the calibrator.** `roster_calibrate.sh` falls back to `FALLBACK_IN/OUT` only when the catalog lookup *misses*; with a 0/0 entry the lookup now succeeds, so every qwen3.8-max-preview response cost 0.00000000 and the "cheapest = 10" rank-normalizer scored Qwen3 the cheapest consultant on the panel — which `--write` would have persisted into `affinity.json`, preferentially routing work to a model whose real consumption is invisible there. It now sources `costs.sh` and skips unpriced models from the cost axis: no data beats wrong data.
- **Applying the review fixes introduced four more bugs, every one caught by a test rather than by reading.** Worth recording because they are all the same shape — a change that *looks* applied but is not: (1) the `roster_calibrate.sh` guard called `is_unpriced_model` in a script that did not source `costs.sh`, so it would have been a silent no-op; (2) the shared CLI effort warning fired in API mode too, where the setting is honored, until it was made self-guarding; (3) `warn_effort_ignored_in_cli` used `is_api_mode "$agent" && return 0` — an &&-list that short-circuits returns non-zero as a statement and kills the caller under `set -e`, **the exact v2.22.0 `prompt_value` bug re-created**; and (4) `build_fallback_response` used `$tokens_in` without declaring the local, so under `set -u` **every unstructured reply would have aborted for all 11 consultants**. Only (4) was caught by an existing test — `test_query_kimi.sh`, whose stub happens to return an unstructured envelope — which means the fallback path had no direct coverage at all. `test_response_tokens` now exercises all three builders at both the legacy and the full arity under `set -u`, plus the `set -e` safety of the warning helper.
- **`${#FULL_QUERY}` is characters, `estimate_tokens` counts bytes.** The estimate summed the two, under-counting multibyte input roughly threefold (`日本語のテスト` is 7 chars, 21 bytes). `resolve_response_tokens` now takes the prompt TEXT and runs both halves through `estimate_tokens`, so the units match.
- **Three suites were not hermetic against `LOG_LEVEL`, and it read as a flake.** One `npm test` run reported `test_api_transport.sh` failed and could not be reproduced. It is not flaky: `_log` filters by `LOG_LEVEL`, so `log_warn` is silent at `ERROR`, and any assertion matching on warning output fails for that environment and passes everywhere else — deterministic per-environment, exactly the v2.23.0 release-gate shape. Found by sweeping every suite at DEBUG/INFO/WARN/ERROR rather than by re-running and hoping. Two were pre-existing: `test_context_optimization.sh` (3 assertions, **two of them the v2.17.2 secret-exfiltration guards** — a security regression test that depended on the caller's verbosity) and `test_update_clis.sh` (2 assertions). All three now `export LOG_LEVEL=INFO` before sourcing `config.sh`, which honors an ambient value. **If you add an assertion that matches on log output, pin the level in the suite.** Note the earlier "all assertions passed" reading was wrong — it came from a `grep -A40` truncated before the summary line, not from the actual output.
- **`npm test` = 22 suites.** New `test_api_transport.sh` (41 assertions) exists mainly as a back-compat gate: `get_api_format` for all 11 consultants and a byte-identical assertion on `build_openai_request`, which six consultants share. That assertion was written and confirmed green against the *unmodified* function before either was touched — the md5 of the default body is unchanged end to end.

### v2.24.0
- **Stale slash commands are finally removed on install/update, on both entry points.** `scripts/install.sh` and `bin/ai-consultants install` only ever `cp`'d `.claude/commands/*.md` into `~/.claude/commands`, never pruning — so a command deleted from the repo lived on the user's machine indefinitely. Measured on a real installation: **14 `ai-consultants:*.md` installed, 4 of which this repo still ships.** The other ten were dropped by `9ae73e3` (v2.10.0, "command consolidation") and had been dead for 13 releases. Not inert — they stay invocable, and `config-features.md` tells the user to set `ENABLE_REFLECTION`, removed in v2.23.0 and now rejected by `configure --set` with a non-zero exit. New `prune_removed_commands()` in `install.sh` plus an inline equivalent in `bin/ai-consultants`; both scoped to the `ai-consultants:*.md` namespace. **Known and accepted bound**: within that namespace it also removes a user-authored command, because nothing distinguishes it from one of ours deleted upstream. Documented in `CHANGELOG.md` rather than worked around.
- **`bin/ai-consultants install` was initially missed** — the first cut of the fix landed only in `install.sh`, leaving npx users (the path the shipped `bin` help and `docs/releases/v2.19.2.md` advertise) with exactly the stale commands the fix targeted. Caught by the `/code-review max` pass, not by tests.
- **`install.sh` gained a define-only source guard, and its first placement was destructive.** The guard (`AI_CONSULTANTS_INSTALL_DEFINE_ONLY`) exists so `test_install.sh` can source the file for its helpers without the installer running. It was placed at line 240 — *below* the argument parser (line 145) and *below* the uninstall block (line 179). A sourced script inherits the **caller's** positional parameters, so `bash scripts/test_install.sh --uninstall` had `install.sh` parse the suite's own flag and execute the real uninstall: `rm -rf` on the installed skill plus every `~/.claude/commands/ai-consultants*.md`. Reproduced in a sandboxed `HOME` with a canary file before fixing, and re-verified after. The guard and `prune_removed_commands()` now sit above the parser, with the invariant stated inline: nothing above the guard may act, and anything below it must be safe to skip. Milder instance of the same root cause: `./scripts/test_install.sh --verbose` exited 1 printing `install.sh`'s help.
- **`scripts/release.sh` dropped from the npm tarball** (`package.json` `files` gains `!scripts/release.sh`). It is the last maintainer-only artifact in the package now that `.claude/skills/` is gone; `bin/ai-consultants` does not route it, so `npx ai-consultants release` reaches the consult fallback, not the script. **Deliberately kept** after checking their consumers: `roster_audit.sh` (backs the shipped `roster-audit` slash command and `doctor --roster-audit`), `roster_calibrate.sh` / `taste_elo.sh` / `run_calibration.sh` (documented for users in the shipped `docs/SMART_ROUTING.md`), `install-hooks.sh` (wired to a `package.json` script, and `CHANGELOG.md` records it as safe for tarball consumption).
- **Release tooling moved out of the repository entirely** (`6a4e70d`, `95d2e29`, `384a1cd`). `scripts/install.sh` git-clones the whole repo into `~/.claude/skills/ai-consultants`, making `.claude/skills/` a distribution channel — confirmed on a real install, which carried both `release` and `verify`. Shipping tooling that pushes tags and deploys the showcase site to end users is both useless and dangerous, so the release and verify skills now live in the maintainer's `~/.claude/skills/`, and this repo has **no** `.claude/skills/` directory. Keep it that way; if a future skill must be in-tree, prune it in `install.sh` after the clone. New `.github/workflows/publish.yml` publishes to npm via **Trusted Publishing (OIDC, no `NPM_TOKEN`)** on a `v*` tag push and creates the GitHub release from `docs/releases/v<VERSION>.md`; it re-runs the full gate because `ci.yml` only covers branches and PRs, and verifies all nine version surfaces server-side (checking only `package.json` would let a hand-bumped tag publish a package whose `config.sh`/`SKILL.md`/`README.md` still report the previous version — unfixable, since npm forbids republishing).
- **Two test-harness weaknesses fixed, both found by breaking them on purpose.** `lib/test_helpers.sh::test_summary` now fails a suite that ran zero assertions: `run_test` takes `"<name>" <function>`, and given the function alone the name absorbs it, `shift` empties the list, `"$@"` expands to nothing — no test runs and the suite exits 0. That is the v2.23.0 `test_functions.sh` fix one level up (that one scored a test by its last assertion; this one scored a suite that had none). The guard only covers suites calling `test_summary`, so `test_routing_parity.sh` got the same check inline, and `test_set_e_safety.sh` — a static lint with no assertion counter — now asserts its scan corpus is non-empty, since a lint that scans nothing reports "zero occurrences" and passes forever.
- **`test_configure.sh`'s parameter-contract scan correctly flagged the new test hook.** It walks every `${VAR:-}` in non-test scripts and demands each be a declared config parameter; `AI_CONSULTANTS_INSTALL_DEFINE_ONLY` joins the existing internal hooks (`SKIP_GATE`, `ROOT`, `FORCE`) in its exclusion list. The test did its job on a change it had no knowledge of.
- **New `scripts/test_install.sh`** (12 assertions, `npm test` now 21 suites): prunes what upstream dropped, keeps what it ships, ignores other tools' commands and lookalike filenames, no-ops on a fresh install (glob matches nothing) and on an absent commands directory, and asserts the define-only guard is still present so a refactor that drops it fails loudly rather than making `npm test` clone.
- **Cosmetic, found while sweeping for dead code**: three SKIP lines in `test_context_optimization.sh` used plain `echo` with `${C_YELLOW}`, which under bash prints a literal `\033[33m` instead of colour (zsh's builtin `echo` interprets escapes, which masked it during the first check). Now `echo -e`. `C_YELLOW` itself is **not** dead despite shellcheck SC2034 — that suite sources `test_helpers.sh` and uses it; shellcheck analyses one file at a time, which is why its message says "or export if used externally".

### v2.23.0
- **`lib/reflection.sh` deleted (452 lines) and `ENABLE_REFLECTION` / `REFLECTION_CYCLES` removed.** The module was never sourced — `grep -rn 'reflection\.sh' scripts/` matched only the file's own header — so the generate-critique-refine loop had never executed. The two flags were declared at `config.sh` and exported by four presets (`high-stakes`, `max_quality`, `fast`, `_disable_all_consultants`) with no consumer anywhere. It had also aged past the codebase: `_exec_consultant` hardcoded a Gemini/Codex/Mistral case (3 of 11 consultants; the rest hit `*) return 1`) and its Gemini branch still used the pre-`agy` CLI syntax retired in v2.15.0 — so wiring it up as written would have produced partial reflection with a broken Gemini path. Removed rather than implemented because v2.16's `converge`/`adversarial` shapes already run critique-refine off measured consensus rather than a fixed cycle count. Worth noting for archaeology: the module was *maintained* in at least three review passes (v2.15.1 de-fencing, the `--model` parity change, the v2.10.9 `((var++))` sweep) — effort spent on code that never ran.
- **The release gate had been red since 2.22.0 and nobody could see it.** `scripts/release.sh:26` sources `lib/common.sh` → `config.sh` → `load_user_config` *before* `npm test` at line 194, so `_apply_env_file`'s `export "$key=$value"` put the maintainer's entire `~/.config/ai-consultants/.env` into the test environment. Two suites were not hermetic against that, and the first aborted the run before the second could surface:
  - **`test_user_config.sh`** — `test_xdg_fallback` (Test 6) unsets `AI_CONSULTANTS_CONFIG_DIR` in the suite's own shell (required, to assert the XDG branch) and `_reset_state` never restored it, so the unset leaked into all 22 later tests, which then read the *real* user config. Fixed by re-exporting an empty temp dir in `_reset_state`, making isolation order-independent; tests that legitimately override the dir per-invocation are unaffected. Plus the whole `*_USE_API` set unset at suite top (the guard `test_configure.sh` already carried), and `ENABLE_KIMI`/`GROK_API_URL` (Test 27) and `GEMINI_MODEL`/`PANIC_KEYWORDS` (Test 28) unset inside their subshells — those assert on *real* var names and `load_user_config` lets the environment win over the file by design, so with `config.sh` pre-sourced the file under test was never consulted.
  - **`test_configure.sh`** — `run_clean_configure` scrubbed the nine API keys but no settings, while `configure`'s `has_explicit_value` reads any ambient value as a deliberate pin. An exported `DEFAULT_PRESET` therefore overrode the value Tests 6 and 10 had just written into the file they assert on. The scrub set is now derived from `configure --show-parameters` — configure's own contract — with a hard failure if that list comes back empty, so the scrub can never silently degrade to a no-op. **Verified pre-existing**: the same two assertions fail at `2ac18d4` (the v2.22.0 release commit) under the same conditions, via a throwaway worktree.
- **`test_functions.sh` was under-reporting failures across the whole file.** `run_tests` scores a test by its exit status — that of its LAST command — so a failing assertion followed by a passing one printed `FAIL`, incremented nothing, and let the suite exit 0. 8 of the 13 test functions run more than one assertion and none accumulated a result. The five `assert_*` helpers now set `_ASSERT_FAILED` on every failure path and `run_tests` fails a test when either that flag or the exit status is non-zero. **Caught by trying to make the new pin fail**: the first version of `test_known_feature_flags_in_sync` printed `FAIL: ... Actual: PHANTOM_FEATURE` while the suite still reported "All tests passed". Verified against a pre-existing test by breaking the first of three assertions in `test_case_conversion` — previously green, now correctly red.
- **`KNOWN_FEATURE_FLAGS` synced (9 → 27) and pinned.** `_discover_custom_api_agents` (`consult_all.sh:48`) walks `env` and enrolls any `ENABLE_X=true` as a consultant when `X_API_URL` is set and `X` is absent from the registry, so the 18 missing flags — `PEER_REVIEW`, `HEALTH_GATE`, `SEMANTIC_CACHE`, … — were latent phantom consultants. Measured drift: 18 missing, 0 stale. `test_known_feature_flags_in_sync` derives the expected set from `config.sh`'s top-level `ENABLE_*` declarations minus `KNOWN_CLI_AGENTS`/`KNOWN_API_AGENTS` and asserts both directions, so the next added flag fails the build. (Checked that the `^ENABLE_` grep misses nothing declared only inside functions — the sole extra hit was `AGENTNAME`, from the convention's doc comment.)
- **Preset tables were documenting a mode that stopped being the default in v2.16.** Found by the `/code-review max` pass on the removal branch: renaming the README column from "Reflection" to "Peer Review" turned a code-enforced "No" into a false claim, because `_disable_all_consultants` exported `ENABLE_REFLECTION=false` — the line this release deletes — whereas nothing ever clears `ENABLE_PEER_REVIEW`. Combined with `consult_all.sh:285` force-enabling peer review for the adversarial shape, `--preset medium` on a SECURITY question runs a full billed peer-review round the table promised it would not. The Debate column was stale for the same reason (`DEBATE_ROUNDS` is only read under `ORCHESTRATION_MODE=fixed`). Both tables now state only what the preset pins, with the planner's role called out. Also corrected `max_quality`'s "All consultants" → 8 of 11, which contradicted the README table in the same commit.

### v2.22.0
- Added the public `ai-consultants configure` / `config` subcommand. Automatic mode detects every current consultant, uses CLI-first transport selection with API fallback when credentials are available, persists to the XDG user config directory, preserves custom values and secrets while refreshing availability-derived `ENABLE_*` flags, backs up rewrites, and keeps files at mode 600.
- Replaced the stale 1,092-line v2.0 configurator with a compact implementation whose accepted parameter surface is derived from `.env.example`. `--set KEY=VALUE` supports repeatable automation and participates in detection, `--interactive` reviews credentials/transports/panel selection, `--advanced` reviews every parameter, `--show-parameters` exposes the contract, and side-effect-free `--dry-run` redacts secrets.
- Added `test_configure.sh` (60 assertions) covering public routing, runtime-wide template parity, full-roster detection, API fallback, override precedence/empty values, transport provenance, advanced defaults, export preservation, dry-run isolation/redaction, symlink refusal, backups/permissions, and rejection of removed settings. The npm-pack smoke path now includes `.env.example` and exercises both `init` and `configure` from the installed tarball.
- **Three interactive/provenance bugs fixed before the subcommand ever shipped** (found in review; none reached a tagged release):
  - **`--interactive` aborted on the first Enter.** `prompt_value` ended both branches with `[[ -n "$reply" ]] && set_value …`. An empty reply — the documented "press Enter to keep" path — made that `&&` list return 1, and since it was the function's last command, `prompt_value` returned 1; under `set -e` the plain call site in the `for` loop killed the script (exit 1, nothing written). Now an explicit `if` block, so an empty reply returns 0.
  - **`--advanced` silently wrote garbage.** `done < <(list_parameters)` redirects fd 0 for the *whole loop body*, so `prompt_value`'s nested `read -r -p` consumed the parameter list instead of the terminal — each prompt took the *next parameter's name* as the user's answer (observed: `AFFINITY_FILE` ← `ANTHROPIC_API_KEY`). Fixed with the standard fd-separation idiom (`read … <&3` / `done 3< <(…)`).
  - **`init` then `configure` disabled four consultants.** `init` copies `.env.example` verbatim, and the template carried uncommented `CODEX_USE_API=false` / `CLAUDE_USE_API` / `MISTRAL_USE_API` / `QWEN3_USE_API`. `has_explicit_value` reads any unmarked value as a user pin, so `configure_switchable` took the explicit-`false` branch: with no CLI and a valid API key the consultant was disabled outright instead of resolving to API. Fixed in the *template*, not the provenance logic — the four switches are now commented out, matching the convention `GEMINI_USE_API`/`MINIMAX_USE_API` already established (commented = auto-resolve, uncommented = force). CLI-first is unaffected: an installed CLI still wins over a present key.
- **Backup names are now collision-proof.** `${OUTPUT_FILE}.backup.$(date +%Y%m%d_%H%M%S)` is only second-precise, so two runs inside one second resolved to the same path and the second `cp` clobbered the first backup — losing the user's *original* config while keeping a machine-generated one. Now `mktemp "…backup.<ts>.XXXXXX"`, which claims the name atomically (also making concurrent runs safe). Note the collision is unreachable from a sequential test — a real run takes ~1-8s, longer than the granularity — so the regression test stubs `date` on `PATH` to freeze the clock; the backup line is the only `date` caller in this path.
- **`test_configure.sh` made hermetic against an ambient config environment — `scripts/release.sh` could not run its own gate.** `config.sh` auto-resolves `GEMINI_USE_API`/`MINIMAX_USE_API` and **exports** them; `configure.sh::has_explicit_value` treats any ambient value as a deliberate user pin. `release.sh` sources `lib/common.sh` (→ `config.sh`) *before* running `npm test`, so the gate inherited pinned transports: Tests 4 and 11 failed under `release.sh` while passing from a clean shell — a "flaky" result that was in fact deterministic per-environment. The suite now `unset`s the whole switchable `*_USE_API` set at the top (one place, covering all 10 configure invocations; only the two auto-resolved ones actually leak today, but the set is cleared so a future auto-resolution — as MiniMax gained in v2.21 — cannot silently reopen it). Same class as the v2.21.0 `test_user_config.sh` fix. Verified green both from a clean shell and under `source lib/common.sh; npm test`. Latent since `test_configure.sh` was introduced; it never affected the shipped runtime, only the release gate.
- Deprecated `setup_wizard.sh` to a compatibility forwarder. Corrected `GOOGLE_API_KEY` to `GEMINI_API_KEY`, removed legacy `/tmp` overrides from the starter template, completed advanced settings/persona coverage, and fixed safe parsing of unquoted `.env` inline comments.

### v2.21.1
- **Kimi upgraded to K3.** `KIMI_MODEL` now defaults to `kimi-code/k3`; premium, standard, and economy tiers all resolve to the same K3 alias.
- **Model selection is now real, not metadata-only.** `query_kimi.sh` passes `--model "$KIMI_MODEL"` to the CLI, overriding a stale user-level Kimi default. A dedicated offline regression test captures the CLI arguments and validates response metadata; a live K3 smoke test returned a structured response with confidence 10.
- **Roster reduced from 15 to 11 supported consultants.** Kilo, Aider, Amp, and Ollama were removed end-to-end: canonical/default lists, presets, routing and personas, debate/synthesis/reflection, doctor/preflight, updater, configuration/wizard, schemas, tests, docs, and their four query adapters. `ENABLE_KILO`/`KILO_*`, `ENABLE_AIDER`/`AIDER_*`, `ENABLE_AMP`/`AMP_*`, `ENABLE_OLLAMA`/`OLLAMA_*`, and the Ollama-only `local` preset are obsolete and ignored.
- **Documentation and release surfaces synced.** README, SKILL, `.env.example`, setup/configuration/recipes, cost catalog, changelog, release note, npm metadata, and the showcase site all identify Kimi K3. Version bumped to 2.21.1.

### v2.21.0
- **CLI-first transport (principle).** Every `*_USE_API` defaults `false`; a consultant with a CLI always uses it, API is opt-in (CLI-less model or explicit choice). `ENABLE_AMP`/`ENABLE_CLAUDE` default true.
- **MiniMax via `mmx` CLI** (was API-only). `query_minimax.sh` runs `mmx text chat --non-interactive …` in CLI mode; `config.sh` adds `MINIMAX_CMD` + auto-resolves `MINIMAX_USE_API` (API iff `MINIMAX_API_KEY` set — back-compat), drops MiniMax from `API_CONSULTANTS`; `doctor.sh` treats it as switchable.
- **`scripts/update_clis.sh`** (+ `ai-consultants update-clis`): per CLI-backed consultant, `detect_method` (brew formula/cask, npm, uv, pipx, pip, self-update, curl installer) → update. `--dry-run`, `--only`.
- **Stance consensus** (opt-in, `ENABLE_STANCE_CONSENSUS`): new `lib/stance.sh` (`generate_stance_options` — one synthesizer call wrapped in `run_with_timeout`; `_stance_clean`; `build_stance_prompt`). `consult_all.sh` generates the shared set → `stance_options.json` + exports `STANCE_OPTIONS_PROMPT`; injected via `personas.sh` (CLI) + `api_query.sh` (API). `voting.sh::calculate_consensus_score` scores the plurality stance over the PANEL size (not the count of emitted stances). `STANCE_MAX_OPTIONS`, `STANCE_TIMEOUT`.
- **Smoke-test reliability**: `query_gemini.sh` (`agy -p "$QUERY"`), `query_kimi.sh` (`stream-json` + `_kimi_extract_content`), `query_kilo.sh` (de-fence). Shared `_is_consultant_response_file` metadata filter in `common.sh`, applied across `voting.sh`/`synthesize.sh`/`peer_review.sh`/`reflection.sh`. `orchestration.sh` stalled→stable relabel (set -e-safe) + debate-round promote validation.
- **Persona fix**: `_AGENT_DEFAULT_PERSONAS` mapped `GLM|17`/`DEEPSEEK|7` (each got the other's persona) → `GLM|7`/`DEEPSEEK|17`.
- **Dev/reliability**: reliability tracking + Amp/Claude default-on; offline e2e test; `scripts/release.sh` version-bump automation; calibration fixes (diagnosable peer-review failures + cost-only path); `test_user_config.sh` XDG test made hermetic (unset the exported `_AI_CONSULTANTS_XDG_*` intermediates). `npm test` = 18 suites. Hardened by a `/code-review max` pass.

### v2.20.0
- **Capability axes (borrowed from the delegation-kit cost/intelligence/taste table).** `references/affinity.json` → v1.1: new `capabilities` (per-consultant {intelligence, taste, cost}, 1-10), `category_axis` (category → the quality axis it stresses: taste for API_DESIGN/ARCHITECTURE/CODE_REVIEW/GENERAL, intelligence otherwise), `capability_default`. New `lib/routing.sh::get_capability` / `get_category_axis` (cached like `get_affinity`).
- **Capability-weighted voting** (`ENABLE_CAPABILITY_WEIGHTING`, opt-in): `lib/voting.sh::_effective_vote_weight` modulates a vote by the consultant's capability on the run's axis — `confidence × (S + cap) / S` (`S = CAPABILITY_WEIGHT_STRENGTH`, default 10). Applied in `calculate_weighted_recommendation` and `calculate_final_score` (normalizer uses the same weight → 1-10 scale preserved). Axis from `QUESTION_CATEGORY`. `cost` never weights a vote (tie-break intelligence > taste > cost).
- **Capability-aware composition** (`ENABLE_CAPABILITY_ROUTING`, opt-in): `select_consultants` keeps the raw-affinity eligibility filter but ranks eligible consultants by `affinity + (cap − capability_default)`, so under a size limit the quality axis reorders who makes the cut.
- **Roster audit** — `scripts/roster_audit.sh` scores each consultant's "distinct approach" rate (max pairwise Jaccard of approaches < threshold) across consultations; verdicts unique-value / some-value / redundant? / insufficient-data. Wired as `doctor.sh --roster-audit` (short-circuit mode) and the `/ai-consultants:roster-audit` slash command (3 hosts). Reuses `voting.sh` keyword/Jaccard helpers.
- **Measured calibration** — `scripts/roster_calibrate.sh` (Tier A): intelligence/taste = mean blind peer-review score sliced by `category_axis`, cost = mean observed `tokens_used`×rate (60/40 split, rank-normalized cheapest=10); emits a `capabilities` block (`--json`/`--write`). `scripts/taste_elo.sh` (Tier B): taste via pairwise-judge Elo (pluggable judge — `JUDGE_CLI` / `TASTE_JUDGE_CMD`). `scripts/run_calibration.sh` drives `references/calibration_benchmark.json` (50 questions, 20 taste / 30 intelligence) through the panel with peer review, then calibrates.
- **Config** (`config.sh`): `ENABLE_CAPABILITY_WEIGHTING`, `ENABLE_CAPABILITY_ROUTING` (both false), `CAPABILITY_WEIGHT_STRENGTH` (10), `CAPABILITY_DEFAULT` (5). **Observability**: `consult_all.sh` records `capability{weighting_enabled, routing_enabled, axis}` in `optimization_metrics.json`.
- **Tests**: `test_capability_weighting` (17), `test_roster_audit` (6), `test_roster_calibrate` (10), `test_taste_elo` (6) — 12 suites total. All opt-in; with flags off, existing voting/routing/parity tests pass unchanged. shellcheck clean.
- **Docs**: `SMART_ROUTING.md` (capability + roster-audit + measured-calibration sections), `.env.example`, `references/configuration.md`, env-var table + structure list here. Website feature card added (sync on push).
- **Seeds are subjective** (Claude/Codex grounded on the model-routing table; the rest heuristic) — the calibration workflow measures them empirically. Fully back-compat.

### v2.19.2
- **Cost tracking silently lost on a fresh install — fixed.** `lib/costs.sh::track_session_cost` wrote to `$COST_TRACKING_FILE` (`$XDG_DATA_HOME/ai-consultants/costs.json`, i.e. `~/.local/share/...`) without ever creating the parent directory. That dir doesn't exist until something makes it, and `costs.json` is the only artifact stored there — so on a fresh install every session's cost write failed with "No such file or directory" (swallowed under `set -e` in the caller) and cumulative cost tracking never accumulated. Fix: `mkdir -p "$(dirname "$COST_TRACKING_FILE")"` up front; on failure, `log_warn` + `return 0`.
- **Function split for lock discipline**: `track_session_cost` (outer) now owns dir creation + locking and delegates the read-modify-write to a new inner `_track_session_cost_update`, which runs with the lock held and never propagates failure. Rationale documented inline: the caller in `consult_all.sh` runs under `set -e` *after* every consultant has already been queried and billed, so a bookkeeping failure must degrade to a warning, never abort the run.
- **Concurrency**: the RMW of `costs.json` was unguarded, so parallel consultations could interleave and lose records or fail the `mv`. Added a portable `mkdir`-based lock (flock is unavailable on macOS) with a bounded wait (50 × 0.1s = 5s); if the lock stays busy it proceeds unlocked with a warning rather than blocking or aborting. Writes now go through `mktemp "${COST_TRACKING_FILE}.XXXXXX"` instead of a fixed `.tmp` sibling, so an unlocked concurrent writer can't clobber another run's temp file (lost record / failed `mv`).
- **Corrupt-file self-heal**: a corrupt `costs.json` (truncated write, interleaved update) previously failed every future `jq` update forever and never recovered. Now `jq empty` gates the file; on failure it's moved aside to `${COST_TRACKING_FILE}.corrupt` and reset, so tracking recovers on the next session.
- **Tests**: `scripts/test_suite.sh::test_cost_tracking_resilience` (5 assertions, wired into `main()`) — costs.json created under a missing nested parent dir (fresh-install path), second session accumulates on the existing file (0.25 → 0.75, both exact in binary float), a corrupt costs.json is reset with the new session recorded, and the corrupt original is preserved as `.corrupt`. Calls guarded with `|| true` so a regression surfaces as a FAIL assertion, not a `set -e` suite abort. 8 suites pass; the new assertions are green; shellcheck clean.
- **No behavioral change for existing installs** whose data dir already exists; the fix only affects the first-run/absent-dir, concurrent, and corrupt-file paths. Cost bookkeeping remains best-effort by design.

### v2.19.1
- Cleanup from `/code-review max` on v2.19.0 (the workflow ran degraded under rate limits but surfaced these once it completed; all confirmed inline):
  - **Report-table mangling fixed**: a failure reason containing a `|` (e.g. a CLI error mentioning a piped command) broke the "Diagnosed Failures" markdown row. The new `lib/common.sh::render_diagnosed_failure <entry> [console|table]` escapes `|`→`\|` in table mode. (Not a security injection — the text is the user's own CLI stderr — but a real rendering bug.)
  - **DRY**: that helper is now the single source of the `name|reason` decode; the console log (quorum FAILED branch) and the report table both call it, so a future delimiter/encoding change touches one place instead of two.
  - **Health gate is cache-aware**: it no longer pings a consultant whose response is already cached (Round 1 would serve it free via `check_cache`) — it keeps the cached consultant without a billed probe. Avoids the opt-in gate defeating the semantic cache.
  - **`ping_consultant` takes the already-lowercased id** (callers compute it for the out/err paths anyway), dropping a redundant `to_lower` fork per consultant on both call sites (`doctor --live`, health gate).
  - **Documented** the health gate's inherent pre-Round-1 startup latency (serial with the run by definition; up to `HEALTH_GATE_TIMEOUT`) in `config.sh` — it's the cost of pruning up front; opt-in + tunable.
- Tests: `test_render_diagnosed_failure` (4 assertions incl. pipe-escaping); `test_ping_consultant` updated for the lowercased-id signature. 8 suites pass; shellcheck clean. Smoke-verified: report table renders correctly, health gate still prunes + the min-2 guard fires on the pruned count.

### v2.19.0
- **Quorum grading + "Diagnosed Failures" report** (the #1 pick from the `/workflows` CLI-reliability investigation — chosen for zero transport/billing risk over the CLI→API fallback, which the workflow's adversarial critique showed would emit persona-less confidence-5 stubs that poison voting). After the round-1 collect loop, `grade_quorum <success> <attempted> <min>` (new pure helper in `lib/common.sh`) classifies the run **MET / DEGRADED / FAILED** vs `QUORUM_MIN` (default 2). The report gains an `**Outcome**:` banner and a **## Diagnosed Failures** table that surfaces each dropped consultant's reason (the v2.18.0 `.err` capture) — so a panel that silently shrank to 2/7 is visibly DEGRADED instead of presenting as authoritative. `QUORUM_ACTION=stop` aborts below quorum; default `warn` continues with the banner. Failures are collected in `_surface_consultant_error` (DRY — same call that already logs the reason).
- **Health gate (`ENABLE_HEALTH_GATE`, opt-in)** — before the run, ping every selected consultant **in parallel** (new `lib/common.sh::ping_consultant`, `HEALTH_GATE_TIMEOUT` default 30s) and drop the non-responsive ones, so installed-but-unauthenticated CLIs (Cursor/Kimi/stale installs) are pruned up front and the quorum/budget checks see the genuinely-working panel. Script-less custom API agents (no `query_*.sh`) return code 2 and are kept (not probeable). Opt-in because it costs one extra tiny query per consultant; it prunes, it does not switch transport.
- **DRY**: `doctor --live` refactored to use the shared `ping_consultant`; both surfaces now share one probe implementation.
- **Explicitly NOT done** (per the workflow synthesis): CLI→API automatic fallback (persona-loss vote poisoning; Gemini already auto-resolves), cold-start retry-timeout escalation (stretches the whole round's worst case), warm-up calls, and the transport-abstraction circuit breaker (racy on-disk state, highest blast radius).
- **Tests**: `test_functions.sh` — `test_grade_quorum` (5 assertions: MET/DEGRADED/FAILED + boundaries) and `test_ping_consultant` (3: valid→0, no-output→fail, missing-script→2, via stub query scripts). 8 suites pass; shellcheck clean. End-to-end smoke verified: a forced CLI failure yields FAILED outcome + Diagnosed Failures table with the real reason, and the health gate prunes the dead consultant pre-run.

### v2.18.0
- **Failures are now diagnosable, not silent (Fix A).** `consult_all.sh` launched every consultant with `> /dev/null 2>&1` — discarding stderr — so a failed consultant produced only a bare `Failed`/`Empty response`, with no way to tell *not installed* vs *not authenticated* vs *transient* (this is exactly what led a real session to mis-attribute failures to a timeout). Now each consultant's stderr is captured to `$OUTPUT_DIR/<consultant>.err`, and on FAILED/EMPTY the run surfaces a one-line reason via the new `lib/common.sh::get_consultant_error_reason` helper (ANSI-stripped, prefers an explicit error line, drops orchestration status noise, falls back to "no error captured — CLI likely missing or not authenticated").
- **`run_query` now embeds the CLI's real error in its failure log.** The per-attempt CLI stderr (`${output_file}.err`, an unpredictable mktemp path the orchestrator can't locate) was only `log_debug`'d. Its first line is now appended to the `Error (code: N)` and `All N attempts failed` warnings, so the actual reason (e.g. `401 Unauthorized`, `command not found`) reaches the captured stderr and the surfaced message.
- **`doctor.sh --live` (Fix B): real ping per consultant.** The static checks only verify a CLI is installed (`--version`), so `doctor` reports a consultant healthy even when it errors at query time (unauthenticated) — a real source of "All systems healthy" while 3 consultants silently fail. `--live` sends a minimal real query to each *enabled* (and not self-excluded) consultant with a short timeout (`DOCTOR_LIVE_TIMEOUT`, default 45s), and reports ✓/✗ with the captured reason; failures become `doctor` issues (exit 1). Opt-in (costs one tiny query each). Standalone short-circuit mode like `--suggest-preset`.
- **Tests**: `test_functions.sh::test_get_consultant_error_reason` (5 assertions: explicit-error line, embedded-auth reason, orchestration-noise-not-mistaken, empty/missing file). 8 suites pass; shellcheck clean (CI invocation).
- **Not fixable in code (documented for users)**: the underlying CLI auth/install state, and cold-start/warm-up retry effects, are environment issues — `--live` *surfaces* them but can't authenticate a CLI for you.

### v2.17.2
- **SECURITY: revert v2.17.1's `allow_absolute=true` for context files — it opened a secret-exfiltration surface (caught in `/code-review max`).** v2.17.1 fixed the macOS `/private/tmp` drop by letting `build_context.sh` accept *any* absolute context path behind `validate_file_path`'s prefix-only blocklist (`/etc /root /var/log /proc /sys /dev`). That blocklist covers no home secrets, so a context arg of `~/.ssh/id_rsa`, `~/.aws/credentials`, `~/.netrc`, or `~/.config/gh/hosts.yml` was read verbatim and **sent to the external AI providers**. It also matched literal non-canonical prefixes, so `/private/etc/master.passwd` (the real `/etc` on macOS, via the same symlink aliasing) bypassed the `/etc` rule.
- **Fix (correct altitude — allowlist, not blocklist)**: context files are now accepted only when (a) relative and in-tree (`validate_file_path ... false` — rejects absolute and `..`), or (b) under a recognized temp root via the new `_is_temp_path` helper (`/tmp`, `/private/tmp`, `$TMPDIR`, and the `/private`-prefixed macOS alias of `$TMPDIR`; `..` rejected so a temp prefix can't traverse out). Still fixes the original macOS scratch-file regression (Claude Code writes `/private/tmp/...`) **without** widening to the whole filesystem. Verified: `/tmp`, `/private/tmp`, `$TMPDIR/...`, relative in-tree → accepted; `~/.ssh/id_rsa`, `/etc/passwd`, `/private/etc/master.passwd`, `/tmp/../etc/passwd` → rejected.
- **`build_context.sh` OUTPUT_FILE**: removed the `/tmp/*` short-circuit that jumped past `validate_file_path` (so `/tmp/../etc/x` bypassed the `..` check). Output paths now always run through `validate_file_path "$OUTPUT_FILE" "true"` — absolute allowed (output lives under the XDG cache or `/tmp`), traversal + sensitive-path guards enforced. The two validation sites are intentionally different now (OUTPUT is tool-chosen; context files are untrusted input) and the comments say so.
- **Test**: rewrote `test_context_optimization.sh` Test 15 as a *boundary* test — temp-root accepted, `~/.ssh/id_rsa` rejected (exfiltration blocked), `/etc/passwd` rejected. Uses the auto-cleaned `$_TMPDIR` (no `$HOME` litter) and `QUESTION_CATEGORY=SECURITY` to skip the project-tree `find` the assertions don't need. Fixed the stale suite-header comment that contradicted the new behavior. 8 suites pass; shellcheck clean.
- **Note**: v2.17.1 was tagged and GitHub-released but **never published to npm** (npm stayed at 2.17.0), so no npm user received the vulnerable version. Publish **2.17.2**, not 2.17.1.

### v2.17.1
- **Fix: context files at absolute paths outside `/tmp` were silently dropped.** `build_context.sh`'s context-file gate accepted only relative paths or a literal `/tmp/*` prefix (`if [[ "$_PARSED_PATH" == /tmp/* ]] || validate_file_path "$_PARSED_PATH" "false"`). On macOS `/tmp` is a symlink to `/private/tmp`, so Claude Code scratch files arrive as `/private/tmp/...` and matched neither branch → `log_warn "Skipping invalid file path"` and the file was excluded, with `build_context.sh` falling back to a generic repo auto-context. Net effect: a consultation looked like it ran with the user's context but the consultants never received it. (Reported from a real session where the passed context was dropped and an auto-context substituted.)
- **Fix**: the gate now uses `validate_file_path "$_PARSED_PATH" "true"` (allow absolute), mirroring the OUTPUT_FILE handling a few lines above. Context files are explicitly user/agent-provided, so absolute paths are legitimate; the sensitive-path blocklist (`/etc /root /var/log /proc /sys /dev`), path-traversal (`..`), and null-byte guards in `validate_file_path` still apply. Verified: `/private/tmp/...`, `/tmp/...`, relative, and `$HOME/...` paths accepted; `/etc/passwd`, `/var/log/...`, `../../etc/...` still rejected.
- **Test**: `test_context_optimization.sh` Test 15 — an absolute context path outside `/tmp` (a `$HOME` file) is now included in the built context, and `/etc/passwd` is still skipped. 8 suites pass; shellcheck clean.
- **Note (not a code change)**: the same session also showed Gemini/Codex failing immediately — because the *installed* skill at `~/.claude/skills/ai-consultants` was **v2.10.0** (pre-agy migration), so Gemini called the deprecated `gemini` binary. Updating the installed skill to ≥v2.15 (now v2.17.1) resolves that; it's an install-staleness issue, not a current-code bug.

### v2.17.0
- **Model catalog refresh (June 2026)** across all three tiers + cost rates, for every agent. Source of truth stays `config.sh::get_model_for_tier` (+ default `*_MODEL` vars) mirrored by `docs/cost_rates.json` (`model_tiers` + `consultant_fallbacks` + per-1K `models` rates). CLI-addressed models verified by querying the installed binaries (`agy models`, `agent --list-models`, kimi config); API/provider models + pricing researched against official sources. Superseded IDs kept in `cost_rates.json` for historical/pinned lookups.
- **Changed models** (premium / standard / economy):
  - **codex**: gpt-5.5 / **gpt-5.4** (was gpt-5.3) / **gpt-5.4-nano** (was gpt-4o-mini).
  - **cursor**: **composer-2.5** / **composer-2** / **gemini-3-flash** (was composer-2 / composer-1.5 / gemini-2.0-flash).
  - **deepseek**: deepseek-v4-pro / **deepseek-v4-flash** ×2 (was deepseek-v3.2 / deepseek-chat; chat+reasoner deprecate 2026-07-24).
  - **glm**: **glm-5.2** premium+standard (was glm-5.1) / glm-4-flash.
  - **grok**: grok-4.3 / **grok-4.1-fast** ×2 (was grok-3 / grok-3-mini).
  - **qwen3**: **qwen3.7-max** (was qwen3.6-plus) / qwen3.6-35b-a3b / qwen3-32b.
  - **aider**: **qwen3-coder:free** (was nvidia/nemotron…:free) / **gpt-5.4** / **gpt-5.4-nano**.
  - **ollama**: **hf.co/prithivMLmods/VibeThinker-3B-GGUF** default (was qwen2.5-coder:32b); standard/economy keep llama3.3/llama3.2.
- **Unchanged (verified current)**: claude, gemini (agy display names — no newer Gemini), mistral, minimax, kimi/amp/kilo.
- **Cost-rate corrections (per-1K)**: `gpt-5.5` → 0.005/0.030 (was understated 0.003/0.012), `deepseek-v4-pro` → 0.000435/0.00087, `minimax-m2.7` → 0.00025/0.001, `composer-2` → 0.0005/0.0025, Gemini Flash → 0.0015/0.009, Gemini 3.1 Pro → 0.002/0.012, mistral-medium → 0.001/0.003, devstral-small-2 → 0 (free). New IDs added with researched per-1K rates. `COST_RATES.md` regenerated to match the JSON.
- **Estimates flagged**: `glm-5.2` (mirrors glm-5.1) and `qwen3.7-max` (mirrors qwen3-max) pending official pricing.
- **Docs synced**: README, configuration.md, .env.example (+ fixed pre-existing GROK/DEEPSEEK default drift), SETUP.md, CLAUDE.md tables. Tests: tier assertions updated +10 new; 8 suites pass; shellcheck clean.
- **Found in `/code-review max` (workflow-backed, 61 agents): case-insensitive cost lookup.** `lib/costs.sh::get_rate_from_file` did an exact-case jq key match, but callers lowercase the model name first — so every **mixed-case** rate key silently missed and fell to `default_rate` ($0.005/$0.015 per 1K). This diff newly tripped it with the Ollama default `hf.co/prithivMLmods/VibeThinker-3B-GGUF` (a free local model billed at $0.02/query → wrong session cost reports, pre-run estimates, and budget halts under `ENABLE_BUDGET_LIMIT`), and it had silently rendered the **Gemini** agy display-name rates inert since v2.15 (every Gemini consultation cost-reported at default, not its real rate). Fixed by matching keys case-insensitively (`ascii_downcase` both sides via `first(.models|to_entries[]|select(...))`) — lowercase keys (minimax/gpt-5.5) still match; mixed-case (Gemini, VibeThinker) now resolve. Verified: VibeThinker → $0, Gemini 3.1 Pro → $0.014 for 1k+1k. Regression tests added (VibeThinker=0, Gemini resolves, local model free).
- **Also from review**: `estimate_query_cost`/`format_cost` now restore the leading zero bc drops on sub-1 values (".014000"/".03¢" → "0.014000"/"0.03¢") — surfaced by the diff's many sub-cent rates. README roster + `references/configuration.md` Grok/DeepSeek default cells synced to grok-4.3 / deepseek-v4-pro (the partial sync had left them stale). `config.sh` codex + ollama inline comments corrected to the new defaults. `cost_rates.json` premium-block date comment → Jun 2026. (Skipped as deliberate: re-ordering legacy entries under `_comment_legacy`, and the `gemini-3-flash-preview` vs `gemini-3-flash` coexistence — the former is the historical Gemini-API id, the latter Cursor's economy model, kept separately on purpose.)

### v2.16.0
- **Dynamic orchestration engine** — the fixed `classify → query → debate(N fixed rounds) → synth` pipeline becomes adaptive, inspired by Claude Code's dynamic workflows but implemented entirely in standalone bash (no Workflow tool / Claude-Code dependency). A planner picks an orchestration **shape** per question; debate becomes a **convergence loop**.
- **New module `lib/orchestration.sh`** (sourced by `consult_all.sh` after voting/costs). Public surface:
  - `detect_intent <query>` → `advise|compare|exhaustive` (heuristic regex over the query; zero-dep).
  - `select_orchestration_shape <category> <complexity> <intent>` → `quick|converge|adversarial|tournament|exhaustive|fixed`. Auto resolution priority: explicit `ORCHESTRATION_MODE` override → intent (`exhaustive`/`compare`) → category (`SECURITY`→adversarial) → complexity (≤`COMPLEXITY_THRESHOLD_SIMPLE`→quick, else converge).
  - `run_orchestration <shape> <dir> <category>` dispatcher; skips multi-round shapes when `SUCCESS_COUNT ≤ 1`.
  - Pure decision helpers, unit-tested without live CLIs: `_convergence_should_stop <score> <prev> <target> <epsilon>` (→`converged|stalled|continue`) and `_approach_signature <dir>` (sorted-unique approach set, the loop-until-dry stop signal).
- **Convergence loop (`run_convergence_loop`)** replaces the fixed `for round=2..DEBATE_ROUNDS`. Stops on: consensus ≥ `CONVERGENCE_TARGET_CONSENSUS` (converged), per-round gain < `CONVERGENCE_STALL_EPSILON` (stalled), `CONVERGENCE_MAX_ROUNDS` reached, or budget. `min_rounds` param forces ≥N critique rounds even on early consensus (adversarial uses 2). Reuses `calculate_consensus_score` (voting.sh) for the stop signal and the extracted `_apply_debate_round` helper (the legacy loop body, now shared) for execution+merge. Trajectory + stop reason written to `orchestration.json`.
- **Shapes**: `quick` (no debate), `converge` (loop to consensus), `adversarial` (forced critique round + peer review as refutation gate — `SECURITY`), `tournament` (converge, then synthesis declares one winner via `ORCHESTRATION_SELECT_WINNER` directive in `synthesize.sh`), `exhaustive` (`run_exhaustive_loop`: iterate until a round adds no new `.response.approach`).
- **`consult_all.sh` integration**: after classification, computes `QUERY_COMPLEXITY` (`calculate_query_complexity`, costs.sh) + `QUERY_INTENT` + `ORCH_SHAPE`; the debate block dispatches on `ORCHESTRATION_MODE` (`fixed` → byte-equivalent legacy loop via `_apply_debate_round`; else → `run_orchestration`). Adversarial shape force-enables `ENABLE_PEER_REVIEW`. Shape/complexity/intent recorded in `optimization_metrics.json`.
- **Config (all back-compat)**: `ORCHESTRATION_MODE` (default `auto`), `CONVERGENCE_MAX_ROUNDS` (4), `CONVERGENCE_TARGET_CONSENSUS` (75), `CONVERGENCE_STALL_EPSILON` (5), `ENABLE_ADVERSARIAL_VERIFY` (true). `ORCHESTRATION_MODE=fixed` restores the exact pre-2.16 pipeline.
- **Behavioral change (default)**: with `auto` the panel may run more or fewer rounds than the old fixed default, driven by consensus — complex/contested questions iterate further, simple ones short-circuit to `quick`. Every round still passes `enforce_budget`, so `MAX_SESSION_COST` continues to cap spend. Set `ORCHESTRATION_MODE=fixed` to opt out.
- **Minor robustness**: `_apply_debate_round` swallows a failed `debate_round.sh` (`|| true`) instead of aborting the whole consultation under `set -e` (the legacy inline `$()` capture would abort). Applies to both `fixed` and dynamic paths.
- **Convergence actually converges (fixed in `/code-review max`).** The round file carries the consultant's *updated* top-level `.response.approach` (`build_structured_response` uses `$inner.response`), but the legacy merge grafts only `.debate` (which `build_structured_response` doesn't even preserve → null). Since `calculate_consensus_score` reads `.response.approach`, the consensus signal was invariant under debate → the loop stalled after exactly one round every time. Fix: `_apply_debate_round` gained a `promote` flag; the dynamic loops (`promote=true`) adopt the round file's post-debate `.response`/`.confidence` so consensus reflects evolved positions. The legacy `fixed` path (`promote=false`) keeps the original `.debate`-only graft. Verified: a stubbed converging panel now moves 0 → 100 (`converged`), where before it logged `stalled` at round 1. Regression tests assert promote adopts the updated approach and legacy preserves the original.
- **ARCHITECTURE keeps its mandatory debate (fixed in `/code-review max`).** The legacy pipeline always debated `SECURITY` *and* `ARCHITECTURE`; the first planner cut only special-cased `SECURITY`→adversarial, letting `ARCHITECTURE` fall through to `converge` (min_rounds=1), which early-exits with zero debate rounds when the fan-out already agrees. Now `ARCHITECTURE` pins to `converge` (never `quick`) and `run_orchestration` forces `min_rounds=2` for both mandatory categories, so they always run ≥1 critique round — restoring pre-2.16 behavior.
- **Tests**: new `scripts/test_orchestration.sh` (32 assertions, 13 tests) — intent detection, shape selection (intent/category/complexity priority + overrides + fixed bypass + threshold boundary + ARCHITECTURE mandatory-debate), convergence stop decision (converged/stalled incl. negative-gain/continue), `_apply_debate_round` promote-vs-legacy merge, `_approach_signature` over fixtures, `ORCHESTRATION_MODE=auto` default. Auto-discovered by `test_all.sh` → now **8 suites**. Convergence loop smoke-tested end-to-end with a stubbed `debate_round.sh` (0 → 100 converged).

### v2.15.1
- **Markdown-fence parsing fix — Gemini's default model produced degraded (fallback) responses under v2.15.0.** The v2.15.0 migration note claimed "agy prints the model JSON directly (top-level `.response`)"; verified against agy 1.0.10, that is only true for some models (e.g. `Gemini 3.5 Flash (Low)` returns bare JSON). The **default** `Gemini 3.1 Pro (High)` wraps the envelope in a ```` ```json … ``` ```` markdown fence, so `process_consultant_response` failed the `.response.summary` jq probe and fell through to `build_fallback_response` — emitting `summary: "Unstructured response - see detailed"`, `confidence: 5`, and empty pros/cons for every Gemini reply, with the real structured envelope buried as a fenced string inside `.detailed`. Fix: new shared helper `lib/common.sh::strip_json_fence` — returns the text unchanged when it already parses as JSON (a real fence makes the text invalid JSON, so the gate reliably detects it), otherwise drops pure fence-marker lines (`/^[[:space:]]*```[[:alnum:]]*[[:space:]]*$/d`). Consultant-agnostic; the bare-JSON path (Flash, every other consultant) is untouched. Verified end-to-end against live agy: `Gemini 3.1 Pro (High)` now yields a populated `build_structured_response` (summary/approach/pros/cons/confidence=10). Regression test in `test_functions.sh::test_process_consultant_response_fence`.
- **Fence fix applied to ALL agy output paths (caught in `/code-review max`).** The fence isn't only seen by `process_consultant_response` — two other paths consume agy output directly and were still corrupting it:
  - **`lib/reflection.sh::run_reflection_cycle`**: `_exec_consultant` returns raw (fenced) agy output. The critique's `jq -r '.needs_refinement'` returned `""` on fenced text (so the early-stop never fired and every cycle ran), and the refined response was written back as fenced non-JSON, making downstream voting/synthesis/report `jq` reads silently fall back — discarding the refined Gemini answer. Now de-fences `critique` and `refined` via `strip_json_fence` at the consumption points.
  - (`peer_review.sh` already had its own `extract_json_from_response` fence handler applied before aggregation — verified safe, left as-is.)
- **Synthesis via agy was broken (caught in `/code-review max`).** `lib/common.sh::build_synthesis_args` gemini branch produced a bare `agy` invocation (`SYNTHESIS_ARGS=("${GEMINI_CMD:-agy}")`), invoked as `echo "$prompt" | agy`. Unlike codex (`--full-auto`) and claude (`--print`), agy with no `--print`/`-p` launches its **interactive** session and never reads the piped prompt, so synthesis hung/produced nothing whenever agy was the chosen synthesizer (reachable e.g. when Claude Code is the invoking agent and agy is on PATH). Fixed to `("${GEMINI_CMD:-agy}" "-p" "-")` + `--model`, mirroring `query_gemini.sh`.
- **Comment rot (caught in `/code-review max`)**: `query_gemini.sh` comments claimed agy emits raw JSON "no envelope to unwrap"; corrected to note the default model fences its JSON and that the fence is stripped centrally.
- **Gemini transport auto-resolution — makes the Gemini consultant work out-of-the-box for npm/npx users.** v2.15.0 left Gemini enabled by default (`ENABLE_GEMINI=true`) in CLI mode (`GEMINI_USE_API=false`), but the agy (Antigravity) CLI cannot be installed via npm (it's a `curl|bash` binary into `~/.local/bin/agy`) and is OAuth-only (no headless/API-key auth). Net effect: every fresh `npx ai-consultants` run silently dropped Gemini from the panel (`consult_all.sh` marks it FAILED and continues) — *even when the user had `GEMINI_API_KEY` exported*, because API mode was opt-in with no auto-detection. The API path (pure `curl` + key, no binary, no browser) is the npm-friendly one but was off by default.
- **`config.sh`**: removed the hardcoded `GEMINI_USE_API="${GEMINI_USE_API:-false}"` at the mode-switching block (`scripts/config.sh:69-72`). The mode is now auto-resolved in the Gemini config section (`scripts/config.sh:113-131`), *after* `GEMINI_CMD`/`GEMINI_API_KEY` are known: when `GEMINI_USE_API` is unset, pick `true` if `GEMINI_API_KEY` is present, else `false` (agy CLI). An explicit `GEMINI_USE_API=true/false` is always honored (back-compat, via the `${GEMINI_USE_API+x}` set-vs-unset guard). Idempotent across the 15-30 `config.sh` re-sources per consultation: once resolved and `export`ed, subsequent sources see it as user-set. Only Gemini auto-resolves; the other four switchable agents (Codex, Claude, Mistral, Qwen3) keep their hardcoded `:-false` defaults since their CLIs are npm/pip-installable.
- **`doctor.sh`**: `check_cli_consultant` now skips the CLI install check when the consultant is in API mode (`${env_var}_USE_API == true`) — previously it would flag a missing CLI as a hard `✗ NOT INSTALLED` failure regardless of mode, which after auto-resolution would false-fail for every key-only Gemini user. This also fixes the same pre-existing latent false-positive for Codex/Claude/Mistral/Qwen3 when those run in API mode. Additionally, when Gemini *is* in CLI mode and `agy` is missing, the failure now prints a `tip: set GEMINI_API_KEY to use API mode (no CLI install needed)` remediation pointing at the npm-friendly path.
- **Tests**: `test_user_config.sh` +4 assertions (Tests 19-22): auto-API with key, auto-CLI without key, explicit `false` wins over a present key, explicit `true` honored without a key. Suite now 42 checks; all 7 suites pass.
- **No breaking change**: agy/OAuth users without a key keep CLI mode; anyone who pinned `GEMINI_USE_API` keeps their value. The only behavioral change is that a key-only environment now reaches Gemini over the API instead of silently dropping it.

### v2.15.0
- **Gemini consultant migrated from the Gemini CLI to the Antigravity CLI (`agy`)**. Google deprecated the Gemini CLI on 2026-06-18 (transitioning all individual/Pro/Ultra users to Antigravity CLI); the `gemini` binary stops serving requests for non-enterprise accounts. The consultant stays "Gemini" / "The Architect" (ID 1) — only the transport binary, model addressing, and auth change. Verified against `agy` 1.0.10.
- **`config.sh`**: `GEMINI_CMD` default `gemini` → `agy` (`scripts/config.sh:106`); `GEMINI_MODEL` default `gemini-3.1-pro-preview` → `Gemini 3.1 Pro (High)` (agy addresses models by display name, not API ID); new `GEMINI_API_MODEL` (default `gemini-3.1-pro-preview`) decouples the API-mode model ID from the CLI display name. `get_model_for_tier "gemini"` now returns agy display names: premium `Gemini 3.1 Pro (High)`, standard `Gemini 3.5 Flash (High)`, economy `Gemini 3.5 Flash (Low)`.
- **`query_gemini.sh`** — three verified flag/parse changes:
  - `-m` → `--model` (agy has no `-m` short alias).
  - **Dropped `--output-format json`** — agy has no such flag and prints the model's response as plain text. Because the persona instruction already forces the model to emit our JSON schema, that plain text *is* the JSON envelope we need.
  - **Dropped the `native_json_field="response"` argument** to `process_consultant_response`. The old Gemini CLI wrapped output as `{"response":"<text>","stats":{…}}` and we extracted `.response`; agy's output is the model JSON directly (top-level `.response`), so extracting `.response` would strip a level and force every structured reply into the fallback path. Confirmed end-to-end: a real `agy` call now yields a correct `build_structured_response` (consultant=Gemini, model="Gemini 3.1 Pro (High)", persona=The Architect, populated pros/cons/confidence).
  - API-mode branch now passes `$GEMINI_API_MODEL` (not `$GEMINI_MODEL`) to `run_api_mode_query`, since the Google AI endpoint builds `…/${model}:generateContent` and needs an API ID, not a display name.
- **CLI invocation parity**: same `-m`→`--model` change applied to the two other call sites — `peer_review.sh:150` and `lib/reflection.sh:71`.
- **Install/auth surfaces** repointed to agy: `doctor.sh` (install hint + `${GEMINI_CMD:-gemini}`→`:-agy` in the two config-dump fallbacks), `setup_wizard.sh`, `configure.sh` (`CLI_AGENT_CMDS`/`CLI_AGENT_HINTS`), `lib/common.sh` synthesis fallbacks (`:-agy`), `.env.example`, `docs/SETUP.md`, `references/configuration.md`. Install is now `curl -fsSL https://antigravity.google/cli/install.sh | bash` (binary lands in `~/.local/bin/agy`). Auth is **OAuth-only** (`agy` with no args → browser sign-in; creds cached); `agy` does **not** honor an API-key env var for headless use — that path remains exclusive to API mode.
- **Cost catalog**: added `Gemini 3.1 Pro (High)` / `Gemini 3.5 Flash (High)` / `Gemini 3.5 Flash (Low)` keys to `cost_rates.json` (per-1K, mirroring the matching Gemini API tier; agy itself bills via OAuth/subscription) and repointed `consultant_fallbacks.gemini` + all three `model_tiers.*.gemini` to the display names. The old `gemini-3.1-pro-preview`/`gemini-3-flash-preview`/`gemini-2.0-flash` entries are kept for API mode and historical lookups. `COST_RATES.md` synced.
- **Tests**: `test_suite.sh` tier assertions updated (`get_model_for_tier "gemini" premium/economy` and `get_economic_model "gemini"` now expect the agy display names) — same pattern as the v2.14.2 claude-opus-4-8 bump. All 7 suites pass.
- **Known follow-up (out of scope, flagged in the release note)**: the *host-side* integration — running ai-consultants **from** Gemini CLI as a slash-command host (`~/.gemini/skills/`, `INVOKING_AGENT=gemini`) — is affected by the same deprecation (Antigravity rebrands Extensions as "plugins", `agy plugin …`). That migration is **not** done here; this release covers only the Gemini *consultant* (the model we query). The relevant README/SETUP host sections were left intact rather than rewritten on unverified plugin mechanics.

### v2.14.2
- **Claude premium tier upgraded `claude-opus-4-7` → `claude-opus-4-8`** (Opus 4.8 released 2026-05-29). Single source of truth is the `premium` case in `config.sh::get_model_for_tier` (`scripts/config.sh:583`) and the `CLAUDE_MODEL` default (`scripts/config.sh:223`); both now resolve to `claude-opus-4-8`. Standard (`claude-sonnet-4-6`) and economy (`claude-haiku-4-5`) tiers are unchanged — Sonnet 4.6 and Haiku 4.5 remain the latest in their classes.
- **`docs/cost_rates.json`**: added a `claude-opus-4-8` entry; repointed `consultant_fallbacks.claude` and `model_tiers.premium.claude` to it; moved `claude-opus-4-7` into the `_comment_legacy` block so cost lookups for cached/historical responses and pinned overrides still resolve. Value entered in per-1K (see units fix below).
- **CRITICAL — cost-catalog unit normalization (fixes ~1000× cost overstatement)**: `lib/costs.sh` computes cost as `(token_count / 1000) * rate`, i.e. every value in `cost_rates.json` MUST be USD **per-1K** tokens (the hardcoded fallback table in `costs.sh`, e.g. `claude-3-haiku → 0.00025`, confirms this contract). But the premium/standard blocks had been populated with **per-MTok** dollar figures (`claude-opus-4-7: 5.00`, `claude-sonnet-4-6: 3.00`, `gpt-5.5: 3.00`, …) — so `estimate_query_cost` reported ~1000× the true cost for almost every premium/standard model (e.g. a ~1k-in/1k-out Opus query was reported as **$30** instead of **$0.03**). The economy block (incl. `claude-haiku-4-5: 0.001`) was already correct per-1K. Normalized the **entire** catalog (premium, standard, economy, legacy, `default_rate`) to per-1K by dividing the per-MTok entries by 1000; left the already-correct per-1K entries untouched. Added a `_comment_units` field documenting the contract to prevent regression. Verified: Opus 1k+1k = $0.03, 1M+1M = $30; Haiku 1k+1k = $0.006; gpt-5.5 1M+1M = $15; gemini-pro 1M+1M = $6.25 — all match provider pricing.
- **`docs/COST_RATES.md`**: Claude rows synced to the corrected per-1K values (`claude-opus-4-8` $0.005/$0.025, `claude-sonnet-4-6` $0.003/$0.015, `claude-haiku-4-5` $0.001/$0.005). Note: non-Claude rows in this human-facing doc may still lag the JSON and should be re-synced in a follow-up — `cost_rates.json` is the runtime source of truth, not this file.
- **Stale-alias cleanup (pre-existing, fixed in passing)**: five spots still carried pre-v2.10.6 short aliases and now use canonical IDs — `scripts/query_claude.sh` (header comment + `MODEL_USED` fallback), `.env.example` (`CLAUDE_MODEL=opus-4.6`), `references/configuration.md`, and the README "Models by Tier" table all → `claude-opus-4-8`; plus `lib/api.sh::build_anthropic_request` default `sonnet-4.6` → `claude-sonnet-4-6`. All were latent (callers always pass an explicit model / `config.sh` always exports `CLAUDE_MODEL`), but would have surfaced if those scripts were invoked standalone or the example `.env` copied verbatim.
- **Test**: `scripts/test_suite.sh::test_model_for_tier` premium assertion updated to expect `claude-opus-4-8`. No other test references the Claude premium ID.
- **Correction of an earlier mis-diagnosis (for the record)**: during development this was first flagged as "Haiku is 1000× *understated*". That was wrong — under the per-1K contract `claude-haiku-4-5: 0.001` is correct ($1/$5 per MTok). The real bug was the *opposite*: premium/standard entries were 1000× *overstated*. The unit normalization above is the actual fix.
- **Behavioral change**: cost *reporting* now drops ~1000× for premium/standard models (it was massively overstating). Orchestration, routing, and synthesis are unaffected. Note: `ENABLE_COST_AWARE_ROUTING` / budget thresholds compare against these figures, so anyone who tuned `MAX_SESSION_COST` against the old inflated numbers should revisit their threshold. Remaining out-of-scope item: a few legacy per-model rates (e.g. Mistral/DeepSeek/GLM tier orderings) reflect pre-existing catalog figures I couldn't verify against authoritative pricing — only the unit bug was fixed, not per-model price accuracy.

### v2.14.1
- **Pre-commit hook**: `scripts/hooks/pre-commit` runs `shellcheck` on staged `.sh` files under `scripts/` using the exact CI invocation (`-S warning -x -e SC1091,SC1090,SC2034,SC2155`). Mirrors `.github/workflows/ci.yml:32` so the same warnings that fail CI also fail the local commit. Filtered with the regex `^scripts/(lib/)?[^/]+\.sh$` to match the CI glob exactly (test fixtures under `scripts/test_fixtures/` are correctly excluded). Bypass: `git commit --no-verify`.
- **`scripts/install-hooks.sh`**: idempotent installer wired to `npm run install-hooks`. Backs up any existing different hook to `.git/hooks/pre-commit.backup.<timestamp>` to avoid clobbering contributor customizations (`FORCE=1` skips backup). Silent no-op outside a git checkout, so it's safe to wire to npm `prepare` or similar lifecycle hooks if ever needed.
- **`npm run lint`**: convenience wrapper for the full-repo shellcheck invocation. Useful pre-push when you want to validate without staging.
- **Motivation**: v2.14.0 push to `main` failed CI due to SC2164 in `scripts/test_context_optimization.sh:18` (`cd "$PROJECT_ROOT"` lacked `|| exit`). The new script passed `bash -n` locally but `bash -n` is purely a parser check — it doesn't run any linter. The pre-commit hook closes that gap. Documented in `CONTRIBUTING.md` Development Environment Setup section and briefly in `## Git Conventions` here.
- **No runtime behavior change**: this is contributor-only tooling. Tests still pass (7 suites, ~510 assertions).

### v2.14.0
- **Context handoff: AST optimization pipeline now engages on the primary slash-command path**. Pre-fix, `/ai-consultants:consult` instructed the invoking agent (Claude/Codex/Gemini) to inline file contents into the query string, which meant `build_context.sh` ran with zero `FILES` and the entire `lib/code_optimizer.sh` + `lib/chunking.sh` + `lib/symbol_map.sh` stack was dead code. The three `.{claude,codex,gemini}/commands/ai-consultants:{consult,debate}.md` slash commands now instruct agents to pass file paths as positional arguments to `consult_all.sh`; `build_context.sh` does the file reading and runs the optimizer.
- **`build_context.sh` reads exported `QUESTION_CATEGORY`** to decide whether to include the project tree section. SECURITY, QUICK_SYNTAX, ALGORITHM, BUG_DEBUG, DATABASE, TESTING categories skip it (noise for pointed questions); ARCHITECTURE, CODE_REVIEW, API_DESIGN, GENERAL include it; unknown categories default to "include" (conservative). New env var `FORCE_PROJECT_TREE=true` bypasses the heuristic. Categories source: `classify_question.sh` already exports `QUESTION_CATEGORY` in `consult_all.sh:241-242` — zero new code path, just consumes existing signal.
- **File relevance tags**: `path/to/file@PRIMARY` (focus of the question) vs `path/to/file@CONTEXT` (ambient reference). Default `PRIMARY` when omitted. Unknown tags fall back to PRIMARY with a `log_warn`. New parallel `FILE_TAGS` array in `build_context.sh`. Rendered as `### File: \`path\` (TAG)` in the `## Relevant Files` section, with a header explaining the two values to consultants. Downstream synthesis/debate/peer_review unaffected (they don't read `context.md`).
- **`--query-file <path>` flag in `consult_all.sh`** as escape hatch for queries exceeding shell ARG_MAX (~256KB on macOS) or containing mixed-quote payloads. Conflicts with positional question arg (parser-level error). Validates file existence at parse time.
- **Slash-command file detection**: replaced the hardcoded extension regex + `/`-in-token heuristic with "use your judgment, verify via Glob/Bash when uncertain". Pre-fix missed `Makefile`, `Dockerfile`, dotfiles; false-positive on URLs and regex patterns in question text. The invoking agent has full conversation context so it's better placed than a regex.
- **Claude-only note in `.claude/commands/ai-consultants:consult.md`**: explicit instruction not to use `Read` tool output (which carries `N\t` line-number prefix) since `build_context.sh` reads files itself. Codex and Gemini variants don't carry this note (their tools don't add the prefix).
- **First test coverage for `lib/code_optimizer.sh` and `lib/chunking.sh`**: new `scripts/test_context_optimization.sh` (17 assertions, 14 tests). Covers: @TAG default/explicit/unknown, QUESTION_CATEGORY routing (SECURITY drops, ARCHITECTURE keeps, unknown includes), FORCE_PROJECT_TREE override, Python AST extraction over MAX_CONTEXT_FILE_BYTES threshold, `optimize_code_file` smoke, `chunk_file_semantically` JSON shape, --query-file parsing (valid/missing/conflict), legacy no-FILES path preservation. Picked up automatically by `test_all.sh`'s `find`-based discovery — no manual wire-up needed. Total: 7 suites, ~510 assertions.
- **Test fixtures**: new `scripts/test_fixtures/context/` with `sample.py` (Python class + methods + main), `sample.sh` (Bash with functions), `sample.json` (config), `sample.txt` (plain text). Deterministic, no timestamps or randomness.
- **Help text updates**: `consult_all.sh --help` documents `--query-file` and `@TAG` syntax; `build_context.sh` usage error documents `QUESTION_CATEGORY` and `FORCE_PROJECT_TREE`.
- **Known gap surfaced**: `_supports_ast_extraction` declares 13 languages (Python, JS, TS, Go, Rust, Java, C++, C, C#, Ruby, PHP, Swift) but `lib/code_optimizer.sh` has dedicated extractors for only 4 (Python, JS/TS, Bash, Go); the other 9 fall back to `_extract_generic` (grep-based). Documented in release note; tracked for future tree-sitter-backed extraction.
- **Backwards compat**: agents that still inline file contents into the query (pinned old slash commands) keep working — `build_context.sh` degrades to no-FILES branch, just without the optimization benefit. Direct bash users of `./scripts/consult_all.sh "q" file1 file2` get the same behavior plus the new optimization.

### v2.13.1
- **Perf**: XDG roots resolved once at first `config.sh` source and **exported** as `_AI_CONSULTANTS_XDG_{CACHE,STATE,DATA}` — child query subshells inherit the values and skip ~6 subshells/child × 14 children = ~84 forks per consultation (~200-400ms saving on macOS).
- **Perf**: `apply_launch_stagger()` switched from `awk "BEGIN{printf}"` to pure-bash `printf '%d.%03d'` — 14 forks eliminated per consultation (~50-70ms aggregate).
- **Perf/DRY**: `_count_available_consultants` entries pre-uppercased (`"GEMINI|ENABLE_GEMINI|gemini"`) — 15 `to_upper` subshells eliminated per `--suggest-preset` invocation. Also fixes the latent self-exclusion case-mismatch the round-2 review caught.
- **Latent bug fix**: 5 `lib/*.sh` files (`cache.sh`, `session.sh`, `api.sh`, `chunking.sh`, `costs.sh`) had hardcoded `/tmp/...` defaults that drifted from the v2.13 XDG migration. Their fallbacks now reference `${_AI_CONSULTANTS_XDG_*}` so they stay aligned even if sourced standalone (e.g. from a future test). `lib/session.sh::cleanup_old_sessions` no longer hardcodes `/tmp/ai_consultations` either — uses `$DEFAULT_OUTPUT_DIR_BASE`.
- **DRY**: extracted `scripts/lib/test_helpers.sh` (~80 LOC) — `assert_eq`, `assert_match`, `run_test`, `test_summary`, `_reset_state` hook. Eliminates the ~90 LOC of triplication across `test_user_config.sh`, `test_doctor.sh`, `test_bin.sh`. Future test suites just `source lib/test_helpers.sh`.
- **CI**: `scripts/test_all.sh` (master runner introduced post-v2.13.0) now also includes `test_suite.sh` — adds 258 library assertions to `npm test`, closing the "tests on disk but not gating CI" gap. Total: 6 suites, ~493 assertions.
- **Style**: 37 → 0 shellcheck warnings under the project exclusions (`-e SC2034,SC2086,SC1091,SC2155,SC2154`). Touches: `configure.sh` (26 SC2004 + 1 SC2129), `lib/routing.sh` (4 SC2004/2321), `lib/code_optimizer.sh` (2 SC2001 sed → param expansion), `lib/chunking.sh` (2 SC2001), `lib/voting.sh` (1 SC2126), `lib/common.sh` (1 SC2005, 1 SC2317 annotated), `lib/api_query.sh` (1 SC2317 annotated), `lib/symbol_map.sh` (2 SC1090 annotated), `config.sh` (1 SC2317 annotated), `test_set_e_safety.sh` (1 SC2001 annotated).
- **Code quality**: `consult_all.sh` `_MODEL` var resolution switched from inline `tr` to `to_upper()` helper for consistency. Stale `name_upper` variable in `_count_available_consultants` renamed to `name`. `find_user_config_file` redundant `|| echo ""` simplified to `|| true`.
- All 6 test suites pass (493 assertions). Zero behavioral change for end users; XDG cache export is the only new env-var contract.

### v2.13.0
- New `doctor --suggest-preset --question "..."` recommends a preset + strategy combo for a question, based on category classification (`classify_question.sh`) and the count of available consultants (gated by `ENABLE_*` flags and self-exclusion). Outputs a one-line `ai-consultants` command + reasoning, or structured JSON via `--json` for tooling/automation.
- Classifier failures now surface explicitly as `Warning: classification of your question failed` (with the underlying error). Pre-fix the failure was masked by `2>/dev/null` and silently degraded to `GENERAL`.
- `--suggest-preset` short-circuits to "install more CLIs" hint when fewer than 2 consultants are usable — previously could recommend e.g. `minimal` preset with 0 consultants.
- `_count_available_consultants()` now respects `ENABLE_*` flags and subtracts the invoking agent (self-exclusion). Pre-fix the count included disabled consultants, leading `_recommend_combo` boundaries to fire on a phantom panel size.
- `ENABLE_DEBATE_OPTIMIZATION` promoted from opt-in to default `true` based on operator experience over 4 stable releases — debate is auto-skipped when confidence spread < `DEBATE_CONFIDENCE_SPREAD_THRESHOLD` (default 2). SECURITY and ARCHITECTURE remain mandatory-debate. No empirical benchmark in-tree yet; tracked for v2.14.
- XDG Base Directory compliance for transient and persistent paths (per freedesktop.org spec):
  - `DEFAULT_OUTPUT_DIR_BASE`: `/tmp/ai_consultations` → `$XDG_CACHE_HOME/ai-consultants/consultations` (typically `~/.cache/...`)
  - `CACHE_DIR`: `/tmp/ai_consultants_cache` → `$XDG_CACHE_HOME/ai-consultants/cache`
  - `RATE_LIMIT_DIR`, `CHUNK_TEMP_DIR`: `/tmp/ai_consultants_*` → `$XDG_CACHE_HOME/ai-consultants/{ratelimit,chunks}`
  - `SESSION_DIR`: `/tmp/ai_consultants_sessions` → `$XDG_STATE_HOME/ai-consultants/sessions` (`~/.local/state/...`)
  - `COST_TRACKING_FILE`: `/tmp/ai_consultants_costs.json` → `$XDG_DATA_HOME/ai-consultants/costs.json` (`~/.local/share/...`)
  - All env vars still respected; restore old behavior with `export DEFAULT_OUTPUT_DIR_BASE=/tmp/ai_consultations` etc.
- New `lib/user_config.sh::get_xdg_dir()` helper — single source of truth for XDG resolution; falls back to `$HOME/.{cache,local/state,local/share}` then `/tmp/ai-consultants-{kind}` for distroless containers.
- README slimmed: env-var section now points to `references/configuration.md` for the full ~150-var list and recommends `ai-consultants init` as the primary onboarding path.
- `RATE_LIMIT_DIR` and `CHUNK_TEMP_DIR` lifted to `config.sh` for consistency (were lib-only defaults).
- `config.sh` now hard-fails with `FATAL: ... get_xdg_dir()` if `lib/user_config.sh` is missing — pre-fix the v2.13 XDG defaults silently regressed to `/tmp/ai_consultants/...` when the helper was absent (corrupt install, refactor regression).
- New `scripts/test_doctor.sh` — 25 assertions in 12 tests: `--suggest-preset` across categories (SECURITY, QUICK_SYNTAX, ARCHITECTURE, ALGORITHM, GENERAL), no-question default, long-question truncation, short-circuit behavior, **+ review fixes**: `--json` output schema, count<2 install hint, classifier failure warning, `config.sh` FATAL on missing helper, count respects ENABLE_* flags.
- `scripts/test_user_config.sh` extended to 38 assertions in 18 tests — added: `get_xdg_dir` cache/state/data resolution, fallback to `~/.{cache,local/state,local/share}`, distroless `/tmp/ai-consultants-*` fallback, invalid kind handling, `config.sh` XDG path defaults, explicit env var override precedence, `ENABLE_DEBATE_OPTIMIZATION=true` default assertion.
- Round-2 review fix: `_count_available_consultants` self-exclusion was dead code due to UPPERCASE vs MixedCase mismatch — counter compared `Claude` to `get_self_consultant_name`'s `CLAUDE`. Now uppercases the entry name via `to_upper`; `INVOKING_AGENT` correctly drops 1 from the count. Regression test added.
- Round-2 fix: `--suggest-preset --json` pre-flights `jq` with a clear error message instead of aborting under `set -e` with `command not found` (the main `check_dependencies` jq probe doesn't run when `--suggest-preset` short-circuits).
- Round-2 fix: `--json` schema gains `schema_version: 1` and `recommended_command` fields — tooling no longer has to reconstruct the invocation client-side and has a signal for future schema evolution.
- Round-2 fix: `_count_available_consultants` no longer hardcodes a "default-true" list that drifted from `config.sh` (claimed Aider default-true; actually false). Removed; relies solely on `config.sh` defaults via `${!flag:-false}`.
- New `scripts/test_all.sh` master runner aggregates all 5 standalone test suites; `npm test` wired in `package.json`. Closes the "tests on disk but not gating CI" gap from v2.11/v2.12 review carryovers.
- `scripts/test_doctor.sh` extended to 31 assertions: + self-exclusion regression, + jq preflight, + schema_version/recommended_command shape.

### v2.12.0
- New persistent user-config dir at `~/.config/ai-consultants/` (XDG-compliant; honors `AI_CONSULTANTS_CONFIG_DIR` and `XDG_CONFIG_HOME`)
- New `lib/user_config.sh` with `load_user_config()` — sourced from `config.sh` at the very top, before any defaults are applied
- `load_user_config` is **idempotent** via a process-wide guard `_AI_CONSULTANTS_USER_CONFIG_LOADED` — `config.sh` is sourced 15-30 times per consultation transitively, and without the guard non-idempotent user config (PATH appends, counters, log appends) would compound silently
- `.env` (KEY=value) and `config.sh` (full bash) both supported; existing env vars always win over user config (precedence: CLI > env > user config > defaults > hardcoded)
- `.env` parser **strips trailing CR** so Windows CR-LF line endings don't silently corrupt values (`ENABLE_DEBATE=true\r` would otherwise break every `[[ "$X" == "true" ]]` comparison downstream)
- New `ai-consultants init [--force]` subcommand scaffolds the user config dir with `.env` (copied from `.env.example`, chmod 600) and `config.sh` (minimal sourceable template); refuses to scaffold into a symlinked dir (root + hostile symlink protection); pre-flights write permission with a friendly error pointing to `AI_CONSULTANTS_CONFIG_DIR`
- `bin/ai-consultants` no longer hardcodes the version (was stuck at 2.10.0 since v2.10.0 release); reads `AI_CONSULTANTS_VERSION` from `scripts/config.sh` and **validates it as semver** — falls back to `vunknown` instead of `vAI_CONSULTANTS_VERSION=2.12.0` when the parse fails
- `get_user_config_dir` returns empty + exit 1 when both `HOME` and `XDG_CONFIG_HOME` are unset (e.g. distroless container) instead of computing the broken path `/.config/ai-consultants`; callers (`doctor.sh`, `bin/init`) handle this with a clear error
- Single source of truth for user-dir resolution: `lib/user_config.sh::get_user_config_dir()`; `routing.sh`, `doctor.sh`, and `bin/ai-consultants` now all source it instead of duplicating the precedence ladder (was DRY-violated 4x)
- `lib/routing.sh::_load_affinity_data` extended with search path: `AFFINITY_FILE` env > `~/.config/ai-consultants/affinity.json` > bundled default — drops a custom matrix in the user dir without setting any env var
- `doctor.sh` adds `check_user_config()` reporting dir presence, files loaded, and warns on `.env` lax permissions — total checks now 22
- New regression test `scripts/test_user_config.sh` — 20 assertions in 11 tests: .env loading + quote stripping, env precedence, edge cases (comments/blanks/`export`/quoted/indented), invalid keys, config.sh ordering, XDG fallback, missing-files silence (now also asserts no stderr output), AI_CONSULTANTS_CONFIG_DIR priority, **CR-LF stripping**, **idempotency guard**, **HOME-unset fallback**
- New `scripts/test_bin.sh` — 10 assertions in 8 tests covering `bin/ai-consultants version` (matches config.sh, semver-shaped, fallback to "unknown" on malformed config) and `init` (chmod 600 enforcement, both files created, idempotent without --force, --force overwrites, refuses symlinked dir)
- `scripts/test_routing_parity.sh` extended to 146 assertions (added user-dir branch of `_resolve_affinity_path`)

### v2.11.0
- Externalized routing affinity matrix from nested `case` statements in `lib/routing.sh` to `references/affinity.json` (~190 lines of bash → 60 lines of JSON)
- Custom matrix at runtime via `AFFINITY_FILE=/path/to/custom.json` (e.g. tweak scores per project, disable consultants by category)
- `get_affinity()` now performs JSON lookup with two-level cache: file content cached on first read, per-(category, consultant) result cached after first lookup
- Cache uses leading-space delimiter to prevent substring collisions (e.g. `DEBUG|X=` would have falsely matched a cached `BUG_DEBUG|X=10` — caught in review pass before release)
- `doctor.sh` adds 3 new checks for affinity: file presence, JSON schema validity, coverage (every consultant in every category) — total checks now 21
- Golden parity test in `scripts/test_routing_parity.sh`: 144 assertions covering all 9 categories × 14 consultants + edge cases (unknown consultant, unknown category, AFFINITY_FILE override, cache auto-invalidation, cache substring-collision regression)
- `docs/SMART_ROUTING.md` rewritten: removed stale per-consultant table (was out of sync with code since v2.8/v2.9/v2.10 added Amp, Kimi, MiniMax), now points to JSON as source of truth and documents schema + override
- `references/affinity.json` `_comment` documents the asymmetric 3-tier fallback (unknown consultant → 5, unknown category → 8, missing cell → 5) and the rationale
- Bug class regression test in `scripts/test_set_e_safety.sh`: static lint covers `((var++))` AND `((var--))` AND `let var++` family, plus dynamic check on bash 4+ for the abort pattern fixed in v2.8.1, v2.10.1, v2.10.9
- `consult_all.sh` ENABLE_PREFLIGHT path no longer swallows doctor output — the diagnostic is now captured to a tmpfile and dumped on failure (previously `>/dev/null 2>&1` repeated the original preflight silent-failure bug)
- Cleaned `((attempt++)) || true || true` artifacts in `lib/api.sh` (introduced by the v2.10.9 mechanical sweep on lines that were already protected)

### v2.10.9
- Fixed silent failure of `preflight_check.sh` under `set -euo pipefail`: helper functions like `check_cli_installed` returned non-zero on missing CLIs, and call sites lacked `|| true` — the script aborted after "Checking CLI installations..." with no diagnostic output
- Deprecated `preflight_check.sh` in favor of `doctor.sh` (stale v2.0 script covering only 6/15 consultants and missing CLI/API mode checks); `preflight_check.sh` is now a thin wrapper that prints a deprecation warning and execs `doctor.sh "$@"`
- Ported `--suggest-config` from preflight to `doctor.sh`, expanded coverage from 6 to 15 consultants (now also detects API-only consultants via API key presence)
- Added `--quick` flag to `doctor.sh` (accepted as no-op for backward compat with preflight)
- Updated `consult_all.sh` ENABLE_PREFLIGHT path to invoke `doctor.sh` directly
- Defensive sweep: protected 15 latent `((var++))` increments across `peer_review.sh`, `setup_wizard.sh`, `test_functions.sh`, `lib/api.sh`, `lib/common.sh`, `lib/reflection.sh`, and `doctor.sh` with `|| true` (matches v2.8.1/v2.10.1 codebase convention; latent because bash 3.2 doesn't abort but bash 4+ does)
- Fixed real shellcheck warnings: SC2059 unsafe printf format string in `lib/progress.sh`, SC2012 `ls | wc -l` race in `install.sh` (replaced with `find`), SC2329 false positive in `query_ollama.sh` cleanup trap (annotated), SC2016 intentional sed pattern in `peer_review.sh` (annotated)

### v2.10.8
- Fixed `docs/cost_rates.json` drift introduced by v2.10.6: `consultant_fallbacks` (used at runtime by `lib/costs.sh`) and `model_tiers` were still pointing at the old IDs (`opus-4.6`, `gpt-5.3-codex`, `composer-1.5`, `deepseek-reasoner`, `sonnet-4.6`, `haiku-4.5`)
- Added price entries for the v2.10.6 model IDs: `claude-opus-4-7`, `claude-sonnet-4-6`, `claude-haiku-4-5`, `gpt-5.5`, `composer-2`, `deepseek-v4-pro`, `nvidia/nemotron-3-super-120b-a12b:free`
- Moved superseded IDs to the legacy section so cost calculation still works for historical responses or pinned overrides
- Synced `COST_RATES.md` tables to match
- Pricing for `nvidia/nemotron-3-super-120b-a12b:free` set to $0/$0 (OpenRouter free tier)

### v2.10.7
- Grok premium upgraded from `grok-4.20-0309-reasoning` to `grok-4.3` (released 2026-04-30)
- ~75% cheaper input ($1.25/M vs $5.00/M) and ~83% cheaper output ($2.50/M vs $15.00/M)
- 1M-token context window
- Moved `grok-4.20-0309-reasoning` to legacy section in cost catalog
- Standard (`grok-3`) and economy (`grok-3-mini`) tiers unchanged

### v2.10.6
- Codex premium upgraded from `gpt-5.3-codex` to `gpt-5.5`
- Cursor premium upgraded from `composer-1.5` to `composer-2`
- Aider switched provider: `gpt-5.3-codex` → `nvidia/nemotron-3-super-120b-a12b:free` (free tier)
- DeepSeek premium upgraded from `deepseek-reasoner` to `deepseek-v4-pro`
- Claude IDs migrated from short aliases to canonical model IDs across all tiers: `opus-4.6` → `claude-opus-4-7`, `sonnet-4.6` → `claude-sonnet-4-6`, `haiku-4.5` → `claude-haiku-4-5`
- Fixed Kilo SIGPIPE abort under `set -euo pipefail` (replaced `head -c` with parameter expansion)

### v2.10.5
- Qwen3 premium model upgraded from `qwen3.5-plus` to `qwen3.6-plus` ($0.325/$1.95 per M tokens)
- Qwen3 standard tier now uses open-weight `qwen3.6-35b-a3b` (MoE, 35B total / 3B active)
- Refactored `get_economic_model()` to delegate to `get_model_for_tier()`, eliminating stale hardcoded mappings
- Moved `qwen3.5-plus` to legacy section in cost catalog
- Fixed `AI_CONSULTANTS_VERSION` in `config.sh` (was stuck at `2.10.0`)

### v2.10.4
- GLM premium/standard model upgraded from `glm-5` to `glm-5.1`
- Fixed Kilo CLI hanging indefinitely in non-TTY mode (query via stdin instead of CLI argument)
- Fixed Kilo CLI picking wrong provider when other consultants' API keys were in the environment
- Collapsed 5-stage ANSI stripping pipeline into single sed invocation in `query_kilo.sh`
- Fixed overly aggressive markdown fence filter in `query_kilo.sh` (only strips standalone ``` lines now)
- Updated `.env.example` GLM signup URL to `open.z.ai`

### v2.10.3
- Grok premium model upgraded to `grok-4.20-0309-reasoning` (replaces `grok-4-1-fast-reasoning`)
- GLM API endpoint migrated from `open.bigmodel.cn` to `api.z.ai/api/coding/paas/v4`
- Removed non-functional MiniMax highspeed models (`MiniMax-M2.7-highspeed`, `MiniMax-M2.5-highspeed`)
- Removed legacy `grok-beta` from cost catalog
- Fixed GLM URL fallback in `common.sh` and `configure.sh` (were still using old endpoint)
- Fixed duplicate `minimax-m2.5` entry with conflicting rates in `cost_rates.json`

### v2.10.2
- MiniMax M2.7 upgrade: premium/standard now use MiniMax-M2.7, economy uses MiniMax-M2.5
- Model tiers: premium (MiniMax-M2.7), standard (MiniMax-M2.7), economy (MiniMax-M2.5)

### v2.10.1
- Slash command quality improvements: file context handling, result presentation templates, error recovery guidance
- debate_round.sh hardening: Amp/Kimi/MiniMax case entries, `((count++)) || true` fixes, `*` default case, stderr to `.err` files, ROUND_NUMBER validation
- Token efficiency: SKILL.md trimmed (-17%), help.md slimmed (-77%), content moved to `references/details.md`
- Self-exclusion consistency in slash command descriptions

### v2.10.0
- MiniMax M2.5 API support via OpenAI-compatible endpoint
- New consultant: MiniMax with "The Pragmatic Optimizer" persona (ID: 21)
- New environment variables: `ENABLE_MINIMAX`, `MINIMAX_API_KEY`, `MINIMAX_MODEL`, `MINIMAX_API_URL`
- Model tiers: premium (MiniMax-M2.5), standard (MiniMax-M2.1), economy (MiniMax-M2.5)
- npx distribution: `npx ai-consultants "question"` (zero dependencies)
- New `bin/ai-consultants` wrapper with symlink resolution and subcommand routing
- Now supports 15 consultants total

### v2.9.1
- Fixed Gemini model names to use real API names
- Premium: `gemini-3.1-pro-preview`, Standard: `gemini-3-flash-preview`, Economy: `gemini-2.0-flash`

### v2.9.0
- Kimi CLI support via kimi-cli (`curl -L code.kimi.com/install.sh | bash`)
- New consultant: Kimi with "The Eastern Sage" persona (ID: 20)
- New environment variables: `ENABLE_KIMI`, `KIMI_CMD`, `KIMI_TIMEOUT`, `KIMI_MODEL`
- Updated doctor.sh diagnostics for Kimi CLI
- Now supports 14 consultants total

### v2.8.1
- CRITICAL: Fixed `((count++))` abort under `set -e` in consult_all.sh and routing.sh
- Fixed missing integer validation for jq confidence values in escalation
- Fixed Amp missing from `_consultant_map` in consult_all.sh
- Fixed hardcoded `"claude"` in synthesize.sh (now uses `$CLAUDE_CMD`)
- Security: Variable name validation before `export` in escalation and cost-aware routing
- DRY: Rewrote `query_kilo.sh` and `query_cursor.sh` using `process_consultant_response()`
- DRY: Added `get_model_for_tier()` as single source of truth for model tier mappings
- Removed hardcoded version numbers from script headers

### v2.8.0
- Amp CLI support via ampcode (`curl -fsSL https://ampcode.com/install.sh | bash`)
- New consultant: Amp with "The Systems Thinker" persona (ID: 19)
- New environment variables: `ENABLE_AMP`, `AMP_CMD`, `AMP_TIMEOUT`
- Updated doctor.sh diagnostics for Amp CLI

### v2.7.0
- Qwen CLI support via qwen-code (`npm install -g @qwen-code/qwen-code@latest`)
- CLI/API mode switching for Qwen3 (now 5 agents support switching)
- New environment variables: `QWEN3_USE_API`, `QWEN3_CMD`
- `QWEN3_USE_API` defaults to `false` to use the qwen CLI by default
- Updated `validate_api_mode()` to support Qwen3
- Moved Qwen3 from API-only to CLI/API switchable consultant
- Updated doctor.sh diagnostics for Qwen3 CLI/API mode

### v2.6.0
- CLI/API mode switching for Gemini, Codex, Claude, and Mistral
- New environment variables: `*_USE_API`, `*_API_URL`
- API request builders for Anthropic and Google AI formats
- Response parsers for all API formats (OpenAI, Anthropic, Google AI)
- New `lib/api_query.sh` module for unified API query execution
- Mode checking functions in `lib/common.sh`
- Doctor diagnostics for CLI/API mode status
- Updated query scripts with CLI/API branching

### v2.5.0
- Model quality tiers: premium, standard, economy
- New `apply_model_tier()` function for programmatic tier selection
- Quality tier presets: `max_quality`, `medium`, `fast`
- Premium model defaults for all consultants (January 2026 models)
- Updated docs/cost_rates.json with tier-based model rates

### v2.4.0
- Budget enforcement (opt-in) with configurable limits
- ENABLE_BUDGET_LIMIT and BUDGET_ACTION configuration
- Budget checks at 4 enforcement points (before/after consultation, debate, synthesis)
- `/ai-consultants:config-budget` slash command
- Updated doctor.sh to display budget status

### v2.3.0
- Semantic caching with fingerprint-based cache keys
- Response length limits (opt-in, per category)
- Cost-aware routing (economic models for simple queries)
- Fallback escalation (premium model if confidence < 7)
- Debate optimization (opt-in, skip if all agree)
- Mandatory debate for SECURITY/ARCHITECTURE categories
- Quality monitoring with `optimization_metrics.json`
- Compact reports (summaries only by default)
- Code simplification (extracted helper functions)

### v2.2.0
- Claude consultant with "The Synthesizer" persona
- Self-exclusion logic (invoking agent excluded from panel)
- Configuration presets (`--preset minimal/balanced/high-stakes/local`)
- Doctor command with auto-fix
- Synthesis strategies (`--strategy majority/risk_averse/security_first`)
- Confidence intervals with statistical ranges
- Anonymous peer review
- Ollama local model support
- Panic button mode for uncertainty detection
- Judge step for overconfidence detection
- One-liner installation

### v2.1.0
- New consultants: Aider, DeepSeek
- 17 configurable personas
- Token optimization with AST extraction

### v2.0.0
- Personas for each consultant (The Architect, The Pragmatist, etc.)
- Confidence scoring 1-10 on every response
- Auto-synthesis with weighted recommendation
- Multi-Agent Debate with cross-critique
- Smart routing based on category
- Session management for follow-up
- Cost tracking and budget limits
- Interactive progress bars
