Compare commits

..

2 Commits

Author SHA1 Message Date
YeonGyu-Kim
b601b20644 docs(roadmap): remove forward ref to unmerged #403 from #410 entry 2026-04-30 20:33:47 +09:00
YeonGyu-Kim
b7a469d66c docs(roadmap): add #410 — agents/skills/mcp list count-field naming drift 2026-04-30 20:01:43 +09:00
3 changed files with 4 additions and 93 deletions

View File

@@ -6301,4 +6301,4 @@ Original filing (2026-04-18): the session emitted `SessionStart hook (completed)
380. **Top-level `tokens --help --output-format json` hangs with zero stdout/stderr instead of returning bounded command help JSON** — dogfooded 2026-04-30 for the 02:30 nudge on current `origin/main` / rebuilt `./rust/target/debug/claw` with embedded `git_sha` `d95b230c`. After verifying #358 covered `cost --help`, a fresh adjacent probe on the token-budget surface showed the same silent failure class: repeated bounded runs of `timeout 8 ./rust/target/debug/claw tokens --help --output-format json` exited `124` with `stdout=0` and `stderr=0`. In the same rebuilt binary, `version --output-format json` returned promptly with version/build metadata, proving the binary itself and JSON output path are reachable. This is distinct from #358's cost help hang: the affected surface is the sibling `tokens` command help, which agents use before estimating prompt/session token budgets. **Required fix shape:** (a) make `tokens --help --output-format json` return static/bounded stdout JSON with `kind:"help"` or `kind:"tokens"`, `action:"help"`, usage, options, examples, supported output formats, and related slash/direct commands; (b) ensure help rendering does not initialize slow token accounting, session, or provider state; (c) if any dynamic provider is consulted, return a typed JSON timeout/unavailable error instead of hanging; (d) add regression coverage proving tokens help in JSON mode returns within a deterministic budget. **Why this matters:** token budgeting is a preflight clawability surface. If help hangs silently, automation cannot safely discover how to inspect or constrain token usage before running expensive prompts, and budget-aware wrappers stall at the discovery step. Source: gaebal-gajae dogfood follow-up for the 02:30 nudge on rebuilt `./rust/target/debug/claw` `d95b230c`.
381. **Top-level `cache --help --output-format json` hangs with zero stdout/stderr instead of returning bounded command help JSON** — dogfooded 2026-04-30 for the 03:00 nudge on current `origin/main` / rebuilt `./rust/target/debug/claw` with embedded `git_sha` `d95b230c`. After #358 and #380 landed for the cost/tokens preflight help hangs, a fresh adjacent probe on the cache-control surface showed the same silent failure class: repeated bounded runs of `timeout --kill-after=1s 8s ./rust/target/debug/claw cache --help --output-format json` exited `124` with `stdout=0` and `stderr=0`. In the same rebuilt binary, `version --output-format json` returned promptly with version/build metadata, proving the binary itself and JSON output path are reachable. This is distinct from the separate `/cache` slash-command envelope mismatch class: the affected surface here is top-level `cache` command help, where agents need bounded local discovery before deciding whether to inspect, clear, or summarize cache state. **Required fix shape:** (a) make `cache --help --output-format json` return static/bounded stdout JSON with `kind:"help"` or `kind:"cache"`, `action:"help"`, usage, options, examples, supported output formats, and related slash/direct commands; (b) ensure help rendering does not initialize slow cache/session/provider state; (c) if any dynamic provider is consulted, return a typed JSON timeout/unavailable error instead of hanging; (d) add regression coverage proving cache help in JSON mode returns within a deterministic budget. **Why this matters:** cache inspection and cleanup are recovery/control-plane operations. If cache help hangs silently, claws cannot safely discover cache semantics before attempting cleanup, and automation stalls before it can choose a non-destructive cache action. Source: gaebal-gajae dogfood follow-up for the 03:00 nudge on rebuilt `./rust/target/debug/claw` `d95b230c`.
422. **`export --output-format json` and `--resume latest` report the same "no managed sessions" scenario using two different `kind` codes — `no_managed_sessions` vs `session_load_failed` — making "no session found" undetectable by a single kind-code check** — dogfooded 2026-04-30 KST (UTC+9) by Jobdori on `e939777f`. Running `claw export --output-format json` with no session present returns (on stderr, exit 1): `{"error":"no managed sessions found in .claw/sessions/<fingerprint>/","hint":"Start \`claw\` to create a session, then rerun with \`--resume latest\`.\nNote: claw partitions sessions per workspace fingerprint; sessions from other CWDs are invisible.","kind":"no_managed_sessions","type":"error"}`. Running `claw --resume latest /status --output-format json` with no session present returns (on stderr, exit 1): `{"error":"failed to restore session: no managed sessions found in .claw/sessions/<fingerprint>/","hint":"Start \`claw\` to create a session, then rerun with \`--resume latest\`.\nNote: claw partitions sessions per workspace fingerprint; sessions from other CWDs are invisible.","kind":"session_load_failed","type":"error"}`. Both describe the same root condition — there are no sessions to operate on — but they expose it via different `kind` discriminants. Automation that checks `kind == "no_managed_sessions"` to detect a cold workspace will miss the `--resume` path's `session_load_failed`, and vice versa. A wrapper that guards "run with --resume only if a session exists" must special-case both codes. The hint text is identical between them, suggesting the messages are logically equivalent. Additionally neither code matches the proposed canonical names `session_not_found` / `session_load_failed` as stable `ErrorKind` discriminants described in ROADMAP #77's fix shape, which explicitly proposes typed error-kind codes for session lifecycle failures. **Required fix shape:** (a) unify "no sessions found for this workspace fingerprint" under a single canonical `kind` code — either `no_managed_sessions` or `session_not_found` — used consistently by every command path that encounters an empty session registry; (b) if `session_load_failed` is a more general category (covering e.g. corrupt session files, IO errors, schema version mismatches), it should nest a concrete `reason:"no_managed_sessions"` or `reason:"session_not_found"` sub-field so callers can distinguish "empty registry" from "found but unreadable"; (c) align with the canonical error-kind contract proposed in #77; (d) add regression coverage proving `export` and `--resume latest` in an empty workspace both return an error with the same top-level `kind` code. **Why this matters:** session guard-rails in orchestration need a single stable `kind` to detect cold workspaces without enumerating all possible no-session synonyms. Two divergent codes for the same condition make defensive automation brittle and contradict the promise of machine-readable error envelopes. Source: Jobdori live dogfood, `e939777f`, 2026-04-30 KST (UTC+9).
410. **`agents list`, `skills list`, and `mcp list` use three different count-field names and divergent envelope schemas despite being sibling list commands** — dogfooded 2026-04-30 by Jobdori on `e939777f`. Running all three list commands with `--output-format json` reveals incompatible envelope shapes: `agents list` emits `count:int` at the top level plus `summary:{active,shadowed,total}` and `working_directory`; `skills list` emits no top-level `count`, only `summary:{active,shadowed,total}`, and omits `working_directory`; `mcp list` uses a different count-field name `configured_servers:int`, has no `count`, no `summary`, and instead adds `status:"ok"` and `config_load_error:null` fields absent from the other two. The three sibling commands cannot be polymorphically consumed with the same count-extraction logic, requiring per-command special-casing at the cardinality check level. **Required fix shape:** (a) define one canonical top-level count field name (`count`, `total`, or `item_count`) and use it across `agents`, `skills`, and `mcp` list envelopes; (b) define one canonical `summary` object shape with at minimum `active`, `total`, and optionally `shadowed` and include it on all three; (c) expose `working_directory` consistently on all list commands or omit it from all; (d) add regression coverage proving the three list envelopes share the same count-field name and summary shape before each release. **Why this matters:** orchestration lanes that inventory agents, skills, and servers before delegation need one count-extraction pattern. Three different field names force per-command special-casing of the most basic cardinality check. Source: Jobdori live dogfood, `e939777f`, 2026-04-30.

View File

@@ -6207,7 +6207,7 @@ fn render_config_report(section: Option<&str>) -> Result<String, Box<dyn std::er
}
fn render_config_json(
section: Option<&str>,
_section: Option<&str>,
) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
let cwd = env::current_dir()?;
let loader = ConfigLoader::default_for(&cwd);
@@ -6240,52 +6240,13 @@ fn render_config_json(
})
.collect();
let base = serde_json::json!({
Ok(serde_json::json!({
"kind": "config",
"cwd": cwd.display().to_string(),
"loaded_files": loaded_paths.len(),
"merged_keys": runtime_config.merged().len(),
"files": files,
});
if let Some(section) = section {
let section_rendered: Option<String> = match section {
"env" => runtime_config.get("env").map(|v| v.render()),
"hooks" => runtime_config.get("hooks").map(|v| v.render()),
"model" => runtime_config.get("model").map(|v| v.render()),
"plugins" => runtime_config
.get("plugins")
.or_else(|| runtime_config.get("enabledPlugins"))
.map(|v| v.render()),
other => {
return Ok(serde_json::json!({
"kind": "config",
"section": other,
"ok": false,
"error": format!("Unsupported config section '{other}'. Use env, hooks, model, or plugins."),
"cwd": cwd.display().to_string(),
"loaded_files": loaded_paths.len(),
"files": files,
}));
}
};
// Parse the rendered JSON string back into serde_json::Value so that
// section_value is a real JSON object/array in the envelope, not a quoted string.
let section_value: serde_json::Value = section_rendered
.as_deref()
.and_then(|s| serde_json::from_str(s).ok())
.unwrap_or(serde_json::Value::Null);
let mut obj = base;
let map = obj.as_object_mut().expect("base is object");
map.insert(
"section".to_string(),
serde_json::Value::String(section.to_string()),
);
map.insert("section_value".to_string(), section_value);
return Ok(obj);
}
Ok(base)
}))
}
fn render_memory_report() -> Result<String, Box<dyn std::error::Error>> {

View File

@@ -105,18 +105,6 @@ fn inventory_commands_emit_structured_json_when_requested() {
let skills = assert_json_command(&root, &["--output-format", "json", "skills"]);
assert_eq!(skills["kind"], "skills");
assert_eq!(skills["action"], "list");
let plugins = assert_json_command(&root, &["--output-format", "json", "plugins"]);
assert_eq!(plugins["kind"], "plugin");
assert_eq!(plugins["action"], "list");
assert!(
plugins["reload_runtime"].is_boolean(),
"plugins reload_runtime should be a boolean"
);
assert!(
plugins["target"].is_null(),
"plugins target should be null when no plugin is targeted"
);
}
#[test]
@@ -396,44 +384,6 @@ fn resumed_version_and_init_emit_structured_json_when_requested() {
assert!(root.join("CLAUDE.md").exists());
}
#[test]
fn config_section_json_emits_section_and_value() {
let root = unique_temp_dir("config-section-json");
fs::create_dir_all(&root).expect("temp dir should exist");
// Without a section: should return base envelope (no section field).
let base = assert_json_command(&root, &["--output-format", "json", "config"]);
assert_eq!(base["kind"], "config");
assert!(base["loaded_files"].is_number());
assert!(base["merged_keys"].is_number());
assert!(
base.get("section").is_none(),
"no section field without section arg"
);
// With a known section: should add section + section_value fields.
for section in &["model", "env", "hooks", "plugins"] {
let result = assert_json_command(&root, &["--output-format", "json", "config", section]);
assert_eq!(result["kind"], "config", "section={section}");
assert_eq!(
result["section"].as_str(),
Some(*section),
"section field must match requested section, got {result:?}"
);
assert!(
result.get("section_value").is_some(),
"section_value field must be present for section={section}"
);
}
// With an unsupported section: should return ok:false + error field.
let bad = assert_json_command(&root, &["--output-format", "json", "config", "unknown"]);
assert_eq!(bad["kind"], "config");
assert_eq!(bad["ok"], false);
assert!(bad["error"].as_str().is_some());
assert!(bad["section"].as_str().is_some());
}
fn assert_json_command(current_dir: &Path, args: &[&str]) -> Value {
assert_json_command_with_env(current_dir, args, &[])
}