cargo-governor 2.0.3

Machine-First, LLM-Ready, CI/CD-Native release automation tool for Rust crates
Documentation
//! Agent-facing commands.

use governor_application::agent::AgentContext;
use serde_json::json;
use std::fs;
use std::path::Path;

use crate::cli::{ContextOpts, ExportDocsOpts};
use crate::error::{CommandExitCode, Result};
use crate::presenter::print_json;

fn push_wrapped_bullet(output: &mut String, text: &str) {
    const WIDTH: usize = 78;
    let mut line = String::from("- ");

    for word in text.split_whitespace() {
        let separator = if line == "- " { "" } else { " " };
        if line.len() + separator.len() + word.len() > WIDTH {
            output.push_str(&line);
            output.push('\n');
            line = format!("  {word}");
        } else {
            line.push_str(separator);
            line.push_str(word);
        }
    }

    output.push_str(&line);
    output.push('\n');
}

pub fn context(opts: &ContextOpts) -> Result<CommandExitCode> {
    let started_at = std::time::Instant::now();
    let context = AgentContext::cargo_governor();
    match opts.format.as_deref() {
        Some("markdown") => {
            println!("{}", context.to_markdown());
        }
        _ => {
            print_json(
                "agent.context",
                Some("cargo-governor".to_string()),
                true,
                context,
                started_at,
                Vec::new(),
            )?;
        }
    }
    Ok(CommandExitCode::Success)
}

fn render_agents_md(context: &AgentContext) -> String {
    let mut output = String::new();
    output.push_str("# cargo-governor Agent Guide\n\n");
    output.push_str(
        "This repository uses `cargo-governor` for release automation and owner management.\n\n",
    );
    output.push_str(&context.to_markdown());
    output.push_str("\n## Exit codes\n");
    for code in &context.exit_codes {
        push_wrapped_bullet(
            &mut output,
            &format!("`{}` (`{}`): {}", code.code, code.name, code.meaning),
        );
    }
    output.push_str("\n## Files\n");
    push_wrapped_bullet(&mut output, "`llms.txt`: short machine-readable overview.");
    push_wrapped_bullet(
        &mut output,
        "`llms-full.txt`: extended machine-readable context.",
    );
    push_wrapped_bullet(
        &mut output,
        "`docs/agent-release-runbook.md`: operator runbook for releases.",
    );
    push_wrapped_bullet(
        &mut output,
        "`schemas/*.schema.json`: stable JSON contracts.",
    );
    output
}

fn render_llms_txt(context: &AgentContext) -> String {
    let safe = context
        .safe_commands
        .iter()
        .map(|command| format!("- {}", command.command))
        .collect::<Vec<_>>()
        .join("\n");
    format!(
        "# cargo-governor\n\nShort context for coding agents.\n\n## Detect\n- cargo-governor binary crate in `crates/`\n- `workspace.metadata.governor` in root `Cargo.toml`\n\n## Safe Commands\n{safe}\n\n## References\n- AGENTS.md\n- llms-full.txt\n- docs/agent-release-runbook.md\n- schemas/agent-context.schema.json\n- schemas/cli-envelope.schema.json\n- schemas/mcp-tools.schema.json\n"
    )
}

fn render_llms_full_txt(context: &AgentContext) -> String {
    let json_context = serde_json::to_string_pretty(context).unwrap_or_else(|_| "{}".to_string());
    format!(
        "# cargo-governor Full Agent Context\n\n{}\n\n## JSON Context\n```json\n{}\n```\n",
        context.to_markdown(),
        json_context
    )
}

fn render_llm_txt() -> String {
    "Deprecated compatibility file. Use `llms.txt` for the short overview and `llms-full.txt` for the extended context.\n".to_string()
}

fn render_runbook(context: &AgentContext) -> String {
    let mut output = String::new();
    output.push_str("# Agent Release Runbook\n\n");
    output.push_str("Use this runbook when an agent needs to inspect or execute a\n");
    output.push_str("release with cargo-governor.\n\n");
    output.push_str("## Recommended flow\n");
    for step in &context.dry_run_workflow {
        push_wrapped_bullet(&mut output, step);
    }
    output.push_str("\n## Mutating commands\n");
    for command in &context.gated_commands {
        push_wrapped_bullet(
            &mut output,
            &format!("`{}`: {}", command.command, command.description),
        );
    }
    output.push_str("\n## Safety rules\n");
    for rule in &context.release_policy {
        push_wrapped_bullet(&mut output, rule);
    }
    for rule in &context.owners_policy {
        push_wrapped_bullet(&mut output, rule);
    }
    output
}

fn agent_context_schema() -> serde_json::Value {
    json!({
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "$id": "https://cargo-governor.dev/schemas/agent-context.schema.json",
        "title": "cargo-governor agent context",
        "type": "object",
        "required": [
            "schema_version",
            "repository",
            "release_tool",
            "detection_hints",
            "safe_commands",
            "gated_commands",
            "release_policy",
            "owners_policy",
            "dry_run_workflow",
            "exit_codes",
            "schema_paths"
        ],
        "properties": {
            "schema_version": { "type": "integer", "const": 2 },
            "repository": { "type": "string" },
            "release_tool": { "type": "string" },
            "detection_hints": { "type": "array", "items": { "type": "string" } },
            "safe_commands": {
                "type": "array",
                "items": {
                    "type": "object",
                    "required": ["command", "description", "safety"],
                    "properties": {
                        "command": { "type": "string" },
                        "description": { "type": "string" },
                        "safety": { "type": "string" }
                    }
                }
            },
            "gated_commands": {
                "type": "array",
                "items": {
                    "type": "object",
                    "required": ["command", "description", "safety"],
                    "properties": {
                        "command": { "type": "string" },
                        "description": { "type": "string" },
                        "safety": { "type": "string" }
                    }
                }
            },
            "release_policy": { "type": "array", "items": { "type": "string" } },
            "owners_policy": { "type": "array", "items": { "type": "string" } },
            "dry_run_workflow": { "type": "array", "items": { "type": "string" } },
            "exit_codes": {
                "type": "array",
                "items": {
                    "type": "object",
                    "required": ["code", "name", "meaning"],
                    "properties": {
                        "code": { "type": "integer" },
                        "name": { "type": "string" },
                        "meaning": { "type": "string" }
                    }
                }
            },
            "schema_paths": { "type": "array", "items": { "type": "string" } }
        }
    })
}

fn cli_envelope_schema() -> serde_json::Value {
    json!({
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "$id": "https://cargo-governor.dev/schemas/cli-envelope.schema.json",
        "title": "cargo-governor CLI envelope",
        "type": "object",
        "required": ["schema_version", "success", "command", "data", "errors", "metrics"],
        "properties": {
            "schema_version": { "type": "integer", "const": 2 },
            "success": { "type": "boolean" },
            "command": { "type": "string" },
            "workspace": { "type": ["string", "null"] },
            "data": {},
            "errors": { "type": "array", "items": { "type": "string" } },
            "metrics": {
                "type": "object",
                "required": ["execution_time_ms"],
                "properties": {
                    "execution_time_ms": { "type": "integer", "minimum": 0 }
                }
            }
        }
    })
}

fn mcp_tools_schema() -> serde_json::Value {
    let tool_names = crate::commands::mcp::tools(false)
        .into_iter()
        .map(|tool| tool.name)
        .collect::<Vec<_>>();
    json!({
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "$id": "https://cargo-governor.dev/schemas/mcp-tools.schema.json",
        "title": "cargo-governor MCP tools",
        "type": "object",
        "required": ["protocol", "read_only_tools", "dry_run_tools"],
        "properties": {
            "protocol": { "type": "string", "const": "stdio-mcp" },
            "read_only_tools": { "type": "array", "items": { "type": "string" } },
            "dry_run_tools": { "type": "array", "items": { "type": "string" } },
            "current_tools": {
                "type": "array",
                "items": { "type": "string", "enum": tool_names }
            }
        }
    })
}

fn write_pretty_json(path: &Path, value: &serde_json::Value) -> Result<()> {
    fs::write(
        path,
        serde_json::to_string_pretty(value)
            .map_err(|error| crate::error::Error::Serialization(error.to_string()))?,
    )?;
    Ok(())
}

pub fn export_docs(opts: &ExportDocsOpts) -> Result<CommandExitCode> {
    let context = AgentContext::cargo_governor();
    let root = Path::new(&opts.output_dir);
    let docs_dir = root.join("docs");
    let schema_dir = root.join("schemas");

    fs::create_dir_all(&docs_dir)?;
    fs::create_dir_all(&schema_dir)?;

    fs::write(root.join("AGENTS.md"), render_agents_md(&context))?;
    fs::write(root.join("llms.txt"), render_llms_txt(&context))?;
    fs::write(root.join("llms-full.txt"), render_llms_full_txt(&context))?;
    fs::write(root.join("llm.txt"), render_llm_txt())?;
    fs::write(
        docs_dir.join("agent-release-runbook.md"),
        render_runbook(&context),
    )?;
    write_pretty_json(
        &schema_dir.join("agent-context.schema.json"),
        &agent_context_schema(),
    )?;
    write_pretty_json(
        &schema_dir.join("cli-envelope.schema.json"),
        &cli_envelope_schema(),
    )?;
    write_pretty_json(
        &schema_dir.join("mcp-tools.schema.json"),
        &mcp_tools_schema(),
    )?;

    Ok(CommandExitCode::Success)
}