rho_coding_agent/cli.rs
1use std::{num::NonZeroUsize, path::PathBuf, time::Duration};
2
3use clap::{Parser, Subcommand, ValueEnum};
4
5use rho_providers::{credentials::CredentialStoreBackend, reasoning::ReasoningLevel};
6
7use crate::app::automation_protocol::parse_duration;
8
9fn parse_credential_store_backend(value: &str) -> Result<CredentialStoreBackend, String> {
10 CredentialStoreBackend::parse(value).map_err(|error| error.to_string())
11}
12
13fn parse_auth_profile(value: &str) -> Result<String, String> {
14 let profiles = rho_providers::auth_profiles();
15 if profiles.contains(&value) {
16 return Ok(value.to_string());
17 }
18 Err(format!(
19 "invalid value '{value}' for '--auth'; expected one of: {}",
20 profiles.join(", ")
21 ))
22}
23
24/// Output contract used by a non-interactive `rho run` invocation.
25#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, ValueEnum)]
26pub enum OutputFormat {
27 /// Print only the authoritative final assistant answer.
28 #[default]
29 Text,
30 /// Stream independently versioned JSON Lines events.
31 Jsonl,
32}
33
34/// Output contract for workflow plans and snapshots.
35#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, ValueEnum)]
36pub enum WorkflowDocumentFormat {
37 /// Print a human-readable document.
38 #[default]
39 Text,
40 /// Print one JSON document.
41 Json,
42}
43
44/// Output contract for workflow execution.
45#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)]
46pub enum WorkflowRunFormat {
47 /// Print human-readable state changes.
48 Text,
49 /// Stream versioned JSON Lines events.
50 Jsonl,
51}
52
53#[derive(Parser, Debug)]
54#[command(name = "rho")]
55pub struct Cli {
56 #[arg(long)]
57 pub provider: Option<String>,
58 #[arg(long)]
59 pub model: Option<String>,
60 #[arg(long)]
61 pub config: Option<PathBuf>,
62 #[arg(long, value_parser = parse_auth_profile)]
63 pub auth: Option<String>,
64 /// Do not send rho's system prompt, including AGENTS.md and skill context.
65 #[arg(long)]
66 pub no_system_prompt: bool,
67 /// Do not expose any tools to the model.
68 #[arg(long)]
69 pub no_tools: bool,
70 /// Do not expose the delegated-agent tools (agent/agents) to the model.
71 #[arg(long, global = true)]
72 pub no_subagents: bool,
73 /// Select the agent definition used for this session or automation run.
74 #[arg(long, global = true, value_name = "ID")]
75 pub agent: Option<String>,
76 /// Override reasoning level: off, minimal, low, medium, high, xhigh, or max.
77 #[arg(long)]
78 pub reasoning: Option<ReasoningLevel>,
79 /// Resume an existing session by UUID or UUID prefix. Omit the ID to choose from a picker.
80 #[arg(short = 'R', long, value_name = "ID", num_args = 0..=1)]
81 pub resume: Option<Option<String>>,
82 #[command(subcommand)]
83 pub command: Option<Command>,
84}
85
86#[derive(Subcommand, Debug)]
87pub enum Command {
88 /// Run one non-interactive automation prompt and print the final answer.
89 Run {
90 /// Read additional prompt text from stdin.
91 #[arg(long)]
92 stdin: bool,
93 /// Stream progress to stdout and write a structured status/result
94 /// file (JSON) that is updated during the run and finalized on exit.
95 #[arg(long, value_name = "PATH")]
96 output_file: Option<PathBuf>,
97 /// Select plain final-answer output or a JSON Lines event stream.
98 #[arg(long, value_enum, default_value_t)]
99 output: OutputFormat,
100 /// Override the model-step budget for this run.
101 #[arg(long, value_name = "N")]
102 max_steps: Option<NonZeroUsize>,
103 /// Cancel the run after this wall-clock duration.
104 #[arg(long, value_name = "DURATION", value_parser = parse_duration)]
105 timeout: Option<Duration>,
106 /// Prompt text to send to the agent.
107 #[arg(value_name = "PROMPT", num_args = 0..)]
108 prompt: Vec<String>,
109 },
110 /// Watch a delegated agent run in a read-only TUI.
111 Attach {
112 /// Delegated run ID shown when the agent was started.
113 #[arg(value_name = "ID")]
114 id: String,
115 },
116 /// Log in to a provider from a browser or device-code flow.
117 Login {
118 /// Provider to authenticate, for example openai-codex or github-copilot.
119 #[arg(value_name = "PROVIDER")]
120 provider: String,
121 /// Use device-code login instead of opening a local browser callback.
122 #[arg(long)]
123 device_auth: bool,
124 },
125 /// Configure or probe provider credential storage.
126 CredentialStore {
127 #[command(subcommand)]
128 command: CredentialStoreCommand,
129 },
130 /// Update rho using the detected installation method.
131 Update,
132 /// List, rename, or delete saved sessions.
133 Sessions {
134 #[command(subcommand)]
135 command: SessionsCommand,
136 },
137 /// Validate, plan, run, and inspect deterministic workflows.
138 Workflow {
139 #[command(subcommand)]
140 command: WorkflowCommand,
141 },
142 /// Internal supervised workflow planner worker. Not a public command.
143 #[command(name = "__workflow_planner_worker", hide = true)]
144 WorkflowPlannerWorker,
145}
146
147/// Argv entry for the internal supervised planner worker process.
148pub const WORKFLOW_PLANNER_WORKER_COMMAND: &str = "__workflow_planner_worker";
149
150#[derive(Subcommand, Debug)]
151pub enum WorkflowCommand {
152 /// Validate a Starlark workflow without creating durable state.
153 Validate {
154 /// Workflow entry file under the current workspace.
155 #[arg(value_name = "FILE")]
156 file: PathBuf,
157 /// Supply one non-secret workflow input as KEY=JSON. May be repeated.
158 #[arg(long, value_name = "KEY=JSON")]
159 input: Vec<String>,
160 },
161 /// Validate, freeze, and persist an immutable workflow plan.
162 Plan {
163 /// Workflow entry file under the current workspace.
164 #[arg(value_name = "FILE")]
165 file: PathBuf,
166 /// Supply one non-secret workflow input as KEY=JSON. May be repeated.
167 #[arg(long, value_name = "KEY=JSON")]
168 input: Vec<String>,
169 /// Select human-readable or machine-readable plan output.
170 #[arg(long, value_enum, default_value_t)]
171 output: WorkflowDocumentFormat,
172 },
173 /// Create and run a workflow from an immutable plan.
174 Run {
175 /// Full plan UUID or unique UUID prefix.
176 #[arg(value_name = "PLAN_ID")]
177 plan_id: String,
178 /// Confirm the exact plan digest without an interactive prompt.
179 #[arg(long)]
180 yes: bool,
181 /// Select text or JSON Lines instead of the workflow TUI.
182 #[arg(long, value_enum)]
183 output: Option<WorkflowRunFormat>,
184 },
185 /// Read one durable workflow run snapshot.
186 Status {
187 /// Full run UUID or unique UUID prefix.
188 #[arg(value_name = "RUN_ID")]
189 run_id: String,
190 /// Select human-readable or machine-readable snapshot output.
191 #[arg(long, value_enum, default_value_t)]
192 output: WorkflowDocumentFormat,
193 },
194 /// Request cancellation of a durable workflow run.
195 Cancel {
196 /// Full run UUID or unique UUID prefix.
197 #[arg(value_name = "RUN_ID")]
198 run_id: String,
199 },
200 /// Resume a durable workflow run from its frozen graph.
201 Resume {
202 /// Full run UUID or unique UUID prefix.
203 #[arg(value_name = "RUN_ID")]
204 run_id: String,
205 /// Confirm the frozen graph without an interactive prompt.
206 #[arg(long)]
207 yes: bool,
208 /// Confirm that no prior process remains and relaunch uncertain attempts.
209 #[arg(long)]
210 recover_uncertain: bool,
211 /// Select text or JSON Lines instead of the workflow TUI.
212 #[arg(long, value_enum)]
213 output: Option<WorkflowRunFormat>,
214 },
215}
216
217#[derive(Subcommand, Debug)]
218pub enum SessionsCommand {
219 /// List saved sessions for the current workspace (or all projects).
220 List {
221 /// Include sessions from every workspace, not only the current directory.
222 #[arg(long)]
223 all_projects: bool,
224 },
225 /// Delete one or more sessions by UUID or UUID prefix.
226 Rm {
227 /// Session UUID or unique prefix. May be repeated.
228 #[arg(value_name = "ID", required = true, num_args = 1..)]
229 ids: Vec<String>,
230 /// Delete even when a parent-linked run is still non-terminal.
231 ///
232 /// Use only for stale Starting/Running artifacts left after a crash.
233 #[arg(long)]
234 force: bool,
235 /// Skip the confirmation prompt for cross-project deletes.
236 #[arg(short = 'y', long)]
237 yes: bool,
238 },
239 /// Rename a session by UUID or UUID prefix.
240 Rename {
241 /// Session UUID or unique prefix.
242 #[arg(value_name = "ID")]
243 id_prefix: String,
244 /// New session title. Multiple words are joined with spaces.
245 #[arg(value_name = "TITLE", required = true, num_args = 1.., trailing_var_arg = true)]
246 title: Vec<String>,
247 },
248}
249
250#[derive(Subcommand, Debug)]
251pub enum CredentialStoreCommand {
252 /// Test a credential backend by writing and deleting a temporary secret.
253 Probe {
254 /// Backend to test: os or file (`auto` is accepted as an alias for os).
255 #[arg(
256 value_name = "BACKEND",
257 default_value = "os",
258 value_parser = parse_credential_store_backend
259 )]
260 backend: CredentialStoreBackend,
261 },
262 /// Show the configured credential backend (unset, os, or file).
263 Status,
264 /// Save the credential backend used by future rho processes.
265 Set {
266 /// Backend to use: os or file (`auto` is accepted as an alias for os).
267 #[arg(value_name = "BACKEND", value_parser = parse_credential_store_backend)]
268 backend: CredentialStoreBackend,
269 },
270}
271
272#[cfg(test)]
273#[path = "cli_tests.rs"]
274mod tests;