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)
}