edict/commands/protocol/
cleanup.rs1use super::context::ProtocolContext;
10use super::executor;
11use super::exit_policy;
12use super::render::{ProtocolGuidance, ProtocolStatus};
13use super::shell;
14use crate::commands::doctor::OutputFormat;
15
16pub fn execute(
24 execute: bool,
25 agent: &str,
26 project: &str,
27 format: OutputFormat,
28) -> anyhow::Result<()> {
29 let ctx = ProtocolContext::collect(project, agent)?;
31
32 let mut guidance = ProtocolGuidance::new("cleanup");
34 guidance.bone = None;
35 guidance.workspace = None;
36 guidance.review = None;
37
38 let bone_claims = ctx.held_bone_claims();
40 let workspace_claims = ctx.held_workspace_claims();
41
42 if bone_claims.is_empty() && workspace_claims.is_empty() {
44 guidance.status = ProtocolStatus::Ready;
45 guidance.advise("No cleanup needed.".to_string());
46 return render_cleanup(&guidance, format, execute);
48 }
49
50 guidance.status = ProtocolStatus::HasResources;
52
53 let mut steps = Vec::new();
55
56 steps.push(shell::bus_send_cmd(
58 "agent",
59 project,
60 "Agent idle",
61 "agent-idle",
62 ));
63
64 steps.push(shell::bus_statuses_clear_cmd("agent"));
66
67 if !bone_claims.is_empty() {
69 let bone_list = bone_claims
71 .iter()
72 .map(|(id, _)| id.to_string())
73 .collect::<Vec<_>>()
74 .join(", ");
75 guidance.diagnostic(format!(
76 "WARNING: Active bone claim(s) held: {}. Releasing these marks them as unowned in doing state.",
77 bone_list
78 ));
79 }
80 steps.push(shell::claims_release_all_cmd("agent"));
81
82 guidance.steps(steps);
83
84 let summary = format!(
86 "Agent {} has {} bone claim(s) and {} workspace claim(s). \
87 Run these commands to clean up and mark as idle.",
88 agent,
89 bone_claims.len(),
90 workspace_claims.len()
91 );
92 guidance.advise(summary);
93
94 render_cleanup(&guidance, format, execute)
95}
96
97fn render_cleanup(
108 guidance: &ProtocolGuidance,
109 format: OutputFormat,
110 execute: bool,
111) -> anyhow::Result<()> {
112 if execute && matches!(guidance.status, ProtocolStatus::HasResources) {
114 let report = executor::execute_steps(&guidance.steps)?;
115 let output = executor::render_report(&report, format);
116 println!("{}", output);
117 return Ok(());
118 }
119
120 match format {
122 OutputFormat::Text => {
123 let status_text = match guidance.status {
125 ProtocolStatus::Ready => "clean",
126 ProtocolStatus::HasResources => "has-resources",
127 _ => "unknown",
128 };
129 println!("status\t{}", status_text);
130
131 if matches!(guidance.status, ProtocolStatus::HasResources) {
133 let claim_count = guidance
134 .diagnostics
135 .iter()
136 .find(|d| d.contains("Active bone claim"))
137 .map(|_| guidance.diagnostics.len())
138 .unwrap_or(0);
139 println!("claims\t{} active", claim_count);
140 println!();
141 println!("Run these commands to clean up:");
142 for step in &guidance.steps {
143 println!(" {}", step);
144 }
145 } else {
146 println!("claims\t0 active");
147 println!();
148 println!("No cleanup needed.");
149 }
150 Ok(())
151 }
152 OutputFormat::Pretty => {
153 let status_text = match guidance.status {
155 ProtocolStatus::Ready => "✓ clean",
156 ProtocolStatus::HasResources => "⚠ has-resources",
157 _ => "? unknown",
158 };
159 println!("Status: {}", status_text);
160
161 if matches!(guidance.status, ProtocolStatus::HasResources) {
162 println!();
163 println!("Run these commands to clean up:");
164 for step in &guidance.steps {
165 println!(" {}", step);
166 }
167
168 if !guidance.diagnostics.is_empty() {
169 println!();
170 println!("Warnings:");
171 for diagnostic in &guidance.diagnostics {
172 println!(" ⚠ {}", diagnostic);
173 }
174 }
175 } else {
176 println!("No cleanup needed.");
177 }
178
179 if let Some(advice) = &guidance.advice {
180 println!();
181 println!("Notes: {}", advice);
182 }
183 Ok(())
184 }
185 OutputFormat::Json => {
186 exit_policy::render_guidance(guidance, format)
188 }
189 }
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195
196 #[test]
197 fn test_cleanup_status_clean() {
198 let mut guidance = ProtocolGuidance::new("cleanup");
200 guidance.status = ProtocolStatus::Ready;
201 guidance.advise("No cleanup needed.".to_string());
202
203 assert_eq!(format!("{:?}", guidance.status), "Ready");
204 assert!(guidance.steps.is_empty());
205 }
206
207 #[test]
208 fn test_cleanup_status_has_resources() {
209 let mut guidance = ProtocolGuidance::new("cleanup");
211 guidance.status = ProtocolStatus::HasResources;
212 guidance.steps(vec![
213 "bus send --agent test-agent test-project \"Agent idle\" -L agent-idle".to_string(),
214 "bus statuses clear --agent test-agent".to_string(),
215 "bus claims release --agent test-agent --all".to_string(),
216 ]);
217
218 assert_eq!(format!("{:?}", guidance.status), "HasResources");
219 assert_eq!(guidance.steps.len(), 3);
220 assert!(guidance.steps.iter().any(|s| s.contains("bus send")));
221 assert!(
222 guidance
223 .steps
224 .iter()
225 .any(|s| s.contains("bus statuses clear"))
226 );
227 assert!(
228 guidance
229 .steps
230 .iter()
231 .any(|s| s.contains("bus claims release"))
232 );
233 }
234
235 #[test]
236 fn test_cleanup_warning_for_active_bones() {
237 let mut guidance = ProtocolGuidance::new("cleanup");
239 guidance.diagnostic(
240 "WARNING: Active bone claim(s) held: bd-3cqv. \
241 Releasing these marks them as unowned in doing state."
242 .to_string(),
243 );
244
245 assert!(guidance.diagnostics.iter().any(|d| d.contains("WARNING")));
246 assert!(guidance.diagnostics.iter().any(|d| d.contains("bd-3cqv")));
247 }
248}