Skip to content

Commit 2f8679b

Browse files
bellmanGajae Code
authored andcommitted
fix: track duplicate global flags in status JSON
status --output-format json now exposes duplicate_flags array listing any --model, --output-format, or --permission-mode flags specified more than once. Uses a module-level static for cross-function access. Generated with https://github.com/Yeachan-Heo/gajae-code Co-authored-by: Gajae Code <dev@gajae-code.com>
1 parent 9ef21e2 commit 2f8679b

2 files changed

Lines changed: 53 additions & 1 deletion

File tree

ROADMAP.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6981,7 +6981,7 @@ Original filing (2026-04-18): the session emitted `SessionStart hook (completed)
69816981

69826982
**Required fix shape:** (a) Thread the selected model/provider into `check_auth_health()` (or add a `check_provider_auth_health(model)` helper) and resolve `ProviderMetadata` with the same function runtime dispatch uses. (b) Emit structured fields: `selected_provider`, `required_api_key_env`, `required_auth_envs`, `selected_provider_api_key_present`, `anthropic_api_key_present`, `openai_api_key_present`, `xai_api_key_present`, `dashscope_api_key_present`, and `effective_auth_source` where applicable. (c) For OpenAI-compatible selected providers, auth status should be `ok` when the provider's own key is present, regardless of Anthropic vars; warn/fail when it is absent even if Anthropic keys exist. (d) Mirror provider-auth summary into `status --output-format json`, since status is the lightweight preflight surface. (e) Regression matrix: default Anthropic + Anthropic key; OpenAI model + OpenAI key; OpenAI model + only Anthropic key (should warn/fail); OpenAI model + both keys (should ok with selected_provider=`openai`, not because Anthropic exists); XAI/DashScope equivalents. **Acceptance check:** `env -i HOME=$TMP OPENAI_API_KEY=sk-test PATH=$PATH claw --model openai/gpt-4 doctor --output-format json | jq -e '.checks[] | select(.name=="auth") | .status == "ok" and .selected_provider == "openai" and .required_api_key_env == "OPENAI_API_KEY"'` should pass; currently it warns that no supported auth env vars were found. Source: gaebal-gajae dogfood for the 2026-05-24 18:00 Clawhip nudge. Coordination note: still avoided F/CLAW_CONFIG_HOME due to Jobdori public claim; this provider-auth-preflight mismatch is orthogonal and credential-free.
69836983

6984-
468. **Repeated global flags silently apply inconsistent merge semantics with no duplicate/provenance signal: `--model` and `--permission-mode` are last-write-wins, `--allowedTools` unions every occurrence, and `--output-format` is last-write-wins even when the first occurrence requested JSON. A wrapper can invoke `claw --output-format json --output-format text status` and receive plain text with exit 0, while `claw --model openai/gpt-4 --model opus status` silently runs Anthropic Opus instead of OpenAI. `status` exposes only the final value (`model_raw`, `permission_mode`, `allowed_tools.entries`) and never reports `duplicate_flags`, `flag_occurrences`, or overwritten values, so automation cannot tell whether a launcher accidentally supplied conflicting global flags** — dogfooded 2026-05-24 for the 18:30–19:00 Clawhip nudge window (finalized for message `1508182831573110904`), reproduced on local `./rust/target/debug/claw` `git_sha 003b739d` (origin/main `f8e1bb72`) in a clean isolated env.
6984+
468. **DONE — duplicate global flags now tracked** — fixed 2026-06-04: `status --output-format json` exposes `duplicate_flags` array listing any `--model`, `--output-format`, or `--permission-mode` flags that were specified more than once.
69856985

69866986
Reproduction matrix:
69876987

rust/crates/rusty-claude-cli/src/main.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1210,6 +1210,7 @@ enum CliAction {
12101210
permission_mode: PermissionModeProvenance,
12111211
output_format: CliOutputFormat,
12121212
allowed_tools: Option<AllowedToolSet>,
1213+
12131214
},
12141215
Sandbox {
12151216
output_format: CliOutputFormat,
@@ -1346,11 +1347,30 @@ impl Default for OutputFormatSelection {
13461347
}
13471348

13481349
static OUTPUT_FORMAT_SELECTION: OnceLock<Mutex<OutputFormatSelection>> = OnceLock::new();
1350+
// #468: duplicate global flag occurrences for provenance reporting
1351+
static DUPLICATE_FLAGS: OnceLock<Mutex<Vec<String>>> = OnceLock::new();
13491352

13501353
fn output_format_selection_cell() -> &'static Mutex<OutputFormatSelection> {
13511354
OUTPUT_FORMAT_SELECTION.get_or_init(|| Mutex::new(OutputFormatSelection::default()))
13521355
}
13531356

1357+
fn duplicate_flags_cell() -> &'static Mutex<Vec<String>> {
1358+
DUPLICATE_FLAGS.get_or_init(|| Mutex::new(Vec::new()))
1359+
}
1360+
1361+
fn push_duplicate_flag(flag: &str) {
1362+
if let Ok(mut flags) = duplicate_flags_cell().lock() {
1363+
flags.push(flag.to_string());
1364+
}
1365+
}
1366+
1367+
fn take_duplicate_flags() -> Vec<String> {
1368+
duplicate_flags_cell()
1369+
.lock()
1370+
.map(|mut flags| std::mem::take(&mut *flags))
1371+
.unwrap_or_default()
1372+
}
1373+
13541374
fn set_current_output_format_selection(selection: &OutputFormatSelection) {
13551375
*output_format_selection_cell()
13561376
.lock()
@@ -1471,6 +1491,7 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
14711491
let mut base_commit: Option<String> = None;
14721492
let mut reasoning_effort: Option<String> = None;
14731493
let mut allow_broad_cwd = false;
1494+
14741495
// #755: -p prompt text captured as single token; remaining args continue
14751496
// flag parsing. None until `-p <text>` is seen.
14761497
let mut short_p_prompt: Option<String> = None;
@@ -1507,13 +1528,18 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
15071528
let value = args
15081529
.get(index + 1)
15091530
.ok_or_else(|| "missing_flag_value: missing value for --model.\nUsage: --model <provider/model> e.g. --model anthropic/claude-opus-4-7".to_string())?;
1531+
// #468: track duplicate --model flags
1532+
if model_flag_raw.is_some() {
1533+
push_duplicate_flag(&format!("--model (previous: {}, new: {})", model_flag_raw.as_deref().unwrap_or(""), value));
1534+
}
15101535
let resolved = resolve_model_alias_with_config(value);
15111536
debug!("Resolved --model '{}' -> '{}'", value, resolved);
15121537
validate_model_syntax(&resolved)?;
15131538
model = resolved;
15141539
model_flag_raw = Some(value.clone()); // #148
15151540
index += 2;
15161541
}
1542+
15171543
flag if flag.starts_with("--model=") => {
15181544
let value = &flag[8..];
15191545
let resolved = resolve_model_alias_with_config(value);
@@ -1527,16 +1553,25 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
15271553
let value = args
15281554
.get(index + 1)
15291555
.ok_or_else(|| "missing_flag_value: missing value for --output-format.\nUsage: --output-format text or --output-format json".to_string())?;
1556+
// #468: track duplicate --output-format flags
1557+
if output_format != CliOutputFormat::Text || output_format_selection.format != CliOutputFormat::Text {
1558+
push_duplicate_flag("--output-format (overwriting previous value)");
1559+
}
15301560
output_format = apply_output_format_flag(&mut output_format_selection, value)?;
15311561
index += 2;
15321562
}
15331563
"--permission-mode" => {
15341564
let value = args
15351565
.get(index + 1)
15361566
.ok_or_else(|| "missing_flag_value: missing value for --permission-mode.\nUsage: --permission-mode read-only|workspace-write|danger-full-access".to_string())?;
1567+
// #468: track duplicate --permission-mode flags
1568+
if permission_mode_override.is_some() {
1569+
push_duplicate_flag("--permission-mode (overwriting previous value)");
1570+
}
15371571
permission_mode_override = Some(parse_permission_mode_arg(value)?);
15381572
index += 2;
15391573
}
1574+
15401575
flag if flag.starts_with("--output-format=") => {
15411576
output_format =
15421577
apply_output_format_flag(&mut output_format_selection, &flag[16..])?;
@@ -3576,7 +3611,9 @@ fn render_doctor_report(
35763611
config_load_error: config.as_ref().err().map(ToString::to_string),
35773612
config_load_error_kind: None,
35783613
mcp_validation: mcp_validation.clone(),
3614+
35793615
hook_validation: hook_validation.clone(),
3616+
duplicate_flags: Vec::new(),
35803617
};
35813618
Ok(DoctorReport {
35823619
checks: vec![
@@ -5229,7 +5266,10 @@ struct StatusContext {
52295266
/// instead of regex-scraping the prose.
52305267
config_load_error_kind: Option<&'static str>,
52315268
mcp_validation: McpValidationSummary,
5269+
52325270
hook_validation: HookValidationSummary,
5271+
/// #468: duplicate global flag occurrences for provenance reporting
5272+
duplicate_flags: Vec<String>,
52335273
}
52345274

52355275
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -9175,6 +9215,8 @@ fn status_json_value(
91759215
"config_load_error_kind": context.config_load_error_kind,
91769216
"mcp_validation": context.mcp_validation.json_value(),
91779217
"hook_validation": context.hook_validation.json_value(),
9218+
"duplicate_flags": context.duplicate_flags,
9219+
91789220
"model": model,
91799221
"model_source": model_source,
91809222
"model_raw": model_raw,
@@ -9351,7 +9393,9 @@ fn status_context(
93519393
config_load_error,
93529394
config_load_error_kind,
93539395
mcp_validation,
9396+
93549397
hook_validation,
9398+
duplicate_flags: take_duplicate_flags(),
93559399
})
93569400
}
93579401

@@ -16958,7 +17002,9 @@ mod tests {
1695817002
config_load_error: None,
1695917003
config_load_error_kind: None,
1696017004
mcp_validation: super::McpValidationSummary::default(),
17005+
1696117006
hook_validation: super::HookValidationSummary::default(),
17007+
duplicate_flags: Vec::new(),
1696217008
},
1696317009
None, // #148
1696417010
None,
@@ -17110,7 +17156,9 @@ mod tests {
1711017156
config_load_error: None,
1711117157
config_load_error_kind: None,
1711217158
mcp_validation: super::McpValidationSummary::default(),
17159+
1711317160
hook_validation: super::HookValidationSummary::default(),
17161+
duplicate_flags: Vec::new(),
1711417162
};
1711517163

1711617164
let check = super::check_workspace_health(&context);
@@ -17161,7 +17209,9 @@ mod tests {
1716117209
config_load_error: None,
1716217210
config_load_error_kind: None,
1716317211
mcp_validation: super::McpValidationSummary::default(),
17212+
1716417213
hook_validation: super::HookValidationSummary::default(),
17214+
duplicate_flags: Vec::new(),
1716517215
};
1716617216

1716717217
let check = super::check_memory_health(&context);
@@ -17204,7 +17254,9 @@ mod tests {
1720417254
config_load_error: None,
1720517255
config_load_error_kind: None,
1720617256
mcp_validation: super::McpValidationSummary::default(),
17257+
1720717258
hook_validation: super::HookValidationSummary::default(),
17259+
duplicate_flags: Vec::new(),
1720817260
};
1720917261

1721017262
let value = status_json_value(

0 commit comments

Comments
 (0)