use super::*;
pub(super) fn emit_claude(
roles: &[&RoleInput],
emitted_roles: &[EmittedRole],
skills: &[&SkillInput],
) -> Result<Vec<EmittedFile>, CompileError> {
let mut files = Vec::new();
for (role, emitted_role) in roles.iter().zip(emitted_roles) {
let startup_skill = required_startup_skill(emitted_role)?;
let mut fields = vec![
("name", role.role.clone()),
("description", quote(&role.description)),
(
"model",
emitted_role
.model
.clone()
.expect("validated Claude role has a model"),
),
("tools", inline_array(&emitted_role.tools)),
("skills", inline_array(&[startup_skill.into()])),
];
fields.extend([
("dispatchable", role.dispatchable.to_string()),
("write_eligible", role.write_eligible.to_string()),
("write_scope", quote(&role.write_scope)),
]);
let content = frontmatter_file(&fields, &role.body)?;
files.push(emitted(
format!("agents/{}.md", role.role),
EmittedKind::Role,
content,
&role.source_path,
&role.source_content,
BudgetClass::Role,
)?);
}
emit_skills(&mut files, TargetHarness::Claude, skills)?;
Ok(files)
}
pub(super) fn emit_codex(
roles: &[&RoleInput],
emitted_roles: &[EmittedRole],
skills: &[&SkillInput],
profile: &HarnessProfile,
) -> Result<Vec<EmittedFile>, CompileError> {
let mut content = String::from(
"# Generated by the canonical Rust shepherd compiler. Source: content/roles/*.md.\n\
# Do not hand-edit; regenerate via `shepherd compile --target codex --out <directory>`.\n\n",
);
writeln!(
content,
"max_concurrent_children = {}\n\n[agent_types]",
profile.max_concurrent_children
)
.expect("writing to String cannot fail");
for role in roles {
if !role.dispatchable {
continue;
}
writeln!(
content,
"{} = \"{}\"",
role.role,
if role.write_eligible {
"worker"
} else {
"explorer"
}
)
.expect("writing to String cannot fail");
}
content.push_str("\n[models]\n");
let mut profiles = alloc::collections::BTreeMap::new();
for role in emitted_roles {
let Some(profile_name) = role.profile.as_deref() else {
continue;
};
let effort = role
.reasoning_effort
.as_deref()
.expect("validated Codex profile has reasoning effort");
writeln!(content, "{} = \"{profile_name}\"", role.role)
.expect("writing to String cannot fail");
if let Some(previous) = profiles.insert(profile_name, effort)
&& previous != effort
{
return Err(CompileError::Invalid(format!(
"Codex profile `{profile_name}` has conflicting reasoning effort"
)));
}
}
content.push('\n');
for (profile_name, effort) in profiles {
writeln!(content, "[profiles.\"{profile_name}\"]").expect("writing to String cannot fail");
writeln!(content, "reasoning_effort = \"{effort}\"\n")
.expect("writing to String cannot fail");
}
if content.ends_with("\n\n") {
content.pop();
}
let source = roles
.iter()
.map(|role| role.source_content.as_str())
.collect::<String>();
let mut files = vec![emitted(
"shepherd.codex.toml".into(),
EmittedKind::Config,
content,
"content/roles/*.md",
&source,
BudgetClass::Command,
)?];
for (role, emitted_role) in roles.iter().zip(emitted_roles) {
let startup_skill = required_startup_skill(emitted_role)?;
if !role.dispatchable {
continue;
}
let mut agent = String::new();
writeln!(agent, "name = {}", quote(&role.role)).expect("writing to String cannot fail");
writeln!(agent, "description = {}", quote(&role.description))
.expect("writing to String cannot fail");
if let Some(model) = &emitted_role.model {
writeln!(agent, "model = {}", quote(model)).expect("writing to String cannot fail");
}
if let Some(effort) = &emitted_role.reasoning_effort {
writeln!(agent, "model_reasoning_effort = {}", quote(effort))
.expect("writing to String cannot fail");
}
writeln!(
agent,
"sandbox_mode = {}",
quote(if role.write_eligible {
"workspace-write"
} else {
"read-only"
})
)
.expect("writing to String cannot fail");
let instructions = format!(
"First invoke the installed `${startup_skill}` skill. Do not continue until it loads successfully.\n\n{}",
role.body.trim()
);
writeln!(agent, "developer_instructions = {}", quote(&instructions))
.expect("writing to String cannot fail");
files.push(emitted(
format!(".codex/agents/{}.toml", role.role),
EmittedKind::Role,
agent,
&role.source_path,
&role.source_content,
BudgetClass::Role,
)?);
}
emit_skills(&mut files, TargetHarness::Codex, skills)?;
Ok(files)
}
pub(super) fn emit_pi(
roles: &[&RoleInput],
emitted_roles: &[EmittedRole],
skills: &[&SkillInput],
) -> Result<Vec<EmittedFile>, CompileError> {
let mut files = Vec::new();
for (role, emitted_role) in roles.iter().zip(emitted_roles) {
let startup_skill = required_startup_skill(emitted_role)?;
let mut fields = vec![
("name", role.role.clone()),
("description", quote(&role.description)),
("capabilities", inline_array(&role.capabilities)),
("skills", startup_skill.into()),
];
fields.extend([
("dispatchable", role.dispatchable.to_string()),
("write_eligible", role.write_eligible.to_string()),
("write_scope", quote(&role.write_scope)),
]);
let content = frontmatter_file(&fields, &role.body)?;
files.push(emitted(
format!("prompts/{}.md", role.role),
EmittedKind::Role,
content,
&role.source_path,
&role.source_content,
BudgetClass::Role,
)?);
}
emit_skills(&mut files, TargetHarness::Pi, skills)?;
Ok(files)
}
pub(super) fn emit_skills(
files: &mut Vec<EmittedFile>,
target: TargetHarness,
skills: &[&SkillInput],
) -> Result<(), CompileError> {
let root = if target == TargetHarness::Codex {
".agents/skills"
} else {
"skills"
};
for skill in skills {
if skill.portability == Portability::ClaudeOnly && target != TargetHarness::Claude {
continue;
}
let content = frontmatter_file(
&[
("name", skill.name.clone()),
("description", quote(&skill.description)),
],
&skill.body,
)?;
let frontmatter_end = content.find("\n---\n").ok_or_else(|| {
CompileError::Invalid(format!(
"{}: emitted frontmatter is malformed",
skill.source_path
))
})? + 5;
if frontmatter_end > 1_024 {
return Err(CompileError::Invalid(format!(
"{}: frontmatter exceeds 1024 bytes",
skill.source_path
)));
}
files.push(emitted(
format!("{root}/{}/SKILL.md", skill.name),
EmittedKind::Skill,
content,
&skill.source_path,
&skill.source_content,
BudgetClass::Skill,
)?);
for resource in &skill.resources {
let content = String::from_utf8(resource.content.clone()).map_err(|_| {
CompileError::Invalid(format!(
"{}: skill resource must be UTF-8",
resource.source_path
))
})?;
files.push(emitted_resource(
format!("{root}/{}/{}", skill.name, resource.relative_path),
content,
&resource.source_path,
&resource.content,
if resource.executable {
EXECUTABLE_MODE
} else {
REGULAR_MODE
},
(resource.relative_path.starts_with("references/") && !resource.content.is_empty())
.then_some(BudgetClass::Reference),
)?);
}
}
Ok(())
}
pub(super) fn frontmatter_file(
fields: &[(&str, String)],
body: &str,
) -> Result<String, CompileError> {
let body = body.trim();
if body.lines().any(|line| line.trim() == "---") {
return Err(CompileError::Invalid(
"body contains a bare `---` frontmatter fence".into(),
));
}
let mut output = String::from("---\n");
for (key, value) in fields {
writeln!(output, "{key}: {value}").expect("writing to String cannot fail");
}
output.push_str("---\n\n");
output.push_str(body);
output.push('\n');
Ok(output)
}
pub(super) fn quote(value: &str) -> String {
let mut output = String::from("\"");
for character in value.chars() {
match character {
'\\' => output.push_str("\\\\"),
'"' => output.push_str("\\\""),
'\n' => output.push_str("\\n"),
'\r' => output.push_str("\\r"),
'\t' => output.push_str("\\t"),
'\0' => output.push_str("\\0"),
character if character.is_control() => {
write!(output, "\\u{:04x}", character as u32)
.expect("writing to String cannot fail");
}
_ => output.push(character),
}
}
output.push('"');
output
}
pub(super) fn inline_array(values: &[String]) -> String {
format!("[{}]", values.join(", "))
}
pub(super) fn emitted(
path: String,
kind: EmittedKind,
content: String,
source_path: &str,
source_content: &str,
budget_class: BudgetClass,
) -> Result<EmittedFile, CompileError> {
emitted_file(
path,
kind,
content,
source_path,
source_content.as_bytes(),
REGULAR_MODE,
Some(budget_class),
)
}
pub(super) fn emitted_resource(
path: String,
content: String,
source_path: &str,
source_content: &[u8],
mode: u32,
budget_class: Option<BudgetClass>,
) -> Result<EmittedFile, CompileError> {
emitted_file(
path,
EmittedKind::Skill,
content,
source_path,
source_content,
mode,
budget_class,
)
}
pub(super) fn emitted_file(
path: String,
kind: EmittedKind,
content: String,
source_path: &str,
source_content: &[u8],
mode: u32,
budget_class: Option<BudgetClass>,
) -> Result<EmittedFile, CompileError> {
let measurement = if let Some(budget_class) = budget_class {
validate_budget(&path, budget_class, &content)?
} else {
measure_text(&content)
};
Ok(EmittedFile {
path,
kind,
source_sha256: sha256(source_content),
content_sha256: sha256(content.as_bytes()),
content,
mode,
source_path: source_path.into(),
measurement,
})
}