forge-pilot 0.1.0

Closed-loop orchestrator over runtime advisories, kernel oracles, and canonical Forge export/import lanes
Documentation
use forge_pilot::{
    bootstrap_source_workspace, import_recent_forge_bundles, observe_scope,
    render_candidate_report, render_loop_report, render_observation_report, score_targets,
    LoopRunner, PilotHistory,
};
use knowledge_runtime::Scope;
use std::path::{Path, PathBuf};

use super::explain::{
    explain_bootstrap_report, explain_candidates, explain_import_report, explain_loop_report,
    explain_observation,
};
use super::storage::{build_loop_config, normalize_user_path, open_resources};

pub(super) async fn run_command_cli(argv: Vec<String>) -> Result<(), String> {
    let args = CommandArgs::parse(argv)?;
    if args.help {
        print!("{}", CommandArgs::help());
        return Ok(());
    }

    let scope = Scope::new(args.namespace.clone());
    let config = build_loop_config(
        &scope,
        args.workspace_path.clone(),
        &args.memory_dir,
        &args.forge_db,
        args.loop_interval_secs,
        args.max_iterations,
    );
    let resources = open_resources(&args.memory_dir, &args.forge_db, &config)?;

    match args.command.as_str() {
        "observe" => {
            let observation = observe_scope(&resources.runtime, &resources.memory_store, &config)
                .await
                .map_err(|error| error.to_string())?;
            if args.json {
                let report = render_observation_report(&observation, true);
                println!("{report}");
            } else {
                println!("{}", explain_observation(&observation));
            }
        }
        "candidates" => {
            let observation = observe_scope(&resources.runtime, &resources.memory_store, &config)
                .await
                .map_err(|error| error.to_string())?;
            let candidates = score_targets(&observation, &PilotHistory::default(), &config);
            if args.json {
                let report = render_candidate_report(&candidates, true);
                println!("{report}");
            } else {
                println!("{}", explain_candidates(&candidates));
            }
        }
        "bootstrap-source" => {
            let report = bootstrap_source_workspace(&resources.memory_store, &config)
                .await
                .map_err(|error| error.to_string())?;
            if args.json {
                println!(
                    "{}",
                    serde_json::to_string_pretty(&report).map_err(|error| error.to_string())?
                );
            } else {
                println!("{}", explain_bootstrap_report(&report));
            }
        }
        "import" => {
            let report = import_recent_forge_bundles(
                &args.namespace,
                &resources.forge_store,
                &resources.memory_store,
                64,
            )
            .await
            .map_err(|error| error.to_string())?;
            if args.json {
                println!(
                    "{}",
                    serde_json::to_string_pretty(&report).map_err(|error| error.to_string())?
                );
            } else {
                println!("{}", explain_import_report(&report));
            }
        }
        "run" => {
            let mut runner = LoopRunner::new(config, resources);
            let report = runner.run().await.map_err(|error| error.to_string())?;
            if args.json {
                let json_report = render_loop_report(&report, true);
                println!("{json_report}");
            } else {
                println!("{}", explain_loop_report(&report));
            }
        }
        _ => {
            return Err(format!(
                "unknown command `{}`\n\n{}",
                args.command,
                CommandArgs::help()
            ));
        }
    }

    Ok(())
}

pub(super) struct CommandArgs {
    command: String,
    namespace: String,
    memory_dir: PathBuf,
    forge_db: PathBuf,
    workspace_path: String,
    loop_interval_secs: u64,
    max_iterations: u32,
    json: bool,
    help: bool,
}

impl CommandArgs {
    pub(super) fn parse(argv: Vec<String>) -> Result<Self, String> {
        let mut args = Self::default();
        let mut iter = argv.into_iter();
        while let Some(arg) = iter.next() {
            match arg.as_str() {
                "-h" | "--help" => args.help = true,
                "--json" => args.json = true,
                "--namespace" => {
                    args.namespace = iter
                        .next()
                        .ok_or_else(|| "--namespace requires a value".to_string())?;
                }
                "--memory-dir" => {
                    args.memory_dir = PathBuf::from(
                        iter.next()
                            .ok_or_else(|| "--memory-dir requires a value".to_string())?,
                    );
                }
                "--forge-db" => {
                    args.forge_db = PathBuf::from(
                        iter.next()
                            .ok_or_else(|| "--forge-db requires a value".to_string())?,
                    );
                }
                "--workspace-path" => {
                    args.workspace_path = iter
                        .next()
                        .ok_or_else(|| "--workspace-path requires a value".to_string())?;
                }
                "--workspace" => {
                    args.workspace_path = iter
                        .next()
                        .ok_or_else(|| "--workspace requires a value".to_string())?;
                }
                "--loop-interval" => {
                    args.loop_interval_secs = iter
                        .next()
                        .ok_or_else(|| "--loop-interval requires a value".to_string())?
                        .parse()
                        .map_err(|_| "--loop-interval must be an integer".to_string())?;
                }
                "--max-iterations" => {
                    args.max_iterations = iter
                        .next()
                        .ok_or_else(|| "--max-iterations requires a value".to_string())?
                        .parse()
                        .map_err(|_| "--max-iterations must be an integer".to_string())?;
                }
                value if value.starts_with('-') => {
                    return Err(format!("unknown flag `{value}`\n\n{}", Self::help()));
                }
                value => args.command = value.into(),
            }
        }
        args.memory_dir = normalize_user_path(&args.memory_dir);
        args.forge_db = normalize_user_path(&args.forge_db);
        args.workspace_path = normalize_user_path(Path::new(&args.workspace_path))
            .to_string_lossy()
            .to_string();
        Ok(args)
    }

    pub(super) fn help() -> &'static str {
        "forge-pilot\n\nUSAGE:\n  cargo run -p forge-pilot --                 # interactive plain-English wrapper\n  cargo run -p forge-pilot -- tui             # same interactive wrapper\n  cargo run -p forge-pilot -- <command> [options]\n\nCOMMANDS:\n  observe           Reconstruct and print the current observation\n  candidates        Score and print current target candidates\n  bootstrap-source  Scan the workspace and bootstrap thin imported source state\n  import            Import recent Forge evidence bundles through the canonical export -> bridge -> memory lane\n  run               Execute one bounded pilot loop\n\nOPTIONS:\n  --namespace <name>        Namespace to inspect (default: default)\n  --memory-dir <path>       Semantic memory base dir (default: ./memory)\n  --forge-db <path>         Forge DB path (default: ./forge.db)\n  --workspace-path <path>   Workspace path recorded in reports (default: .)\n  --workspace <path>        Alias for --workspace-path\n  --loop-interval <secs>    Background loop pause for the TUI worker (default: 5)\n  --max-iterations <n>      Max iterations per bounded run (default: 4)\n  --json                    Print raw JSON for command mode\n  -h, --help                Show this help\n"
    }
}

impl Default for CommandArgs {
    fn default() -> Self {
        Self {
            command: "run".into(),
            namespace: "default".into(),
            memory_dir: PathBuf::from("memory"),
            forge_db: PathBuf::from("forge.db"),
            workspace_path: ".".into(),
            loop_interval_secs: 5,
            max_iterations: 4,
            json: false,
            help: false,
        }
    }
}