diff --git a/desktop/scripts/check-file-sizes.mjs b/desktop/scripts/check-file-sizes.mjs
index 2651318bc3..96f97126e0 100644
--- a/desktop/scripts/check-file-sizes.mjs
+++ b/desktop/scripts/check-file-sizes.mjs
@@ -498,7 +498,9 @@ const overrides = new Map([
// route PowerShell CLI installs natively on Windows (bypasses Git Bash PATH
// poisoning that resolved GNU tar instead of bsdtar → Codex install failure).
// Includes unit tests for detection, routing, and -Command body preservation.
- ["src-tauri/src/commands/agent_discovery.rs", 1810],
+ // +16: test_powershell_command_goose_catalog_dequoted proves the \$→$ escape
+ // fix for the Goose Windows installer (PR #2680 interaction with #2750).
+ ["src-tauri/src/commands/agent_discovery.rs", 1826],
// draft-persistence predicate: submit-time `loadDraft` check + inline comment
// + deps-array entry in submitMessage closes the never-persisted-boundary
// defect (Thufir Pass-3 finding). Load-bearing correctness fix; queued to
diff --git a/desktop/src-tauri/src/commands/agent_config.rs b/desktop/src-tauri/src/commands/agent_config.rs
index 05fb179125..f6094ed529 100644
--- a/desktop/src-tauri/src/commands/agent_config.rs
+++ b/desktop/src-tauri/src/commands/agent_config.rs
@@ -596,8 +596,6 @@ fn parse_models(raw: Option<&serde_json::Value>) -> (Vec, Option<
#[cfg(test)]
mod tests {
- use std::collections::BTreeMap;
-
use super::*;
use crate::managed_agents::{BackendKind, RespondTo};
@@ -614,7 +612,8 @@ mod tests {
cli_install_commands: &[],
cli_install_commands_windows: &[],
adapter_install_commands: &[],
- install_instructions_url: "",
+ cli_install_instructions_url: "",
+ adapter_install_instructions_url: "",
cli_install_hint: "",
adapter_install_hint: "",
skill_dir: None,
@@ -654,7 +653,7 @@ mod tests {
parallelism: 1,
system_prompt: None,
model: None,
- env_vars: BTreeMap::new(),
+ env_vars: Default::default(),
start_on_app_launch: false,
auto_restart_on_config_change: true,
runtime_pid: None,
@@ -705,7 +704,7 @@ mod tests {
is_active: true,
source_team: None,
source_team_persona_slug: None,
- env_vars: BTreeMap::new(),
+ env_vars: Default::default(),
respond_to: None,
respond_to_allowlist: Vec::new(),
parallelism: None,
diff --git a/desktop/src-tauri/src/commands/agent_discovery.rs b/desktop/src-tauri/src/commands/agent_discovery.rs
index f080272914..7b1979d654 100644
--- a/desktop/src-tauri/src/commands/agent_discovery.rs
+++ b/desktop/src-tauri/src/commands/agent_discovery.rs
@@ -12,6 +12,8 @@ use crate::{
relay::query_relay,
};
+mod post_install_verification;
+
fn active_installs() -> &'static std::sync::Mutex> {
use std::collections::HashSet;
use std::sync::{Mutex, OnceLock};
@@ -229,11 +231,10 @@ fn install_acp_runtime_blocking(runtime_id: &str) -> Result>(),
+ vec![
+ "-NoProfile",
+ "-ExecutionPolicy",
+ "Bypass",
+ "-Command",
+ "$env:CONFIGURE='false'; irm https://raw.githubusercontent.com/aaif-goose/goose/main/download_cli.ps1 | iex",
+ ],
+ "Goose catalog command must dequote with bare $env: (no backslash before $)"
+ );
+ }
+
// ── install retry ─────────────────────────────────────────────────────────
/// Build an `InstallStepResult` with just the fields the retry loop reads.
diff --git a/desktop/src-tauri/src/commands/agent_discovery/post_install_verification.rs b/desktop/src-tauri/src/commands/agent_discovery/post_install_verification.rs
new file mode 100644
index 0000000000..3155104b56
--- /dev/null
+++ b/desktop/src-tauri/src/commands/agent_discovery/post_install_verification.rs
@@ -0,0 +1,74 @@
+use crate::managed_agents::{AcpAvailabilityStatus, InstallStepResult};
+
+pub(super) fn run(runtime_id: &str, steps: &mut Vec) {
+ // Observe PATH changes and binaries added after Buzz launched.
+ crate::managed_agents::refresh_login_shell_path();
+ crate::managed_agents::clear_resolve_cache();
+
+ let availability = crate::managed_agents::discover_acp_runtime_availability(runtime_id);
+ if let Some(failure) = failure(runtime_id, availability) {
+ steps.push(failure);
+ }
+}
+
+fn failure(
+ runtime_id: &str,
+ availability: Option,
+) -> Option {
+ if availability == Some(AcpAvailabilityStatus::Available) {
+ return None;
+ }
+
+ let observed = availability
+ .map(availability_label)
+ .unwrap_or_else(|| "missing from the runtime catalog".to_string());
+ Some(InstallStepResult {
+ step: "verify".to_string(),
+ command: format!("discover {runtime_id}"),
+ success: false,
+ stdout: String::new(),
+ stderr: format!(
+ "The installer finished, but Buzz still could not use {runtime_id} (observed: {observed})."
+ ),
+ exit_code: None,
+ hint: Some(
+ "Buzz requires the vendor CLI executable, not only its desktop app. If the CLI was installed while Buzz was open, restart Buzz and check again."
+ .to_string(),
+ ),
+ })
+}
+
+fn availability_label(status: AcpAvailabilityStatus) -> String {
+ match status {
+ AcpAvailabilityStatus::Available => "available",
+ AcpAvailabilityStatus::AdapterMissing => "ACP adapter missing",
+ AcpAvailabilityStatus::AdapterOutdated => "ACP adapter outdated",
+ AcpAvailabilityStatus::CliMissing => "CLI missing",
+ AcpAvailabilityStatus::NotInstalled => "not installed",
+ }
+ .to_string()
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn accepts_available_runtime() {
+ assert!(failure("goose", Some(AcpAvailabilityStatus::Available)).is_none());
+ }
+
+ #[test]
+ fn rejects_unresolved_runtime() {
+ let failure = failure("goose", Some(AcpAvailabilityStatus::NotInstalled))
+ .expect("not-installed runtime must fail verification");
+
+ assert_eq!(failure.step, "verify");
+ assert!(!failure.success);
+ assert!(failure.stderr.contains("observed: not installed"));
+ assert!(failure
+ .hint
+ .as_deref()
+ .is_some_and(|hint| hint.contains("desktop app")));
+ }
+}
diff --git a/desktop/src-tauri/src/managed_agents/config_bridge/reader_tests.rs b/desktop/src-tauri/src/managed_agents/config_bridge/reader_tests.rs
index f5fc7c3899..4c11cd6c49 100644
--- a/desktop/src-tauri/src/managed_agents/config_bridge/reader_tests.rs
+++ b/desktop/src-tauri/src/managed_agents/config_bridge/reader_tests.rs
@@ -40,7 +40,8 @@ fn test_runtime() -> &'static KnownAcpRuntime {
cli_install_commands: &[],
cli_install_commands_windows: &[],
adapter_install_commands: &[],
- install_instructions_url: "",
+ cli_install_instructions_url: "",
+ adapter_install_instructions_url: "",
cli_install_hint: "",
adapter_install_hint: "",
skill_dir: None,
@@ -615,7 +616,8 @@ fn buzz_agent_runtime() -> &'static KnownAcpRuntime {
cli_install_commands: &[],
cli_install_commands_windows: &[],
adapter_install_commands: &[],
- install_instructions_url: "",
+ cli_install_instructions_url: "",
+ adapter_install_instructions_url: "",
cli_install_hint: "",
adapter_install_hint: "",
skill_dir: None,
diff --git a/desktop/src-tauri/src/managed_agents/discovery.rs b/desktop/src-tauri/src/managed_agents/discovery.rs
index f40eed5a13..ab76965259 100644
--- a/desktop/src-tauri/src/managed_agents/discovery.rs
+++ b/desktop/src-tauri/src/managed_agents/discovery.rs
@@ -73,10 +73,13 @@ const KNOWN_ACP_RUNTIMES: &[KnownAcpRuntime] = &[
mcp_hooks: false,
underlying_cli: Some("goose"),
cli_install_commands: &["curl -fsSL https://github.com/aaif-goose/goose/releases/download/stable/download_cli.sh | CONFIGURE=false bash"],
- cli_install_commands_windows: &[], // goose install script is already Windows-aware
+ // Goose's stable release currently publishes only the Unix installer;
+ // its official Windows instructions intentionally point at this main-branch script.
+ cli_install_commands_windows: &["powershell.exe -NoProfile -ExecutionPolicy Bypass -Command \"$env:CONFIGURE='false'; irm https://raw.githubusercontent.com/aaif-goose/goose/main/download_cli.ps1 | iex\""],
adapter_install_commands: &[],
- install_instructions_url: "https://block.github.io/goose/",
- cli_install_hint: "Install Goose via the official install script.",
+ cli_install_instructions_url: "https://goose-docs.ai/docs/getting-started/installation/",
+ adapter_install_instructions_url: "",
+ cli_install_hint: "Buzz requires the Goose CLI; the desktop app alone is not enough.",
adapter_install_hint: "",
skill_dir: Some(".goose/skills"),
supports_acp_model_switching: false,
@@ -106,8 +109,9 @@ const KNOWN_ACP_RUNTIMES: &[KnownAcpRuntime] = &[
cli_install_commands: &["curl -fsSL https://claude.ai/install.sh | bash"],
cli_install_commands_windows: &["powershell.exe -NoProfile -ExecutionPolicy Bypass -Command \"irm https://claude.ai/install.ps1 | iex\""],
adapter_install_commands: &["npm install -g @agentclientprotocol/claude-agent-acp"],
- install_instructions_url: "https://github.com/agentclientprotocol/claude-agent-acp",
- cli_install_hint: "Install the Claude Code CLI via the official install script.",
+ cli_install_instructions_url: "https://code.claude.com/docs/en/getting-started",
+ adapter_install_instructions_url: "https://github.com/agentclientprotocol/claude-agent-acp",
+ cli_install_hint: "Buzz requires the Claude Code CLI; the desktop app alone is not enough.",
adapter_install_hint: "Install the Claude Code ACP adapter via npm.",
skill_dir: Some(".claude/skills"),
supports_acp_model_switching: false,
@@ -137,8 +141,9 @@ const KNOWN_ACP_RUNTIMES: &[KnownAcpRuntime] = &[
cli_install_commands: &["curl -fsSL https://chatgpt.com/codex/install.sh | sh"],
cli_install_commands_windows: &["powershell.exe -NoProfile -ExecutionPolicy Bypass -Command \"irm https://chatgpt.com/codex/install.ps1 | iex\""],
adapter_install_commands: &["npm install -g @agentclientprotocol/codex-acp"],
- install_instructions_url: "https://github.com/agentclientprotocol/codex-acp",
- cli_install_hint: "Install the Codex CLI via the official install script.",
+ cli_install_instructions_url: "https://developers.openai.com/codex/cli/",
+ adapter_install_instructions_url: "https://github.com/agentclientprotocol/codex-acp",
+ cli_install_hint: "Buzz requires the Codex CLI; the desktop app alone is not enough.",
adapter_install_hint: "Install the Codex ACP adapter via npm.",
skill_dir: Some(".codex/skills"),
supports_acp_model_switching: false,
@@ -169,7 +174,8 @@ const KNOWN_ACP_RUNTIMES: &[KnownAcpRuntime] = &[
cli_install_commands: &[],
cli_install_commands_windows: &[],
adapter_install_commands: &[],
- install_instructions_url: "https://github.com/block/buzz",
+ cli_install_instructions_url: "https://github.com/block/buzz",
+ adapter_install_instructions_url: "https://github.com/block/buzz",
cli_install_hint: "Ships with the Buzz desktop app.",
adapter_install_hint: "",
skill_dir: None,
@@ -1158,109 +1164,130 @@ struct PartialEntry {
entry: AcpRuntimeCatalogEntry,
}
-pub fn discover_acp_runtimes() -> Vec {
- // Phase 1: build all entries (fast — no probes yet).
- let mut partials: Vec = KNOWN_ACP_RUNTIMES
+fn discover_acp_runtime_phase1(runtime: &'static KnownAcpRuntime) -> PartialEntry {
+ let adapter_result = runtime
+ .commands
.iter()
- .map(|runtime| {
- let adapter_result = runtime
- .commands
- .iter()
- .find_map(|command| find_command(command).map(|path| (*command, path)));
-
- let underlying_cli_found = runtime
- .underlying_cli
- .map(|cli| find_command(cli).is_some())
- .unwrap_or(false);
- let (mut availability, command, binary_path) =
- classify_runtime(adapter_result, runtime.underlying_cli, underlying_cli_found);
-
- // For codex-acp: when the adapter resolves as Available, probe the
- // version. An adapter with major version < 1 is treated as outdated —
- // the CODEX_CONFIG spawn contract requires 1.x.
- if runtime.id == "codex"
- && availability == AcpAvailabilityStatus::Available
- && command.as_deref() == Some("codex-acp")
- {
- if let Some(path_str) = &binary_path {
- availability = codex_adapter_availability(&PathBuf::from(path_str));
- }
- }
+ .find_map(|command| find_command(command).map(|path| (*command, path)));
+
+ let underlying_cli_found = runtime
+ .underlying_cli
+ .map(|cli| find_command(cli).is_some())
+ .unwrap_or(false);
+ let (mut availability, command, binary_path) =
+ classify_runtime(adapter_result, runtime.underlying_cli, underlying_cli_found);
+
+ // For codex-acp: when the adapter resolves as Available, probe the
+ // version. An adapter with major version < 1 is treated as outdated —
+ // the CODEX_CONFIG spawn contract requires 1.x.
+ if runtime.id == "codex"
+ && availability == AcpAvailabilityStatus::Available
+ && command.as_deref() == Some("codex-acp")
+ {
+ if let Some(path_str) = &binary_path {
+ availability = codex_adapter_availability(&PathBuf::from(path_str));
+ }
+ }
- // Warm the adapter-availability cache for the badge fallback.
- // The cache is scoped to the codex runtime; other runtimes leave it
- // unchanged. Invalidated by `clear_resolve_cache`.
- if runtime.id == "codex" {
- cache_adapter_availability(availability.clone());
- }
+ // Warm the adapter-availability cache for the badge fallback.
+ // The cache is scoped to the codex runtime; other runtimes leave it
+ // unchanged. Invalidated by `clear_resolve_cache`.
+ if runtime.id == "codex" {
+ cache_adapter_availability(availability.clone());
+ }
- let underlying_cli_path = runtime
- .underlying_cli
- .and_then(find_command)
- .map(|p| p.display().to_string());
-
- let default_args = command
- .as_deref()
- .map(|cmd| normalize_agent_args(cmd, Vec::new()))
- .unwrap_or_default();
-
- let can_auto_install = !runtime.cli_install_commands_for_os().is_empty()
- || !runtime.adapter_install_commands.is_empty();
-
- let cli_hint = runtime.cli_install_hint;
- let adapter_hint = runtime.adapter_install_hint;
- let install_hint = match availability {
- AcpAvailabilityStatus::Available => cli_hint.to_string(),
- AcpAvailabilityStatus::CliMissing => cli_hint.to_string(),
- AcpAvailabilityStatus::AdapterMissing => adapter_hint.to_string(),
- AcpAvailabilityStatus::AdapterOutdated => adapter_hint.to_string(),
- AcpAvailabilityStatus::NotInstalled => {
- if !cli_hint.is_empty() && !adapter_hint.is_empty() {
- format!("{cli_hint} {adapter_hint}")
- } else if !cli_hint.is_empty() {
- cli_hint.to_string()
- } else {
- adapter_hint.to_string()
- }
- }
- };
+ let underlying_cli_path = runtime
+ .underlying_cli
+ .and_then(find_command)
+ .map(|p| p.display().to_string());
- // node_required now means Buzz cannot provide npm for this platform.
- // On supported desktop platforms, Buzz downloads a private Node/npm
- // runtime into app data before running npm-backed adapter installs.
- let node_required = matches!(
- availability,
- AcpAvailabilityStatus::AdapterMissing | AcpAvailabilityStatus::NotInstalled
- ) && runtime_needs_npm(runtime)
- && buzz_managed_node_bin_dir().is_none()
- && resolve_command("npm").is_none()
- && resolve_command("node").is_none();
-
- PartialEntry {
- runtime,
- entry: AcpRuntimeCatalogEntry {
- id: runtime.id.to_string(),
- label: runtime.label.to_string(),
- avatar_url: runtime.avatar_url.to_string(),
- availability,
- command,
- binary_path,
- default_args,
- mcp_command: runtime.mcp_command.map(str::to_string),
- model_env_var: runtime.model_env_var.map(str::to_string),
- provider_env_var: runtime.provider_env_var.map(str::to_string),
- thinking_env_var: runtime.thinking_env_var.map(str::to_string),
- install_hint,
- install_instructions_url: runtime.install_instructions_url.to_string(),
- can_auto_install,
- underlying_cli_path,
- node_required,
- // Filled in by the probe phase below.
- auth_status: AuthStatus::Unknown,
- login_hint: None,
- },
+ let default_args = command
+ .as_deref()
+ .map(|cmd| normalize_agent_args(cmd, Vec::new()))
+ .unwrap_or_default();
+
+ let can_auto_install = !runtime.cli_install_commands_for_os().is_empty()
+ || !runtime.adapter_install_commands.is_empty();
+
+ let cli_hint = runtime.cli_install_hint;
+ let adapter_hint = runtime.adapter_install_hint;
+ let install_hint = match availability {
+ AcpAvailabilityStatus::Available => cli_hint.to_string(),
+ AcpAvailabilityStatus::CliMissing => cli_hint.to_string(),
+ AcpAvailabilityStatus::AdapterMissing => adapter_hint.to_string(),
+ AcpAvailabilityStatus::AdapterOutdated => adapter_hint.to_string(),
+ AcpAvailabilityStatus::NotInstalled => {
+ if !cli_hint.is_empty() && !adapter_hint.is_empty() {
+ format!("{cli_hint} {adapter_hint}")
+ } else if !cli_hint.is_empty() {
+ cli_hint.to_string()
+ } else {
+ adapter_hint.to_string()
}
- })
+ }
+ };
+ let install_instructions_url = match availability {
+ AcpAvailabilityStatus::AdapterMissing | AcpAvailabilityStatus::AdapterOutdated => {
+ runtime.adapter_install_instructions_url
+ }
+ AcpAvailabilityStatus::Available
+ | AcpAvailabilityStatus::CliMissing
+ | AcpAvailabilityStatus::NotInstalled => runtime.cli_install_instructions_url,
+ };
+
+ // node_required now means Buzz cannot provide npm for this platform.
+ // On supported desktop platforms, Buzz downloads a private Node/npm
+ // runtime into app data before running npm-backed adapter installs.
+ let node_required = matches!(
+ availability,
+ AcpAvailabilityStatus::AdapterMissing | AcpAvailabilityStatus::NotInstalled
+ ) && runtime_needs_npm(runtime)
+ && buzz_managed_node_bin_dir().is_none()
+ && resolve_command("npm").is_none()
+ && resolve_command("node").is_none();
+
+ PartialEntry {
+ runtime,
+ entry: AcpRuntimeCatalogEntry {
+ id: runtime.id.to_string(),
+ label: runtime.label.to_string(),
+ avatar_url: runtime.avatar_url.to_string(),
+ availability,
+ command,
+ binary_path,
+ default_args,
+ mcp_command: runtime.mcp_command.map(str::to_string),
+ model_env_var: runtime.model_env_var.map(str::to_string),
+ provider_env_var: runtime.provider_env_var.map(str::to_string),
+ thinking_env_var: runtime.thinking_env_var.map(str::to_string),
+ install_hint,
+ install_instructions_url: install_instructions_url.to_string(),
+ can_auto_install,
+ requires_external_cli: runtime.underlying_cli.is_some(),
+ underlying_cli_path,
+ node_required,
+ // Filled in by the auth-probe phase in full catalog discovery.
+ auth_status: AuthStatus::Unknown,
+ login_hint: None,
+ },
+ }
+}
+
+/// Discover one runtime's filesystem availability without running any auth probes.
+///
+/// Post-install verification only needs to know whether the requested runtime
+/// resolves, so it should not pay the cost of authenticating every catalog entry.
+pub(crate) fn discover_acp_runtime_availability(runtime_id: &str) -> Option {
+ known_acp_runtime_exact(runtime_id)
+ .map(discover_acp_runtime_phase1)
+ .map(|partial| partial.entry.availability)
+}
+
+pub fn discover_acp_runtimes() -> Vec {
+ // Phase 1: build all entries (fast — no probes yet).
+ let mut partials: Vec = KNOWN_ACP_RUNTIMES
+ .iter()
+ .map(discover_acp_runtime_phase1)
.collect();
// Phase 2: run auth probes in parallel for entries that need them.
diff --git a/desktop/src-tauri/src/managed_agents/discovery/runtime_metadata.rs b/desktop/src-tauri/src/managed_agents/discovery/runtime_metadata.rs
index 4dfcad4256..2fb6a471d4 100644
--- a/desktop/src-tauri/src/managed_agents/discovery/runtime_metadata.rs
+++ b/desktop/src-tauri/src/managed_agents/discovery/runtime_metadata.rs
@@ -20,8 +20,10 @@ pub(crate) struct KnownAcpRuntime {
pub cli_install_commands_windows: &'static [&'static str],
/// Shell commands to install the ACP adapter (run sequentially, after CLI).
pub adapter_install_commands: &'static [&'static str],
- /// Link to docs/repo for manual instructions.
- pub install_instructions_url: &'static str,
+ /// Official CLI installation documentation.
+ pub cli_install_instructions_url: &'static str,
+ /// ACP adapter installation documentation.
+ pub adapter_install_instructions_url: &'static str,
/// Human-readable hint about installing the CLI binary.
pub cli_install_hint: &'static str,
/// Human-readable hint about installing the ACP adapter.
@@ -78,3 +80,45 @@ impl KnownAcpRuntime {
self.cli_install_commands
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::super::known_acp_runtime_exact;
+
+ #[test]
+ fn vendor_metadata_distinguishes_cli_and_adapter_guidance() {
+ let goose = known_acp_runtime_exact("goose").unwrap();
+ assert_eq!(
+ goose.cli_install_instructions_url,
+ "https://goose-docs.ai/docs/getting-started/installation/"
+ );
+ assert!(goose.adapter_install_instructions_url.is_empty());
+ assert!(goose.cli_install_hint.contains("desktop app alone"));
+ assert!(goose
+ .cli_install_commands_windows
+ .iter()
+ .any(|command| command.contains("raw.githubusercontent.com/aaif-goose/goose/main")));
+ assert!(goose
+ .cli_install_commands_windows
+ .iter()
+ .any(|command| command.contains("$env:CONFIGURE='false'")));
+
+ let claude = known_acp_runtime_exact("claude").unwrap();
+ assert_eq!(
+ claude.cli_install_instructions_url,
+ "https://code.claude.com/docs/en/getting-started"
+ );
+ assert!(claude
+ .adapter_install_instructions_url
+ .contains("claude-agent-acp"));
+ assert!(claude.cli_install_hint.contains("desktop app alone"));
+
+ let codex = known_acp_runtime_exact("codex").unwrap();
+ assert_eq!(
+ codex.cli_install_instructions_url,
+ "https://developers.openai.com/codex/cli/"
+ );
+ assert!(codex.adapter_install_instructions_url.contains("codex-acp"));
+ assert!(codex.cli_install_hint.contains("desktop app alone"));
+ }
+}
diff --git a/desktop/src-tauri/src/managed_agents/discovery/tests.rs b/desktop/src-tauri/src/managed_agents/discovery/tests.rs
index 0ed4fe0f6a..364caa452b 100644
--- a/desktop/src-tauri/src/managed_agents/discovery/tests.rs
+++ b/desktop/src-tauri/src/managed_agents/discovery/tests.rs
@@ -1104,16 +1104,18 @@ fn test_cli_install_commands_for_os_non_empty_for_claude_codex() {
);
}
-/// On Windows, Claude and Codex select the PowerShell install commands.
+/// On Windows, every vendor CLI selects its official PowerShell installer.
#[cfg(windows)]
#[test]
fn test_cli_install_commands_for_os_selects_powershell_on_windows() {
let claude = super::known_acp_runtime_exact("claude").unwrap();
let codex = super::known_acp_runtime_exact("codex").unwrap();
+ let goose = super::known_acp_runtime_exact("goose").unwrap();
// Windows must select the PowerShell commands, not the curl|bash ones.
let claude_cmds = claude.cli_install_commands_for_os();
let codex_cmds = codex.cli_install_commands_for_os();
+ let goose_cmds = goose.cli_install_commands_for_os();
assert_ne!(
claude_cmds, claude.cli_install_commands,
@@ -1123,6 +1125,7 @@ fn test_cli_install_commands_for_os_selects_powershell_on_windows() {
codex_cmds, codex.cli_install_commands,
"Windows must NOT use the default curl|bash commands for codex"
);
+ assert_eq!(goose_cmds, goose.cli_install_commands_windows);
// Verify they are the PowerShell installers.
assert!(
@@ -1133,13 +1136,9 @@ fn test_cli_install_commands_for_os_selects_powershell_on_windows() {
codex_cmds.iter().any(|c| c.contains("powershell")),
"codex Windows install must use powershell; got: {codex_cmds:?}"
);
-
- // Goose and buzz-agent must NOT use Windows-specific commands.
- let goose = super::known_acp_runtime_exact("goose").unwrap();
- assert_eq!(
- goose.cli_install_commands_for_os(),
- goose.cli_install_commands,
- "goose must use the same commands on all platforms"
+ assert!(
+ goose_cmds.iter().any(|c| c.contains("download_cli.ps1")),
+ "Goose Windows install must use download_cli.ps1; got: {goose_cmds:?}"
);
}
diff --git a/desktop/src-tauri/src/managed_agents/readiness.rs b/desktop/src-tauri/src/managed_agents/readiness.rs
index 87ee6241ee..c04dfa88a4 100644
--- a/desktop/src-tauri/src/managed_agents/readiness.rs
+++ b/desktop/src-tauri/src/managed_agents/readiness.rs
@@ -869,7 +869,8 @@ mod tests {
cli_install_commands: &[],
cli_install_commands_windows: &[],
adapter_install_commands: &[],
- install_instructions_url: "",
+ cli_install_instructions_url: "",
+ adapter_install_instructions_url: "",
cli_install_hint: "",
adapter_install_hint: "",
skill_dir: None,
@@ -1063,7 +1064,8 @@ mod tests {
cli_install_commands: &[],
cli_install_commands_windows: &[],
adapter_install_commands: &[],
- install_instructions_url: "",
+ cli_install_instructions_url: "",
+ adapter_install_instructions_url: "",
cli_install_hint: "",
adapter_install_hint: "",
skill_dir: None,
diff --git a/desktop/src-tauri/src/managed_agents/types.rs b/desktop/src-tauri/src/managed_agents/types.rs
index 90da759af0..af4908911b 100644
--- a/desktop/src-tauri/src/managed_agents/types.rs
+++ b/desktop/src-tauri/src/managed_agents/types.rs
@@ -585,6 +585,8 @@ pub struct AcpRuntimeCatalogEntry {
pub install_instructions_url: String,
/// true when at least one automated install step is available
pub can_auto_install: bool,
+ /// true when this runtime depends on a separately installed vendor CLI.
+ pub requires_external_cli: bool,
pub underlying_cli_path: Option,
/// true when an npm adapter step is pending but Node.js / npm is absent.
/// The UI hides the Install button and shows a Node.js install callout.
diff --git a/desktop/src/features/agents/AGENTS.md b/desktop/src/features/agents/AGENTS.md
index dc61984487..2af9ddb98c 100644
--- a/desktop/src/features/agents/AGENTS.md
+++ b/desktop/src/features/agents/AGENTS.md
@@ -15,7 +15,9 @@ Plan of record: `Buzz/Harness-Provider-Model.md` in Morgan's Obsidian vault
declares each harness's model/provider/effort env keys and capabilities. Spawn
applies them; `AcpRuntimeCatalogEntry` exposes them over IPC; and
`lib/agentConfigCore.ts` projects them into field descriptors. The frontend
-never maintains a rival copy of this table.
+never maintains a rival copy of this table. Setup guidance follows the same
+rule: `requires_external_cli` is derived from `KnownAcpRuntime` and projected
+to the UI rather than inferred from a runtime ID in a component.
If you need a new capability fact (a new env key, a native option, a "supports
X" flag): add it to `KnownAcpRuntime` first, expose it on
diff --git a/desktop/src/features/agents/ui/AgentDefinitionDialog.tsx b/desktop/src/features/agents/ui/AgentDefinitionDialog.tsx
index 7556a79ba2..5263920854 100644
--- a/desktop/src/features/agents/ui/AgentDefinitionDialog.tsx
+++ b/desktop/src/features/agents/ui/AgentDefinitionDialog.tsx
@@ -627,8 +627,8 @@ export function AgentDefinitionDialog({
? `${selectedRuntime.label} CLI is installed but the ACP adapter is missing.`
: selectedRuntime.availability === "adapter_outdated"
? `${selectedRuntime.label} ACP adapter is outdated — reinstall to continue.`
- : selectedRuntime.availability === "cli_missing"
- ? `${selectedRuntime.label} ACP adapter is installed but the CLI is missing.`
+ : selectedRuntime.requiresExternalCli
+ ? `${selectedRuntime.label} CLI is missing. ${selectedRuntime.installHint}`
: `${selectedRuntime.label} is not installed.`}{" "}
Visit Settings > Agents to set it up.
diff --git a/desktop/src/features/onboarding/ui/SetupStep.tsx b/desktop/src/features/onboarding/ui/SetupStep.tsx
index aa02486706..288ca30259 100644
--- a/desktop/src/features/onboarding/ui/SetupStep.tsx
+++ b/desktop/src/features/onboarding/ui/SetupStep.tsx
@@ -384,8 +384,11 @@ function runtimeDetailText(runtime: AcpRuntimeCatalogEntry): string {
if (runtime.availability === "adapter_outdated") {
return "ACP adapter detected but outdated — reinstall required.";
}
- if (runtime.availability === "cli_missing") {
- return "ACP adapter detected; CLI missing.";
+ if (
+ runtime.availability === "cli_missing" ||
+ runtime.availability === "not_installed"
+ ) {
+ return "CLI not detected; the desktop app alone isn’t enough.";
}
return "";
}
@@ -605,8 +608,8 @@ function RuntimeProvidersSection({
Set up your agent harnesses
- Buzz detected the harnesses available on this machine. Install or sign
- in to at least one to continue.
+ Buzz checks for command-line harnesses on this machine. Install the
+ CLI or sign in to at least one to continue.