Ctrl K
ComunidadGuías…

crates/tui/src/lib.rs

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
//! Codewhale TUI library — single-binary entry point.

#![allow(
    clippy::large_enum_variant,
    clippy::too_many_arguments,
    clippy::uninlined_format_args
)]

use codewhale_engine::runtime_options::RuntimeOptions;
use codewhale_engine::{
    fleet, pricing, project_context, receipts, runtime_threads, session_export,
    session_image_prune, session_manager, session_secret_scrub, task_manager,
};
use codewhale_runtime::config;
use std::collections::BTreeSet;
use std::io::{self, IsTerminal, Read, Write};
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::sync::Arc;
use std::time::{Duration, Instant};

use anyhow::{Context, Result, anyhow, bail};
use clap::{Args, CommandFactory, FromArgMatches, Parser, Subcommand, ValueEnum};
use tempfile::NamedTempFile;
use wait_timeout::ChildExt;

use codewhale_dependencies::ExternalTool;

mod commands;
mod composer_history;
mod composer_stash;
#[cfg(test)]
mod conformance;
mod context_report;
/// Guarded installation I/O reusing Engine's confined file primitives.
mod doctor;
mod doctor_fix;
use codewhale_engine::fleet::executor::exec_stream_final_answer_excerpt;
mod integrations;
#[cfg(test)]
mod plugins_dsh_tests;
#[cfg(test)]
mod plugins_provider_tests;
#[cfg(test)]
mod plugins_skill_tests;
#[cfg(test)]
mod sandbox_stay_tests;
// Acceptance matrix for #2934 / #4397. Test-only: the table documents the
// contract for reviewers and is enforced by the tests beside it, so it does
// not need to exist in a shipped binary.
#[cfg(test)]
#[path = "main/tests.rs"]
mod doctor_loader_tests;
#[cfg(test)]
mod session_control_acceptance;
#[cfg(test)]
mod test_support;
mod tui;
/// Portable, dependency-free dot-whale core and conformance helpers.
/// The terminal and external renderers share this implementation.
pub use tui::ambient_life::pet_sim as pet;
pub mod whats_new;

use crate::tui::app::ScreenMode;
use codewhale_app_server::eval::EvalHarness;
use codewhale_app_server::eval::EvalHarnessConfig;
use codewhale_app_server::eval::ScenarioStepKind;
use codewhale_app_server::exec_agent::{
    CliAutoRoute, ExecOutputFormat, ExecStreamEvent, ExecStreamInputAnalysis, ExecStreamMeta,
    ONE_SHOT_DEFAULT_MAX_STEPS, admit_cli_saved_operation, cli_reasoning_effort_value,
    config_for_cli_route, emit_exec_stream_event, exec_max_steps, exec_stream_provider_route,
    exec_stream_session_ref, paths_equal_for_config, resolve_cli_exec_route, run_exec_agent,
    start_binary_sha256, tool_artifact_receipt, validate_exec_tool_authority_resume,
};
use codewhale_config::descriptors::defaults::DEFAULT_TEXT_MODEL;
use codewhale_engine::llm_client::LlmClient;
use codewhale_engine::mcp::{McpCommandAvailability, McpPool};
#[cfg(test)]
use codewhale_engine::session_manager::create_saved_session;
use codewhale_engine::session_manager::{SessionManager, truncate_id};
use codewhale_mcp::config::{McpServerConfig, McpServerOAuthConfig, is_relative_stdio_path_arg};
use codewhale_models::Role;
#[cfg(test)]
use codewhale_models::{ContentBlock, Message, SystemPrompt};
use codewhale_runtime::child_env;
use codewhale_runtime::config::{
    Config, DEFAULT_MAX_SUBAGENTS, MAX_SUBAGENTS, initialize_cloud_facts,
};
use codewhale_runtime::features::{Feature, render_feature_table};
use codewhale_runtime::{logging, startup_trace};

#[cfg(windows)]
fn configure_windows_console_utf8() {
    use windows::Win32::System::Console::{SetConsoleCP, SetConsoleOutputCP};

    const CP_UTF8: u32 = 65001;
    // SAFETY: integer argument only; failures discarded.
    unsafe {
        let _ = SetConsoleCP(CP_UTF8);
        let _ = SetConsoleOutputCP(CP_UTF8);
    }
}

#[cfg(not(windows))]
fn configure_windows_console_utf8() {}

fn install_rustls_crypto_provider() {
    codewhale_release::tls::ensure_rustls_crypto_provider();
}

// The canonical executable owns process arguments. This decoder handles only
// delegated Engine commands and their command-specific interactive flags.
#[derive(Parser, Debug)]
#[command(name = "codewhale", bin_name = "codewhale", version = env!("CODEWHALE_BUILD_VERSION"))]
struct Cli {
    #[command(subcommand)]
    command: Option<Commands>,
    #[command(flatten)]
    options: RuntimeOptions,
    /// Initial prompt to submit in the interactive TUI.
    #[arg(short, long, value_name = "PROMPT", num_args = 1..)]
    prompt: Vec<String>,
    /// Resume a previous session by ID or prefix.
    #[arg(short, long, conflicts_with = "continue_session")]
    resume: Option<String>,
    /// Continue the most recent session in this workspace.
    #[arg(short = 'c', long = "continue", conflicts_with = "resume")]
    continue_session: bool,
    /// Recover the exact durable Resume/Fork intent without repeating its mutation.
    #[arg(long, global = true, value_name = "KEY")]
    operation_key: Option<String>,
}

impl std::ops::Deref for Cli {
    type Target = RuntimeOptions;
    fn deref(&self) -> &Self::Target {
        &self.options
    }
}

#[derive(Subcommand, Debug, Clone)]
#[allow(clippy::large_enum_variant)]
enum Commands {
    /// Run system diagnostics and check configuration
    Doctor(DoctorArgs),
    /// Summarize failure signals from a local JSONL session log without raw content
    SessionDiagnostics(SessionDiagnosticsArgs),
    #[command(about = "Install a local, GitHub or version-pinned npm plugin bundle")]
    Install { source: String },
    #[command(about = "Inspect or restage reviewed plugin bundles")]
    Plugins {
        #[command(subcommand)]
        command: PluginsCommand,
    },
    /// Bootstrap MCP config and/or skills directories
    Setup(SetupArgs),
    /// Generate a remote Codewhale agent deploy bundle (cloud + chat bridge)
    RemoteSetup(codewhale_app_server::remote_setup::RemoteSetupArgs),
    /// List saved sessions, or export one as a full-fidelity archive
    Sessions {
        /// Maximum number of sessions to display
        #[arg(short, long, default_value = "20")]
        limit: usize,
        /// Search sessions by title
        #[arg(short, long)]
        search: Option<String>,
        #[command(subcommand)]
        command: Option<SessionsCommand>,
    },
    /// Show what a session did: files changed, commands run, web and MCP
    /// calls, agents, approvals, and failures, read from its saved record
    #[command(visible_alias = "receipt")]
    Receipts {
        /// Session id or unique prefix, or a Runtime thread id (thr_...).
        /// Omit it (or pass --last) for the most recently updated one.
        #[arg(value_name = "SESSION_ID")]
        id: Option<String>,
        /// Use the most recently updated session or thread
        #[arg(long, conflicts_with = "id")]
        last: bool,
        /// Limit to one turn: a thread's turn id, or a session's turn number
        #[arg(long, value_name = "TURN")]
        turn: Option<String>,
        /// Output format
        #[arg(long, value_enum, default_value = "md")]
        format: receipts::ReceiptFormat,
    },
    /// Create default AGENTS.md in current directory
    Init,
    /// Sign in to your Codewhale account (use the `codewhale` CLI).
    Login {
        /// Legacy provider-key flag: rejected with a redirect to `auth set`.
        #[arg(long, hide = true)]
        api_key: Option<String>,
    },
    /// Remove the saved API key
    Logout,
    /// Manage provider authentication flows.
    Auth(TuiAuthArgs),
    /// List cached models, or update catalogs for configured providers
    Models(ModelsArgs),
    /// Generate speech audio with Xiaomi MiMo TTS models
    #[command(visible_alias = "tts")]
    Speech(SpeechArgs),
    /// Run a non-interactive prompt. Use --auto for agent-with-tools mode.
    Exec(ExecArgs),
    /// Manage local Agent fleet runs and workers (`fleet` is a compatibility alias)
    #[command(name = "fleet")]
    Fleet(FleetArgs),
    /// Internal model-free Workflow tool dispatcher used by Lane Runtime.
    #[command(name = "workflow-tool", hide = true)]
    WorkflowTool(WorkflowToolArgs),
    /// Run a code review over a git diff
    Review(ReviewArgs),
    /// Open the TUI pre-seeded with a GitHub PR's title, body, and diff
    Pr {
        /// PR number
        #[arg(value_name = "NUMBER")]
        number: u32,
        /// Repository in `owner/name` form. Defaults to the current
        /// workspace's `gh` config (i.e. the repo gh thinks you're in).
        #[arg(short = 'R', long)]
        repo: Option<String>,
        /// Skip `gh pr checkout` even if gh is available. By default
        /// the working tree is left as-is — checkout is opt-in via
        /// `--checkout` because dirty trees fail it loudly.
        #[arg(long, default_value_t = false)]
        checkout: bool,
    },
    /// Apply a patch file (or stdin) to the working tree
    Apply(ApplyArgs),
    /// Run the offline evaluation harness (no network/LLM calls)
    Eval(EvalArgs),
    /// Score a run's token/cache/cost from recorded turns; flag regressions vs a baseline
    Scorecard(ScorecardArgs),
    /// Manage MCP servers
    Mcp {
        #[command(subcommand)]
        command: McpCommand,
    },
    /// Inspect feature flags
    Features(FeaturesCli),
    /// Connect third-party harnesses through Codewhale (currently: DeepSeek Harness `dsh`)
    Integrations {
        #[command(subcommand)]
        command: IntegrationsCommand,
    },
    /// Run a command inside the sandbox
    Sandbox(SandboxArgs),
    /// Run a local server (e.g. MCP)
    Serve(ServeArgs),
    /// Resume a previous session by ID (use --last for most recent)
    Resume {
        /// Conversation/session id (UUID or prefix)
        #[arg(value_name = "SESSION_ID")]
        session_id: Option<String>,
        /// Continue the most recent session in this workspace without a picker
        #[arg(long = "last", default_value_t = false, conflicts_with = "session_id")]
        last: bool,
    },
    /// Fork a previous session by ID (use --last for most recent)
    Fork {
        /// Conversation/session id (UUID or prefix)
        #[arg(value_name = "SESSION_ID")]
        session_id: Option<String>,
        /// Fork the most recent session in this workspace without a picker
        #[arg(long = "last", default_value_t = false, conflicts_with = "session_id")]
        last: bool,
    },
}

#[derive(Subcommand, Debug, Clone)]
enum PluginsCommand {
    #[command(about = "Inspect plugin status and diagnostics", alias = "list")]
    Status {
        #[arg(value_name = "NAME")]
        name: Option<String>,
        #[arg(long)]
        json: bool,
    },
    #[command(about = "Restore an eligible reviewed plugin snapshot")]
    Restage {
        #[arg(value_name = "NAME")]
        name: String,
        #[arg(long)]
        json: bool,
    },
}

/// Subcommands of `codewhale sessions`. Without one, the command falls back
/// to listing sessions.
#[derive(Subcommand, Debug, Clone)]
enum SessionsCommand {
    /// List saved sessions (default when no subcommand is given)
    List {
        /// Maximum number of sessions to display
        #[arg(short, long, default_value = "20")]
        limit: usize,
        /// Search sessions by title
        #[arg(short, long)]
        search: Option<String>,
    },
    /// Mask credentials that older builds stored in saved sessions' tool
    /// output. Reports what it would change unless `--apply` is given. Run it
    /// while no Codewhale session is open.
    ScrubSecrets {
        /// Rewrite the affected session files (default: report only)
        #[arg(long, default_value_t = false)]
        apply: bool,
    },
    /// Delete pasted clipboard images that no saved session, draft or history
    /// still names. Reports what it would delete unless `--apply` is given.
    /// Run it while no Codewhale session is open.
    PruneImages {
        /// Delete the listed images (default: report only)
        #[arg(long, default_value_t = false)]
        apply: bool,
        /// Keep unreferenced images newer than this many days
        #[arg(long, value_name = "DAYS", default_value_t = 7)]
        older_than_days: u64,
    },
    /// Export a session as a full-fidelity tar.xz archive (complete context:
    /// system prompt, messages, tool calls and results, plus artifacts)
    Export {
        /// Session id (or unambiguous id prefix) to export
        #[arg(value_name = "SESSION_ID")]
        id: String,
        /// Destination .tar.xz path (default: codewhale-session-<id>.tar.xz)
        #[arg(short, long, value_name = "PATH")]
        output: Option<PathBuf>,
        /// Exclude the session artifacts directory from the archive
        #[arg(long, default_value_t = false)]
        skip_artifacts: bool,
        /// xz compression preset, 0 (fastest) through 9 (smallest)
        #[arg(long, default_value_t = session_export::DEFAULT_XZ_COMPRESSION_LEVEL)]
        compression: u32,
        /// Overwrite the destination file if it already exists
        #[arg(long, default_value_t = false)]
        force: bool,
    },
}

#[derive(Args, Debug, Clone)]
#[command(after_help = codewhale_localization::tr(
    codewhale_localization::resolve_locale("auto"),
    codewhale_localization::MessageId::ExecAfterHelp,
))]
struct ExecArgs {
    /// Override model for this run
    #[arg(long)]
    model: Option<String>,
    /// Override the provider for this run (e.g. `deepseek`, `openrouter`).
    /// Non-secret identifier only — credentials still resolve from the
    /// environment/config. Fleet uses this to launch a worker on its
    /// profile-pinned provider even when the parent session is on another
    /// one (#4093).
    #[arg(long)]
    provider: Option<String>,
    /// Override reasoning/thinking effort for this run.
    /// Accepted values: auto, off, low, medium, high, max.
    #[arg(long = "reasoning-effort", value_name = "EFFORT")]
    reasoning_effort: Option<String>,
    #[arg(long, default_value_t = false, help = codewhale_localization::tr(
        codewhale_localization::resolve_locale("auto"),
        codewhale_localization::MessageId::ExecAutoHelp,
    ))]
    auto: bool,
    #[arg(long, value_name = "POLICY", help = codewhale_localization::tr(
        codewhale_localization::resolve_locale("auto"),
        codewhale_localization::MessageId::ExecSandboxHelp,
    ))]
    sandbox: Option<String>,
    /// Explicitly allow a denied tool to retry with danger-full-access.
    #[arg(long, default_value_t = false)]
    allow_sandbox_elevation: bool,
    /// Emit machine-readable JSON output
    #[arg(long, default_value_t = false, conflicts_with = "output_format")]
    json: bool,
    /// Resume a previous session by ID or prefix
    #[arg(long, value_name = "SESSION_ID", conflicts_with_all = ["session_id", "continue_session"])]
    resume: Option<String>,
    /// Resume a previous session by ID or prefix
    #[arg(long = "session-id", value_name = "SESSION_ID", conflicts_with_all = ["resume", "continue_session"])]
    session_id: Option<String>,
    /// Continue the most recent session for this workspace
    #[arg(long = "continue", default_value_t = false, conflicts_with_all = ["resume", "session_id"])]
    continue_session: bool,
    /// Output format for exec mode
    #[arg(long, value_enum, default_value_t = ExecOutputFormat::Text)]
    output_format: ExecOutputFormat,
    /// Comma-separated list of canonical tools to allow (all others denied).
    /// Names are case-insensitive: Bash, File, Git, Run, etc.
    #[arg(long, value_delimiter = ',')]
    allowed_tools: Option<Vec<String>>,
    /// Comma-separated list of tools to deny (deny wins over allow).
    #[arg(long, value_delimiter = ',')]
    disallowed_tools: Option<Vec<String>>,
    /// Maximum number of model steps before the run ends. Omitted means unlimited.
    #[arg(long, value_parser = clap::value_parser!(u32).range(1..))]
    max_turns: Option<u32>,
    /// Maximum number of tool calls admitted in one model turn. Omitted means unlimited.
    #[arg(long, value_parser = clap::value_parser!(u32).range(1..))]
    max_tool_calls: Option<u32>,
    /// Shut down when the parent closes this process's stdin. Fleet workers
    /// pass this automatically: a dead manager must not leave detached workers
    /// spending forever (R7).
    #[arg(long, default_value_t = false)]
    parent_death_watch: bool,
    /// Extra text appended to the system prompt for this run.
    #[arg(long)]
    append_system_prompt: Option<String>,
    /// Internal Fleet worker authority envelope. Non-secret, versioned JSON.
    #[arg(long, value_name = "JSON", hide = true)]
    tool_authority_json: Option<String>,
    /// Fire the configured hooks in this run (opt-in). On the headless path
    /// the engine-side events are `tool_call_before` — which may still deny
    /// a call — and `shell_env`. A hook `ask` resolves fail-closed because
    /// nothing can prompt headlessly. Fleet worker subprocesses never fire
    /// operator hooks.
    #[arg(long, default_value_t = false)]
    hooks: bool,
    /// Run one answer with no tools. The default offers the normal tool
    /// surface under the configured sandbox and approval posture.
    #[arg(long, default_value_t = false)]
    no_tools: bool,
    /// Additional directory the file tools may read and write (repeatable).
    #[arg(long = "add-dir", value_name = "PATH")]
    add_dir: Vec<PathBuf>,
    /// Read the prompt from a file (`-` reads stdin) instead of argv, for
    /// prompts past the OS per-argument limit (~128 KiB on Linux).
    #[arg(long, value_name = "PATH", conflicts_with = "prompt")]
    prompt_file: Option<PathBuf>,
    #[arg(
        value_name = "PROMPT",
        trailing_var_arg = true,
        help = codewhale_localization::tr(
            codewhale_localization::resolve_locale("auto"),
            codewhale_localization::MessageId::ExecPromptHelp,
        )
    )]
    prompt: Vec<String>,
}

fn parse_cli_args(args: &[impl AsRef<str>]) -> Result<Cli, clap::Error> {
    let mut command = Cli::command();
    let matches = command.try_get_matches_from_mut(args.iter().map(|arg| arg.as_ref()))?;
    let cli = Cli::from_arg_matches(&matches)?;
    if let Some(Commands::Exec(exec)) = &cli.command {
        let prompt_start = args.len().saturating_sub(exec.prompt.len());
        if !args[..prompt_start]
            .last()
            .is_some_and(|arg| arg.as_ref() == "--")
        {
            let command = command.find_subcommand_mut("exec").expect("exec command");
            for word in exec.prompt.iter().take_while(|word| *word != "--") {
                let word = ascii_flag_spelling(word);
                let flag = word.split_once('=').map_or(word.as_str(), |(flag, _)| flag);
                if flag.strip_prefix("--").is_some_and(|name| {
                    command.get_arguments().any(|arg| {
                        arg.get_long() == Some(name)
                            || arg
                                .get_all_aliases()
                                .is_some_and(|aliases| aliases.contains(&name))
                    })
                }) {
                    return Err(command.error(
                        clap::error::ErrorKind::UnknownArgument,
                        codewhale_localization::tr(
                            codewhale_localization::resolve_locale("auto"),
                            codewhale_localization::MessageId::ExecFlagAfterPrompt,
                        )
                        .replace("{flag}", flag),
                    ));
                }
            }
        }
    }
    Ok(cli)
}

fn ascii_flag_spelling(word: &str) -> String {
    let typographic = |c: char| matches!(c, '\u{2010}'..='\u{2015}' | '\u{2212}' | '\u{FF0D}');
    let folded: String = word
        .chars()
        .map(|c| match c {
            '\u{FF01}'..='\u{FF5E}' => char::from_u32(c as u32 - 0xFEE0).unwrap_or(c),
            c if typographic(c) => '-',
            c => c,
        })
        .collect();
    let single_typographic = word.chars().next().is_some_and(typographic)
        && !word.chars().nth(1).is_some_and(typographic);
    if single_typographic && folded.len() > 1 {
        format!("-{folded}")
    } else {
        folded
    }
}

fn headless_exec_args(
    resume: Option<String>,
    continue_session: bool,
    prompt: &[String],
) -> Result<ExecArgs> {
    use clap::{Args as _, FromArgMatches as _};
    let mut argv: Vec<String> = vec!["exec".to_string()];
    if let Some(id) = &resume {
        argv.push("--resume".to_string());
        argv.push(id.clone());
    }
    if continue_session {
        argv.push("--continue".to_string());
    }
    if !prompt.is_empty() {
        argv.push("--".to_string());
        argv.extend(prompt.iter().cloned());
    }
    let command = ExecArgs::augment_args(clap::Command::new("exec"));
    let matches = command
        .try_get_matches_from(argv)
        .map_err(|error| anyhow!(error.to_string()))?;
    ExecArgs::from_arg_matches(&matches).map_err(|error| anyhow!(error.to_string()))
}

#[derive(Args, Debug, Clone)]
struct WorkflowToolArgs {
    /// Authority provenance stamped by the public `workflow run` command.
    #[arg(long, value_name = "SOURCE")]
    approval_source: String,
    /// Exact Workflow tool input serialized as one JSON object.
    #[arg(long, value_name = "JSON")]
    input_json: String,
}

#[derive(Args, Debug, Clone)]
struct TuiAuthArgs {
    #[command(subcommand)]
    command: TuiAuthCommand,
}

#[derive(Subcommand, Debug, Clone)]
enum TuiAuthCommand {
    /// Sign in to xAI/Grok with an SSH-friendly device code; run again to switch accounts.
    #[command(name = "xai-device")]
    XaiDevice,
    /// Sign in with ChatGPT for Codex subscription access; run again to switch accounts.
    #[command(name = "chatgpt")]
    Chatgpt,
    /// Revoke Codewhale-owned ChatGPT tokens. Codex CLI consent is unchanged.
    #[command(name = "chatgpt-revoke")]
    ChatgptRevoke,
    /// Sign in to OrcaRouter with OAuth 2.0 + PKCE; run again to switch accounts.
    #[command(name = "orcarouter")]
    Orcarouter,
    /// Revoke the saved OrcaRouter credential. The OrcaRouter console also
    /// revokes every key it issued to this app in one click.
    #[command(name = "orcarouter-revoke")]
    OrcarouterRevoke,
    /// Sign in to a provider contributed by an enabled, reviewed plugin.
    PluginLogin {
        #[arg(long)]
        provider: String,
    },
    /// Remove credentials for one plugin provider without changing trust.
    PluginLogout {
        #[arg(long)]
        provider: String,
    },
}

const CODEWHALE_TOOL_SURFACE_ENV: &str = "CODEWHALE_TOOL_SURFACE";
const SHELL_ONLY_EXEC_TOOLS: &[&str] = &["bash"];

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ExecToolSurface {
    ShellOnly,
}

fn exec_tool_surface_from_env() -> Option<ExecToolSurface> {
    std::env::var(CODEWHALE_TOOL_SURFACE_ENV)
        .ok()
        .and_then(|value| {
            if should_warn_unknown_exec_tool_surface(&value) {
                eprintln!(
                    "warning: unrecognized {CODEWHALE_TOOL_SURFACE_ENV}; leaving exec tool surface unchanged. Use `shell-only`, `full`, or `native-tools`."
                );
            }
            parse_exec_tool_surface(&value)
        })
}

fn parse_exec_tool_surface(value: &str) -> Option<ExecToolSurface> {
    match value.trim().to_ascii_lowercase().as_str() {
        "shell-only" | "shell_only" | "shell" => Some(ExecToolSurface::ShellOnly),
        "full" | "native-tools" | "native_tools" | "" => None,
        _ => None,
    }
}

fn should_warn_unknown_exec_tool_surface(value: &str) -> bool {
    let normalized = value.trim().to_ascii_lowercase();
    !matches!(
        normalized.as_str(),
        "" | "shell-only" | "shell_only" | "shell" | "full" | "native-tools" | "native_tools"
    )
}

fn normalize_exec_tool_names(tools: &[String]) -> Vec<String> {
    tools
        .iter()
        .map(|name| name.to_ascii_lowercase().trim().to_string())
        .collect()
}

fn shell_only_exec_allowed_tools() -> Vec<String> {
    SHELL_ONLY_EXEC_TOOLS
        .iter()
        .map(|name| (*name).to_string())
        .collect()
}

/// Whether an `exec` run is offered tools. The default is the normal tool
/// surface, still approval-gated by the configured sandbox. `--no-tools` is
/// the explicit one-shot. A resumed session and a Fleet authority envelope
/// keep their own surface.
fn exec_grants_tool_surface(
    args: &ExecArgs,
    _yolo: bool,
    _resuming: bool,
    _env_tool_surface: bool,
) -> bool {
    !args.no_tools
}

/// Flags that only shape a tool surface, passed on a run that has none.
fn exec_tool_flags_without_grant(args: &ExecArgs) -> Vec<&'static str> {
    [
        (args.max_tool_calls.is_some(), "--max-tool-calls"),
        (args.disallowed_tools.is_some(), "--disallowed-tools"),
        (args.sandbox.is_some(), "--sandbox"),
        (args.allow_sandbox_elevation, "--allow-sandbox-elevation"),
        (args.hooks, "--hooks"),
    ]
    .into_iter()
    .filter_map(|(passed, flag)| passed.then_some(flag))
    .collect()
}

fn resolve_exec_allowed_tools(
    cli_allowed_tools: Option<&[String]>,
    env_tool_surface: Option<ExecToolSurface>,
) -> Option<Vec<String>> {
    if let Some(tools) = cli_allowed_tools {
        return Some(normalize_exec_tool_names(tools));
    }

    env_tool_surface.map(|ExecToolSurface::ShellOnly| shell_only_exec_allowed_tools())
}

#[derive(Args, Debug, Clone)]
struct FleetArgs {
    #[command(subcommand)]
    command: FleetCommand,
}

#[derive(Subcommand, Debug, Clone)]
enum FleetCommand {
    /// Initialize the local fleet ledger for this workspace
    Init,
    /// Create a run from a task spec and start the foreground manager loop
    Run(FleetRunArgs),
    /// List durable fleet runs from this workspace's ledger
    List,
    /// Show queued/running/completed/failed/stale fleet counts
    Status,
    /// Inspect one worker's status, heartbeat, latest event, and artifacts
    Inspect {
        /// Worker id printed by `codewhale fleet run`
        worker_id: String,
    },
    /// Print bounded log artifacts for one worker
    Logs {
        /// Worker id printed by `codewhale fleet run`
        worker_id: String,
    },
    /// List artifact refs for one worker
    Artifacts {
        /// Worker id printed by `codewhale fleet run`
        worker_id: String,
    },
    /// Interrupt a running worker task and record a terminal cancellation
    Interrupt {
        /// Worker id printed by `codewhale fleet run`
        worker_id: String,
    },
    /// Restart the latest task for a worker
    Restart {
        /// Worker id printed by `codewhale fleet run`
        worker_id: String,
    },
    /// Resume a run from durable ledger state, reconciling orphaned/stale leases
    Resume {
        /// Run id printed by `codewhale fleet run`
        run_id: String,
        /// Seconds without heartbeat before a leased task is treated as stale
        #[arg(long, default_value_t = 300)]
        stale_after_seconds: u64,
    },
    /// Stop all queued and running fleet work
    Stop {
        /// Confirm stopping all queued and running fleet tasks
        #[arg(long, required = true)]
        all: bool,
    },
    /// Render a redacted fleet alert payload without sending it
    AlertDryRun(FleetAlertDryRunArgs),
}

#[derive(Args, Debug, Clone)]
struct FleetRunArgs {
    /// JSON or TOML task spec to enqueue
    #[arg(value_name = "TASK_SPEC")]
    task_spec: PathBuf,
    /// Maximum workers to lease concurrently
    #[arg(long, default_value_t = 4)]
    max_workers: usize,
    #[arg(
        long = "require-placement",
        help = "Require environment placement support; older Engines refuse this flag"
    )]
    _require_placement: bool,
    /// Seconds without heartbeat before a running task is counted stale
    #[arg(long, default_value_t = 300)]
    stale_after_seconds: u64,
    /// Schedule once and return instead of staying in the manager loop
    #[arg(long, hide = true, default_value_t = false)]
    once: bool,
    /// Validate the spec (shape, roster members, profiles, model routes)
    /// without creating a run or starting any worker
    #[arg(long, default_value_t = false)]
    check: bool,
}

#[derive(Args, Debug, Clone)]
struct FleetAlertDryRunArgs {
    /// Alert event class to render
    #[arg(long, value_enum)]
    event: FleetAlertEventArg,
    /// fleet run id
    #[arg(long)]
    run_id: String,
    /// Worker id, when the event belongs to one worker
    #[arg(long)]
    worker_id: Option<String>,
    /// Task id, when the event belongs to one task
    #[arg(long)]
    task_id: Option<String>,
    /// Short human-readable reason for the alert
    #[arg(long, default_value = "manual fleet alert dry-run")]
    reason: String,
    /// Status label to include in the payload
    #[arg(long)]
    status: Option<String>,
    /// Adapter payload shape to render
    #[arg(long, value_enum, default_value_t = FleetAlertAdapterArg::Slack)]
    adapter: FleetAlertAdapterArg,
    /// Environment variable containing the Slack webhook URL
    #[arg(long, default_value = "CODEWHALE_FLEET_SLACK_WEBHOOK")]
    slack_webhook_env: String,
    /// Environment variable containing the generic webhook URL
    #[arg(long, default_value = "CODEWHALE_FLEET_WEBHOOK_URL")]
    webhook_url_env: String,
    /// Optional environment variable containing the generic webhook secret
    #[arg(long)]
    webhook_secret_env: Option<String>,
    /// Environment variable containing the PagerDuty routing key
    #[arg(long, default_value = "CODEWHALE_FLEET_PAGERDUTY_ROUTING_KEY")]
    pagerduty_routing_key_env: String,
    /// PagerDuty severity to render
    #[arg(long, default_value = "error")]
    pagerduty_severity: String,
}

#[derive(ValueEnum, Debug, Clone, Copy)]
enum FleetAlertEventArg {
    Stale,
    RestartExhausted,
    NeedsHuman,
    BudgetExceeded,
    VerifierFailed,
    RunCompleted,
}

#[derive(ValueEnum, Debug, Clone, Copy)]
enum FleetAlertAdapterArg {
    Slack,
    Webhook,
    PagerDuty,
}

/// Spawn a tokio task that listens for terminating signals (SIGINT, SIGTERM
/// and SIGHUP on Unix; Ctrl+C, Ctrl+Break and console close on Windows), gives
/// the interactive owner and persistence actor a bounded recovery window,
/// restores terminal modes and exits with the signal cancellation code.
/// Multiple deliveries are tolerated: once the cleanup runs, a second
/// signal short-circuits to plain exit so a stuck cleanup can never
/// trap a frustrated user pressing Ctrl+C repeatedly.
///
/// See the call site in `main` for the rationale (#1583).
///
/// Registration is synchronous, before the spawn: a `tokio::spawn`ed task does
/// not run until the scheduler first polls it, so registering the signal
/// streams *inside* it leaves a window — unbounded under load — where SIGINT
/// still has its default disposition and kills the process outright. That is
/// the very outcome this handler exists to prevent, and it produced a real
/// terminated-by-signal exit (no code, no terminal restore, no `session_end`).
/// After this function returns, the signals are armed.
fn spawn_signal_cleanup_task() {
    let mut signals = TerminatingSignals::register();
    tokio::spawn(async move {
        let exit_code = signals.wait().await;
        // Persistence and a serving Runtime API share a bounded window to
        // flush queued saves and end open streams with a typed `stream.end`.
        // A second signal skips both waits.
        tokio::select! {
            _ = async {
                tokio::join!(
                    codewhale_app_server::runtime_api::drain_for_signal_exit(),
                    crate::tui::persistence_actor::flush_for_signal_exit(),
                )
            } => {}
            _ = signals.wait() => {}
        }
        // If we get here a fatal signal arrived. Restore the terminal
        // and exit. A second signal during cleanup re-enters this
        // path and aborts via `std::process::exit` directly.
        static CLEANED_UP: std::sync::atomic::AtomicBool =
            std::sync::atomic::AtomicBool::new(false);
        if !CLEANED_UP.swap(true, std::sync::atomic::Ordering::SeqCst) {
            #[cfg(unix)]
            codewhale_engine::tools::shell::abort_pending_persistent_process_groups_for_exit();
            #[cfg(unix)]
            codewhale_runtime::process_tree::kill_contained_trees_for_exit();
            crate::tui::ui::emergency_restore_terminal();
            // Nothing async survives the `exit` below, so this is the last
            // chance to say how the session ended. `record_blocking` is one
            // `O_APPEND` write with no lock: taking the compaction lock here
            // would let a second Codewhale process sharing CODEWHALE_HOME hang
            // Ctrl-C, and the second-signal short-circuit below has to stay
            // reachable. A no-op unless this process was armed.
            //
            // The class is stated, not derived: `RunTerminationReason::Canceled`
            // also exits 130, so `exit_code` cannot tell a signal from an
            // Esc-cancelled turn.
            record_signal_session_end();
        }
        std::process::exit(exit_code);
    });
}

/// When this process's armed telemetry session began. Set once, at arming, and
/// read from both the ordinary teardown and the signal path.
static TELEMETRY_SESSION_START: std::sync::OnceLock<std::time::Instant> =
    std::sync::OnceLock::new();

/// Build `session_end` from what this process actually accumulated.
///
/// The exit class is read from the process-wide atomic and never derived from
/// an exit code: `RunTerminationReason::Canceled` maps to 130, the same value
/// the SIGINT path uses, so a code-based derivation would report every
/// Esc-cancelled turn as a signal.
///
/// The cold-start bucket is `None` unless the interactive event loop actually
/// began, which is what keeps it absent rather than invented on the surfaces
/// that have no event loop.
fn telemetry_session_end() -> codewhale_telemetry::Event {
    let counters = codewhale_telemetry::session_counters();
    codewhale_telemetry::Event::SessionEnd {
        duration_bucket: codewhale_telemetry::DurationBucket::from_secs(
            TELEMETRY_SESSION_START
                .get()
                .map_or(0, |start| start.elapsed().as_secs()),
        ),
        exit_class: codewhale_telemetry::exit_class(),
        cold_start_bucket: codewhale_runtime::startup_trace::cold_start_ms()
            .map(codewhale_telemetry::ColdStartBucket::from_millis),
        providers: counters.providers(),
        counters: counters.counters(),
        errors: counters.errors(),
        turn_wall: counters.turn_wall(),
    }
}

/// Close the session synchronously, from the signal handler.
///
/// A no-op unless this process was armed.
fn record_signal_session_end() {
    codewhale_telemetry::set_exit_class(codewhale_telemetry::ExitClass::Signal);
    for event in codewhale_telemetry::session_counters().usage_events() {
        codewhale_telemetry::record_blocking(event);
    }
    codewhale_telemetry::record_blocking(telemetry_session_end());
}

/// Terminating-signal streams, registered up front and awaited later.
///
/// Splitting registration from the await is the point: the OS disposition
/// changes when `register` returns, not when the waiting task is first polled.
#[cfg(unix)]
struct TerminatingSignals {
    sigint: Option<tokio::signal::unix::Signal>,
    sigterm: Option<tokio::signal::unix::Signal>,
    sighup: Option<tokio::signal::unix::Signal>,
}

#[cfg(unix)]
impl TerminatingSignals {
    /// Install the handlers. Failing to install any individual stream is
    /// non-fatal: we still want the others to work.
    fn register() -> Self {
        use tokio::signal::unix::{SignalKind, signal};
        Self {
            sigint: signal(SignalKind::interrupt()).ok(),
            sigterm: signal(SignalKind::terminate()).ok(),
            sighup: signal(SignalKind::hangup()).ok(),
        }
    }

    /// Resolve with 128 + signal number for whichever arrives first. The
    /// fallback never-resolving future keeps `select!` well-typed when a
    /// stream failed to register.
    async fn wait(&mut self) -> i32 {
        tokio::select! {
            _ = async { match self.sigint.as_mut() { Some(s) => { s.recv().await; }, None => std::future::pending::<()>().await, } } => 130,
            _ = async { match self.sigterm.as_mut() { Some(s) => { s.recv().await; }, None => std::future::pending::<()>().await, } } => 143,
            _ = async { match self.sighup.as_mut() { Some(s) => { s.recv().await; }, None => std::future::pending::<()>().await, } } => 129,
        }
    }
}

/// Windows console events share the bounded cleanup path. Logoff and shutdown
/// notifications are not handled here.
#[cfg(not(unix))]
struct TerminatingSignals {
    ctrl_c: Option<tokio::signal::windows::CtrlC>,
    ctrl_break: Option<tokio::signal::windows::CtrlBreak>,
    ctrl_close: Option<tokio::signal::windows::CtrlClose>,
}

#[cfg(not(unix))]
impl TerminatingSignals {
    fn register() -> Self {
        Self {
            ctrl_c: tokio::signal::windows::ctrl_c().ok(),
            ctrl_break: tokio::signal::windows::ctrl_break().ok(),
            ctrl_close: tokio::signal::windows::ctrl_close().ok(),
        }
    }

    async fn wait(&mut self) -> i32 {
        tokio::select! {
            _ = async { match self.ctrl_c.as_mut() { Some(s) => { s.recv().await; }, None => std::future::pending::<()>().await, } } => {},
            _ = async { match self.ctrl_break.as_mut() { Some(s) => { s.recv().await; }, None => std::future::pending::<()>().await, } } => {},
            _ = async { match self.ctrl_close.as_mut() { Some(s) => { s.recv().await; }, None => std::future::pending::<()>().await, } } => {},
        }
        130
    }
}

fn join_prompt_parts(parts: &[String]) -> String {
    parts.join(" ")
}

/// Maximum bytes accepted for an exec prompt read from `--prompt-file` or
/// stdin. Far past argv's per-argument ceiling; the model's context window
/// stays the real limit (#6688).
const MAX_EXEC_PROMPT_BYTES: u64 = 64 * 1024 * 1024;

/// The effective exec prompt: argv words, a `--prompt-file`, or piped stdin.
/// A positional `-` stays literal text. When stdin is not a terminal and no
/// `--prompt-file` is set, piped stdin is the prompt, or is appended to the
/// argv prompt when it delivers data within `EXEC_STDIN_GRACE`.
/// `--parent-death-watch` owns stdin and does not read it as a prompt.
fn resolve_exec_prompt(args: &ExecArgs) -> Result<String> {
    let Some(path) = &args.prompt_file else {
        return resolve_argv_or_piped_prompt(args);
    };
    let prompt = if path.as_os_str() == "-" {
        if args.parent_death_watch {
            bail!("--parent-death-watch owns stdin; pass the prompt with --prompt-file <PATH>");
        }
        let stdin = io::stdin();
        if stdin.is_terminal() {
            bail!("--prompt-file - reads stdin, but stdin is a terminal; pipe the prompt in.");
        }
        read_capped_text(stdin.lock(), MAX_EXEC_PROMPT_BYTES, "exec prompt on stdin")?
    } else {
        let file = std::fs::File::open(path)
            .with_context(|| format!("failed to open --prompt-file {}", path.display()))?;
        read_capped_text(file, MAX_EXEC_PROMPT_BYTES, "--prompt-file")
            .with_context(|| format!("failed to read --prompt-file {}", path.display()))?
    };
    if prompt.trim().is_empty() {
        let source = if path.as_os_str() == "-" {
            "stdin".to_string()
        } else {
            path.display().to_string()
        };
        bail!("exec prompt from {source} is empty");
    }
    Ok(prompt)
}

const EXEC_STDIN_GRACE: Duration = Duration::from_secs(2);

fn read_stdin_context(grace: Duration) -> Result<Option<String>> {
    let (tx, rx) = std::sync::mpsc::channel::<Result<Option<String>, String>>();
    let (started_tx, started_rx) = std::sync::mpsc::channel::<()>();
    std::thread::spawn(move || {
        let mut stdin = io::stdin().lock();
        let mut first = [0u8; 1];
        let read = loop {
            match stdin.read(&mut first) {
                Err(error) if error.kind() == io::ErrorKind::Interrupted => continue,
                other => break other,
            }
        };
        let _ = started_tx.send(());
        let outcome = match read {
            Ok(0) => Ok(Some(String::new())),
            Ok(_) => read_capped_text(
                io::Cursor::new(first).chain(stdin),
                MAX_EXEC_PROMPT_BYTES,
                "exec prompt on stdin",
            )
            .map(Some)
            .map_err(|error| error.to_string()),
            Err(error) => Err(error.to_string()),
        };
        let _ = tx.send(outcome);
    });
    if started_rx.recv_timeout(grace).is_err() {
        return Ok(None);
    }
    match rx.recv() {
        Ok(Ok(piped)) => Ok(piped),
        Ok(Err(message)) => bail!("{message}"),
        Err(_) => bail!("stdin reader stopped unexpectedly"),
    }
}

fn resolve_argv_or_piped_prompt(args: &ExecArgs) -> Result<String> {
    let argv = join_prompt_parts(&args.prompt);
    if args.parent_death_watch || io::stdin().is_terminal() {
        if argv.trim().is_empty() {
            bail!("missing exec prompt; pass it as arguments, --prompt-file, or a pipe");
        }
        return Ok(argv);
    }
    let piped = if argv.trim().is_empty() {
        read_capped_text(
            io::stdin().lock(),
            MAX_EXEC_PROMPT_BYTES,
            "exec prompt on stdin",
        )?
    } else {
        match read_stdin_context(EXEC_STDIN_GRACE)? {
            Some(piped) => piped,
            None => {
                eprintln!(
                    "note: stdin produced no data within {}s; running with the prompt argument alone. To wait for a slow producer, pipe the prompt too: {{ echo \"<prompt>\"; cmd; }} | codewhale exec.",
                    EXEC_STDIN_GRACE.as_secs()
                );
                String::new()
            }
        }
    };
    if argv.trim().is_empty() {
        if piped.trim().is_empty() {
            bail!("exec prompt from stdin is empty");
        }
        return Ok(piped);
    }
    if piped.trim().is_empty() {
        return Ok(argv);
    }
    Ok(format!("{argv}\n\n{piped}"))
}

fn resolve_exec_model(config: &Config, explicit_model: Option<&str>) -> String {
    explicit_model
        .map(str::trim)
        .filter(|model| !model.is_empty())
        .map(ToOwned::to_owned)
        .or_else(exec_model_env_override)
        .unwrap_or_else(|| config.default_model())
}

fn apply_exec_provider_override(config: &mut Config, provider_arg: &str) -> Result<()> {
    let provider_arg = provider_arg.trim();
    if provider_arg.is_empty() {
        return Ok(());
    }
    if config
        .providers
        .as_ref()
        .and_then(|providers| providers.custom_provider_config(provider_arg))
        .is_some()
    {
        config.provider = Some(provider_arg.to_string());
        return Ok(());
    }
    if let Some(provider) = codewhale_config::ProviderKind::parse(provider_arg) {
        config.provider = Some(provider.as_str().to_string());
        return Ok(());
    }
    bail!(
        "Unrecognized --provider {provider_arg:?}. Known providers: {} \
         or a configured [providers.<name>] custom provider",
        codewhale_config::ProviderKind::names_hint()
    );
}

fn exec_model_env_override() -> Option<String> {
    let read = || {
        ["CODEWHALE_MODEL", "DEEPSEEK_MODEL"]
            .into_iter()
            .find_map(|key| {
                std::env::var(key)
                    .ok()
                    .map(|model| model.trim().to_string())
                    .filter(|model| !model.is_empty())
            })
    };
    #[cfg(test)]
    {
        crate::test_support::with_test_env_lock(read)
    }
    #[cfg(not(test))]
    {
        read()
    }
}

fn top_level_prompt_initial_input(parts: &[String]) -> Option<tui::InitialInput> {
    (!parts.is_empty()).then(|| tui::InitialInput::Submit(join_prompt_parts(parts)))
}

fn resolve_exec_resume_session_id(args: &ExecArgs, workspace: &Path) -> Result<Option<String>> {
    if let Some(id) = args.resume.as_ref().or(args.session_id.as_ref()) {
        return Ok(Some(id.clone()));
    }
    if !args.continue_session {
        return Ok(None);
    }
    latest_session_id_for_workspace(workspace)?.map_or_else(
        || {
            bail!(
                "No saved sessions found for workspace {}. Use `codewhale sessions` to list sessions, or pass `codewhale exec --resume <SESSION_ID> ...`.",
                workspace.display()
            )
        },
        |id| Ok(Some(id)),
    )
}

fn load_exec_resume_session(session_id: &str) -> Result<session_manager::SavedSession> {
    match SessionManager::default_location()
        .context("could not open session manager for resume")?
        .attach_session_by_prefix(session_id)
    {
        Ok((recovery, lease)) => {
            // This exec run owns the session until it exits.
            lease.commit();
            Ok(recovery.session)
        }
        // Resuming a session a TUI has open would give its document two
        // writers; the TUI's next autosave would drop this run's turns.
        Err(error) if error.kind() == io::ErrorKind::ResourceBusy => {
            bail!(exec_resume_busy_error(session_id))
        }
        Err(error) => Err(error).with_context(|| exec_resume_load_error(session_id)),
    }
}

fn exec_resume_busy_error(session_id: &str) -> String {
    format!(
        "session {} is open in another Codewhale window. Continue it there, or run \
         `codewhale fork <SESSION_ID>` and resume the copy.",
        exec_stream_session_ref(session_id)
    )
}

/// The typed `--resume` value stays redacted in every output mode: exec runs
/// in CI logs, and a mistyped or pasted value can be a secret.
fn exec_resume_load_error(session_id: &str) -> String {
    format!(
        "could not load session {}. Run `codewhale sessions` to list ids.",
        exec_stream_session_ref(session_id)
    )
}

/// Select the route for `exec --resume` before any engine/client is built.
///
/// Precedence is intentionally field-aware:
/// - no explicit `--provider` or `--model`: restore the saved provider/model;
/// - explicit `--provider`: keep that route and use its configured/default model
///   unless `--model` is also present;
/// - explicit `--model` alone: restore the saved provider, then use that model.
fn resolve_exec_resume_route(
    config: &mut Config,
    saved: &session_manager::SavedSession,
    explicit_provider: bool,
    explicit_model: Option<&str>,
) -> Result<String> {
    if !explicit_provider {
        let saved_provider_identity = saved
            .metadata
            .model_provider_id
            .as_deref()
            .filter(|identity| !identity.trim().is_empty())
            .unwrap_or(&saved.metadata.model_provider);
        let identity = config
            .resolve_persisted_provider_identity(
                Some(&saved.metadata.model_provider),
                saved.metadata.model_provider_id.as_deref(),
            )
            .map_err(anyhow::Error::msg)
            .with_context(|| {
                format!(
                    "saved session provider '{}' is unavailable; Codewhale will not fall back. {}",
                    saved_provider_identity,
                    session_export::export_recovery_hint(&saved.metadata.id)
                )
            })?;
        config
            .scope_to_provider_identity(&identity)
            .map_err(anyhow::Error::msg)?;
    }

    if let Some(model) = explicit_model {
        return Ok(resolve_exec_model(config, Some(model)));
    }
    if explicit_provider {
        return Ok(resolve_exec_model(config, None));
    }
    Ok(saved.metadata.model.clone())
}

/// Fold the dispatcher-forwarded launch overrides (`CODEWHALE_PROVIDER` /
/// `CODEWHALE_MODEL`, set by `codewhale --provider X --model Y exec ...`)
/// into the explicit route signals `exec --resume`/`--continue` honour.
///
/// Exec-level flags win when both are present; either source counts as
/// "the user named a route for this run", so a resume must not silently
/// restore the saved provider/model over it.
fn exec_resume_route_overrides(
    exec_provider: Option<&str>,
    exec_model: Option<&str>,
    launch_provider: Option<&str>,
    launch_model: Option<&str>,
) -> (bool, Option<String>) {
    let non_empty = |value: Option<&str>| {
        value
            .map(str::trim)
            .filter(|value| !value.is_empty())
            .map(str::to_string)
    };
    let explicit_provider =
        non_empty(exec_provider).is_some() || non_empty(launch_provider).is_some();
    let explicit_model = non_empty(exec_model).or_else(|| non_empty(launch_model));
    (explicit_provider, explicit_model)
}

#[derive(Args, Debug, Clone, Default)]
struct SetupArgs {
    /// Initialize MCP configuration at the configured path
    #[arg(long, default_value_t = false)]
    mcp: bool,
    /// Initialize skills directory and an example skill
    #[arg(long, default_value_t = false)]
    skills: bool,
    /// Initialize tools directory with a self-describing example script
    #[arg(long, default_value_t = false)]
    tools: bool,
    /// Initialize plugins directory with a self-describing example
    #[arg(long, default_value_t = false)]
    plugins: bool,
    /// Initialize MCP config, skills, tools, and plugins
    #[arg(long, default_value_t = false)]
    all: bool,
    /// Create a local workspace skills directory (./.codewhale/skills)
    #[arg(long, default_value_t = false)]
    local: bool,
    /// Overwrite existing template files
    #[arg(long, default_value_t = false)]
    force: bool,
    /// Print a compact, read-only status report (no network calls)
    #[arg(long, default_value_t = false, conflicts_with_all = ["mcp", "skills", "tools", "plugins", "all", "local", "clean"])]
    status: bool,
    /// Remove crash checkpoints while preserving unsent offline input
    #[arg(long, default_value_t = false, conflicts_with_all = ["mcp", "skills", "tools", "plugins", "all", "local", "status"])]
    clean: bool,
}

#[derive(Args, Debug, Clone, Default)]
struct DoctorArgs {
    /// Emit machine-readable structural JSON output (always offline)
    #[arg(long, default_value_t = false)]
    json: bool,
    /// Emit only the diagnostic context source map as JSON
    #[arg(long, default_value_t = false, conflicts_with = "json")]
    context_json: bool,
    /// Opt in to probing a local provider endpoint (may start a local service)
    #[arg(
        long,
        default_value_t = false,
        conflicts_with_all = ["json", "context_json"]
    )]
    probe_local: bool,
    /// Opt in to probing the configured hosted provider API
    #[arg(
        long,
        default_value_t = false,
        conflicts_with_all = ["json", "context_json"]
    )]
    probe_api: bool,
    /// Opt in to contacting the release service for an update check
    #[arg(
        long,
        default_value_t = false,
        conflicts_with_all = ["json", "context_json"]
    )]
    check_updates: bool,
    /// Opt in to starting enabled MCP servers and checking process/protocol reachability
    #[arg(
        long,
        default_value_t = false,
        conflicts_with_all = ["json", "context_json"]
    )]
    probe_mcp: bool,
    /// Opt in to a credential-free transport probe of the selected search provider
    #[arg(
        long,
        default_value_t = false,
        conflicts_with_all = ["json", "context_json"]
    )]
    probe_search: bool,
    /// Plan and apply automatic repairs with consent (#5552)
    #[arg(
        long,
        default_value_t = false,
        conflicts_with_all = ["json", "context_json"]
    )]
    fix: bool,
    /// Apply the planned repairs without prompting (requires --fix)
    #[arg(long, default_value_t = false, requires = "fix")]
    yes: bool,
    /// Repair the saved-session store now: re-index recoverable sessions,
    /// unbind dead thread links, set aside (never delete) what nothing uses
    #[arg(
        long,
        default_value_t = false,
        conflicts_with_all = ["json", "context_json"]
    )]
    repair_sessions: bool,
    /// With --repair-sessions: report what would be repaired, change nothing
    #[arg(long, default_value_t = false, requires = "repair_sessions")]
    dry_run: bool,
    /// Report every Codewhale copy on this machine and who owns it (offline; nothing is run or changed)
    #[arg(long, default_value_t = false, conflicts_with_all = ["context_json", "repair_sessions", "fix"])]
    installs: bool,
    /// With --installs: print only what needs attention, nothing on a tidy machine
    #[arg(long, default_value_t = false, requires = "installs")]
    brief: bool,
    /// Move everything from a recovery folder back where it was (the newest when STAMP is omitted)
    #[arg(
        long,
        value_name = "STAMP",
        num_args = 0..=1,
        default_missing_value = "latest",
        conflicts_with_all = ["json", "context_json", "fix", "installs", "repair_sessions"]
    )]
    restore: Option<String>,
    /// Show every resolved path, the search provider and secret-backend details
    #[arg(long, default_value_t = false, conflicts_with_all = ["json", "context_json"])]
    all: bool,
}

#[derive(Args, Debug, Clone)]
struct SessionDiagnosticsArgs {
    /// JSONL session log to inspect
    #[arg(value_name = "JSONL")]
    path: PathBuf,
    /// Emit machine-readable JSON with redacted source handles
    #[arg(long, default_value_t = false)]
    json: bool,
}

#[derive(Args, Debug, Clone)]
struct ScorecardArgs {
    /// JSON file with the recorded turns to score: an array of
    /// `{ "turn_id", "provider", "model", "billing_surface", "usage": {…} }`.
    /// `turn_end` hooks emit this route provenance plus `created_at`; persisted
    /// runtime exports may instead use `id`, `effective_provider`,
    /// `effective_model`, and `effective_billing_surface`.
    /// Shell-only hook rows marked `model_backed: false` are excluded. Legacy
    /// rows without provider remain readable but their cost is unavailable.
    #[arg(long, value_name = "FILE")]
    input: PathBuf,
    /// Optional baseline scorecard-metrics JSON to compare against. When set,
    /// the command exits non-zero if any metric regresses past the threshold.
    #[arg(long, value_name = "FILE")]
    baseline: Option<PathBuf>,
    /// Regression threshold, in percent increase over the baseline.
    #[arg(long, default_value_t = 5.0, value_parser = parse_regression_threshold)]
    threshold: f64,
    /// Emit machine-readable JSON instead of the human summary.
    #[arg(long, default_value_t = false)]
    json: bool,
}

#[derive(Args, Debug, Clone)]
struct EvalArgs {
    /// Intentionally fail a specific step (list, read, search, edit, patch, shell)
    #[arg(long, value_name = "STEP")]
    fail_step: Option<String>,
    /// Shell command to run during the exec step
    #[arg(long, default_value = "printf eval-harness")]
    shell_command: String,
    /// Token that must appear in shell output for validation
    #[arg(long, default_value = "eval-harness")]
    shell_expect_token: String,
    /// Maximum characters stored per step output summary
    #[arg(long, default_value_t = 240)]
    max_output_chars: usize,
    /// Emit machine-readable JSON output
    #[arg(long, default_value_t = false)]
    json: bool,
    /// Append one JSONL fixture line per step to `<DIR>/<scenario>.jsonl`.
    /// Mock LLM tests can later replay these fixtures.
    #[arg(long, value_name = "DIR")]
    record: Option<PathBuf>,
}

#[derive(Args, Debug, Clone, Default)]
struct ModelsArgs {
    /// Print models as pretty JSON
    #[arg(long, default_value_t = false)]
    json: bool,
    /// Refresh catalogs for all configured providers (no inference requests)
    #[arg(long, visible_alias = "refresh")]
    update: bool,
    /// Limit listing or refresh to this exact provider identity
    #[arg(long, value_name = "ID")]
    provider: Option<String>,
}

#[derive(Args, Debug, Clone)]
struct SpeechArgs {
    /// Text to synthesize. This is sent as the assistant message content.
    #[arg(value_name = "TEXT")]
    text: String,

    /// Output audio path. Defaults to `speech.<format>` in `--output-dir`,
    /// `[speech].output_dir`, or the current directory.
    #[arg(short, long, value_name = "FILE")]
    output: Option<PathBuf>,

    /// Directory for the default `speech.<format>` output file when `-o`/`--output` is omitted.
    #[arg(long = "output-dir", value_name = "DIR")]
    output_dir: Option<PathBuf>,

    /// TTS model. Defaults to built-in voices, or is inferred from --voice-prompt/--clone-voice.
    #[arg(long)]
    model: Option<String>,

    /// Built-in voice ID, or a data:audio/...;base64,... URI for voice clone.
    #[arg(long)]
    voice: Option<String>,

    /// Natural language style instruction; not spoken verbatim.
    #[arg(long)]
    instruction: Option<String>,

    /// Voice design prompt. Implies mimo-v2.5-tts-voicedesign when --model is omitted.
    #[arg(long = "voice-prompt")]
    voice_prompt: Option<String>,

    /// MP3/WAV sample used for voice cloning. Implies mimo-v2.5-tts-voiceclone when --model is omitted.
    #[arg(long = "clone-voice", value_name = "FILE")]
    clone_voice: Option<PathBuf>,

    /// Output audio format requested from the API
    #[arg(long, default_value = "wav")]
    format: String,

    /// Emit machine-readable JSON output
    #[arg(long, default_value_t = false)]
    json: bool,
}

#[derive(Args, Debug, Clone)]
struct ReviewArgs {
    /// Review staged changes instead of the working tree
    #[arg(long, conflicts_with_all = ["pr", "base"])]
    staged: bool,
    /// Review GitHub pull request #N instead of a local diff (fetched via `gh`)
    #[arg(long, conflicts_with_all = ["staged", "base", "path"])]
    pr: Option<u32>,
    /// Repository in `owner/name` form for --pr. Defaults to the current
    /// workspace's `gh` config (i.e. the repo gh thinks you're in).
    #[arg(long, requires = "pr", conflicts_with_all = ["staged", "base", "path"])]
    repo: Option<String>,
    /// Post the review to the pull request (one COMMENT review with inline
    /// line comments plus a summary). Requires --pr; without it the review
    /// is only printed locally.
    #[arg(long, requires = "pr", conflicts_with_all = ["staged", "base", "path"])]
    post: bool,
    /// Base ref to diff against (e.g. origin/main). The reviewer also gets the
    /// base..HEAD commit messages as the author's stated intent.
    #[arg(long, conflicts_with_all = ["staged", "pr"])]
    base: Option<String>,
    /// Limit diff to a specific path
    #[arg(long)]
    path: Option<PathBuf>,
    /// Override model for this review
    #[arg(long)]
    model: Option<String>,
    /// Override the provider route for this review (e.g. `zai`, `openrouter`,
    /// `deepseek`). Non-secret identifier only — credentials still resolve
    /// from the environment/config. Use it to disambiguate a `--model` that
    /// more than one configured route offers (which otherwise hard-errors
    /// with "available from configured provider route(s): ...").
    #[arg(long)]
    provider: Option<String>,
    /// Maximum diff characters; an oversized diff is refused, never truncated
    #[arg(long, default_value_t = 200_000)]
    max_chars: usize,
    /// Maximum complete PR review passes. Values above 1 explicitly authorize
    /// additional model requests; the default preserves single-pass behavior.
    #[arg(long, default_value_t = 1)]
    max_passes: usize,
    /// Per-review money cap in USD. It is only enforceable together with
    /// `--input-usd-per-mtok` and `--output-usd-per-mtok`; without both, the
    /// receipt reports the cap as unsupported instead of implying it applied.
    #[arg(long)]
    money_cap_usd: Option<f64>,
    /// Input price per million tokens for this exact route, in USD
    #[arg(long, requires_all = ["money_cap_usd"])]
    input_usd_per_mtok: Option<f64>,
    /// Output price per million tokens for this exact route, in USD
    #[arg(long, requires_all = ["money_cap_usd"])]
    output_usd_per_mtok: Option<f64>,
    /// Write a durable pre-push review receipt after a successful review
    #[arg(long, default_value_t = false)]
    write_receipt: bool,
    /// Validate the current diff against a durable review receipt without calling a model
    #[arg(long, default_value_t = false)]
    check_receipt: bool,
    /// Override where the review receipt is written or read
    #[arg(long)]
    receipt_path: Option<PathBuf>,
    /// Emit machine-readable JSON output
    #[arg(long, default_value_t = false)]
    json: bool,
}

#[derive(Args, Debug, Clone)]
struct ApplyArgs {
    /// Patch file to apply (defaults to stdin)
    #[arg(value_name = "PATCH_FILE")]
    patch_file: Option<PathBuf>,
}

#[derive(Args, Debug, Clone)]
struct ServeArgs {
    /// Start MCP server over stdio
    #[arg(long)]
    mcp: bool,
    /// Start runtime HTTP/SSE API server
    #[arg(long)]
    http: bool,
    /// Start runtime HTTP/SSE API server with the built-in mobile control page
    #[arg(long)]
    mobile: bool,
    /// Start the embedded loopback-only browser client and open it
    #[arg(long)]
    web: bool,
    /// Show a QR code for the mobile URL in the terminal (requires --mobile)
    #[arg(long, requires = "mobile")]
    qr: bool,
    /// Start ACP server over stdio for editor clients such as Zed
    #[arg(long)]
    acp: bool,
    /// Bind host for HTTP server (default loopback; mobile is always loopback-only)
    #[arg(long)]
    host: Option<String>,
    /// Bind port for HTTP server
    #[arg(long, default_value_t = 7878)]
    port: u16,
    /// Background task worker count (1-8)
    #[arg(long, default_value_t = 2)]
    workers: usize,
    /// Additional CORS origin to allow (repeatable). Stacks on top of the
    /// built-in defaults (localhost:3000, localhost:1420, tauri://localhost).
    /// Also reads `CODEWHALE_CORS_ORIGINS` (comma-separated), then
    /// `DEEPSEEK_CORS_ORIGINS` as an alias, and `[runtime_api] cors_origins`
    /// from `config.toml`. Whalescale#255.
    #[arg(long = "cors-origin", value_name = "URL")]
    cors_origin: Vec<String>,
    /// Require this bearer token for `/v1/*` runtime API routes. Also reads
    /// `CODEWHALE_RUNTIME_TOKEN` when omitted, then `DEEPSEEK_RUNTIME_TOKEN`
    /// as an alias.
    #[arg(long = "auth-token", value_name = "TOKEN")]
    auth_token: Option<String>,
    /// Disable runtime API auth when no token is configured. Only use on a trusted loopback.
    #[arg(long = "insecure")]
    insecure_no_auth: bool,
}

#[derive(Debug, Clone, PartialEq, Eq)]
struct ServeBindHost {
    host: String,
}

fn resolve_serve_bind_host(_mobile: bool, host: Option<String>) -> ServeBindHost {
    ServeBindHost {
        host: host.unwrap_or_else(|| "127.0.0.1".to_string()),
    }
}

fn validate_serve_mode_selection(
    mcp: bool,
    http: bool,
    mobile: bool,
    web: bool,
    acp: bool,
) -> Result<bool> {
    if http && mobile {
        bail!("--http and --mobile are mutually exclusive; choose one");
    }
    if web && (http || mobile) {
        bail!("--web is mutually exclusive with --http and --mobile");
    }
    let http_selected = http || mobile || web;
    let selected_modes = [mcp, http_selected, acp]
        .into_iter()
        .filter(|selected| *selected)
        .count();
    if selected_modes != 1 {
        bail!("Choose exactly one server mode: --mcp, --http/--mobile/--web, or --acp");
    }
    Ok(http_selected)
}

#[derive(Args, Debug, Clone)]
struct McpAddArgs {
    /// Server name
    name: String,
    /// Command to launch stdio server
    #[arg(long, conflicts_with = "url")]
    command: Option<String>,
    /// URL for streamable HTTP/SSE server
    #[arg(long, conflicts_with = "command")]
    url: Option<String>,
    /// Explicit URL transport override. Use "sse" for legacy SSE endpoints.
    #[arg(long, requires = "url")]
    transport: Option<String>,
    /// Environment variable containing a bearer token for URL-based servers
    #[arg(long, requires = "url")]
    bearer_token_env_var: Option<String>,
    /// Pre-registered OAuth client ID, bound to --oauth-issuer
    #[arg(long, requires_all = ["url", "oauth_issuer"])]
    oauth_client_id: Option<String>,
    #[arg(
        long,
        requires = "url",
        help = "Exact authorization issuer bound to this OAuth client"
    )]
    oauth_issuer: Option<String>,
    #[arg(
        long,
        requires = "url",
        help = "HTTPS URL of a hosted OAuth client metadata document"
    )]
    oauth_client_metadata_url: Option<String>,
    /// Exact OAuth resource pin checked against server discovery
    #[arg(long, requires = "url")]
    oauth_resource: Option<String>,
    /// OAuth scope to request during login. Repeat or comma-separate.
    #[arg(long = "scope", requires = "url", value_delimiter = ',')]
    scopes: Vec<String>,
    /// Arguments for command-based servers
    #[arg(long = "arg", allow_hyphen_values = true)]
    args: Vec<String>,
}

#[derive(Subcommand, Debug, Clone)]
enum McpCommand {
    /// List configured MCP servers
    List,
    /// Create a template MCP config at the configured path
    Init {
        /// Overwrite an existing MCP config file
        #[arg(long, default_value_t = false)]
        force: bool,
    },
    /// Connect to MCP servers and report status (does not attach to a
    /// running session)
    Connect {
        /// Optional server name to connect to
        #[arg(value_name = "SERVER")]
        server: Option<String>,
    },
    /// List tools discovered from MCP servers
    Tools {
        /// Optional server name to list tools for
        #[arg(value_name = "SERVER")]
        server: Option<String>,
    },
    /// Add an MCP server entry
    Add(Box<McpAddArgs>),
    /// Authenticate to a URL-based MCP server using OAuth
    Login {
        /// Server name
        name: String,
        /// OAuth scope to request. Repeat or comma-separate; defaults to config/discovery.
        #[arg(long = "scope", value_delimiter = ',')]
        scopes: Vec<String>,
    },
    /// Delete stored OAuth credentials for a URL-based MCP server
    Logout {
        /// Server name
        name: String,
    },
    /// Remove an MCP server entry
    Remove {
        /// Server name
        name: String,
    },
    /// Enable an MCP server
    Enable {
        /// Server name
        name: String,
    },
    /// Disable an MCP server
    Disable {
        /// Server name
        name: String,
    },
    /// Validate MCP config and required servers
    Validate,
    /// Register this Codewhale binary as a local MCP stdio server.
    ///
    /// This adds a config entry that runs `codewhale serve --mcp` (stdio protocol).
    /// For the HTTP/SSE runtime API, use `codewhale serve --http` directly instead.
    #[command(
        name = "add-self",
        long_about = "Register this Codewhale binary as a local MCP stdio server.\n\nAdds a config entry to ~/.codewhale/mcp.json that launches `codewhale serve --mcp`\nvia the stdio transport. Other Codewhale sessions (or any MCP client) can then\ndiscover and call tools exposed by this server.\n\nUse `codewhale serve --http` instead if you need the HTTP/SSE runtime API."
    )]
    AddSelf {
        /// Server name in mcp.json (default: "codewhale")
        #[arg(long, default_value = "codewhale")]
        name: String,
        /// Workspace directory for the MCP server
        #[arg(long)]
        workspace: Option<String>,
    },
}

/// The `codewhale mcp` subcommands with their one-line descriptions, for the
/// dispatcher's `codewhale mcp --help`.
///
/// That help exits in the dispatcher's parser, before this crate's parser
/// runs, and the dispatcher only sees forwarded arguments. Reading the list
/// off `McpCommand` keeps it from naming a subcommand that does not exist
/// or missing one that does.
#[must_use]
pub fn mcp_subcommand_help() -> String {
    use std::fmt::Write as _;

    let command = McpCommand::augment_subcommands(clap::Command::new("mcp"));
    let width = command
        .get_subcommands()
        .map(|subcommand| subcommand.get_name().len())
        .max()
        .unwrap_or(0);
    let mut help = String::from("Commands:\n");
    for subcommand in command.get_subcommands() {
        let about = subcommand
            .get_about()
            .map(ToString::to_string)
            .unwrap_or_default();
        let _ = writeln!(help, "  {:<width$}  {about}", subcommand.get_name());
    }
    help.push_str("\nRun `codewhale mcp <COMMAND> --help` for a command's options.");
    help
}

#[must_use]
pub fn doctor_options_help() -> String {
    DoctorArgs::augment_args(clap::Command::new("doctor"))
        .disable_help_flag(true)
        .help_template("Doctor options:\n{options}")
        .render_help()
        .to_string()
}

#[derive(Subcommand, Debug, Clone)]
pub(crate) enum IntegrationsCommand {
    /// Official DeepSeek Harness (`dsh`) connected through Codewhale
    Dsh {
        #[command(subcommand)]
        command: DshIntegrationCommand,
    },
}

#[derive(Subcommand, Debug, Clone)]
pub(crate) enum DshIntegrationCommand {
    /// Detect dsh and report the integration state without writing anything
    Status {
        /// Emit machine-readable JSON
        #[arg(long, default_value_t = false)]
        json: bool,
    },
    /// Show exactly what `connect`/`update` would write, without writing it
    Plan {
        #[arg(long, default_value_t = false)]
        json: bool,
        /// DSH profile the overlay targets (`web` or `headless`)
        #[arg(long, default_value = "web")]
        profile: String,
        /// Mirror Codewhale full access as DSH danger-full-access (only when Codewhale itself runs with full access)
        #[arg(long, default_value_t = false)]
        allow_full_access: bool,
        /// Record the Codewhale palette (skin) decision for the bundle profile; applied via DSH's `overrideTokens`, never through the overlay
        #[arg(long, default_value_t = false)]
        skin: bool,
    },
    /// Write the overlay and receipt under $CODEWHALE_HOME/integrations/dsh
    Connect {
        #[arg(long, default_value = "web")]
        profile: String,
        #[arg(long, default_value_t = false)]
        allow_full_access: bool,
        #[arg(long, default_value_t = false)]
        skin: bool,
        /// Confirm the disclosed plan without an interactive prompt (required when stdin is not a terminal)
        #[arg(long, default_value_t = false)]
        yes: bool,
    },
    /// Re-derive the overlay from the current Codewhale route
    Update {
        #[arg(long)]
        profile: Option<String>,
        #[arg(long, default_value_t = false)]
        allow_full_access: bool,
        /// Turn the bundle-profile skin on/off (`--skin false`; defaults to the previous choice)
        #[arg(long)]
        skin: Option<bool>,
        /// Turn the ambient ocean scene behind the DSH web UI on/off (`--ocean false`; defaults to the previous choice, initially on; needs the skin)
        #[arg(long)]
        ocean: Option<bool>,
        #[arg(long, default_value_t = false)]
        yes: bool,
    },
    /// Run dsh with the Codewhale overlay; extra args go to the dsh app
    Launch {
        /// Override the recorded profile (`web` or `headless`)
        #[arg(long)]
        profile: Option<String>,
        /// Print the exact command instead of running it
        #[arg(long, default_value_t = false)]
        dry_run: bool,
        #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
        args: Vec<String>,
    },
    /// Keep the overlay but refuse launches
    Disable,
    /// Allow launches again
    Enable,
    /// Delete Codewhale-owned files only; $DSH_HOME is never touched
    Remove {
        #[arg(long, default_value_t = false)]
        yes: bool,
    },
    /// Documented DSH plugin path: install the Codewhale bundle into a dedicated `codewhale` DSH profile via `dsh plugin add` (pnpm required)
    InstallBundle {
        /// Which shipped DSH app the dedicated profile boots (`web` or `headless`)
        #[arg(long, default_value = "web")]
        app: String,
        #[arg(long, default_value_t = false)]
        yes: bool,
    },
    /// `dsh plugin --profile codewhale remove codewhale-dsh-bundle`, then delete only Codewhale-owned bundle files
    RemoveBundle {
        #[arg(long, default_value_t = false)]
        yes: bool,
    },
}

#[derive(Args, Debug, Clone)]
struct FeaturesCli {
    #[command(subcommand)]
    command: FeaturesSubcommand,
}

#[derive(Subcommand, Debug, Clone)]
enum FeaturesSubcommand {
    /// List known feature flags and their state
    List,
}

#[derive(Args, Debug, Clone)]
struct SandboxArgs {
    #[command(subcommand)]
    command: SandboxCommand,
}

#[derive(Subcommand, Debug, Clone)]
enum SandboxCommand {
    /// Run a command with sandboxing
    Run {
        /// Sandbox policy (danger-full-access, read-only, external-sandbox, workspace-write)
        #[arg(long, default_value = "workspace-write")]
        policy: String,
        /// Allow outbound network access
        #[arg(long)]
        network: bool,
        /// Additional writable roots (repeatable)
        #[arg(long, value_name = "PATH")]
        writable_root: Vec<PathBuf>,
        /// Exclude TMPDIR from writable paths
        #[arg(long)]
        exclude_tmpdir: bool,
        /// Exclude /tmp from writable paths
        #[arg(long)]
        exclude_slash_tmp: bool,
        /// Command working directory
        #[arg(long)]
        cwd: Option<PathBuf>,
        /// Timeout in milliseconds
        #[arg(long, default_value_t = 60_000)]
        timeout_ms: u64,
        /// Command and arguments to run
        #[arg(required = true, trailing_var_arg = true)]
        command: Vec<String>,
    },
}

/// Pre-clap seam feeding `apply_process_hardening` (#5723): resolve only the
/// *startup* sandbox posture — `CODEWHALE_SANDBOX_MODE` /
/// `DEEPSEEK_SANDBOX_MODE` first, then the config file's `sandbox_mode` key —
/// so the irreversible `PR_SET_NO_NEW_PRIVS` decision can honor a
/// `danger-full-access` launch.
///
/// This is deliberately a narrow single-key read, not a config-system
/// reorder: the full pipeline (clap flags such as `--config`/`exec
/// --sandbox`, profiles, managed and project overlays) cannot run before
/// process hardening, which must land before Tokio and any worker threads.
/// What the seam cannot see keeps the hardened default — fail-closed. The
/// one deliberate gap in the other direction: a later-resolved override that
/// *tightens* a config-file `danger-full-access` (e.g. managed requirements)
/// leaves the flag off; `CODEWHALE_NO_NEW_PRIVS=1` remains the explicit
/// override that forces it on in any posture. The env path override
/// (`CODEWHALE_CONFIG_PATH`) is honored through `resolve_load_config_path`;
/// only clap-parsed paths are invisible here.
fn resolve_startup_sandbox_mode_for_hardening() -> Option<String> {
    if let Ok(value) =
        std::env::var("CODEWHALE_SANDBOX_MODE").or_else(|_| std::env::var("DEEPSEEK_SANDBOX_MODE"))
    {
        return Some(value);
    }
    let path = codewhale_runtime::config::resolve_load_config_path(None)
        .ok()
        .flatten()?;
    let raw = std::fs::read_to_string(path).ok()?;
    let doc = raw.parse::<toml::Value>().ok()?;
    doc.get("sandbox_mode")
        .and_then(toml::Value::as_str)
        .map(str::to_string)
}

/// Run a delegated Engine command in the canonical CLI process. Startup facts
/// are typed; `args` contains only command arguments, without a binary name.
pub fn run(options: RuntimeOptions, args: Vec<String>) -> std::process::ExitCode {
    let outcome = run_with_args(options, args);
    codewhale_app_server::runtime_api::listener::discard_runtime_listener_handoff();
    match outcome {
        Ok(()) => std::process::ExitCode::SUCCESS,
        Err(err) => {
            eprintln!("error: {err}");
            for cause in err.chain().skip(1) {
                eprintln!("  caused by: {cause}");
            }
            std::process::ExitCode::FAILURE
        }
    }
}

/// Internal implementation that mirrors the old `main()` but takes explicit
/// args instead of reading `std::env::args()`. Startup facts are captured by
/// the canonical CLI and applied before config/discovery or worker startup.
fn run_with_args(options: RuntimeOptions, args: Vec<String>) -> Result<()> {
    codewhale_engine::extension_host::install_ports();
    let inherited_listener =
        codewhale_app_server::runtime_api::listener::prepare_runtime_listener_handoff()?;
    let args: Vec<String> = std::iter::once("codewhale".to_string())
        .chain(args)
        .collect();
    // Match the dispatcher entrypoint: Unix shells and supervisors may inherit
    // SIGPIPE ignored, which turns short pipelines such as `codewhale doctor |
    // head` into BrokenPipe panics once this delegated TUI binary prints.
    // SAFETY: first call at startup; no threads or handlers yet.
    #[cfg(unix)]
    unsafe {
        libc::signal(libc::SIGPIPE, libc::SIG_DFL);
    }

    startup_trace::mark_process_start();
    configure_windows_console_utf8();
    install_rustls_crypto_provider();
    // The TUI is the terminal host for every mode this binary runs
    // (interactive, exec, serve): runtime code reaches raw mode and
    // notification delivery only through this port.
    crate::tui::ui::install_host_terminal();
    codewhale_engine::command_catalog::install(std::sync::Arc::new(crate::commands::CommandTable));

    // ── Process hardening (#2183) ─────────────────────────────────────────
    // MUST run before Tokio is booted and before any threads are spawned.
    // See crates/sandbox/src/process_hardening.rs for ordering rationale.
    // The startup-posture read is the narrow seam documented above: a startup
    // resolved to danger-full-access skips PR_SET_NO_NEW_PRIVS (#5723), every
    // other outcome keeps it.
    let startup_sandbox_mode = resolve_startup_sandbox_mode_for_hardening();
    codewhale_sandbox::process_hardening::apply_process_hardening(startup_sandbox_mode.as_deref());

    if args.get(1).is_some_and(|arg| arg == "pet") {
        let root = crate::tui::pet_watch::owner::directory()?;
        match args.get(2).map(String::as_str) {
            Some("serve") if args.len() == 3 => {
                return crate::tui::pet_watch::owner::serve(
                    root,
                    std::env::var("CODEWHALE_PET_PORT")
                        .ok()
                        .map(|p| p.parse::<u16>())
                        .transpose()?
                        .unwrap_or(4633),
                );
            }
            _ => anyhow::bail!("Usage: codewhale pet serve"),
        }
    }

    // ── Fatal-signal terminal guard (#5424) ───────────────────────────────
    // Abort-class deaths (stack overflow, allocation failure, double panic)
    // skip the panic hook AND every Drop guard, leaving mouse capture and
    // the kitty keyboard stack leaking into the user's shell. A classic
    // sigaction handler restores the terminal and stamps a marker before
    // re-raising. Also before any threads exist.
    crate::tui::ui::fatal_signal_guard::install_fatal_signal_guard();

    // Set up process panic hook before anything else — writes crash dumps
    // to the selected profile's crashes/ even before tokio is up,
    // and restores the terminal so a panicked TUI doesn't leave the user's
    // shell stuck in alt-screen mode.
    let orig_hook = std::panic::take_hook();
    std::panic::set_hook(Box::new(move |panic_info| {
        // Restore the terminal first so the panic message itself, plus the
        // user's shell after exit, are visible. Best-effort — we may not be
        // in raw / alt-screen mode if the panic happens pre-TUI. Shared
        // with the signal handler installed below so both exit paths leave
        // the terminal in the same well-defined state.
        crate::tui::ui::emergency_restore_terminal();

        let msg = if let Some(s) = panic_info.payload().downcast_ref::<&str>() {
            s.to_string()
        } else if let Some(s) = panic_info.payload().downcast_ref::<String>() {
            s.clone()
        } else {
            format!("{:?}", panic_info.payload())
        };
        let location = panic_info
            .location()
            .map(|loc| loc.to_string())
            .unwrap_or_else(|| "unknown".to_string());
        tracing::error!(target: "panic", "Process panicked at {location}: {msg}");

        // Telemetry, if and only if this process was armed. This hook is
        // installed before `Cli::parse()` and long before any config is
        // resolved, so it cannot consult a resolved value — but it can consult
        // a `OnceLock` that is by construction empty until resolution
        // completes. A user who never opted in panics without writing a byte
        // and without creating a directory.
        //
        // The site is allowlist-reduced and `msg` is deliberately not read: a
        // slicing panic embeds the entire string being sliced, and this tree
        // slices user and model text in dozens of places.
        codewhale_telemetry::set_exit_class(codewhale_telemetry::ExitClass::Panic);
        if let Some(site) = panic_info
            .location()
            .map(|loc| codewhale_telemetry::reduce_panic_site(loc.file(), loc.line(), loc.column()))
        {
            codewhale_telemetry::record_blocking(codewhale_telemetry::Event::Panic { site });
        }
        // Write crash dump best-effort
        if let Ok(home) = codewhale_config::codewhale_home() {
            let crash_dir = home.join("crashes");
            let _ = std::fs::create_dir_all(&crash_dir);
            use chrono::Utc;
            let ts = Utc::now().format("%Y%m%dT%H%M%S%.3fZ");
            let path = crash_dir.join(format!("{ts}-process-panic.log"));
            let contents =
                format!("Process panicked\nLocation: {location}\nTimestamp: {ts}\nPanic: {msg}\n",);
            let _ = std::fs::write(&path, contents);
        }
        // Invoke the original hook (prints to stderr, etc.)
        orig_hook(panic_info);
    }));

    // Parse and freeze every startup authority before Tokio or any other
    // worker thread exists. A workspace `.env` is intentionally a narrow
    // credential convenience surface: it must never redirect product state,
    // configuration, MCP, trust, sandbox, executable lookup, or plugin
    // discovery. Plugin discovery therefore runs first, and the loader below
    // admits only built-in provider credential names from a stable file read.
    let cli = match parse_cli_args(&args) {
        Ok(mut cli) => {
            cli.options = options.merge(cli.options)?;
            cli
        }
        Err(e) => {
            e.exit();
        }
    };
    anyhow::ensure!(
        !inherited_listener
            || matches!(
                cli.command.as_ref(),
                Some(Commands::Serve(args)) if args.http && !args.mcp && !args.acp
            ),
        "An inherited Runtime listener requires serve --http or app-server --http"
    );
    // #5098: project-scope fleet agent profiles (`.codewhale/agents/*.toml`)
    // join the dispatch roster under the same trust decision as the rest of
    // project-level config — `--no-project-config` opts the layer out for
    // every roster read in this process.
    codewhale_engine::fleet::roster::set_project_agent_profiles_enabled(!cli.no_project_config);
    let workspace = resolve_workspace(&cli);
    let inspect_plugin_cli = matches!(cli.command.as_ref(), Some(Commands::Plugins { .. }));
    let mut plugin_discovery = None;
    let mut plugin_registry = None;
    let (cli, command) = prepare_cli_startup(
        cli,
        || {
            let discovery = codewhale_plugins::PluginDiscoveryContext::capture_pre_dotenv();
            plugin_registry = Some(if inspect_plugin_cli {
                discovery.inspect_registry_for_workspace(&workspace)
            } else {
                discovery.registry_for_workspace(&workspace)
            });
            plugin_discovery = Some(discovery);
        },
        || warn_on_workspace_dotenv_result(&workspace),
    );
    let plugin_discovery = plugin_discovery
        .expect("plugin discovery initialization must precede workspace dotenv loading");
    let plugin_registry = plugin_registry
        .expect("plugin discovery initialization must precede workspace dotenv loading");

    codewhale_plugins::providers::install_startup_registry(plugin_registry.clone());

    // The interactive runtime intentionally carries a large state machine:
    // terminal rendering, modal dispatch, provider setup, and fleet/workflow
    // events all share one async owner. Debug builds retain enough stack
    // temporaries that nesting a modal event over the TUI loop can exceed the
    // platform main-thread default (8 MiB on macOS). Give that owner an
    // explicit stack while keeping process hardening and the global panic hook
    // above this boundary, before Tokio or any worker thread exists.
    //
    // 16 MiB stopped being enough: in a debug build the deepest measured
    // chain — the event-loop poll stack down to the trust-confirm engine
    // respawn (`handle_view_events` → `apply_command_result` →
    // `spawn_tui_engine` → `Engine::new` → `CodewhaleClient::new` →
    // `resolve_runtime_route` → `Config::clone`) — consumed ~16.5 MiB and
    // aborted on the guard page (the plugin_toml_binary cucumber acceptance
    // on the ubuntu CI leg). The fat frames are the debug poll functions of
    // the giant top-level futures (`run_async_main_inner` ~5.5 MiB, `run_tui`
    // ~3.2 MiB, `handle_view_events` ~2.1 MiB), so any small addition to them
    // re-tips a zero-margin stack. 32 MiB restores real headroom; the cost is
    // address space only, since thread stacks commit lazily.
    let runtime_thread = std::thread::Builder::new()
        .name("codewhale-main".to_string())
        .stack_size(codewhale_runtime::CODEWHALE_MAIN_STACK_BYTES)
        .spawn(move || run_async_main(cli, command, plugin_discovery, plugin_registry))
        .context("Failed to start the Codewhale runtime thread")?;
    match runtime_thread.join() {
        Ok(result) => result,
        Err(payload) => {
            let message = payload
                .downcast_ref::<&str>()
                .map(|value| (*value).to_string())
                .or_else(|| payload.downcast_ref::<String>().cloned())
                .unwrap_or_else(|| "unknown panic payload".to_string());
            Err(anyhow!("Codewhale runtime thread panicked: {message}"))
        }
    }
}

fn run_async_main(
    cli: Cli,
    command: Option<Commands>,
    plugin_discovery: Arc<codewhale_plugins::PluginDiscoveryContext>,
    plugin_registry: Arc<codewhale_plugins::PluginRegistry>,
) -> Result<()> {
    build_runtime(command.as_ref())?.block_on(run_async_main_inner(
        cli,
        command,
        plugin_discovery,
        plugin_registry,
    ))
}

/// Build the runtime that owns every async task in this binary.
///
/// `#[tokio::main]` used to expand here, which left every worker thread on
/// tokio's 2 MiB default while only the `codewhale-main` owner thread above
/// received `CODEWHALE_MAIN_STACK_BYTES`. The engine does not run on that owner
/// thread — `core::engine::spawn_engine` hands `Engine::run` to
/// `utils::spawn_supervised`, a bare `tokio::spawn` — so the explicit stack
/// never applied where the depth actually is.
///
/// A debug-build `agent` dispatch (turn_loop -> FuturesUnordered ->
/// execute_rich_full_with_context -> AgentTool::execute -> spawn_subagent_from_input)
/// measured a stack high-water mark between 2.25 and 2.5 MiB and aborted the
/// whole process on the guard page. A Rust stack overflow is not a panic: it
/// raises SIGABRT, so `spawn_supervised`'s `catch_unwind` cannot see it and the
/// process dies with 134 mid-dispatch.
///
/// Build the runtime that owns every async task in this binary.
///
/// `command` selects the worker-count policy: read-only diagnostic commands
/// run on a small fixed pool instead of tokio's one-worker-per-CPU default
/// (see [`diagnostic_worker_count`]). Interactive sessions and servers keep
/// the default sizing unchanged.
///
/// `#[tokio::main]` used to expand here, which left every worker thread on
/// tokio's 2 MiB default while only the `codewhale-main` owner thread above
/// received `CODEWHALE_MAIN_STACK_BYTES`. The engine does not run on that owner
/// thread — `core::engine::spawn_engine` hands `Engine::run` to
/// `utils::spawn_supervised`, a bare `tokio::spawn` — so the explicit stack
/// never applied where the depth actually is.
///
/// A debug-build `agent` dispatch (turn_loop -> FuturesUnordered ->
/// execute_rich_full_with_context -> AgentTool::execute -> spawn_subagent_from_input)
/// measured a stack high-water mark between 2.25 and 2.5 MiB and aborted the
/// whole process on the guard page. A Rust stack overflow is not a panic: it
/// raises SIGABRT, so `spawn_supervised`'s `catch_unwind` cannot see it and the
/// process dies with 134 mid-dispatch.
///
/// This is behavior-identical to the old `#[tokio::main]` expansion apart from
/// the stack size, and it makes the knob greppable.
pub(crate) fn build_runtime(command: Option<&Commands>) -> Result<tokio::runtime::Runtime> {
    let mut builder = tokio_runtime_builder();
    if let Some(workers) = diagnostic_worker_count(command) {
        builder.worker_threads(workers);
    }
    builder
        .build()
        .context("Failed to build the Codewhale Tokio runtime")
}

/// Number of async workers to request from tokio.
///
/// Unset means tokio's default: one worker per CPU. That default is right for
/// interactive sessions and long-running servers, but short-lived offline
/// commands gain nothing from a full-CPU pool — they pay thread spawn, stack
/// reservation, and teardown futex traffic for capacity they never use (perf
/// attribution: pthread_create under `Builder::build` dominates init samples).
const DIAGNOSTIC_WORKER_CAP: usize = 2;

fn diagnostic_worker_count(command: Option<&Commands>) -> Option<usize> {
    let capped = match command {
        // Read-only diagnostic surfaces (doctor family).
        Some(
            Commands::Doctor(_)
            | Commands::Eval(_)
            | Commands::SessionDiagnostics(_)
            | Commands::Sessions { .. }
            | Commands::Receipts { .. },
        ) => true,
        // Only the read-only status report; mutating setup keeps defaults.
        Some(Commands::Setup(args)) => args.status,
        Some(Commands::Plugins { .. }) => true,
        _ => false,
    };
    capped.then_some(DIAGNOSTIC_WORKER_CAP)
}

fn tokio_runtime_builder() -> tokio::runtime::Builder {
    let mut builder = tokio::runtime::Builder::new_multi_thread();
    builder
        .enable_all()
        .thread_stack_size(codewhale_runtime::CODEWHALE_MAIN_STACK_BYTES);
    builder
}

/// Which product surface this process is serving.
///
/// A function of the parsed subcommand, never of the executable: this one
/// binary serves at least five surfaces, so `current_exe()` would label all of
/// them the same.
fn telemetry_surface(command: Option<&Commands>) -> codewhale_telemetry::Surface {
    telemetry_surface_with(command, embedded_surface_override())
}

/// The injectable form of [`telemetry_surface`].
///
/// The declared surface is a parameter rather than an environment read inside
/// the match, so the "only the server branch consults it" rule can be proven
/// without a test mutating process-wide environment — which would race every
/// other test in this binary.
fn telemetry_surface_with(
    command: Option<&Commands>,
    declared: Option<codewhale_telemetry::Surface>,
) -> codewhale_telemetry::Surface {
    use codewhale_telemetry::Surface;
    match command {
        None | Some(Commands::Resume { .. } | Commands::Fork { .. } | Commands::Pr { .. }) => {
            Surface::Tui
        }
        Some(Commands::Exec(_)) => Surface::Exec,
        Some(Commands::Serve(args)) => {
            if args.mcp {
                Surface::McpServer
            } else {
                declared.unwrap_or(Surface::Serve)
            }
        }
        Some(_) => Surface::Cli,
    }
}

/// The surface name an embedding client declared for a server it started.
///
/// An API server is the one surface a third party legitimately *starts*, so it
/// is the one surface an embedder may name. The editor extension, for instance,
/// runs `codewhale serve --http` and declares itself, so its sessions are not
/// counted as anonymous `serve` traffic from nobody in particular.
///
/// Three properties are deliberate:
///
/// 1. **`serve` only.** The variable is inherited by any process this one
///    starts, and an agent running a shell command is running `codewhale`
///    descendants with this environment. A nested `codewhale exec` that
///    inherited the label would report itself as the embedder — wrong, and
///    invisible. Consulting it only in the branch that *is* the embedder's own
///    server bounds that to a process the embedder actually started.
/// 2. **`tui` is refused.** Nothing starts the interactive terminal UI on a
///    user's behalf, and reporting a server as `tui` would put it in the one
///    surface whose numbers are the terminal's. `mcp` is decided before this is
///    consulted, so a server that *is* an MCP server still says so.
/// 3. **Unrecognised is not an error.** A name outside [`Surface::ALL`] is
///    ignored and the caller falls back, rather than failing a startup over a
///    field that is only a label. The value is not "sanitised" into a variant
///    it resembles; it is dropped.
fn embedded_surface_override() -> Option<codewhale_telemetry::Surface> {
    embedded_surface_override_from(std::env::var("CODEWHALE_TELEMETRY_SURFACE").ok())
}

/// The injectable form of [`embedded_surface_override`], used by tests.
///
/// Separate from the environment read for the same reason
/// `load_setup_state_for_decision_at` is separate from
/// `load_setup_state_for_decision`: a test that mutated process-wide
/// environment for this would race every other test in the binary.
fn embedded_surface_override_from(raw: Option<String>) -> Option<codewhale_telemetry::Surface> {
    use codewhale_telemetry::Surface;
    let surface = Surface::parse(raw?.trim())?;
    if surface == Surface::Tui {
        return None;
    }
    Some(surface)
}

/// How this session was started, for `session_start`.
fn telemetry_session_source(command: Option<&Commands>) -> codewhale_telemetry::SessionSource {
    use codewhale_telemetry::SessionSource;
    match command {
        None | Some(Commands::Pr { .. }) => SessionSource::Interactive,
        Some(Commands::Resume { .. }) => SessionSource::Resume,
        Some(Commands::Fork { .. }) => SessionSource::Fork,
        Some(Commands::Serve(_)) => SessionSource::Api,
        Some(_) => SessionSource::Unknown,
    }
}

fn telemetry_session_source_for_run(
    command: Option<&Commands>,
) -> codewhale_telemetry::SessionSource {
    let flag = |name: &str| {
        std::env::var(name).is_ok_and(|value| !matches!(value.trim(), "" | "0" | "false"))
    };
    if flag("CI") || flag("GITHUB_ACTIONS") {
        codewhale_telemetry::SessionSource::Ci
    } else {
        telemetry_session_source(command)
    }
}

/// Read-only commands must not create telemetry state as a side effect.
// Sessions export only projects local records to an explicit output. Like
// listing, it must not initialize telemetry or interactive session state.
fn telemetry_command_is_read_only(command: Option<&Commands>) -> bool {
    matches!(
        command,
        Some(
            Commands::Doctor(_)
                | Commands::SessionDiagnostics(_)
                | Commands::Sessions { .. }
                | Commands::Receipts { .. }
        )
    ) || matches!(command, Some(Commands::Setup(args)) if args.status)
        || matches!(
            command,
            Some(Commands::Plugins {
                command: PluginsCommand::Status { .. }
            })
        )
}

/// Resolve the emit predicate and arm, once, before anything can record.
///
/// This is the read that v1 of the design was missing entirely:
/// `resolve_runtime_options` had no non-test caller in this crate, so neither
/// `telemetry = false` in the config file nor `CODEWHALE_TELEMETRY=0` was ever
/// consulted by a process that would have emitted.
///
/// `CliRuntimeOverrides::default()` is correct here. The dispatcher has already
/// applied the kill-switch floor and forwarded the *resolved* value through
/// `CODEWHALE_TELEMETRY`, which `EnvRuntimeOverrides::load()` picks up — and
/// re-reading `CODEWHALE_TELEMETRY` inside the telemetry crate would fork
/// `parse_bool`, the `DEEPSEEK_TELEMETRY` alias, and the floor into a second
/// source of truth.
fn arm_telemetry_with_setup(
    config_path: Option<PathBuf>,
    surface: codewhale_telemetry::Surface,
    source: codewhale_telemetry::SessionSource,
    setup_override: Option<&codewhale_config::SetupState>,
) {
    let Ok(store) = codewhale_config::ConfigStore::load(config_path) else {
        return;
    };
    let resolved = store
        .config
        .resolve_runtime_options(&codewhale_config::CliRuntimeOverrides::default());
    let setup = if let Some(setup) = setup_override {
        setup.clone()
    } else {
        let Some(setup) = codewhale_telemetry::load_setup_state_for_decision() else {
            // An existing unreadable privacy record may contain a decline.
            // Failing closed is safer than replacing it with default-on.
            return;
        };
        setup
    };
    let codewhale_telemetry::TelemetryDecision::Enabled(consent) =
        codewhale_telemetry::decide(&resolved, &setup, surface)
    else {
        return;
    };
    codewhale_telemetry::init(consent.with_config_path(Some(store.path().to_path_buf())));
    let _ = TELEMETRY_SESSION_START.set(std::time::Instant::now());
    codewhale_telemetry::record(codewhale_telemetry::Event::SessionStart { source });
}

fn arm_telemetry(cli: &Cli, command: Option<&Commands>) {
    if telemetry_command_is_read_only(command) {
        return;
    }
    arm_telemetry_with_setup(
        cli.config.clone(),
        telemetry_surface(command),
        telemetry_session_source_for_run(command),
        None,
    );
}

/// Whether a resolved route is the canonical public ChatGPT API endpoint.
/// CLI diagnostics use the same destination check as inference dispatch.
#[must_use]
pub fn is_official_chatgpt_api_base(base_url: &str) -> bool {
    pricing::is_official_chatgpt_api(base_url)
}

/// Compatibility command to explicitly enable usage under the current policy.
/// This optional choice never arms the current process.
pub fn accept_telemetry_notice(config_path: Option<PathBuf>, version: u32) -> Result<String> {
    if version != codewhale_telemetry::NOTICE_VERSION {
        anyhow::bail!(
            "read `codewhale config telemetry`, then explicitly accept current notice version {}",
            codewhale_config::TELEMETRY_NOTICE_VERSION
        );
    }
    set_telemetry_preference(config_path, true)
}

/// Set the durable usage preference through the same transition as Settings.
/// Enabling affects the next launch; disabling immediately erases queued usage.
pub fn set_telemetry_preference(config_path: Option<PathBuf>, enabled: bool) -> Result<String> {
    let applied =
        codewhale_engine::telemetry_notice::apply_persistent_preference(config_path, enabled);
    let message = applied.message(codewhale_localization::Locale::En);
    if applied.is_error() {
        anyhow::bail!("{message}");
    }
    Ok(message)
}

/// Close the armed session and flush, bounded.
///
/// Short CLI (`config`, `doctor`, `auth`, …) records `session_end`, seals the
/// local queue within a much smaller deadline, and returns. The 3s network
/// flush is a TUI/exec concern: a hung TLS handshake must not hold
/// `codewhale config list`. A configured endpoint remains buffered for the
/// next interactive session; an explicitly empty endpoint writes its local
/// dry-run batch immediately.
async fn finish_telemetry(outcome: &Result<()>, surface: codewhale_telemetry::Surface) {
    if !codewhale_telemetry::is_armed() {
        return;
    }
    // Only escalate: the panic hook and the signal path have already spoken if
    // they ran, and a stated class must not be overwritten by an inferred one.
    if outcome.is_err()
        && codewhale_telemetry::exit_class() == codewhale_telemetry::ExitClass::Clean
    {
        codewhale_telemetry::set_exit_class(codewhale_telemetry::ExitClass::Error);
    }
    for event in codewhale_telemetry::session_counters().usage_events() {
        codewhale_telemetry::record(event);
    }
    codewhale_telemetry::record(telemetry_session_end());
    if surface == codewhale_telemetry::Surface::Cli {
        let persistence = codewhale_telemetry::persist_local_blocking();
        logging::info(format!(
            "telemetry local persistence outcome={persistence:?}"
        ));
        return;
    }
    // `shutdown_blocking` parks a thread waiting on the writer, so it goes to
    // the blocking pool, and it is bounded there. The persistence actor's
    // unbounded `let _ = task.await` next door is not a pattern to copy here: a
    // hung TLS handshake would hold the process open past the last frame.
    let _ = tokio::time::timeout(
        codewhale_telemetry::SHUTDOWN_FLUSH_TIMEOUT,
        tokio::task::spawn_blocking(|| {
            codewhale_telemetry::shutdown_blocking(codewhale_telemetry::SHUTDOWN_FLUSH_TIMEOUT)
        }),
    )
    .await;
}

async fn run_async_main_inner(
    cli: Cli,
    command: Option<Commands>,
    plugin_discovery: Arc<codewhale_plugins::PluginDiscoveryContext>,
    plugin_registry: Arc<codewhale_plugins::PluginRegistry>,
) -> Result<()> {
    // Install signal handlers that restore the terminal before the process
    // exits. Without this, Ctrl+C delivered while raw mode / kitty keyboard
    // enhancement / alt-screen are active (or in the brief windows around
    // startup and teardown where they're being toggled) leaves the user's shell
    // receiving raw CSI sequences like `^[[>5u` until they run `reset` (#1583).
    //
    // Once the TUI's raw mode is engaged the terminal driver delivers Ctrl+C as
    // the byte 0x03 rather than SIGINT, so the in-TUI key handler — not this
    // handler — is what processes user interrupts during normal operation. This
    // handler exists for the gaps: pre-TUI subcommands (--version, doctor,
    // login, …), the moments around enable_raw_mode / disable_raw_mode, the
    // external-editor suspend path, and SIGTERM / SIGHUP from the OS.
    //
    // It goes up before arming and before the notice: arming is the first
    // externally observable thing this process does (it creates the telemetry
    // buffer), and the notice is the first thing that can sit waiting on a
    // human. A Ctrl-C in either window must still restore the terminal and exit
    // 130 rather than kill the process outright. Recording a `session_end` from
    // the signal path is a no-op until `arm_telemetry` runs, so installing
    // ahead of it collects nothing.
    spawn_signal_cleanup_task();

    let mut command = command;
    if cli.operation_key.is_none()
        && (!io::stdin().is_terminal() || !io::stdout().is_terminal())
        && matches!(command, Some(Commands::Resume { .. }))
        && let Some(Commands::Resume { session_id, last }) = command.take()
    {
        let exec = if last || session_id.is_none() {
            headless_exec_args(None, true, &cli.prompt)?
        } else {
            headless_exec_args(session_id, false, &cli.prompt)?
        };
        command = Some(Commands::Exec(exec));
    }

    // A due interactive disclosure belongs to the native TUI. Presentation
    // does not gate the default-on policy; unreadable privacy state does.
    // Settings can disable this session and erase its pending aggregates.
    let surface = telemetry_surface(command.as_ref());
    let telemetry_notice_plan = if surface == codewhale_telemetry::Surface::Tui {
        codewhale_engine::telemetry_notice::plan_if_due(
            cli.config.clone(),
            telemetry_session_source_for_run(command.as_ref()),
        )
    } else {
        codewhale_engine::telemetry_notice::TelemetryNoticePlan::NotDue
    };
    let should_arm_before_dispatch = surface != codewhale_telemetry::Surface::Tui
        || telemetry_notice_plan.should_arm_before_tui();
    let pending_telemetry_notice = telemetry_notice_plan.into_pending();
    if should_arm_before_dispatch {
        arm_telemetry(&cli, command.as_ref());
    }
    let outcome = Box::pin(run_async_main_dispatch(
        cli,
        command,
        plugin_discovery,
        plugin_registry,
        pending_telemetry_notice,
    ))
    .await;
    finish_telemetry(&outcome, surface).await;
    outcome
}

async fn run_async_main_dispatch(
    cli: Cli,
    command: Option<Commands>,
    plugin_discovery: Arc<codewhale_plugins::PluginDiscoveryContext>,
    plugin_registry: Arc<codewhale_plugins::PluginRegistry>,
    mut pending_telemetry_notice: Option<
        codewhale_engine::telemetry_notice::PendingTelemetryNotice,
    >,
) -> Result<()> {
    logging::set_verbose(cli.verbose || logging::env_requests_verbose_logging());
    anyhow::ensure!(
        cli.operation_key.is_none()
            || matches!(
                command.as_ref(),
                Some(Commands::Resume { .. } | Commands::Fork { .. })
            ),
        "--operation-key is an exact read-only recovery of an explicit resume/fork intent"
    );

    // Install any user prompt overrides from the config directory before an
    // engine can compose a system prompt. The override cells are
    // first-call-wins; doing this once here keeps every downstream turn
    // consistent. Missing files are a no-op (bundled defaults). See #3638.
    codewhale_engine::prompts::load_prompt_overrides_from_config_home();

    // Plugins own one read-only discovery snapshot per process. Initialize it
    // before the subcommand match so plain launch, resume, fork, exec, serve,
    // and every other runtime surface use the same plugin trust decision
    // (#3916, #4399). Discovery never enables, trusts, executes, or persists a
    // bundle.

    let mut command = command;
    if cli.operation_key.is_none()
        && (!io::stdin().is_terminal() || !io::stdout().is_terminal())
        && matches!(command, Some(Commands::Fork { .. }))
        && let Some(Commands::Fork { session_id, last }) = command.take()
    {
        let config = load_config_from_cli(&cli)?;
        let registry = policy_current_registry(std::sync::Arc::clone(&plugin_registry));
        let prepared = prepare_interactive_config(&cli, &config, true)?;
        let source_id = resolve_session_id(session_id, last, &prepared.workspace)?;
        let (_, new_session_id) = prepare_mounted_session(
            &cli,
            prepared,
            source_id,
            MountedHistoryIntent::Fork,
            registry,
        )
        .await?;
        command = Some(Commands::Exec(headless_exec_args(
            Some(new_session_id),
            false,
            &cli.prompt,
        )?));
    }

    // Handle subcommands first
    if let Some(command) = command {
        return match command {
            Commands::Doctor(args) => {
                if let Some(stamp) = &args.restore {
                    return crate::doctor::run_restore(stamp);
                }
                if args.installs {
                    return crate::doctor::run_installs_report(args.json, args.brief);
                }
                let config = match load_doctor_config_from_cli(&cli, &args) {
                    Ok(config) => config,
                    Err(error) if args.json => return run_doctor_json_config_error(&error),
                    Err(error) => bail!(doctor_config_error_text(&error)),
                };
                let workspace = resolve_workspace(&cli);
                if args.repair_sessions {
                    return run_doctor_repair_sessions(args.dry_run);
                }
                if args.context_json {
                    run_doctor_context_json(&config, &workspace)
                } else if args.json {
                    run_doctor_json(
                        &config,
                        &workspace,
                        cli.config.as_deref(),
                        plugin_registry.as_ref(),
                    )
                } else {
                    let probes = crate::doctor::DoctorProbeRequest {
                        check_updates: args.check_updates,
                        probe_api: args.probe_api,
                        probe_local: args.probe_local,
                        probe_mcp: args.probe_mcp,
                        probe_search: args.probe_search,
                    };
                    run_doctor(
                        &config,
                        &workspace,
                        cli.config.as_deref(),
                        effective_config_profile(&cli).as_deref(),
                        probes,
                        plugin_registry.as_ref(),
                        args.all,
                    )
                    .await;
                    if args.fix {
                        let plan = crate::doctor_fix::plan_fixes(
                            &config,
                            &workspace,
                            plugin_registry.as_ref(),
                        );
                        crate::doctor_fix::print_fix_plan(&plan);
                        if !plan.is_empty() && (args.yes || crate::doctor_fix::confirm_fix(&plan)) {
                            let results = crate::doctor_fix::apply_fixes(&plan);
                            crate::doctor_fix::print_apply_results(&results);
                        }
                    }
                    Ok(())
                }
            }
            Commands::SessionDiagnostics(args) => run_session_diagnostics(args),
            Commands::Install { source } => {
                let config = load_config_from_cli(&cli)?;
                codewhale_plugins::mutation::install_from_cli(
                    source,
                    config,
                    (*plugin_registry).clone(),
                )
                .await
            }
            Commands::Plugins { command } => tokio::task::spawn_blocking(move || {
                let _config = load_structural_config_from_cli(&cli)?;
                let workspace = resolve_workspace(&cli);
                let registry = plugin_discovery.inspect_registry_for_workspace(&workspace);
                run_plugins_cli(command, registry)
            })
            .await
            .context("Plugin command failed to finish")?,
            Commands::Setup(args) => {
                let config = load_config_from_cli(&cli)?;
                let workspace = resolve_workspace(&cli);
                run_setup(&config, &workspace, args, plugin_registry.as_ref())
            }
            Commands::RemoteSetup(args) => {
                codewhale_app_server::remote_setup::run_remote_setup(args)
            }
            Commands::Sessions {
                command,
                limit,
                search,
            } => match command {
                None => list_sessions(limit, search),
                Some(SessionsCommand::List { limit, search }) => list_sessions(limit, search),
                Some(SessionsCommand::ScrubSecrets { apply }) => {
                    run_sessions_scrub_secrets(
                        apply,
                        cli.config.clone(),
                        effective_config_profile(&cli),
                    )
                    .await
                }
                Some(SessionsCommand::PruneImages {
                    apply,
                    older_than_days,
                }) => run_sessions_prune_images(apply, older_than_days).await,
                Some(SessionsCommand::Export {
                    id,
                    output,
                    skip_artifacts,
                    compression,
                    force,
                }) => {
                    run_sessions_export(&id, output.as_deref(), skip_artifacts, compression, force)
                }
            },
            Commands::Receipts {
                id,
                last,
                turn,
                format,
            } => receipts::run_receipts_command(
                if last { None } else { id.as_deref() },
                turn.as_deref(),
                format,
            ),
            Commands::Init => init_project(),
            Commands::Login { api_key } => run_login(api_key),
            Commands::Logout => run_logout(),
            Commands::Auth(args) => match args.command {
                TuiAuthCommand::XaiDevice => run_xai_device_auth(cli.config.as_deref()).await,
                TuiAuthCommand::Chatgpt => run_chatgpt_pkce_auth(cli.config.as_deref()).await,
                TuiAuthCommand::ChatgptRevoke => run_chatgpt_pkce_revoke(cli.config.as_deref()),
                TuiAuthCommand::Orcarouter => run_orcarouter_pkce_auth(cli.config.as_deref()).await,
                TuiAuthCommand::OrcarouterRevoke => run_orcarouter_revoke(cli.config.as_deref()),
                TuiAuthCommand::PluginLogin { provider } => {
                    let entry = plugin_auth_entry_from_cli(&cli, &provider).await?;
                    codewhale_plugins::providers::plugin_oauth_login(
                        provider,
                        entry.base_url.clone().unwrap(),
                        entry.oauth.clone().unwrap(),
                        entry.plugin_authority.clone().unwrap(),
                    )
                    .await
                }
                TuiAuthCommand::PluginLogout { provider } => {
                    let entry = plugin_auth_entry_from_cli(&cli, &provider).await?;
                    let policy = codewhale_plugins::activation::extension_host_policy_enabled();
                    tokio::task::spawn_blocking(move || {
                        let _scope = codewhale_plugins::activation::PolicyScope::propagate(policy);
                        codewhale_plugins::registry::verify_plugin_component_authority(
                            entry.plugin_authority.as_ref().unwrap(),
                            codewhale_plugins::activation::PluginActivationCapability::Providers,
                        )
                        .map_err(anyhow::Error::msg)?;
                        codewhale_runtime::oauth::plugin_oauth_logout(
                            &provider,
                            entry.base_url.as_deref().unwrap(),
                            entry.oauth.as_ref().unwrap(),
                        )
                    })
                    .await?
                }
            },
            Commands::Models(args) => {
                let config = load_config_from_cli(&cli)?;
                initialize_cloud_facts(&config);
                run_models(&config, args).await
            }
            Commands::Speech(args) => {
                let config = load_config_from_cli(&cli)?;
                run_speech(&config, args).await
            }
            Commands::Exec(args) => {
                let config = load_config_from_cli(&cli)?;
                let plugin_registry = policy_current_registry(plugin_registry);
                let workspace = cli.workspace.clone().unwrap_or_else(|| {
                    std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
                });
                let mut config = config.clone();
                // #4641: `--no-project-config` skips the workspace-specific
                // `[workspace]`/`[projects]` user-config overlay so a headless
                // launch (e.g. a future Verifiers harness) sees a reproducible
                // config surface that depends only on the explicit `--config`.
                if !cli.no_project_config {
                    merge_user_workspace_config(&mut config, cli.config.clone(), &workspace);
                }
                if let Some(sandbox) = args.sandbox.as_deref() {
                    let _ = parse_sandbox_policy(sandbox, true, Vec::new(), false, false)?;
                    config.sandbox_mode = Some(sandbox.to_ascii_lowercase());
                }
                // Honour CODEWHALE_BASE_URL (forwarded by the CLI dispatcher
                // from --base-url) or the user-set legacy DEEPSEEK_BASE_URL.
                if let Ok(env_url) = std::env::var("CODEWHALE_BASE_URL")
                    .or_else(|_| std::env::var("DEEPSEEK_BASE_URL"))
                {
                    let trimmed = env_url.trim();
                    if !trimmed.is_empty() {
                        let identity = config
                            .active_provider_identity()
                            .map_err(anyhow::Error::msg)?;
                        config
                            .set_provider_base_url_override(&identity, Some(trimmed.to_string()))
                            .map_err(anyhow::Error::msg)?;
                    }
                }
                // Honour `--provider` (#4093): a Fleet worker whose profile pins
                // a provider launches on that provider even when the parent
                // session is on another one. This sets ONLY the non-secret
                // provider identity (`config.provider`); credentials/base URL
                // still resolve from the worker's own env/config, and the
                // endpoint above stays in the previously active provider's
                // own table, so a different pinned provider never reads it.
                // Must precede model
                // resolution so an `auto`/default model resolves to the
                // overridden provider's default.
                let explicit_provider = args
                    .provider
                    .as_deref()
                    .map(str::trim)
                    .filter(|provider| !provider.is_empty());
                if let Some(provider_arg) = explicit_provider {
                    apply_exec_provider_override(&mut config, provider_arg)?;
                }
                let explicit_reasoning = args
                    .reasoning_effort
                    .as_deref()
                    .map(str::trim)
                    .filter(|value| !value.is_empty());
                if let Some(reasoning_arg) = explicit_reasoning {
                    config.reasoning_effort = normalize_cli_reasoning_effort(reasoning_arg)?;
                    config.reasoning_effort_inferred_from_legacy_alias = false;
                }
                initialize_cloud_facts(&config);
                // #6705: OpenCode Zen's per-model wire comes from its
                // Models.dev catalog. Seed the persisted snapshot (disk only,
                // no network) so exec routes the models the picker offers
                // instead of only the ones compiled into this build. This runs
                // unconditionally, as in the interactive path: the final route
                // is not known yet (a selected Fleet operator or `--resume` can
                // still move it onto Zen, and both resolve through the lake).
                // The read and JSON parse are blocking, so they leave the
                // runtime's worker threads.
                if let Err(error) = tokio::task::spawn_blocking(
                    codewhale_engine::models_dev_live::maybe_load_persisted_cache,
                )
                .await
                {
                    tracing::warn!(
                        target: "models_dev_live",
                        %error,
                        "persisted Models.dev cache load did not complete; keeping bundled"
                    );
                }
                if !args.add_dir.is_empty() {
                    let mut extra = Vec::with_capacity(args.add_dir.len());
                    for path in &args.add_dir {
                        let canonical = path.canonicalize().with_context(|| {
                            format!("--add-dir {} does not exist", path.display())
                        })?;
                        if !canonical.is_dir() {
                            bail!("--add-dir {} is not a directory", path.display());
                        }
                        extra.push(canonical);
                    }
                    config.extra_trusted_paths = extra;
                }
                let prompt = resolve_exec_prompt(&args)?;
                let resume_session_id = resolve_exec_resume_session_id(&args, &workspace)?;
                validate_exec_tool_authority_resume(
                    args.tool_authority_json.as_deref(),
                    resume_session_id.is_some(),
                )?;
                let resume_session = resume_session_id
                    .as_deref()
                    .map(load_exec_resume_session)
                    .transpose()?;
                let explicit_model = args
                    .model
                    .as_deref()
                    .map(str::trim)
                    .filter(|model| !model.is_empty());
                if resume_session.is_none() {
                    let explicit_route_override = explicit_provider.is_some()
                        || explicit_model.is_some()
                        || codewhale_runtime::config::explicit_launch_provider_override().is_some()
                        || codewhale_runtime::config::explicit_launch_model_override().is_some();
                    let fleet_route_applied = apply_selected_fleet_operator_for_launch(
                        &mut config,
                        &workspace,
                        explicit_route_override,
                        explicit_reasoning.is_some(),
                    )?;
                    if !explicit_route_override
                        && !fleet_route_applied
                        && let Some(tag) =
                            codewhale_engine::local_ollama::adopt_live_local_ollama_for_run(
                                &mut config,
                                false,
                            )
                            .await
                    {
                        eprintln!(
                            "codewhale exec: no cloud API key, so this run uses local Ollama {tag}; no configuration was changed."
                        );
                    }
                }
                // The `codewhale` dispatcher refuses `--provider`/`--model`
                // after `exec` and forwards the top-level flags as
                // `CODEWHALE_PROVIDER` / `CODEWHALE_MODEL` instead, so a
                // resume must treat those launch overrides as explicit or it
                // silently restores the saved route (cloud-agent e2e,
                // 2026-08-30).
                let (resume_explicit_provider, resume_explicit_model) = exec_resume_route_overrides(
                    explicit_provider,
                    explicit_model,
                    codewhale_runtime::config::explicit_launch_provider_override().as_deref(),
                    codewhale_runtime::config::explicit_launch_model_override().as_deref(),
                );
                let model = if let Some(saved) = resume_session.as_ref() {
                    resolve_exec_resume_route(
                        &mut config,
                        saved,
                        resume_explicit_provider,
                        resume_explicit_model.as_deref(),
                    )?
                } else {
                    resolve_exec_model(&config, explicit_model)
                };
                let force_configured_route = should_force_configured_exec_route(
                    resume_session.is_some(),
                    explicit_provider,
                    explicit_model,
                );
                // A launcher can forward `--yolo` to this binary via the
                // CODEWHALE_YOLO env var (which the config loader folds into
                // `config.yolo`), not as a CLI flag. Honour either source.
                let yolo = cli.yolo || config.yolo.unwrap_or(false);
                let env_tool_surface = exec_tool_surface_from_env();
                // #6510: every exec runs on the Engine — one turn loop, one
                // prompt authority (BASE_PROMPT, AGENTS.md, skills). Without
                // a tool grant the run is a one-shot answer on a zero-tool
                // surface.
                let tool_surface_requested = exec_grants_tool_surface(
                    &args,
                    yolo,
                    resume_session_id.is_some(),
                    env_tool_surface.is_some(),
                );
                {
                    if args.parent_death_watch {
                        spawn_parent_death_watch();
                    }
                    let identity = config
                        .active_provider_identity()
                        .map_err(anyhow::Error::msg)?;
                    let max_subagents = cli.max_subagents.map_or_else(
                        || config.max_subagents_for_provider(&identity),
                        |value| value.clamp(1, MAX_SUBAGENTS),
                    );
                    let auto_mode = args.auto || yolo;
                    // A zero-tool run only spends model steps on output-limit
                    // continuations; without `--max-turns` those would run to
                    // the turn wall clock, so it gets a small default ceiling.
                    let max_turns = exec_max_steps(
                        args.max_turns
                            .or((!tool_surface_requested).then_some(ONE_SHOT_DEFAULT_MAX_STEPS)),
                    );
                    if !tool_surface_requested {
                        let ignored = exec_tool_flags_without_grant(&args);
                        if !ignored.is_empty() {
                            eprintln!(
                                "codewhale exec: {} only apply when tools are offered; drop --no-tools to use them.",
                                ignored.join(", ")
                            );
                        }
                    }
                    let allowed_tools = if tool_surface_requested {
                        resolve_exec_allowed_tools(args.allowed_tools.as_deref(), env_tool_surface)
                    } else {
                        Some(Vec::new())
                    };
                    let disallowed_tools = args
                        .disallowed_tools
                        .as_deref()
                        .map(normalize_exec_tool_names);
                    run_exec_agent(
                        &config,
                        &model,
                        &prompt,
                        workspace,
                        max_subagents,
                        auto_mode,
                        args.allow_sandbox_elevation,
                        args.sandbox.as_deref(),
                        args.json,
                        resume_session,
                        force_configured_route,
                        args.output_format,
                        max_turns,
                        args.max_tool_calls,
                        allowed_tools,
                        disallowed_tools,
                        args.append_system_prompt.clone(),
                        args.tool_authority_json.clone(),
                        args.hooks,
                        std::sync::Arc::clone(&plugin_registry),
                        !tool_surface_requested,
                    )
                    .await
                }
            }
            Commands::Fleet(args) => {
                let config = load_config_from_cli(&cli)?;
                let workspace = resolve_workspace(&cli);
                run_fleet_command(&workspace, &config, args).await
            }
            Commands::WorkflowTool(args) => {
                run_workflow_tool_command(&cli, args, std::sync::Arc::clone(&plugin_registry)).await
            }
            Commands::Review(args) => {
                let config = load_config_from_cli(&cli)?;
                run_review(&config, args).await
            }
            Commands::Pr {
                number,
                repo,
                checkout,
            } => {
                let config = load_config_from_cli(&cli)?;
                run_pr(
                    &cli,
                    &config,
                    number,
                    repo.as_deref(),
                    checkout,
                    pending_telemetry_notice.take(),
                    Arc::clone(&plugin_registry),
                )
                .await
            }
            Commands::Apply(args) => run_apply(args),
            Commands::Eval(args) => run_eval(args),
            Commands::Scorecard(args) => run_scorecard(args),
            Commands::Mcp { command } => {
                let config = load_config_from_cli(&cli)?;
                let workspace = resolve_workspace(&cli);
                run_mcp_command(&config, &workspace, command, plugin_registry.as_ref()).await
            }
            Commands::Features(command) => {
                let config = load_config_from_cli(&cli)?;
                run_features_command(&config, command)
            }
            Commands::Integrations { command } => {
                // Identity derivation is structural: credential-bearing
                // environment values never enter this path.
                let config = load_structural_config_from_cli(&cli)?;
                let workspace = resolve_workspace(&cli);
                integrations::cli::run(&config, &workspace, command)
            }
            Commands::Sandbox(args) => {
                let config = load_config_from_cli(&cli)?;
                run_sandbox_command(&config, args)
            }
            Commands::Serve(args) => {
                let workspace = cli.workspace.clone().unwrap_or_else(|| {
                    std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
                });
                let http_selected = validate_serve_mode_selection(
                    args.mcp,
                    args.http,
                    args.mobile,
                    args.web,
                    args.acp,
                )?;
                if args.mcp {
                    let config = load_structural_config_from_cli(&cli)?;
                    let skills_dir = config.skills_dir();
                    let skills_config = std::sync::Arc::new(config.skills_config());
                    codewhale_app_server::mcp_server::run_mcp_server(
                        workspace,
                        skills_dir,
                        skills_config,
                    )
                    .await
                } else if http_selected {
                    let (mut config, config_profile) =
                        load_config_from_cli_with_effective_profile(&cli)?;
                    initialize_cloud_facts(&config);
                    let explicit_route_override =
                        codewhale_runtime::config::explicit_launch_provider_override().is_some()
                            || codewhale_runtime::config::explicit_launch_model_override()
                                .is_some();
                    apply_selected_fleet_operator_for_launch(
                        &mut config,
                        &workspace,
                        explicit_route_override,
                        false,
                    )?;
                    let cors_origins = resolve_cors_origins(&config, &args.cors_origin);
                    let bind_host = resolve_serve_bind_host(args.mobile, args.host);
                    if args.web && bind_host.host != "127.0.0.1" {
                        bail!("Codewhale web is loopback-only and must bind to 127.0.0.1");
                    }
                    codewhale_app_server::runtime_api::run_http_server(
                        config,
                        workspace,
                        std::sync::Arc::clone(&plugin_discovery),
                        codewhale_app_server::runtime_api::RuntimeApiOptions {
                            host: bind_host.host,
                            port: args.port,
                            workers: args.workers.clamp(1, 8),
                            cors_origins,
                            auth_token: args.auth_token,
                            insecure_no_auth: args.insecure_no_auth,
                            mobile: args.mobile,
                            web: args.web,
                            show_qr: args.qr,
                            config_path: cli.config.clone(),
                            config_profile,
                            control_frontend: cli.control_frontend.clone(),
                            runtime_event_tap: Some(crate::tui::pet_watch::observe_runtime),
                        },
                    )
                    .await
                } else if args.acp {
                    let (config, config_profile) =
                        load_config_from_cli_with_effective_profile(&cli)?;
                    initialize_cloud_facts(&config);
                    let model = config.default_model();
                    codewhale_app_server::acp_server::run_acp_server(
                        config,
                        model,
                        workspace,
                        std::sync::Arc::clone(&plugin_discovery),
                        cli.config.clone(),
                        config_profile,
                    )
                    .await
                } else {
                    unreachable!("server mode count checked above")
                }
            }
            Commands::Resume { session_id, last } => {
                tui::ui::require_interactive_terminal(
                    io::stdin().is_terminal(),
                    io::stdout().is_terminal(),
                )?;
                let config = load_config_from_cli(&cli)?;
                let workspace = resolve_workspace(&cli);
                let resume_id = resolve_session_id(session_id, last, &workspace)?;
                Box::pin(run_interactive(
                    &cli,
                    &config,
                    Some(resume_id),
                    None,
                    pending_telemetry_notice.take(),
                    std::sync::Arc::clone(&plugin_registry),
                ))
                .await
            }
            Commands::Fork { session_id, last } => {
                tui::ui::require_interactive_terminal(
                    io::stdin().is_terminal(),
                    io::stdout().is_terminal(),
                )?;
                let config = load_config_from_cli(&cli)?;
                let plugin_registry = policy_current_registry(plugin_registry);
                let prepared = prepare_interactive_config(&cli, &config, true)?;
                let source_id = resolve_session_id(session_id, last, &prepared.workspace)?;
                let (prepared, new_session_id) = prepare_mounted_session(
                    &cli,
                    prepared,
                    source_id,
                    MountedHistoryIntent::Fork,
                    std::sync::Arc::clone(&plugin_registry),
                )
                .await?;
                Box::pin(run_interactive_prepared(
                    &cli,
                    prepared,
                    Some(new_session_id),
                    None,
                    None,
                    pending_telemetry_notice.take(),
                    std::sync::Arc::clone(&plugin_registry),
                ))
                .await
            }
        };
    }

    tui::ui::require_interactive_terminal(io::stdin().is_terminal(), io::stdout().is_terminal())?;

    // Top-level prompt mode: submit the initial prompt, then keep the TUI alive
    // for follow-up messages. Use `codewhale exec` for explicit non-interactive
    // one-shot behavior (#2370).
    let config = load_config_from_cli(&cli)?;
    if let Some(initial_input) = top_level_prompt_initial_input(&cli.prompt) {
        return Box::pin(run_interactive(
            &cli,
            &config,
            None,
            Some(initial_input),
            pending_telemetry_notice.take(),
            std::sync::Arc::clone(&plugin_registry),
        ))
        .await;
    }

    // Handle session resume. Plain `codewhale` starts fresh: interrupted
    // snapshots are preserved for explicit resume, but never auto-attached.
    let mut startup_notice = None;
    let resume_session_id = if cli.continue_session {
        let workspace = resolve_workspace(&cli);
        resolve_continue_session_id(
            &workspace,
            io::stdin().is_terminal() && io::stdout().is_terminal(),
        )
    } else if let Some(id) = cli.resume.clone() {
        Some(id)
    } else if !cli.fresh {
        let workspace = resolve_workspace(&cli);
        let checkpoint_notice = preserve_interrupted_checkpoint_for_explicit_resume(&workspace);
        // Opt-in auto-resume (#2934). Off by default, so the historical
        // "plain `codewhale` starts fresh" behaviour is unchanged unless the
        // user asked for something else. The decision never resumes an
        // archived, unreadable, or foreign-workspace session; every fallback
        // carries a receipt rather than silently starting blank.
        let (session_id, notice) = resolve_auto_resume(&workspace);
        startup_notice = notice.or(checkpoint_notice);
        session_id
    } else {
        None
    };

    // Default: Interactive TUI
    // --yolo starts in YOLO mode (auto-approve; shell enabled)
    Box::pin(run_interactive_with_notice(
        &cli,
        &config,
        resume_session_id,
        None,
        startup_notice,
        pending_telemetry_notice.take(),
        plugin_registry,
    ))
    .await
}

/// Resolve the opt-in auto-resume setting into a session id plus a receipt.
///
/// Deliberately scoped to the plain interactive launch. `codewhale "do X"`
/// (top-level prompt) and `codewhale exec` are not covered: silently prefixing
/// a one-shot task with a prior conversation would change what is sent to the
/// model, which is not a layout preference the user opted into.
fn resolve_auto_resume(workspace: &Path) -> (Option<String>, Option<String>) {
    use codewhale_engine::session_resume::{ResumeRequest, decide_auto_resume};

    let enabled = codewhale_runtime::settings::Settings::load_persisted()
        .map(|settings| settings.session_auto_resume)
        .unwrap_or(false);
    if !enabled {
        return (None, None);
    }
    let Ok(manager) = SessionManager::default_location() else {
        return (None, None);
    };
    let decision = decide_auto_resume(true, &ResumeRequest::default(), workspace, &manager);
    (
        decision.session_id().map(str::to_string),
        decision.status_message(),
    )
}

fn prepare_cli_startup(
    cli: Cli,
    initialize_plugins: impl FnOnce(),
    load_dotenv: impl FnOnce(),
) -> (Cli, Option<Commands>) {
    initialize_plugins();
    let command = cli.command.clone();
    let should_load_dotenv = match command.as_ref() {
        Some(Commands::Doctor(args)) => args.probe_api || args.probe_local,
        Some(Commands::Plugins { .. }) => false,
        _ => true,
    };
    if should_load_dotenv {
        load_dotenv();
    }
    (cli, command)
}

const MAX_WORKSPACE_DOTENV_BYTES: u64 = 1024 * 1024;

#[derive(Debug, Default)]
struct WorkspaceDotenvReport {
    path: PathBuf,
    loaded: BTreeSet<String>,
    ignored: BTreeSet<String>,
}

/// Load the narrow, data-plane subset of a workspace `.env` before Tokio.
///
/// Repository content is not product authority. In particular, a committed
/// `.env` must not be able to redirect `CODEWHALE_HOME`, config/profile files,
/// MCP servers, plugin trust, executable lookup, sandbox/approval posture, or
/// network destinations. Shell-exported values and config/CLI arguments remain
/// the explicit surfaces for those controls.
fn warn_on_workspace_dotenv_result(workspace: &Path) {
    match load_workspace_dotenv_credentials(workspace) {
        Ok(Some(report)) if !report.ignored.is_empty() => {
            eprintln!(
                "Codewhale ignored non-credential settings in {}: {}. Use config.toml, CLI flags, or the launching shell for control settings.",
                report.path.display(),
                display_env_key_set(&report.ignored)
            );
        }
        Ok(_) => {}
        Err(error) => {
            // The error intentionally contains no file contents or parsed
            // values. A malformed or unsafe workspace file fails closed while
            // shell/config credentials remain available.
            eprintln!("Codewhale did not load workspace .env: {error}");
        }
    }
}

fn display_env_key_set(keys: &BTreeSet<String>) -> String {
    const MAX_DISPLAYED: usize = 12;
    let mut labels = keys
        .iter()
        .take(MAX_DISPLAYED)
        .map(|key| {
            if key
                .chars()
                .all(|ch| ch.is_ascii_uppercase() || ch.is_ascii_digit() || ch == '_')
            {
                key.as_str()
            } else {
                "<invalid-name>"
            }
        })
        .collect::<Vec<_>>();
    if keys.len() > MAX_DISPLAYED {
        labels.push("...");
    }
    labels.join(", ")
}

fn load_workspace_dotenv_credentials(workspace: &Path) -> Result<Option<WorkspaceDotenvReport>> {
    let Some(path) = find_workspace_dotenv(workspace)? else {
        return Ok(None);
    };
    load_workspace_dotenv_credentials_from_path(&path).map(Some)
}

/// The nearest `.env` from the resolved launch workspace (`--workspace`, else
/// the current directory) up to its repository root. Searching from the
/// process directory instead would load another tree's credentials when the
/// two differ.
fn find_workspace_dotenv(workspace: &Path) -> Result<Option<PathBuf>> {
    let start = if workspace.is_absolute() {
        workspace.to_path_buf()
    } else {
        std::env::current_dir()
            .context("could not resolve the current workspace")?
            .join(workspace)
    };
    let boundary = start
        .ancestors()
        .find(|ancestor| std::fs::symlink_metadata(ancestor.join(".git")).is_ok())
        .unwrap_or(start.as_path());

    for ancestor in start.ancestors() {
        let candidate = ancestor.join(".env");
        match std::fs::symlink_metadata(&candidate) {
            Ok(_) => return Ok(Some(candidate)),
            Err(error) if error.kind() == io::ErrorKind::NotFound => {}
            Err(error) => {
                return Err(anyhow!(
                    "could not inspect {}: {error}",
                    candidate.display()
                ));
            }
        }
        if ancestor == boundary {
            break;
        }
    }
    Ok(None)
}

fn load_workspace_dotenv_credentials_from_path(path: &Path) -> Result<WorkspaceDotenvReport> {
    let contents = read_stable_workspace_dotenv(path)?;
    let text = std::str::from_utf8(&contents)
        .map_err(|_| anyhow!("{} is not valid UTF-8", path.display()))?;
    if dotenv_has_variable_expansion(text) {
        bail!(
            "{} uses variable expansion; workspace .env values must be literal to prevent ambient-secret substitution",
            path.display()
        );
    }

    let mut report = WorkspaceDotenvReport {
        path: path.to_path_buf(),
        ..WorkspaceDotenvReport::default()
    };
    let entries = dotenvy::from_read_iter(std::io::Cursor::new(contents))
        .collect::<std::result::Result<Vec<_>, _>>()
        .map_err(|_| anyhow!("{} could not be parsed safely", path.display()))?;
    for entry in entries {
        let (key, value) = entry;
        if !is_workspace_dotenv_credential_key(&key) {
            report.ignored.insert(key);
            continue;
        }
        if std::env::var_os(&key).is_some() {
            continue;
        }

        // SAFETY: this loader runs synchronously in `main` before the runtime
        // owner or Tokio workers are spawned. No concurrent environment reader
        // exists inside Codewhale, and later startup code treats this process
        // environment as immutable.
        unsafe { std::env::set_var(&key, value) };
        report.loaded.insert(key);
    }
    Ok(report)
}

fn is_workspace_dotenv_credential_key(key: &str) -> bool {
    codewhale_config::provider::providers_sorted_for_display()
        .into_iter()
        .any(|provider| provider.env_vars().contains(&key))
        || matches!(
            key,
            "DEEPSEEK_SEARCH_API_KEY"
                | "SOFYA_API_KEY"
                | "SERPLY_API_KEY"
                | "METASO_API_KEY"
                | "BAIDU_SEARCH_API_KEY"
                | "TAVILY_API_KEY"
                | "DEEPSEEK_SANDBOX_API_KEY"
        )
}

fn dotenv_has_variable_expansion(contents: &str) -> bool {
    let mut escaped = false;
    let mut single_quoted = false;
    let mut double_quoted = false;
    let mut comment = false;

    for ch in contents.chars() {
        if comment {
            // Reject expansion markers even in comments. This is deliberately
            // conservative, and ignoring other comment text prevents an
            // unmatched quote there from changing how the next line is read.
            if ch == '$' {
                return true;
            }
            if ch == '\n' {
                comment = false;
                escaped = false;
            }
            continue;
        }
        if single_quoted {
            if ch == '\'' {
                single_quoted = false;
            }
            continue;
        }
        if escaped {
            escaped = false;
            continue;
        }
        if ch == '\\' {
            escaped = true;
            continue;
        }
        if ch == '\'' && !double_quoted {
            single_quoted = true;
            continue;
        }
        if ch == '"' {
            double_quoted = !double_quoted;
            continue;
        }
        if ch == '#' && !double_quoted {
            comment = true;
            continue;
        }
        if ch == '$' {
            return true;
        }
    }
    false
}

fn read_stable_workspace_dotenv(path: &Path) -> Result<Vec<u8>> {
    let mut file = open_workspace_dotenv_without_following_links(path)?;
    let metadata = file
        .metadata()
        .map_err(|error| anyhow!("could not inspect {}: {error}", path.display()))?;
    if !metadata.is_file() {
        bail!("{} is not a regular file", path.display());
    }
    if workspace_dotenv_has_multiple_links(&file, &metadata)? {
        bail!(
            "{} has multiple filesystem links, not a unique workspace-owned file",
            path.display()
        );
    }
    if metadata.len() > MAX_WORKSPACE_DOTENV_BYTES {
        bail!(
            "{} exceeds the {} byte workspace .env limit",
            path.display(),
            MAX_WORKSPACE_DOTENV_BYTES
        );
    }

    let mut contents = Vec::with_capacity(metadata.len() as usize);
    (&mut file)
        .take(MAX_WORKSPACE_DOTENV_BYTES + 1)
        .read_to_end(&mut contents)
        .map_err(|error| anyhow!("could not read {}: {error}", path.display()))?;
    if contents.len() as u64 > MAX_WORKSPACE_DOTENV_BYTES {
        bail!(
            "{} exceeds the {} byte workspace .env limit",
            path.display(),
            MAX_WORKSPACE_DOTENV_BYTES
        );
    }
    Ok(contents)
}

#[cfg(unix)]
fn workspace_dotenv_has_multiple_links(
    _file: &std::fs::File,
    metadata: &std::fs::Metadata,
) -> Result<bool> {
    use std::os::unix::fs::MetadataExt;

    Ok(metadata.nlink() > 1)
}

#[cfg(windows)]
fn workspace_dotenv_has_multiple_links(
    file: &std::fs::File,
    _metadata: &std::fs::Metadata,
) -> Result<bool> {
    use std::os::windows::io::AsRawHandle;
    use windows::Win32::Foundation::HANDLE;
    use windows::Win32::Storage::FileSystem::{
        BY_HANDLE_FILE_INFORMATION, GetFileInformationByHandle,
    };

    let mut information = BY_HANDLE_FILE_INFORMATION::default();
    // SAFETY: `file` owns a live kernel handle for the already-open `.env`;
    // `information` remains writable for the duration of this synchronous
    // call. No path lookup or re-open occurs here.
    unsafe {
        GetFileInformationByHandle(HANDLE(file.as_raw_handle()), &mut information)
            .map_err(|error| anyhow!("could not inspect workspace .env link count: {error}"))?;
    }
    Ok(information.nNumberOfLinks > 1)
}

#[cfg(not(any(unix, windows)))]
fn workspace_dotenv_has_multiple_links(
    _file: &std::fs::File,
    _metadata: &std::fs::Metadata,
) -> Result<bool> {
    Ok(false)
}

#[cfg(unix)]
fn open_workspace_dotenv_without_following_links(path: &Path) -> Result<std::fs::File> {
    use std::os::unix::fs::OpenOptionsExt;

    std::fs::OpenOptions::new()
        .read(true)
        // `O_NONBLOCK` is inert for regular files but prevents a FIFO named
        // `.env` from hanging startup before the metadata check can reject it.
        .custom_flags(libc::O_CLOEXEC | libc::O_NOFOLLOW | libc::O_NONBLOCK)
        .open(path)
        .map_err(|error| anyhow!("could not securely open {}: {error}", path.display()))
}

#[cfg(windows)]
fn open_workspace_dotenv_without_following_links(path: &Path) -> Result<std::fs::File> {
    use std::os::windows::fs::{MetadataExt, OpenOptionsExt};

    const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x0000_0400;
    const FILE_FLAG_OPEN_REPARSE_POINT: u32 = 0x0020_0000;
    let file = std::fs::OpenOptions::new()
        .read(true)
        .custom_flags(FILE_FLAG_OPEN_REPARSE_POINT)
        .open(path)
        .map_err(|error| anyhow!("could not securely open {}: {error}", path.display()))?;
    let metadata = file
        .metadata()
        .map_err(|error| anyhow!("could not inspect {}: {error}", path.display()))?;
    if metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0 {
        bail!(
            "{} is a reparse point, not a workspace-owned file",
            path.display()
        );
    }
    Ok(file)
}

#[cfg(not(any(unix, windows)))]
fn open_workspace_dotenv_without_following_links(path: &Path) -> Result<std::fs::File> {
    let metadata = std::fs::symlink_metadata(path)
        .map_err(|error| anyhow!("could not inspect {}: {error}", path.display()))?;
    if metadata.file_type().is_symlink() {
        bail!(
            "{} is a symbolic link, not a workspace-owned file",
            path.display()
        );
    }
    std::fs::File::open(path)
        .map_err(|error| anyhow!("could not securely open {}: {error}", path.display()))
}

/// Run the offline evaluation harness (no network/LLM calls).
fn run_eval(args: EvalArgs) -> Result<()> {
    let fail_step = match args.fail_step.as_deref() {
        Some(value) => ScenarioStepKind::parse(value)
            .map(Some)
            .ok_or_else(|| anyhow!("invalid --fail-step '{value}'"))?,
        None => None,
    };

    let config = EvalHarnessConfig {
        fail_step,
        shell_command: args.shell_command,
        shell_expect_token: args.shell_expect_token,
        max_output_chars: args.max_output_chars,
        record_dir: args.record.clone(),
        ..EvalHarnessConfig::default()
    };

    let harness = EvalHarness::new(config);
    let run = harness.run().context("evaluation harness failed")?;
    let report = run.to_report();

    if args.json {
        let json = serde_json::to_string_pretty(&report)?;
        println!("{json}");
    } else {
        println!("Offline Eval Harness");
        println!("scenario: {}", report.scenario_name);
        println!("workspace: {}", report.workspace_root.display());
        println!("success: {}", report.metrics.success);
        println!("steps: {}", report.metrics.steps);
        println!("tool_errors: {}", report.metrics.tool_errors);
        println!("duration_ms: {}", report.metrics.duration.as_millis());

        if !report.metrics.per_tool.is_empty() {
            println!("per_tool:");
            for (kind, stats) in &report.metrics.per_tool {
                println!(
                    "  {} invocations={} errors={} duration_ms={}",
                    kind.tool_name(),
                    stats.invocations,
                    stats.errors,
                    stats.total_duration.as_millis()
                );
            }
        }

        let failed_steps: Vec<_> = report.steps.iter().filter(|s| !s.success).collect();
        if !failed_steps.is_empty() {
            println!("failed_steps:");
            for step in failed_steps {
                let error = step.error.as_deref().unwrap_or("unknown error");
                println!(
                    "  {} tool={} error={}",
                    step.kind.tool_name(),
                    step.tool_name,
                    error
                );
            }
        }
    }

    if report.metrics.success {
        Ok(())
    } else {
        bail!("offline evaluation harness reported failure")
    }
}

/// A regression gate threshold must be a finite percentage: `NaN` compares
/// false against every change (a gate that always passes) and infinity
/// disables the gate outright.
fn parse_regression_threshold(raw: &str) -> Result<f64, String> {
    let value: f64 = raw
        .trim()
        .parse()
        .map_err(|_| format!("`{raw}` is not a number"))?;
    if !value.is_finite() {
        return Err(format!("`{raw}` is not a finite percentage"));
    }
    Ok(value)
}

/// Score a run's token/cache/cost from recorded turns and (optionally) flag
/// regressions against a committed baseline. Offline: reads recorded usage from
/// a JSON file, reuses the pricing layer, never calls a model. Exits non-zero
/// when a baseline is supplied and a metric regresses past the threshold, so it
/// can be wired as a release gate (#3388).
fn run_scorecard(args: ScorecardArgs) -> Result<()> {
    use codewhale_engine::scorecard::{RecordedTurn, Scorecard, ScorecardMetrics};

    let raw = std::fs::read_to_string(&args.input)
        .with_context(|| format!("failed to read scorecard input {}", args.input.display()))?;
    let recorded: Vec<RecordedTurn> = serde_json::from_str(&raw)
        .with_context(|| format!("failed to parse scorecard input {}", args.input.display()))?;

    let card = Scorecard::from_recorded_turns(&recorded);

    let regressions = match &args.baseline {
        Some(path) => {
            let baseline_raw = std::fs::read_to_string(path)
                .with_context(|| format!("failed to read baseline {}", path.display()))?;
            let baseline: ScorecardMetrics = serde_json::from_str(&baseline_raw)
                .with_context(|| format!("failed to parse baseline {}", path.display()))?;
            card.metrics.regressions_against(&baseline, args.threshold)
        }
        None => Vec::new(),
    };

    if args.json {
        let out = serde_json::json!({
            "per_turn": card.per_turn,
            "metrics": card.metrics,
            "regressions": regressions,
        });
        println!("{}", serde_json::to_string_pretty(&out)?);
    } else {
        print!("{}", card.to_summary());
        for r in &regressions {
            println!(
                "REGRESSION {}: baseline {:.4} -> current {:.4} (+{:.1}%)",
                r.metric, r.baseline, r.current, r.pct_increase
            );
        }
    }

    if regressions.is_empty() {
        Ok(())
    } else {
        bail!(
            "{} metric(s) regressed past the {:.1}% threshold",
            regressions.len(),
            args.threshold
        )
    }
}

async fn run_fleet_command(workspace: &Path, config: &Config, args: FleetArgs) -> Result<()> {
    use codewhale_engine::fleet::alerts::{
        FleetAlertAdapterConfig, FleetAlertConfig, FleetAlertDispatcher, FleetAlertEvent,
        FleetEnvSecretResolver,
    };
    use codewhale_engine::fleet::control as fleet_control;
    use codewhale_engine::fleet::executor::FleetExecutor;
    use codewhale_engine::fleet::manager::{
        FleetManager, FleetStatusSnapshot, FleetWorkerInspection,
    };
    use codewhale_lane::{ControlOperation, ControlSurface};
    use codewhale_protocol::fleet::{FleetAlertEventClass, FleetArtifactKind, FleetRunId};

    // Every label and every row below comes from the shared Fleet control
    // surface, so `codewhale fleet …` and `/fleet …` cannot drift in how they
    // describe the same durable ledger (#1888, #4022).
    fn print_status(status: &FleetStatusSnapshot) {
        println!("{}", fleet_control::render_fleet_status_snapshot(status));
    }

    fn print_inspection(inspection: &FleetWorkerInspection) {
        println!("{}", fleet_control::render_inspection(inspection));
    }

    fn print_artifacts(inspection: &FleetWorkerInspection) {
        println!("{}", fleet_control::render_artifacts(inspection));
    }

    /// Print one shared control receipt on the CLI surface.
    fn emit_fleet_receipt(receipt: &codewhale_lane::ControlReceipt) -> Result<()> {
        if receipt.is_error() {
            eprintln!("{}", receipt.render());
            let detail = receipt
                .failure
                .as_ref()
                .map(ToString::to_string)
                .unwrap_or_else(|| receipt.outcome.as_str().to_string());
            bail!("{}: {detail}", receipt.operation_id);
        }
        println!("{}", receipt.render());
        Ok(())
    }

    fn print_logs(workspace: &Path, inspection: &FleetWorkerInspection) -> Result<()> {
        let mut printed = false;
        for artifact in inspection
            .artifacts
            .iter()
            .filter(|artifact| matches!(artifact.kind, FleetArtifactKind::Log))
        {
            let path = workspace.join(&artifact.path);
            println!("== {} ==", artifact.path.display());
            let contents = std::fs::read_to_string(&path)
                .with_context(|| format!("reading Fleet log {}", path.display()))?;
            let preview: String = contents.chars().take(16 * 1024).collect();
            // Worker logs can contain captured terminal bytes (a child TUI's
            // mouse-tracking handshake, SGR, OSC). Printing them raw would
            // re-arm mouse reporting in the caller's shell and leave it
            // executing escape fragments after this command exits.
            let mut safe_preview = String::with_capacity(preview.len());
            codewhale_tui_kit::osc8::strip_ansi_into(&preview, &mut safe_preview);
            print!("{safe_preview}");
            if contents.chars().count() > preview.chars().count() {
                println!("\n[truncated]");
            } else if !preview.ends_with('\n') {
                println!();
            }
            printed = true;
        }
        if !printed {
            println!("logs: none");
        }
        Ok(())
    }

    fn alert_event_class(arg: FleetAlertEventArg) -> FleetAlertEventClass {
        match arg {
            FleetAlertEventArg::Stale => FleetAlertEventClass::Stale,
            FleetAlertEventArg::RestartExhausted => FleetAlertEventClass::RestartExhausted,
            FleetAlertEventArg::NeedsHuman => FleetAlertEventClass::NeedsHuman,
            FleetAlertEventArg::BudgetExceeded => FleetAlertEventClass::BudgetExceeded,
            FleetAlertEventArg::VerifierFailed => FleetAlertEventClass::VerifierFailed,
            FleetAlertEventArg::RunCompleted => FleetAlertEventClass::RunCompleted,
        }
    }

    fn alert_status(class: FleetAlertEventClass, override_status: Option<String>) -> String {
        if let Some(status) = override_status {
            return status;
        }
        match class {
            FleetAlertEventClass::Stale => "stale",
            FleetAlertEventClass::RestartExhausted => "failed",
            FleetAlertEventClass::NeedsHuman => "needs_human",
            FleetAlertEventClass::BudgetExceeded => "budget_exceeded",
            FleetAlertEventClass::VerifierFailed => "verifier_failed",
            FleetAlertEventClass::RunCompleted => "completed",
        }
        .to_string()
    }

    fn alert_adapter(args: &FleetAlertDryRunArgs) -> FleetAlertAdapterConfig {
        match args.adapter {
            FleetAlertAdapterArg::Slack => FleetAlertAdapterConfig::Slack {
                webhook_env: args.slack_webhook_env.clone(),
                channel: None,
            },
            FleetAlertAdapterArg::Webhook => FleetAlertAdapterConfig::Webhook {
                url_env: args.webhook_url_env.clone(),
                secret_env: args.webhook_secret_env.clone(),
            },
            FleetAlertAdapterArg::PagerDuty => FleetAlertAdapterConfig::PagerDuty {
                routing_key_env: args.pagerduty_routing_key_env.clone(),
                severity: args.pagerduty_severity.clone(),
            },
        }
    }

    let fleet_config = config.fleet_config();
    // `fleet run --check` must not conjure the ledger or the sub-agent state it
    // would write to, so it validates before either is opened below.
    if let FleetCommand::Run(run_args) = &args.command
        && run_args.check
    {
        initialize_cloud_facts(config);
        let check = FleetManager::check_task_spec_path_in(
            workspace,
            fleet_config,
            config.default_model(),
            config.clone(),
            &run_args.task_spec,
        )?;
        println!(
            "Fleet spec ok: {} ({} task{}). Nothing was created or launched.",
            run_args.task_spec.display(),
            check.task_count,
            if check.task_count == 1 { "" } else { "s" }
        );
        for warning in &check.warnings {
            println!("warning: {warning}");
        }
        return Ok(());
    }

    let identity = config
        .active_provider_identity()
        .map_err(anyhow::Error::msg)?;

    // Probe the durable ledger *before* opening the manager: FleetManager::open
    // creates `.codewhale/fleet.jsonl` as a side effect, so a later probe would
    // always find a ledger and the CLI would report availability differently
    // from the slash surface for the same workspace (#4022).
    let fleet_context = fleet_control::fleet_control_context(workspace);
    // Probing is not enough on its own: `FleetManager::open` *creates* the
    // ledger, and it used to run for every subcommand before this match. That
    // made `codewhale fleet status` in a ledgerless workspace print
    // "no_fleet_ledger" while simultaneously creating the file it said was
    // missing — and the next invocation then reported an empty ledger as if a
    // Fleet had existed all along. Refuse the control verbs here, before the
    // manager exists, so the CLI and `/fleet` agree and neither surface
    // conjures the store it is reporting on (#4022).
    if let Some(operation) = match &args.command {
        FleetCommand::List => Some(ControlOperation::FleetList),
        FleetCommand::Status => Some(ControlOperation::FleetStatus),
        FleetCommand::Interrupt { .. } => Some(ControlOperation::FleetInterrupt),
        FleetCommand::Resume { .. } => Some(ControlOperation::FleetResume),
        _ => None,
    } {
        let descriptor = operation.descriptor();
        let availability = descriptor.availability(ControlSurface::Cli, fleet_context);
        if !availability.is_available() {
            return emit_fleet_receipt(&codewhale_lane::ControlReceipt::unavailable(
                descriptor,
                ControlSurface::Cli,
                availability,
            ));
        }
    }

    // The configured route is the operator: fleet workers without a
    // task/profile model pin inherit the session's active model.
    let owned_workspace = workspace.to_path_buf();
    let owned_config = config.clone();
    let manager = codewhale_runtime::runtime_control::owner_work(move || {
        let config = &owned_config;
        let max_subagents = config.max_subagents_for_provider(&identity);
        let coordination_manager =
            codewhale_engine::tools::subagent::new_shared_subagent_manager_with_timeout(
                owned_workspace.clone(),
                max_subagents,
                config
                    .max_admitted_subagents_for_provider(&identity)
                    .max(max_subagents),
                Duration::from_secs(config.subagent_heartbeat_timeout_secs_for_provider(&identity)),
                config.launch_concurrency_for_provider(&identity),
            );
        let manager = FleetManager::open(&owned_workspace)?
            .with_exec_config(fleet_config.exec.clone())
            .with_fleet_config(fleet_config)
            .with_sub_agent_manager(coordination_manager)
            .with_session_model(config.default_model())
            .with_route_config(config.clone());
        Ok(manager)
    })
    .await?;
    match args.command {
        FleetCommand::Init => {
            println!("Fleet ledger: {}", manager.ledger_path().display());
            Ok(())
        }
        FleetCommand::Run(args) => {
            initialize_cloud_facts(config);
            let max_workers = args.max_workers.clamp(1, 128);
            let manager =
                manager.with_stale_after(Duration::from_secs(args.stale_after_seconds.max(1)));
            let launch_manager = manager.clone();
            let task_spec = args.task_spec.clone();
            let report = codewhale_runtime::runtime_control::owner_work(move || {
                launch_manager.create_run_from_task_spec_path(&task_spec, max_workers)
            })
            .await?;
            println!(
                "Fleet run: {} tasks={} leased={} queued={}",
                report.run_id.0, report.task_count, report.leased, report.queued
            );
            for warning in &report.warnings {
                println!("warning: {warning}");
            }
            println!("workers:");
            for worker_id in &report.worker_ids {
                println!("  {worker_id}");
            }
            if args.once {
                print_status(&manager.run_status(&report.run_id)?);
                return Ok(());
            }
            println!(
                "manager loop running; use `codewhale fleet status`, `inspect`, `interrupt`, or `stop --all` from another terminal."
            );
            let executor = FleetExecutor::new(workspace)
                .with_sessions_dir(session_manager::default_sessions_dir()?);
            let codewhale_binary = fleet::executor::configured_codewhale_binary();
            let status = manager
                .run_to_completion(
                    &report.run_id,
                    max_workers,
                    executor,
                    &codewhale_binary,
                    None,
                    Duration::from_secs(2),
                )
                .await?
                .0;
            print_status(&status);
            Ok(())
        }
        FleetCommand::List => emit_fleet_receipt(&fleet_control::execute_fleet_control_with(
            ControlSurface::Cli,
            workspace,
            fleet_context,
            &manager,
            ControlOperation::FleetList,
            None,
        )),
        FleetCommand::Status => emit_fleet_receipt(&fleet_control::execute_fleet_control_with(
            ControlSurface::Cli,
            workspace,
            fleet_context,
            &manager,
            ControlOperation::FleetStatus,
            None,
        )),
        FleetCommand::Inspect { worker_id } => {
            print_inspection(&manager.inspect_worker(&worker_id)?);
            Ok(())
        }
        FleetCommand::Logs { worker_id } => {
            let inspection = manager.inspect_worker(&worker_id)?;
            print_logs(workspace, &inspection)
        }
        FleetCommand::Artifacts { worker_id } => {
            let inspection = manager.inspect_worker(&worker_id)?;
            print_artifacts(&inspection);
            Ok(())
        }
        FleetCommand::Interrupt { worker_id } => {
            emit_fleet_receipt(&fleet_control::execute_fleet_control_with(
                ControlSurface::Cli,
                workspace,
                fleet_context,
                &manager,
                ControlOperation::FleetInterrupt,
                Some(&worker_id),
            ))
        }
        FleetCommand::Restart { worker_id } => {
            let restart_manager = manager.clone();
            let report = codewhale_runtime::runtime_control::owner_work(move || {
                restart_manager.restart_worker(&worker_id)
            })
            .await?;
            print_inspection(&report.inspection);
            println!(
                "manager loop running for restarted run {}; use `codewhale fleet status`, `inspect`, `interrupt`, or `stop --all` from another terminal.",
                report.run_id.0
            );
            let executor = FleetExecutor::new(workspace)
                .with_sessions_dir(session_manager::default_sessions_dir()?);
            let codewhale_binary = fleet::executor::configured_codewhale_binary();
            let status = manager
                .run_to_completion(
                    &report.run_id,
                    report.max_workers,
                    executor,
                    &codewhale_binary,
                    None,
                    Duration::from_secs(2),
                )
                .await?
                .0;
            print_status(&status);
            Ok(())
        }
        FleetCommand::Resume {
            run_id,
            stale_after_seconds,
        } => {
            let manager = manager.with_stale_after(Duration::from_secs(stale_after_seconds.max(1)));
            let owned_workspace = workspace.to_path_buf();
            let receipt = codewhale_runtime::runtime_control::owner_work(move || {
                Ok(fleet_control::execute_fleet_control_with(
                    ControlSurface::Cli,
                    &owned_workspace,
                    fleet_context,
                    &manager,
                    ControlOperation::FleetResume,
                    Some(&run_id),
                ))
            })
            .await?;
            emit_fleet_receipt(&receipt)
        }
        FleetCommand::Stop { all } => {
            if !all {
                bail!("pass --all to stop all Fleet work");
            }
            let stopped = manager.stop_all()?;
            println!("stopped: {stopped}");
            Ok(())
        }
        FleetCommand::AlertDryRun(args) => {
            let class = alert_event_class(args.event);
            let adapter = alert_adapter(&args);
            let event = FleetAlertEvent {
                class,
                run_id: FleetRunId::from(args.run_id.clone()),
                worker_id: args.worker_id.clone(),
                task_id: args.task_id.clone(),
                status: alert_status(class, args.status.clone()),
                reason: args.reason.clone(),
            };
            let dispatcher = FleetAlertDispatcher::new(
                FleetAlertConfig::dry_run_for_adapter(adapter),
                FleetEnvSecretResolver,
            );
            let deliveries = dispatcher.dispatch(&event)?;
            for delivery in deliveries {
                println!(
                    "{}",
                    serde_json::to_string_pretty(&delivery.redacted_payload)?
                );
            }
            Ok(())
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum WriteStatus {
    Created,
    Overwritten,
    SkippedExists,
}

fn ensure_parent_dir(path: &Path) -> Result<()> {
    if let Some(parent) = path.parent()
        && !parent.as_os_str().is_empty()
    {
        std::fs::create_dir_all(parent)
            .with_context(|| format!("Failed to create directory for {}", parent.display()))?;
    }
    Ok(())
}

fn write_template_file(path: &Path, contents: &str, force: bool) -> Result<WriteStatus> {
    ensure_parent_dir(path)?;

    if path.exists() && !force {
        return Ok(WriteStatus::SkippedExists);
    }

    let status = if path.exists() {
        WriteStatus::Overwritten
    } else {
        WriteStatus::Created
    };

    std::fs::write(path, contents)
        .with_context(|| format!("Failed to write template at {}", path.display()))?;

    Ok(status)
}

fn init_mcp_config(path: &Path, force: bool) -> Result<WriteStatus> {
    Ok(match codewhale_engine::mcp::init_config(path, force)? {
        codewhale_engine::mcp::McpWriteStatus::Created => WriteStatus::Created,
        codewhale_engine::mcp::McpWriteStatus::Overwritten => WriteStatus::Overwritten,
        codewhale_engine::mcp::McpWriteStatus::SkippedExists => WriteStatus::SkippedExists,
    })
}

fn skills_template(name: &str) -> String {
    format!(
        "\
---\n\
name: {name}\n\
description: Quick repo diagnostics and setup guidance\n\
allowed-tools: diagnostics, list_dir, read_file, grep_files, git_status, git_diff\n\
---\n\n\
When this skill is active:\n\
1. Run the diagnostics tool to report workspace and sandbox status.\n\
2. Skim key project files (README.md, Cargo.toml, AGENTS.md) before editing.\n\
3. Prefer small, validated changes and summarize what you verified.\n\
"
    )
}

fn init_skills_dir(skills_dir: &Path, force: bool) -> Result<(PathBuf, WriteStatus)> {
    std::fs::create_dir_all(skills_dir)
        .with_context(|| format!("Failed to create skills dir {}", skills_dir.display()))?;

    let skill_name = "getting-started";
    let skill_path = skills_dir.join(skill_name).join("SKILL.md");
    ensure_parent_dir(&skill_path)?;

    let status = write_template_file(&skill_path, &skills_template(skill_name), force)?;
    Ok((skill_path, status))
}

fn tools_readme_template() -> &'static str {
    "# Local tools\n\n\
     Drop self-describing scripts here so they can be discovered by\n\
     `codewhale setup --status` and surfaced in `codewhale doctor`.\n\n\
     When `[tools.plugin_dir]` is set in config.toml (or when the default\n\
     `~/.codewhale/tools/` directory exists), they are auto-discovered and\n\
     registered as model-visible tools.\n\n\
     Each script should start with a frontmatter-style header so the\n\
     description is visible without executing the file and the agent knows\n\
     the tool name, description, and input schema:\n\n\
     ```\n\
     # name: my-tool\n\
     # description: One-line summary of what this tool does\n\
     # usage: my-tool [args...]\n\
     ```\n\n\
     The directory is intentionally not auto-loaded into the agent's tool\n\
     catalog. Wire individual tools through MCP, hooks, or skills when you\n\
     want them available inside a session.\n"
}

fn tools_example_script() -> &'static str {
    "#!/usr/bin/env sh\n\
     # name: example\n\
     # description: Print a confirmation that local tool discovery works\n\
     # usage: example [name]\n\
     printf 'codewhale local tool ok: %s\\n' \"${1:-world}\"\n"
}

fn init_tools_dir(tools_dir: &Path, force: bool) -> Result<(PathBuf, WriteStatus, WriteStatus)> {
    std::fs::create_dir_all(tools_dir)
        .with_context(|| format!("Failed to create tools dir {}", tools_dir.display()))?;

    let readme_path = tools_dir.join("README.md");
    let readme_status = write_template_file(&readme_path, tools_readme_template(), force)?;

    let example_path = tools_dir.join("example.sh");
    let example_status = write_template_file(&example_path, tools_example_script(), force)?;

    Ok((tools_dir.to_path_buf(), readme_status, example_status))
}

fn plugins_readme_template() -> &'static str {
    "# Local plugins\n\n\
     Each Codewhale plugin bundle lives in its own subdirectory with a\n\
     versioned `plugin.toml`. User bundles live here; workspace bundles live\n\
     under `<workspace>/.codewhale/plugins/`. Both are discovered read-only,\n\
     untrusted, and disabled by default.\n\n\
     A v0.9.1 bundle layout looks like:\n\n\
     ```\n\
     plugins/\n\
       my-plugin/\n\
         plugin.toml\n\
         skills/\n\
           my-skill/SKILL.md\n\
     ```\n\n\
     Run `/plugin validate`, `/plugin show <name>`, then `/plugin enable <name>`.\n\
     Enablement opens a content- and capability-bound trust review;\n\
     confirm the displayed `/plugin trust` command to create an owner-only,\n\
     content-addressed runtime snapshot, then enable the bundle. Remote MCP\n\
     authentication must name environment sources; never store secret values\n\
     in `plugin.toml`.\n\n\
     Codewhale activates declarative Skills, MCP servers, Commands, Agent\n\
     profiles, and Hooks through their existing engines. LSP, native\n\
     extensions, filesystem grants, and lifecycle mutation stay inventoried\n\
     and inactive; a mixed bundle can still activate supported components.\n\
     Marketplace catalogs, install, update, and uninstall all feed this same\n\
     disabled-and-untrusted review path; none grants automatic trust. Codewhale\n\
     does not scan other applications for ambient plugins.\n"
}

fn plugin_example_manifest_template() -> &'static str {
    "schema_version = 1\n\n\
     [plugin]\n\
     name = \"example\"\n\
     version = \"0.1.0\"\n\
     description = \"Starter Codewhale plugin bundle\"\n\n\
     [skills]\n\
     path = \"skills\"\n"
}

fn plugin_example_skill_template() -> &'static str {
    "---\n\
     name: hello\n\
     description: Explain that the example plugin bundle is active.\n\
     ---\n\n\
     Tell the user this instruction came from the namespaced\n\
     `example:hello` plugin skill. Do not perform side effects.\n"
}

fn init_plugins_dir(
    plugins_dir: &Path,
    force: bool,
) -> Result<(
    PathBuf,
    PathBuf,
    PathBuf,
    WriteStatus,
    WriteStatus,
    WriteStatus,
)> {
    std::fs::create_dir_all(plugins_dir)
        .with_context(|| format!("Failed to create plugins dir {}", plugins_dir.display()))?;

    let readme_path = plugins_dir.join("README.md");
    let readme_status = write_template_file(&readme_path, plugins_readme_template(), force)?;

    let manifest_path = plugins_dir.join("example").join("plugin.toml");
    ensure_parent_dir(&manifest_path)?;
    let manifest_status =
        write_template_file(&manifest_path, plugin_example_manifest_template(), force)?;

    let skill_path = plugins_dir
        .join("example")
        .join("skills")
        .join("hello")
        .join("SKILL.md");
    ensure_parent_dir(&skill_path)?;
    let skill_status = write_template_file(&skill_path, plugin_example_skill_template(), force)?;

    Ok((
        readme_path,
        manifest_path,
        skill_path,
        readme_status,
        manifest_status,
        skill_status,
    ))
}

/// Resolve the user-supplied CORS origins for `codewhale serve --http`.
///
/// Sources, in priority order (later sources extend earlier ones):
/// 1. `--cors-origin URL` flags (repeatable)
/// 2. `CODEWHALE_CORS_ORIGINS` env var (comma-separated),
///    then `DEEPSEEK_CORS_ORIGINS` as an alias
/// 3. `[runtime_api] cors_origins = [...]` in `config.toml`
///
/// The runtime API always allows the built-in dev defaults
/// (localhost:3000, localhost:1420, tauri://localhost). User entries are
/// appended on top — empty strings are skipped, and duplicates are deduped
/// while preserving first-seen order. Whalescale#255 / #561.
fn resolve_cors_origins(config: &Config, flag_origins: &[String]) -> Vec<String> {
    let mut out: Vec<String> = Vec::new();
    let mut push = |raw: &str| {
        let trimmed = raw.trim();
        if trimmed.is_empty() {
            return;
        }
        if !out.iter().any(|existing| existing == trimmed) {
            out.push(trimmed.to_string());
        }
    };
    for o in flag_origins {
        push(o);
    }
    if let Ok(env_value) =
        std::env::var("CODEWHALE_CORS_ORIGINS").or_else(|_| std::env::var("DEEPSEEK_CORS_ORIGINS"))
    {
        for piece in env_value.split(',') {
            push(piece);
        }
    }
    if let Some(rt) = &config.runtime_api
        && let Some(list) = &rt.cors_origins
    {
        for o in list {
            push(o);
        }
    }
    out
}

fn deepseek_home_dir() -> PathBuf {
    codewhale_config::codewhale_home().unwrap_or_else(|_| {
        codewhale_paths::user_home()
            .map_or_else(|| PathBuf::from(".codewhale"), |h| h.join(".codewhale"))
    })
}

/// Resolve the default tools directory. Mirrors `default_skills_dir` shape.
fn default_tools_dir() -> PathBuf {
    deepseek_home_dir().join("tools")
}

/// Resolve the default plugins directory.
fn default_plugins_dir() -> PathBuf {
    deepseek_home_dir().join("plugins")
}

/// Default location for crash/offline-queue checkpoints managed by the TUI.
fn default_checkpoints_dir() -> PathBuf {
    deepseek_home_dir().join("sessions").join("checkpoints")
}

#[derive(Debug, Clone, PartialEq, Eq)]
struct CleanPlan {
    targets: Vec<PathBuf>,
}

fn collect_clean_targets(checkpoints_dir: &Path) -> CleanPlan {
    // Unsent input is not regenerable. Preserve legacy and per-session queue
    // files by name even if their contents are malformed or from a newer
    // schema; cleanup must not erase drafts it cannot currently decode.
    let mut targets: Vec<PathBuf> = std::fs::read_dir(checkpoints_dir)
        .map(|entries| {
            entries
                .filter_map(|entry| entry.ok().map(|e| e.path()))
                .filter(|p| p.is_file() && p.extension().is_some_and(|ext| ext == "json"))
                .filter(|p| {
                    p.file_name()
                        .and_then(|name| name.to_str())
                        .is_some_and(|name| {
                            !codewhale_engine::session_manager::is_offline_queue_file(name)
                        })
                })
                .collect()
        })
        .unwrap_or_default();
    targets.sort();
    CleanPlan { targets }
}

fn execute_clean_plan(plan: &CleanPlan) -> Result<Vec<PathBuf>> {
    let mut removed = Vec::with_capacity(plan.targets.len());
    for path in &plan.targets {
        std::fs::remove_file(path)
            .with_context(|| format!("Failed to remove {}", path.display()))?;
        removed.push(path.clone());
    }
    Ok(removed)
}

fn run_setup(
    config: &Config,
    workspace: &Path,
    args: SetupArgs,
    plugins: &codewhale_plugins::PluginRegistry,
) -> Result<()> {
    if args.status {
        return run_setup_status(config, workspace, plugins);
    }
    if args.clean {
        return run_setup_clean(&default_checkpoints_dir(), args.force);
    }

    use codewhale_palette as palette;
    use colored::Colorize;

    let (aqua_r, aqua_g, aqua_b) = palette::WHALE_ACTION_RGB;
    let (sky_r, sky_g, sky_b) = palette::WHALE_ACTION_RGB;

    let any_explicit = args.mcp || args.skills || args.tools || args.plugins;
    let run_mcp = args.mcp || args.all || !any_explicit;
    let run_skills = args.skills || args.all || !any_explicit;
    let run_tools = args.tools || args.all;
    let run_plugins = args.plugins || args.all;

    println!(
        "{}",
        "Codewhale Setup".truecolor(aqua_r, aqua_g, aqua_b).bold()
    );
    println!("{}", "==============".truecolor(sky_r, sky_g, sky_b));
    println!(
        "Workspace: {}",
        codewhale_util::utils::display_path(workspace)
    );

    if run_mcp {
        let mcp_path = config.mcp_config_path();
        let status = init_mcp_config(&mcp_path, args.force)?;
        match status {
            WriteStatus::Created => {
                println!("  ✓ Created MCP config at {}", mcp_path.display());
            }
            WriteStatus::Overwritten => {
                println!("  ✓ Overwrote MCP config at {}", mcp_path.display());
            }
            WriteStatus::SkippedExists => {
                println!("  · MCP config already exists at {}", mcp_path.display());
            }
        }
        println!(
            "    Next: edit the file, then run `codewhale mcp list` or `codewhale mcp tools`."
        );
    }

    if run_skills {
        let skills_dir = if args.local {
            workspace.join(".codewhale").join("skills")
        } else {
            config.skills_dir()
        };
        let (skill_path, status) = init_skills_dir(&skills_dir, args.force)?;
        match status {
            WriteStatus::Created => {
                println!("  ✓ Created example skill at {}", skill_path.display());
            }
            WriteStatus::Overwritten => {
                println!("  ✓ Overwrote example skill at {}", skill_path.display());
            }
            WriteStatus::SkippedExists => {
                println!(
                    "  · Example skill already exists at {}",
                    skill_path.display()
                );
            }
        }
        if args.local {
            println!(
                "    Project skills dir (loads after workspace trust): {}",
                codewhale_util::utils::display_path(&skills_dir)
            );
        } else {
            println!(
                "    Skills dir: {}",
                codewhale_util::utils::display_path(&skills_dir)
            );
        }
        println!("    Next: run the TUI and use `/skills` then `/skill getting-started`.");
    }

    if run_tools {
        let tools_dir = default_tools_dir();
        let (dir, readme_status, example_status) = init_tools_dir(&tools_dir, args.force)?;
        report_write_status("Tools README", &dir.join("README.md"), readme_status);
        report_write_status("Example tool", &dir.join("example.sh"), example_status);
        println!(
            "    Tools dir: {}",
            codewhale_util::utils::display_path(&dir)
        );
        println!("    Next: drop scripts here; surface them via skills/MCP when ready.");
    }

    if run_plugins {
        let plugins_dir = default_plugins_dir();
        let (readme_path, manifest_path, skill_path, readme_status, manifest_status, skill_status) =
            init_plugins_dir(&plugins_dir, args.force)?;
        report_write_status("Plugins README", &readme_path, readme_status);
        report_write_status("Example plugin manifest", &manifest_path, manifest_status);
        report_write_status("Example plugin skill", &skill_path, skill_status);
        println!(
            "    Plugins dir: {}",
            codewhale_util::utils::display_path(&plugins_dir)
        );
        println!("    Next: run `/plugin validate`, review `example`, then trust and enable it.");
    }

    let sandbox =
        codewhale_sandbox::get_platform_sandbox_with_bwrap_preference(config.prefers_bwrap());
    if let Some(kind) = sandbox {
        println!("  ✓ Sandbox available: {kind}");
    } else {
        println!("  · Sandbox not available on this platform (best-effort only).");
    }

    Ok(())
}

fn report_write_status(label: &str, path: &Path, status: WriteStatus) {
    match status {
        WriteStatus::Created => {
            println!("  ✓ Created {label} at {}", path.display());
        }
        WriteStatus::Overwritten => {
            println!("  ✓ Overwrote {label} at {}", path.display());
        }
        WriteStatus::SkippedExists => {
            println!("  · {label} already exists at {}", path.display());
        }
    }
}

/// Source of the resolved API key, used only by static doctor/setup reports.
///
/// These reports must not migrate a legacy secret store or acquire a
/// write-capable credential handle just to label a source.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ApiKeySource {
    ConfigDeclared,
    EnvDeclared,
    ExternalAuthDeclared,
    SecretStoreUnprobed,
    SecretStoreUnavailable,
    OAuth,
    ExternalConsent,
    NoAuth,
    LocalRuntime,
    Unknown,
}

/// What structural diagnostics can truthfully say about credential
/// availability without consulting environment values or durable stores.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum CredentialAvailability {
    Present,
    NotRequired,
    Unknown,
    NotProbed,
    Unavailable,
}

impl CredentialAvailability {
    fn label(self) -> &'static str {
        match self {
            Self::Present => "present",
            Self::NotRequired => "not_required",
            Self::Unknown => "unknown",
            Self::NotProbed => "not_probed",
            Self::Unavailable => "unavailable",
        }
    }

    fn certifies_ready(self) -> bool {
        matches!(self, Self::Present | Self::NotRequired)
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct CredentialDiagnostic {
    source: ApiKeySource,
    availability: CredentialAvailability,
}

impl CredentialDiagnostic {
    const fn new(source: ApiKeySource, availability: CredentialAvailability) -> Self {
        Self {
            source,
            availability,
        }
    }
}

fn resolve_credential_diagnostic(config: &Config) -> CredentialDiagnostic {
    let Ok(identity) = config.active_provider_identity() else {
        return CredentialDiagnostic::new(
            ApiKeySource::Unknown,
            CredentialAvailability::Unavailable,
        );
    };
    let provider = identity.provider;
    let base_url = config.base_url_for_route(&identity);
    let auth_mode = config.auth_mode_for_provider(&identity);
    if codewhale_config::auth_mode_disables_api_key(auth_mode.as_deref()) {
        return CredentialDiagnostic::new(
            ApiKeySource::NoAuth,
            CredentialAvailability::NotRequired,
        );
    }
    if !codewhale_config::auth_mode_requires_api_key(auth_mode.as_deref())
        && (codewhale_runtime::config::provider_route_is_keyless_self_hosted(provider, &base_url)
            || codewhale_runtime::config::base_url_uses_local_host(&base_url))
    {
        return CredentialDiagnostic::new(
            ApiKeySource::LocalRuntime,
            CredentialAvailability::NotRequired,
        );
    }
    let custom_endpoint = config.provider_uses_custom_endpoint(&identity);
    if !custom_endpoint && provider == codewhale_config::ProviderKind::OpenaiCodex {
        return config
            .external_credential_consent_status(&identity)
            .filter(|status| status.route_state == "active")
            .map_or_else(
                || {
                    CredentialDiagnostic::new(
                        ApiKeySource::OAuth,
                        CredentialAvailability::NotProbed,
                    )
                },
                |_| {
                    CredentialDiagnostic::new(
                        ApiKeySource::ExternalConsent,
                        CredentialAvailability::NotProbed,
                    )
                },
            );
    }
    if !custom_endpoint
        && provider == codewhale_config::ProviderKind::Xai
        && auth_mode
            .as_deref()
            .is_some_and(codewhale_runtime::oauth::auth_mode_uses_xai_oauth)
    {
        return config
            .external_credential_consent_status(&identity)
            .filter(|status| status.route_state == "active")
            .map_or_else(
                || {
                    CredentialDiagnostic::new(
                        ApiKeySource::OAuth,
                        CredentialAvailability::NotProbed,
                    )
                },
                |_| {
                    CredentialDiagnostic::new(
                        ApiKeySource::ExternalConsent,
                        CredentialAvailability::NotProbed,
                    )
                },
            );
    }
    let provider_config = config.provider_config();
    let provider_config_key_kind = provider_config
        .and_then(|entry| entry.api_key.as_deref())
        .map(codewhale_config::classify_config_api_key_value);
    // DeepSeek-CN shares `[providers.deepseek]`'s key (the two identities
    // used to share the top-level `api_key`, #6394).
    let root_key_kind = (identity.key.as_str()
        == codewhale_config::descriptors::LEGACY_DEEPSEEK_CN.id)
        .then(|| {
            config
                .builtin_provider_identity(codewhale_config::ProviderKind::Deepseek)
                .ok()
        })
        .flatten()
        .and_then(|owner| config.provider_config_for(&owner))
        .and_then(|entry| entry.api_key.as_deref())
        .map(codewhale_config::classify_config_api_key_value);

    if matches!(
        provider_config_key_kind,
        Some(codewhale_config::ConfigApiKeyValueKind::Literal)
    ) || matches!(
        root_key_kind,
        Some(codewhale_config::ConfigApiKeyValueKind::Literal)
    ) {
        CredentialDiagnostic::new(
            ApiKeySource::ConfigDeclared,
            CredentialAvailability::Present,
        )
    } else if config
        .provider_config()
        .and_then(|entry| entry.api_key_env.as_deref())
        .is_some_and(|name| !name.trim().is_empty())
    {
        CredentialDiagnostic::new(ApiKeySource::EnvDeclared, CredentialAvailability::NotProbed)
    } else if config
        .provider_config()
        .and_then(|entry| entry.auth.as_ref())
        .is_some()
    {
        CredentialDiagnostic::new(
            ApiKeySource::ExternalAuthDeclared,
            CredentialAvailability::NotProbed,
        )
    } else if matches!(
        provider_config_key_kind,
        Some(codewhale_config::ConfigApiKeyValueKind::SecretStoreSentinel)
    ) || matches!(
        root_key_kind,
        Some(codewhale_config::ConfigApiKeyValueKind::SecretStoreSentinel)
    ) {
        if config.should_skip_secret_store_for_provider(&identity) {
            return CredentialDiagnostic::new(
                ApiKeySource::SecretStoreUnavailable,
                CredentialAvailability::Unavailable,
            );
        }
        // The sentinel is a declaration that runtime resolution should use
        // the secret-store layer, never a literal key. Doctor does not read it.
        CredentialDiagnostic::new(
            ApiKeySource::SecretStoreUnprobed,
            CredentialAvailability::NotProbed,
        )
    } else if !config.should_skip_secret_store_for_provider(&identity) {
        // No literal config declaration was found, but this route can continue
        // through the durable store and ambient provider environment. Ordinary
        // doctor deliberately does not inspect either source.
        CredentialDiagnostic::new(
            ApiKeySource::SecretStoreUnprobed,
            CredentialAvailability::NotProbed,
        )
    } else {
        CredentialDiagnostic::new(ApiKeySource::Unknown, CredentialAvailability::Unknown)
    }
}

#[cfg(test)]
fn resolve_api_key_source(config: &Config) -> ApiKeySource {
    resolve_credential_diagnostic(config).source
}

#[cfg(test)]
fn provider_config_table_key(provider: codewhale_config::ProviderKind) -> String {
    format!(
        "providers.{}",
        codewhale_config::descriptors::compatibility_for_kind(provider).config_key
    )
}

fn count_dir_entries(dir: &Path) -> usize {
    std::fs::read_dir(dir)
        .map(|entries| entries.filter_map(std::result::Result::ok).count())
        .unwrap_or(0)
}

fn skills_count_for(dir: &Path) -> usize {
    if !dir.exists() {
        return 0;
    }
    codewhale_skills::SkillRegistry::discover(dir).len()
}

fn run_setup_status(
    config: &Config,
    workspace: &Path,
    plugins: &codewhale_plugins::PluginRegistry,
) -> Result<()> {
    use codewhale_palette as palette;
    use colored::Colorize;

    let (aqua_r, aqua_g, aqua_b) = palette::WHALE_ACTION_RGB;
    let (sky_r, sky_g, sky_b) = palette::WHALE_ACTION_RGB;

    println!(
        "{}",
        "Codewhale Status".truecolor(aqua_r, aqua_g, aqua_b).bold()
    );
    println!("{}", "===============".truecolor(sky_r, sky_g, sky_b));
    println!("workspace: {}", workspace.display());

    let credential = resolve_credential_diagnostic(config);
    match credential.source {
        ApiKeySource::ConfigDeclared => println!(
            "  {} api_key: literal config value structurally present",
            "✓".truecolor(aqua_r, aqua_g, aqua_b)
        ),
        ApiKeySource::EnvDeclared => println!(
            "  {} api_key: environment source declared (value not inspected)",
            "·".dimmed()
        ),
        ApiKeySource::ExternalAuthDeclared => println!(
            "  {} api_key: external auth source declared (value not inspected)",
            "·".dimmed()
        ),
        ApiKeySource::SecretStoreUnprobed => println!(
            "  {} api_key: secret store eligible (store not probed)",
            "·".dimmed()
        ),
        ApiKeySource::SecretStoreUnavailable => println!(
            "  {} api_key: secret-store sentinel declared, but this route cannot use that store",
            "!".truecolor(sky_r, sky_g, sky_b)
        ),
        ApiKeySource::OAuth => println!(
            "  {} oauth: Codewhale-owned route selected (token availability not probed)",
            "·".dimmed()
        ),
        ApiKeySource::ExternalConsent => println!(
            "  {} oauth: external read-only consent configured (credential file not probed)",
            "·".dimmed()
        ),
        ApiKeySource::NoAuth => println!(
            "  {} api_key: disabled for this route",
            "✓".truecolor(aqua_r, aqua_g, aqua_b)
        ),
        ApiKeySource::LocalRuntime => println!(
            "  {} api_key: not required for this local runtime",
            "✓".truecolor(aqua_r, aqua_g, aqua_b)
        ),
        ApiKeySource::Unknown => println!(
            "  {} api_key: unknown (credential environment and durable stores not inspected)",
            "·".dimmed()
        ),
    }
    println!(
        "  · credential availability: {}",
        credential.availability.label()
    );
    println!(
        "  · base_url: {}",
        codewhale_config::structural_url_authority(&config.active_route_base_url())
    );
    let model = config
        .default_text_model
        .clone()
        .unwrap_or_else(|| DEFAULT_TEXT_MODEL.to_string());
    println!("  · default_text_model: {model}");
    let (default_mode, default_mode_source) = doctor_runtime_default_mode();
    println!("  · default_mode: {default_mode} ({default_mode_source})");

    let mcp_path = config.mcp_config_path();
    let project_mcp_path = codewhale_engine::mcp::workspace_mcp_config_path(workspace);
    let mcp_count = match codewhale_engine::mcp::load_config_with_workspace_and_plugins(
        &mcp_path, workspace, plugins,
    ) {
        Ok(cfg) => cfg.servers.len(),
        Err(_) => 0,
    };
    let mcp_present = if mcp_path.exists() { "" } else { "  (missing)" };
    let project_mcp_present = if project_mcp_path.exists() {
        ""
    } else {
        "  (missing)"
    };
    println!(
        "  · mcp servers: {mcp_count} from {}{mcp_present} + {}{project_mcp_present}",
        mcp_path.display(),
        project_mcp_path.display()
    );

    let skills_dir = config.skills_dir();
    println!(
        "  · skills: {} at {}",
        skills_count_for(&skills_dir),
        codewhale_util::utils::display_path(&skills_dir)
    );

    let tools_dir = default_tools_dir();
    let tools_present = if tools_dir.exists() {
        ""
    } else {
        "  (missing — run `setup --tools`)"
    };
    println!(
        "  · tools: {} entries at {}{tools_present}",
        if tools_dir.exists() {
            count_dir_entries(&tools_dir)
        } else {
            0
        },
        codewhale_util::utils::display_path(&tools_dir)
    );

    let plugins_dir = default_plugins_dir();
    let plugins_present = if plugins_dir.exists() {
        ""
    } else {
        "  (missing — run `setup --plugins`)"
    };
    println!(
        "  · plugins: {} entries at {}{plugins_present}",
        if plugins_dir.exists() {
            count_dir_entries(&plugins_dir)
        } else {
            0
        },
        codewhale_util::utils::display_path(&plugins_dir)
    );

    let sandbox =
        codewhale_sandbox::get_platform_sandbox_with_bwrap_preference(config.prefers_bwrap());
    match sandbox {
        Some(kind) => println!(
            "  {} sandbox: {kind}",
            "✓".truecolor(aqua_r, aqua_g, aqua_b)
        ),
        None => println!(
            "  {} sandbox: unavailable (commands run best-effort)",
            "!".truecolor(sky_r, sky_g, sky_b)
        ),
    }

    println!("  {} {}", "·".dimmed(), dotenv_status_line(workspace));

    println!();
    println!("Run `codewhale doctor --json` for a machine-readable check.");
    Ok(())
}

fn dotenv_status_line(workspace: &Path) -> String {
    let dotenv = workspace.join(".env");
    if dotenv.exists() {
        return format!(
            ".env present at {} (literal provider credentials only)",
            dotenv.display()
        );
    }

    if workspace.join(".env.example").exists() {
        return ".env not present in workspace (run `cp .env.example .env` and edit)".to_string();
    }

    ".env not present in workspace".to_string()
}

fn run_setup_clean(checkpoints_dir: &Path, force: bool) -> Result<()> {
    use colored::Colorize;

    if !checkpoints_dir.exists() {
        println!(
            "Nothing to clean — checkpoints dir does not exist: {}",
            checkpoints_dir.display()
        );
        return Ok(());
    }

    let plan = collect_clean_targets(checkpoints_dir);
    if plan.targets.is_empty() {
        println!(
            "Nothing to clean — no checkpoint files in {}",
            checkpoints_dir.display()
        );
        return Ok(());
    }

    if !force {
        println!(
            "Would remove {} checkpoint file(s) (use --force to apply):",
            plan.targets.len()
        );
        for path in &plan.targets {
            println!("  · {}", path.display());
        }
        return Ok(());
    }

    let removed = execute_clean_plan(&plan)?;
    println!("{}", "Cleaned checkpoints:".bold());
    for path in &removed {
        println!("  ✓ {}", path.display());
    }
    Ok(())
}

fn run_session_diagnostics(args: SessionDiagnosticsArgs) -> Result<()> {
    let contents = std::fs::read_to_string(&args.path).with_context(|| {
        format!(
            "read session diagnostic JSONL from {}",
            codewhale_util::utils::display_path(&args.path)
        )
    })?;
    let summary = codewhale_runtime::session_diagnostics::analyze_session_failure_jsonl(&contents);
    if args.json {
        println!("{}", serde_json::to_string_pretty(&summary)?);
    } else {
        println!(
            "{}",
            codewhale_runtime::session_diagnostics::format_redacted_failure_summary(&summary)
        );
    }
    Ok(())
}

/// Live API checks are explicit. Local endpoints have a separate opt-in because
/// an HTTP request can wake a desktop-managed daemon (notably Ollama.app).
fn doctor_should_probe_api(
    provider: codewhale_config::ProviderKind,
    base_url: &str,
    probes: crate::doctor::DoctorProbeRequest,
) -> bool {
    let local =
        codewhale_runtime::config::provider_route_is_keyless_self_hosted(provider, base_url)
            || codewhale_runtime::config::base_url_uses_local_host(base_url);
    probes.should_probe_api(local)
}

/// Providers whose credential *presence* `codewhale doctor` reports.
///
/// The retired Antigravity identity is a non-runnable tombstone kept only so
/// legacy tables deserialize and clear; doctor never advertises it as a slot.
fn doctor_api_key_providers() -> impl Iterator<Item = codewhale_config::ProviderKind> {
    codewhale_config::ProviderKind::all()
        .iter()
        .copied()
        .filter(|provider| *provider != codewhale_config::ProviderKind::Antigravity)
}

/// Doctor must never turn credential inspection into a refresh/write path.
/// OAuth connectivity is exercised by an ordinary user request instead;
/// doctor limits itself to non-mutating readiness inspection.
fn doctor_should_probe_auth(config: &Config) -> bool {
    let Ok(identity) = config.active_provider_identity() else {
        return false;
    };
    let provider = identity.provider;
    if provider == codewhale_config::ProviderKind::OpenaiCodex
        && !config.provider_uses_custom_endpoint(&identity)
    {
        return false;
    }
    let auth_mode = config.auth_mode_for_provider(&identity);
    if provider == codewhale_config::ProviderKind::Xai
        && auth_mode
            .as_deref()
            .is_some_and(codewhale_runtime::oauth::auth_mode_uses_xai_oauth)
    {
        return false;
    }
    !(provider == codewhale_config::ProviderKind::Moonshot
        && auth_mode
            .as_deref()
            .is_some_and(codewhale_config::auth_mode_uses_kimi_imported_token))
}

/// Run system diagnostics
async fn run_doctor(
    config: &Config,
    workspace: &Path,
    config_path_override: Option<&Path>,
    profile: Option<&str>,
    probes: crate::doctor::DoctorProbeRequest,
    plugins: &codewhale_plugins::PluginRegistry,
    all: bool,
) {
    use codewhale_palette as palette;
    use colored::Colorize;

    let (accent_r, accent_g, accent_b) = palette::WHALE_HUMAN_RGB;
    let (sky_r, sky_g, sky_b) = palette::WHALE_ACTION_RGB;
    let (aqua_r, aqua_g, aqua_b) = palette::WHALE_ACTION_RGB;
    let (red_r, red_g, red_b) = palette::WHALE_ERROR_RGB;

    println!(
        "{}",
        "codewhale Doctor"
            .truecolor(accent_r, accent_g, accent_b)
            .bold()
    );
    println!("{}", "==================".truecolor(sky_r, sky_g, sky_b));
    let local_route = if probes.probe_local {
        let mut local = config.clone();
        codewhale_engine::local_ollama::adopt_live_local_ollama_for_run(&mut local, true)
            .await
            .map(|tag| (local, tag))
    } else {
        None
    };
    if let Some((_, tag)) = &local_route {
        println!(
            "No cloud API key, so Codewhale and `codewhale exec` use local Ollama {tag}; this report checks that route, and no configuration was changed."
        );
    }
    let config = local_route.as_ref().map_or(config, |(local, _)| local);
    // The answer comes before the detail (U7). A requested live probe runs
    // first so that answer can include it; the probe line is the only thing
    // printed while that check is in flight.
    let (verdict_state, _) = doctor_setup_state(config, workspace);
    let identity = config.active_provider_identity().ok();
    let api_target = doctor_api_target(config);
    let live_api_requested = identity.as_ref().is_some_and(|identity| {
        doctor_should_probe_api(identity.provider, &api_target.base_url, probes)
    });
    // The opt-in live check runs once, before the verdict, so the verdict,
    // the credential lines and API Connectivity all report the same result.
    let live_probe = if doctor_should_probe_auth(config) && live_api_requested {
        println!("{} Testing connection...", "·".dimmed());
        // Resolve a credential through the diagnostic-only store first, then
        // probe with an in-memory clone. Constructing the normal client from
        // the original config could otherwise trigger its legacy secret-store
        // migration while a user merely asks doctor to test connectivity.
        Some(match config.with_read_only_api_key_for_diagnostic() {
            Ok(diagnostic_config) => test_api_connectivity(&diagnostic_config).await,
            Err(error) => Err(error),
        })
    } else {
        None
    };
    // Presence and variable name only; the value is never held here.
    let env_key_source = codewhale_runtime::config::active_provider_env_api_key_source(config);
    let verdict = doctor_verdict(
        &verdict_state,
        identity
            .as_ref()
            .map_or("unavailable", |identity| identity.key.as_str()),
        &DoctorVerdictFacts {
            onboarded: crate::tui::onboarding::is_onboarded(),
            env_key_source: env_key_source.clone(),
            live_probe: live_probe.as_ref().map(Result::is_ok),
            live_probe_timed_out: live_probe
                .as_ref()
                .is_some_and(|result| result.as_ref().is_err_and(doctor_probe_timed_out)),
            local_discovery: !probes.probe_local
                && identity.as_ref().is_some_and(|identity| {
                    codewhale_engine::local_ollama::should_adopt_live_local_ollama(
                        codewhale_engine::local_ollama::startup_route_configured(config),
                        identity.provider,
                        false,
                        true,
                    )
                }),
        },
    );
    println!("{}", verdict.truecolor(aqua_r, aqua_g, aqua_b).bold());
    println!();

    // Version info
    println!("{}", "Version Information:".bold());
    println!("  codewhale: {}", env!("CODEWHALE_BUILD_VERSION"));
    // A release binary needs no Rust toolchain; this line describes the host,
    // not the build, so a missing rustc must not read as a fault.
    println!("  host rustc: {}", rustc_version());
    println!();

    println!("{}", "Updates:".bold());
    crate::doctor::print_update_report(probes, &config.update_config()).await;
    println!();

    println!("{}", "Installations:".bold());
    let installation_lines =
        tokio::task::spawn_blocking(|| crate::doctor::installation_lines(false))
            .await
            .unwrap_or_default();
    for line in installation_lines {
        println!("  {line}");
    }
    println!();

    // Configuration summary
    let doctor_paths = match crate::doctor::DoctorPathReport::resolve(config_path_override) {
        Ok(paths) => paths,
        Err(error) => {
            println!("{}", "Resolved User Paths:".bold());
            println!(
                "  {} unavailable: {error:#}",
                "✗".truecolor(red_r, red_g, red_b)
            );
            return;
        }
    };
    println!("{}", "Configuration:".bold());
    let config_path = &doctor_paths.config;

    if tokio::fs::try_exists(config_path).await.unwrap_or(false) {
        println!(
            "  {} config.toml found at {}",
            "✓".truecolor(aqua_r, aqua_g, aqua_b),
            codewhale_util::utils::display_path(config_path)
        );
        // Secret hygiene: name the keys, never the values. Plain-text config
        // is not a secret store.
        if let Ok(raw) = tokio::fs::read_to_string(config_path).await {
            let flagged = crate::doctor::config_credential_shaped_keys(&raw);
            if !flagged.is_empty() {
                println!(
                    "  {} credential-shaped value(s) in config.toml ({}): move them to the secret backend, then scrub the file — config.toml is plain text",
                    "!".truecolor(sky_r, sky_g, sky_b),
                    flagged.join(", ")
                );
            }
        }
        // Legacy top-level `base_url` / `api_key` (#6394): which moves the
        // file still has pending, which pair disagrees and which side is in
        // use. Sources only, never values.
        for note in &config.legacy_root.notes {
            let pending = !matches!(
                note,
                codewhale_config::legacy_root::LegacyRootNote::Conflict { .. }
            );
            println!(
                "  {} {note}{}",
                "!".truecolor(sky_r, sky_g, sky_b),
                if pending {
                    " (read that way already; `codewhale config migrate` updates the file)"
                } else {
                    ""
                }
            );
        }
    } else {
        println!(
            "  {} config.toml not found at {} (using defaults/env)",
            "!".truecolor(sky_r, sky_g, sky_b),
            codewhale_util::utils::display_path(config_path)
        );
    }
    println!(
        "  workspace: {}",
        codewhale_util::utils::display_path(workspace)
    );
    println!("  {}", doctor_search_provider_line(config));

    println!();
    println!("{}", "Resolved User Paths (read-only):".bold());
    let entries = doctor_paths.entries();
    let shown = if all { entries.len() } else { 5 };
    for (label, path) in entries.iter().take(shown) {
        println!("  · {label}: {}", codewhale_util::utils::display_path(path));
    }

    let secret_backend = codewhale_secrets::diagnose_secret_backend();
    println!();
    println!("{}", "Secret Backend (structural only):".bold());
    let backend_lines = crate::doctor::secret_backend_human_lines(&secret_backend);
    let shown = if all { backend_lines.len() } else { 1 };
    for line in backend_lines.iter().take(shown) {
        println!("  · {line}");
    }
    if !all {
        println!("  · `codewhale doctor --all` shows every path and backend detail");
    }

    println!();
    println!("{}", "Sessions:".bold());
    println!(
        "  · {}",
        match crate::doctor::session_store_counts(&doctor_paths.sessions) {
            Ok(Some(counts)) => format!(
                "{} saved sessions, {} crash checkpoints",
                counts.saved, counts.checkpoints
            ),
            Ok(None) => "no sessions directory yet".to_string(),
            Err(error) => format!("could not count sessions: {error}"),
        }
    );
    println!(
        "  · {}",
        codewhale_engine::session_reconcile::last_run(&doctor_paths.sessions).map_or_else(
            || "no session repair has run yet (it runs at launch; `codewhale doctor --repair-sessions` runs it now)".to_string(),
            |summary| summary.doctor_detail()
        )
    );

    // State root (v0.8.44)
    println!();
    println!("{}", "State Root:".bold());
    let (code_home, legacy_home) = doctor_state_roots();
    let active_root = if code_home.exists() {
        &code_home
    } else if legacy_home.exists() {
        &legacy_home
    } else {
        &code_home
    };
    println!(
        "  active: {}",
        codewhale_util::utils::display_path(active_root)
    );
    if active_root != &code_home {
        println!(
            "  note: legacy {} found; start Codewhale once to trigger safe migration where available.",
            codewhale_util::utils::display_path(&legacy_home)
        );
    }
    if legacy_home.exists() && code_home.exists() {
        println!(
            "  dual roots: {} (primary) + {} (legacy)",
            codewhale_util::utils::display_path(&code_home),
            codewhale_util::utils::display_path(&legacy_home)
        );
    }
    let legacy_state_report = doctor_legacy_state_report(&code_home, &legacy_home);
    let session_recovery = doctor_session_recovery_report(
        &code_home,
        &legacy_home,
        codewhale_config::codewhale_home_is_explicit(),
    );
    print_doctor_legacy_state_report(
        &legacy_state_report,
        &session_recovery,
        (aqua_r, aqua_g, aqua_b),
        (sky_r, sky_g, sky_b),
    );
    print_doctor_stored_secrets_report(
        config_path_override.map(Path::to_path_buf),
        profile.map(str::to_owned),
    )
    .await;

    let (setup_state, setup_source) = doctor_setup_state(config, workspace);
    print_doctor_setup_report(
        config,
        workspace,
        &setup_state,
        setup_source,
        (aqua_r, aqua_g, aqua_b),
        (sky_r, sky_g, sky_b),
    );
    print_doctor_fleet_roster_layers(config, workspace);

    // Check API keys
    println!();
    println!("{}", "API Keys:".bold());

    // Per-provider state: env + config file only (no values printed).
    // Keep doctor/status prompt-free and credential-value-free even for
    // unsigned rebuilt binaries.
    for provider in doctor_api_key_providers() {
        let slot = provider.as_str();
        let provider_identity = config.builtin_provider_identity(provider).ok();
        let provider_config = provider_identity
            .as_ref()
            .and_then(|identity| config.provider_config_for(identity));
        let config_declared = provider_config.is_some_and(|entry| {
            entry.api_key.as_deref().is_some_and(|key| {
                codewhale_config::classify_config_api_key_value(key)
                    == codewhale_config::ConfigApiKeyValueKind::Literal
            })
        });
        let declared_env = provider_config
            .and_then(|entry| entry.api_key_env.as_deref())
            .map(str::trim)
            .filter(|name| !name.is_empty());
        let env_source_declared = declared_env.is_some();
        // Presence only: name the variable that holds a key, never its value.
        let env_set = declared_env
            .filter(|name| {
                std::env::var_os(name)
                    .is_some_and(|value| !value.to_string_lossy().trim().is_empty())
            })
            .or_else(|| codewhale_runtime::config::provider_env_api_key_var(provider));
        let icon = if config_declared || env_source_declared || env_set.is_some() {
            "·".truecolor(aqua_r, aqua_g, aqua_b)
        } else {
            "·".dimmed()
        };
        println!(
            "  {} {slot}: env_source={}, config_source={}",
            icon,
            match env_set {
                Some(var) => doctor_env_key_label(var),
                None if env_source_declared => "declared (value not inspected)".to_string(),
                None => "not inspected".to_string(),
            },
            if config_declared {
                "declared (value not inspected)"
            } else {
                "not declared"
            }
        );
    }
    println!("  · credential precedence is unchanged; doctor does not inspect credential values");
    println!();
    println!(
        "{}",
        "External credential consent (configuration only):".bold()
    );
    for line in doctor_external_credential_consent_lines(config) {
        println!("  {line}");
    }

    println!();
    println!(
        "{}",
        "DeepSeek Harness integration (read-only detection):".bold()
    );
    for line in doctor_dsh_integration_lines(config, workspace) {
        println!("  {line}");
    }

    let credential = resolve_credential_diagnostic(config);
    let source_label = match credential.source {
        ApiKeySource::ConfigDeclared => "literal config value structurally present",
        ApiKeySource::EnvDeclared => "environment source declared; value not inspected",
        ApiKeySource::ExternalAuthDeclared => {
            "external auth source declared; credential not resolved"
        }
        ApiKeySource::SecretStoreUnprobed => "secret store eligible; store not probed",
        ApiKeySource::SecretStoreUnavailable => {
            "secret-store sentinel declared, but this route cannot use that store"
        }
        ApiKeySource::OAuth => "OAuth route configured; token availability not probed",
        ApiKeySource::ExternalConsent => "external consent configured; token file not read",
        ApiKeySource::NoAuth => "no-auth route",
        ApiKeySource::LocalRuntime => "local runtime; credentials not required",
        ApiKeySource::Unknown => "unknown; credential environment and stores not inspected",
    };
    match env_key_source.as_deref() {
        Some(source) => println!(
            "  {} active provider credential source: {}",
            "·".dimmed(),
            doctor_env_key_label(source)
        ),
        None => println!(
            "  {} active provider credential source: {source_label}",
            "·".dimmed()
        ),
    }
    println!(
        "  · active provider credential availability: {}",
        credential.availability.label()
    );
    match &live_probe {
        Some(Ok(())) => println!(
            "  {} active provider credential: accepted by the live API check",
            "✓".truecolor(aqua_r, aqua_g, aqua_b)
        ),
        // #6889: no answer in time says nothing about the credential.
        Some(Err(error)) if doctor_probe_timed_out(error) => println!(
            "  {} active provider credential: not confirmed, the live API check got no answer in time (see API Connectivity)",
            "·".dimmed()
        ),
        Some(Err(_)) => println!(
            "  {} active provider credential: live API check failed (see API Connectivity)",
            "✗".truecolor(red_r, red_g, red_b)
        ),
        None => {}
    }

    // API connectivity test
    println!();
    println!("{}", "API Connectivity:".bold());
    // Configured-vs-active honesty (DGF-01): doctor describes the route a
    // session launched NOW would resolve. It cannot see inside an already
    // running session, which keeps the route it resolved at its own launch.
    println!(
        "  · scope: configured route — what a session launched now would use; a running session keeps the route it resolved at launch (its TUI header shows the live route)"
    );
    println!("  · provider: {}", api_target.provider);
    println!(
        "  · base_url: {}",
        codewhale_config::structural_url_authority(&api_target.base_url)
    );
    match api_target.resolution {
        DoctorModelResolution::Resolved => {
            println!("  · model: {} (resolved)", api_target.model);
        }
        DoctorModelResolution::ConfiguredOnly => {
            println!(
                "  · model: {} (configured; route resolution unavailable)",
                api_target.model
            );
        }
    }
    let tls_status = doctor_tls_status(config);
    if !tls_status.certificate_verification {
        println!("  ! {}", tls_status.message);
        println!("    Prefer SSL_CERT_FILE with a trusted custom CA bundle when possible.");
    }
    let strict_tool_mode = doctor_strict_tool_mode_status(config);
    let strict_icon = match strict_tool_mode.status {
        "ready" => "✓".truecolor(aqua_r, aqua_g, aqua_b),
        "fallback_non_beta" | "custom_endpoint" => "!".truecolor(sky_r, sky_g, sky_b),
        _ => "·".dimmed(),
    };
    println!(
        "  {} strict_tool_mode: {}",
        strict_icon, strict_tool_mode.message
    );
    if let Some(recommended) = strict_tool_mode.recommended_base_url.as_deref() {
        println!(
            "    Use the {} endpoint for DeepSeek strict schemas.",
            codewhale_config::structural_url_authority(recommended)
        );
    }
    let capability = identity.as_ref().map(|identity| {
        codewhale_engine::provider_lake::provider_capability(identity.provider, &api_target.model)
    });
    if let Some(alias) = capability
        .as_ref()
        .and_then(|capability| capability.alias_deprecation.as_ref())
    {
        println!(
            "  ! model alias {} retires {}; switch to {}",
            alias.alias, alias.retirement_date, alias.replacement
        );
    }
    let endpoint_is_local = identity.as_ref().is_some_and(|identity| {
        codewhale_runtime::config::provider_route_is_keyless_self_hosted(
            identity.provider,
            &api_target.base_url,
        ) || codewhale_runtime::config::base_url_uses_local_host(&api_target.base_url)
    });
    if let Some(connectivity_result) = &live_probe {
        match connectivity_result {
            Ok(()) => {
                println!(
                    "  {} API connection successful",
                    "✓".truecolor(aqua_r, aqua_g, aqua_b)
                );
            }
            Err(e) => {
                let error_msg = e.to_string();
                let timed_out = doctor_probe_timed_out(e);
                println!(
                    "  {} {}",
                    "✗".truecolor(red_r, red_g, red_b),
                    if timed_out {
                        "API check got no answer in time"
                    } else {
                        "API connection failed"
                    }
                );
                let names_status = |status| {
                    codewhale_engine::mcp::oauth::text_names_http_status(&error_msg, status)
                };
                let provider = identity
                    .as_ref()
                    .map(|identity| identity.provider)
                    .unwrap_or(codewhale_config::ProviderKind::Deepseek);
                if names_status("401") || error_msg.contains("Unauthorized") {
                    println!(
                        "    Invalid API key. Check `codewhale auth status`, {}, or config.toml",
                        doctor_provider_key_place(provider)
                    );
                } else if names_status("403") || error_msg.contains("Forbidden") {
                    println!(
                        "    API key lacks permissions. Verify the {} key is active.",
                        provider.provider().display_name()
                    );
                } else if timed_out
                    || error_msg.contains("timeout")
                    || error_msg.contains("Timeout")
                {
                    for line in doctor_timeout_recovery_lines(config) {
                        println!("    {line}");
                    }
                } else if error_msg.contains("dns") || error_msg.contains("resolve") {
                    println!("    DNS resolution failed. Check your network connection");
                } else if error_msg.contains("connect") {
                    println!("    Connection failed. Check firewall settings or try again");
                } else if crate::doctor::is_keyless_ds4_route(config) {
                    println!("    {error_msg}");
                } else {
                    println!(
                        "    Error details omitted because provider failures can contain credential material."
                    );
                }
            }
        }
    } else if !doctor_should_probe_auth(config) {
        println!(
            "  {} Live OAuth connectivity not checked by non-mutating doctor",
            "·".dimmed()
        );
        println!(
            "    Doctor never refreshes or rewrites credentials; exercise the route with a normal request."
        );
    } else {
        if endpoint_is_local {
            println!(
                "  {} Live connectivity not checked for this local endpoint",
                "·".dimmed()
            );
            println!(
                "    Run `codewhale doctor --probe-local` to opt in; the request may start a local service."
            );
        } else {
            println!(
                "  {} Live hosted connectivity not checked (offline default)",
                "·".dimmed()
            );
            println!("    Run `codewhale doctor --probe-api` to opt in.");
        }
    }

    println!();
    println!("{}", "Search Provider Reachability:".bold());
    let search_probe = crate::doctor::doctor_search_probe(config, probes).await;
    for line in crate::doctor::doctor_search_probe_lines(&search_probe) {
        println!("  {line}");
    }

    // MCP configuration
    println!();
    println!("{}", "MCP Servers (configuration only):".bold());
    println!("  · Static check only; no server process was started.");
    let features = config.features();
    if features.enabled(Feature::Mcp) {
        println!(
            "  {} MCP feature flag enabled",
            "✓".truecolor(aqua_r, aqua_g, aqua_b)
        );
    } else {
        println!(
            "  {} MCP feature flag disabled",
            "!".truecolor(sky_r, sky_g, sky_b)
        );
    }

    let mcp_config_path = config.mcp_config_path();
    let project_mcp_config_path = codewhale_engine::mcp::workspace_mcp_config_path(workspace);
    if mcp_config_path.exists() {
        println!(
            "  {} MCP config found at {}",
            "✓".truecolor(aqua_r, aqua_g, aqua_b),
            codewhale_util::utils::display_path(&mcp_config_path)
        );
    } else {
        println!(
            "  {} MCP config not found at {}",
            "·".dimmed(),
            codewhale_util::utils::display_path(&mcp_config_path)
        );
    }
    if project_mcp_config_path.exists() {
        println!(
            "  {} Project MCP config found at {}",
            "✓".truecolor(aqua_r, aqua_g, aqua_b),
            codewhale_util::utils::display_path(&project_mcp_config_path)
        );
    } else {
        println!(
            "  {} Project MCP config not found at {}",
            "·".dimmed(),
            codewhale_util::utils::display_path(&project_mcp_config_path)
        );
    }

    match codewhale_engine::mcp::load_config_with_workspace_and_plugins(
        &mcp_config_path,
        workspace,
        plugins,
    ) {
        Ok(cfg) if cfg.servers.is_empty() => {
            println!("  {} 0 merged server(s) configured", "·".dimmed());
            if !mcp_config_path.exists() && !project_mcp_config_path.exists() {
                println!("    Run `codewhale mcp init` or add `.codewhale/mcp.json`.");
            }
        }
        Ok(cfg) => {
            println!(
                "  {} {} merged server(s) configured",
                "·".dimmed(),
                cfg.servers.len()
            );
            let duplicate_computer_use =
                codewhale_engine::mcp::duplicate_computer_use_servers(&cfg);
            for (name, server) in &cfg.servers {
                let status = doctor_check_mcp_server(server);
                let icon = match &status {
                    McpServerDoctorStatus::Ok(detail) => {
                        format!(
                            "  {} {name}: configuration valid; {}",
                            "✓".truecolor(aqua_r, aqua_g, aqua_b),
                            detail
                        )
                    }
                    McpServerDoctorStatus::Warning(detail) => {
                        format!(
                            "  {} {name}: configuration warning; {}",
                            "!".truecolor(sky_r, sky_g, sky_b),
                            detail
                        )
                    }
                    McpServerDoctorStatus::Error(detail) => {
                        format!(
                            "  {} {name}: configuration invalid; {}",
                            "✗".truecolor(red_r, red_g, red_b),
                            detail
                        )
                    }
                };
                println!("{icon}");
                if !server.is_enabled() {
                    println!("      disabled; live health not checked");
                } else {
                    println!(
                        "      process/protocol/backend: not checked; `codewhale mcp validate` explicitly starts and initializes configured servers"
                    );
                }
            }
            for (name, _) in &duplicate_computer_use {
                println!(
                    "  {} {}",
                    "!".truecolor(sky_r, sky_g, sky_b),
                    duplicate_computer_use_warning(name).trim_start()
                );
            }
            if probes.should_probe_mcp() {
                println!();
                println!(
                    "  {} Live MCP probe enabled: starting enabled servers; backend tool health remains untested.",
                    "!".truecolor(sky_r, sky_g, sky_b)
                );
                match codewhale_engine::mcp::McpPool::from_config_path_with_workspace_and_plugins(
                    &mcp_config_path,
                    workspace,
                    std::sync::Arc::new(plugins.clone()),
                ) {
                    Ok(pool) => {
                        let mut pool = pool.with_backend(config.mcp_backend());
                        let errors = pool.connect_all().await;
                        let failed = errors
                            .iter()
                            .map(|(name, _)| name.as_str())
                            .collect::<std::collections::BTreeSet<_>>();
                        for (name, server) in &cfg.servers {
                            if !server.is_enabled() {
                                continue;
                            }
                            if failed.contains(name.as_str()) {
                                println!(
                                    "      {} {name}: process/protocol unreachable; error details omitted",
                                    "✗".truecolor(red_r, red_g, red_b)
                                );
                            } else {
                                println!(
                                    "      {} {name}: process reachable and protocol initialized; backend tool health not checked",
                                    "✓".truecolor(aqua_r, aqua_g, aqua_b)
                                );
                            }
                        }
                    }
                    Err(_) => println!(
                        "      {} live MCP probe could not load merged configuration; details omitted",
                        "✗".truecolor(red_r, red_g, red_b)
                    ),
                }
            } else {
                println!(
                    "    Use codewhale doctor --probe-mcp to opt in to live process/protocol checks; it may start configured servers."
                );
            }
        }
        Err(_) => {
            println!(
                "  {} MCP configuration could not be loaded; details omitted",
                "✗".truecolor(red_r, red_g, red_b)
            );
        }
    }

    // Skills configuration
    println!();
    println!("{}", "Skills:".bold());
    let global_skills_dir = config.skills_dir();
    let skills_config = config.skills_config();
    let skill_registry = codewhale_skills::discover_for_workspace(
        workspace,
        Some(&global_skills_dir),
        &skills_config,
        None,
    );
    if let Some(status) = codewhale_skills::bundled_skills_status(&skills_config, &skill_registry) {
        println!("  {}", status.summary());
        if !status.unknown.is_empty() {
            println!("  ⚠ Unknown bundled names: {}", status.unknown.join(", "));
        }
        if !status.ignored.is_empty() {
            println!(
                "  ⚠ Ignored explicit_only entries: {}",
                status.ignored.join(", ")
            );
        }
        if !status.selected_missing.is_empty() {
            println!(
                "  Selected but not installed: {}",
                status.selected_missing.join(", ")
            );
        }
    }

    let agents_skills_dir = workspace.join(".agents").join("skills");
    let local_skills_dir = workspace.join("skills");
    let agents_global_skills_dir = codewhale_skills::agents_global_skills_dir();
    // #432: cross-tool skill discovery dirs. Presence is reported here
    // even though they sit lower in the precedence chain so users can
    // see at a glance whether a `.opencode/skills/`, `.claude/skills/`,
    // `.cursor/skills/`, or global agentskills.io directory is contributing
    // to the merged catalogue.
    let opencode_skills_dir = workspace.join(".opencode").join("skills");
    let claude_skills_dir = workspace.join(".claude").join("skills");
    let selected_skills_dir =
        crate::tui::app::resolve_skills_dir(workspace, &global_skills_dir, config);

    let describe_dir = |dir: &Path| -> usize {
        std::fs::read_dir(dir)
            .map(|entries| entries.filter_map(std::result::Result::ok).count())
            .unwrap_or(0)
    };

    if local_skills_dir.exists() {
        println!(
            "  {} local skills dir found at {} ({} items)",
            "✓".truecolor(aqua_r, aqua_g, aqua_b),
            codewhale_util::utils::display_path(&local_skills_dir),
            describe_dir(&local_skills_dir)
        );
    } else {
        println!(
            "  {} local skills dir not found at {}",
            "·".dimmed(),
            codewhale_util::utils::display_path(&local_skills_dir)
        );
    }

    if agents_skills_dir.exists() {
        println!(
            "  {} .agents skills dir found at {} ({} items)",
            "✓".truecolor(aqua_r, aqua_g, aqua_b),
            codewhale_util::utils::display_path(&agents_skills_dir),
            describe_dir(&agents_skills_dir)
        );
    } else {
        println!(
            "  {} .agents skills dir not found at {}",
            "·".dimmed(),
            codewhale_util::utils::display_path(&agents_skills_dir)
        );
    }

    if let Some(agents_global_skills_dir) = agents_global_skills_dir.as_ref() {
        if agents_global_skills_dir.exists() {
            println!(
                "  {} global .agents skills dir found at {} ({} items)",
                "✓".truecolor(aqua_r, aqua_g, aqua_b),
                codewhale_util::utils::display_path(agents_global_skills_dir),
                describe_dir(agents_global_skills_dir)
            );
        } else {
            println!(
                "  {} global .agents skills dir not found at {}",
                "·".dimmed(),
                codewhale_util::utils::display_path(agents_global_skills_dir)
            );
        }
    }

    if global_skills_dir.exists() {
        println!(
            "  {} global skills dir found at {} ({} items)",
            "✓".truecolor(aqua_r, aqua_g, aqua_b),
            codewhale_util::utils::display_path(&global_skills_dir),
            describe_dir(&global_skills_dir)
        );
    } else {
        println!(
            "  {} global skills dir not found at {}",
            "·".dimmed(),
            codewhale_util::utils::display_path(&global_skills_dir)
        );
    }

    // #432: only print interop dirs when they're populated — empty
    // .opencode/.claude folders are common and would just clutter
    // the report with false-positive "absent" lines.
    if opencode_skills_dir.exists() {
        println!(
            "  {} .opencode skills dir found at {} ({} items)",
            "✓".truecolor(aqua_r, aqua_g, aqua_b),
            codewhale_util::utils::display_path(&opencode_skills_dir),
            describe_dir(&opencode_skills_dir)
        );
    }
    if claude_skills_dir.exists() {
        println!(
            "  {} .claude skills dir found at {} ({} items)",
            "✓".truecolor(aqua_r, aqua_g, aqua_b),
            codewhale_util::utils::display_path(&claude_skills_dir),
            describe_dir(&claude_skills_dir)
        );
    }

    println!(
        "  {} selected skills dir: {}",
        "·".dimmed(),
        codewhale_util::utils::display_path(&selected_skills_dir)
    );
    let unrecognized_system_skills = codewhale_runtime::config::owned_skills_dir()
        .map(|dir| {
            codewhale_skills::unrecognized_system_skills(&dir, &config.skills_config().bundled)
        })
        .unwrap_or_default();
    if !unrecognized_system_skills.is_empty() {
        println!(
            "  {} {} bundled skill(s) in the owned global root are customized or from an unknown generation and are never refreshed: {}",
            "!".truecolor(sky_r, sky_g, sky_b),
            unrecognized_system_skills.len(),
            unrecognized_system_skills.join(", ")
        );
    }
    if !workspace.join(".codewhale").join("skills").is_dir()
        && !agents_skills_dir.exists()
        && !local_skills_dir.exists()
        && !agents_global_skills_dir
            .as_ref()
            .is_some_and(|dir| dir.exists())
        && !global_skills_dir.exists()
    {
        println!("    Run `codewhale setup --skills` (or add --local for ./.codewhale/skills).");
    }

    // Tools directory
    println!();
    println!("{}", "Tools:".bold());
    let tools_dir = default_tools_dir();
    if tools_dir.exists() {
        let count = count_dir_entries(&tools_dir);
        println!(
            "  {} tools dir found at {} ({} items)",
            "✓".truecolor(aqua_r, aqua_g, aqua_b),
            codewhale_util::utils::display_path(&tools_dir),
            count
        );
    } else {
        println!(
            "  {} tools dir not found at {}",
            "·".dimmed(),
            codewhale_util::utils::display_path(&tools_dir)
        );
        println!("    Run `codewhale setup --tools` to scaffold a starter dir.");
    }

    // Plugins directory
    println!();
    println!("{}", "Plugins:".bold());
    let plugins_dir = default_plugins_dir();
    if plugins_dir.exists() {
        let count = count_dir_entries(&plugins_dir);
        println!(
            "  {} plugins dir found at {} ({} items)",
            "✓".truecolor(aqua_r, aqua_g, aqua_b),
            codewhale_util::utils::display_path(&plugins_dir),
            count
        );
    } else {
        println!(
            "  {} plugins dir not found at {}",
            "·".dimmed(),
            codewhale_util::utils::display_path(&plugins_dir)
        );
        println!("    Run `codewhale setup --plugins` to scaffold a starter dir.");
    }

    // Storage surfaces (#422 / #440 / #500)
    println!();
    println!("{}", "Storage:".bold());
    if let Some(spillover_root) = codewhale_engine::tools::truncate::spillover_root() {
        let (present, count) = if spillover_root.is_dir() {
            (true, count_dir_entries(&spillover_root))
        } else {
            (false, 0)
        };
        if present {
            println!(
                "  {} tool-output spillover at {} ({} file{})",
                "✓".truecolor(aqua_r, aqua_g, aqua_b),
                codewhale_util::utils::display_path(&spillover_root),
                count,
                if count == 1 { "" } else { "s" }
            );
        } else {
            println!(
                "  {} tool-output spillover dir not yet created at {}",
                "·".dimmed(),
                codewhale_util::utils::display_path(&spillover_root)
            );
        }
    }
    let stash = crate::composer_stash::diagnostic_stash_report();
    if let Some(stash_path) = stash.path.as_ref() {
        if let Some(error) = stash.error.as_deref() {
            println!(
                "  {} composer stash was not inspected at {}: {error}",
                "!".truecolor(sky_r, sky_g, sky_b),
                codewhale_util::utils::display_path(stash_path),
            );
        } else if stash.present {
            println!(
                "  {} composer stash at {} ({} parked draft{})",
                "✓".truecolor(aqua_r, aqua_g, aqua_b),
                codewhale_util::utils::display_path(stash_path),
                stash.count,
                if stash.count == 1 { "" } else { "s" }
            );
        } else {
            println!(
                "  {} composer stash empty (Ctrl+G or Ctrl+S in the composer to park a draft)",
                "·".dimmed()
            );
        }
    } else if let Some(error) = stash.error.as_deref() {
        println!(
            "  {} composer stash was not inspected: {error}",
            "!".truecolor(sky_r, sky_g, sky_b),
        );
    }

    // Tool dependencies — probe external binaries that individual
    // tools rely on (Python for code_execution, pdftotext for PDF
    // reading) so users see explicit ✓/✗ rather than the tool failing
    // at execution time with "program not found". New in v0.8.31.
    println!();
    println!("{}", "Tool Dependencies:".bold());

    match codewhale_dependencies::resolve_python_interpreter() {
        Some(name) => println!(
            "  {} Python: {} → code_execution tool registered",
            "✓".truecolor(aqua_r, aqua_g, aqua_b),
            name
        ),
        None => {
            println!(
                "  {} Python: not found (tried {:?})",
                "✗".truecolor(red_r, red_g, red_b),
                codewhale_dependencies::PYTHON_CANDIDATES,
            );
            println!("    code_execution tool is NOT advertised to the model on this install.");
            println!("    Install Python 3 and ensure one of those names is on PATH:");
            match std::env::consts::OS {
                "macos" => {
                    println!("      brew install python@3.12   (or download from python.org)")
                }
                "linux" => println!(
                    "      sudo apt install python3    (Debian/Ubuntu) — or your distro's equivalent"
                ),
                "windows" => {
                    println!("      winget install Python.Python.3   (or download from python.org)")
                }
                other => println!("      install Python 3 for {other} from python.org"),
            }
        }
    }

    match codewhale_dependencies::resolve_node() {
        Some(_) => println!(
            "  {} Node.js: present → js_execution tool registered",
            "✓".truecolor(aqua_r, aqua_g, aqua_b),
        ),
        None => {
            println!(
                "  {} Node.js: not found (tried `node`)",
                "✗".truecolor(red_r, red_g, red_b),
            );
            println!("    js_execution tool is NOT advertised to the model on this install.");
            println!("    Install Node 18+ and ensure `node` is on PATH:");
            match std::env::consts::OS {
                "macos" => println!("      brew install node   (or download from nodejs.org)"),
                "linux" => println!(
                    "      sudo apt install nodejs    (Debian/Ubuntu) — or your distro's equivalent"
                ),
                "windows" => {
                    println!("      winget install OpenJS.NodeJS   (or download from nodejs.org)")
                }
                other => println!("      install Node.js for {other} from nodejs.org"),
            }
        }
    }

    {
        // The runtime the TypeScript extension host would use, resolved the
        // same way the host launcher does (`[extension_host] runtime`), and
        // with the host on, the OS sandbox it would get, planned (on Linux,
        // bwrap-probed) the way a launch plans it. Both run child processes,
        // so they stay off the async runtime. Known limit: the runtime probes
        // have no timeout, so a runtime binary that hangs on `--version`
        // stalls doctor here.
        let options = codewhale_engine::extension_host::ExtensionHostOptions::from_config(
            config.extension_host.as_ref(),
        );
        let native_enabled = config
            .features()
            .enabled(codewhale_runtime::features::Feature::ExtensionHost);
        let mcp_enabled = config.mcp_backend() == codewhale_config::McpBackend::Host;
        let enabled = native_enabled || mcp_enabled;
        let tier = if mcp_enabled {
            codewhale_engine::extension_host::tier::HostTier::Builtin
        } else {
            codewhale_engine::extension_host::tier::HostTier::Plugin
        };
        let resolution = tokio::task::spawn_blocking(move || {
            let resolution = codewhale_dependencies::resolve_extension_host_runtime(
                options.runtime,
                options.node_override.as_deref(),
                options.bun_override.as_deref(),
                codewhale_engine::extension_host::bundle_sha256(),
            );
            let sandbox = resolution
                .selected
                .as_ref()
                .filter(|_| enabled)
                .map(|runtime| {
                    codewhale_engine::extension_host::planned_sandbox(&options, runtime, tier)
                });
            (resolution, sandbox)
        })
        .await;
        let state = if enabled {
            ""
        } else {
            " (unused: Native extensions and Host MCP are off)"
        };
        let failed = if enabled {
            "✗".truecolor(red_r, red_g, red_b)
        } else {
            "·".dimmed()
        };
        match resolution {
            Ok((resolution, sandbox)) => {
                match &resolution.selected {
                    Some(runtime) => {
                        println!(
                            "  {} Extension host runtime: {}{state}",
                            "✓".truecolor(aqua_r, aqua_g, aqua_b),
                            resolution.summary(),
                        );
                        println!(
                            "    Node remains the default; Bun default eligibility awaits measured runtime and platform isolation gates."
                        );
                        if native_enabled {
                            println!(
                                "    Native admission requires a verified OS sandbox; an unsandboxed pinned Builtin host does not qualify Native extensions."
                            );
                        }
                        println!(
                        "    {}",
                        codewhale_engine::extension_host::supervisor::MemoryEnforcement::planned(runtime.kind)
                            .describe(codewhale_engine::extension_host::supervisor::HOST_MEMORY_CAP)
                    );
                        match sandbox {
                            Some(Ok(sandbox)) => println!("    {sandbox}"),
                            Some(Err(error)) => {
                                println!("    host sandbox not determined: {error}")
                            }
                            None => {}
                        }
                    }
                    None => println!(
                        "  {failed} Extension host runtime: {}{state}",
                        resolution.failure(),
                    ),
                }
            }
            Err(error) => println!(
                "  {failed} Extension host runtime: the runtime probe did not finish ({error}){state}"
            ),
        }
    }

    match codewhale_dependencies::resolve_pandoc() {
        Some(_) => println!(
            "  {} pandoc: present → pandoc_convert tool registered",
            "✓".truecolor(aqua_r, aqua_g, aqua_b),
        ),
        None => {
            println!("  {} pandoc: not found (optional)", "·".dimmed(),);
            println!(
                "    pandoc_convert tool is NOT advertised to the model. Install pandoc 2.15+ to enable:"
            );
            match std::env::consts::OS {
                "macos" => println!("      brew install pandoc"),
                "linux" => println!(
                    "      release package from pandoc.org/installing.html (distro packages may predate 2.15)"
                ),
                "windows" => {
                    println!("      winget install JohnMacFarlane.Pandoc")
                }
                other => println!("      install pandoc for {other} from pandoc.org"),
            }
        }
    }

    match codewhale_dependencies::resolve_tesseract() {
        Some(_) => {
            if cfg!(target_os = "macos") {
                println!(
                    "  {} OCR: macOS Vision + tesseract available → image_ocr/read_file screenshot OCR enabled",
                    "✓".truecolor(aqua_r, aqua_g, aqua_b),
                );
            } else {
                println!(
                    "  {} tesseract: present → image_ocr/read_file screenshot OCR enabled",
                    "✓".truecolor(aqua_r, aqua_g, aqua_b),
                );
            }
        }
        None => {
            if cfg!(target_os = "macos") {
                println!(
                    "  {} OCR: macOS Vision available → image_ocr/read_file screenshot OCR enabled",
                    "✓".truecolor(aqua_r, aqua_g, aqua_b),
                );
                println!(
                    "    tesseract not found (optional; install only for alternate OCR packs)."
                );
            } else {
                println!("  {} tesseract: not found (optional)", "·".dimmed(),);
                println!(
                    "    image_ocr tool is NOT advertised to the model. Install tesseract to enable:"
                );
                match std::env::consts::OS {
                    "macos" => println!("      brew install tesseract"),
                    "linux" => println!(
                        "      sudo apt install tesseract-ocr    (Debian/Ubuntu) — or your distro's equivalent"
                    ),
                    "windows" => println!("      winget install UB-Mannheim.TesseractOCR"),
                    other => {
                        println!("      install tesseract for {other} from tesseract-ocr.github.io")
                    }
                }
            }
        }
    }

    // PDF text extraction is an optional integration. Codewhale itself stays
    // a single required executable; file and web tools report a typed
    // failed `binary_unavailable` result when Poppler is not installed.
    match codewhale_dependencies::resolve_pdftotext() {
        Some(_) => println!(
            "  {} pdftotext: available → PDF text extraction enabled",
            "✓".truecolor(aqua_r, aqua_g, aqua_b),
        ),
        None => {
            println!(
                "  {} pdftotext: not found (optional; PDF text reads fail as `binary_unavailable`)",
                "·".dimmed(),
            );
            match std::env::consts::OS {
                "macos" => println!("    Install via: brew install poppler"),
                "linux" => {
                    println!("    Install via: sudo apt install poppler-utils   (Debian/Ubuntu)")
                }
                "windows" => println!(
                    "    Install Poppler for Windows from https://blog.alivate.com.au/poppler-windows/"
                ),
                _ => {}
            }
        }
    }

    // Terminal-quirk overrides currently active. Mirrors the env
    // signals checked by `Settings::apply_env_overrides` so users
    // can see at a glance which a11y/compat overrides fired.
    println!();
    println!("{}", "Terminal Quirks:".bold());
    let term_program = std::env::var("TERM_PROGRAM").unwrap_or_default();
    let term_program_lc = term_program.to_ascii_lowercase();
    let mut any_quirk = false;
    if matches!(term_program.as_str(), "vscode" | "ghostty") {
        println!(
            "  {} TERM_PROGRAM={} → low_motion + fancy_animations=false (auto)",
            "•".truecolor(sky_r, sky_g, sky_b),
            term_program
        );
        any_quirk = true;
    }
    if term_program == "Termius"
        || std::env::var_os("SSH_CLIENT").is_some_and(|v| !v.is_empty())
        || std::env::var_os("SSH_TTY").is_some_and(|v| !v.is_empty())
    {
        println!(
            "  {} SSH/Termius session → low_motion + fancy_animations=false (auto, #1433)",
            "•".truecolor(sky_r, sky_g, sky_b)
        );
        any_quirk = true;
    }
    if term_program_lc.contains("ptyxis")
        || std::env::var_os("PTYXIS_VERSION").is_some_and(|v| !v.is_empty())
    {
        println!(
            "  {} Ptyxis detected → synchronized_output=off (auto, v0.8.31)",
            "•".truecolor(sky_r, sky_g, sky_b)
        );
        any_quirk = true;
    }
    if codewhale_runtime::settings::detected_legacy_windows_console_host() {
        println!(
            "  {} legacy Windows console host → low_motion + fancy_animations=false + bracketed_paste=false + synchronized_output=off (auto)",
            "•".truecolor(sky_r, sky_g, sky_b)
        );
        any_quirk = true;
    }
    if !any_quirk {
        println!(
            "  {} no env-driven terminal-quirk overrides active",
            "·".dimmed()
        );
    }

    // Platform and sandbox checks
    println!();
    println!("{}", "Platform:".bold());
    println!("  OS: {}", std::env::consts::OS);
    println!("  Arch: {}", std::env::consts::ARCH);

    let sandbox =
        codewhale_sandbox::get_platform_sandbox_with_bwrap_preference(config.prefers_bwrap());
    if let Some(kind) = sandbox {
        println!(
            "  {} sandbox available: {}",
            "✓".truecolor(aqua_r, aqua_g, aqua_b),
            kind
        );
    } else {
        println!(
            "  {} sandbox not available (commands run best-effort)",
            "!".truecolor(sky_r, sky_g, sky_b)
        );
    }

    println!();
    println!("{}", verdict.truecolor(aqua_r, aqua_g, aqua_b).bold());
}

/// Human-facing facts the verdict uses beyond the setup-state record. None of
/// these change structural Setup/Fleet readiness (the JSON contract): a set
/// environment variable is reported, never certified, until a live probe.
#[derive(Debug, Clone, Default)]
struct DoctorVerdictFacts {
    /// The TUI's own first-run receipt (`.onboarded`). TUI onboarding finishes
    /// after its key gate but never fills the `/setup` wizard's language and
    /// constitution steps, so `first_run_ready()` alone misreads it.
    onboarded: bool,
    /// Name of the env place holding the active provider's key, from
    /// `config::active_provider_env_api_key_source`; never the value.
    env_key_source: Option<String>,
    /// Outcome of the opt-in live API check; `None` when it did not run.
    live_probe: Option<bool>,
    /// The live check failed by running out of time, not by being refused
    /// (#6889). A model the provider is still loading looks exactly like this.
    live_probe_timed_out: bool,
    local_discovery: bool,
}

/// `set via <VAR> (value not shown; not checked offline)`.
fn doctor_env_key_label(source: &str) -> String {
    format!("set via {source} (value not shown; not checked offline)")
}

/// Where a rejected key might live, named without reading it. A route that
/// binds `api_key_env` is named by that variable; otherwise the provider's
/// first ambient variable.
fn doctor_provider_key_place(provider: codewhale_config::ProviderKind) -> String {
    provider
        .provider()
        .env_vars()
        .first()
        .copied()
        .unwrap_or("the provider environment variable")
        .to_string()
}

/// Doctor's one-line answer: ready, or the single next step (U7). Readiness
/// starts from the setup lane's record. Offline doctor never reads the secret
/// store, so a stored key is "not checked", not missing; "Not ready" is kept
/// for a missing route, a key nothing can account for, or a failed probe.
fn doctor_verdict(
    state: &codewhale_config::SetupState,
    provider: &str,
    facts: &DoctorVerdictFacts,
) -> String {
    use codewhale_config::StepStatus;
    const PROBE_HINT: &str = "run `codewhale doctor --probe-api` to verify";
    match facts.live_probe {
        Some(false) if facts.live_probe_timed_out => {
            return format!(
                "Not confirmed: the live {provider} API check got no answer in time → the model may still be loading, so wait a minute and run `codewhale doctor --probe-api` again; see API Connectivity below."
            );
        }
        Some(false) => {
            return format!(
                "Not ready: the live {provider} API check failed → see API Connectivity below; `codewhale auth set --provider {provider}` replaces a rejected key."
            );
        }
        Some(true) => return format!("Ready: the live {provider} API check passed."),
        None => {}
    }
    let env_ready = facts.env_key_source.as_deref().map(|source| {
        format!("Ready: {provider} key is set via {source} (not checked offline; {PROBE_HINT}).")
    });
    // NeedsAction means a named route exists but its key is missing, unchecked
    // or failed. Configured routes can be used without a prior probe.
    // `first_run_ready` accepts NeedsAction (a failed key still reaches the
    // wizard's ready screen), so check the provider first.
    match state.status(codewhale_config::SetupStep::ProviderModel) {
        StepStatus::Verified if state.first_run_ready() || facts.onboarded => {
            "Ready: setup is complete.".to_string()
        }
        StepStatus::Configured | StepStatus::Verified
            if state.first_run_ready() || facts.onboarded =>
        {
            format!("Ready: setup is complete (saved key not checked offline; {PROBE_HINT}).")
        }
        StepStatus::Configured | StepStatus::Verified => env_ready.unwrap_or_else(|| {
            "Not ready: first-run setup is unfinished → run `codewhale setup`.".to_string()
        }),
        StepStatus::NeedsAction => env_ready.unwrap_or_else(|| {
            // A derived NeedsAction only means offline doctor cannot see the
            // stored key; onboarding already gated on one. A NeedsAction the
            // setup lane persisted is a real missing or failed key.
            if state.inherited && facts.onboarded {
                format!(
                    "Ready: onboarding is complete (saved {provider} key not checked offline; {PROBE_HINT})."
                )
            } else if facts.local_discovery {
                format!(
                    "Not confirmed: the {provider} route has no verified key, so Codewhale and `codewhale exec` use a local Ollama model if one is running → `codewhale doctor --probe-local` checks for one; or save a key with /provider in Codewhale or `codewhale auth set --provider {provider}`."
                )
            } else {
                format!(
                    "Not ready: the {provider} route has no verified key → save one with /provider in Codewhale or `codewhale auth set --provider {provider}`; `codewhale doctor --probe-api` checks a key already saved."
                )
            }
        }),
        _ => env_ready.unwrap_or_else(|| {
            if facts.local_discovery {
                "Not confirmed: no model provider is set up, so Codewhale and `codewhale exec` use a local Ollama model if one is running → `codewhale doctor --probe-local` checks for one; or run /provider in Codewhale, or `codewhale setup`."
                    .to_string()
            } else {
                "Not ready: no model provider set up → run /provider in Codewhale, or `codewhale setup`."
                    .to_string()
            }
        }),
    }
}

#[cfg(test)]
mod doctor_verdict_tests {
    #[test]
    fn a_fresh_home_is_not_ready_and_names_the_provider_step() {
        let verdict = super::doctor_verdict(
            &codewhale_config::SetupState::default(),
            "deepseek",
            &super::DoctorVerdictFacts::default(),
        );
        assert!(verdict.starts_with("Not ready"), "{verdict}");
        assert!(verdict.contains("/provider"), "{verdict}");
    }

    #[test]
    fn a_route_without_a_verified_key_is_not_called_missing() {
        // A fresh home derives NeedsAction for the default route because
        // doctor does not read saved keys; the verdict must not claim there is
        // no provider, and names both the headless fix and the probe.
        use codewhale_config::{SetupState, SetupStep, StepEntry, StepStatus};
        let mut state = SetupState::default();
        state.set_step(
            SetupStep::ProviderModel,
            StepEntry::new(StepStatus::NeedsAction, true, "inherited"),
        );
        let verdict =
            super::doctor_verdict(&state, "deepseek", &super::DoctorVerdictFacts::default());
        assert!(verdict.starts_with("Not ready"), "{verdict}");
        assert!(!verdict.contains("no model provider"), "{verdict}");
        assert!(
            verdict.contains("`codewhale auth set --provider deepseek`"),
            "{verdict}"
        );
        assert!(verdict.contains("--probe-api"), "{verdict}");
    }

    #[test]
    fn finished_setup_with_an_unconfirmed_key_is_not_ready() {
        use codewhale_config::{
            ConstitutionChoice, RuntimePostureSource, SetupState, SetupStep, StepEntry, StepStatus,
        };
        let mut state = SetupState::default();
        state.set_step(
            SetupStep::Language,
            StepEntry::new(StepStatus::Verified, true, "0.10.1"),
        );
        state.set_step(
            SetupStep::ProviderModel,
            StepEntry::new(StepStatus::NeedsAction, true, "0.10.1"),
        );
        state.runtime_posture_source = RuntimePostureSource::Confirmed;
        state.constitution_choice = ConstitutionChoice::Bundled;
        assert!(state.first_run_ready(), "fixture must be wizard-ready");
        let verdict =
            super::doctor_verdict(&state, "deepseek", &super::DoctorVerdictFacts::default());
        assert!(verdict.starts_with("Not ready"), "{verdict}");
        assert!(verdict.contains("/provider"), "{verdict}");

        state.set_step(
            SetupStep::ProviderModel,
            StepEntry::new(StepStatus::Verified, true, "0.10.1"),
        );
        assert_eq!(
            super::doctor_verdict(&state, "deepseek", &super::DoctorVerdictFacts::default()),
            "Ready: setup is complete."
        );
    }

    fn facts(
        onboarded: bool,
        env_key_source: Option<&str>,
        live_probe: Option<bool>,
    ) -> super::DoctorVerdictFacts {
        super::DoctorVerdictFacts {
            onboarded,
            env_key_source: env_key_source.map(str::to_string),
            live_probe,
            live_probe_timed_out: false,
            local_discovery: false,
        }
    }

    #[test]
    fn completed_tui_onboarding_is_not_unfinished_setup() {
        // TUI onboarding records the route step only; never the wizard's
        // language/constitution steps.
        use codewhale_config::{SetupState, SetupStep, StepEntry, StepStatus};
        let mut state = SetupState::default();
        state.set_step(
            SetupStep::ProviderModel,
            StepEntry::new(StepStatus::Configured, true, "0.10.1"),
        );
        assert!(!state.first_run_ready(), "fixture is not wizard-ready");
        let verdict = super::doctor_verdict(&state, "openai", &facts(true, None, None));
        assert!(verdict.starts_with("Ready: setup is complete"), "{verdict}");
        assert!(verdict.contains("--probe-api"), "{verdict}");
        assert!(!verdict.contains("unfinished"), "{verdict}");

        // Without the receipt (or a key) the wizard's verdict stands.
        let verdict = super::doctor_verdict(&state, "openai", &facts(false, None, None));
        assert!(
            verdict.contains("first-run setup is unfinished"),
            "{verdict}"
        );
    }

    #[test]
    fn onboarded_home_without_a_record_is_ready_but_a_recorded_failure_is_not() {
        use codewhale_config::{InheritedConfigFacts, SetupState};
        let derived = SetupState::derive_inherited(&InheritedConfigFacts {
            has_provider_route: true,
            ..Default::default()
        });
        let verdict = super::doctor_verdict(&derived, "openai", &facts(true, None, None));
        assert!(
            verdict.starts_with("Ready: onboarding is complete"),
            "{verdict}"
        );

        let mut recorded = derived.clone();
        recorded.inherited = false;
        let verdict = super::doctor_verdict(&recorded, "openai", &facts(true, None, None));
        assert!(verdict.starts_with("Not ready"), "{verdict}");
    }

    #[test]
    fn a_set_env_key_is_ready_and_named_for_any_provider() {
        use codewhale_config::{InheritedConfigFacts, SetupState};
        // Structural derivation keeps NeedsAction: env never certifies it.
        let derived = SetupState::derive_inherited(&InheritedConfigFacts {
            has_provider_route: true,
            ..Default::default()
        });
        let verdict = super::doctor_verdict(
            &derived,
            "anthropic",
            &facts(false, Some("ANTHROPIC_API_KEY"), None),
        );
        assert_eq!(
            verdict,
            "Ready: anthropic key is set via ANTHROPIC_API_KEY (not checked offline; run `codewhale doctor --probe-api` to verify)."
        );
    }

    #[test]
    fn the_live_probe_result_decides_the_verdict() {
        use codewhale_config::{SetupState, SetupStep, StepEntry, StepStatus};
        let mut state = SetupState::default();
        state.set_step(
            SetupStep::ProviderModel,
            StepEntry::new(StepStatus::NeedsAction, true, "0.10.1"),
        );
        let verdict = super::doctor_verdict(&state, "openai", &facts(false, None, Some(true)));
        assert!(
            verdict.starts_with("Ready: the live openai API check passed"),
            "{verdict}"
        );

        let verdict = super::doctor_verdict(
            &state,
            "openai",
            &facts(true, Some("OPENAI_API_KEY"), Some(false)),
        );
        assert!(verdict.starts_with("Not ready"), "{verdict}");
        assert!(verdict.contains("API Connectivity"), "{verdict}");
    }

    /// #6889: a live check that ran out of time has not shown the key or the
    /// route to be wrong, so the verdict does not say "Not ready" or point at
    /// replacing the key.
    #[test]
    fn a_live_probe_that_timed_out_is_not_reported_as_a_rejected_key() {
        let state = codewhale_config::SetupState::default();
        let verdict = super::doctor_verdict(
            &state,
            "openai",
            &super::DoctorVerdictFacts {
                live_probe_timed_out: true,
                ..facts(true, Some("OPENAI_API_KEY"), Some(false))
            },
        );
        assert!(verdict.starts_with("Not confirmed"), "{verdict}");
        assert!(verdict.contains("may still be loading"), "{verdict}");
        assert!(verdict.contains("--probe-api"), "{verdict}");
        assert!(!verdict.contains("auth set"), "{verdict}");

        let timed_out = anyhow::Error::new(codewhale_engine::llm_client::LlmError::Timeout(
            super::DOCTOR_PROBE_TIMEOUT,
        ))
        .context("live check");
        assert!(super::doctor_probe_timed_out(&timed_out));
        let refused = anyhow::Error::new(codewhale_engine::llm_client::LlmError::ServerError {
            status: 502,
            message: "resources busy".to_string(),
        });
        assert!(!super::doctor_probe_timed_out(&refused));
        assert!(!super::doctor_probe_timed_out(&anyhow::anyhow!(
            "connect timeout"
        )));
    }

    #[test]
    fn env_key_source_names_the_providers_own_variable_without_its_value() {
        let _lock = crate::test_support::lock_test_env();
        let _cli = crate::test_support::EnvVarGuard::remove(codewhale_config::CLI_API_KEY_ENV);
        let _openai = crate::test_support::EnvVarGuard::set("OPENAI_API_KEY", "MUST-NOT-BE-SHOWN");
        let config = {
            let __cw_config_init_0_0 = Some("openai".to_string());
            let mut __cw_config_init_0_value: codewhale_runtime::config::Config =
                Default::default();
            __cw_config_init_0_value.provider = __cw_config_init_0_0;
            __cw_config_init_0_value
        };
        let source = codewhale_runtime::config::active_provider_env_api_key_source(&config)
            .expect("ambient key present");
        assert_eq!(source, "OPENAI_API_KEY");
        let label = super::doctor_env_key_label(&source);
        assert_eq!(
            label,
            "set via OPENAI_API_KEY (value not shown; not checked offline)"
        );
        assert!(!label.contains("MUST-NOT-BE-SHOWN"));
        // The structural contract is untouched: env presence is not readiness.
        assert!(
            !super::resolve_credential_diagnostic(&config)
                .availability
                .certifies_ready()
        );

        let _openai = crate::test_support::EnvVarGuard::remove("OPENAI_API_KEY");
        assert_eq!(
            codewhale_runtime::config::active_provider_env_api_key_source(&config),
            None
        );
    }
}

const DOCTOR_LEGACY_STATE_ITEMS: &[&str] = &[
    "sessions",
    "tasks",
    "skills",
    "slop_ledger",
    "trophies",
    "catalog",
    "review-receipts",
    "config.toml",
    "settings.toml",
    "mcp.json",
];
const DOCTOR_SESSION_RECOVERY_HUMAN_SAMPLE_LIMIT: usize = 20;
const DOCTOR_SESSION_RECOVERY_JSON_SAMPLE_LIMIT: usize = 100;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum DoctorLegacyStateStatus {
    PrimaryOnly,
    LegacyOnly,
    Both,
    Absent,
}

impl DoctorLegacyStateStatus {
    fn as_str(self) -> &'static str {
        match self {
            Self::PrimaryOnly => "primary_only",
            Self::LegacyOnly => "legacy_only",
            Self::Both => "both",
            Self::Absent => "absent",
        }
    }
}

#[derive(Debug, Clone)]
struct DoctorLegacyStateEntry {
    name: &'static str,
    primary_path: PathBuf,
    legacy_path: PathBuf,
    primary_present: bool,
    legacy_present: bool,
    status: DoctorLegacyStateStatus,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum DoctorSessionRecoveryStatus {
    Isolated,
    NoLegacySessions,
    MigrationPending,
    MigrationIncomplete,
    MigrationComplete,
    ScanFailed,
}

impl DoctorSessionRecoveryStatus {
    fn as_str(self) -> &'static str {
        match self {
            Self::Isolated => "isolated",
            Self::NoLegacySessions => "no_legacy_sessions",
            Self::MigrationPending => "migration_pending",
            Self::MigrationIncomplete => "migration_incomplete",
            Self::MigrationComplete => "migration_complete",
            Self::ScanFailed => "scan_failed",
        }
    }
}

#[derive(Debug, Clone)]
struct DoctorRecoverableSessionEntry {
    name: PathBuf,
    source_path: PathBuf,
    destination_path: PathBuf,
}

#[derive(Debug, Clone)]
struct DoctorSessionRecoveryReport {
    status: DoctorSessionRecoveryStatus,
    primary_sessions_path: PathBuf,
    legacy_sessions_path: PathBuf,
    codewhale_home_is_explicit: bool,
    legacy_session_file_count: usize,
    already_present_file_count: usize,
    recoverable_file_count: usize,
    /// Bounded filename/path sample; the total is `recoverable_file_count`.
    recoverable: Vec<DoctorRecoverableSessionEntry>,
    error: Option<String>,
}

impl DoctorSessionRecoveryReport {
    fn needs_attention(&self) -> bool {
        matches!(
            self.status,
            DoctorSessionRecoveryStatus::MigrationPending
                | DoctorSessionRecoveryStatus::MigrationIncomplete
                | DoctorSessionRecoveryStatus::ScanFailed
        )
    }
}

fn doctor_legacy_state_status(
    primary_present: bool,
    legacy_present: bool,
) -> DoctorLegacyStateStatus {
    match (primary_present, legacy_present) {
        (true, false) => DoctorLegacyStateStatus::PrimaryOnly,
        (false, true) => DoctorLegacyStateStatus::LegacyOnly,
        (true, true) => DoctorLegacyStateStatus::Both,
        (false, false) => DoctorLegacyStateStatus::Absent,
    }
}

fn doctor_state_roots() -> (PathBuf, PathBuf) {
    let code_home =
        codewhale_config::codewhale_home().unwrap_or_else(|_| PathBuf::from("~/.codewhale"));
    let legacy_home = if codewhale_config::codewhale_home_is_explicit() {
        code_home.join(codewhale_config::LEGACY_APP_DIR)
    } else {
        codewhale_config::legacy_deepseek_home().unwrap_or_else(|_| PathBuf::from("~/.deepseek"))
    };
    (code_home, legacy_home)
}

fn doctor_legacy_state_report(
    primary_root: &Path,
    legacy_root: &Path,
) -> Vec<DoctorLegacyStateEntry> {
    DOCTOR_LEGACY_STATE_ITEMS
        .iter()
        .copied()
        .map(|name| {
            let primary_path = primary_root.join(name);
            let legacy_path = legacy_root.join(name);
            let primary_present = primary_path.exists();
            let legacy_present = legacy_path.exists();
            let status = doctor_legacy_state_status(primary_present, legacy_present);
            DoctorLegacyStateEntry {
                name,
                primary_path,
                legacy_path,
                primary_present,
                legacy_present,
                status,
            }
        })
        .collect()
}

/// Compare legacy and primary session filenames without opening session files.
///
/// This is deliberately separate from `SessionManager::default_location()`:
/// constructing the manager can trigger the additive legacy migration, while
/// doctor must remain a read-only diagnostic. Session history is stored as
/// top-level JSON files. The state and sessions roots may be links the user
/// made to relocate state (a junction onto another volume is the supported way
/// to keep `~/.codewhale` off the system drive on Windows), so a linked root is
/// resolved and read through; symlink *entries* observed during the scan are
/// still ignored, so the diagnostic does not traverse checkpoint internals or
/// per-file link targets. These checks are best-effort observations, not a
/// race-free no-follow guarantee. A matching filename is only a regular-file
/// counterpart check: doctor does not parse or compare session descriptors.
fn doctor_session_recovery_report(
    primary_root: &Path,
    legacy_root: &Path,
    codewhale_home_is_explicit: bool,
) -> DoctorSessionRecoveryReport {
    let primary_sessions_path = primary_root.join("sessions");
    let legacy_sessions_path = legacy_root.join("sessions");
    let mut report = DoctorSessionRecoveryReport {
        status: DoctorSessionRecoveryStatus::NoLegacySessions,
        primary_sessions_path,
        legacy_sessions_path,
        codewhale_home_is_explicit,
        legacy_session_file_count: 0,
        already_present_file_count: 0,
        recoverable_file_count: 0,
        recoverable: Vec::new(),
        error: None,
    };

    if codewhale_home_is_explicit {
        report.status = DoctorSessionRecoveryStatus::Isolated;
        return report;
    }

    let legacy_root_is_present =
        match doctor_session_directory_is_safe(legacy_root, "legacy state root") {
            Ok(present) => present,
            Err(error) => {
                report.status = DoctorSessionRecoveryStatus::ScanFailed;
                report.error = Some(error);
                return report;
            }
        };
    if !legacy_root_is_present {
        return report;
    }
    if let Err(error) = doctor_session_directory_is_safe(primary_root, "primary state root") {
        report.status = DoctorSessionRecoveryStatus::ScanFailed;
        report.error = Some(error);
        return report;
    }

    let legacy_sessions_are_present = match doctor_session_directory_is_safe(
        &report.legacy_sessions_path,
        "legacy sessions root",
    ) {
        Ok(present) => present,
        Err(error) => {
            report.status = DoctorSessionRecoveryStatus::ScanFailed;
            report.error = Some(error);
            return report;
        }
    };
    if !legacy_sessions_are_present {
        return report;
    }
    let primary_sessions_are_present = match doctor_session_directory_is_safe(
        &report.primary_sessions_path,
        "primary sessions root",
    ) {
        Ok(present) => present,
        Err(error) => {
            report.status = DoctorSessionRecoveryStatus::ScanFailed;
            report.error = Some(error);
            return report;
        }
    };

    let entries = match std::fs::read_dir(&report.legacy_sessions_path) {
        Ok(entries) => entries,
        Err(err) => {
            report.status = DoctorSessionRecoveryStatus::ScanFailed;
            report.error = Some(format!(
                "could not inspect legacy session filenames at {}: {err}",
                codewhale_util::utils::display_path(&report.legacy_sessions_path)
            ));
            return report;
        }
    };

    for entry in entries {
        let entry = match entry {
            Ok(entry) => entry,
            Err(err) => {
                report.status = DoctorSessionRecoveryStatus::ScanFailed;
                report.error = Some(format!(
                    "could not inspect an entry under {}: {err}",
                    codewhale_util::utils::display_path(&report.legacy_sessions_path)
                ));
                return report;
            }
        };
        let file_type = match entry.file_type() {
            Ok(file_type) => file_type,
            Err(err) => {
                report.status = DoctorSessionRecoveryStatus::ScanFailed;
                report.error = Some(format!(
                    "could not inspect legacy session entry metadata under {}: {err}",
                    codewhale_util::utils::display_path(&report.legacy_sessions_path)
                ));
                return report;
            }
        };
        if !file_type.is_file() || entry.path().extension().is_none_or(|ext| ext != "json") {
            continue;
        }

        report.legacy_session_file_count += 1;
        let name = PathBuf::from(entry.file_name());
        let destination_path = report.primary_sessions_path.join(&name);
        match std::fs::symlink_metadata(&destination_path) {
            Ok(metadata) if metadata.file_type().is_file() => {
                report.already_present_file_count += 1;
            }
            Ok(metadata) => {
                report.status = DoctorSessionRecoveryStatus::ScanFailed;
                let shape = if metadata.file_type().is_symlink() {
                    "destination session entry is a symlink"
                } else {
                    "destination session entry is not a regular file"
                };
                report.error = Some(format!(
                    "could not inspect destination session metadata at {}: {shape}",
                    codewhale_util::utils::display_path(&destination_path)
                ));
                return report;
            }
            Err(err) if err.kind() == io::ErrorKind::NotFound => {
                report.recoverable_file_count += 1;
                record_doctor_recoverable_session(
                    &mut report.recoverable,
                    DoctorRecoverableSessionEntry {
                        source_path: entry.path(),
                        destination_path,
                        name,
                    },
                );
            }
            Err(err) => {
                report.status = DoctorSessionRecoveryStatus::ScanFailed;
                report.error = Some(format!(
                    "could not inspect destination metadata at {}: {err}",
                    codewhale_util::utils::display_path(&destination_path)
                ));
                return report;
            }
        }
    }

    report.status = if report.legacy_session_file_count == 0 {
        DoctorSessionRecoveryStatus::NoLegacySessions
    } else if report.recoverable_file_count == 0 {
        DoctorSessionRecoveryStatus::MigrationComplete
    } else if primary_sessions_are_present {
        DoctorSessionRecoveryStatus::MigrationIncomplete
    } else {
        DoctorSessionRecoveryStatus::MigrationPending
    };
    report
}

/// Validate a session-state directory from observed metadata.
///
/// `doctor` only compares top-level filenames. A directory the user relocated
/// behind a link is resolved and inspected as its target: a junction is the
/// supported way to keep `~/.codewhale` off the system drive on Windows, and
/// refusing it left the migration diagnostic unable to run on exactly the
/// layout it reports about. Only this directory check follows the link; the
/// enumeration below still walks the path it was handed.
///
/// This is a best-effort observation, not a race-free no-follow guarantee.
/// Missing paths are normal on a fresh install and are reported as `false`.
fn doctor_session_directory_is_safe(path: &Path, label: &str) -> std::result::Result<bool, String> {
    let metadata = match std::fs::symlink_metadata(path) {
        Ok(metadata) => metadata,
        Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(false),
        Err(error) => {
            return Err(format!(
                "could not inspect {label} at {}: {error}",
                codewhale_util::utils::display_path(path)
            ));
        }
    };
    let metadata = if codewhale_util::path_identity::metadata_is_link_or_reparse(&metadata) {
        match std::fs::metadata(path) {
            Ok(target) => target,
            Err(error) => {
                return Err(format!(
                    "could not resolve {label} at {}: {error}",
                    codewhale_util::utils::display_path(path)
                ));
            }
        }
    } else {
        metadata
    };
    if !metadata.is_dir() {
        return Err(format!(
            "could not inspect {label} at {}: path is not a directory",
            codewhale_util::utils::display_path(path)
        ));
    }
    Ok(true)
}

/// Keep the report bounded while preserving a deterministic, lexical sample.
/// `read_dir` order is platform- and filesystem-dependent, so retaining the
/// first entries encountered would make the JSON and human receipts drift.
fn record_doctor_recoverable_session(
    recoverable: &mut Vec<DoctorRecoverableSessionEntry>,
    entry: DoctorRecoverableSessionEntry,
) {
    let insert_at = recoverable
        .binary_search_by(|existing| existing.name.cmp(&entry.name))
        .unwrap_or_else(|index| index);
    if recoverable.len() == DOCTOR_SESSION_RECOVERY_JSON_SAMPLE_LIMIT
        && insert_at == recoverable.len()
    {
        return;
    }
    recoverable.insert(insert_at, entry);
    if recoverable.len() > DOCTOR_SESSION_RECOVERY_JSON_SAMPLE_LIMIT {
        recoverable.pop();
    }
}

fn legacy_state_needs_attention(entry: &DoctorLegacyStateEntry) -> bool {
    entry.name != "sessions"
        && matches!(
            entry.status,
            DoctorLegacyStateStatus::LegacyOnly | DoctorLegacyStateStatus::Both
        )
}

fn print_doctor_legacy_state_report(
    report: &[DoctorLegacyStateEntry],
    session_recovery: &DoctorSessionRecoveryReport,
    ok_rgb: (u8, u8, u8),
    warn_rgb: (u8, u8, u8),
) {
    use colored::Colorize;

    let attention: Vec<_> = report
        .iter()
        .filter(|entry| legacy_state_needs_attention(entry))
        .collect();
    if attention.is_empty()
        && !session_recovery.needs_attention()
        && session_recovery.status != DoctorSessionRecoveryStatus::Isolated
    {
        println!(
            "  {} legacy state: no known .deepseek entries need migration",
            "✓".truecolor(ok_rgb.0, ok_rgb.1, ok_rgb.2)
        );
    } else if !attention.is_empty() {
        println!(
            "  {} legacy state needs review:",
            "!".truecolor(warn_rgb.0, warn_rgb.1, warn_rgb.2)
        );
        for entry in attention {
            match entry.status {
                DoctorLegacyStateStatus::LegacyOnly => {
                    println!(
                        "    {} {} exists but {} is missing",
                        "!".truecolor(warn_rgb.0, warn_rgb.1, warn_rgb.2),
                        codewhale_util::utils::display_path(&entry.legacy_path),
                        codewhale_util::utils::display_path(&entry.primary_path),
                    );
                }
                DoctorLegacyStateStatus::Both => {
                    println!(
                        "    {} {} exists alongside primary {}; legacy data may still need review",
                        "!".truecolor(warn_rgb.0, warn_rgb.1, warn_rgb.2),
                        codewhale_util::utils::display_path(&entry.legacy_path),
                        codewhale_util::utils::display_path(&entry.primary_path),
                    );
                }
                DoctorLegacyStateStatus::PrimaryOnly | DoctorLegacyStateStatus::Absent => {}
            }
        }
        println!(
            "    Start Codewhale once to trigger safe migration where available, then rerun `codewhale doctor`."
        );
    }

    print_doctor_session_recovery_report(session_recovery, ok_rgb, warn_rgb);
}

fn print_doctor_session_recovery_report(
    report: &DoctorSessionRecoveryReport,
    ok_rgb: (u8, u8, u8),
    warn_rgb: (u8, u8, u8),
) {
    use colored::Colorize;

    match report.status {
        DoctorSessionRecoveryStatus::Isolated => {
            println!(
                "  {} legacy sessions: ambient ~/.deepseek/sessions was not inspected because CODEWHALE_HOME is set",
                "·".dimmed()
            );
            println!(
                "    This preserves the explicit home boundary. To inspect the default home, use a separate shell with CODEWHALE_HOME unset and rerun `codewhale doctor`."
            );
        }
        DoctorSessionRecoveryStatus::NoLegacySessions => {
            println!(
                "  {} legacy sessions: no top-level session JSON files found",
                "✓".truecolor(ok_rgb.0, ok_rgb.1, ok_rgb.2)
            );
        }
        DoctorSessionRecoveryStatus::MigrationComplete => {
            println!(
                "  {} legacy sessions: all {} filename(s) have regular-file counterparts under {}; descriptor contents were not compared and legacy originals remain preserved",
                "✓".truecolor(ok_rgb.0, ok_rgb.1, ok_rgb.2),
                report.legacy_session_file_count,
                codewhale_util::utils::display_path(&report.primary_sessions_path),
            );
        }
        DoctorSessionRecoveryStatus::MigrationPending
        | DoctorSessionRecoveryStatus::MigrationIncomplete => {
            let label = if report.status == DoctorSessionRecoveryStatus::MigrationIncomplete {
                "migration is incomplete"
            } else {
                "migration has not completed"
            };
            println!(
                "  {} legacy sessions: {label}; {} recoverable file(s) are absent from {}",
                "!".truecolor(warn_rgb.0, warn_rgb.1, warn_rgb.2),
                report.recoverable_file_count,
                codewhale_util::utils::display_path(&report.primary_sessions_path),
            );
            for entry in report
                .recoverable
                .iter()
                .take(DOCTOR_SESSION_RECOVERY_HUMAN_SAMPLE_LIMIT)
            {
                println!(
                    "    {} {} -> {}",
                    "·".dimmed(),
                    codewhale_util::utils::display_path(&entry.source_path),
                    codewhale_util::utils::display_path(&entry.destination_path),
                );
            }
            if report.recoverable_file_count > DOCTOR_SESSION_RECOVERY_HUMAN_SAMPLE_LIMIT {
                println!(
                    "    · {} more filename(s); `codewhale doctor --json` includes a bounded metadata-only sample",
                    report.recoverable_file_count - DOCTOR_SESSION_RECOVERY_HUMAN_SAMPLE_LIMIT
                );
            }
            println!("    Safe recovery:");
            println!(
                "      1. Back up {} and {} (if present).",
                codewhale_util::utils::display_path(&report.legacy_sessions_path),
                codewhale_util::utils::display_path(&report.primary_sessions_path),
            );
            println!(
                "      2. Close other Codewhale processes, then run `codewhale sessions`; migration adds only missing files, never overwrites primary files, and leaves legacy originals in place."
            );
            println!(
                "      3. Rerun `codewhale doctor`. If filenames remain, keep both backups and report only the listed source/destination names."
            );
        }
        DoctorSessionRecoveryStatus::ScanFailed => {
            println!(
                "  {} legacy sessions: recovery diagnostic could not complete",
                "!".truecolor(warn_rgb.0, warn_rgb.1, warn_rgb.2)
            );
            if let Some(error) = report.error.as_deref() {
                println!("    {error}");
            }
            println!(
                "    Keep both session directories unchanged, back them up, fix path permissions or shape, and rerun `codewhale doctor` before attempting migration."
            );
        }
    }
    if report.status != DoctorSessionRecoveryStatus::Isolated {
        println!(
            "    Doctor inspected filenames and filesystem metadata only; it did not read chat contents, traverse checkpoints, or modify session files."
        );
    }
}

fn doctor_session_recovery_json(report: &DoctorSessionRecoveryReport) -> serde_json::Value {
    use serde_json::json;

    let recoverable: Vec<_> = report
        .recoverable
        .iter()
        .take(DOCTOR_SESSION_RECOVERY_JSON_SAMPLE_LIMIT)
        .map(|entry| {
            json!({
                "name": entry.name.display().to_string(),
                "source_path": entry.source_path.display().to_string(),
                "destination_path": entry.destination_path.display().to_string(),
            })
        })
        .collect();

    json!({
        "status": report.status.as_str(),
        "needs_attention": report.needs_attention(),
        "read_only": true,
        "chat_contents_read": false,
        "checkpoint_internals_scanned": false,
        "session_descriptors_compared": false,
        "counterpart_check": "top_level_filename_and_regular_file_only",
        "codewhale_home_is_explicit": report.codewhale_home_is_explicit,
        "legacy_sessions_path": report.legacy_sessions_path.display().to_string(),
        "primary_sessions_path": report.primary_sessions_path.display().to_string(),
        "legacy_session_file_count": report.legacy_session_file_count,
        "already_present_file_count": report.already_present_file_count,
        "recoverable_file_count": report.recoverable_file_count,
        "recoverable_files": recoverable,
        "recoverable_files_truncated": report.recoverable_file_count > report.recoverable.len(),
        "error": report.error,
        "recovery_command": if report.needs_attention() && report.status != DoctorSessionRecoveryStatus::ScanFailed {
            Some("codewhale sessions")
        } else {
            None
        },
    })
}

fn doctor_legacy_state_json(
    primary_root: &Path,
    legacy_root: &Path,
    report: &[DoctorLegacyStateEntry],
    session_recovery: &DoctorSessionRecoveryReport,
) -> serde_json::Value {
    use serde_json::json;

    let legacy_only = report
        .iter()
        .filter(|entry| entry.status == DoctorLegacyStateStatus::LegacyOnly)
        .count();
    let both = report
        .iter()
        .filter(|entry| entry.status == DoctorLegacyStateStatus::Both)
        .count();
    let entries: Vec<_> = report
        .iter()
        .map(|entry| {
            json!({
                "name": entry.name,
                "primary_path": entry.primary_path.display().to_string(),
                "legacy_path": entry.legacy_path.display().to_string(),
                "primary_present": entry.primary_present,
                "legacy_present": entry.legacy_present,
                "status": entry.status.as_str(),
            })
        })
        .collect();

    json!({
        "primary_root": primary_root.display().to_string(),
        "legacy_root": legacy_root.display().to_string(),
        "needs_attention": report.iter().any(legacy_state_needs_attention) || session_recovery.needs_attention(),
        "legacy_only_count": legacy_only,
        "dual_present_count": both,
        "session_recovery": doctor_session_recovery_json(session_recovery),
        "entries": entries,
    })
}

fn doctor_setup_state(
    config: &Config,
    workspace: &Path,
) -> (codewhale_config::SetupState, &'static str) {
    if let Ok(Some(state)) = codewhale_config::SetupState::load() {
        return (state, "persisted");
    }

    (
        codewhale_config::SetupState::derive_inherited(&doctor_inherited_setup_facts(
            config, workspace,
        )),
        "derived",
    )
}

fn doctor_inherited_setup_facts(
    config: &Config,
    workspace: &Path,
) -> codewhale_config::InheritedConfigFacts {
    let user_constitution = codewhale_config::UserConstitution::load().ok();
    let user_constitution_validity = user_constitution.as_ref().map_or(
        codewhale_config::ConstitutionValidity::Unknown,
        codewhale_config::UserConstitutionLoad::validity,
    );
    let has_user_constitution = user_constitution
        .as_ref()
        .is_some_and(|loaded| !matches!(loaded, codewhale_config::UserConstitutionLoad::Missing));
    let has_expert_override = codewhale_config::codewhale_home()
        .ok()
        .map(|home| {
            home.join(Path::new(
                codewhale_engine::prompts::CONSTITUTION_OVERRIDE_FILE,
            ))
        })
        .is_some_and(|path| path.exists());

    codewhale_config::InheritedConfigFacts {
        language: None,
        has_provider_route: !config.default_model().trim().is_empty(),
        has_credentials_or_local_runtime: doctor_has_credentials_or_local_runtime(config),
        trust_chosen: !codewhale_runtime::config::needs_trust(workspace),
        has_expert_override,
        has_user_constitution,
        user_constitution_validity,
    }
}

fn doctor_has_credentials_or_local_runtime(config: &Config) -> bool {
    resolve_credential_diagnostic(config)
        .availability
        .certifies_ready()
}

fn print_doctor_setup_report(
    config: &Config,
    workspace: &Path,
    state: &codewhale_config::SetupState,
    source: &str,
    ok_rgb: (u8, u8, u8),
    warn_rgb: (u8, u8, u8),
) {
    use colored::Colorize;

    let credential = resolve_credential_diagnostic(config);
    // Setup completion is persisted independently from credential probing.
    // Ordinary doctor deliberately does not read environment values or the
    // durable secret store, so `not_probed` must not erase a completed lane.
    let first_run_ready = state.first_run_ready();
    let update_ready =
        state.update_ready(codewhale_engine::prompts::CONSTITUTION_CHECKPOINT_VERSION);
    let operate_ready = state.operate_ready();
    let first_run_icon = if first_run_ready {
        "✓".truecolor(ok_rgb.0, ok_rgb.1, ok_rgb.2)
    } else {
        "!".truecolor(warn_rgb.0, warn_rgb.1, warn_rgb.2)
    };
    let update_icon = if update_ready {
        "✓".truecolor(ok_rgb.0, ok_rgb.1, ok_rgb.2)
    } else {
        "!".truecolor(warn_rgb.0, warn_rgb.1, warn_rgb.2)
    };
    let operate_icon = if operate_ready {
        "✓".truecolor(ok_rgb.0, ok_rgb.1, ok_rgb.2)
    } else {
        "!".truecolor(warn_rgb.0, warn_rgb.1, warn_rgb.2)
    };

    println!();
    println!("{}", "Setup State:".bold());
    println!("  · source: {source}");
    println!(
        "  · credential: source={}, availability={}",
        doctor_api_key_source_label(credential.source),
        credential.availability.label()
    );
    println!(
        "  {first_run_icon} first-run: {}",
        doctor_ready_label(first_run_ready)
    );
    // An update checkpoint only means something once a prior setup exists;
    // on a fresh home it is a stale version number with nothing to update.
    if first_run_ready {
        println!(
            "  {update_icon} update checkpoint {}: {}",