cellstate_pipeline/pack/
agents_md.rs

1//! AGENTS.md generation from Pack configuration.
2//!
3//! Generates an AGENTS.md file compatible with the open standard (60k+ repos).
4//! AGENTS.md provides project-specific guidance for AI coding agents.
5//!
6//! Re-export path: cellstate_pipeline::pack::agents_md::*
7
8use crate::compiler::CompiledConfig;
9
10/// CELLSTATE schema version comment embedded in emitted AGENTS.md.
11pub const AGENTS_MD_SCHEMA_VERSION: &str = "cellstate.agents_md.v1";
12
13/// Generate an AGENTS.md string from a compiled CELLSTATE config.
14///
15/// The output follows the AGENTS.md convention used by Codex, Cursor,
16/// GitHub Copilot, Gemini CLI, and 60k+ open source projects.
17pub fn generate_agents_md(config: &CompiledConfig) -> String {
18    let mut out = String::new();
19
20    // Header
21    let project_name = config
22        .pack_meta_project
23        .as_deref()
24        .unwrap_or("CELLSTATE Agent Pack");
25    out.push_str(&format!("# {}\n\n", project_name));
26    out.push_str(&format!(
27        "<!-- schema_version: {} -->\n\n",
28        AGENTS_MD_SCHEMA_VERSION
29    ));
30
31    // Description
32    if let Some(ref desc) = config.pack_meta_description {
33        out.push_str(&format!("{}\n\n", desc));
34    }
35
36    // Agents section
37    if !config.pack_agents.is_empty() {
38        out.push_str("## Agents\n\n");
39        out.push_str("This project defines the following AI agents:\n\n");
40        for agent in &config.pack_agents {
41            if !agent.enabled {
42                continue;
43            }
44            let desc = agent
45                .description
46                .as_deref()
47                .unwrap_or("No description provided");
48            out.push_str(&format!("### {}\n\n", agent.name));
49            out.push_str(&format!("{}\n\n", desc));
50            out.push_str(&format!("- **Profile:** {}\n", agent.profile));
51            if let Some(budget) = agent.token_budget {
52                out.push_str(&format!("- **Token budget:** {}\n", budget));
53            }
54            if !agent.toolsets.is_empty() {
55                out.push_str(&format!("- **Toolsets:** {}\n", agent.toolsets.join(", ")));
56            }
57            if !agent.tags.is_empty() {
58                out.push_str(&format!("- **Tags:** {}\n", agent.tags.join(", ")));
59            }
60            out.push('\n');
61        }
62    }
63
64    // Tools section
65    if !config.tools.is_empty() {
66        out.push_str("## Tools\n\n");
67        out.push_str("Available tools in this pack:\n\n");
68        for tool in &config.tools {
69            out.push_str(&format!("- `{}` — {}\n", tool.id, tool_description(tool)));
70        }
71        out.push('\n');
72    }
73
74    // Toolsets section
75    if !config.toolsets.is_empty() {
76        out.push_str("## Toolsets\n\n");
77        for toolset in &config.toolsets {
78            out.push_str(&format!(
79                "- **{}:** {}\n",
80                toolset.name,
81                toolset.tools.join(", ")
82            ));
83        }
84        out.push('\n');
85    }
86
87    // Instructions section
88    if let Some(ref instructions) = config.pack_meta_instructions {
89        out.push_str("## Instructions\n\n");
90        out.push_str(instructions);
91        if !instructions.ends_with('\n') {
92            out.push('\n');
93        }
94        out.push('\n');
95    }
96
97    // Memory configuration section
98    if !config.pack_injections.is_empty() {
99        out.push_str("## Memory Configuration\n\n");
100        out.push_str("Context injection rules:\n\n");
101        for injection in &config.pack_injections {
102            out.push_str(&format!(
103                "- **{}** → {} (priority: {}, mode: {:?})\n",
104                injection.source, injection.target, injection.priority, injection.mode,
105            ));
106        }
107        out.push('\n');
108    }
109
110    // Footer
111    out.push_str("---\n\n");
112    out.push_str(
113        "*Generated by CELLSTATE Pack. See [cellstate.batterypack.dev](https://cellstate.batterypack.dev) for details.*\n",
114    );
115
116    out
117}
118
119/// Extract a human-readable description from a compiled tool config.
120fn tool_description(tool: &crate::compiler::CompiledToolConfig) -> String {
121    match tool.kind {
122        crate::compiler::CompiledToolKind::Exec => {
123            format!(
124                "Executable: {}",
125                tool.cmd.as_deref().unwrap_or("unknown command")
126            )
127        }
128        crate::compiler::CompiledToolKind::WasmExec => {
129            format!(
130                "WASM executable: {}",
131                tool.cmd.as_deref().unwrap_or("unknown module")
132            )
133        }
134        crate::compiler::CompiledToolKind::Prompt => {
135            format!(
136                "Prompt tool ({})",
137                tool.prompt_md.as_deref().unwrap_or("unknown")
138            )
139        }
140        crate::compiler::CompiledToolKind::Bash => "Sandboxed bash execution".to_string(),
141        crate::compiler::CompiledToolKind::Browser => {
142            "Accessibility-first browser automation".to_string()
143        }
144        crate::compiler::CompiledToolKind::Composio => {
145            format!(
146                "Composio integration ({})",
147                tool.composio_toolkit.as_deref().unwrap_or("unknown")
148            )
149        }
150        crate::compiler::CompiledToolKind::ComposioGateway => {
151            "Composio tool discovery gateway".to_string()
152        }
153    }
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159
160    #[test]
161    fn test_generate_agents_md_empty_config() {
162        let config = CompiledConfig::default();
163        let md = generate_agents_md(&config);
164        assert!(md.contains("CELLSTATE Agent Pack"));
165        assert!(md.contains("schema_version: cellstate.agents_md.v1"));
166        assert!(md.contains("Generated by CELLSTATE Pack"));
167    }
168
169    #[test]
170    fn test_generate_agents_md_with_project_name() {
171        let config = CompiledConfig {
172            pack_meta_project: Some("My Research Bot".to_string()),
173            pack_meta_description: Some("A bot that researches things".to_string()),
174            ..CompiledConfig::default()
175        };
176        let md = generate_agents_md(&config);
177        assert!(md.starts_with("# My Research Bot"));
178        assert!(md.contains("A bot that researches things"));
179    }
180}