1pub mod bundle;
2pub mod policy;
3pub mod prompt;
4
5use std::path::PathBuf;
6
7use bundle::{LaunchBundle, ScaffoldSlots, SkillsMetadata, ToolsSpec};
8use policy::{PolicyInput, resolve_policy};
9use prompt::compile_prompt_surface;
10
11use crate::cli::MarsContext;
12use crate::compiler::agents::{HarnessKind, parse_agent_content};
13use crate::error::{ConfigError, MarsError};
14
15pub struct LaunchBundleRequest {
16 pub agent: String,
17 pub model: Option<String>,
18 pub harness: Option<String>,
19 pub effort: Option<String>,
20 pub approval: Option<String>,
21 pub sandbox: Option<String>,
22 pub extra_skills: Vec<String>,
23}
24
25pub fn build_launch_bundle(
26 ctx: &MarsContext,
27 request: LaunchBundleRequest,
28) -> Result<LaunchBundle, MarsError> {
29 let agent_path = agent_file_path(&ctx.project_root, &request.agent);
30 let agent_content = std::fs::read_to_string(&agent_path).map_err(|source| MarsError::Io {
31 operation: "read launch bundle agent".to_string(),
32 path: agent_path.clone(),
33 source,
34 })?;
35
36 let mut parse_diags = Vec::new();
37 let (profile, frontmatter) =
38 parse_agent_content(&agent_content, &mut parse_diags).map_err(|err| {
39 MarsError::Config(ConfigError::Invalid {
40 message: format!(
41 "failed to parse agent `{}` from {}: {err}",
42 request.agent,
43 agent_path.display()
44 ),
45 })
46 })?;
47
48 if let Some(fatal) = parse_diags.iter().find(|diag| diag.is_error()) {
49 return Err(MarsError::Config(ConfigError::Invalid {
50 message: format!(
51 "agent `{}` has invalid frontmatter in {}: {}",
52 request.agent,
53 agent_path.display(),
54 fatal.message()
55 ),
56 }));
57 }
58
59 let mut warnings: Vec<String> = parse_diags
60 .iter()
61 .map(|diag| format!("agent `{}`: {}", request.agent, diag.message()))
62 .collect();
63
64 let policy = resolve_policy(PolicyInput {
65 project_root: &ctx.project_root,
66 profile: &profile,
67 model_override: request.model.as_deref(),
68 harness_override: request.harness.as_deref(),
69 effort_override: request.effort.as_deref(),
70 approval_override: request.approval.as_deref(),
71 sandbox_override: request.sandbox.as_deref(),
72 })?;
73
74 warnings.extend(policy.warnings);
75
76 let mars_dir = ctx.project_root.join(".mars");
77 let effective_skills = resolve_effective_skills(&profile, &policy.routing.harness)?;
78
79 let prompt = compile_prompt_surface(
80 &mars_dir,
81 frontmatter.body(),
82 &effective_skills,
83 &request.extra_skills,
84 &policy.routing.harness,
85 &policy.routing.model_token,
86 &policy.routing.model,
87 )?;
88
89 warnings.extend(prompt.warnings);
90 let resolved_tools = resolve_bundle_tools(&profile, &policy.routing.harness)?;
91
92 Ok(LaunchBundle {
93 version: 1,
94 agent: request.agent,
95 routing: policy.routing,
96 execution_policy: policy.execution_policy,
97 prompt_surface: bundle::PromptSurface {
98 system_instruction: prompt.system_instruction,
99 supplemental_documents: prompt.supplemental_documents,
100 inventory_prompt: prompt.inventory_prompt,
101 },
102 scaffold_slots: ScaffoldSlots::placeholders(),
103 tools: resolved_tools,
104 skills_metadata: SkillsMetadata {
105 loaded: prompt.loaded_skills,
106 missing: prompt.missing_skills,
107 },
108 provenance: policy.provenance,
109 warnings,
110 })
111}
112
113fn agent_file_path(project_root: &std::path::Path, agent: &str) -> PathBuf {
114 project_root
115 .join(".mars")
116 .join("agents")
117 .join(format!("{agent}.md"))
118}
119
120fn resolve_bundle_tools(
121 profile: &crate::compiler::agents::AgentProfile,
122 harness: &str,
123) -> Result<ToolsSpec, MarsError> {
124 let harness_kind = parse_harness_kind(harness)?;
125
126 let effective_tools = profile.effective_tool_policy(&harness_kind);
127
128 Ok(ToolsSpec {
129 allowed: effective_tools.allowed,
130 disallowed: effective_tools.disallowed,
131 mcp: effective_tools.mcp,
132 })
133}
134
135fn resolve_effective_skills(
136 profile: &crate::compiler::agents::AgentProfile,
137 harness: &str,
138) -> Result<Vec<String>, MarsError> {
139 let harness_kind = parse_harness_kind(harness)?;
140 Ok(profile.effective_skills(&harness_kind).to_vec())
141}
142
143fn parse_harness_kind(harness: &str) -> Result<HarnessKind, MarsError> {
144 HarnessKind::from_str(harness).ok_or_else(|| {
145 MarsError::Config(ConfigError::Invalid {
146 message: format!(
147 "invalid harness `{harness}` for launch bundle resolution; expected one of: claude, codex, opencode, cursor, pi"
148 ),
149 })
150 })
151}