Skip to main content

locode_exec/
cli.rs

1//! clap surface (plan §3.1 + the positional-prompt addendum).
2//!
3//! The prompt is the **positional** argument — the field convention (Claude
4//! Code's `-p/--print` is a mode flag with the prompt positional; `codex exec
5//! "…"` likewise), and `locode-exec` is always headless so it needs no mode
6//! flag at all. `-` or omitted → read stdin (Codex's convention).
7
8use std::path::PathBuf;
9
10use clap::{Parser, ValueEnum};
11
12/// Minimal headless runner for the locode engine: one JSON report on stdout
13/// (ADR-0009), diagnostics on stderr, exit code from the run's status.
14#[derive(Parser, Debug)]
15#[command(name = "locode-exec", version, about)]
16#[allow(clippy::struct_excessive_bools)] // CLI flags are naturally bools
17pub struct Cli {
18    /// The task prompt. `-` or omitted reads the prompt from stdin.
19    pub prompt: Option<String>,
20
21    /// Workspace root / working directory (the path-jail root). Defaults to
22    /// the current directory.
23    #[arg(long)]
24    pub cwd: Option<PathBuf>,
25
26    /// Harness pack selecting the toolset + system prompt. Omitted ⇒ the
27    /// settings `harness` default (ADR-0024 §1.4), else `claude`.
28    #[arg(long, value_enum)]
29    pub harness: Option<Harness>,
30
31    /// Provider wire schema: `anthropic`, `openai-responses`, or `mock`
32    /// (keyless CI) — plus any custom providers the binary registered
33    /// (ADR-0015). Unknown names fail pre-run listing the available set.
34    /// Omitted ⇒ the settings `api_schema` default (ADR-0024 §1.4 — the
35    /// project layers may not set it), else `anthropic`.
36    #[arg(long, env = "LOCODE_API_SCHEMA")]
37    pub api_schema: Option<String>,
38
39    /// Model id override. Omitted ⇒ the settings `model` default, else the
40    /// wire's built-in default (ADR-0024 §1.4 — same precedence chain as every
41    /// other knob; there is deliberately no model env var).
42    #[arg(long)]
43    pub model: Option<String>,
44
45    /// Extra settings layer: a path to a JSON file, or inline JSON (highest-
46    /// precedence settings layer, ADR-0024 §1.2).
47    #[arg(long)]
48    pub settings: Option<String>,
49
50    /// Do not write (or append to) a session trace for this run — nothing lands
51    /// under `~/.locode/sessions` (ADR-0024 §2; Claude Code's
52    /// `--no-session-persistence`). `--continue`/`--resume` still *read*.
53    #[arg(long)]
54    pub no_session_persistence: bool,
55
56    /// Continue the newest session started in this cwd (ADR-0024 §2.5): the
57    /// recovered transcript seeds the run and the same rollout keeps appending.
58    #[arg(short = 'c', long = "continue", conflicts_with = "resume")]
59    pub continue_session: bool,
60
61    /// Resume the session with this id — found in this cwd's sessions first,
62    /// then anywhere (ADR-0024 §2.5).
63    #[arg(short = 'r', long = "resume", value_name = "SESSION_ID")]
64    pub resume: Option<String>,
65
66    /// Hard ceiling on sample→dispatch turns. **Unlimited when omitted**
67    /// (ADR-0005 amendment — no studied harness caps turns by default).
68    #[arg(long)]
69    pub max_turns: Option<u32>,
70
71    /// stdout contract: `json` = one Report; `stream-json` = Event JSONL;
72    /// `text` = the final message.
73    #[arg(long, value_enum, default_value_t = OutputFormat::Json)]
74    pub output_format: OutputFormat,
75
76    /// Disable the path jail (`PathPolicy::Unrestricted`). The shell's
77    /// timeout/output caps stay on (ADR-0008 amendment).
78    #[arg(long, visible_alias = "yolo")]
79    pub dangerously_skip_permissions: bool,
80
81    /// Strip harness identity sentences from the rendered system prompt
82    /// (A/B contamination control; default = faithful reproduction).
83    #[arg(long)]
84    pub strip_identity: bool,
85
86    /// Stream the model turn (SSE) instead of one buffered call (ADR-0021).
87    /// Needed for **unbounded output** — Anthropic rejects non-streaming
88    /// requests that may exceed ~10 min. The `stream-json` trace stays
89    /// whole-message: token deltas are assembled but **not** written to it.
90    #[arg(long)]
91    pub stream: bool,
92
93    /// Skip project-instruction loading (`AGENTS.md`) — the `--bare`-style disable
94    /// (ADR-0023). Auto-discovery is otherwise on by default.
95    #[arg(long)]
96    pub no_project_instructions: bool,
97}
98
99/// The registered harness packs (a closed set — clap validates and lists them).
100#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
101#[value(rename_all = "kebab-case")]
102pub enum Harness {
103    /// Grok Build's toolset + system prompt (ADR-0012).
104    Grok,
105    /// Claude Code's toolset + system prompt (ADR-0012, ADR-0023).
106    Claude,
107    /// Codex CLI's toolset + system prompt (ADR-0012, ADR-0023).
108    Codex,
109}
110
111impl Harness {
112    /// The pack name for `locode_core::resolve`.
113    #[must_use]
114    pub fn as_str(self) -> &'static str {
115        match self {
116            Harness::Grok => "grok",
117            Harness::Claude => "claude",
118            Harness::Codex => "codex",
119        }
120    }
121}
122
123/// The stdout artifact selector (ADR-0014; Claude Code's three-way shape).
124#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
125#[value(rename_all = "kebab-case")]
126pub enum OutputFormat {
127    /// One `Report` JSON object (the default).
128    Json,
129    /// The final assistant message only.
130    Text,
131    /// The live JSONL `Event` stream (`init` → `message`… → `result`).
132    StreamJson,
133}