mirror of
https://github.com/instructkr/claude-code.git
synced 2026-05-28 00:16:45 +00:00
fix(#698): dedup config deprecation warnings per process; add tempfile dev-dep to runtime crate (fixes pre-existing test compile error)
This commit is contained in:
@@ -7559,3 +7559,5 @@ Original filing (2026-04-18): the session emitted `SessionStart hook (completed)
|
|||||||
696. **`claw compact` (and likely other REPL-only commands) hangs indefinitely in non-interactive mode with no TTY — there is no timeout, no stdin-closed guard, and no `--output-format json` fast-exit path** — dogfooded 2026-05-25 on `bb2a9238`. Running `./rust/target/debug/claw compact --output-format json --help </dev/null` (or `compact` without `--help`) never returns: the process blocks waiting for interactive input that will never arrive. No timeout fires, no error is emitted on stdout, no `{"kind":"error",...}` JSON is produced. The caller must `kill` the process externally. This is a clawability gap: any orchestrator or claw that probes `compact` to discover its schema or trigger non-interactive compaction will hang until a watchdog kills it. **Required fix shape:** (a) detect stdin EOF / non-TTY at startup for REPL-only commands and emit `{"kind":"error","error_kind":"interactive_only","message":"claw compact requires an interactive terminal"}` with exit 1 instead of blocking; (b) honour `--output-format json` as an implicit non-interactive signal — if the flag is set and no TTY is present, exit immediately with a typed error; (c) add a global `--timeout <seconds>` flag or default 30 s watchdog for non-interactive invocations; (d) add regression coverage proving `claw compact </dev/null` exits non-zero within 1 s with a structured error. **Why this matters:** automation layers probe commands via `</dev/null` to discover schema; a hung process blocks the probe indefinitely and requires external timeout management, making compact undiscoverable and unscriptable. Source: Jobdori dogfood on `bb2a9238`, 2026-05-25.
|
696. **`claw compact` (and likely other REPL-only commands) hangs indefinitely in non-interactive mode with no TTY — there is no timeout, no stdin-closed guard, and no `--output-format json` fast-exit path** — dogfooded 2026-05-25 on `bb2a9238`. Running `./rust/target/debug/claw compact --output-format json --help </dev/null` (or `compact` without `--help`) never returns: the process blocks waiting for interactive input that will never arrive. No timeout fires, no error is emitted on stdout, no `{"kind":"error",...}` JSON is produced. The caller must `kill` the process externally. This is a clawability gap: any orchestrator or claw that probes `compact` to discover its schema or trigger non-interactive compaction will hang until a watchdog kills it. **Required fix shape:** (a) detect stdin EOF / non-TTY at startup for REPL-only commands and emit `{"kind":"error","error_kind":"interactive_only","message":"claw compact requires an interactive terminal"}` with exit 1 instead of blocking; (b) honour `--output-format json` as an implicit non-interactive signal — if the flag is set and no TTY is present, exit immediately with a typed error; (c) add a global `--timeout <seconds>` flag or default 30 s watchdog for non-interactive invocations; (d) add regression coverage proving `claw compact </dev/null` exits non-zero within 1 s with a structured error. **Why this matters:** automation layers probe commands via `</dev/null` to discover schema; a hung process blocks the probe indefinitely and requires external timeout management, making compact undiscoverable and unscriptable. Source: Jobdori dogfood on `bb2a9238`, 2026-05-25.
|
||||||
|
|
||||||
697. **`claw plugins remove <name>` silently returns `status:"ok"` with exit 0 when the named plugin does not exist — no `not_found` error, no non-zero exit, no indication the operation was a no-op; sibling: `claw agents <unknown-subcommand>` returns `action:"help"` with exit 0 instead of a typed `unknown_subcommand` error** — dogfooded 2026-05-25 on `63a5a874`. Reproduction: `claw plugins remove nonexistent-plugin --output-format json </dev/null` returns `{"kind":"plugin","action":"remove","status":"ok","error":null,...}` and exits 0. No plugin named `nonexistent-plugin` exists. A caller cannot distinguish "plugin was successfully removed" from "plugin was never there" — both produce the same `status:"ok"` envelope. Sibling: `claw agents stop nonexistent-agent --output-format json </dev/null` returns `{"kind":"agents","action":"help","unexpected":"stop nonexistent-agent",...}` and exits 0 — unknown subcommand falls through to help output rather than a typed error with exit 1. **Required fix shape:** (a) `plugins remove` must check whether the named plugin exists before reporting success; emit `{"kind":"plugin","action":"remove","status":"error","error_kind":"plugin_not_found","plugin_name":"<name>"}` with exit 1 when the plugin is absent; (b) `agents <unknown>` must emit `{"kind":"agents","action":"error","error_kind":"unknown_subcommand","subcommand":"<token>","supported":["list","help"]}` with exit 1 instead of falling back to help output with exit 0; (c) add regression tests proving both paths exit 1 with typed error envelopes. **Why this matters:** idempotent-but-silent remove is fine for infrastructure tools with explicit idempotency contracts; claw has no such contract, and `status:"ok"` for a name-miss means automation cannot audit whether a remove actually ran vs was a no-op. Source: Jobdori dogfood on `63a5a874`, 2026-05-25.
|
697. **`claw plugins remove <name>` silently returns `status:"ok"` with exit 0 when the named plugin does not exist — no `not_found` error, no non-zero exit, no indication the operation was a no-op; sibling: `claw agents <unknown-subcommand>` returns `action:"help"` with exit 0 instead of a typed `unknown_subcommand` error** — dogfooded 2026-05-25 on `63a5a874`. Reproduction: `claw plugins remove nonexistent-plugin --output-format json </dev/null` returns `{"kind":"plugin","action":"remove","status":"ok","error":null,...}` and exits 0. No plugin named `nonexistent-plugin` exists. A caller cannot distinguish "plugin was successfully removed" from "plugin was never there" — both produce the same `status:"ok"` envelope. Sibling: `claw agents stop nonexistent-agent --output-format json </dev/null` returns `{"kind":"agents","action":"help","unexpected":"stop nonexistent-agent",...}` and exits 0 — unknown subcommand falls through to help output rather than a typed error with exit 1. **Required fix shape:** (a) `plugins remove` must check whether the named plugin exists before reporting success; emit `{"kind":"plugin","action":"remove","status":"error","error_kind":"plugin_not_found","plugin_name":"<name>"}` with exit 1 when the plugin is absent; (b) `agents <unknown>` must emit `{"kind":"agents","action":"error","error_kind":"unknown_subcommand","subcommand":"<token>","supported":["list","help"]}` with exit 1 instead of falling back to help output with exit 0; (c) add regression tests proving both paths exit 1 with typed error envelopes. **Why this matters:** idempotent-but-silent remove is fine for infrastructure tools with explicit idempotency contracts; claw has no such contract, and `status:"ok"` for a name-miss means automation cannot audit whether a remove actually ran vs was a no-op. Source: Jobdori dogfood on `63a5a874`, 2026-05-25.
|
||||||
|
|
||||||
|
698. **Config deprecation warnings emit once per `ConfigLoader::load()` call, so surfaces that call `load()` multiple times in a single invocation emit duplicate `warning:` lines to stderr — `claw plugins list` and `claw mcp list` each print the same deprecation warning twice** — dogfooded 2026-05-25 on `c345ce6d`. Reproduction: `echo '{"enabledPlugins": {}}' > ~/.claw/settings.json && claw plugins list 2>&1 | grep warning` prints the same `field "enabledPlugins" is deprecated. Use "plugins.enabled" instead` line twice. Root cause: `config.rs:304` emits `eprintln!("warning: {warning}")` for every warning in every `loader.load()` call; surfaces like `plugins_command_payload_for` and `render_mcp_report_json_for` each trigger an independent `loader.load()` (one for runtime config, one inside the command handler), multiplying the stderr output. `skills list` emits only one warning because its command path calls `load()` once; `plugins` and `mcp` emit two. **Required fix shape:** (a) track already-emitted warning strings in a process-lifetime `std::sync::OnceLock<Mutex<HashSet<String>>>` in `config.rs` and skip re-emitting duplicates within the same process run; or (b) collect all warnings at a single call site after all config loads are complete and emit once with dedup; or (c) change `load()` to return warnings alongside the result instead of eagerly printing them, letting call sites emit once. Option (a) is a minimal one-file fix. **Why this matters:** duplicate warnings make the CLI look buggy, cause CI log noise, and — when the deprecation warning fires on every invocation — are more likely to be `tail -f`'d away than acted on. A single clean warning per invocation is the standard. Source: Jobdori dogfood on `c345ce6d`, 2026-05-25.
|
||||||
|
|||||||
@@ -16,5 +16,8 @@ telemetry = { path = "../telemetry" }
|
|||||||
tokio = { version = "1", features = ["io-std", "io-util", "macros", "process", "rt", "rt-multi-thread", "time"] }
|
tokio = { version = "1", features = ["io-std", "io-util", "macros", "process", "rt", "rt-multi-thread", "time"] }
|
||||||
walkdir = "2"
|
walkdir = "2"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tempfile = "3"
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|||||||
@@ -1,7 +1,22 @@
|
|||||||
use std::collections::BTreeMap;
|
use std::collections::{BTreeMap, HashSet};
|
||||||
use std::fmt::{Display, Formatter};
|
use std::fmt::{Display, Formatter};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
/// Process-lifetime set of already-emitted config deprecation warning strings.
|
||||||
|
/// Prevents duplicate warnings when `ConfigLoader::load()` is called multiple
|
||||||
|
/// times within a single CLI invocation. (ROADMAP #698)
|
||||||
|
static EMITTED_CONFIG_WARNINGS: std::sync::OnceLock<Mutex<HashSet<String>>> =
|
||||||
|
std::sync::OnceLock::new();
|
||||||
|
|
||||||
|
fn emit_config_warning_once(warning: &str) {
|
||||||
|
let set = EMITTED_CONFIG_WARNINGS.get_or_init(|| Mutex::new(HashSet::new()));
|
||||||
|
let mut guard = set.lock().unwrap_or_else(|e| e.into_inner());
|
||||||
|
if guard.insert(warning.to_string()) {
|
||||||
|
eprintln!("warning: {warning}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
use crate::json::JsonValue;
|
use crate::json::JsonValue;
|
||||||
use crate::sandbox::{FilesystemIsolationMode, SandboxConfig};
|
use crate::sandbox::{FilesystemIsolationMode, SandboxConfig};
|
||||||
@@ -301,7 +316,7 @@ impl ConfigLoader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for warning in &all_warnings {
|
for warning in &all_warnings {
|
||||||
eprintln!("warning: {warning}");
|
emit_config_warning_once(&warning.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
let merged_value = JsonValue::Object(merged.clone());
|
let merged_value = JsonValue::Object(merged.clone());
|
||||||
|
|||||||
Reference in New Issue
Block a user