Skip to main content

edict/commands/
iteration_start.rs

1use std::io::IsTerminal;
2
3use serde::Deserialize;
4
5use crate::config::Config;
6use crate::subprocess::Tool;
7
8// ===== Data Structures =====
9
10#[derive(Debug, Deserialize)]
11pub struct InboxResponse {
12    pub total_unread: i32,
13    pub channels: Option<Vec<InboxChannel>>,
14}
15
16#[derive(Debug, Deserialize)]
17pub struct InboxChannel {
18    pub messages: Option<Vec<InboxMessage>>,
19}
20
21#[derive(Debug, Deserialize)]
22pub struct InboxMessage {
23    pub agent: String,
24    pub label: Option<String>,
25    pub body: String,
26}
27
28#[derive(Debug, Deserialize)]
29pub struct ReviewsResponse {
30    pub reviews_awaiting_vote: Option<Vec<ReviewInfo>>,
31    pub threads_with_new_responses: Option<Vec<ThreadInfo>>,
32}
33
34#[derive(Debug, Deserialize)]
35pub struct ReviewInfo {
36    pub review_id: String,
37    pub title: Option<String>,
38    pub description: Option<String>,
39}
40
41#[derive(Debug, Deserialize)]
42pub struct ThreadInfo {}
43
44#[derive(Debug, Deserialize)]
45pub struct ClaimsResponse {
46    pub claims: Option<Vec<Claim>>,
47}
48
49#[derive(Debug, Deserialize)]
50pub struct Claim {
51    pub patterns: Option<Vec<String>>,
52    pub expires_in_secs: Option<i32>,
53}
54
55// ANSI color codes — conditionally applied based on TTY detection
56pub struct Colors {
57    pub reset: &'static str,
58    pub bold: &'static str,
59    pub dim: &'static str,
60    pub cyan: &'static str,
61    pub green: &'static str,
62}
63
64impl Colors {
65    pub fn detect() -> Self {
66        if std::io::stdout().is_terminal() {
67            Self {
68                reset: "\x1b[0m",
69                bold: "\x1b[1m",
70                dim: "\x1b[2m",
71                cyan: "\x1b[36m",
72                green: "\x1b[32m",
73            }
74        } else {
75            Self {
76                reset: "",
77                bold: "",
78                dim: "",
79                cyan: "",
80                green: "",
81            }
82        }
83    }
84}
85
86pub fn h1(c: &Colors, s: &str) -> String {
87    format!("{}{}# {}{}", c.bold, c.cyan, s, c.reset)
88}
89
90pub fn h2(c: &Colors, s: &str) -> String {
91    format!("{}{}## {}{}", c.bold, c.green, s, c.reset)
92}
93
94pub fn hint(c: &Colors, s: &str) -> String {
95    format!("{}> {}{}", c.dim, s, c.reset)
96}
97
98/// Fetch config from .edict.toml/.botbox.toml or ws/default/
99fn load_config() -> anyhow::Result<Config> {
100    let cwd = std::path::Path::new(".");
101    let (config_path, _) = crate::config::find_config_in_project(cwd)?;
102    Config::load(&config_path)
103}
104
105/// Helper to run a tool and parse JSON output, returning None on failure
106fn run_json_tool(tool: &str, args: &[&str]) -> Option<String> {
107    if tool == "bn" || tool == "crit" {
108        // These need to be run in the default workspace
109        let mut output = Tool::new(tool);
110        for arg in args {
111            output = output.arg(arg);
112        }
113        output = output.arg("--format").arg("json");
114
115        let result = output.in_workspace("default").ok()?.run().ok()?;
116
117        if result.success() {
118            Some(result.stdout)
119        } else {
120            None
121        }
122    } else {
123        // Direct tool execution
124        let mut output = Tool::new(tool);
125        for arg in args {
126            output = output.arg(arg);
127        }
128        output = output.arg("--format").arg("json");
129
130        let result = output.run().ok()?;
131        if result.success() {
132            Some(result.stdout)
133        } else {
134            None
135        }
136    }
137}
138
139/// Run iteration-start with optional overrides
140pub fn run_iteration_start(agent_override: Option<&str>) -> anyhow::Result<()> {
141    let config = load_config()?;
142    let default_agent = config.default_agent();
143    let agent = agent_override.unwrap_or(default_agent.as_str());
144    let project = config.channel();
145    let c = Colors::detect();
146
147    println!("{}", h1(&c, &format!("Iteration Start: {}", agent)));
148    println!();
149
150    // 1. Unread bus messages
151    let inbox_output = run_json_tool("bus", &["inbox", "--agent", agent, "--channels", &project]);
152    let mut unread_count = 0;
153
154    if let Some(output) = &inbox_output
155        && let Ok(inbox) = serde_json::from_str::<InboxResponse>(output)
156    {
157        unread_count = inbox.total_unread;
158    }
159
160    println!(
161        "{}",
162        h2(&c, &format!("Unread Bus Messages ({})", unread_count))
163    );
164
165    if let Some(output) = inbox_output {
166        if let Ok(inbox) = serde_json::from_str::<InboxResponse>(&output) {
167            if inbox.total_unread > 0 {
168                if let Some(channels) = inbox.channels {
169                    for channel in channels {
170                        if let Some(messages) = channel.messages {
171                            for msg in messages.iter().take(5) {
172                                let label = msg
173                                    .label
174                                    .as_ref()
175                                    .map(|l| format!("[{}]", l))
176                                    .unwrap_or_default();
177                                let body = if msg.body.len() > 60 {
178                                    format!("{}...", &msg.body[..msg.body.floor_char_boundary(60)])
179                                } else {
180                                    msg.body.clone()
181                                };
182                                println!(
183                                    "   {}{}{} {}: {}",
184                                    c.dim, msg.agent, c.reset, label, body
185                                );
186                            }
187                        }
188                    }
189                }
190            } else {
191                println!("   {}No unread messages{}", c.dim, c.reset);
192            }
193        } else {
194            println!("   {}No unread messages{}", c.dim, c.reset);
195        }
196    } else {
197        println!("   {}No unread messages{}", c.dim, c.reset);
198    }
199    println!();
200
201    // 2. Bones (via bn triage)
202    if let Err(e) = super::triage::run_triage() {
203        println!("   {}Could not fetch triage data: {}{}", c.dim, e, c.reset);
204    }
205    println!();
206
207    // 3. Pending reviews
208    println!("{}", h2(&c, "Pending Reviews"));
209    let reviews_output = run_json_tool("crit", &["inbox", "--agent", agent]);
210    let mut has_reviews = false;
211
212    if let Some(output) = reviews_output {
213        if let Ok(reviews) = serde_json::from_str::<ReviewsResponse>(&output) {
214            let awaiting = reviews.reviews_awaiting_vote.unwrap_or_default();
215            let threads = reviews.threads_with_new_responses.unwrap_or_default();
216
217            if !awaiting.is_empty() || !threads.is_empty() {
218                has_reviews = true;
219                if !awaiting.is_empty() {
220                    println!("   {} review(s) awaiting vote", awaiting.len());
221                    for r in awaiting.iter().take(3) {
222                        let no_title = "(no title)".to_string();
223                        let title = r
224                            .title
225                            .as_ref()
226                            .or(r.description.as_ref())
227                            .unwrap_or(&no_title);
228                        println!("   {}: {}", r.review_id, title);
229                    }
230                }
231                if !threads.is_empty() {
232                    println!("   {} thread(s) with new responses", threads.len());
233                }
234            } else {
235                println!("   {}No pending reviews{}", c.dim, c.reset);
236            }
237        } else {
238            println!("   {}No pending reviews{}", c.dim, c.reset);
239        }
240    } else {
241        println!("   {}Could not fetch reviews{}", c.dim, c.reset);
242    }
243    println!();
244
245    // 4. Active claims
246    println!("{}", h2(&c, "Active Claims"));
247    let claims_output = run_json_tool("bus", &["claims", "list", "--agent", agent, "--mine"]);
248
249    if let Some(output) = claims_output {
250        if let Ok(claims_data) = serde_json::from_str::<ClaimsResponse>(&output) {
251            if let Some(claims) = claims_data.claims {
252                // Filter out agent identity claims (those that start with "agent://")
253                let resource_claims: Vec<_> = claims
254                    .iter()
255                    .filter(|cl| {
256                        cl.patterns
257                            .as_ref()
258                            .map(|p| !p.iter().all(|pat| pat.starts_with("agent://")))
259                            .unwrap_or(true)
260                    })
261                    .collect();
262
263                if !resource_claims.is_empty() {
264                    println!("   {} active claim(s)", resource_claims.len());
265                    for claim in resource_claims.iter().take(5) {
266                        if let Some(patterns) = &claim.patterns {
267                            let resource_patterns: Vec<_> = patterns
268                                .iter()
269                                .filter(|p| !p.starts_with("agent://"))
270                                .collect();
271                            for pattern in resource_patterns {
272                                let expires = claim
273                                    .expires_in_secs
274                                    .map(|s| format!("({}m left)", s / 60))
275                                    .unwrap_or_default();
276                                println!("   {} {}", pattern, expires);
277                            }
278                        }
279                    }
280                } else {
281                    println!("   {}No resource claims{}", c.dim, c.reset);
282                }
283            } else {
284                println!("   {}No active claims{}", c.dim, c.reset);
285            }
286        } else {
287            println!("   {}No active claims{}", c.dim, c.reset);
288        }
289    } else {
290        println!("   {}No active claims{}", c.dim, c.reset);
291    }
292    println!();
293
294    // Summary hint
295    if unread_count > 0 {
296        println!(
297            "{}",
298            hint(
299                &c,
300                &format!(
301                    "Get unread messages and mark them as read: bus inbox --agent {} --channels {} --mark-read",
302                    agent, project
303                )
304            )
305        );
306    } else if has_reviews {
307        println!(
308            "{}",
309            hint(
310                &c,
311                &format!(
312                    "Start review: maw exec default -- crit inbox --agent {}",
313                    agent
314                )
315            )
316        );
317    } else {
318        println!("{}", hint(&c, "No work pending"));
319    }
320
321    Ok(())
322}