use super::*;
pub(super) fn resolve_roles(
roles: &[&RoleInput],
skills: &[&SkillInput],
profile: &HarnessProfile,
) -> Result<Vec<EmittedRole>, CompileError> {
let resolve_tools = profile.target != TargetHarness::Codex;
roles
.iter()
.map(|role| {
let resolution = profile
.model_by_hint
.get(&role.model_hint)
.ok_or_else(|| {
CompileError::Invalid(format!(
"{}: target `{}` has no model mapping for hint `{}`",
role.source_path,
profile.target.as_str(),
role.model_hint
))
})?;
validate_model_resolution(role, profile.target, resolution)?;
let startup_skill = role.startup_skill.as_str();
let skill = skills
.iter()
.find(|skill| skill.name == startup_skill)
.ok_or_else(|| {
CompileError::Invalid(format!(
"{}: unknown startup skill `{startup_skill}`",
role.source_path
))
})?;
if skill.portability == Portability::ClaudeOnly && profile.target != TargetHarness::Claude {
return Err(CompileError::Invalid(format!(
"{}: startup skill `{startup_skill}` is not portable to target `{}`",
role.source_path,
profile.target.as_str()
)));
}
let mut seen_tools = BTreeSet::new();
let mut tools = Vec::new();
let mut unsupported_capabilities = Vec::new();
if resolve_tools {
for capability in &role.capabilities {
if let Some(mapped) = profile.tools_by_capability.get(capability) {
for tool in mapped {
validate_tool(tool)?;
if seen_tools.insert(tool.as_str()) {
tools.push(tool.clone());
}
}
} else if profile.unsupported_capabilities.contains(capability) {
unsupported_capabilities.push(capability.clone());
} else {
return Err(CompileError::Invalid(format!(
"{}: target `{}` has no tool or unsupported-capability mapping for `{capability}`",
role.source_path,
profile.target.as_str()
)));
}
}
}
Ok(EmittedRole {
role: role.role.clone(),
carrier_path: match profile.target {
TargetHarness::Claude => format!("agents/{}.md", role.role),
TargetHarness::Codex if role.dispatchable => {
format!(".codex/agents/{}.toml", role.role)
}
TargetHarness::Codex => "shepherd.codex.toml".into(),
TargetHarness::Pi => format!("prompts/{}.md", role.role),
},
description: role.description.clone(),
model_hint: role.model_hint.clone(),
model: resolution.model.clone(),
profile: resolution.profile.clone(),
reasoning_effort: resolution.reasoning_effort.clone(),
tools,
unsupported_capabilities,
capabilities: role.capabilities.clone(),
startup_skill: Some(role.startup_skill.clone()),
startup_skill_sha256: None,
write_eligible: role.write_eligible,
dispatchable: role.dispatchable,
write_scope: role.write_scope.clone(),
})
})
.collect()
}
pub(super) fn required_startup_skill(role: &EmittedRole) -> Result<&str, CompileError> {
role.startup_skill.as_deref().ok_or_else(|| {
CompileError::Invalid(format!(
"compiled role `{}` has no startup skill contract",
role.role
))
})
}