atomr-agents-coding-cli-vendor-gemini 0.16.3

Google Gemini CLI adapter for the coding-cli harness: stream-json parser + system-instruction projection.
Documentation
//! Gemini lacks a native skill registry. Persona, skills, and project
//! memory are concatenated into a single system-instructions file.

use std::collections::BTreeMap;
use std::path::Path;

use serde_json::json;
use tokio::fs;

use atomr_agents_coding_cli_core::{
    ConceptProjection, MapperError, PersonaSnapshot, SkillSnapshot, ToolSetSnapshot,
};

pub async fn materialize(projection: &ConceptProjection, workdir: &Path) -> Result<(), MapperError> {
    write_system_instructions(projection, workdir).await?;
    write_settings(&projection.toolsets, workdir).await?;
    Ok(())
}

async fn write_system_instructions(
    p: &ConceptProjection,
    workdir: &Path,
) -> Result<(), MapperError> {
    let body = compose(p.persona.as_ref(), p.project_memory.as_deref(), &p.skills);
    let dir = workdir.join(".gemini");
    fs::create_dir_all(&dir)
        .await
        .map_err(|e| MapperError::io(dir.display().to_string(), e))?;
    let path = dir.join("system_instructions.md");
    fs::write(&path, body.as_bytes())
        .await
        .map_err(|e| MapperError::io(path.display().to_string(), e))
}

fn compose(
    persona: Option<&PersonaSnapshot>,
    project_memory: Option<&str>,
    skills: &[SkillSnapshot],
) -> String {
    let mut out = String::new();
    out.push_str("# System instructions\n\n");
    out.push_str(
        "<!-- Generated by atomr-agents-coding-cli-harness. Do not edit by hand. -->\n\n",
    );
    if let Some(p) = persona {
        out.push_str("## Identity\n\n");
        out.push_str(&p.identity);
        if !p.identity.ends_with('\n') {
            out.push('\n');
        }
        if !p.salient_traits.is_empty() {
            out.push_str("\n### Salient traits\n\n");
            for t in &p.salient_traits {
                out.push_str("- ");
                out.push_str(t);
                out.push('\n');
            }
        }
        if let Some(tone) = &p.style_tone {
            out.push_str("\n### Style / tone\n\n");
            out.push_str(tone);
            out.push('\n');
        }
        out.push('\n');
    }
    if let Some(mem) = project_memory {
        out.push_str("## Project memory\n\n");
        out.push_str(mem);
        if !mem.ends_with('\n') {
            out.push('\n');
        }
        out.push('\n');
    }
    if !skills.is_empty() {
        out.push_str("## Skills\n\n");
        for s in skills {
            out.push_str(&format!("### {} (priority {})\n\n", s.name, s.priority));
            out.push_str(&s.instruction_fragment);
            if !s.instruction_fragment.ends_with('\n') {
                out.push('\n');
            }
            out.push('\n');
        }
    }
    out
}

async fn write_settings(toolsets: &[ToolSetSnapshot], workdir: &Path) -> Result<(), MapperError> {
    if toolsets.iter().all(|t| t.mcp_servers.is_empty()) {
        return Ok(());
    }
    let dir = workdir.join(".gemini");
    fs::create_dir_all(&dir)
        .await
        .map_err(|e| MapperError::io(dir.display().to_string(), e))?;
    let mut servers = BTreeMap::new();
    for ts in toolsets {
        for s in &ts.mcp_servers {
            servers.insert(
                s.name.clone(),
                json!({"command": s.command, "args": s.args, "env": s.env}),
            );
        }
    }
    let body = json!({ "mcpServers": servers });
    let pretty = serde_json::to_vec_pretty(&body)?;
    let path = dir.join("settings.json");
    fs::write(&path, &pretty)
        .await
        .map_err(|e| MapperError::io(path.display().to_string(), e))
}

#[cfg(test)]
mod tests {
    use super::*;
    use atomr_agents_coding_cli_core::{
        McpServerSnapshot, PersonaSnapshot, SkillSnapshot, ToolSetSnapshot,
    };
    use tempfile::TempDir;

    #[tokio::test]
    async fn materializes_system_instructions_and_settings() {
        let dir = TempDir::new().unwrap();
        let p = ConceptProjection {
            persona: Some(PersonaSnapshot {
                identity: "Web hardener.".into(),
                salient_traits: vec![],
                style_tone: None,
            }),
            skills: vec![SkillSnapshot {
                id: "review".into(),
                name: "Review".into(),
                instruction_fragment: "Look for XSS.".into(),
                keywords: vec![],
                priority: 5,
                tools: vec![],
            }],
            toolsets: vec![ToolSetSnapshot {
                id: "mcp".into(),
                mcp_servers: vec![McpServerSnapshot {
                    name: "linear".into(),
                    command: "npx".into(),
                    args: vec![],
                    env: Default::default(),
                }],
                tool_names: vec![],
            }],
            policy: Default::default(),
            project_memory: None,
        };
        materialize(&p, dir.path()).await.unwrap();
        let body = std::fs::read_to_string(dir.path().join(".gemini/system_instructions.md")).unwrap();
        assert!(body.contains("# System instructions"));
        assert!(body.contains("Web hardener"));
        assert!(body.contains("### Review (priority 5)"));
        let settings = std::fs::read_to_string(dir.path().join(".gemini/settings.json")).unwrap();
        assert!(settings.contains("\"linear\""));
    }
}