use serde::Serialize;
#[derive(Debug, Clone, Copy, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum CommandSafety {
Safe,
Mutating,
Gated,
}
#[derive(Debug, Clone, Serialize)]
pub struct AgentCommand {
pub command: String,
pub description: String,
pub safety: CommandSafety,
}
#[derive(Debug, Clone, Serialize)]
pub struct ExitCodeInfo {
pub code: u8,
pub name: String,
pub meaning: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct AgentContext {
pub schema_version: u32,
pub repository: String,
pub release_tool: String,
pub detection_hints: Vec<String>,
pub safe_commands: Vec<AgentCommand>,
pub gated_commands: Vec<AgentCommand>,
pub release_policy: Vec<String>,
pub owners_policy: Vec<String>,
pub dry_run_workflow: Vec<String>,
pub exit_codes: Vec<ExitCodeInfo>,
pub schema_paths: Vec<String>,
}
impl AgentContext {
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');
}
fn detection_hints() -> Vec<String> {
vec![
"Look for a `cargo-governor` binary crate in `crates/`.".to_string(),
"Look for `workspace.metadata.governor` in the root `Cargo.toml`.".to_string(),
"Look for repo docs mentioning `cargo-governor release ...`.".to_string(),
]
}
fn safe_commands() -> Vec<AgentCommand> {
vec![
AgentCommand {
command: "cargo-governor agent context --format json".to_string(),
description: "Load machine-readable release and safety context.".to_string(),
safety: CommandSafety::Safe,
},
AgentCommand {
command: "cargo-governor release analyze --format json".to_string(),
description: "Analyze commits and proposed semantic version bump.".to_string(),
safety: CommandSafety::Safe,
},
AgentCommand {
command: "cargo-governor --dry-run release plan --format json".to_string(),
description: "Inspect publication order and already-published crates.".to_string(),
safety: CommandSafety::Safe,
},
AgentCommand {
command: "cargo-governor release status --format json".to_string(),
description: "Read branch, tag and workspace release status.".to_string(),
safety: CommandSafety::Safe,
},
AgentCommand {
command: "cargo-governor --dry-run release check --format json".to_string(),
description: "Run release checks without mutating the workspace.".to_string(),
safety: CommandSafety::Safe,
},
]
}
fn gated_commands() -> Vec<AgentCommand> {
vec![
AgentCommand {
command: "cargo-governor --dry-run release bump --format json".to_string(),
description: "Preview version/changelog/git effects before mutation.".to_string(),
safety: CommandSafety::Gated,
},
AgentCommand {
command: "cargo-governor --dry-run release publish --format json".to_string(),
description: "Preview publish decisions and registry skips.".to_string(),
safety: CommandSafety::Gated,
},
AgentCommand {
command: "cargo-governor --dry-run release full --format json".to_string(),
description: "Preview the end-to-end release workflow.".to_string(),
safety: CommandSafety::Gated,
},
AgentCommand {
command: "cargo-governor owners sync --dry-run".to_string(),
description: "Preview crates.io owner changes before applying them.".to_string(),
safety: CommandSafety::Gated,
},
]
}
fn release_policy() -> Vec<String> {
vec![
"Normal UX: make code changes, commit them with Conventional Commits, then let `cargo-governor release full` do the version bump, changelog, tag, and publish."
.to_string(),
"Do not pre-bump versions in `Cargo.toml`; cargo-governor expects the workspace version to match the last release tag before a new release starts."
.to_string(),
"Mutating release commands expect a clean working tree so the release commit only contains cargo-governor-managed changes."
.to_string(),
"Prefer `analyze`, `plan`, `status`, and `check` before any mutating release step."
.to_string(),
"Use global `--dry-run` for release commands when an agent is exploring impact."
.to_string(),
"Treat `release bump`, `release publish`, `release full`, and `owners sync` as mutating commands.".to_string(),
"Interpret non-zero exit codes as policy failures, not only process crashes."
.to_string(),
]
}
fn owners_policy() -> Vec<String> {
vec![
"Resolve owners from workspace and package metadata before checking crates.io."
.to_string(),
"Use `owners show` or `owners check` before `owners sync`.".to_string(),
"Treat `owners sync` as gated even in automated environments.".to_string(),
]
}
fn dry_run_workflow() -> Vec<String> {
vec![
"Run `cargo-governor agent context --format json` to discover policies and schemas."
.to_string(),
"Run `cargo-governor release status --format json` to confirm the tree is clean and the workspace version still matches the last release tag."
.to_string(),
"Run `cargo-governor release analyze --format json` to determine semantic impact."
.to_string(),
"Run `cargo-governor --dry-run release plan --format json` to inspect order and skips."
.to_string(),
"Run `cargo-governor --dry-run release full --format json` before any mutating release."
.to_string(),
]
}
fn exit_codes() -> Vec<ExitCodeInfo> {
vec![
ExitCodeInfo {
code: 0,
name: "success".to_string(),
meaning: "Command completed successfully.".to_string(),
},
ExitCodeInfo {
code: 2,
name: "invalid_arguments".to_string(),
meaning: "CLI or policy arguments were invalid.".to_string(),
},
ExitCodeInfo {
code: 10,
name: "git_error".to_string(),
meaning: "Source control state blocked the workflow.".to_string(),
},
ExitCodeInfo {
code: 11,
name: "registry_error".to_string(),
meaning: "Registry access or publish checks failed.".to_string(),
},
ExitCodeInfo {
code: 13,
name: "check_failed".to_string(),
meaning: "Pre-release checks failed.".to_string(),
},
ExitCodeInfo {
code: 20,
name: "partial_success".to_string(),
meaning: "The workflow completed with partial failures.".to_string(),
},
ExitCodeInfo {
code: 32,
name: "drift_detected".to_string(),
meaning: "Owner drift was detected and requires action.".to_string(),
},
]
}
#[must_use]
pub fn cargo_governor() -> Self {
Self {
schema_version: 2,
repository: "cargo-governor".to_string(),
release_tool: "cargo-governor".to_string(),
detection_hints: Self::detection_hints(),
safe_commands: Self::safe_commands(),
gated_commands: Self::gated_commands(),
release_policy: Self::release_policy(),
owners_policy: Self::owners_policy(),
dry_run_workflow: Self::dry_run_workflow(),
exit_codes: Self::exit_codes(),
schema_paths: vec![
"schemas/agent-context.schema.json".to_string(),
"schemas/cli-envelope.schema.json".to_string(),
"schemas/mcp-tools.schema.json".to_string(),
],
}
}
#[must_use]
pub fn to_markdown(&self) -> String {
let mut output = String::new();
output.push_str("## Agent Context\n\n");
output.push_str("Use `cargo-governor` for release and owners workflows.\n\n");
output.push_str("## Detect\n");
for hint in &self.detection_hints {
Self::push_wrapped_bullet(&mut output, hint);
}
output.push_str("\n## Safe commands\n");
for command in &self.safe_commands {
Self::push_wrapped_bullet(
&mut output,
&format!("`{}`: {}", command.command, command.description),
);
}
output.push_str("\n## Gated commands\n");
for command in &self.gated_commands {
Self::push_wrapped_bullet(
&mut output,
&format!("`{}`: {}", command.command, command.description),
);
}
output.push_str("\n## Release policy\n");
for rule in &self.release_policy {
Self::push_wrapped_bullet(&mut output, rule);
}
output.push_str("\n## Owners policy\n");
for rule in &self.owners_policy {
Self::push_wrapped_bullet(&mut output, rule);
}
output.push_str("\n## Dry-run workflow\n");
for step in &self.dry_run_workflow {
Self::push_wrapped_bullet(&mut output, step);
}
output.push_str("\n## Schemas\n");
for path in &self.schema_paths {
Self::push_wrapped_bullet(&mut output, &format!("`{path}`"));
}
output
}
}