Skip to main content

governor_application/
agent.rs

1//! Agent-facing repository context.
2
3use serde::Serialize;
4
5/// Safe or mutating command class.
6#[derive(Debug, Clone, Copy, Serialize)]
7#[serde(rename_all = "snake_case")]
8pub enum CommandSafety {
9    /// Read-only command.
10    Safe,
11    /// Mutating command.
12    Mutating,
13    /// Mutating command gated by dry-run or confirmation.
14    Gated,
15}
16
17/// Single command documented for coding agents.
18#[derive(Debug, Clone, Serialize)]
19pub struct AgentCommand {
20    /// Canonical CLI invocation.
21    pub command: String,
22    /// Purpose of the command.
23    pub description: String,
24    /// Safety category.
25    pub safety: CommandSafety,
26}
27
28/// Exit code documentation for agents.
29#[derive(Debug, Clone, Serialize)]
30pub struct ExitCodeInfo {
31    /// Numeric process exit code.
32    pub code: u8,
33    /// Stable symbolic name.
34    pub name: String,
35    /// Operational meaning.
36    pub meaning: String,
37}
38
39/// Machine-readable repository context for coding agents.
40#[derive(Debug, Clone, Serialize)]
41pub struct AgentContext {
42    /// Stable schema version for the context payload.
43    pub schema_version: u32,
44    /// Repository identifier.
45    pub repository: String,
46    /// Tool this repo uses for releases.
47    pub release_tool: String,
48    /// How an agent can detect the tool.
49    pub detection_hints: Vec<String>,
50    /// Read-only commands agents can run freely.
51    pub safe_commands: Vec<AgentCommand>,
52    /// Mutating commands with guidance.
53    pub gated_commands: Vec<AgentCommand>,
54    /// Important release guarantees and caveats.
55    pub release_policy: Vec<String>,
56    /// Important owners-management guarantees and caveats.
57    pub owners_policy: Vec<String>,
58    /// Recommended dry-run workflow for agents.
59    pub dry_run_workflow: Vec<String>,
60    /// Important exit codes.
61    pub exit_codes: Vec<ExitCodeInfo>,
62    /// Schema documents an agent can fetch.
63    pub schema_paths: Vec<String>,
64}
65
66impl AgentContext {
67    fn push_wrapped_bullet(output: &mut String, text: &str) {
68        const WIDTH: usize = 78;
69        let mut line = String::from("- ");
70
71        for word in text.split_whitespace() {
72            let separator = if line == "- " { "" } else { " " };
73            if line.len() + separator.len() + word.len() > WIDTH {
74                output.push_str(&line);
75                output.push('\n');
76                line = format!("  {word}");
77            } else {
78                line.push_str(separator);
79                line.push_str(word);
80            }
81        }
82
83        output.push_str(&line);
84        output.push('\n');
85    }
86
87    fn detection_hints() -> Vec<String> {
88        vec![
89            "Look for a `cargo-governor` binary crate in `crates/`.".to_string(),
90            "Look for `workspace.metadata.governor` in the root `Cargo.toml`.".to_string(),
91            "Look for repo docs mentioning `cargo-governor release ...`.".to_string(),
92        ]
93    }
94
95    fn safe_commands() -> Vec<AgentCommand> {
96        vec![
97            AgentCommand {
98                command: "cargo-governor agent context --format json".to_string(),
99                description: "Load machine-readable release and safety context.".to_string(),
100                safety: CommandSafety::Safe,
101            },
102            AgentCommand {
103                command: "cargo-governor release analyze --format json".to_string(),
104                description: "Analyze commits and proposed semantic version bump.".to_string(),
105                safety: CommandSafety::Safe,
106            },
107            AgentCommand {
108                command: "cargo-governor --dry-run release plan --format json".to_string(),
109                description: "Inspect publication order and already-published crates.".to_string(),
110                safety: CommandSafety::Safe,
111            },
112            AgentCommand {
113                command: "cargo-governor release status --format json".to_string(),
114                description: "Read branch, tag and workspace release status.".to_string(),
115                safety: CommandSafety::Safe,
116            },
117            AgentCommand {
118                command: "cargo-governor --dry-run release check --format json".to_string(),
119                description: "Run release checks without mutating the workspace.".to_string(),
120                safety: CommandSafety::Safe,
121            },
122        ]
123    }
124
125    fn gated_commands() -> Vec<AgentCommand> {
126        vec![
127            AgentCommand {
128                command: "cargo-governor --dry-run release bump --format json".to_string(),
129                description: "Preview version/changelog/git effects before mutation.".to_string(),
130                safety: CommandSafety::Gated,
131            },
132            AgentCommand {
133                command: "cargo-governor --dry-run release publish --format json".to_string(),
134                description: "Preview publish decisions and registry skips.".to_string(),
135                safety: CommandSafety::Gated,
136            },
137            AgentCommand {
138                command: "cargo-governor --dry-run release full --format json".to_string(),
139                description: "Preview the end-to-end release workflow.".to_string(),
140                safety: CommandSafety::Gated,
141            },
142            AgentCommand {
143                command: "cargo-governor owners sync --dry-run".to_string(),
144                description: "Preview crates.io owner changes before applying them.".to_string(),
145                safety: CommandSafety::Gated,
146            },
147        ]
148    }
149
150    fn release_policy() -> Vec<String> {
151        vec![
152            "Normal UX: make code changes, commit them with Conventional Commits, then let `cargo-governor release full` do the version bump, changelog, tag, and publish."
153                .to_string(),
154            "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."
155                .to_string(),
156            "Mutating release commands expect a clean working tree so the release commit only contains cargo-governor-managed changes."
157                .to_string(),
158            "Prefer `analyze`, `plan`, `status`, and `check` before any mutating release step."
159                .to_string(),
160            "Use global `--dry-run` for release commands when an agent is exploring impact."
161                .to_string(),
162            "Treat `release bump`, `release publish`, `release full`, and `owners sync` as mutating commands.".to_string(),
163            "Interpret non-zero exit codes as policy failures, not only process crashes."
164                .to_string(),
165        ]
166    }
167
168    fn owners_policy() -> Vec<String> {
169        vec![
170            "Resolve owners from workspace and package metadata before checking crates.io."
171                .to_string(),
172            "Use `owners show` or `owners check` before `owners sync`.".to_string(),
173            "Treat `owners sync` as gated even in automated environments.".to_string(),
174        ]
175    }
176
177    fn dry_run_workflow() -> Vec<String> {
178        vec![
179            "Run `cargo-governor agent context --format json` to discover policies and schemas."
180                .to_string(),
181            "Run `cargo-governor release status --format json` to confirm the tree is clean and the workspace version still matches the last release tag."
182                .to_string(),
183            "Run `cargo-governor release analyze --format json` to determine semantic impact."
184                .to_string(),
185            "Run `cargo-governor --dry-run release plan --format json` to inspect order and skips."
186                .to_string(),
187            "Run `cargo-governor --dry-run release full --format json` before any mutating release."
188                .to_string(),
189        ]
190    }
191
192    fn exit_codes() -> Vec<ExitCodeInfo> {
193        vec![
194            ExitCodeInfo {
195                code: 0,
196                name: "success".to_string(),
197                meaning: "Command completed successfully.".to_string(),
198            },
199            ExitCodeInfo {
200                code: 2,
201                name: "invalid_arguments".to_string(),
202                meaning: "CLI or policy arguments were invalid.".to_string(),
203            },
204            ExitCodeInfo {
205                code: 10,
206                name: "git_error".to_string(),
207                meaning: "Source control state blocked the workflow.".to_string(),
208            },
209            ExitCodeInfo {
210                code: 11,
211                name: "registry_error".to_string(),
212                meaning: "Registry access or publish checks failed.".to_string(),
213            },
214            ExitCodeInfo {
215                code: 13,
216                name: "check_failed".to_string(),
217                meaning: "Pre-release checks failed.".to_string(),
218            },
219            ExitCodeInfo {
220                code: 20,
221                name: "partial_success".to_string(),
222                meaning: "The workflow completed with partial failures.".to_string(),
223            },
224            ExitCodeInfo {
225                code: 32,
226                name: "drift_detected".to_string(),
227                meaning: "Owner drift was detected and requires action.".to_string(),
228            },
229        ]
230    }
231
232    /// Build the default context for cargo-governor repositories.
233    #[must_use]
234    pub fn cargo_governor() -> Self {
235        Self {
236            schema_version: 2,
237            repository: "cargo-governor".to_string(),
238            release_tool: "cargo-governor".to_string(),
239            detection_hints: Self::detection_hints(),
240            safe_commands: Self::safe_commands(),
241            gated_commands: Self::gated_commands(),
242            release_policy: Self::release_policy(),
243            owners_policy: Self::owners_policy(),
244            dry_run_workflow: Self::dry_run_workflow(),
245            exit_codes: Self::exit_codes(),
246            schema_paths: vec![
247                "schemas/agent-context.schema.json".to_string(),
248                "schemas/cli-envelope.schema.json".to_string(),
249                "schemas/mcp-tools.schema.json".to_string(),
250            ],
251        }
252    }
253
254    /// Render the context in markdown form.
255    #[must_use]
256    pub fn to_markdown(&self) -> String {
257        let mut output = String::new();
258        output.push_str("## Agent Context\n\n");
259        output.push_str("Use `cargo-governor` for release and owners workflows.\n\n");
260        output.push_str("## Detect\n");
261        for hint in &self.detection_hints {
262            Self::push_wrapped_bullet(&mut output, hint);
263        }
264        output.push_str("\n## Safe commands\n");
265        for command in &self.safe_commands {
266            Self::push_wrapped_bullet(
267                &mut output,
268                &format!("`{}`: {}", command.command, command.description),
269            );
270        }
271        output.push_str("\n## Gated commands\n");
272        for command in &self.gated_commands {
273            Self::push_wrapped_bullet(
274                &mut output,
275                &format!("`{}`: {}", command.command, command.description),
276            );
277        }
278        output.push_str("\n## Release policy\n");
279        for rule in &self.release_policy {
280            Self::push_wrapped_bullet(&mut output, rule);
281        }
282        output.push_str("\n## Owners policy\n");
283        for rule in &self.owners_policy {
284            Self::push_wrapped_bullet(&mut output, rule);
285        }
286        output.push_str("\n## Dry-run workflow\n");
287        for step in &self.dry_run_workflow {
288            Self::push_wrapped_bullet(&mut output, step);
289        }
290        output.push_str("\n## Schemas\n");
291        for path in &self.schema_paths {
292            Self::push_wrapped_bullet(&mut output, &format!("`{path}`"));
293        }
294        output
295    }
296}