locode-exec 0.1.16

Minimal headless runner for the locode agent engine - one JSON report on stdout
Documentation
//! clap surface (plan §3.1 + the positional-prompt addendum).
//!
//! The prompt is the **positional** argument — the field convention (Claude
//! Code's `-p/--print` is a mode flag with the prompt positional; `codex exec
//! "…"` likewise), and `locode-exec` is always headless so it needs no mode
//! flag at all. `-` or omitted → read stdin (Codex's convention).

use std::path::PathBuf;

use clap::{Parser, ValueEnum};

/// Minimal headless runner for the locode engine: one JSON report on stdout
/// (ADR-0009), diagnostics on stderr, exit code from the run's status.
#[derive(Parser, Debug)]
#[command(name = "locode-exec", version, about)]
#[allow(clippy::struct_excessive_bools)] // CLI flags are naturally bools
pub struct Cli {
    /// The task prompt. `-` or omitted reads the prompt from stdin.
    pub prompt: Option<String>,

    /// Workspace root / working directory (the path-jail root). Defaults to
    /// the current directory.
    #[arg(long)]
    pub cwd: Option<PathBuf>,

    /// Harness pack selecting the toolset + system prompt. Omitted ⇒ the
    /// settings `harness` default (ADR-0024 §1.4), else `claude`.
    #[arg(long, value_enum)]
    pub harness: Option<Harness>,

    /// Provider wire schema: `anthropic`, `openai-responses`, or `mock`
    /// (keyless CI) — plus any custom providers the binary registered
    /// (ADR-0015). Unknown names fail pre-run listing the available set.
    /// Omitted ⇒ the settings `api_schema` default (ADR-0024 §1.4 — the
    /// project layers may not set it), else `anthropic`.
    #[arg(long, env = "LOCODE_API_SCHEMA")]
    pub api_schema: Option<String>,

    /// Model id override. Omitted ⇒ the settings `model` default, else the
    /// wire's built-in default (ADR-0024 §1.4 — same precedence chain as every
    /// other knob; there is deliberately no model env var).
    #[arg(long)]
    pub model: Option<String>,

    /// Extra settings layer: a path to a JSON file, or inline JSON (highest-
    /// precedence settings layer, ADR-0024 §1.2).
    #[arg(long)]
    pub settings: Option<String>,

    /// Do not write (or append to) a session trace for this run — nothing lands
    /// under `~/.locode/sessions` (ADR-0024 §2; Claude Code's
    /// `--no-session-persistence`). `--continue`/`--resume` still *read*.
    #[arg(long)]
    pub no_session_persistence: bool,

    /// Continue the newest session started in this cwd (ADR-0024 §2.5): the
    /// recovered transcript seeds the run and the same rollout keeps appending.
    #[arg(short = 'c', long = "continue", conflicts_with = "resume")]
    pub continue_session: bool,

    /// Resume the session with this id — found in this cwd's sessions first,
    /// then anywhere (ADR-0024 §2.5).
    #[arg(short = 'r', long = "resume", value_name = "SESSION_ID")]
    pub resume: Option<String>,

    /// Hard ceiling on sample→dispatch turns. **Unlimited when omitted**
    /// (ADR-0005 amendment — no studied harness caps turns by default).
    #[arg(long)]
    pub max_turns: Option<u32>,

    /// stdout contract: `json` = one Report; `stream-json` = Event JSONL;
    /// `text` = the final message.
    #[arg(long, value_enum, default_value_t = OutputFormat::Json)]
    pub output_format: OutputFormat,

    /// Restrict file access to the working directory and ask before each tool
    /// call. Incomplete — there is no way to record an answer yet, so every call
    /// asks again. Off by default.
    #[arg(
        long,
        visible_alias = "no-yolo",
        conflicts_with = "dangerously_skip_permissions"
    )]
    pub restricted: bool,

    /// Accepted for compatibility and does nothing — this is the default now.
    /// Use `--restricted` to turn the restrictions on.
    #[arg(long, visible_alias = "yolo", hide = true)]
    pub dangerously_skip_permissions: bool,

    /// Strip harness identity sentences from the rendered system prompt
    /// (A/B contamination control; default = faithful reproduction).
    #[arg(long)]
    pub strip_identity: bool,

    /// Stream the model turn (SSE) instead of one buffered call (ADR-0021).
    /// Needed for **unbounded output** — Anthropic rejects non-streaming
    /// requests that may exceed ~10 min. The `stream-json` trace stays
    /// whole-message: token deltas are assembled but **not** written to it.
    #[arg(long)]
    pub stream: bool,

    /// Skip project-instruction loading (`AGENTS.md`) — the `--bare`-style disable
    /// (ADR-0023). Auto-discovery is otherwise on by default.
    #[arg(long)]
    pub no_project_instructions: bool,

    /// Add a directory the agent may read and write, beyond the working
    /// directory. Repeatable. Its `AGENTS.md` and `.agents/skills` are picked up
    /// too — the point of pointing at a subtree of a large monorepo.
    #[arg(long = "add-dir", value_name = "DIR")]
    pub add_dir: Vec<std::path::PathBuf>,

    /// How hard the model should think. Omitted uses the `effort` setting, then
    /// the API's own default.
    #[arg(long, value_name = "LEVEL")]
    pub effort: Option<EffortArg>,
}

/// How hard the model should think. locode's own ladder — each wire maps it
/// onto whatever that API accepts, so the same word means the same intent
/// whichever model is in use.
#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
#[value(rename_all = "kebab-case")]
pub enum EffortArg {
    /// Fastest; short, scoped tasks.
    Low,
    /// Balanced.
    Medium,
    /// The API default.
    High,
    /// Best for coding and agentic work.
    XHigh,
    /// Deepest; correctness over cost.
    Max,
}

impl From<EffortArg> for locode_core::Effort {
    fn from(arg: EffortArg) -> Self {
        match arg {
            EffortArg::Low => locode_core::Effort::Low,
            EffortArg::Medium => locode_core::Effort::Medium,
            EffortArg::High => locode_core::Effort::High,
            EffortArg::XHigh => locode_core::Effort::XHigh,
            EffortArg::Max => locode_core::Effort::Max,
        }
    }
}

/// The registered harness packs (a closed set — clap validates and lists them).
#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
#[value(rename_all = "kebab-case")]
pub enum Harness {
    /// Grok Build's toolset + system prompt.
    Grok,
    /// Claude Code's toolset + system prompt.
    Claude,
    /// Codex CLI's toolset + system prompt.
    Codex,
}

impl Harness {
    /// The pack name for `locode_core::resolve`.
    #[must_use]
    pub fn as_str(self) -> &'static str {
        match self {
            Harness::Grok => "grok",
            Harness::Claude => "claude",
            Harness::Codex => "codex",
        }
    }
}

/// The stdout artifact selector (ADR-0014; Claude Code's three-way shape).
#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
#[value(rename_all = "kebab-case")]
pub enum OutputFormat {
    /// One `Report` JSON object (the default).
    Json,
    /// The final assistant message only.
    Text,
    /// The live JSONL `Event` stream (`init` → `message`… → `result`).
    StreamJson,
}