govctl 0.9.5

Project governance CLI for RFC, ADR, and Work Item management
use serde::{Deserialize, Serialize};
use std::error::Error;
use std::fs;
use std::path::Path;

/// YAML frontmatter from a Claude Code agent .md file.
#[derive(Debug, Deserialize)]
struct AgentFrontmatter {
    name: String,
    description: String,
}

/// Codex agent role TOML structure.
#[derive(Debug, Serialize)]
struct CodexAgentRole {
    name: String,
    description: String,
    developer_instructions: String,
}

/// Agent source files to convert (relative to repo root).
const AGENT_SOURCES: &[&str] = &[
    ".claude/agents/rfc-reviewer.md",
    ".claude/agents/adr-reviewer.md",
    ".claude/agents/wi-reviewer.md",
    ".claude/agents/compliance-checker.md",
];

pub fn generate_codex_agent_templates() -> Result<(), Box<dyn Error>> {
    let mut entries = Vec::new();

    for source in AGENT_SOURCES {
        let content =
            fs::read_to_string(source).map_err(|e| format!("failed to read {source}: {e}"))?;

        let (frontmatter, body) = parse_agent_frontmatter(&content)
            .map_err(|e| format!("failed to parse frontmatter in {source}: {e}"))?;

        let role = CodexAgentRole {
            name: frontmatter.name.clone(),
            description: frontmatter.description.clone(),
            developer_instructions: body.to_string(),
        };

        let toml_content = toml::to_string_pretty(&role)
            .map_err(|e| format!("failed to serialize codex TOML for {source}: {e}"))?;

        let out_filename = format!("agents/{}.toml", frontmatter.name);
        entries.push((out_filename, toml_content));
    }

    // Generate Rust source with the TOML strings as constants
    let mut out = String::new();
    out.push_str("// @generated by build.rs from .claude/agents/*.md\n");
    out.push_str("// Do not edit manually.\n\n");
    out.push_str("pub const AGENT_TEMPLATES_CODEX: &[(&str, &str)] = &[\n");
    for (path, content) in &entries {
        out.push_str(&format!("    ({:?}, {:?}),\n", path, content));
    }
    out.push_str("];\n");

    let out_dir = std::env::var("OUT_DIR")?;
    let out_path = Path::new(&out_dir).join("agent_codex_templates.rs");
    fs::write(out_path, out)?;
    Ok(())
}

/// Parse `---` delimited YAML frontmatter from a markdown file.
/// Returns (parsed frontmatter, body after second `---`).
fn parse_agent_frontmatter(content: &str) -> Result<(AgentFrontmatter, &str), Box<dyn Error>> {
    let content = content.strip_prefix("---").ok_or("missing opening ---")?;
    let (yaml_block, rest) = content.split_once("---").ok_or("missing closing ---")?;
    let fm: AgentFrontmatter = serde_yaml::from_str(yaml_block.trim())?;
    let body = rest.trim_start_matches(['\n', '\r']);
    Ok((fm, body))
}