Compare commits

..

3 Commits

Author SHA1 Message Date
YeonGyu-Kim
46ddb248f2 fix(config): emit section and section_value in JSON output for config subcommands
`claw config model --output-format json` and all other section subcommands
(`env`, `hooks`, `plugins`) returned identical output with no section field
— the section arg was parsed but discarded (_section parameter).

Fix: render_config_json now:
- Passes section through to handler
- Looks up the section value via runtime_config.get(), converting the
  internal JsonValue to serde_json::Value via render()+parse
- Emits `section` (string) and `section_value` (JSON value or null)
  in the response envelope
- Returns ok:false + error for unsupported section tokens

Test: config_section_json_emits_section_and_value asserts:
- No section field when no section arg
- section + section_value fields present for all known sections
- ok:false + error for unknown section

Pinpoint: ROADMAP #126
2026-05-04 19:05:09 +09:00
YeonGyu-Kim
8e45f1850c test(output_format_contract): add plugins json coverage to inventory_commands test (#2972)
Add four assertions to inventory_commands_emit_structured_json_when_requested:
- kind == "plugin"
- action == "list"
- reload_runtime is boolean
- target is null when no plugin is targeted

Closes the only major --output-format json surface with zero contract
coverage. All other surfaces (agents, mcp, skills, status, sandbox,
doctor, help, version, acp, bootstrap-plan, system-prompt, init, diff,
config) already had test assertions.
2026-05-01 06:03:31 +09:00
YeonGyu-Kim
57096b0a1a docs(roadmap): add no-session kind drift item
Adds ROADMAP #422 documenting the concrete export/resume no-session ErrorKind drift.
2026-05-01 02:46:12 +09:00
3 changed files with 94 additions and 3 deletions

View File

@@ -6300,3 +6300,5 @@ Original filing (2026-04-18): the session emitted `SessionStart hook (completed)
358. **Top-level `cost --help --output-format json` hangs with zero stdout/stderr instead of returning bounded command help JSON** — dogfooded 2026-04-30 for the 02:00 nudge on current `origin/main` / rebuilt `./rust/target/debug/claw` with embedded `git_sha` `d95b230c`. After rebuilding and verifying the binary provenance, repeated bounded runs of `timeout 8 ./rust/target/debug/claw cost --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 the JSON output path are reachable; the hang is specific to the cost help path, though other help surfaces have separate known JSON contract issues (#356/#357). **Required fix shape:** (a) make `cost --help --output-format json` return static/bounded stdout JSON with `kind:"help"` or `kind:"cost"`, `action:"help"`, usage, options, examples, supported output formats, and related slash/direct commands; (b) ensure help rendering does not initialize slow cost/session/accounting providers; (c) if any dynamic provider is accidentally consulted, return a typed JSON timeout/unavailable error instead of hanging; (d) add regression coverage proving cost help in JSON mode returns within a deterministic budget. **Why this matters:** cost/tokens surfaces are commonly consumed by automation for budgeting. If even cost help can hang silently, claws cannot discover cost command semantics or present safe budget diagnostics before running potentially slow accounting paths. Source: gaebal-gajae dogfood follow-up for the 02:00 nudge on rebuilt `./rust/target/debug/claw` `d95b230c`.
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).

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,13 +6240,52 @@ fn render_config_json(
})
.collect();
Ok(serde_json::json!({
let base = 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,6 +105,18 @@ 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]
@@ -384,6 +396,44 @@ 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, &[])
}