opencrabs 0.3.66

The autonomous, self-improving AI agent. Single Rust binary. Every channel. Install with: cargo install opencrabs
//! Startup config diagnostics (#477): non-fatal warnings for config
//! mistakes that otherwise produce silent, confusing behavior. Pure
//! function so the rules are unit tested; the caller logs each line once
//! at startup (never on hot reload, which would spam).

use crate::config::Config;

/// Collect startup warning lines for `config`. `raw_toml` is the config
/// file's raw text when available, used to spot keys the schema silently
/// ignores (serde drops unknown keys without a peep — the root of the
/// "why does my config change do nothing" archaeology this exists to end).
pub fn startup_warnings(config: &Config, raw_toml: Option<&str>) -> Vec<String> {
    let mut warns = Vec::new();

    // 1. `working_directory` is NOT a config key: the schema has no such
    //    field, so serde ignores it silently while the agent falls back to
    //    its own cwd — RSI cron, sub-agents and command_code start
    //    somewhere unexpected with zero errors. Flag the ignored key, and
    //    flag a nonexistent path on top.
    if let Some(raw) = raw_toml
        && let Ok(value) = raw.parse::<toml::Value>()
    {
        let candidates = [
            value.get("working_directory"),
            value.get("agent").and_then(|a| a.get("working_directory")),
        ];
        for c in candidates.into_iter().flatten() {
            if let Some(path) = c.as_str() {
                let expanded = expand_home(path);
                let missing = if std::path::Path::new(&expanded).is_dir() {
                    ""
                } else {
                    " — and the path does not exist on disk"
                };
                warns.push(format!(
                    "config: working_directory = \"{path}\" has NO effect — the schema has \
                     no such key (unknown keys are silently ignored). The working directory \
                     is per-session: use /cd, or set it when creating the session{missing}"
                ));
            }
        }
    }

    // 2. default_provider without force_default: new sessions get the
    //    default, but every already-open session keeps its own pair
    //    (isolation by design), so a config change looks like it did
    //    nothing. Hint at the #466 escape hatch.
    if let Some(dp) = config.agent.default_provider.as_deref() {
        match crate::brain::provider::factory::provider_config_by_name(config, dp) {
            None => warns.push(format!(
                "config: [agent] default_provider = \"{dp}\" does not match any configured \
                 provider section — new sessions will fall back to the priority list"
            )),
            Some(section) if !section.force_default => warns.push(format!(
                "config: default_provider = \"{dp}\" applies to NEW sessions only; existing \
                 sessions keep their stored pair. Add force_default = true under \
                 [providers.{dp}] to push the default to all sessions on config reload"
            )),
            Some(_) => {}
        }
    }

    warns
}

fn expand_home(path: &str) -> String {
    if let Some(rest) = path.strip_prefix("~/")
        && let Some(home) = dirs::home_dir()
    {
        return home.join(rest).to_string_lossy().to_string();
    }
    path.to_string()
}