ROADMAP #105: claw status ignores .claw.json model, doctor mislabels alias as Resolved, 4 surfaces disagree

Dogfooded 2026-04-18 on main HEAD 6580903 from /tmp/cdZ.

.claw.json with {"model":"haiku"} produces:
  claw status → model: 'claude-opus-4-6' (DEFAULT_MODEL, config ignored)
  claw doctor → 'Resolved model    haiku' (raw alias, label lies)
  turn dispatch → claude-haiku-4-5-20251213 (actually-resolved canonical)
  ANTHROPIC_MODEL=sonnet → status still says claude-opus-4-6

FOUR separate understandings of 'active model':
  1. config file (alias as written)
  2. doctor (alias mislabeled as 'Resolved')
  3. status (hardcoded DEFAULT_MODEL ignoring config entirely)
  4. turn dispatch (canonical, alias-resolved, what turns actually use)

Trace:
  main.rs:59  DEFAULT_MODEL const = claude-opus-4-6
  main.rs:400 parse_args starts model = DEFAULT_MODEL
  main.rs:753 Status dispatch: model.to_string() — never calls
      resolve_repl_model, never reads config or env
  main.rs:1125 resolve_repl_model: source of truth for actual
      model, consults ANTHROPIC_MODEL env + config + alias table.
      Called from Prompt and Repl dispatch. NOT from Status.
  main.rs:1701 check_config_health: 'Resolved model {model}'
      where model is raw configured string, not resolved.
      Label says Resolved, value is pre-resolution alias.

Orchestration hazard: a claw picks tool strategy based on
status.model assuming it reflects what turns will use. Status
lies: always reports DEFAULT_MODEL unless --model flag was
passed. Config and env var completely ignored by status.

Fix shape (~30 lines):
- call resolve_repl_model from print_status_snapshot
- add effective_model field to status JSON (or rename/enrich)
- fix doctor 'Resolved model' label (either rename to 'Configured'
  or actually alias-resolve before emitting)
- honor ANTHROPIC_MODEL env in status
- regression tests per model source with cross-surface equality

Joins truth-audit (#80-#84, #86, #87, #89, #100, #102, #103).
Joins two-paths-diverge (#91, #101, #104) — now 4-way with #105.
Joins doctor-surface-coverage triangle (#100 + #102 + #105).

Filed in response to Clawhip pinpoint nudge 1494819785676947543
in #clawcode-building-in-public.
This commit is contained in:
YeonGyu-Kim 2026-04-18 07:08:25 +09:00
parent 6580903d20
commit 71e77290b9

View File

@ -2707,3 +2707,88 @@ ear], /color [scheme], /effort [low|medium|high], /fast, /summary, /tag [label],
**Blocker.** None. Unification + extension-preservation is ~50 lines. Path-traversal policy is ~20 lines + an architectural decision on whether to restrict. All additive, backward-compatible if the "append `.txt` if extension isn't `.txt`" logic is replaced with "pass through whatever the caller asked for."
**Source.** Jobdori dogfood 2026-04-18 against `/tmp/cdY` on main HEAD `7447232` in response to Clawhip pinpoint nudge at `1494812230372294849`. Joins the **silent-flag / documented-but-unenforced** cluster (#96-#101) on the filename-rewrite dimension: documented interface is `/export [file]`, actual behavior silently rewrites the file extension. Joins the **two-paths-diverge** sub-cluster with the permission-mode parser disagreement (#91) and CLI↔env surface mismatch (#101): different input surfaces for the same logical action with non-equivalent semantics. Natural bundle: **#91 + #101 + #104** — three instances of the same meta-pattern (parallel entry points to the same subsystem that do subtly different things). Also **#96 + #98 + #99 + #101 + #104** as the full silent-rewrite-or-silent-noop quintet.
105. **`claw status` ignores `.claw.json`'s `model` field entirely and always reports the compile-time `DEFAULT_MODEL` (`claude-opus-4-6`), while `claw doctor` reports the raw *configured* alias string (e.g. `haiku`) mislabeled as "Resolved model", and the actual turn-dispatch path resolves the alias to the canonical name (e.g. `claude-haiku-4-5-20251213`) via a third code path (`resolve_repl_model`). Four separate surfaces disagree on "what is this lane's active model?": config file (alias as written), `doctor` (alias mislabeled as resolved), `status` (hardcoded default, config ignored), and turn dispatch (canonical, alias-resolved). A claw reading `status` JSON to pick a tool/routing strategy based on the active model will make decisions against a model string that is neither configured nor actually used** — dogfooded 2026-04-18 on main HEAD `6580903` from `/tmp/cdZ`. `.claw.json` with `{"model":"haiku"}` produces `status.model = "claude-opus-4-6"` and `doctor` config detail `Resolved model haiku` simultaneously. Neither value matches what an actual turn would use (`claude-haiku-4-5-20251213`).
**Concrete repro.**
```
$ cd /tmp/cdZ && git init -q .
$ echo '{"model":"haiku"}' > .claw.json
# status JSON — ignores config, returns DEFAULT_MODEL
$ claw --output-format json status | jq '.model'
"claude-opus-4-6"
# doctor — reads config, shows raw alias mislabeled as "Resolved"
$ claw --output-format json doctor | jq '.checks[] | select(.name=="config") | .details[] | select(contains("model"))'
"Resolved model haiku"
# Actual resolution at turn dispatch would be claude-haiku-4-5-20251213
# (via resolve_repl_model → resolve_model_alias_with_config → resolve_model_alias)
$ echo '{"model":"claude-opus-4-6"}' > .claw.json
$ claw --output-format json status | jq '.model'
"claude-opus-4-6"
# Same status output regardless of what the config says
# The only reason it's "correct" here is that DEFAULT_MODEL happens to match.
$ echo '{"model":"sonnet"}' > .claw.json
$ claw --output-format json status | jq '.model'
"claude-opus-4-6"
# Config says sonnet. Status says opus. Reality (turn dispatch) would use claude-sonnet-4-6.
```
**Trace path.**
- `rust/crates/rusty-claude-cli/src/main.rs:59``const DEFAULT_MODEL: &str = "claude-opus-4-6";`
- `rust/crates/rusty-claude-cli/src/main.rs:400``parse_args` starts with `let mut model = DEFAULT_MODEL.to_string();`. Model is set by `--model` flag only.
- `rust/crates/rusty-claude-cli/src/main.rs:753-757` — Status dispatch:
```rust
"status" => Some(Ok(CliAction::Status {
model: model.to_string(), // ← DEFAULT_MODEL unless --model flag given
permission_mode: permission_mode_override.unwrap_or_else(default_permission_mode),
output_format,
})),
```
No call to `config_model_for_current_dir()`, no alias resolution.
- `rust/crates/rusty-claude-cli/src/main.rs:222``CliAction::Status { model, ... } => print_status_snapshot(&model, ...)`. The hardcoded default flows straight into the status JSON builder.
- `rust/crates/rusty-claude-cli/src/main.rs:1125-1140``resolve_repl_model` (actual turn-dispatch model resolution):
```rust
fn resolve_repl_model(cli_model: String) -> String {
if cli_model != DEFAULT_MODEL {
return cli_model;
}
if let Some(env_model) = env::var("ANTHROPIC_MODEL").ok()...{ return resolve_model_alias_with_config(&env_model); }
if let Some(config_model) = config_model_for_current_dir() { return resolve_model_alias_with_config(&config_model); }
cli_model
}
```
**This is the function that actually produces the model a turn will use.** It consults `ANTHROPIC_MODEL` env, `config_model_for_current_dir`, and runs alias resolution. It is called from `Prompt` and `Repl` dispatch paths. **It is NOT called from the `Status` dispatch path.**
- `rust/crates/rusty-claude-cli/src/main.rs:1018-1024``resolve_model_alias`:
```rust
"opus" => "claude-opus-4-6",
"sonnet" => "claude-sonnet-4-6",
"haiku" => "claude-haiku-4-5-20251213",
```
Alias → canonical mapping. Only applied by `resolve_model_alias_with_config`, which `status` never calls.
- `rust/crates/rusty-claude-cli/src/main.rs:1701-1780``check_config_health` (doctor config check) emits `format!("Resolved model {model}")` where `model` is whatever `runtime_config.model()` returned — the raw configured string, *not* alias-resolved. Label says "Resolved" but the value is the pre-resolution alias.
**Why this is specifically a clawability gap.**
1. *Four separate "active model" values.* Config file (what was written), `doctor` ("Resolved model" = raw alias), `status` (hardcoded DEFAULT_MODEL ignoring config entirely), turn dispatch (canonical, alias-resolved). A claw has no way from any single surface to know what the *real* active model is.
2. *Orchestration hazard.* A claw picks tool strategy or routing based on `status.model` — a reasonable assumption that `status` tells you the active model. The status JSON lies: it says "claude-opus-4-6" even when `.claw.json` says "haiku" and turns will actually run against haiku. A claw that specializes prompts for opus vs haiku will specialize for the wrong model.
3. *Label mismatch in doctor.* `doctor` reports "Resolved model haiku" — the word "Resolved" implies alias resolution happened. It didn't. The actual resolved value is `claude-haiku-4-5-20251213`. The label is misleading.
4. *Silent config drop by status.* No warning, no error. A claw's `.claw.json` configuration is simply ignored by the most visible diagnostic surface. Operators debugging why "model switch isn't taking effect" get the same false-answer from `status` whether they configured haiku, sonnet, or anything else.
5. *ANTHROPIC_MODEL env var is also status-invisible.* `ANTHROPIC_MODEL=haiku claw --output-format json status | jq '.model'` returns `"claude-opus-4-6"`. Same as config: status ignores it. Actual turn dispatch honors it. Third surface that disagrees with status.
6. *Joins truth-audit cluster as a severe case.* #80 (`claw status` Project root vs session partition) and #87 (fresh-workspace default permissions) both captured "status lies by omission or wrong-default." This is "status lies by outright reporting a value that is not the real one, despite the information being readable from adjacent code paths."
**Fix shape — make status consult config + alias resolution, match doctor's honesty, align with turn dispatch.**
1. *Call `resolve_repl_model` from `print_status_snapshot`.* The function already exists and is the source of truth for "what model will this lane use." ~5 lines to route the status model through it before emitting JSON.
2. *Add an `effective_model` field to status JSON.* Field name choice: either replace `model` with the resolved value, or split into `configured_model` (from config), `env_model` (from `ANTHROPIC_MODEL`), and `effective_model` (what turns will use). The three-field form is more machine-readable; the single replaced field is simpler. Pick based on back-compat preference. ~15 lines.
3. *Fix doctor's "Resolved model" label.* Change to "Configured model" since that's what the value actually is, or alias-resolve before emitting so the label matches the content. ~5 lines.
4. *Honor `ANTHROPIC_MODEL` env in status.* Same resolution path as turn dispatch. ~3 lines.
5. *Regression tests.* One per model source (default / flag / env / config / alias / canonical). Assert `status`, `doctor`, and turn-dispatch model-resolution all produce equivalent values for the same inputs.
**Acceptance.** `.claw.json` with `{"model":"haiku"}` produces `status.model = "claude-haiku-4-5-20251213"` (or `status.effective_model` plus `configured_model: "haiku"` for the multi-field variant). `doctor` either labels the value "Configured model" (honest label for raw alias) or alias-resolves the value to match `status`. `ANTHROPIC_MODEL=sonnet claw status` shows `claude-sonnet-4-6`. All four surfaces agree.
**Blocker.** None. Calling `resolve_repl_model` from `status` is trivially small. The architectural decision is whether to rename `model` to `effective_model` (breaks consumers who rely on the current field semantics — but the current field is wrong anyway) or to add a sibling field (safer). Either way, ~30 lines plus tests.
**Source.** Jobdori dogfood 2026-04-18 against `/tmp/cdZ` on main HEAD `6580903` in response to Clawhip pinpoint nudge at `1494819785676947543`. Joins **truth-audit / diagnostic-integrity** (#80#84, #86, #87, #89, #100, #102, #103) — status JSON lies about the active model. Joins **two-paths-diverge** (#91, #101, #104) — three separate model-resolution paths with incompatible outputs. Sibling of **#100** (status JSON missing commit identity) and **#102** (doctor silent on MCP reachability) — same pattern: status/doctor surfaces incomplete or wrong information about things they claim to report. Natural bundle: **#100 + #102 + #105** — status/doctor surface completeness triangle (commit identity + MCP reachability + model-resolution truth). Also **#91 + #101 + #104 + #105** — four-way parallel-entry-point asymmetry (config↔CLI parser, CLI↔env silent-vs-loud, slash↔CLI export, config↔status↔dispatch model). Session tally: ROADMAP #105.