crtx 0.1.1

CLI for the Cortex supervisory memory substrate.
//! `cortex` — operator CLI for the Cortex supervisory memory substrate.
//!
//! Lane 1.C wires the bootstrap commands (BUILD_SPEC §11):
//!
//! - `cortex init` — create the data directory, DB file, JSONL log.
//! - `cortex ingest` — append session events to the JSONL ledger.
//! - `cortex audit verify` — verify the JSONL hash chain end-to-end.
//! - `cortex reflect` — produce a deterministic `SessionReflection` via the
//!   replay adapter.
//! - Phase 2 command surfaces (`memory`, `principles`, `principle promote`,
//!   `context build`, `run`) fail closed until their durable backing APIs are
//!   wired.
//!
//! Every subcommand returns a typed [`exit::Exit`] code. `main` casts the
//! variant to `i32` and hands it to [`std::process::exit`]; there is no
//! other mechanism for signalling success or failure to a parent shell.
//!
//! ## Why no `anyhow` plumbing here
//!
//! The exit-code table (BUILD_SPEC §11) is the contract. Wrapping it in
//! `Result<(), anyhow::Error>` would force every error to round-trip
//! through `Display` and lose the typed exit. The dispatcher therefore
//! works in `Exit` directly.

#![deny(unsafe_code, missing_debug_implementations)]
#![warn(missing_docs)]

mod cmd;
mod config;
mod exit;
mod output;
mod paths;

use clap::{Parser, Subcommand};
use tracing_subscriber::EnvFilter;

use crate::cmd::{
    audit::AuditSub, completions::CompletionsArgs, context::ContextSub, init::InitArgs,
    session::SessionSub,
};
use crate::exit::Exit;

const COMMAND_OBSERVABILITY_OPERATION: &str = "cli.command";
const COMMAND_OBSERVABILITY_SCHEMA_VERSION: u64 = 1;

/// Top-level `cortex` CLI.
#[derive(Debug, Parser)]
#[command(
    name = "cortex",
    version,
    about = "Longitudinal supervisory memory substrate — CLI entrypoint",
    propagate_version = true
)]
pub(crate) struct Cli {
    /// Print effective non-secret configuration as JSON.
    #[arg(long = "print-config")]
    print_config: bool,

    /// Emit a structured JSON envelope on stdout instead of the
    /// human-readable command output.
    ///
    /// Phase 4.A polish: every subcommand that ships an envelope serializes
    /// the same `command`/`exit_code`/`outcome`/`report` shape so downstream
    /// tooling can grep on a single set of fields rather than parsing prose.
    /// The flag does not change exit codes or any side effect — it only
    /// switches the stdout rendering.
    ///
    /// Must appear before the subcommand: `cortex --json audit verify ...`.
    /// The flag is intentionally NOT `global = true` so existing per-
    /// subcommand `--json <string>` parameters (e.g. `memory admit-axiom
    /// --json <inline_json>`) keep their meaning.
    #[arg(long = "json")]
    json: bool,

    #[command(subcommand)]
    cmd: Option<Cmd>,
}

/// Top-level subcommands. Each variant maps to a module under `cmd/`.
#[derive(Debug, Subcommand)]
enum Cmd {
    /// Initialise the local Cortex data directory (DB + JSONL log).
    Init(InitArgs),

    /// Ingest session events into the append-only JSONL ledger.
    Ingest(cmd::ingest::IngestArgs),

    /// Audit subcommands (hash-chain/signed-chain verification, export diagnostics, etc.).
    Audit {
        /// Audit operation to run.
        #[command(subcommand)]
        sub: AuditSub,
    },

    /// Create a local offline SQLite + JSONL backup bundle.
    Backup(cmd::backup::BackupArgs),

    /// Release-readiness authority gates.
    Release {
        /// Release operation to run.
        #[command(subcommand)]
        sub: cmd::release::ReleaseSub,
    },

    /// Compliance evidence authority gates.
    Compliance {
        /// Compliance operation to run.
        #[command(subcommand)]
        sub: cmd::compliance::ComplianceSub,
    },

    /// Operator-fired scheduled decay-job surface (Phase 4.D).
    Decay {
        /// Decay operation to run.
        #[command(subcommand)]
        sub: cmd::decay::DecaySub,
    },

    /// Run reflection over a trace (read-only at lane 1.C — prints, does
    /// not persist).
    Reflect(cmd::reflect::ReflectArgs),

    /// Memory lifecycle and retrieval commands.
    Memory {
        /// Memory operation to run.
        #[command(subcommand)]
        sub: cmd::memory::MemorySub,
    },

    /// Principle candidate extraction commands.
    Principles {
        /// Principles operation to run.
        #[command(subcommand)]
        sub: cmd::principles::PrinciplesSub,
    },

    /// Single-principle doctrine operations.
    Principle {
        /// Principle operation to run.
        #[command(subcommand)]
        sub: cmd::principle_promote::PrincipleSub,
    },

    /// Context-pack commands.
    Context {
        /// Context operation to run.
        #[command(subcommand)]
        sub: ContextSub,
    },

    /// Proof-closure diagnostics.
    Proof {
        /// Proof operation to run.
        #[command(subcommand)]
        sub: cmd::proof::ProofSub,
    },

    /// Schema migration commands.
    Migrate {
        /// Migration operation to run.
        #[command(subcommand)]
        sub: cmd::migrate::MigrateSub,
    },

    /// Restore validation commands.
    Restore {
        /// Restore operation to run.
        #[command(subcommand)]
        sub: cmd::restore::RestoreSub,
    },

    /// Strict store precondition checks.
    Doctor(cmd::doctor::DoctorArgs),

    /// Ed25519 detached sign of an arbitrary payload file.
    Sign(cmd::sign::SignArgs),

    /// Run a task with Cortex context.
    Run(cmd::run::RunArgs),

    /// Development drill for JSONL-ack-before-SQLite recovery.
    RunLedgerDrill(cmd::run_ledger_drill::RunLedgerDrillArgs),

    /// Session lifecycle commands.
    Session {
        /// Session operation to run.
        #[command(subcommand)]
        sub: SessionSub,
    },

    /// Emit a shell completion script to stdout for the requested shell.
    ///
    /// Pipe to your shell's completion path, e.g.
    /// `cortex completions bash > ~/.bash_completion.d/cortex`. See
    /// `docs/RUNBOOK.md` for per-shell setup details.
    Completions(CompletionsArgs),

    /// Start the Cortex MCP stdio server (`cortex serve`).
    ///
    /// Exposes Cortex memories, context, and session-close as JSON-RPC 2.0
    /// tools over stdin/stdout for Claude Code and other MCP-compatible clients.
    Serve(cmd::serve::ServeArgs),
    /// Discover available models from the configured LLM backend.
    Models {
        /// Models operation to run.
        #[command(subcommand)]
        sub: cmd::models::ModelsSub,
    },
}

fn main() {
    tracing_subscriber::fmt()
        .with_env_filter(EnvFilter::from_default_env())
        .with_writer(std::io::stderr)
        .init();

    // `clap`'s default `Cli::parse()` aborts the process with exit code 2
    // for parse failures, which lines up with `Exit::Usage`. We use the
    // `try_parse` form only so we can surface a typed error if needed in
    // tests; the runtime path is identical.
    let cli = match Cli::try_parse() {
        Ok(c) => c,
        Err(e) => {
            // `clap` writes the error to stderr in its canonical format.
            // Use `e.exit_code()` so `--help` / `--version` exit 0 (as
            // they do under `Cli::parse()`).
            e.print().ok();
            std::process::exit(if e.use_stderr() {
                Exit::Usage as i32
            } else {
                Exit::Ok as i32
            });
        }
    };

    output::set_json_mode(cli.json);

    let command = cli.command_name();
    let correlation_id = new_command_correlation_id();
    output::set_correlation_id(correlation_id.clone());
    tracing::info!(
        audit_schema_version = COMMAND_OBSERVABILITY_SCHEMA_VERSION,
        operation = COMMAND_OBSERVABILITY_OPERATION,
        correlation_id = %correlation_id,
        command,
        proof_state = "UNKNOWN",
        authority_class = "diagnostic_only",
        diagnostic_only = true,
        "command_started"
    );

    let exit = dispatch(cli);
    tracing::info!(
        audit_schema_version = COMMAND_OBSERVABILITY_SCHEMA_VERSION,
        operation = COMMAND_OBSERVABILITY_OPERATION,
        correlation_id = %correlation_id,
        command,
        exit_code = exit.code(),
        status = exit_status(exit),
        proof_state = "UNKNOWN",
        authority_class = "diagnostic_only",
        diagnostic_only = true,
        "command_completed"
    );
    std::process::exit(exit.code());
}

fn dispatch(cli: Cli) -> Exit {
    if cli.print_config {
        if cli.cmd.is_some() {
            eprintln!("cortex --print-config: cannot be combined with a subcommand");
            return Exit::Usage;
        }
        return config::print_effective_config();
    }

    match cli.cmd {
        Some(Cmd::Init(args)) => cmd::init::run(args),
        Some(Cmd::Ingest(args)) => cmd::ingest::run(args),
        Some(Cmd::Audit { sub }) => cmd::audit::run(sub),
        Some(Cmd::Backup(args)) => cmd::backup::run(args),
        Some(Cmd::Release { sub }) => cmd::release::run(sub),
        Some(Cmd::Compliance { sub }) => cmd::compliance::run(sub),
        Some(Cmd::Decay { sub }) => cmd::decay::run(sub),
        Some(Cmd::Reflect(args)) => cmd::reflect::run(args),
        Some(Cmd::Memory { sub }) => cmd::memory::run(sub),
        Some(Cmd::Principles { sub }) => cmd::principles::run(sub),
        Some(Cmd::Principle { sub }) => cmd::principle_promote::run(sub),
        Some(Cmd::Context { sub }) => cmd::context::run(sub),
        Some(Cmd::Proof { sub }) => cmd::proof::run(sub),
        Some(Cmd::Migrate { sub }) => cmd::migrate::run(sub),
        Some(Cmd::Restore { sub }) => cmd::restore::run(sub),
        Some(Cmd::Doctor(args)) => cmd::doctor::run(args),
        Some(Cmd::Sign(args)) => cmd::sign::run(args),
        Some(Cmd::Run(args)) => cmd::run::run(args),
        Some(Cmd::RunLedgerDrill(args)) => cmd::run_ledger_drill::run(args),
        Some(Cmd::Session { sub }) => cmd::session::run(sub),
        Some(Cmd::Completions(args)) => cmd::completions::run(args),
        Some(Cmd::Serve(args)) => cmd::serve::run(args),
        Some(Cmd::Models { sub }) => cmd::models::run(sub),
        None => {
            if output::json_enabled() {
                let payload = serde_json::json!({ "detail": "missing subcommand" });
                let envelope = output::Envelope::new("cortex", Exit::Usage, payload);
                return output::emit(&envelope, Exit::Usage);
            }
            eprintln!("cortex: missing subcommand; use --help for usage");
            Exit::Usage
        }
    }
}

impl Cli {
    fn command_name(&self) -> &'static str {
        if self.print_config {
            return "print_config";
        }
        match self.cmd.as_ref() {
            Some(cmd) => cmd.command_name(),
            None => "none",
        }
    }
}

impl Cmd {
    fn command_name(&self) -> &'static str {
        match self {
            Cmd::Init(_) => "init",
            Cmd::Ingest(_) => "ingest",
            Cmd::Audit { .. } => "audit",
            Cmd::Backup(_) => "backup",
            Cmd::Release { .. } => "release",
            Cmd::Compliance { .. } => "compliance",
            Cmd::Decay { .. } => "decay",
            Cmd::Reflect(_) => "reflect",
            Cmd::Memory { .. } => "memory",
            Cmd::Principles { .. } => "principles",
            Cmd::Principle { .. } => "principle",
            Cmd::Context { .. } => "context",
            Cmd::Proof { .. } => "proof",
            Cmd::Migrate { .. } => "migrate",
            Cmd::Restore { .. } => "restore",
            Cmd::Doctor(_) => "doctor",
            Cmd::Sign(_) => "sign",
            Cmd::Run(_) => "run",
            Cmd::RunLedgerDrill(_) => "run-ledger-drill",
            Cmd::Session { .. } => "session",
            Cmd::Completions(_) => "completions",
            Cmd::Serve(_) => "serve",
            Cmd::Models { .. } => "models",
        }
    }
}

fn new_command_correlation_id() -> String {
    let now = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap_or_default()
        .as_nanos();
    format!("cmd_{now:x}_{:x}", std::process::id())
}

fn exit_status(exit: Exit) -> &'static str {
    if exit == Exit::Ok {
        "ok"
    } else {
        "error"
    }
}