Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -6428,3 +6428,41 @@ Original filing (2026-04-18): the session emitted `SessionStart hook (completed)


450. **`prompt` emits `kind:"missing_credentials"` JSON on STDERR (not stdout), leaving stdout at 0 bytes — automation pattern `output=$(claw prompt hello --output-format json)` captures nothing on auth-absent failure; `doctor` correctly surfaces `auth.status:"warn"` with `api_key_present:false` but exposes no `prompt_ready:false` field that automation can check before invoking `prompt`** — dogfooded 2026-05-16 by Jobdori on `a35ee9a0` in response to Clawhip pinpoint nudge at `1505208225321062521`. Exact reproduction (isolated env, no creds, fresh git repo, HEAD `a35ee9a0`): `timeout 5 env -i HOME=$ISOLATED_HOME PATH=$PATH CLAW_CONFIG_HOME=$PROBE/.claw-cfg claw prompt hello --output-format json > stdout.txt 2> stderr.txt` → stdout = **0 bytes**, stderr = 195 bytes containing `{"error":"missing Anthropic credentials…","exit_code":1,"hint":null,"kind":"missing_credentials","type":"error"}`, exit code 1. Confirms Gaebal's `1505208553793781792` pinpoint that `prompt` timeout + zero bytes was the prior state — HEAD `a35ee9a0` now correctly exits 1 with `kind:"missing_credentials"` **but the envelope is still routed to stderr** (issue #447 class, same class as prior entries #422, #435). **Contrast with `doctor`:** `claw doctor --output-format json 2>/dev/null` succeeds to stdout with `checks[auth].status:"warn"`, `api_key_present:false`, `auth_token_present:false` — but the auth check has no `prompt_ready:false` field. Automation that gates on `doctor` before invoking `prompt` must re-derive readiness from `api_key_present && auth_token_present` — there is no single canonical boolean. **Three compound problems:** (a) **stdout-empty on `--output-format json` failure**: same class as #447; `prompt`'s error envelope goes to stderr, not stdout. The canonical automation idiom `if ! result=$(claw prompt "q" --output-format json); then echo "$result" | jq .kind; fi` sees `$result=""` on failure — the jq call gets nothing. All `--output-format json` error paths must route JSON to stdout per #447 contract; (b) **`doctor` missing `prompt_ready` field**: `doctor --output-format json` already knows auth is absent (`api_key_present:false`) but surfaces no derived `prompt_ready:bool` or `prompt_blocked_reason:string` field. Automation must infer readiness from `api_key_present || auth_token_present || legacy_*_present` — a 5-field OR across legacy fields that is fragile as auth mechanisms evolve. A single `prompt_ready:false` (with `prompt_blocked_reason:"auth_missing"`) inside the `auth` check would give downstream a stable contract; (c) **`claw prompt` with no auth does no preflight and fires straight at the API**: the preflight check that `doctor` runs (auth discovery) is not reused by `prompt` to emit a fast typed error before attempting the network call. Both Gaebal's pinpoint (prompt hanging silently on older HEAD) and the current behavior (prompt hitting auth gate after a brief API attempt) stem from the same root: prompt does not short-circuit at the point where `doctor` already knows auth is absent. If `doctor` can emit `kind:"doctor"` with `auth.status:"warn"` in ~20ms without a network call, `prompt` should emit `kind:"missing_credentials"` in the same window and output it to stdout. **Required fix shape:** (a) `prompt --output-format json` must write the `kind:"missing_credentials"` JSON envelope to **stdout**, not stderr — same fix as #447 for all error envelopes; (b) add `prompt_ready:bool` and `prompt_blocked_reason:string|null` to the `auth` check in `doctor --output-format json`; derive it as `api_key_present || auth_token_present || legacy_saved_oauth_present`; (c) `prompt` must run the credential preflight check (same codepath as doctor's auth check) before attempting any API call and emit `{"kind":"missing_credentials","prompt_blocked_reason":"auth_missing"}` on **stdout** with exit 1 if the check fails; (d) `--output-format json` stdout routing fix must cover: `prompt`, `session list` (cross-ref #449), `skills uninstall` (cross-ref #431), `resume` (cross-ref #435), `acp serve` (cross-ref #443) — the full `kind:"missing_credentials"` class; (e) regression test: `claw prompt hello --output-format json` with no creds writes JSON to stdout (0 bytes stderr), exits 1, `kind:"missing_credentials"`, in under 200ms (no network attempt). **Why this matters:** `prompt` is the primary consumer entry point. Auth-absent failure routing to stderr breaks every automation wrapper that captures `$(claw prompt ... --output-format json)`. The `doctor` preflight metadata gap means auth-readiness checks require parsing 5 legacy fields instead of reading one boolean. Cross-references #447 (all JSON error envelopes on stderr), #449 (session list hits auth gate), #431 (skills uninstall hits auth gate), #357 (auth gate on local ops cluster), #422 (exit-code parity). Source: Jobdori live dogfood, `a35ee9a0`, 2026-05-16.

461. **`.claw.json` / `.claw/settings.json` unknown-key validator uses pure-edit-distance ranking with threshold ≤3, which actively misleads users away from the canonical `mcpServers` key — `{ "mcp": { "servers": {} } }` (the VS Code MCP convention, also used by hermes_cli and many other tools) produces `unknown key "mcp" (line 2). Did you mean "env"?`. Edit-distance(`mcp`, `mcpServers`) = 7 (exceeds threshold), edit-distance(`mcp`, `env`) = 3 (at threshold), so the validator hands the user a "go configure environment variables" hint instead of "you wrote the VS Code-style nested form, write the flat `mcpServers` form instead." User follows the suggestion → no MCP servers configured → no warning that MCP was their actual intent → silent loss of functionality with an actively wrong remediation path** — dogfooded 2026-05-24 for the 12:00 Clawhip pinpoint nudge at message `1508077133459751073` (also covers the 11:30 nudge `1508069585461706783`), reproduced on local `./rust/target/debug/claw` `git_sha 003b739d` (origin/main `f8e1bb72`). Suggestion-quality matrix in clean isolated env (`HOME=/tmp/iso15/home`, fresh `/tmp/iso15/proj` git-init, `.claw/settings.json` with one top-level typo each):

| Input | Edit distance to `mcpServers` | Edit distance to `env` | Actual suggestion | Correct? |
|---|---|---|---|---|
| `mcp` (VS Code nested form) | 7 | **3** | `env` | ❌ **opposite intent** |
| `mcpServer` (singular camelCase) | 1 | 7 | `mcpServers` | ✅ |
| `mcpserver` (singular lowercase) | 2 | 6 | `mcpServers` | ✅ |
| `mcpServrs` (typo) | 1 | 8 | `mcpServers` | ✅ |
| `MCPServers` (case variant) | 3 | 8 | `mcpServers` | ✅ |
| `mcp_servers` (snake_case) | 2 | 7 | `mcpServers` | ✅ |
| `servers` (just servers) | 3 | 5 | `mcpServers` | ✅ |
| `mcp` ← **the actual VS Code convention** | 7 | 3 | `env` | ❌ |

**Root cause (traced):** `rust/crates/runtime/src/config_validate.rs:396-407`:

```rust
fn suggest_field(input: &str, candidates: &[&str]) -> Option<String> {
let input_lower = input.to_ascii_lowercase();
candidates
.iter()
.filter_map(|candidate| {
let distance = simple_edit_distance(&input_lower, &candidate.to_ascii_lowercase());
(distance <= 3).then_some((distance, *candidate))
})
.min_by_key(|(distance, _)| *distance)
.map(|(_, name)| name.to_string())
}
```

Pure Levenshtein, threshold-3 filter, no prefix awareness, no semantic awareness. The candidate set is the 14 top-level field names registered in `FIELD_SPECS` (`config_validate.rs:144-200`): `$schema`, `model`, `hooks`, `permissions`, `permissionMode`, `mcpServers`, `oauth`, `enabledPlugins`, `plugins`, `sandbox`, `env`, `aliases`, `providerFallbacks`, `trustedRoots`. For input `"mcp"` (length 3):

- distance to `mcpServers` = 7 (insertions of `Servers`) — filtered out by threshold
- distance to `env` = 3 (full substitution) — at threshold, wins by `.min_by_key()`
- distance to `oauth`/`model`/`hooks` = 5 — filtered out
- distance to `$schema` = 6 — filtered out

So `env` is structurally the ONLY survivor of the threshold for short inputs that happen to share zero characters with their actual intended target. **`mcp` is a 3-character prefix of `mcpServers` and shares 100% of its characters with the intended key**, but edit-distance has no way to express that — every additional character in the longer candidate counts as a +1 insertion penalty. **Why distinct from existing items:** ROADMAP #110 covers `ConfigLoader::discover` ancestor-walk under-discovery (config files invisible from subdirs). ROADMAP #28 covers `MissingCredentials` error-copy improvements (adjacent-provider env-var hints). ROADMAP #108 covers CLI subcommand typo fallthrough (no levenshtein for subcommands). **None** address the config-key validator's suggestion-ranking algorithm itself producing actively wrong suggestions for prefix-substring inputs. The "unknown key" diagnostic was added to be helpful — but for the most common real-world miss (`mcp` from users following VS Code convention or from MCP docs that use nested form), it points at the wrong remediation. This is **worse than no suggestion** because users following a bad suggestion lose more time than users getting "unknown key" with no hint and going to read the docs. **Why this matters:** (1) **The VS Code MCP convention is widespread.** VS Code's settings.json uses `"mcp": { "servers": { ... } }`. Cursor uses `mcpServers` flat. Claude Desktop uses `mcpServers` flat. A user who has been configuring MCP servers in VS Code naturally writes the nested form. claw advertises MCP support ("Claude Code parity") but rejects the most-common convention with an **actively misleading remediation**. (2) **Hermes CLI** (in `external/hermes-agent/hermes_cli/config.py:478`) uses `"mcp": { ... }` nested. Any user copying from hermes config will hit this. (3) **`env` is the absolute worst possible suggestion** — it's a completely different concept (environment variables for the CLI) with zero overlap with MCP server registration. A user who follows the suggestion will add an `env` block, fail to register their server, and have no signal that MCP was their actual intent. (4) **Silent functionality loss:** the MCP-related work the user intended is silently dropped (configured_servers: 0 in `mcp list`). No `doctor` check warns "your config has an `mcp` block that probably should have been `mcpServers`." (5) **The bug class is general** — any short input that is a strict prefix of a long canonical key, where the long key edit-distance exceeds 3, will fall through to whatever happens to be within edit-distance-3 of the input. Edge cases for other current top-level keys: `auth` → suggests `env` (distance 3, vs `oauth` distance 1 — but `oauth` wins because distance 1, OK by luck); `plugin` → distance 1 to `plugins`, fine; `perms` → distance 6 to `permissions`, no suggestion. Future schema additions could create more `mcp`-like edge cases silently. (6) **The `--help` text and docs say `.claw/settings.json` accepts MCP config**, and any user reading the JSON Schema for `mcpServers` and naturally writing the nested form gets pointed at `env`. Documentation/validator divergence. **Required fix shape:** (a) **Add prefix-match as a higher-priority signal than edit-distance.** In `suggest_field`, before computing edit-distance, check if `input` is a prefix of any candidate (case-insensitive) AND that candidate's length ≤ `input.len() * 4` (sanity bound). If yes, return that candidate immediately. Specifically: `if input.len() >= 2 && input is a strict prefix of candidate`, that candidate gets priority over edit-distance matches. This makes `mcp` → `mcpServers` (prefix match wins) instead of `mcp` → `env` (edit-distance match wins). (b) **Add nested-key awareness for known scoped patterns.** When the unknown key is the parent of a known-nested form (e.g. `mcp.servers` would be valid under VS Code convention), emit a specific diagnostic: `"unknown key 'mcp' — claw uses the flat camelCase form 'mcpServers' instead of the VS Code-style 'mcp.servers' nested block. Rewrite as: { \"mcpServers\": { ... } }"`. Hardcode this for `mcp` initially; generalize if other nested-vs-flat divergences appear. (c) **Add a `claw doctor` check `mcp_config_form_drift`** that detects `mcp` keys at the top level of `.claw.json` / `.claw/settings.json` and warns: `WARN: .claw/settings.json contains a top-level "mcp" block (VS Code convention). claw uses "mcpServers" (flat). Migrate to: { "mcpServers": <inner.servers> }`. (d) **Update README and `--help`** to document the canonical `mcpServers` key and explicitly call out that `mcp.servers` (VS Code style) is NOT accepted. (e) **Regression coverage:** add a test for `suggest_field("mcp", FIELD_SPECS)` that asserts the result is `Some("mcpServers")` (not `Some("env")`). Add the full suggestion matrix above as parameterized tests. **Acceptance check (one-liner):** `cd /tmp/test && mkdir -p .claw && echo '{"mcp":{"servers":{"a":{"command":"x"}}}}' > .claw/settings.json && claw mcp list --output-format json | jq -r '.config_load_error' | grep -E '"mcpServers"|VS Code|migrate'` should match (it currently outputs `Did you mean "env"?`). Source: gaebal-gajae dogfood follow-up spanning two consecutive Clawhip pinpoint nudges (2026-05-24 11:30 message `1508069585461706783` triggered the MCP investigation; 12:00 message `1508077133459751073` triggered finishing it; matrix completed and root-cause traced between the two).
Loading