1use serde::Serialize;
4
5#[derive(Debug, Clone, Copy, Serialize)]
7#[serde(rename_all = "snake_case")]
8pub enum CommandSafety {
9 Safe,
11 Mutating,
13 Gated,
15}
16
17#[derive(Debug, Clone, Serialize)]
19pub struct AgentCommand {
20 pub command: String,
22 pub description: String,
24 pub safety: CommandSafety,
26}
27
28#[derive(Debug, Clone, Serialize)]
30pub struct ExitCodeInfo {
31 pub code: u8,
33 pub name: String,
35 pub meaning: String,
37}
38
39#[derive(Debug, Clone, Serialize)]
41pub struct AgentContext {
42 pub schema_version: u32,
44 pub repository: String,
46 pub release_tool: String,
48 pub detection_hints: Vec<String>,
50 pub safe_commands: Vec<AgentCommand>,
52 pub gated_commands: Vec<AgentCommand>,
54 pub release_policy: Vec<String>,
56 pub owners_policy: Vec<String>,
58 pub dry_run_workflow: Vec<String>,
60 pub exit_codes: Vec<ExitCodeInfo>,
62 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 #[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 #[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}