use anyhow::{anyhow, bail, Context, Result}; use clap::{Parser, Subcommand}; use std::env; use std::fs; use std::io::{BufRead, BufReader}; use std::path::Path; use std::path::PathBuf; use std::process::{Command, Stdio}; use termimad::MadSkin; mod git; mod sdlc; mod session; use git::GitWorktree; use session::{Session, SessionStatus}; /// Studio - Multi-agent workspace manager for g3 #[derive(Parser)] #[command(name = "studio", subcommand_required = false)] #[command(about = "Manage multiple g3 agent sessions using git worktrees")] struct Cli { #[command(subcommand)] command: Option, } #[derive(Subcommand)] enum Commands { /// Start a new interactive g3 session in an isolated worktree #[command(alias = "c")] Cli, /// Resume a paused interactive session #[command(alias = "r")] Resume { /// Session ID to resume session_id: String, }, /// Run a new g3 session (tails output until complete) Run { /// Agent name (e.g., carmack, torvalds). If omitted, runs g3 in one-shot mode. #[arg(long)] agent: Option, /// Automatically accept the session if it completes with commits #[arg(long)] accept: bool, /// Additional arguments to pass to g3 #[arg(trailing_var_arg = true, allow_hyphen_values = true)] g3_args: Vec, }, /// Execute a g3 agent session in detached mode (for future use) Exec { /// Agent name (e.g., carmack, torvalds). If omitted, runs g3 in one-shot mode. #[arg(long)] agent: Option, /// Additional arguments to pass to g3 #[arg(trailing_var_arg = true, allow_hyphen_values = true)] g3_args: Vec, }, /// List all active sessions List, /// Show status of a session Status { /// Session ID session_id: String, }, /// Accept a session: merge to main and cleanup Accept { /// Session ID session_id: String, }, /// Discard a session: delete without merging Discard { /// Session ID session_id: String, }, /// Run the SDLC maintenance pipeline Sdlc { #[command(subcommand)] action: SdlcAction, }, } #[derive(Subcommand)] enum SdlcAction { /// Run the SDLC pipeline (or resume if interrupted) Run { /// Number of commits to process per stage (default: 10) #[arg(long, short, default_value = "10")] commits: u32, /// Set the commit cursor to start from (skips commits before this) #[arg(long)] from: Option, }, /// Show current pipeline status Status, /// Reset pipeline state (start fresh) Reset, } fn main() -> Result<()> { let cli = Cli::parse(); match cli.command.unwrap_or(Commands::Cli) { Commands::Cli => cmd_cli(), Commands::Resume { session_id } => cmd_resume(&session_id), Commands::Run { agent, accept, g3_args } => cmd_run(agent.as_deref(), accept, &g3_args), Commands::Exec { agent, g3_args } => cmd_exec(agent.as_deref(), &g3_args), Commands::List => cmd_list(), Commands::Status { session_id } => cmd_status(&session_id), Commands::Accept { session_id } => cmd_accept(&session_id), Commands::Discard { session_id } => cmd_discard(&session_id), Commands::Sdlc { action } => match action { SdlcAction::Run { commits, from } => cmd_sdlc_run(commits, from), SdlcAction::Status => cmd_sdlc_status(), SdlcAction::Reset => cmd_sdlc_reset(), }, } } /// Start a new interactive g3 session fn cmd_cli() -> Result<()> { let g3_binary = get_g3_binary_path()?; let repo_root = get_repo_root()?; let session = Session::new_interactive(); // Create worktree let worktree = GitWorktree::new(&repo_root); let worktree_path = worktree.create(&session)?; println!("πŸ“ Worktree: {}", worktree_path.display()); println!("🌿 Branch: {}", session.branch_name()); println!("πŸ†” Session: {}", session.id); println!(); // Save session metadata session.save(&repo_root, &worktree_path)?; // Build g3 command with inherited stdio for interactive use let mut cmd = Command::new(&g3_binary); cmd.arg("--workspace").arg(&worktree_path); cmd.current_dir(&worktree_path); cmd.stdin(Stdio::inherit()); cmd.stdout(Stdio::inherit()); cmd.stderr(Stdio::inherit()); // Spawn and wait let status = cmd.status().context("Failed to run g3")?; // Mark session as paused (user can resume later or accept/discard) session.mark_paused(&repo_root)?; println!(); println!("Session {} paused.", session.id); println!(); println!("Next steps:"); println!(" studio resume {} - Continue working", session.id); println!(" studio accept {} - Merge changes to main", session.id); println!(" studio discard {} - Discard changes", session.id); if !status.success() { std::process::exit(status.code().unwrap_or(1)); } Ok(()) } /// Resume a paused interactive session fn cmd_resume(session_id: &str) -> Result<()> { let g3_binary = get_g3_binary_path()?; let repo_root = get_repo_root()?; let session = Session::load(&repo_root, session_id)?; let worktree_path = session.worktree_path.as_ref() .ok_or_else(|| anyhow!("Session {} has no worktree path", session_id))?; if !worktree_path.exists() { bail!("Worktree for session {} no longer exists at {}", session_id, worktree_path.display()); } println!("πŸ“ Resuming session {} in {}", session_id, worktree_path.display()); println!(); // Build g3 command with inherited stdio let mut cmd = Command::new(&g3_binary); cmd.arg("--workspace").arg(worktree_path); cmd.current_dir(worktree_path); cmd.stdin(Stdio::inherit()); cmd.stdout(Stdio::inherit()); cmd.stderr(Stdio::inherit()); // Spawn and wait let status = cmd.status().context("Failed to run g3")?; // Mark session as paused again session.mark_paused(&repo_root)?; println!(); println!("Session {} paused.", session_id); println!(); println!("Next steps:"); println!(" studio resume {} - Continue working", session_id); println!(" studio accept {} - Merge changes to main", session_id); println!(" studio discard {} - Discard changes", session_id); if !status.success() { std::process::exit(status.code().unwrap_or(1)); } Ok(()) } /// Get the path to the g3 binary (same directory as studio) fn get_g3_binary_path() -> Result { let current_exe = env::current_exe().context("Failed to get current executable path")?; let exe_dir = current_exe .parent() .ok_or_else(|| anyhow!("Failed to get executable directory"))?; let g3_path = exe_dir.join("g3"); if !g3_path.exists() { bail!( "g3 binary not found at {:?}. Ensure g3 is built and in the same directory as studio.", g3_path ); } Ok(g3_path) } /// Get the repository root (where .git is) fn get_repo_root() -> Result { let output = Command::new("git") .args(["rev-parse", "--show-toplevel"]) .output() .context("Failed to run git rev-parse")?; if !output.status.success() { bail!("Not in a git repository"); } let path = String::from_utf8(output.stdout) .context("Invalid UTF-8 in git output")? .trim() .to_string(); Ok(PathBuf::from(path)) } /// Filter --accept from g3_args and return (filtered_args, was_accept_present) /// This handles the case where --accept is passed after positional args, /// which clap's trailing_var_arg captures instead of parsing as our flag. fn filter_accept_flag(g3_args: &[String]) -> (Vec, bool) { let accept_in_args = g3_args.iter().any(|arg| arg == "--accept"); let filtered: Vec = g3_args .iter() .filter(|arg| *arg != "--accept") .cloned() .collect(); (filtered, accept_in_args) } /// Run a new g3 session (foreground, tails output) fn cmd_run(agent: Option<&str>, auto_accept: bool, g3_args: &[String]) -> Result<()> { let (g3_args, accept_in_args) = filter_accept_flag(g3_args); let auto_accept = auto_accept || accept_in_args; let g3_binary = get_g3_binary_path()?; let repo_root = get_repo_root()?; // Use "single" as the agent name for non-agent runs let agent_name = agent.unwrap_or("single"); let session = Session::new(agent_name); // Create worktree let worktree = GitWorktree::new(&repo_root); let worktree_path = worktree.create(&session)?; // Print compact session info: "studio:" in bold green, session id in inline-code orange println!("\x1b[1;32mstudio:\x1b[0m new session \x1b[38;2;216;177;114m{}\x1b[0m", session.id); // Build g3 command with --workspace prepended let mut cmd = Command::new(&g3_binary); cmd.arg("--workspace").arg(&worktree_path); // Only add --agent if an agent was specified if let Some(a) = agent { cmd.arg("--agent").arg(a); } cmd.args(g3_args); cmd.current_dir(&worktree_path); cmd.stdout(Stdio::piped()); cmd.stderr(Stdio::piped()); // Save session metadata session.save(&repo_root, &worktree_path)?; println!(); // Spawn and tail output let mut child = cmd.spawn().context("Failed to spawn g3 process")?; // Update session with PID session.update_pid(&repo_root, child.id())?; // Tail stdout in a separate thread let stdout = child.stdout.take().expect("Failed to capture stdout"); let stderr = child.stderr.take().expect("Failed to capture stderr"); let stdout_handle = std::thread::spawn(move || { let reader = BufReader::new(stdout); for line in reader.lines() { if let Ok(line) = line { println!("{}", line); } } }); let stderr_handle = std::thread::spawn(move || { let reader = BufReader::new(stderr); for line in reader.lines() { if let Ok(line) = line { eprintln!("{}", line); } } }); // Wait for process to complete let status = child.wait().context("Failed to wait for g3 process")?; stdout_handle.join().ok(); stderr_handle.join().ok(); println!(); // Update session status session.mark_complete(&repo_root, status.success())?; if status.success() { // Auto-accept if flag is set and there are commits on the branch if auto_accept { if has_commits_on_branch(&worktree_path, &session.branch_name())? { return accept_session(&session.id, "\x1b[1;32mstudio:\x1b[0m"); } else { println!(); println!("⚠️ --accept flag set but no commits on branch, skipping auto-accept"); } } println!(); println!("Next steps:"); println!(" studio accept {} - Merge changes to main", session.id); println!(" studio discard {} - Discard changes", session.id); } else { println!("❌ Session {} failed (exit code: {:?})", session.id, status.code()); } Ok(()) } /// Check if a branch has commits ahead of main fn has_commits_on_branch(worktree_path: &Path, branch_name: &str) -> Result { // Count commits on branch that aren't on main let output = Command::new("git") .current_dir(worktree_path) .args(["rev-list", "--count", &format!("main..{}", branch_name)]) .output() .context("Failed to check commits on branch")?; if !output.status.success() { return Ok(false); } let count: u32 = String::from_utf8_lossy(&output.stdout).trim().parse().unwrap_or(0); Ok(count > 0) } /// Execute a g3 session in detached mode (placeholder for future) fn cmd_exec(agent: Option<&str>, g3_args: &[String]) -> Result<()> { // For now, just print what would happen println!("exec command not yet implemented"); match agent { Some(a) => println!("Would run agent '{}' with args: {:?}", a, g3_args), None => println!("Would run one-shot session with args: {:?}", g3_args), } Ok(()) } /// List all sessions fn cmd_list() -> Result<()> { let repo_root = get_repo_root()?; let sessions = Session::list_all(&repo_root)?; if sessions.is_empty() { println!("No active sessions."); return Ok(()); } println!("{:<12} {:<12} {:<10} {:<20}", "SESSION", "AGENT", "STATUS", "CREATED"); println!("{}", "─".repeat(60)); for session in sessions { let status_str = match session.status { SessionStatus::Running => "πŸ”„ running", SessionStatus::Paused => "⏸️ paused", SessionStatus::Complete => "βœ… complete", SessionStatus::Failed => "❌ failed", }; println!( "{:<12} {:<12} {:<10} {:<20}", session.id, session.agent, status_str, session.created_at.format("%Y-%m-%d %H:%M") ); } Ok(()) } /// Show status of a specific session fn cmd_status(session_id: &str) -> Result<()> { let repo_root = get_repo_root()?; let session = Session::load(&repo_root, session_id)?; println!("Session: {}", session.id); println!("Agent: {}", session.agent); println!("Branch: {}", session.branch_name()); println!("Created: {}", session.created_at.format("%Y-%m-%d %H:%M:%S")); println!("Status: {:?}", session.status); if let Some(path) = &session.worktree_path { println!("Worktree: {}", path.display()); } // Check if process is still running if session.status == SessionStatus::Running { if let Some(pid) = session.pid { let is_running = is_process_running(pid); if is_running { println!("Process: Running (PID {})", pid); } else { println!("Process: Not running (stale session)"); } } } // Try to extract summary from session logs if complete if session.status != SessionStatus::Running { if let Some(summary) = extract_session_summary(&session) { println!(); println!("Summary:"); println!("{}", "─".repeat(60)); let skin = MadSkin::default(); skin.print_text(&summary); } } Ok(()) } /// Accept a session: merge to main and cleanup fn cmd_accept(session_id: &str) -> Result<()> { accept_session(session_id, ">") } /// Internal function to accept a session with a custom prefix fn accept_session(session_id: &str, prefix: &str) -> Result<()> { let repo_root = get_repo_root()?; let session = Session::load(&repo_root, session_id)?; // Check session is not still running if session.status == SessionStatus::Running { if let Some(pid) = session.pid { if is_process_running(pid) { bail!("Session {} is still running (PID {}). Wait for it to complete or kill it first.", session_id, pid); } } } let worktree = GitWorktree::new(&repo_root); let branch_name = session.branch_name(); // Print status line without newline, then complete after operations use std::io::Write; print!("{} session {} ... ", prefix, session_id); std::io::stdout().flush().ok(); // Merge the branch to main worktree.merge_to_main(&branch_name)?; // Remove worktree and branch worktree.remove(&session)?; // Remove session metadata session.delete(&repo_root)?; println!("[\x1b[1;32mmerged\x1b[0m]"); Ok(()) } /// Discard a session: delete without merging fn cmd_discard(session_id: &str) -> Result<()> { let repo_root = get_repo_root()?; let session = Session::load(&repo_root, session_id)?; // Check session is not still running if session.status == SessionStatus::Running { if let Some(pid) = session.pid { if is_process_running(pid) { bail!("Session {} is still running (PID {}). Wait for it to complete or kill it first.", session_id, pid); } } } let worktree = GitWorktree::new(&repo_root); // Print status line without newline, then complete after operations use std::io::Write; print!("> session {} ... ", session_id); std::io::stdout().flush().ok(); // Remove worktree and branch worktree.remove(&session)?; // Remove session metadata session.delete(&repo_root)?; println!("[\x1b[1;33mdiscarded\x1b[0m]"); Ok(()) } /// Run the SDLC pipeline fn cmd_sdlc_run(commits_per_run: u32, from_commit: Option) -> Result<()> { let repo_root = get_repo_root()?; // Load or create pipeline state let mut state = match sdlc::PipelineState::load(&repo_root)? { Some(mut existing) => { // Resume from where we left off existing.resume(); println!("\x1b[1;32msdlc:\x1b[0m resuming pipeline run \x1b[38;2;216;177;114m{}\x1b[0m", existing.run_id); existing } None => { let mut state = sdlc::PipelineState::new(commits_per_run); // If --from is specified, set the cursor if let Some(ref from) = from_commit { // Resolve the commit hash let resolved = resolve_commit(&repo_root, from)?; state.commit_cursor = Some(resolved.clone()); println!("\x1b[1;32msdlc:\x1b[0m starting new pipeline run \x1b[38;2;216;177;114m{}\x1b[0m (from {})", state.run_id, &resolved[..8.min(resolved.len())]); } else { println!("\x1b[1;32msdlc:\x1b[0m starting new pipeline run \x1b[38;2;216;177;114m{}\x1b[0m", state.run_id); } state } }; // Get current HEAD commit let head_commit = get_head_commit(&repo_root)?; // Check if there are commits to process let commits_to_process = if let Some(cursor) = &state.commit_cursor { count_commits_between(&repo_root, cursor, &head_commit)? } else { // First run - use commits_per_run as the count commits_per_run }; if commits_to_process == 0 { println!("\x1b[1;32msdlc:\x1b[0m no new commits since last run"); return Ok(()); } println!("\x1b[1;32msdlc:\x1b[0m {} commits to process", commits_to_process.min(commits_per_run)); // Display the pipeline sdlc::display_pipeline(&state); // Create a dedicated worktree for SDLC let g3_binary = get_g3_binary_path()?; let sdlc_session = Session::new("sdlc"); let worktree = GitWorktree::new(&repo_root); let worktree_path = worktree.create(&sdlc_session)?; // Save session info for crash recovery state.session_id = Some(sdlc_session.id.clone()); sdlc_session.save(&repo_root, &worktree_path)?; state.save(&repo_root)?; // Run each stage while !state.is_complete() && state.current_stage < sdlc::PIPELINE_STAGES.len() { let stage = &sdlc::PIPELINE_STAGES[state.current_stage]; // Display current stage sdlc::display_current_stage(&state); println!(); // Mark as running and save state.mark_running(); state.save(&repo_root)?; let start_time = std::time::Instant::now(); // Build the task prompt for this agent let task = format!( "Focus on changes in the past {} commits (up to {}). {}", commits_per_run.min(commits_to_process), &head_commit[..8.min(head_commit.len())], stage.focus ); // Run the agent let result = run_agent_in_worktree( &g3_binary, &worktree_path, stage.name, &task, ); let duration = start_time.elapsed().as_secs(); match result { Ok(true) => { // Success state.mark_complete(duration, commits_to_process.min(commits_per_run), &head_commit); println!(); println!("\x1b[1;32msdlc:\x1b[0m stage \x1b[1m{}\x1b[0m complete in {}", stage.name, format_duration_short(duration)); } Ok(false) => { // Agent completed but with non-zero exit state.mark_failed("Agent exited with non-zero status"); println!(); println!("\x1b[1;31msdlc:\x1b[0m stage \x1b[1m{}\x1b[0m failed", stage.name); state.save(&repo_root)?; break; } Err(e) => { // Error running agent state.mark_failed(&e.to_string()); println!(); println!("\x1b[1;31msdlc:\x1b[0m stage \x1b[1m{}\x1b[0m error: {}", stage.name, e); state.save(&repo_root)?; break; } } state.save(&repo_root)?; // Display updated pipeline sdlc::display_pipeline(&state); } // Handle completion: merge if successful, otherwise preserve for debugging if state.is_complete() { // Check if there are commits to merge let branch_name = sdlc_session.branch_name(); if has_commits_on_branch(&worktree_path, &branch_name)? { // Merge to main before cleanup print!("\x1b[1;32msdlc:\x1b[0m merging changes to main ... "); std::io::Write::flush(&mut std::io::stdout()).ok(); match worktree.merge_to_main(&branch_name) { Ok(()) => { println!("[\x1b[1;32mmerged\x1b[0m]"); } Err(e) => { println!("[\x1b[1;31mfailed\x1b[0m]"); println!("\x1b[1;31msdlc:\x1b[0m merge failed: {}", e); println!(" Worktree preserved at: {}", worktree_path.display()); println!(" Resolve conflicts manually, then run 'studio accept {}'", sdlc_session.id); state.save(&repo_root)?; return Ok(()); } } } // Cleanup worktree after successful merge (or if no commits) worktree.remove(&sdlc_session)?; sdlc_session.delete(&repo_root)?; } // If not complete (failures), preserve worktree for debugging/retry // Generate and display summary if state.is_complete() { let summary = sdlc::generate_summary(&state); println!("{}", summary); // Save summary to .g3/sessions/sdlc/ let summary_dir = repo_root.join(".g3").join("sessions").join("sdlc"); fs::create_dir_all(&summary_dir).ok(); let summary_path = summary_dir.join(format!("run-{}.md", state.run_id)); fs::write(&summary_path, &summary).ok(); println!("\x1b[1;32msdlc:\x1b[0m pipeline complete!"); } else if state.has_failures() { println!(); println!("\x1b[1;32msdlc:\x1b[0m worktree preserved at: {}", worktree_path.display()); println!("\x1b[1;33msdlc:\x1b[0m pipeline paused due to failures"); println!(" Run 'studio sdlc run' to retry failed stages"); } Ok(()) } /// Show SDLC pipeline status fn cmd_sdlc_status() -> Result<()> { let repo_root = get_repo_root()?; match sdlc::PipelineState::load(&repo_root)? { Some(state) => { println!("\x1b[1;32msdlc:\x1b[0m pipeline run \x1b[38;2;216;177;114m{}\x1b[0m", state.run_id); sdlc::display_pipeline(&state); if state.is_complete() { println!("Status: \x1b[1;32mComplete\x1b[0m"); } else if state.has_failures() { println!("Status: \x1b[1;31mFailed\x1b[0m (run 'studio sdlc run' to retry)"); } else { println!("Status: \x1b[1;33mIn Progress\x1b[0m (stage {}/{})", state.current_stage + 1, sdlc::PIPELINE_STAGES.len()); } if let Some(cursor) = &state.commit_cursor { println!("Commit cursor: {}", cursor); } } None => { println!("\x1b[1;32msdlc:\x1b[0m no active pipeline"); println!(); println!("Run 'studio sdlc run' to start a new pipeline"); } } Ok(()) } /// Reset SDLC pipeline state fn cmd_sdlc_reset() -> Result<()> { let repo_root = get_repo_root()?; if sdlc::PipelineState::load(&repo_root)?.is_some() { sdlc::PipelineState::delete(&repo_root)?; println!("\x1b[1;32msdlc:\x1b[0m pipeline state reset"); } else { println!("\x1b[1;32msdlc:\x1b[0m no pipeline state to reset"); } Ok(()) } /// Format duration in short form fn format_duration_short(secs: u64) -> String { if secs < 60 { format!("{}s", secs) } else if secs < 3600 { format!("{}m {}s", secs / 60, secs % 60) } else { format!("{}h {}m", secs / 3600, (secs % 3600) / 60) } } /// Resolve a commit reference (hash, branch, tag, etc.) to a full hash fn resolve_commit(repo_root: &Path, commit_ref: &str) -> Result { let output = Command::new("git") .current_dir(repo_root) .args(["rev-parse", commit_ref]) .output() .context("Failed to resolve commit")?; if !output.status.success() { bail!("Failed to resolve commit '{}'", commit_ref); } Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()) } /// Get the current HEAD commit hash fn get_head_commit(repo_root: &Path) -> Result { let output = Command::new("git") .current_dir(repo_root) .args(["rev-parse", "HEAD"]) .output() .context("Failed to get HEAD commit")?; if !output.status.success() { bail!("Failed to get HEAD commit"); } Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()) } /// Count commits between two refs fn count_commits_between(repo_root: &Path, from: &str, to: &str) -> Result { let output = Command::new("git") .current_dir(repo_root) .args(["rev-list", "--count", &format!("{}..{}", from, to)]) .output() .context("Failed to count commits")?; if !output.status.success() { // If the from commit doesn't exist (first run), return a large number return Ok(u32::MAX); } let count: u32 = String::from_utf8_lossy(&output.stdout) .trim() .parse() .unwrap_or(0); Ok(count) } /// Run a g3 agent in a worktree fn run_agent_in_worktree( g3_binary: &Path, worktree_path: &Path, agent: &str, task: &str, ) -> Result { let mut cmd = Command::new(g3_binary); cmd.arg("--workspace").arg(worktree_path); cmd.arg("--agent").arg(agent); cmd.arg(task); cmd.current_dir(worktree_path); cmd.stdout(Stdio::inherit()); cmd.stderr(Stdio::inherit()); let status = cmd.status().context("Failed to run g3 agent")?; // If the agent made commits, commit them if status.success() { // Stage and commit any changes made by the agent let _ = Command::new("git") .current_dir(worktree_path) .args(["add", "-A"]) .output(); } Ok(status.success()) } /// Check if a process is running by PID fn is_process_running(pid: u32) -> bool { // Use kill -0 to check if process exists Command::new("kill") .args(["-0", &pid.to_string()]) .output() .map(|o| o.status.success()) .unwrap_or(false) } /// Extract summary from session logs fn extract_session_summary(session: &Session) -> Option { // Look for session logs in the worktree's .g3 directory let worktree_path = session.worktree_path.as_ref()?; let session_dir = worktree_path.join(".g3").join("sessions"); if !session_dir.exists() { return None; } // Find the most recent session log let mut latest_log: Option<(PathBuf, std::time::SystemTime)> = None; if let Ok(entries) = fs::read_dir(&session_dir) { for entry in entries.flatten() { let path = entry.path(); if path.is_dir() { let log_file = path.join("session.json"); if log_file.exists() { if let Ok(metadata) = log_file.metadata() { if let Ok(modified) = metadata.modified() { if latest_log.is_none() || modified > latest_log.as_ref().unwrap().1 { latest_log = Some((log_file, modified)); } } } } } } } let log_file = latest_log?.0; let content = fs::read_to_string(&log_file).ok()?; // Parse JSON and extract the last assistant message as summary let json: serde_json::Value = serde_json::from_str(&content).ok()?; // Try the new format first: context_window.conversation_history // Fall back to old format: messages let messages = json .get("context_window") .and_then(|cw| cw.get("conversation_history")) .and_then(|ch| ch.as_array()) .or_else(|| json.get("messages").and_then(|m| m.as_array()))?; // Find the last assistant message for msg in messages.iter().rev() { if msg.get("role")?.as_str()? == "assistant" { if let Some(content) = msg.get("content") { if let Some(text) = content.as_str() { // Return the full summary text return Some(text.to_string()); } } } } None } #[cfg(test)] mod tests { use super::*; #[test] fn test_filter_accept_flag_removes_accept() { let args = vec!["task".to_string(), "--accept".to_string()]; let (filtered, found) = filter_accept_flag(&args); assert!(found); assert_eq!(filtered, vec!["task".to_string()]); } #[test] fn test_filter_accept_flag_no_accept() { let args = vec!["task".to_string(), "--verbose".to_string()]; let (filtered, found) = filter_accept_flag(&args); assert!(!found); assert_eq!(filtered, vec!["task".to_string(), "--verbose".to_string()]); } #[test] fn test_filter_accept_flag_empty() { let args: Vec = vec![]; let (filtered, found) = filter_accept_flag(&args); assert!(!found); assert!(filtered.is_empty()); } #[test] fn test_filter_accept_flag_accept_in_middle() { let args = vec![ "task".to_string(), "--accept".to_string(), "--other".to_string(), ]; let (filtered, found) = filter_accept_flag(&args); assert!(found); assert_eq!(filtered, vec!["task".to_string(), "--other".to_string()]); } #[test] fn test_filter_accept_flag_only_accept() { let args = vec!["--accept".to_string()]; let (filtered, found) = filter_accept_flag(&args); assert!(found); assert!(filtered.is_empty()); } }