malvin 0.2.4

Non-interactive research and coding agent
use super::{
    Commands, Exit, SharedOpts, WorkflowCliOptions, run_do, run_init, run_router,
};
use crate::do_flow::DoArgs;

pub(crate) const fn command_accepts_session_name(_command: &Commands) -> bool {
    false
}

pub(crate) const fn unsupported_name_error() -> &'static str {
    "`--name` is only supported for bare `malvin REQUEST`, `--do`, and `malvin -g`"
}

#[path = "entrypoint_from.rs"]
mod entrypoint_from;
#[path = "entrypoint_gates_only.rs"]
mod entrypoint_gates_only;
#[path = "entrypoint_short_help.rs"]
mod entrypoint_short_help;
pub use entrypoint_from::entrypoint_from;
pub(crate) use entrypoint_gates_only::dispatch_gates_only_route;

pub fn print_command_error(message: &str) {
    use crate::output::{MALVIN_WHO, print_log_error, print_stderr_line};
    use crate::repo_checks::{
        GATE_FAILURE_MARKER, is_gate_failure_error, is_pure_gate_failure_summary,
    };
    if is_pure_gate_failure_summary(message) {
        return;
    }
    if is_gate_failure_error(message) {
        let display = message.replace(GATE_FAILURE_MARKER, "");
        print_stderr_line(MALVIN_WHO, &display);
        return;
    }
    if super::error_run_log::command_error_already_emitted(message) {
        return;
    }
    super::error_run_log::note_command_error_emitted(message);
    super::error_run_log::append_command_error_to_run_log(message);
    print_log_error(message);
}

pub fn try_tokio_runtime() -> Result<tokio::runtime::Runtime, String> {
    tokio::runtime::Builder::new_multi_thread()
        .enable_all()
        .build()
        .map_err(|e| format!("failed to create Tokio runtime: {e}"))
}

pub fn run_async_cli<F, Fut>(f: F) -> Result<(), String>
where
    F: FnOnce() -> Fut,
    Fut: std::future::Future<Output = Result<(), String>> + Send,
{
    let rt = try_tokio_runtime()?;
    rt.block_on(async {
        spawn_ctrl_c_teardown();
        f().await
    })
}

fn spawn_ctrl_c_teardown() {
    tokio::spawn(async {
        if tokio::signal::ctrl_c().await.is_err() {
            return;
        }
        crate::malvin_sandbox::teardown_active_sandbox_for_interrupt();
        std::process::exit(130);
    });
}

pub fn entrypoint() -> Exit {
    entrypoint_from(std::env::args_os())
}

pub(crate) fn finish_entrypoint(res: Result<(), String>) -> Exit {
    match res {
        Ok(()) => {
            super::error_run_log::clear_command_error_run_dir();
            Exit::Success
        }
        Err(e) => {
            print_command_error(&e);
            super::error_run_log::clear_command_error_run_dir();
            Exit::Failure
        }
    }
}

pub(crate) fn prepare_cli_output(global: &crate::cli::args::GlobalOpts) {
    let theme = std::env::current_dir()
        .ok()
        .map(|cwd| crate::malvin_config_file::load_malvin_config(&cwd).theme)
        .unwrap_or_default();
    crate::terminal_palette::init_terminal_theme(theme);
    crate::output::init_stdout_style();
    crate::output::set_stdout_suppressed(global.background);
}

pub(crate) fn dispatch_command(
    command: Commands,
    shared: &SharedOpts,
    matches: &clap::ArgMatches,
) -> Result<(), String> {
    let mut shared = shared.clone();
    match command {
        cmd @ Commands::Write(_) => {
            super::entrypoint_commands::dispatch_plan_authoring_gate(cmd, &mut shared, matches)
        }
        Commands::Inspire(inspire) | Commands::Adaptix(inspire) => {
            super::entrypoint_commands::run_inspire_command(inspire, &shared)
        }
        Commands::Models(models) => dispatch_models(models, &shared),
    }
}

pub fn dispatch_do_workflow(do_args: DoArgs, shared: &SharedOpts) -> Result<(), String> {
    run_async_cli(|| {
        run_do(
            do_args,
            shared,
            WorkflowCliOptions {
                force: !shared.no_force,
            },
        )
    })
}

pub struct DefaultRouteDispatch<'a> {
    pub request: String,
    pub max_loops: usize,
    pub max_hypotheses: usize,
    pub shared: &'a mut SharedOpts,
    pub matches: &'a clap::ArgMatches,
}

pub fn dispatch_default_route(input: DefaultRouteDispatch<'_>) -> Result<(), String> {
    use crate::router_flow::RouterArgs;
    let DefaultRouteDispatch {
        request,
        mut max_loops,
        max_hypotheses,
        shared,
        matches,
    } = input;
    super::loop_opts::apply_default_route_tenacious(
        &mut max_loops,
        &mut shared.max_acp_retries,
        shared.no_tenacious,
        matches,
    );
    if crate::cli::init_flow::should_bootstrap_gates(shared)? {
        let bootstrap_shared = crate::cli::init_flow::shared_for_init_bootstrap(shared);
        return run_async_cli(|| {
            run_init(
                crate::cli::init_flow::InitWorkflowOpts {
                    max_loops,
                    max_hypotheses,
                },
                &bootstrap_shared,
                WorkflowCliOptions {
                    force: !shared.no_force,
                },
            )
        });
    }
    run_async_cli(|| {
        run_router(
            RouterArgs {
                request: Some(request),
                max_loops,
                max_hypotheses,
            },
            shared,
            WorkflowCliOptions {
                force: !shared.no_force,
            },
        )
    })
}

fn dispatch_models(
    models: super::models_cmd::ModelsArgs,
    shared: &super::SharedOpts,
) -> Result<(), String> {
    let model = shared.model.canonical();
    super::models_cmd::run_models(models, &model)
}

#[cfg(test)]
#[path = "entrypoint_tenacious_tests.rs"]
mod entrypoint_tenacious_tests;

#[cfg(test)]
#[path = "entrypoint_doc_tests.rs"]
mod entrypoint_doc_tests;

#[cfg(test)]
#[path = "entrypoint_name_tests.rs"]
mod entrypoint_name_tests;

#[cfg(test)]
#[path = "entrypoint_name_unix_tests.rs"]
mod entrypoint_name_unix_tests;