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 /// Restrict file access to the working directory and ask before each tool
77 /// call. Incomplete — there is no way to record an answer yet, so every call
78 /// asks again. Off by default.
79 #[arg(
80 long,
81 visible_alias = "no-yolo",
82 conflicts_with = "dangerously_skip_permissions"
83 )]
84 pub restricted: bool,
85
86 /// Accepted for compatibility and does nothing — this is the default now.
87 /// Use `--restricted` to turn the restrictions on.
88 #[arg(long, visible_alias = "yolo", hide = true)]
89 pub dangerously_skip_permissions: bool,
90
91 /// Strip harness identity sentences from the rendered system prompt
92 /// (A/B contamination control; default = faithful reproduction).
93 #[arg(long)]
94 pub strip_identity: bool,
95
96 /// Stream the model turn (SSE) instead of one buffered call (ADR-0021).
97 /// Needed for **unbounded output** — Anthropic rejects non-streaming
98 /// requests that may exceed ~10 min. The `stream-json` trace stays
99 /// whole-message: token deltas are assembled but **not** written to it.
100 #[arg(long)]
101 pub stream: bool,
102
103 /// Skip project-instruction loading (`AGENTS.md`) — the `--bare`-style disable
104 /// (ADR-0023). Auto-discovery is otherwise on by default.
105 #[arg(long)]
106 pub no_project_instructions: bool,
107
108 /// Add a directory the agent may read and write, beyond the working
109 /// directory. Repeatable. Its `AGENTS.md` and `.agents/skills` are picked up
110 /// too — the point of pointing at a subtree of a large monorepo.
111 #[arg(long = "add-dir", value_name = "DIR")]
112 pub add_dir: Vec<std::path::PathBuf>,
113
114 /// How hard the model should think. Omitted uses the `effort` setting, then
115 /// the API's own default.
116 #[arg(long, value_name = "LEVEL")]
117 pub effort: Option<EffortArg>,
118}
119
120/// How hard the model should think. locode's own ladder — each wire maps it
121/// onto whatever that API accepts, so the same word means the same intent
122/// whichever model is in use.
123#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
124#[value(rename_all = "kebab-case")]
125pub enum EffortArg {
126 /// Fastest; short, scoped tasks.
127 Low,
128 /// Balanced.
129 Medium,
130 /// The API default.
131 High,
132 /// Best for coding and agentic work.
133 XHigh,
134 /// Deepest; correctness over cost.
135 Max,
136}
137
138impl From<EffortArg> for locode_core::Effort {
139 fn from(arg: EffortArg) -> Self {
140 match arg {
141 EffortArg::Low => locode_core::Effort::Low,
142 EffortArg::Medium => locode_core::Effort::Medium,
143 EffortArg::High => locode_core::Effort::High,
144 EffortArg::XHigh => locode_core::Effort::XHigh,
145 EffortArg::Max => locode_core::Effort::Max,
146 }
147 }
148}
149
150/// The registered harness packs (a closed set — clap validates and lists them).
151#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
152#[value(rename_all = "kebab-case")]
153pub enum Harness {
154 /// Grok Build's toolset + system prompt.
155 Grok,
156 /// Claude Code's toolset + system prompt.
157 Claude,
158 /// Codex CLI's toolset + system prompt.
159 Codex,
160}
161
162impl Harness {
163 /// The pack name for `locode_core::resolve`.
164 #[must_use]
165 pub fn as_str(self) -> &'static str {
166 match self {
167 Harness::Grok => "grok",
168 Harness::Claude => "claude",
169 Harness::Codex => "codex",
170 }
171 }
172}
173
174/// The stdout artifact selector (ADR-0014; Claude Code's three-way shape).
175#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)]
176#[value(rename_all = "kebab-case")]
177pub enum OutputFormat {
178 /// One `Report` JSON object (the default).
179 Json,
180 /// The final assistant message only.
181 Text,
182 /// The live JSONL `Event` stream (`init` → `message`… → `result`).
183 StreamJson,
184}