darq 0.1.0

darq CLI + TUI — autonomous issue → PR pipeline with SAT and a learning loop.
Documentation
use clap::{Parser, Subcommand};

mod commands;
mod daemon;
mod tui;

#[derive(Parser)]
#[command(name = "darq", version, about = "Dark software factory CLI")]
struct Cli {
    /// Path to darq.yaml config file
    #[arg(long, default_value = "darq.yaml")]
    config: String,

    /// Enable verbose logging
    #[arg(short, long)]
    verbose: bool,

    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand)]
enum Commands {
    /// Launch the interactive TUI dashboard
    Tui {
        /// Replay a captured JSONL event stream instead of connecting to the daemon.
        /// Produced by scripts/capture-events.sh or the synthetic fixtures in .darq-captures-synthetic/.
        #[arg(long)]
        replay: Option<std::path::PathBuf>,

        /// Replay speed multiplier (default 1.0 = real-time; 4.0 = 4× faster).
        /// Only used when --replay is set.
        #[arg(long, default_value_t = 1.0)]
        speed: f64,
    },
    /// Show daemon status and current run state
    Status,
    /// Daemon management
    Daemon {
        #[command(subcommand)]
        command: DaemonCommands,
    },
    /// Run-related commands
    Run {
        #[command(subcommand)]
        command: RunCommands,
    },
    /// Milestone-related commands
    Milestone {
        #[command(subcommand)]
        command: MilestoneCommands,
    },
    /// Issue workflow commands
    Issue {
        #[command(subcommand)]
        command: commands::issue::IssueCommands,
    },
    /// Pull request workflow commands
    Pr {
        #[command(subcommand)]
        command: commands::pr::PrCommands,
    },
    /// Scenario-driven testing commands
    Sat {
        #[command(subcommand)]
        command: commands::sat::SatCommands,
    },
    /// Learning analytics and pattern effectiveness
    Stats,
    /// Initialize darq configuration
    Init {
        #[command(flatten)]
        args: commands::init::InitArgs,
    },
    /// Validate project setup — config present, agent on PATH, daemon reachable, GitHub auth
    Check {
        #[command(flatten)]
        args: commands::check::CheckArgs,
    },
}

#[derive(Subcommand)]
enum DaemonCommands {
    /// Start the daemon in the background
    Start {
        /// Run in background (detached from terminal)
        #[arg(long)]
        background: bool,
    },
    /// Stop the running daemon
    Stop,
    /// Check if daemon is running
    Status,
}

#[derive(Subcommand)]
enum RunCommands {
    /// List all runs with optional filters
    List {
        /// Filter by status: running, pending, completed, failed, cancelled, awaiting_approval
        #[arg(long)]
        status: Option<String>,
        /// Filter by milestone name
        #[arg(long)]
        milestone: Option<String>,
    },
    /// Show details of a specific run
    Show {
        /// Run ID
        id: String,
    },
    /// Approve a run awaiting human approval
    Approve {
        /// Run ID
        id: String,
    },
    /// Cancel a running run
    Cancel {
        /// Run ID
        id: String,
        /// Reason for cancellation
        #[arg(long)]
        reason: Option<String>,
    },
    /// Run the full workflow chain for a single issue (plan → implement → review → merge → SAT → learn)
    Issue {
        /// Issue number
        number: u64,
        /// Run full chain (7 steps). Default runs only the first workflow step.
        #[arg(long)]
        full: bool,
    },
    /// Sweep a milestone — process all issues through the workflow chain
    Milestone {
        /// Milestone name
        name: String,
        /// Dry run — show what would be processed without executing
        #[arg(long)]
        dry_run: bool,
    },
}

#[derive(Subcommand)]
enum MilestoneCommands {
    /// Pick the next issue from the current milestone
    Next,
    /// Process all issues in a milestone
    Sweep {
        /// Milestone name
        name: String,
        /// Dry run — show what would be processed
        #[arg(long)]
        dry_run: bool,
    },
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let cli = Cli::parse();

    let filter = if cli.verbose { "debug" } else { "info" };

    // When running the TUI, redirect tracing to ~/.darq/tui.log instead of stderr.
    // The TUI owns stderr (alt-screen + raw mode); any tracing output that bleeds
    // through becomes glitchy overlay text on top of widgets.
    if matches!(cli.command, Commands::Tui { .. }) {
        if let Some(home) = std::env::var_os("HOME") {
            let log_dir = std::path::PathBuf::from(home).join(".darq");
            let _ = std::fs::create_dir_all(&log_dir);
            let log_path = log_dir.join("tui.log");
            // Truncate per-session — log file is for live debugging while the TUI runs.
            if let Ok(file) = std::fs::OpenOptions::new()
                .create(true)
                .write(true)
                .truncate(true)
                .open(&log_path)
            {
                tracing_subscriber::fmt()
                    .with_env_filter(filter)
                    .with_writer(std::sync::Mutex::new(file))
                    .with_ansi(false)
                    .init();
            } else {
                // Fallback: silent if we can't open the log file. Better than corrupting the TUI.
                tracing_subscriber::fmt()
                    .with_env_filter("off")
                    .with_writer(std::io::sink)
                    .init();
            }
        } else {
            tracing_subscriber::fmt()
                .with_env_filter("off")
                .with_writer(std::io::sink)
                .init();
        }
    } else {
        tracing_subscriber::fmt().with_env_filter(filter).init();
    }

    tracing::info!(config = %cli.config, "darq starting");

    match cli.command {
        // ── Daemon management (no client needed) ──
        Commands::Daemon { command } => match command {
            DaemonCommands::Start { background } => {
                daemon::handle_start(&cli.config, background).await?;
            }
            DaemonCommands::Stop => {
                daemon::handle_stop().await?;
            }
            DaemonCommands::Status => {
                daemon::handle_daemon_status();
            }
        },
        Commands::Init { args } => commands::init::handle(args).await?,
        Commands::Check { args } => {
            let exit_code = commands::check::handle(args).await?;
            std::process::exit(exit_code as i32);
        }

        // ── TUI (connects to daemon via socket, or replays a captured JSONL) ──
        Commands::Tui { replay, speed } => {
            if let Some(path) = replay {
                tracing::info!(?path, speed, "starting TUI in replay mode");
                tui::run_replay(path, speed).await?;
            } else {
                let client = daemon::client::DaemonClient::connect_or_start(&cli.config).await?;
                tracing::info!("starting TUI (connected to daemon)");
                tui::run(client).await?;
            }
        }

        // ── All other commands: connect to daemon via socket ──
        _ => {
            let mut client = daemon::client::DaemonClient::connect_or_start(&cli.config).await?;

            match cli.command {
                Commands::Status => commands::status(&mut client).await?,
                Commands::Run { command } => match command {
                    RunCommands::List { status, milestone } => {
                        commands::run_list(&mut client, status.as_deref(), milestone.as_deref())
                            .await?
                    }
                    RunCommands::Show { id } => commands::run_show(&mut client, &id).await?,
                    RunCommands::Approve { id } => commands::run_approve(&mut client, &id).await?,
                    RunCommands::Cancel { id, reason } => {
                        commands::run_cancel(&mut client, &id, reason).await?
                    }
                    RunCommands::Issue { number, full } => {
                        let max_steps = if full { 20 } else { 1 };
                        commands::run_workflow_chain(
                            &mut client,
                            "plan_issue",
                            number,
                            max_steps,
                            true,
                        )
                        .await?
                    }
                    RunCommands::Milestone { name, dry_run } => {
                        commands::milestone::handle_sweep(&mut client, &name, dry_run).await?
                    }
                },
                Commands::Milestone { command } => match command {
                    MilestoneCommands::Next => {
                        println!("Next issue: use `darq milestone sweep` to process all issues");
                    }
                    MilestoneCommands::Sweep { name, dry_run } => {
                        commands::milestone::handle_sweep(&mut client, &name, dry_run).await?
                    }
                },
                Commands::Issue { command } => {
                    commands::issue::handle(command, &mut client).await?
                }
                Commands::Pr { command } => commands::pr::handle(command, &mut client).await?,
                Commands::Sat { command } => commands::sat::handle(command, &mut client).await?,
                Commands::Stats => commands::stats(&mut client).await?,
                _ => unreachable!(), // Daemon and TUI handled above
            }
        }
    }

    Ok(())
}