Skip to main content

atomr_agents_coding_cli_vendor_codex/
mapper.rs

1//! One-way projection: atomr concepts → Codex on-disk config.
2
3use std::path::Path;
4
5use tokio::fs;
6
7use atomr_agents_coding_cli_core::{
8    ConceptProjection, MapperError, PersonaSnapshot, SkillSnapshot, ToolSetSnapshot,
9};
10
11pub async fn materialize(projection: &ConceptProjection, workdir: &Path) -> Result<(), MapperError> {
12    write_agents_md(projection, workdir).await?;
13    write_mcp_toml(&projection.toolsets, workdir).await?;
14    Ok(())
15}
16
17async fn write_agents_md(p: &ConceptProjection, workdir: &Path) -> Result<(), MapperError> {
18    let body = compose_agents_md(p.persona.as_ref(), p.project_memory.as_deref(), &p.skills);
19    let path = workdir.join("AGENTS.md");
20    write_file(&path, body.as_bytes()).await
21}
22
23fn compose_agents_md(
24    persona: Option<&PersonaSnapshot>,
25    project_memory: Option<&str>,
26    skills: &[SkillSnapshot],
27) -> String {
28    let mut out = String::new();
29    out.push_str("# AGENTS.md\n\n");
30    out.push_str("<!-- Generated by atomr-agents-coding-cli-harness. Do not edit by hand. -->\n\n");
31    if let Some(p) = persona {
32        out.push_str("## Identity\n\n");
33        out.push_str(&p.identity);
34        if !p.identity.ends_with('\n') {
35            out.push('\n');
36        }
37        if !p.salient_traits.is_empty() {
38            out.push_str("\n### Salient traits\n\n");
39            for t in &p.salient_traits {
40                out.push_str("- ");
41                out.push_str(t);
42                out.push('\n');
43            }
44        }
45        if let Some(tone) = &p.style_tone {
46            out.push_str("\n### Style / tone\n\n");
47            out.push_str(tone);
48            out.push('\n');
49        }
50        out.push('\n');
51    }
52    if let Some(mem) = project_memory {
53        out.push_str("## Project memory\n\n");
54        out.push_str(mem);
55        if !mem.ends_with('\n') {
56            out.push('\n');
57        }
58        out.push('\n');
59    }
60    if !skills.is_empty() {
61        out.push_str("## Skills\n\n");
62        out.push_str("Codex lacks a native skill registry, so each skill is inlined below.\n\n");
63        for s in skills {
64            out.push_str(&format!("### {} (priority {})\n\n", s.name, s.priority));
65            if !s.keywords.is_empty() {
66                out.push_str("Keywords: ");
67                out.push_str(&s.keywords.join(", "));
68                out.push_str("\n\n");
69            }
70            if !s.tools.is_empty() {
71                out.push_str("Tools: ");
72                out.push_str(&s.tools.join(", "));
73                out.push_str("\n\n");
74            }
75            out.push_str(&s.instruction_fragment);
76            if !s.instruction_fragment.ends_with('\n') {
77                out.push('\n');
78            }
79            out.push('\n');
80        }
81    }
82    out
83}
84
85async fn write_mcp_toml(toolsets: &[ToolSetSnapshot], workdir: &Path) -> Result<(), MapperError> {
86    if toolsets.iter().all(|t| t.mcp_servers.is_empty()) {
87        return Ok(());
88    }
89    let dir = workdir.join(".codex");
90    fs::create_dir_all(&dir)
91        .await
92        .map_err(|e| MapperError::io(dir.display().to_string(), e))?;
93    let mut toml = String::from("# Generated by atomr-agents-coding-cli-harness.\n");
94    for ts in toolsets {
95        for s in &ts.mcp_servers {
96            toml.push_str(&format!("\n[mcp_servers.{}]\n", s.name));
97            toml.push_str(&format!("command = {:?}\n", s.command));
98            let args = s
99                .args
100                .iter()
101                .map(|a| format!("{a:?}"))
102                .collect::<Vec<_>>()
103                .join(", ");
104            toml.push_str(&format!("args = [{args}]\n"));
105            if !s.env.is_empty() {
106                toml.push_str("env = {\n");
107                for (k, v) in &s.env {
108                    toml.push_str(&format!("  {k:?} = {v:?},\n"));
109                }
110                toml.push_str("}\n");
111            }
112        }
113    }
114    let path = dir.join("config.toml");
115    write_file(&path, toml.as_bytes()).await
116}
117
118async fn write_file(path: &Path, body: &[u8]) -> Result<(), MapperError> {
119    if let Some(parent) = path.parent() {
120        fs::create_dir_all(parent)
121            .await
122            .map_err(|e| MapperError::io(parent.display().to_string(), e))?;
123    }
124    fs::write(path, body)
125        .await
126        .map_err(|e| MapperError::io(path.display().to_string(), e))
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132    use atomr_agents_coding_cli_core::{
133        McpServerSnapshot, PersonaSnapshot, SkillSnapshot, ToolSetSnapshot,
134    };
135    use tempfile::TempDir;
136
137    #[tokio::test]
138    async fn materializes_agents_md_and_mcp() {
139        let dir = TempDir::new().unwrap();
140        let p = ConceptProjection {
141            persona: Some(PersonaSnapshot {
142                identity: "Codex backend hardener.".into(),
143                salient_traits: vec!["paranoid".into()],
144                style_tone: None,
145            }),
146            skills: vec![SkillSnapshot {
147                id: "audit".into(),
148                name: "Audit".into(),
149                instruction_fragment: "Look for unsafe blocks.".into(),
150                keywords: vec!["safety".into()],
151                priority: 9,
152                tools: vec!["Grep".into()],
153            }],
154            toolsets: vec![ToolSetSnapshot {
155                id: "linear".into(),
156                mcp_servers: vec![McpServerSnapshot {
157                    name: "linear".into(),
158                    command: "npx".into(),
159                    args: vec!["@linear/mcp".into()],
160                    env: Default::default(),
161                }],
162                tool_names: vec![],
163            }],
164            policy: Default::default(),
165            project_memory: Some("Shard by tenant id.".into()),
166        };
167        materialize(&p, dir.path()).await.unwrap();
168        let agents = std::fs::read_to_string(dir.path().join("AGENTS.md")).unwrap();
169        assert!(agents.contains("Codex backend hardener"));
170        assert!(agents.contains("### Audit (priority 9)"));
171        assert!(agents.contains("Shard by tenant id"));
172        let toml = std::fs::read_to_string(dir.path().join(".codex/config.toml")).unwrap();
173        assert!(toml.contains("[mcp_servers.linear]"));
174        assert!(toml.contains("command = \"npx\""));
175    }
176}