shepherd-compiler 6.6.0

Pure, deterministic Shepherd content compiler and prompt-budget engine.
//! Per-harness emission.
//!
//! `emit_claude`, `emit_codex`, and `emit_pi` are the same shape keyed on
//! `Harness`, plus the helpers that build one emitted file.

// Tightly coupled to its siblings by construction: this is one compiler
// split by concern, not four independent modules.
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 {
        // Keyed on `dispatchable`, which is what the field MEANS, not on the
        // model hint, which merely correlated with it for one role. The proxy
        // was wrong in both directions: `planter` is `dispatchable: false` and
        // was listed here anyway because its hint is `reasoning-high`, so Codex
        // advertised the operator-escalation role as a spawnable agent type;
        // and any lead that adopts `inherit-caller` would have silently
        // vanished from this table.
        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,
    })
}