use crate::{
cancellation::AgentCancellation,
config::{AppearanceSettings, CliConfigOverrides, EffectiveConfig, McPaths, Settings},
instructions::{InstructionFile, discover_agents_with_additional_markdown},
primary_agents::{PrimaryAgentProfileDiscovery, resolve_persisted_primary_agent_selection},
skills::{SkillDiscovery, SkillRoots, discover_skills, filter_enabled_skills},
};
use std::{
collections::HashSet,
path::PathBuf,
sync::{Arc, Mutex},
};
pub(crate) struct LaunchContext {
pub(crate) cwd: PathBuf,
pub(crate) config: EffectiveConfig,
pub(crate) settings: Settings,
pub(crate) appearance_settings: AppearanceSettings,
pub(crate) retention_days: u64,
pub(crate) instructions: Vec<InstructionFile>,
pub(crate) discovered_skills: SkillDiscovery,
pub(crate) skills: SkillDiscovery,
}
impl LaunchContext {
pub(crate) fn load(
cwd: PathBuf,
paths: McPaths,
overrides: CliConfigOverrides,
) -> anyhow::Result<Self> {
paths.ensure_runtime_dirs()?;
let (config, settings, appearance_settings, session_settings) =
crate::config::load_startup_config_with_settings(paths.clone(), overrides)?;
let instructions = discover_agents_with_additional_markdown(
&paths.user_agents,
&cwd,
&settings.instructions.additional_markdown_paths,
)?;
let mut roots = SkillRoots::from_paths(&paths.root, &cwd);
roots.additional_paths = settings.skills.additional_paths.clone();
let discovered_skills = discover_skills(&roots);
let skills = filter_enabled_skills(
&discovered_skills,
&crate::config::disabled_skill_names_from_settings(&settings),
);
Ok(Self {
cwd,
config,
settings,
appearance_settings,
retention_days: session_settings.retention_days,
instructions,
discovered_skills,
skills,
})
}
}
#[derive(Debug, Clone)]
pub(crate) struct StartupPolicy {
pub(crate) disabled_tools: HashSet<String>,
pub(crate) disabled_subagent_profiles: HashSet<String>,
pub(crate) skills: SkillDiscovery,
pub(crate) subagent_profile_discovery: crate::subagents::profiles::SubagentProfileDiscovery,
pub(crate) primary_agent_discovery: PrimaryAgentProfileDiscovery,
pub(crate) selected_primary_agent: Option<String>,
pub(crate) mcp: Option<Arc<Mutex<crate::mcp::manager::McpManager>>>,
}
#[derive(Debug)]
pub(crate) struct StartupFailure {
#[cfg_attr(
not(unix),
expect(
dead_code,
reason = "structured startup codes belong to Unix local-agent protocol"
)
)]
pub(crate) code: &'static str,
pub(crate) detail: String,
}
impl std::fmt::Display for StartupFailure {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.detail)
}
}
impl std::error::Error for StartupFailure {}
pub(crate) fn bounded_startup_diagnostic(message: impl std::fmt::Display) -> String {
const MAX_CHARS: usize = 512;
let sanitized = crate::output::sanitize_display_text(&message.to_string());
let normalized = sanitized.split_whitespace().collect::<Vec<_>>().join(" ");
let mut bounded = normalized.chars().take(MAX_CHARS).collect::<String>();
if normalized.chars().count() > MAX_CHARS {
bounded.pop();
bounded.push('…');
}
bounded
}
pub(crate) fn enabled_subagent_diagnostic(
diagnostic: &crate::subagents::profiles::SubagentProfileDiagnostic,
disabled: &HashSet<String>,
) -> bool {
diagnostic.id.as_deref().is_some_and(|id| {
crate::subagents::profiles::validate_subagent_identity_id(id).is_ok()
&& !disabled.contains(id)
})
}
impl StartupPolicy {
pub(crate) fn load(
settings: &Settings,
discovered_skills: &SkillDiscovery,
paths: &McPaths,
cancellation: &AgentCancellation,
) -> Result<Self, StartupFailure> {
let failure = |code, detail| StartupFailure {
code,
detail: bounded_startup_diagnostic(detail),
};
cancellation
.check()
.map_err(|error| failure("startup_cancelled", error.to_string()))?;
let disabled_tools = crate::config::disabled_tool_names_from_settings(settings)
.into_iter()
.collect();
let disabled_subagent_profiles =
crate::config::disabled_subagent_profile_names_from_settings(settings)
.into_iter()
.collect();
let skills = filter_enabled_skills(
discovered_skills,
&crate::config::disabled_skill_names_from_settings(settings),
);
let subagent_profile_discovery =
crate::subagents::profiles::discover_subagent_profiles(&paths.subagents);
if let Some(diagnostic) = subagent_profile_discovery
.diagnostics
.iter()
.find(|diagnostic| enabled_subagent_diagnostic(diagnostic, &disabled_subagent_profiles))
{
return Err(failure(
"profile_invalid",
format!(
"enabled subagent profile '{}' failed discovery: {}",
bounded_startup_diagnostic(diagnostic.id.as_deref().unwrap_or("<unknown>")),
diagnostic.message
),
));
}
let primary_agent_discovery =
crate::primary_agents::discover_primary_agent_profiles(&paths.primary_agents);
let selection = resolve_persisted_primary_agent_selection(
settings.selected_primary_agent.as_deref(),
&primary_agent_discovery.profiles,
);
if settings.selected_primary_agent.is_some() && selection.selected_id.is_none() {
return Err(failure(
"profile_invalid",
format!(
"stored primary agent selection is unavailable: {}",
selection
.diagnostics
.first()
.map(|item| item.message.as_str())
.unwrap_or("stored primary agent selection is unavailable")
),
));
}
cancellation
.check()
.map_err(|error| failure("startup_cancelled", error.to_string()))?;
let mcp = (!settings.mcp_servers.is_empty())
.then(|| {
crate::mcp::manager::McpManager::from_settings_strict_cancellable(
&settings.mcp_servers,
Some(&paths.root),
cancellation,
)
.map(|manager| Arc::new(Mutex::new(manager)))
})
.transpose()
.map_err(|error| failure("mcp_unavailable", error.to_string()))?;
Ok(Self {
disabled_tools,
disabled_subagent_profiles,
skills,
subagent_profile_discovery,
primary_agent_discovery,
selected_primary_agent: selection.selected_id,
mcp,
})
}
}