pub mod agents;
pub mod config_entries;
pub mod context;
pub mod hooks;
pub mod mcp;
pub mod skills;
pub mod variants;
pub mod visibility;
use std::path::Path;
use indexmap::IndexMap;
use crate::config::AgentEmission;
use crate::diagnostic::DiagnosticCollector;
use crate::error::MarsError;
use crate::model::ReaderIr;
use crate::sync::{
SyncReport, SyncRequest,
apply::{ActionOutcome, ActionTaken},
apply_plan, build_target, check_frozen_gate, create_plan, finalize, sync_targets,
};
use crate::types::MarsContext;
pub fn compile(
ctx: &MarsContext,
ir: ReaderIr,
request: &SyncRequest,
diag: &mut DiagnosticCollector,
) -> Result<SyncReport, MarsError> {
let targeted = build_target(ctx, ir.resolved, ir.local_items, request, diag)?;
let planned = create_plan(ctx, targeted, request, diag)?;
if request.options.frozen {
check_frozen_gate(&planned)?;
}
let applied = apply_plan(ctx, planned, request)?;
let agent_surface_policy = agent_surface_policy(
applied
.planned
.targeted
.resolved
.loaded
.config
.settings
.agent_emission
.as_ref(),
ctx.meridian_managed,
);
let mars_dir = ctx.project_root.join(".mars");
reconcile_native_agent_surfaces(
agent_surface_policy,
&ctx.project_root,
&mars_dir,
&applied.applied.outcomes,
&applied.planned.targeted.resolved.loaded.old_lock,
request.options.dry_run,
diag,
);
let compiled_native_outputs = if matches!(agent_surface_policy, AgentSurfacePolicy::EmitAll) {
let model_aliases =
merged_model_aliases_for_native_agents(&applied.planned.targeted.resolved);
let cursor_probe_slugs = cached_cursor_probe_slugs_for_native_agents();
dual_surface_compile(
&ctx.project_root,
&mars_dir,
&model_aliases,
&cursor_probe_slugs,
&applied.planned.targeted.resolved.loaded.old_lock,
NativeAgentSurfaceCompileOptions {
force: request.options.force,
collision_hint: crate::surface_ownership::CollisionAdoptHint::SyncForce,
dry_run: request.options.dry_run,
},
diag,
)
} else {
Vec::new()
};
let config_entry_records =
config_entries::compile_config_entries(ctx, &applied, request.options.dry_run, diag);
let mut synced = sync_targets(ctx, applied, request, agent_surface_policy, diag);
synced.config_entries = config_entry_records;
synced.compiled_native_outputs = compiled_native_outputs;
finalize(ctx, synced, request, diag)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AgentSurfacePolicy {
EmitAll,
SuppressAll,
}
pub fn agent_surface_policy(
agent_emission: Option<&AgentEmission>,
meridian_managed: bool,
) -> AgentSurfacePolicy {
match agent_emission.unwrap_or(&AgentEmission::Auto) {
AgentEmission::Always => AgentSurfacePolicy::EmitAll,
AgentEmission::Never => AgentSurfacePolicy::SuppressAll,
AgentEmission::Auto if meridian_managed => AgentSurfacePolicy::SuppressAll,
AgentEmission::Auto => AgentSurfacePolicy::EmitAll,
}
}
pub fn suppress_agent_outcomes(outcomes: &[ActionOutcome]) -> Vec<ActionOutcome> {
outcomes
.iter()
.cloned()
.map(|mut outcome| {
if outcome.item_id.kind == crate::lock::ItemKind::Agent {
outcome.action = ActionTaken::Removed;
}
outcome
})
.collect()
}
fn reconcile_native_agent_surfaces(
policy: AgentSurfacePolicy,
project_root: &Path,
mars_dir: &Path,
outcomes: &[crate::sync::apply::ActionOutcome],
old_lock: &crate::lock::LockFile,
dry_run: bool,
diag: &mut DiagnosticCollector,
) {
use crate::lock::ItemKind;
if matches!(policy, AgentSurfacePolicy::SuppressAll) {
remove_current_native_agent_surfaces(project_root, mars_dir, old_lock, dry_run, diag);
}
for outcome in outcomes {
if outcome.item_id.kind != ItemKind::Agent
|| !matches!(outcome.action, ActionTaken::Removed)
{
continue;
}
let agent_name = outcome.dest_path.item_name(ItemKind::Agent);
remove_native_agent_shapes(project_root, &agent_name, old_lock, dry_run, diag);
}
}
fn remove_current_native_agent_surfaces(
project_root: &Path,
mars_dir: &Path,
old_lock: &crate::lock::LockFile,
dry_run: bool,
diag: &mut DiagnosticCollector,
) {
use crate::compiler::agents::parse_agent_content;
let agents_dir = mars_dir.join("agents");
let Ok(entries) = std::fs::read_dir(&agents_dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
if path.extension().is_none_or(|ext| ext != "md") {
continue;
}
let content = match std::fs::read_to_string(&path) {
Ok(c) => c,
Err(e) => {
diag.warn(
"native-agent-remove-read",
format!("could not read {}: {e}", path.display()),
);
continue;
}
};
let mut agent_diags = Vec::new();
let (profile, _fm) = match parse_agent_content(&content, &mut agent_diags) {
Ok(r) => r,
Err(e) => {
diag.warn(
"native-agent-remove-parse",
format!("could not parse {}: {e}", path.display()),
);
continue;
}
};
let agent_name = profile.name.as_deref().unwrap_or_else(|| {
path.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("unknown")
});
remove_native_agent_shapes(project_root, agent_name, old_lock, dry_run, diag);
}
}
fn remove_native_agent_shapes(
project_root: &Path,
agent_name: &str,
old_lock: &crate::lock::LockFile,
dry_run: bool,
diag: &mut DiagnosticCollector,
) {
use crate::compiler::agents::HarnessKind;
for harness in HarnessKind::all() {
let target = harness.target_dir();
for extension in ["md", "toml"] {
let dest_rel = format!("agents/{agent_name}.{extension}");
if !old_lock.contains_output(target, &dest_rel) {
continue;
}
let native_path = project_root
.join(target)
.join("agents")
.join(format!("{agent_name}.{extension}"));
if !native_path.exists() && native_path.symlink_metadata().is_err() {
continue;
}
if dry_run {
continue;
}
if let Err(e) = crate::reconcile::fs_ops::safe_remove(&native_path) {
diag.warn(
"native-agent-remove",
format!("could not remove {}: {e}", native_path.display()),
);
}
}
}
}
struct NativeAgentSurfaceCompileOptions {
force: bool,
collision_hint: crate::surface_ownership::CollisionAdoptHint,
dry_run: bool,
}
fn dual_surface_compile(
project_root: &Path,
mars_dir: &Path,
model_aliases: &IndexMap<String, crate::models::ModelAlias>,
cursor_probe_slugs: &[String],
old_lock: &crate::lock::LockFile,
options: NativeAgentSurfaceCompileOptions,
diag: &mut DiagnosticCollector,
) -> Vec<(String, String, crate::types::ContentHash)> {
use crate::compiler::agents::{
HarnessKind,
lower::{lower_for_harness, lower_for_harness_with_model},
parse_agent_content,
};
use crate::surface_ownership::{self, SurfaceCopyDecision};
let agents_dir = mars_dir.join("agents");
let Ok(entries) = std::fs::read_dir(&agents_dir) else {
return Vec::new();
};
let mut records = Vec::new();
for entry in entries.flatten() {
let path = entry.path();
let Some(ext) = path.extension() else {
continue;
};
if ext != "md" {
continue;
}
let content = match std::fs::read_to_string(&path) {
Ok(c) => c,
Err(e) => {
diag.warn(
"dual-surface-read",
format!("could not read {}: {e}", path.display()),
);
continue;
}
};
let mut agent_diags = Vec::new();
let (profile, fm) = match parse_agent_content(&content, &mut agent_diags) {
Ok(r) => r,
Err(e) => {
diag.warn(
"dual-surface-parse",
format!("could not parse {}: {e}", path.display()),
);
continue;
}
};
let agent_name = profile.name.as_deref().unwrap_or_else(|| {
path.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("unknown")
});
for d in &agent_diags {
if d.is_error() {
diag.warn(
"agent-schema-error",
format!("agent `{agent_name}`: {}", d.message()),
);
} else {
diag.warn(
"agent-schema-warning",
format!("agent `{agent_name}`: {}", d.message()),
);
}
}
let Some(harness) = &profile.harness else {
continue;
};
let body = fm.body().to_string();
let model_override =
native_model_override_for_harness(harness, &profile, model_aliases, cursor_probe_slugs);
let lowered = match model_override.as_deref() {
Some(model) => lower_for_harness_with_model(harness, &profile, &fm, &body, Some(model)),
None => lower_for_harness(harness, &profile, &fm, &body),
};
for lf in &lowered.lossy_fields {
use crate::compiler::agents::lower::Lossiness;
match &lf.classification {
Lossiness::Dropped | Lossiness::MeridianOnly => {}
Lossiness::Approximate { note } => {
diag.warn(
"agent-field-approximate",
format!(
"agent `{agent_name}`: field `{}` approximately mapped in {} ({note})",
lf.field, lf.target
),
);
}
}
}
let harness_dir = project_root.join(harness.target_dir());
let native_agents_dir = harness_dir.join("agents");
let file_name = match harness {
HarnessKind::Codex => format!("{agent_name}.toml"),
_ => format!("{agent_name}.md"),
};
let native_path = native_agents_dir.join(&file_name);
let dest_rel = format!("agents/{file_name}");
let target_dir = harness.target_dir();
let dest_exists = surface_ownership::target_dest_exists(&native_path);
match surface_ownership::copy_decision(
old_lock,
target_dir,
&dest_rel,
dest_exists,
options.force,
) {
SurfaceCopyDecision::SkipUnmanagedCollision => {
surface_ownership::warn_unmanaged_collision(
target_dir,
&dest_rel,
options.collision_hint,
diag,
);
continue;
}
SurfaceCopyDecision::Proceed => {
if dest_exists && options.force && !old_lock.contains_output(target_dir, &dest_rel)
{
surface_ownership::warn_unmanaged_adopted(
target_dir,
&dest_rel,
options.collision_hint,
diag,
);
}
}
}
if !options.dry_run {
if let Err(e) = std::fs::create_dir_all(&native_agents_dir) {
diag.warn(
"dual-surface-mkdir",
format!("could not create {}: {e}", native_agents_dir.display()),
);
continue;
}
if let Err(e) = crate::fs::atomic_write(&native_path, &lowered.bytes) {
diag.warn(
"dual-surface-write",
format!("could not write {}: {e}", native_path.display()),
);
} else {
let checksum =
crate::types::ContentHash::from(crate::hash::hash_bytes(&lowered.bytes));
records.push((target_dir.to_string(), dest_rel, checksum));
}
}
}
records
}
fn merged_model_aliases_for_native_agents(
resolved: &crate::sync::ResolvedState,
) -> IndexMap<String, crate::models::ModelAlias> {
let dep_models =
crate::sync::declaration_ordered_dep_models(&resolved.graph, &resolved.loaded.effective);
let mut local_diag = DiagnosticCollector::new();
crate::models::merge_model_config(
&resolved.loaded.config.models,
&dep_models,
&mut local_diag,
None,
)
}
fn cached_cursor_probe_slugs_for_native_agents() -> Vec<String> {
crate::models::probes::cursor_cache::read_cached_probe_result_usable()
.map(|probe| probe.slugs)
.unwrap_or_default()
}
fn native_model_override_for_harness(
harness: &crate::compiler::agents::HarnessKind,
profile: &crate::compiler::agents::AgentProfile,
aliases: &IndexMap<String, crate::models::ModelAlias>,
cursor_probe_slugs: &[String],
) -> Option<String> {
if !matches!(harness, crate::compiler::agents::HarnessKind::Cursor) {
return None;
}
map_cursor_native_model(profile, aliases, cursor_probe_slugs)
}
fn map_cursor_native_model(
profile: &crate::compiler::agents::AgentProfile,
aliases: &IndexMap<String, crate::models::ModelAlias>,
cursor_probe_slugs: &[String],
) -> Option<String> {
let token = profile.model.as_deref()?;
if token.contains('[') {
return None;
}
let alias = aliases.get(token);
let model_id = alias.and_then(pinned_model_id).unwrap_or(token);
let effort = cursor_effective_effort(profile, alias).unwrap_or("medium");
if cursor_probe_slugs.is_empty() {
return None;
}
for candidate in cursor_probe_lookup_model_ids(model_id) {
if let Ok(resolution) = crate::models::probes::cursor::resolve_cursor_effort_slug(
&candidate,
effort,
cursor_probe_slugs,
) {
return Some(resolution.slug);
}
}
None
}
fn pinned_model_id(alias: &crate::models::ModelAlias) -> Option<&str> {
match &alias.spec {
crate::models::ModelSpec::Pinned { model, .. }
| crate::models::ModelSpec::PinnedWithMatch { model, .. } => Some(model.as_str()),
crate::models::ModelSpec::AutoResolve { .. } => None,
}
}
fn cursor_effective_effort<'a>(
profile: &'a crate::compiler::agents::AgentProfile,
alias: Option<&'a crate::models::ModelAlias>,
) -> Option<&'a str> {
profile
.harness_overrides
.cursor
.as_ref()
.and_then(|overrides| overrides.effort.as_ref())
.map(crate::compiler::agents::EffortLevel::as_str)
.or_else(|| {
profile
.effort
.as_ref()
.map(crate::compiler::agents::EffortLevel::as_str)
})
.or_else(|| alias.and_then(|resolved| resolved.default_effort.as_deref()))
.map(|effort| match effort {
"auto" => "medium",
other => other,
})
}
fn cursor_probe_lookup_model_ids(model_id: &str) -> Vec<String> {
let mut candidates = vec![model_id.to_string()];
if let Some(shimmed) = cursor_probe_model_id_shim(model_id) {
candidates.push(shimmed);
}
candidates
}
fn cursor_probe_model_id_shim(model_id: &str) -> Option<String> {
match model_id.to_ascii_lowercase().as_str() {
"claude-opus-4-6" => Some("claude-4.6-opus".to_string()),
"claude-sonnet-4-6" => Some("claude-4.6-sonnet".to_string()),
_ => None,
}
}
#[cfg(test)]
mod skill_surface_tests {
use super::*;
use crate::compiler::agents::HarnessKind;
use crate::diagnostic::DiagnosticCollector;
use crate::lock::{ItemId, ItemKind, LockFile, LockedItemV2, OutputRecord};
use crate::models::{ModelAlias, ModelSpec};
use crate::sync::apply::{ActionOutcome, ActionTaken};
use crate::types::{DestPath, ItemName};
use indexmap::IndexMap;
use tempfile::TempDir;
#[test]
fn native_agent_emission_defaults_to_standalone_auto() {
assert_eq!(
agent_surface_policy(None, false),
AgentSurfacePolicy::EmitAll
);
}
#[test]
fn native_agent_emission_auto_suppresses_meridian_managed() {
assert_eq!(
agent_surface_policy(Some(&AgentEmission::Auto), true),
AgentSurfacePolicy::SuppressAll
);
}
#[test]
fn native_agent_emission_always_ignores_meridian_managed() {
assert_eq!(
agent_surface_policy(Some(&AgentEmission::Always), true),
AgentSurfacePolicy::EmitAll
);
}
#[test]
fn native_agent_emission_never_suppresses_standalone() {
assert_eq!(
agent_surface_policy(Some(&AgentEmission::Never), false),
AgentSurfacePolicy::SuppressAll
);
}
fn profile_with_cursor_model(model: &str) -> crate::compiler::agents::AgentProfile {
crate::compiler::agents::AgentProfile {
name: None,
description: None,
harness: Some(HarnessKind::Cursor),
model: Some(model.to_string()),
mode: None,
model_invocable: true,
approval: None,
sandbox: None,
effort: None,
autocompact: None,
autocompact_pct: None,
skills: Vec::new(),
tools: Vec::new(),
tools_denied: Vec::new(),
disallowed_tools: Vec::new(),
mcp_tools: Vec::new(),
harness_overrides: crate::compiler::agents::HarnessOverrides::default(),
model_policies: Vec::new(),
fanout: Vec::new(),
}
}
fn pinned_alias(model: &str, default_effort: Option<&str>) -> ModelAlias {
ModelAlias {
harness: Some("codex".to_string()),
description: None,
default_effort: default_effort.map(str::to_owned),
autocompact: None,
autocompact_pct: None,
spec: ModelSpec::Pinned {
model: model.to_string(),
provider: None,
},
}
}
#[test]
fn cursor_native_model_mapping_uses_shared_resolver_for_alias_and_effort() {
let profile = profile_with_cursor_model("gpt55");
let mut aliases = IndexMap::new();
aliases.insert("gpt55".to_string(), pinned_alias("gpt-5.5", Some("high")));
let slugs = vec!["gpt-5.5-high".to_string(), "gpt-5.5-low".to_string()];
assert_eq!(
native_model_override_for_harness(&HarnessKind::Cursor, &profile, &aliases, &slugs),
Some("gpt-5.5-high".to_string())
);
}
#[test]
fn cursor_native_model_mapping_preserves_unknown_or_cursor_literal_tokens() {
let profile = profile_with_cursor_model("composer-2.5[fast=false]");
let slugs = vec!["composer-2.5".to_string(), "composer-2.5-fast".to_string()];
assert_eq!(
native_model_override_for_harness(
&HarnessKind::Cursor,
&profile,
&IndexMap::new(),
&slugs
),
None
);
let profile = profile_with_cursor_model("unmapped-model");
assert_eq!(
native_model_override_for_harness(
&HarnessKind::Cursor,
&profile,
&IndexMap::new(),
&slugs
),
None
);
}
#[test]
fn cursor_native_model_mapping_uses_claude_shim_with_shared_resolver() {
let profile = profile_with_cursor_model("opus");
let mut aliases = IndexMap::new();
aliases.insert(
"opus".to_string(),
pinned_alias("claude-opus-4-6", Some("high")),
);
let slugs = vec![
"claude-4.6-opus-thinking-high".to_string(),
"claude-4.6-opus-thinking-medium".to_string(),
];
assert_eq!(
native_model_override_for_harness(&HarnessKind::Cursor, &profile, &aliases, &slugs),
Some("claude-4.6-opus-thinking-high".to_string())
);
}
fn lock_with_target_outputs(targets: &[&str], dest: &str, checksum: &str) -> LockFile {
let mut lock = LockFile::empty();
let outputs = targets
.iter()
.map(|target| OutputRecord {
target_root: (*target).to_string(),
dest_path: dest.into(),
installed_checksum: checksum.into(),
})
.collect();
lock.items.insert(
"agent/coder".to_string(),
LockedItemV2 {
source: "test".into(),
kind: ItemKind::Agent,
version: None,
source_checksum: "sha256:src".into(),
outputs,
},
);
lock
}
fn agent_outcome(name: &str, action: ActionTaken) -> ActionOutcome {
ActionOutcome {
item_id: ItemId {
kind: ItemKind::Agent,
name: ItemName::from(name),
},
action,
dest_path: DestPath::from(format!("agents/{name}.md")),
source_name: "test-source".into(),
source_checksum: None,
installed_checksum: None,
}
}
#[test]
fn reconcile_emit_all_removes_native_shapes_for_removed_agents() {
let dir = TempDir::new().unwrap();
for harness in HarnessKind::all() {
let agents_dir = dir.path().join(harness.target_dir()).join("agents");
std::fs::create_dir_all(&agents_dir).unwrap();
std::fs::write(agents_dir.join("coder.md"), "# Old\n").unwrap();
std::fs::write(agents_dir.join("coder.toml"), "old = true\n").unwrap();
}
let tracked_targets: Vec<&str> =
HarnessKind::all().iter().map(|h| h.target_dir()).collect();
let mut lock =
lock_with_target_outputs(&tracked_targets, "agents/coder.md", "sha256:coder");
for target in &tracked_targets {
lock.items
.get_mut("agent/coder")
.unwrap()
.outputs
.push(OutputRecord {
target_root: (*target).to_string(),
dest_path: "agents/coder.toml".into(),
installed_checksum: "sha256:coder-toml".into(),
});
}
let mut diag = DiagnosticCollector::new();
reconcile_native_agent_surfaces(
AgentSurfacePolicy::EmitAll,
dir.path(),
&dir.path().join(".mars"),
&[agent_outcome("coder", ActionTaken::Removed)],
&lock,
false,
&mut diag,
);
for harness in HarnessKind::all() {
assert!(
!dir.path()
.join(harness.target_dir())
.join("agents/coder.md")
.exists()
);
assert!(
!dir.path()
.join(harness.target_dir())
.join("agents/coder.toml")
.exists()
);
}
assert!(diag.drain().is_empty());
}
#[test]
fn reconcile_suppress_all_removes_native_shapes_for_current_agents() {
let dir = TempDir::new().unwrap();
let mars_agents = dir.path().join(".mars").join("agents");
std::fs::create_dir_all(&mars_agents).unwrap();
std::fs::write(
mars_agents.join("coder.md"),
"---\nname: coder\n---\n# Coder\n",
)
.unwrap();
for target in [".claude", ".codex", ".opencode"] {
let agents_dir = dir.path().join(target).join("agents");
std::fs::create_dir_all(&agents_dir).unwrap();
std::fs::write(agents_dir.join("coder.md"), "# Native\n").unwrap();
}
let mut diag = DiagnosticCollector::new();
let lock = lock_with_target_outputs(
&[".claude", ".codex", ".opencode"],
"agents/coder.md",
"sha256:coder",
);
reconcile_native_agent_surfaces(
AgentSurfacePolicy::SuppressAll,
dir.path(),
&dir.path().join(".mars"),
&[agent_outcome("coder", ActionTaken::Installed)],
&lock,
false,
&mut diag,
);
for target in [".claude", ".codex", ".opencode"] {
assert!(
!dir.path().join(target).join("agents/coder.md").exists(),
"native agent should be removed under SuppressAll for target {target}"
);
}
}
#[test]
fn reconcile_suppress_all_preserves_untracked_native_agents() {
let dir = TempDir::new().unwrap();
let mars_agents = dir.path().join(".mars").join("agents");
std::fs::create_dir_all(&mars_agents).unwrap();
std::fs::write(
mars_agents.join("coder.md"),
"---\nname: coder\n---\n# Coder\n",
)
.unwrap();
let agents_dir = dir.path().join(".cursor").join("agents");
std::fs::create_dir_all(&agents_dir).unwrap();
std::fs::write(agents_dir.join("coder.md"), "# hand-written\n").unwrap();
let mut diag = DiagnosticCollector::new();
reconcile_native_agent_surfaces(
AgentSurfacePolicy::SuppressAll,
dir.path(),
&dir.path().join(".mars"),
&[agent_outcome("coder", ActionTaken::Installed)],
&LockFile::empty(),
false,
&mut diag,
);
assert!(dir.path().join(".cursor/agents/coder.md").exists());
}
#[test]
fn reconcile_emit_all_preserves_non_removed_agents() {
let dir = TempDir::new().unwrap();
let agents_dir = dir.path().join(".claude").join("agents");
std::fs::create_dir_all(&agents_dir).unwrap();
std::fs::write(agents_dir.join("coder.md"), "# Native\n").unwrap();
let mut diag = DiagnosticCollector::new();
reconcile_native_agent_surfaces(
AgentSurfacePolicy::EmitAll,
dir.path(),
&dir.path().join(".mars"),
&[agent_outcome("coder", ActionTaken::Installed)],
&LockFile::empty(),
false,
&mut diag,
);
assert!(dir.path().join(".claude/agents/coder.md").exists());
}
}