darq 0.1.0

darq CLI + TUI — autonomous issue → PR pipeline with SAT and a learning loop.
Documentation
//! SAT (Scenario-driven AI Testing) commands.

use clap::Subcommand;

use crate::daemon::client::DaemonClient;
use crate::daemon::protocol::Method;

#[derive(Subcommand, Debug)]
pub enum SatCommands {
    /// Run SAT on a completed run
    Run {
        /// Parent run ID (the completed run to verify)
        #[arg(conflicts_with = "target")]
        run_id: Option<String>,
        /// Workspace directory (default: current directory)
        #[arg(short, long, default_value = ".")]
        workspace: String,
        /// Run SAT directly against a target repo (skips daemon)
        #[arg(long)]
        target: Option<String>,
    },
}

pub async fn handle(command: SatCommands, client: &mut DaemonClient) -> anyhow::Result<()> {
    match command {
        SatCommands::Run {
            run_id,
            workspace,
            target,
        } => {
            if let Some(ref target_path) = target {
                // When --target is a path, use it as both repo identifier and workspace
                let ws = if workspace == "." {
                    target_path.as_str()
                } else {
                    &workspace
                };
                return handle_standalone_sat(target_path, ws).await;
            }

            let run_id =
                run_id.ok_or_else(|| anyhow::anyhow!("either --run-id or --target is required"))?;
            println!("Running SAT for run {}...", &run_id[..8.min(run_id.len())]);

            let params = serde_json::json!({
                "kind": "sat_verify",
                "issue": 0,
                "workspace": workspace,
                "run_id": run_id,
            });

            let response = client.send(Method::WorkflowStart, params).await?;

            if let Some(err) = DaemonClient::extract_error(&response) {
                eprintln!("SAT failed: {err}");
                std::process::exit(1);
            }

            let result = DaemonClient::extract_result(&response)
                .ok_or_else(|| anyhow::anyhow!("no result"))?;

            println!();
            println!("SAT Result:");
            println!("{}", serde_json::to_string_pretty(result)?);
            Ok(())
        }
    }
}

async fn handle_standalone_sat(repo: &str, workspace: &str) -> anyhow::Result<()> {
    use darq_core::config::{load_or_default, load_sat_config};
    use darq_core::sat::{SatContext, execute_sat};
    use std::path::PathBuf;

    let workspace_dir = PathBuf::from(workspace);

    let sat_config = load_sat_config(&workspace_dir).map_err(|e| anyhow::anyhow!(e))?;

    let config = load_or_default();
    let agent_settings = config.agent.clone();

    // Use test_command from pipeline config (darq.yaml)
    let test_command = config.pipeline.test_command.clone();

    // Capture git diff of uncommitted changes or last commit
    let code_diff = {
        use std::process::Command;
        let output = Command::new("git")
            .args(["diff", "HEAD"])
            .current_dir(&workspace_dir)
            .output();
        match output {
            Ok(o) if o.status.success() => {
                let diff = String::from_utf8_lossy(&o.stdout).to_string();
                if diff.is_empty() {
                    // Try diff against parent commit
                    let output2 = Command::new("git")
                        .args(["diff", "HEAD~1"])
                        .current_dir(&workspace_dir)
                        .output();
                    match output2 {
                        Ok(o2) if o2.status.success() => {
                            let d = String::from_utf8_lossy(&o2.stdout).to_string();
                            if d.is_empty() { None } else { Some(d) }
                        }
                        _ => None,
                    }
                } else {
                    Some(diff)
                }
            }
            _ => None,
        }
    };

    let ctx = SatContext {
        workspace_dir: workspace_dir.clone(),
        project_dir: workspace_dir.clone(),
        parent_run_id: format!("sat-target-{}", repo),
        config: sat_config,
        test_command,
        code_diff,
        agent_settings,
    };

    println!("Running SAT against: {}", repo);
    println!("  Workspace: {}", workspace);
    println!("  Test command: {}", ctx.test_command);
    println!("  Personas: {}", ctx.config.personas.len());
    println!(
        "  Multi-judge: {}",
        if ctx
            .config
            .judges
            .as_ref()
            .map(|j| j.enabled)
            .unwrap_or(false)
        {
            "enabled"
        } else {
            "disabled"
        }
    );

    let result = execute_sat(&ctx).await?;

    println!();
    println!("SAT Result:");
    println!("{}", serde_json::to_string_pretty(&result.result)?);
    Ok(())
}