diff --git a/CHANGELOG.md b/CHANGELOG.md index e398b5c..2828a5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,20 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] +### Added + +- **`/model --default` and `/backend --default`.** Pin the active (or newly + selected) backend/model into `agent.config.json` without re-running `/setup`. + `/model … --default` writes `backend` + `modelOverride`; `/backend … --default` + writes `backend` and clears `modelOverride`. Session changes still apply if the + file cannot be written; invalid JSON is never overwritten. +- **Default-aware `/model` and `/backend` pickers.** Interactive lists now mark + each entry with `(selected)` when it is the live session choice and `(default)` + when it is what `agent.config.json` would load on next launch (both show when + they coincide). Append `--default` to a selection (e.g. `3 --default`) to pin + it as you pick; otherwise a `y/N` prompt offers to save the choice as the + project default. + ## [1.2.3] - 2026-07-16 ### Fixed diff --git a/README.md b/README.md index 5fd480d..bbed012 100644 --- a/README.md +++ b/README.md @@ -249,7 +249,14 @@ Switch at runtime with `/backend `. Endpoint overrides: Each backend has one sensible default; local backends default to a 7B coder that runs on modest hardware. Override any time with `/model`, `AGENT_MODEL`, -or `modelOverride` in your config. +or `modelOverride` in your config. Append `--default` to `/model` or `/backend` +to write the choice into `agent.config.json` (surgical merge: only `backend` and +`modelOverride`). `/model --default` pins the active model; `/backend --default` +pins the active backend and clears `modelOverride` so the next launch uses that +backend's built-in default model. In the interactive pickers you can also append +`--default` to a selection (e.g. `3 --default`) to pin it as you choose, or +answer the `y/N` save prompt afterwards; each entry is tagged `(selected)` for +the live session choice and `(default)` for what's persisted on disk. | Backend | Default model | |---------|---------------| @@ -354,8 +361,8 @@ this exact call`. The session cache resets on `/new`. **Backend, model, tools** ``` -/backend switch backend -/model [id] list / pick a model (shows context + cost when known) +/backend [--default] switch backend; --default writes agent.config.json +/model [id] [--default] list / pick a model; --default pins backend+model /tools auto|fixed|<…> show or set the active tool pool /auth manage API keys and OAuth credentials /login openai-codex sign in with ChatGPT/Codex subscription OAuth @@ -430,12 +437,16 @@ when present. If a cloud model does not expose cost, the turn shows `$?` and prefixes the session total with `≥` to signal it is a lower bound, not a fiction. -The `/model` picker shows the same data while you choose: +The `/model` picker shows the same data while you choose, tagging the live +session choice `(selected)` and the value persisted in `agent.config.json` +`(default)`: ```text - 1) gpt-4o-mini 128k ctx · $0.15/$0.60 per Mtoken - 2) gpt-4o 128k ctx · $2.50/$10.00 per Mtoken - 3) o1-mini 128k ctx · $3.00/$12.00 per Mtoken + 1) gpt-4o-mini (selected) (default) 128k ctx · $0.15/$0.60 per Mtoken + 2) gpt-4o 128k ctx · $2.50/$10.00 per Mtoken + 3) o1-mini 128k ctx · $3.00/$12.00 per Mtoken + + Select (1-3) · append --default to pin: ``` ### Claude Fable tracker @@ -939,8 +950,8 @@ Full list with comments in [`.env.example`](.env.example). ### `agent.config.json` -For project-level defaults, run `/setup` or drop a JSON file at the repo -root. Common shape: +For project-level defaults, run `/setup`, use `/backend --default` / +`/model --default`, or drop a JSON file at the repo root. Common shape: ```json { diff --git a/src/commands/config_cmds.rs b/src/commands/config_cmds.rs index b5b89a5..06727d6 100644 --- a/src/commands/config_cmds.rs +++ b/src/commands/config_cmds.rs @@ -499,20 +499,109 @@ fn guess_image_mime(path: &std::path::Path) -> &'static str { } } +fn report_default_persist(result: Result<()>) { + match result { + Ok(()) => println!( + " {DIM}· saved default in {RESET}{CYAN}{}{RESET}", + crate::config::AGENT_CONFIG_PATH + ), + Err(e) => println!(" {RED}✗{RESET} {DIM}could not save default: {e}{RESET}"), + } +} + +fn persist_model_as_default(state: &AppState) { + report_default_persist(crate::config::persist_backend_model_defaults( + Path::new(crate::config::AGENT_CONFIG_PATH), + state.config.backend, + Some(state.model.as_str()), + )); +} + +fn persist_backend_as_default(state: &AppState) { + // Clears modelOverride on disk so the next launch uses the backend default. + report_default_persist(crate::config::persist_backend_model_defaults( + Path::new(crate::config::AGENT_CONFIG_PATH), + state.config.backend, + None, + )); +} + +/// Build the trailing picker marker for an entry: `(selected)` when it is the +/// live session choice, `(default)` when it is the value persisted in +/// `agent.config.json`. Both may show together when they coincide. +fn picker_marker(is_selected: bool, is_default: bool) -> String { + let mut out = String::new(); + if is_selected { + out.push_str(&format!(" {DIM}(selected){RESET}")); + } + if is_default { + out.push_str(&format!(" {DIM}(default){RESET}")); + } + out +} + +/// Ask whether to pin the just-selected choice as the project default. +/// Used by interactive pickers when the user did not already pass `--default`. +async fn confirm_save_as_default() -> Result { + println!( + " {DIM}Save as project default in {RESET}{CYAN}{}{RESET}{DIM}? [y/N]{RESET}", + crate::config::AGENT_CONFIG_PATH + ); + let answer = plain_read_line(format!(" {DIM}?{RESET} ")).await?; + let a = answer.trim().to_lowercase(); + Ok(a == "y" || a == "yes") +} + +/// After a session switch, persist if requested via flag or interactive confirm. +async fn maybe_persist_model_default(state: &AppState, as_default: bool) -> Result<()> { + if as_default || confirm_save_as_default().await? { + persist_model_as_default(state); + } + Ok(()) +} + +async fn maybe_persist_backend_default(state: &AppState, as_default: bool) -> Result<()> { + if as_default || confirm_save_as_default().await? { + persist_backend_as_default(state); + } + Ok(()) +} + pub(super) async fn cmd_backend(args: &str, state: &mut AppState) -> Result<()> { - let chosen: Option = if !args.is_empty() { - BackendName::parse(args) + let (rest, as_default) = crate::config::strip_default_flag(args); + + // Pin the current backend as project default without switching. + if rest.is_empty() && as_default { + println!( + " {GREEN}✓{RESET} {DIM}default backend →{RESET} {CYAN}{}{RESET} {DIM}(modelOverride cleared on disk){RESET}", + state.config.backend.as_str() + ); + persist_backend_as_default(state); + return Ok(()); + } + + let mut picked_with_default = as_default; + let chosen: Option = if !rest.is_empty() { + BackendName::parse(&rest) } else { println!( " {DIM}Current:{RESET} {CYAN}{}{RESET}", state.config.backend.as_str() ); + let current = state.config.backend; + let (persisted_backend, _) = crate::config::persisted_defaults(); for (i, b) in BackendName::all().iter().enumerate() { - println!(" {DIM}{}){RESET} {}", i + 1, b.as_str()); + let marker = picker_marker(*b == current, persisted_backend == Some(*b)); + println!(" {DIM}{}){RESET} {}{}", i + 1, b.as_str(), marker); } - let prompt = format!(" {DIM}Select (1-{}):{RESET} ", BackendName::all().len()); + let prompt = format!( + " {DIM}Select (1-{}) · append --default to pin:{RESET} ", + BackendName::all().len() + ); let pick = plain_read_line(prompt).await?.trim().to_string(); - pick.parse::() + let (sel, flag_default) = crate::config::parse_picker_selection(&pick); + picked_with_default = flag_default; + sel.parse::() .ok() .and_then(|n| n.checked_sub(1)) .and_then(|i| BackendName::all().get(i).copied()) @@ -554,23 +643,46 @@ pub(super) async fn cmd_backend(args: &str, state: &mut AppState) -> Result<()> chosen.as_str(), state.model ); + // Direct `/backend name --default` skips the confirm; interactive path + // either carried `--default` on the selection or gets the y/N prompt. + if rest.is_empty() { + maybe_persist_backend_default(state, picked_with_default).await?; + } else if as_default { + persist_backend_as_default(state); + } Ok(()) } pub(super) async fn cmd_model(args: &str, state: &mut AppState) -> Result<()> { use std::io::Write; - if !args.is_empty() { + let (rest, as_default) = crate::config::strip_default_flag(args); + + // Pin the currently active model (+ backend) as project default. + if rest.is_empty() && as_default { + // Materialize the resolved id into model_override so memory matches disk. + if state.config.model_override.as_deref() != Some(state.model.as_str()) { + state.config.model_override = Some(state.model.clone()); + } + println!( + " {GREEN}✓{RESET} {DIM}model →{RESET} {CYAN}{}{RESET}", + state.model + ); + persist_model_as_default(state); + return Ok(()); + } + + if !rest.is_empty() { let model_override = if matches!(state.config.backend, BackendName::OpenAiCodex) { - let Some(canonical) = crate::codex_responses::canonical_codex_model(args) else { + let Some(canonical) = crate::codex_responses::canonical_codex_model(&rest) else { println!( - " {RED}✗{RESET} {DIM}{args} is not supported with ChatGPT/Codex login. Try one of: {}{RESET}", + " {RED}✗{RESET} {DIM}{rest} is not supported with ChatGPT/Codex login. Try one of: {}{RESET}", crate::codex_responses::codex_model_list().join(", ") ); return Ok(()); }; canonical.to_string() } else { - args.to_string() + rest }; state.config.model_override = Some(model_override); state.active_effort = None; @@ -579,6 +691,9 @@ pub(super) async fn cmd_model(args: &str, state: &mut AppState) -> Result<()> { " {GREEN}✓{RESET} {DIM}model →{RESET} {CYAN}{}{RESET}", state.model ); + if as_default { + persist_model_as_default(state); + } return Ok(()); } let mut out = std::io::stdout(); @@ -617,25 +732,36 @@ pub(super) async fn cmd_model(args: &str, state: &mut AppState) -> Result<()> { println!(" {DIM}No matches.{RESET}"); return Ok(()); } + let active_model = state.model.as_str(); + let (_, persisted_model) = crate::config::persisted_defaults(); let name_width = shown.iter().map(|m| m.len()).max().unwrap_or(0); for (i, m) in shown.iter().enumerate() { + let marker = picker_marker( + m == active_model, + persisted_model.as_deref() == Some(m.as_str()), + ); match catalog::lookup(state.config.backend, m) { Some(info) => println!( - " {DIM}{:>2}){RESET} {:2}){RESET} {: println!(" {DIM}{:>2}){RESET} {}", i + 1, m), + None => println!(" {DIM}{:>2}){RESET} {}{}", i + 1, m, marker), } } if total > shown.len() { println!(" {DIM}…and {} more{RESET}", total - shown.len()); } - let prompt = format!(" {DIM}Select (1-{}):{RESET} ", shown.len()); + let prompt = format!( + " {DIM}Select (1-{}) · append --default to pin:{RESET} ", + shown.len() + ); let pick = plain_read_line(prompt).await?.trim().to_string(); - if let Some(idx) = pick.parse::().ok().and_then(|n| n.checked_sub(1)) { + let (sel, flag_default) = crate::config::parse_picker_selection(&pick); + if let Some(idx) = sel.parse::().ok().and_then(|n| n.checked_sub(1)) { if let Some(m) = shown.get(idx) { state.config.model_override = Some(m.clone()); state.active_effort = None; @@ -644,6 +770,7 @@ pub(super) async fn cmd_model(args: &str, state: &mut AppState) -> Result<()> { " {GREEN}✓{RESET} {DIM}model →{RESET} {CYAN}{}{RESET}", state.model ); + maybe_persist_model_default(state, flag_default).await?; return Ok(()); } } diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 2607a2f..6cd9251 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -189,11 +189,11 @@ pub const COMMANDS: &[(&str, &str)] = &[ ), ( "/backend", - "Switch backend (ollama, lm-studio, mlx, llamacpp, openrouter, openai, openai-codex)", + "Switch backend (ollama, lm-studio, mlx, llamacpp, openrouter, openai, openai-codex); --default persists", ), ( "/model", - "List models from the current backend and pick one", + "List/pick a model; --default pins backend+model in agent.config.json", ), ( "/tools", diff --git a/src/config.rs b/src/config.rs index 6124492..13f3408 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,11 +1,16 @@ +use anyhow::{anyhow, bail, Result}; use serde::{Deserialize, Serialize}; -use serde_json::Value; +use serde_json::{json, Value}; use std::collections::BTreeMap; use std::path::{Path, PathBuf}; use crate::backends::{BackendDescriptor, BackendName, OpenRouterConfig}; use crate::model_system::ModelSystemConfig; +/// Project-local config file read by [`load_config`] and updated by +/// `/backend --default` / `/model --default`. +pub const AGENT_CONFIG_PATH: &str = "agent.config.json"; + pub const ALL_TOOL_NAMES: &[&str] = &[ "apply_patch", "batch_edit", @@ -1097,11 +1102,110 @@ where Some(crate::hooks::HookGroupConfig { matcher, hooks }) } +/// Strip a lone `--default` token from slash-command args. +/// +/// Returns the remaining argument string (tokens rejoined with spaces) and +/// whether the flag was present. Order is free: `--default` may appear first, +/// last, or between other tokens. +pub fn strip_default_flag(args: &str) -> (String, bool) { + let mut as_default = false; + let mut kept = Vec::new(); + for token in args.split_whitespace() { + if token == "--default" { + as_default = true; + } else { + kept.push(token); + } + } + (kept.join(" "), as_default) +} + +/// Parse an interactive picker reply like `3` or `3 --default`. +/// +/// Only the literal token `--default` marks persistence intent (not `d` / `!`). +/// Returns `(1-based selection text without the flag, as_default)`. +pub fn parse_picker_selection(input: &str) -> (String, bool) { + strip_default_flag(input) +} + +/// The backend/model currently persisted as project defaults in +/// `agent.config.json`, used to mark `(default)` in interactive pickers. +/// +/// Returns `(backend, model_override)`; either is `None` when the file is +/// missing, unreadable, not a JSON object, or omits that key. This reflects +/// what would load on next launch, independent of the live session. +pub fn persisted_defaults() -> (Option, Option) { + persisted_defaults_from(Path::new(AGENT_CONFIG_PATH)) +} + +fn persisted_defaults_from(path: &Path) -> (Option, Option) { + let Ok(text) = std::fs::read_to_string(path) else { + return (None, None); + }; + let Ok(file) = serde_json::from_str::(&text) else { + return (None, None); + }; + let backend = file.backend.as_deref().and_then(BackendName::parse); + (backend, file.model_override) +} + +/// Surgically merge `backend` / `modelOverride` into `agent.config.json`. +/// +/// - Creates the file when missing. +/// - Preserves every other key when the existing root is a JSON object. +/// - When `model_override` is `None`, removes `modelOverride` from the file. +/// - Refuses to overwrite an existing file whose contents are not a JSON object +/// (invalid JSON or non-object root). +pub fn persist_backend_model_defaults( + path: &Path, + backend: BackendName, + model_override: Option<&str>, +) -> Result<()> { + let mut root = if path.exists() { + let text = std::fs::read_to_string(path) + .map_err(|e| anyhow!("could not read {}: {e}", path.display()))?; + let parsed: Value = serde_json::from_str(&text) + .map_err(|e| anyhow!("could not merge {}: invalid JSON ({e})", path.display()))?; + match parsed { + Value::Object(map) => Value::Object(map), + _ => bail!( + "could not merge {}: root must be a JSON object", + path.display() + ), + } + } else { + Value::Object(serde_json::Map::new()) + }; + + { + let Some(obj) = root.as_object_mut() else { + bail!( + "could not merge {}: root must be a JSON object", + path.display() + ); + }; + obj.insert("backend".into(), json!(backend.as_str())); + match model_override { + Some(model) => { + obj.insert("modelOverride".into(), json!(model)); + } + None => { + obj.remove("modelOverride"); + } + } + } + + let body = serde_json::to_string_pretty(&root)?; + std::fs::write(path, format!("{body}\n")) + .map_err(|e| anyhow!("could not write {}: {e}", path.display()))?; + Ok(()) +} + pub fn load_config() -> AgentConfig { let mut config = AgentConfig::default(); let dotenv = dotenv_values(); - let path = Path::new("agent.config.json"); + let path = Path::new(AGENT_CONFIG_PATH); if path.exists() { match std::fs::read_to_string(path) { Ok(text) => match serde_json::from_str::(&text) { @@ -1334,6 +1438,142 @@ mod tests { std::env::remove_var("SMALL_HARNESS_TEST_LAYER"); } + #[test] + fn strip_default_flag_positions_and_absence() { + assert_eq!( + strip_default_flag("grok-4.5 --default"), + ("grok-4.5".into(), true) + ); + assert_eq!( + strip_default_flag("--default grok-4.5"), + ("grok-4.5".into(), true) + ); + assert_eq!(strip_default_flag("--default"), ("".into(), true)); + assert_eq!(strip_default_flag("ollama"), ("ollama".into(), false)); + assert_eq!( + strip_default_flag("provider/model-name --default"), + ("provider/model-name".into(), true) + ); + // Multi-token ids rejoin with a single space. + assert_eq!( + strip_default_flag("--default foo bar"), + ("foo bar".into(), true) + ); + } + + #[test] + fn parse_picker_selection_splits_index_and_flag() { + assert_eq!(parse_picker_selection("3"), ("3".into(), false)); + assert_eq!(parse_picker_selection("3 --default"), ("3".into(), true)); + assert_eq!(parse_picker_selection("--default 3"), ("3".into(), true)); + // Bare index only; sugar like `d`/`!` is intentionally not persistence. + assert_eq!(parse_picker_selection("3d"), ("3d".into(), false)); + } + + #[test] + fn persisted_defaults_reads_backend_and_model() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("agent.config.json"); + + // Missing file → no defaults. + assert_eq!(persisted_defaults_from(&path), (None, None)); + + std::fs::write( + &path, + r#"{ "backend": "openrouter", "modelOverride": "x-ai/grok-4.5" }"#, + ) + .unwrap(); + assert_eq!( + persisted_defaults_from(&path), + (Some(BackendName::Openrouter), Some("x-ai/grok-4.5".into())) + ); + + // Backend without a pinned model → only the backend is a default. + std::fs::write(&path, r#"{ "backend": "ollama" }"#).unwrap(); + assert_eq!( + persisted_defaults_from(&path), + (Some(BackendName::Ollama), None) + ); + + // Non-object / invalid JSON is treated as no defaults, never trusted. + std::fs::write(&path, "not-json {").unwrap(); + assert_eq!(persisted_defaults_from(&path), (None, None)); + } + + #[test] + fn persist_backend_model_defaults_creates_and_merges() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("agent.config.json"); + + persist_backend_model_defaults(&path, BackendName::OpenAi, Some("gpt-4o-mini")).unwrap(); + let created: Value = + serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap(); + assert_eq!(created["backend"], "openai"); + assert_eq!(created["modelOverride"], "gpt-4o-mini"); + + std::fs::write( + &path, + r#"{ + "backend": "ollama", + "modelOverride": "qwen2.5:7b", + "approvalPolicy": "dangerous-only", + "display": { "showBanner": false } +} +"#, + ) + .unwrap(); + + persist_backend_model_defaults(&path, BackendName::Openrouter, Some("x-ai/grok-3")) + .unwrap(); + let merged: Value = serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap(); + assert_eq!(merged["backend"], "openrouter"); + assert_eq!(merged["modelOverride"], "x-ai/grok-3"); + assert_eq!(merged["approvalPolicy"], "dangerous-only"); + assert_eq!(merged["display"]["showBanner"], false); + } + + #[test] + fn persist_backend_model_defaults_clears_model_override() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("agent.config.json"); + std::fs::write( + &path, + r#"{"backend":"openai","modelOverride":"gpt-4o","tools":["shell"]}"#, + ) + .unwrap(); + + persist_backend_model_defaults(&path, BackendName::Ollama, None).unwrap(); + let merged: Value = serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap(); + assert_eq!(merged["backend"], "ollama"); + assert!(merged.get("modelOverride").is_none()); + assert_eq!(merged["tools"], json!(["shell"])); + } + + #[test] + fn persist_backend_model_defaults_refuses_invalid_json() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("agent.config.json"); + std::fs::write(&path, "not-json {").unwrap(); + let err = persist_backend_model_defaults(&path, BackendName::Ollama, None).unwrap_err(); + let msg = err.to_string(); + assert!(msg.contains("invalid JSON"), "unexpected error: {msg}"); + assert_eq!(std::fs::read_to_string(&path).unwrap(), "not-json {"); + } + + #[test] + fn persist_backend_model_defaults_refuses_non_object_root() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("agent.config.json"); + std::fs::write(&path, r#"[1, 2, 3]"#).unwrap(); + let err = persist_backend_model_defaults(&path, BackendName::Ollama, None).unwrap_err(); + let msg = err.to_string(); + assert!( + msg.contains("root must be a JSON object"), + "unexpected error: {msg}" + ); + assert_eq!(std::fs::read_to_string(&path).unwrap(), r#"[1, 2, 3]"#); + } + #[test] fn parses_openrouter_fusion_config() { let file: FileConfig = serde_json::from_str( diff --git a/src/setup.rs b/src/setup.rs index d5fc8ec..ebe486c 100644 --- a/src/setup.rs +++ b/src/setup.rs @@ -4,7 +4,9 @@ use std::path::Path; use std::time::Duration; use crate::backends::{backend, default_model, validate, BackendName}; -use crate::config::{dotenv_values, layered_env, AgentConfig, ApprovalPolicy, ToolSelection}; +use crate::config::{ + dotenv_values, layered_env, AgentConfig, ApprovalPolicy, ToolSelection, AGENT_CONFIG_PATH, +}; use crate::hardware::{detect_hardware_spec, save_hardware_summary}; use crate::input::plain_read_line; use crate::openai::{build_http_client, chat_oneshot, list_models, ChatMessage, ChatRequest}; @@ -19,7 +21,6 @@ const GREEN: crate::theme::Style = crate::theme::SUCCESS; const YELLOW: crate::theme::Style = crate::theme::WARN; const RED: crate::theme::Style = crate::theme::ERROR; -const CONFIG_PATH: &str = "agent.config.json"; const NO_WIZARD_ENV: &str = "SMALL_HARNESS_NO_WIZARD"; pub fn setup_disabled() -> bool { @@ -34,7 +35,7 @@ pub fn should_run_first_run_setup(config_path: &Path) -> bool { } pub async fn maybe_run_first_run_setup(base: &AgentConfig) -> Result> { - if should_run_first_run_setup(Path::new(CONFIG_PATH)) { + if should_run_first_run_setup(Path::new(AGENT_CONFIG_PATH)) { let mut first_run_defaults = base.clone(); let spec = detect_hardware_spec(); let _ = save_hardware_summary(&first_run_defaults.session_dir, &spec); @@ -51,7 +52,7 @@ pub async fn run_setup_wizard(base: &AgentConfig) -> Result> println!("{pad}{CYAN}{BOLD}Small Harness setup{RESET}"); println!("{}", crate::theme::rule()); println!( - "{pad}{DIM}A few quick questions — I'll write {CONFIG_PATH}. Press Enter to keep the\n{pad}shown default ({CYAN}*{RESET}{DIM}); type q to cancel.{RESET}" + "{pad}{DIM}A few quick questions — I'll write {AGENT_CONFIG_PATH}. Press Enter to keep the\n{pad}shown default ({CYAN}*{RESET}{DIM}); type q to cancel.{RESET}" ); println!(); @@ -85,8 +86,8 @@ pub async fn run_setup_wizard(base: &AgentConfig) -> Result> config.approval_policy = approval_policy; config.tool_selection = tool_selection; - write_agent_config(Path::new(CONFIG_PATH), &config)?; - println!(" {GREEN}✓{RESET} {DIM}wrote {CONFIG_PATH}{RESET}"); + write_agent_config(Path::new(AGENT_CONFIG_PATH), &config)?; + println!(" {GREEN}✓{RESET} {DIM}wrote {AGENT_CONFIG_PATH}{RESET}"); probe_setup_backend(&config).await; Ok(Some(config))