Clearer usage errors for sigit run prompts

A second positional prompt now says "unexpected extra argument" instead of "unknown argument", a bare `sigit run` says "missing prompt", and a mistyped flag like --quite is rejected instead of becoming the prompt.

Seto Elkahfi committed Sep 25, 2026 at 18:16 UTC 26f8c3c629238f60264aa8f634e942880c865988
1 file changed +40 -2
src/headless.rs
+40 -2
@@ -94,7 +94,7 @@ pub fn parse_args(args: &[String]) -> Result<Option<HeadlessConfig>, String> {
94 .next()
95 .ok_or_else(|| format!("{arg} requires a prompt argument"))?;
96 if prompt.is_some() {
97 - return Err(format!("{arg} was given more than once"));
97 + return Err("the prompt was given more than once".to_string());
98 }
99 prompt = Some(value.clone());
100 }
@@ -143,14 +143,28 @@ pub fn parse_args(args: &[String]) -> Result<Option<HeadlessConfig>, String> {
143 .ok_or_else(|| "--deny-tool requires a tool name".to_string())?;
144 deny_tools.push(value.clone());
145 }
146 + // A mistyped flag must not become the prompt. A prompt that
147 + // really starts with '-' can still go through -p.
148 + other if other.starts_with('-') => return Err(format!("unknown argument: {other}")),
149 other if run_command && prompt.is_none() => prompt = Some(other.to_string()),
150 + other if run_command => {
151 + return Err(format!(
152 + "unexpected extra argument: {other} (quote the prompt as one argument)"
153 + ));
154 + }
155 other => return Err(format!("unknown argument: {other}")),
156 }
157 }
158
159 // Reaching this without a prompt means `run` had no positional prompt, or
160 // a legacy prompt flag was consumed as another flag's value.
153 - let prompt = prompt.ok_or_else(|| "missing -p/--prompt".to_string())?;
161 + let prompt = prompt.ok_or_else(|| {
162 + if run_command {
163 + "missing prompt".to_string()
164 + } else {
165 + "missing -p/--prompt".to_string()
166 + }
167 + })?;
168 if prompt.trim().is_empty() {
169 return Err("the prompt must not be empty".to_string());
170 }
@@ -682,6 +696,30 @@ mod tests {
696 assert!(parse_args(&args(&["-p", "a", "--prompt", "b"])).is_err());
697 }
698
699 + #[test]
700 + fn run_rejects_a_second_prompt_from_either_source() {
701 + let error = parse_args(&args(&["run", "a", "b"])).unwrap_err();
702 + assert!(error.contains("unexpected extra argument: b"), "{error}");
703 + let error = parse_args(&args(&["run", "a", "-p", "b"])).unwrap_err();
704 + assert!(error.contains("more than once"), "{error}");
705 + let error = parse_args(&args(&["run", "-p", "a", "b"])).unwrap_err();
706 + assert!(error.contains("unexpected extra argument: b"), "{error}");
707 + }
708 +
709 + #[test]
710 + fn run_does_not_take_a_mistyped_flag_as_the_prompt() {
711 + let error = parse_args(&args(&["run", "--quite", "fix it"])).unwrap_err();
712 + assert!(error.contains("unknown argument: --quite"), "{error}");
713 + }
714 +
715 + #[test]
716 + fn bare_run_reports_a_missing_prompt() {
717 + assert_eq!(
718 + parse_args(&args(&["run"])),
719 + Err("missing prompt".to_string())
720 + );
721 + }
722 +
723 #[test]
724 fn empty_prompt_is_a_usage_error() {
725 assert!(parse_args(&args(&["-p", " "])).is_err());