diff --git a/crates/daemon/src/availability.rs b/crates/daemon/src/availability.rs index 8ac89f39..d3cc2a58 100644 --- a/crates/daemon/src/availability.rs +++ b/crates/daemon/src/availability.rs @@ -202,6 +202,51 @@ pub async fn probe_smith(cache: &std::sync::Mutex) -> Availab Availability::missing("no API key or OAuth credential found") } +/// Model-spec prefixes the title-gen one-shot cannot build a provider for. +/// OAuth subscriptions drive full smith sessions fine; `--title-mode` only +/// knows the direct-API-key providers and bails loudly on these (spec +/// 0071). Kept in sync with title-mode's own provider selection. +const TITLE_GEN_UNSUPPORTED_PREFIXES: [&str; 4] = + ["claude-oauth", "codex-oauth", "grok-oauth", "kimi-oauth"]; + +/// Whether the cheap `smith --title-mode` one-shot could actually resolve a +/// model: an explicit `CONSTRUCT_SMITH_MODEL` pin naming a provider it can +/// build, or one of the direct API keys on its ladder. +/// +/// Deliberately narrower than [`probe_smith`], which answers "could a +/// session start via *some* explicit choice" and therefore counts OAuth +/// subscriptions and a reachable Ollama — the asymmetry spec 0071 records +/// as intentional. Auto-title needs the narrower question: asking the wider +/// one sends an OAuth-only machine into a one-shot that can only fail, +/// instead of the same-harness probe fallback written for exactly that +/// machine (spec 0151). +pub fn smith_title_gen_available() -> bool { + title_gen_available_with(crate::daemon_env::var) +} + +/// Pure core of [`smith_title_gen_available`], parameterized over the +/// environment lookup so tests don't have to mutate process env. +fn title_gen_available_with(lookup: impl Fn(&str) -> Option) -> bool { + // A pin is an explicit user choice, so it wins over the key ladder the + // same way it does inside title-mode — but only when title-mode can + // actually honor it. + if let Some(pin) = lookup("CONSTRUCT_SMITH_MODEL") { + let prefix = pin.split(':').next().unwrap_or_default(); + return !TITLE_GEN_UNSUPPORTED_PREFIXES.contains(&prefix); + } + [ + "ANTHROPIC_API_KEY", + "OPENAI_API_KEY", + "GEMINI_API_KEY", + "GOOGLE_API_KEY", + "META_API_KEY", + "MODEL_API_KEY", + "DEEPSEEK_API_KEY", + ] + .iter() + .any(|k| lookup(k).is_some()) +} + /// Existence-only mirror of `CredStore::locate` in /// `adapter-smith/src/provider/claude_oauth.rs`: explicit file override, /// then the default credentials file, then the macOS keychain item. No @@ -503,6 +548,11 @@ pub async fn smith_auth_methods( pub struct FeatureInputs { /// smith's credential probe result ([`probe_smith`]). pub smith: Availability, + /// Whether the title-gen one-shot can resolve a model + /// ([`smith_title_gen_available`]). Narrower than `smith`: an + /// OAuth-only machine can run smith sessions but not `--title-mode`, + /// and the auto-title row must report what auto-title actually does. + pub title_gen: bool, /// `[suggest] enabled` from config. pub suggest_enabled: bool, /// The orchestrator's configured harness and that harness's @@ -517,7 +567,11 @@ pub struct FeatureInputs { pub fn ambient_features(inputs: &FeatureInputs) -> Vec { use construct_protocol::{FeatureInfo, FeatureStatus}; let smith_ok = inputs.smith.available; - let auto_title = if smith_ok { + // Auto-title keys off `title_gen`, not `smith_ok`: a machine with only + // an OAuth subscription runs smith sessions fine but cannot run the + // title one-shot, and reporting Ok there promises a generator that + // never runs. + let auto_title = if inputs.title_gen { FeatureInfo { id: "auto_title".to_string(), label: "Session auto-naming".to_string(), @@ -529,9 +583,9 @@ pub fn ambient_features(inputs: &FeatureInputs) -> Vec