mirror of
https://github.com/ultraworkers/claw-code.git
synced 2026-04-24 21:28:11 +08:00
## Scope Two deltas in one commit: ### #128 closure (docs) Re-verified on main HEAD `4cb8fa0`: malformed `--model` strings already rejected at parse time (`validate_model_syntax` in parse_args). All historical repro cases now produce specific errors: claw --model '' → error: model string cannot be empty claw --model 'bad model' → error: invalid model syntax: 'bad model' contains spaces claw --model 'sonet' → error: invalid model syntax: 'sonet'. Expected provider/model or known alias claw --model '@invalid' → error: invalid model syntax: '@invalid'. Expected provider/model ... claw --model 'totally-not-real-xyz' → error: invalid model syntax: ... claw --model sonnet → ok, resolves to claude-sonnet-4-6 claw --model anthropic/claude-opus-4-6 → ok, passes through Marked #128 CLOSED in ROADMAP with repro block. Residual provenance gap split off as #148. ### #148 implementation **Problem.** After #128 closure, `claw status --output-format json` still surfaces only the resolved model string. No way for a claw to distinguish whether `claude-sonnet-4-6` came from `--model sonnet` (alias resolution) vs `--model claude-sonnet-4-6` (pass-through) vs `ANTHROPIC_MODEL` env vs `.claw.json` config vs compiled-in default. Debug forensics had to re-read argv instead of reading a structured field. Clawhip orchestrators sending `--model` couldn't confirm the flag was honored vs falling back to default. **Fix.** Added two fields to status JSON envelope: - `model_source`: "flag" | "env" | "config" | "default" - `model_raw`: user's input before alias resolution (null on default) Text mode appends a `Model source` line under `Model`, showing the source and raw input (e.g. `Model source flag (raw: sonnet)`). **Resolution order** (mirrors resolve_repl_model but with source attribution): 1. If `--model` / `--model=` flag supplied → source: flag, raw: flag value 2. Else if ANTHROPIC_MODEL set → source: env, raw: env value 3. Else if `.claw.json` model key set → source: config, raw: config value 4. Else → source: default, raw: null ## Changes ### rust/crates/rusty-claude-cli/src/main.rs - Added `ModelSource` enum (Flag/Env/Config/Default) with `as_str()`. - Added `ModelProvenance` struct (resolved, raw, source) with three constructors: `default_fallback()`, `from_flag(raw)`, and `from_env_or_config_or_default(cli_model)`. - Added `model_flag_raw: Option<String>` field to `CliAction::Status`. - Parse loop captures raw input in `--model` and `--model=` arms. - Extended `parse_single_word_command_alias` to thread `model_flag_raw: Option<&str>` through. - Extended `print_status_snapshot` signature to accept `model_flag_raw: Option<&str>`. Resolves provenance at dispatch time (flag provenance from arg; else probe env/config/default). - Extended `status_json_value` signature with `provenance: Option<&ModelProvenance>`. On Some, adds `model_source` and `model_raw` fields; on None (legacy resume paths), omits them for backward compat. - Extended `format_status_report` signature with optional provenance. On Some, renders `Model source` line after `Model`. - Updated all existing callers (REPL /status, resume /status, tests) to pass None (legacy paths don't carry flag provenance). - Added 2 regression assertions in parse_args test covering both `--model sonnet` and `--model=...` forms. ### ROADMAP.md - Marked #128 CLOSED with re-verification block. - Filed #148 documenting the provenance gap split, fix shape, and acceptance criteria. ## Live verification $ claw --model sonnet --output-format json status | jq '{model,model_source,model_raw}' {"model": "claude-sonnet-4-6", "model_source": "flag", "model_raw": "sonnet"} $ claw --output-format json status | jq '{model,model_source,model_raw}' {"model": "claude-opus-4-6", "model_source": "default", "model_raw": null} $ ANTHROPIC_MODEL=haiku claw --output-format json status | jq '{model,model_source,model_raw}' {"model": "claude-haiku-4-5-20251213", "model_source": "env", "model_raw": "haiku"} $ echo '{"model":"claude-opus-4-7"}' > .claw.json && claw --output-format json status | jq '{model,model_source,model_raw}' {"model": "claude-opus-4-7", "model_source": "config", "model_raw": "claude-opus-4-7"} $ claw --model sonnet status Status Model claude-sonnet-4-6 Model source flag (raw: sonnet) Permission mode danger-full-access ... ## Tests - rusty-claude-cli bin: 177 tests pass (2 new assertions for #148) - Full workspace green except pre-existing resume_latest flake (unrelated) Closes ROADMAP #128, #148.
This commit is contained in:
parent
4cb8fa059a
commit
f84c7c4ed5
57
ROADMAP.md
57
ROADMAP.md
@ -4815,7 +4815,19 @@ ear], /color [scheme], /effort [low|medium|high], /fast, /summary, /tag [label],
|
|||||||
|
|
||||||
**Source.** Jobdori dogfood 2026-04-20 against `/tmp/claw-dogfood` (env-cleaned, no git, no config) on main HEAD `7370546` in response to Clawhip pinpoint nudge at `1495620050424434758`. Joins **Silent-flag / documented-but-unenforced** (#96–#101, #104, #108, #111, #115, #116, #117, #118, #119, #121, #122, #123, #124, #126) as 18th — `--json` silently swallowed into Prompt dispatch instead of being recognized or rejected. Joins **Parser-level trust gap quintet** (#108, #117, #119, #122, **#127**) as 5th — same `_other => Prompt` fall-through arm, fifth distinct entry case (#108 = typoed verb, #117 = `-p` greedy, #119 = bare slash + arg, #122 = `--base-commit` greedy, **#127 = valid verb + unrecognized suffix arg**). Joins **Cred-error misdirection / failure-classification gaps** as a sibling of #99 (system-prompt unvalidated) — same family of "local diagnostic verb pretends to need API creds." Joins **Truth-audit / diagnostic-integrity** (#80–#87, #89, #100, #102, #103, #105, #107, #109, #110, #112, #114, #115, #125) — `claw --help` lies about per-verb accepted flags. Joins **Parallel-entry-point asymmetry** (#91, #101, #104, #105, #108, #114, #117, #122, #123, #124) as 11th — three working forms and one broken form for the same logical intent (`--json` doctor output). Joins **Claude Code migration parity** (#103, #109, #116) as 4th — Claude Code's `--json` convention shorthand is unrecognized in claw-code's verb-suffix position; users migrating get cred errors instead. Cross-cluster with **README/USAGE doc-vs-implementation gap** — README explicitly recommends `claw doctor` as the first health check; the natural JSON form of that exact command is broken. Natural bundle: **#108 + #117 + #119 + #122 + #127** — parser-level trust gap quintet: complete `_other => Prompt` fall-through audit (typoed verb + greedy `-p` + bare slash-verb + greedy `--base-commit` + valid verb + unrecognized suffix). Also **#99 + #127** — local-diagnostic cred-error misdirection pair: `system-prompt` and verb-suffix `--json` both pretend to need creds for pure-local operations. Also **#126 + #127** — diagnostic-verb surface integrity pair: `/config` section args ignored (#126) + verb-suffix args silently mis-dispatched (#127). Session tally: ROADMAP #127.
|
**Source.** Jobdori dogfood 2026-04-20 against `/tmp/claw-dogfood` (env-cleaned, no git, no config) on main HEAD `7370546` in response to Clawhip pinpoint nudge at `1495620050424434758`. Joins **Silent-flag / documented-but-unenforced** (#96–#101, #104, #108, #111, #115, #116, #117, #118, #119, #121, #122, #123, #124, #126) as 18th — `--json` silently swallowed into Prompt dispatch instead of being recognized or rejected. Joins **Parser-level trust gap quintet** (#108, #117, #119, #122, **#127**) as 5th — same `_other => Prompt` fall-through arm, fifth distinct entry case (#108 = typoed verb, #117 = `-p` greedy, #119 = bare slash + arg, #122 = `--base-commit` greedy, **#127 = valid verb + unrecognized suffix arg**). Joins **Cred-error misdirection / failure-classification gaps** as a sibling of #99 (system-prompt unvalidated) — same family of "local diagnostic verb pretends to need API creds." Joins **Truth-audit / diagnostic-integrity** (#80–#87, #89, #100, #102, #103, #105, #107, #109, #110, #112, #114, #115, #125) — `claw --help` lies about per-verb accepted flags. Joins **Parallel-entry-point asymmetry** (#91, #101, #104, #105, #108, #114, #117, #122, #123, #124) as 11th — three working forms and one broken form for the same logical intent (`--json` doctor output). Joins **Claude Code migration parity** (#103, #109, #116) as 4th — Claude Code's `--json` convention shorthand is unrecognized in claw-code's verb-suffix position; users migrating get cred errors instead. Cross-cluster with **README/USAGE doc-vs-implementation gap** — README explicitly recommends `claw doctor` as the first health check; the natural JSON form of that exact command is broken. Natural bundle: **#108 + #117 + #119 + #122 + #127** — parser-level trust gap quintet: complete `_other => Prompt` fall-through audit (typoed verb + greedy `-p` + bare slash-verb + greedy `--base-commit` + valid verb + unrecognized suffix). Also **#99 + #127** — local-diagnostic cred-error misdirection pair: `system-prompt` and verb-suffix `--json` both pretend to need creds for pure-local operations. Also **#126 + #127** — diagnostic-verb surface integrity pair: `/config` section args ignored (#126) + verb-suffix args silently mis-dispatched (#127). Session tally: ROADMAP #127.
|
||||||
|
|
||||||
128. **`claw --model <malformed>` (spaces, empty string, special chars, invalid provider/model syntax) silently falls through to API-layer cred error instead of rejecting at parse time** — dogfooded 2026-04-20 on main HEAD `d284ef7` from a fresh environment (no config, no auth). The `--model` flag accepts any string without syntactic validation: spaces (`claw --model "bad model"`), empty strings (`claw --model ""`), special characters (`claw --model "@invalid"`), non-existent provider/model combinations all parse successfully. The malformed model string then flows into the runtime's provider-detection layer, which silently accepts it as Anthropic fallback or passes it to an API layer that fails with `missing Anthropic credentials` (misdirection) rather than a clear "invalid model syntax" error at parse time. With API credentials configured, a malformed model string gets sent to the API, billing tokens against a request that should have failed client-side.
|
128. **[CLOSED 2026-04-21]** **`claw --model <malformed>` (spaces, empty string, special chars, invalid provider/model syntax) silently falls through to API-layer cred error instead of rejecting at parse time** — dogfooded 2026-04-20 on main HEAD `d284ef7` from a fresh environment (no config, no auth). The `--model` flag accepts any string without syntactic validation: spaces (`claw --model "bad model"`), empty strings (`claw --model ""`), special characters (`claw --model "@invalid"`), non-existent provider/model combinations all parse successfully. The malformed model string then flows into the runtime's provider-detection layer, which silently accepts it as Anthropic fallback or passes it to an API layer that fails with `missing Anthropic credentials` (misdirection) rather than a clear "invalid model syntax" error at parse time. With API credentials configured, a malformed model string gets sent to the API, billing tokens against a request that should have failed client-side.
|
||||||
|
|
||||||
|
**Closure (2026-04-21):** Re-verified on main HEAD `4cb8fa0`. All cases now rejected at parse time:
|
||||||
|
```
|
||||||
|
$ claw --model '' status → error: model string cannot be empty
|
||||||
|
$ claw --model 'bad model' status → error: invalid model syntax: 'bad model' contains spaces
|
||||||
|
$ claw --model 'sonet' status → error: invalid model syntax: 'sonet'. Expected provider/model ...
|
||||||
|
$ claw --model '@invalid' status → error: invalid model syntax: '@invalid'. Expected provider/model ...
|
||||||
|
$ claw --model 'totally-not-real-xyz' status → error: invalid model syntax ...
|
||||||
|
$ claw --model sonnet status → ok, resolves to claude-sonnet-4-6
|
||||||
|
$ claw --model anthropic/claude-opus-4-6 status → ok, passes through
|
||||||
|
```
|
||||||
|
Validation happens in `validate_model_syntax()` before `resolve_model_alias_with_config()`. All `--model` and `--model=` parse paths call it. No API call ever reached with malformed input. Residual gap (model provenance in status JSON — raw input vs resolved value) was split off as #148 (see below).
|
||||||
|
|
||||||
129. **MCP server startup blocks credential validation — `claw <prompt>` with any `.claw.json` `mcpServers` entry awaits the MCP server's stdio handshake BEFORE checking whether the operator has Anthropic credentials. With no `ANTHROPIC_AUTH_TOKEN` / `ANTHROPIC_API_KEY` set and `mcpServers.everything = { command: "npx", args: ["-y", "@modelcontextprotocol/server-everything"] }` configured, the CLI hangs forever (verified via `timeout 30s` — still in MCP startup at 30s with three repeated `"Starting default (STDIO) server..."` lines), instead of fail-fasting with the same `missing Anthropic credentials` error that fires in milliseconds when no MCP is configured. A misconfigured-but-running MCP server (one that spawns successfully but never completes its `initialize` handshake) wedges every `claw <prompt>` invocation permanently. A misconfigured MCP server with a slow-but-eventually-succeeding init (npx download, container pull, network roundtrip) burns startup latency on every Prompt invocation regardless of whether the LLM call would even succeed. This is the runtime-side companion to #102's config-time MCP diagnostic gap: #102 says doctor doesn't surface MCP reachability; #129 says the Prompt path's reachability check is implicit, blocking, retried, and runs *before* the cheaper auth precondition that should run first** — dogfooded 2026-04-20 on main HEAD `d284ef7` from `/tmp/claw-mcp-test` with `env -i PATH=$PATH HOME=$HOME` (all auth env vars unset).
|
129. **MCP server startup blocks credential validation — `claw <prompt>` with any `.claw.json` `mcpServers` entry awaits the MCP server's stdio handshake BEFORE checking whether the operator has Anthropic credentials. With no `ANTHROPIC_AUTH_TOKEN` / `ANTHROPIC_API_KEY` set and `mcpServers.everything = { command: "npx", args: ["-y", "@modelcontextprotocol/server-everything"] }` configured, the CLI hangs forever (verified via `timeout 30s` — still in MCP startup at 30s with three repeated `"Starting default (STDIO) server..."` lines), instead of fail-fasting with the same `missing Anthropic credentials` error that fires in milliseconds when no MCP is configured. A misconfigured-but-running MCP server (one that spawns successfully but never completes its `initialize` handshake) wedges every `claw <prompt>` invocation permanently. A misconfigured MCP server with a slow-but-eventually-succeeding init (npx download, container pull, network roundtrip) burns startup latency on every Prompt invocation regardless of whether the LLM call would even succeed. This is the runtime-side companion to #102's config-time MCP diagnostic gap: #102 says doctor doesn't surface MCP reachability; #129 says the Prompt path's reachability check is implicit, blocking, retried, and runs *before* the cheaper auth precondition that should run first** — dogfooded 2026-04-20 on main HEAD `d284ef7` from `/tmp/claw-mcp-test` with `env -i PATH=$PATH HOME=$HOME` (all auth env vars unset).
|
||||||
|
|
||||||
@ -5665,3 +5677,46 @@ With valid credentials, the empty string would be sent to Claude as a user promp
|
|||||||
**Blocker.** None. 5-line change in `parse_subcommand()`.
|
**Blocker.** None. 5-line change in `parse_subcommand()`.
|
||||||
|
|
||||||
**Source.** Jobdori dogfood 2026-04-21 20:32 KST on main HEAD `f877aca` in response to Clawhip nudge. Joins **prompt misdelivery** cluster (#145 sibling). Session tally: ROADMAP #147.
|
**Source.** Jobdori dogfood 2026-04-21 20:32 KST on main HEAD `f877aca` in response to Clawhip nudge. Joins **prompt misdelivery** cluster (#145 sibling). Session tally: ROADMAP #147.
|
||||||
|
|
||||||
|
## Pinpoint #148. `claw status` JSON shows resolved model but not raw input or source — post-hoc "why did my --model flag behave this way?" requires re-reading argv
|
||||||
|
|
||||||
|
**Gap.** After #128 closed (malformed model strings now rejected at parse time), the residual provenance gap from the original #124 pinpoint remains: `claw status --output-format json` surfaces only the resolved model string. No trace of whether the user passed `--model sonnet` (alias → resolved), `--model anthropic/claude-opus-4-6` (pass-through), or relied on env/config default. A claw debugging "which model actually runs if I invoke this?" has to inspect argv instead of reading a structured field.
|
||||||
|
|
||||||
|
**Verified on main HEAD `4cb8fa0` (2026-04-21 20:40 KST):**
|
||||||
|
|
||||||
|
```
|
||||||
|
$ claw --model sonnet --output-format json status | jq '{model}'
|
||||||
|
{"model": "claude-sonnet-4-6"}
|
||||||
|
|
||||||
|
$ claw --model anthropic/claude-opus-4-6 --output-format json status | jq '{model}'
|
||||||
|
{"model": "anthropic/claude-opus-4-6"}
|
||||||
|
|
||||||
|
# Same resolved value can come from three different sources;
|
||||||
|
# JSON envelope gives no way to distinguish.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why this is a clawability gap.**
|
||||||
|
1. **Loss of origin information**: alias resolution collapses `sonnet` and `claude-sonnet-4-6` and `{"aliases":{"x":"claude-sonnet-4-6"}}` + `--model x` into one string. Debug forensics has to read argv.
|
||||||
|
2. **Clawhip orchestration**: a clawhip dispatcher sending `--model` wants to confirm its flag was honored, not that the default kicked in (#105 model-resolution-source disagreement is adjacent).
|
||||||
|
3. **Truth-audit / diagnostic-integrity**: the status envelope is supposed to be the single source of truth for "what would this process run as". Missing provenance weakens the contract.
|
||||||
|
|
||||||
|
**Fix shape (~50 lines).** Add two fields to status JSON:
|
||||||
|
- `model_source`: `"flag" | "env" | "config" | "default"` — where the model string came from.
|
||||||
|
- `model_raw`: the user's original input (pre-alias-resolution). Null when source is `default`.
|
||||||
|
|
||||||
|
Text mode appends a line: `Model source flag (raw: sonnet)` or `Model source default`.
|
||||||
|
|
||||||
|
Threading: parser already knows the source (it's the arm that sets `model`). Propagate `(model, model_raw, model_source)` tuple through `CliAction::Status` and into `StatusContext`. Env/default resolution paths are in `resolve_repl_model*` helpers.
|
||||||
|
|
||||||
|
**Acceptance.**
|
||||||
|
- `claw --model sonnet --output-format json status` → `model: "claude-sonnet-4-6"`, `model_raw: "sonnet"`, `model_source: "flag"`.
|
||||||
|
- `claw --model anthropic/claude-opus-4-6 --output-format json status` → `model_raw: "anthropic/claude-opus-4-6"`, `model_source: "flag"`.
|
||||||
|
- `claw --output-format json status` (no flag) → `model_raw: null`, `model_source: "default"` (or `"env"` if `ANTHROPIC_MODEL` set; or `"config"` if `.claw.json` set `model`).
|
||||||
|
- Text mode shows same provenance.
|
||||||
|
- Regression test: parse_args + status_json_value roundtrip asserts each source value.
|
||||||
|
|
||||||
|
**Blocker.** None. All resolution sites already exist; only plumbing + one serialization addition.
|
||||||
|
|
||||||
|
**Not a regression of #128.** #128 was about rejecting malformed strings (now closed). #148 is about labeling the valid ones after resolution.
|
||||||
|
|
||||||
|
**Source.** Jobdori dogfood 2026-04-21 20:40 KST on main HEAD `4cb8fa0` in response to Q's bundle hint. Split from historical #124 residual. Joins **truth-audit / diagnostic-integrity** cluster. Session tally: ROADMAP #148.
|
||||||
|
|||||||
@ -57,6 +57,96 @@ use tools::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULT_MODEL: &str = "claude-opus-4-6";
|
const DEFAULT_MODEL: &str = "claude-opus-4-6";
|
||||||
|
|
||||||
|
/// #148: Model provenance for `claw status` JSON/text output. Records where
|
||||||
|
/// the resolved model string came from so claws don't have to re-read argv
|
||||||
|
/// to audit whether their `--model` flag was honored vs falling back to env
|
||||||
|
/// or config or default.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
enum ModelSource {
|
||||||
|
/// Explicit `--model` / `--model=` CLI flag.
|
||||||
|
Flag,
|
||||||
|
/// ANTHROPIC_MODEL environment variable (when no flag was passed).
|
||||||
|
Env,
|
||||||
|
/// `model` key in `.claw.json` / `.claw/settings.json` (when neither
|
||||||
|
/// flag nor env set it).
|
||||||
|
Config,
|
||||||
|
/// Compiled-in DEFAULT_MODEL fallback.
|
||||||
|
Default,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ModelSource {
|
||||||
|
fn as_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
ModelSource::Flag => "flag",
|
||||||
|
ModelSource::Env => "env",
|
||||||
|
ModelSource::Config => "config",
|
||||||
|
ModelSource::Default => "default",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
struct ModelProvenance {
|
||||||
|
/// Resolved model string (after alias expansion).
|
||||||
|
resolved: String,
|
||||||
|
/// Raw user input before alias resolution. None when source is Default.
|
||||||
|
raw: Option<String>,
|
||||||
|
/// Where the resolved model string originated.
|
||||||
|
source: ModelSource,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ModelProvenance {
|
||||||
|
fn default_fallback() -> Self {
|
||||||
|
Self {
|
||||||
|
resolved: DEFAULT_MODEL.to_string(),
|
||||||
|
raw: None,
|
||||||
|
source: ModelSource::Default,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_flag(raw: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
resolved: resolve_model_alias_with_config(raw),
|
||||||
|
raw: Some(raw.to_string()),
|
||||||
|
source: ModelSource::Flag,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_env_or_config_or_default(cli_model: &str) -> Self {
|
||||||
|
// Only called when no --model flag was passed. Probe env first,
|
||||||
|
// then config, else fall back to default. Mirrors the logic in
|
||||||
|
// resolve_repl_model() but captures the source.
|
||||||
|
if cli_model != DEFAULT_MODEL {
|
||||||
|
// Already resolved from some prior path; treat as flag.
|
||||||
|
return Self {
|
||||||
|
resolved: cli_model.to_string(),
|
||||||
|
raw: Some(cli_model.to_string()),
|
||||||
|
source: ModelSource::Flag,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if let Some(env_model) = env::var("ANTHROPIC_MODEL")
|
||||||
|
.ok()
|
||||||
|
.map(|value| value.trim().to_string())
|
||||||
|
.filter(|value| !value.is_empty())
|
||||||
|
{
|
||||||
|
return Self {
|
||||||
|
resolved: resolve_model_alias_with_config(&env_model),
|
||||||
|
raw: Some(env_model),
|
||||||
|
source: ModelSource::Env,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if let Some(config_model) = config_model_for_current_dir() {
|
||||||
|
return Self {
|
||||||
|
resolved: resolve_model_alias_with_config(&config_model),
|
||||||
|
raw: Some(config_model),
|
||||||
|
source: ModelSource::Config,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Self::default_fallback()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn max_tokens_for_model(model: &str) -> u32 {
|
fn max_tokens_for_model(model: &str) -> u32 {
|
||||||
if model.contains("opus") {
|
if model.contains("opus") {
|
||||||
32_000
|
32_000
|
||||||
@ -217,9 +307,10 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
} => resume_session(&session_path, &commands, output_format),
|
} => resume_session(&session_path, &commands, output_format),
|
||||||
CliAction::Status {
|
CliAction::Status {
|
||||||
model,
|
model,
|
||||||
|
model_flag_raw,
|
||||||
permission_mode,
|
permission_mode,
|
||||||
output_format,
|
output_format,
|
||||||
} => print_status_snapshot(&model, permission_mode, output_format)?,
|
} => print_status_snapshot(&model, model_flag_raw.as_deref(), permission_mode, output_format)?,
|
||||||
CliAction::Sandbox { output_format } => print_sandbox_status_snapshot(output_format)?,
|
CliAction::Sandbox { output_format } => print_sandbox_status_snapshot(output_format)?,
|
||||||
CliAction::Prompt {
|
CliAction::Prompt {
|
||||||
prompt,
|
prompt,
|
||||||
@ -351,6 +442,10 @@ enum CliAction {
|
|||||||
},
|
},
|
||||||
Status {
|
Status {
|
||||||
model: String,
|
model: String,
|
||||||
|
// #148: raw `--model` flag input (pre-alias-resolution), if any.
|
||||||
|
// None means no flag was supplied; env/config/default fallback is
|
||||||
|
// resolved inside `print_status_snapshot`.
|
||||||
|
model_flag_raw: Option<String>,
|
||||||
permission_mode: PermissionMode,
|
permission_mode: PermissionMode,
|
||||||
output_format: CliOutputFormat,
|
output_format: CliOutputFormat,
|
||||||
},
|
},
|
||||||
@ -447,6 +542,9 @@ impl CliOutputFormat {
|
|||||||
#[allow(clippy::too_many_lines)]
|
#[allow(clippy::too_many_lines)]
|
||||||
fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
||||||
let mut model = DEFAULT_MODEL.to_string();
|
let mut model = DEFAULT_MODEL.to_string();
|
||||||
|
// #148: when user passes --model/--model=, capture the raw input so we
|
||||||
|
// can attribute source: "flag" later. None means no flag was supplied.
|
||||||
|
let mut model_flag_raw: Option<String> = None;
|
||||||
let mut output_format = CliOutputFormat::Text;
|
let mut output_format = CliOutputFormat::Text;
|
||||||
let mut permission_mode_override = None;
|
let mut permission_mode_override = None;
|
||||||
let mut wants_help = false;
|
let mut wants_help = false;
|
||||||
@ -496,12 +594,14 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
|||||||
.ok_or_else(|| "missing value for --model".to_string())?;
|
.ok_or_else(|| "missing value for --model".to_string())?;
|
||||||
validate_model_syntax(value)?;
|
validate_model_syntax(value)?;
|
||||||
model = resolve_model_alias_with_config(value);
|
model = resolve_model_alias_with_config(value);
|
||||||
|
model_flag_raw = Some(value.clone()); // #148
|
||||||
index += 2;
|
index += 2;
|
||||||
}
|
}
|
||||||
flag if flag.starts_with("--model=") => {
|
flag if flag.starts_with("--model=") => {
|
||||||
let value = &flag[8..];
|
let value = &flag[8..];
|
||||||
validate_model_syntax(value)?;
|
validate_model_syntax(value)?;
|
||||||
model = resolve_model_alias_with_config(value);
|
model = resolve_model_alias_with_config(value);
|
||||||
|
model_flag_raw = Some(value.to_string()); // #148
|
||||||
index += 1;
|
index += 1;
|
||||||
}
|
}
|
||||||
"--output-format" => {
|
"--output-format" => {
|
||||||
@ -683,7 +783,7 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
|||||||
return action;
|
return action;
|
||||||
}
|
}
|
||||||
if let Some(action) =
|
if let Some(action) =
|
||||||
parse_single_word_command_alias(&rest, &model, permission_mode_override, output_format)
|
parse_single_word_command_alias(&rest, &model, model_flag_raw.as_deref(), permission_mode_override, output_format)
|
||||||
{
|
{
|
||||||
return action;
|
return action;
|
||||||
}
|
}
|
||||||
@ -885,6 +985,8 @@ fn is_help_flag(value: &str) -> bool {
|
|||||||
fn parse_single_word_command_alias(
|
fn parse_single_word_command_alias(
|
||||||
rest: &[String],
|
rest: &[String],
|
||||||
model: &str,
|
model: &str,
|
||||||
|
// #148: raw --model flag input for status provenance. None = no flag.
|
||||||
|
model_flag_raw: Option<&str>,
|
||||||
permission_mode_override: Option<PermissionMode>,
|
permission_mode_override: Option<PermissionMode>,
|
||||||
output_format: CliOutputFormat,
|
output_format: CliOutputFormat,
|
||||||
) -> Option<Result<CliAction, String>> {
|
) -> Option<Result<CliAction, String>> {
|
||||||
@ -922,6 +1024,7 @@ fn parse_single_word_command_alias(
|
|||||||
"version" => Some(Ok(CliAction::Version { output_format })),
|
"version" => Some(Ok(CliAction::Version { output_format })),
|
||||||
"status" => Some(Ok(CliAction::Status {
|
"status" => Some(Ok(CliAction::Status {
|
||||||
model: model.to_string(),
|
model: model.to_string(),
|
||||||
|
model_flag_raw: model_flag_raw.map(str::to_string), // #148
|
||||||
permission_mode: permission_mode_override.unwrap_or_else(default_permission_mode),
|
permission_mode: permission_mode_override.unwrap_or_else(default_permission_mode),
|
||||||
output_format,
|
output_format,
|
||||||
})),
|
})),
|
||||||
@ -3009,6 +3112,7 @@ fn run_resume_command(
|
|||||||
},
|
},
|
||||||
default_permission_mode().as_str(),
|
default_permission_mode().as_str(),
|
||||||
&context,
|
&context,
|
||||||
|
None, // #148: resumed sessions don't have flag provenance
|
||||||
)),
|
)),
|
||||||
json: Some(status_json_value(
|
json: Some(status_json_value(
|
||||||
session.model.as_deref(),
|
session.model.as_deref(),
|
||||||
@ -3021,6 +3125,7 @@ fn run_resume_command(
|
|||||||
},
|
},
|
||||||
default_permission_mode().as_str(),
|
default_permission_mode().as_str(),
|
||||||
&context,
|
&context,
|
||||||
|
None, // #148: resumed sessions don't have flag provenance
|
||||||
)),
|
)),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -4375,6 +4480,7 @@ impl LiveCli {
|
|||||||
},
|
},
|
||||||
self.permission_mode.as_str(),
|
self.permission_mode.as_str(),
|
||||||
&status_context(Some(&self.session.path)).expect("status context should load"),
|
&status_context(Some(&self.session.path)).expect("status context should load"),
|
||||||
|
None, // #148: REPL /status doesn't carry flag provenance
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -5208,6 +5314,7 @@ fn render_repl_help() -> String {
|
|||||||
|
|
||||||
fn print_status_snapshot(
|
fn print_status_snapshot(
|
||||||
model: &str,
|
model: &str,
|
||||||
|
model_flag_raw: Option<&str>,
|
||||||
permission_mode: PermissionMode,
|
permission_mode: PermissionMode,
|
||||||
output_format: CliOutputFormat,
|
output_format: CliOutputFormat,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
@ -5219,18 +5326,30 @@ fn print_status_snapshot(
|
|||||||
estimated_tokens: 0,
|
estimated_tokens: 0,
|
||||||
};
|
};
|
||||||
let context = status_context(None)?;
|
let context = status_context(None)?;
|
||||||
|
// #148: resolve model provenance. If user passed --model, source is
|
||||||
|
// "flag" with the raw input preserved. Otherwise probe env -> config
|
||||||
|
// -> default and record the winning source.
|
||||||
|
let provenance = match model_flag_raw {
|
||||||
|
Some(raw) => ModelProvenance {
|
||||||
|
resolved: model.to_string(),
|
||||||
|
raw: Some(raw.to_string()),
|
||||||
|
source: ModelSource::Flag,
|
||||||
|
},
|
||||||
|
None => ModelProvenance::from_env_or_config_or_default(model),
|
||||||
|
};
|
||||||
match output_format {
|
match output_format {
|
||||||
CliOutputFormat::Text => println!(
|
CliOutputFormat::Text => println!(
|
||||||
"{}",
|
"{}",
|
||||||
format_status_report(model, usage, permission_mode.as_str(), &context)
|
format_status_report(&provenance.resolved, usage, permission_mode.as_str(), &context, Some(&provenance))
|
||||||
),
|
),
|
||||||
CliOutputFormat::Json => println!(
|
CliOutputFormat::Json => println!(
|
||||||
"{}",
|
"{}",
|
||||||
serde_json::to_string_pretty(&status_json_value(
|
serde_json::to_string_pretty(&status_json_value(
|
||||||
Some(model),
|
Some(&provenance.resolved),
|
||||||
usage,
|
usage,
|
||||||
permission_mode.as_str(),
|
permission_mode.as_str(),
|
||||||
&context,
|
&context,
|
||||||
|
Some(&provenance),
|
||||||
))?
|
))?
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
@ -5242,6 +5361,12 @@ fn status_json_value(
|
|||||||
usage: StatusUsage,
|
usage: StatusUsage,
|
||||||
permission_mode: &str,
|
permission_mode: &str,
|
||||||
context: &StatusContext,
|
context: &StatusContext,
|
||||||
|
// #148: optional provenance for `model` field. Surfaces `model_source`
|
||||||
|
// ("flag" | "env" | "config" | "default") and `model_raw` (user input
|
||||||
|
// before alias resolution, or null when source is "default"). Callers
|
||||||
|
// that don't have provenance (legacy resume paths) pass None, in which
|
||||||
|
// case both new fields are omitted.
|
||||||
|
provenance: Option<&ModelProvenance>,
|
||||||
) -> serde_json::Value {
|
) -> serde_json::Value {
|
||||||
// #143: top-level `status` marker so claws can distinguish
|
// #143: top-level `status` marker so claws can distinguish
|
||||||
// a clean run from a degraded run (config parse failed but other fields
|
// a clean run from a degraded run (config parse failed but other fields
|
||||||
@ -5249,11 +5374,15 @@ fn status_json_value(
|
|||||||
// when present; it's a string rather than a typed object in Phase 1 and
|
// when present; it's a string rather than a typed object in Phase 1 and
|
||||||
// will join the typed-error taxonomy in Phase 2 (ROADMAP §4.44).
|
// will join the typed-error taxonomy in Phase 2 (ROADMAP §4.44).
|
||||||
let degraded = context.config_load_error.is_some();
|
let degraded = context.config_load_error.is_some();
|
||||||
|
let model_source = provenance.map(|p| p.source.as_str());
|
||||||
|
let model_raw = provenance.and_then(|p| p.raw.clone());
|
||||||
json!({
|
json!({
|
||||||
"kind": "status",
|
"kind": "status",
|
||||||
"status": if degraded { "degraded" } else { "ok" },
|
"status": if degraded { "degraded" } else { "ok" },
|
||||||
"config_load_error": context.config_load_error,
|
"config_load_error": context.config_load_error,
|
||||||
"model": model,
|
"model": model,
|
||||||
|
"model_source": model_source,
|
||||||
|
"model_raw": model_raw,
|
||||||
"permission_mode": permission_mode,
|
"permission_mode": permission_mode,
|
||||||
"usage": {
|
"usage": {
|
||||||
"messages": usage.message_count,
|
"messages": usage.message_count,
|
||||||
@ -5352,6 +5481,10 @@ fn format_status_report(
|
|||||||
usage: StatusUsage,
|
usage: StatusUsage,
|
||||||
permission_mode: &str,
|
permission_mode: &str,
|
||||||
context: &StatusContext,
|
context: &StatusContext,
|
||||||
|
// #148: optional model provenance to surface in a `Model source` line.
|
||||||
|
// Callers without provenance (legacy resume paths) pass None and the
|
||||||
|
// source line is omitted for backward compat.
|
||||||
|
provenance: Option<&ModelProvenance>,
|
||||||
) -> String {
|
) -> String {
|
||||||
// #143: if config failed to parse, surface a degraded banner at the top
|
// #143: if config failed to parse, surface a degraded banner at the top
|
||||||
// of the text report so humans see the parse error before the body, while
|
// of the text report so humans see the parse error before the body, while
|
||||||
@ -5368,10 +5501,21 @@ fn format_status_report(
|
|||||||
"Config load error\n Status fail\n Summary runtime config failed to load; reporting partial status\n Details {err}\n Hint `claw doctor` classifies config parse errors; fix the listed field and rerun"
|
"Config load error\n Status fail\n Summary runtime config failed to load; reporting partial status\n Details {err}\n Hint `claw doctor` classifies config parse errors; fix the listed field and rerun"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
// #148: render Model source line after Model, showing where the string
|
||||||
|
// came from (flag / env / config / default) and the raw input if any.
|
||||||
|
let model_source_line = provenance
|
||||||
|
.map(|p| match &p.raw {
|
||||||
|
Some(raw) if raw != model => {
|
||||||
|
format!("\n Model source {} (raw: {raw})", p.source.as_str())
|
||||||
|
}
|
||||||
|
Some(_) => format!("\n Model source {}", p.source.as_str()),
|
||||||
|
None => format!("\n Model source {}", p.source.as_str()),
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
blocks.extend([
|
blocks.extend([
|
||||||
format!(
|
format!(
|
||||||
"{status_line}
|
"{status_line}
|
||||||
Model {model}
|
Model {model}{model_source_line}
|
||||||
Permission mode {permission_mode}
|
Permission mode {permission_mode}
|
||||||
Messages {}
|
Messages {}
|
||||||
Turns {}
|
Turns {}
|
||||||
@ -9786,6 +9930,50 @@ mod tests {
|
|||||||
typo_err.starts_with("unknown subcommand:"),
|
typo_err.starts_with("unknown subcommand:"),
|
||||||
"typo guard should fire for 'sttaus', got: {typo_err}"
|
"typo guard should fire for 'sttaus', got: {typo_err}"
|
||||||
);
|
);
|
||||||
|
// #148: `--model` flag must be captured as model_flag_raw so status
|
||||||
|
// JSON can report provenance (source: flag, raw: <user-input>).
|
||||||
|
match parse_args(&[
|
||||||
|
"--model".to_string(),
|
||||||
|
"sonnet".to_string(),
|
||||||
|
"status".to_string(),
|
||||||
|
])
|
||||||
|
.expect("--model sonnet status should parse")
|
||||||
|
{
|
||||||
|
CliAction::Status {
|
||||||
|
model,
|
||||||
|
model_flag_raw,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
assert_eq!(model, "claude-sonnet-4-6", "sonnet alias should resolve");
|
||||||
|
assert_eq!(
|
||||||
|
model_flag_raw.as_deref(),
|
||||||
|
Some("sonnet"),
|
||||||
|
"raw flag input should be preserved"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
other => panic!("expected CliAction::Status, got: {other:?}"),
|
||||||
|
}
|
||||||
|
// --model= form should also capture raw.
|
||||||
|
match parse_args(&[
|
||||||
|
"--model=anthropic/claude-opus-4-6".to_string(),
|
||||||
|
"status".to_string(),
|
||||||
|
])
|
||||||
|
.expect("--model=... status should parse")
|
||||||
|
{
|
||||||
|
CliAction::Status {
|
||||||
|
model,
|
||||||
|
model_flag_raw,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
assert_eq!(model, "anthropic/claude-opus-4-6");
|
||||||
|
assert_eq!(
|
||||||
|
model_flag_raw.as_deref(),
|
||||||
|
Some("anthropic/claude-opus-4-6"),
|
||||||
|
"--model= form should also preserve raw input"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
other => panic!("expected CliAction::Status, got: {other:?}"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -9971,7 +10159,7 @@ mod tests {
|
|||||||
cumulative: runtime::TokenUsage::default(),
|
cumulative: runtime::TokenUsage::default(),
|
||||||
estimated_tokens: 0,
|
estimated_tokens: 0,
|
||||||
};
|
};
|
||||||
let json = super::status_json_value(Some("test-model"), usage, "workspace-write", &context);
|
let json = super::status_json_value(Some("test-model"), usage, "workspace-write", &context, None);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
json.get("status").and_then(|v| v.as_str()),
|
json.get("status").and_then(|v| v.as_str()),
|
||||||
Some("degraded"),
|
Some("degraded"),
|
||||||
@ -9999,7 +10187,7 @@ mod tests {
|
|||||||
});
|
});
|
||||||
assert!(clean_context.config_load_error.is_none());
|
assert!(clean_context.config_load_error.is_none());
|
||||||
let clean_json =
|
let clean_json =
|
||||||
super::status_json_value(Some("test-model"), usage, "workspace-write", &clean_context);
|
super::status_json_value(Some("test-model"), usage, "workspace-write", &clean_context, None);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
clean_json.get("status").and_then(|v| v.as_str()),
|
clean_json.get("status").and_then(|v| v.as_str()),
|
||||||
Some("ok"),
|
Some("ok"),
|
||||||
@ -10073,6 +10261,7 @@ mod tests {
|
|||||||
parse_args(&["status".to_string()]).expect("status should parse"),
|
parse_args(&["status".to_string()]).expect("status should parse"),
|
||||||
CliAction::Status {
|
CliAction::Status {
|
||||||
model: DEFAULT_MODEL.to_string(),
|
model: DEFAULT_MODEL.to_string(),
|
||||||
|
model_flag_raw: None, // #148: no --model flag passed
|
||||||
permission_mode: PermissionMode::DangerFullAccess,
|
permission_mode: PermissionMode::DangerFullAccess,
|
||||||
output_format: CliOutputFormat::Text,
|
output_format: CliOutputFormat::Text,
|
||||||
}
|
}
|
||||||
@ -11147,6 +11336,7 @@ mod tests {
|
|||||||
sandbox_status: runtime::SandboxStatus::default(),
|
sandbox_status: runtime::SandboxStatus::default(),
|
||||||
config_load_error: None,
|
config_load_error: None,
|
||||||
},
|
},
|
||||||
|
None, // #148
|
||||||
);
|
);
|
||||||
assert!(status.contains("Status"));
|
assert!(status.contains("Status"));
|
||||||
assert!(status.contains("Model claude-sonnet"));
|
assert!(status.contains("Model claude-sonnet"));
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user