mirror of
https://github.com/ultraworkers/claw-code.git
synced 2026-05-30 20:04:17 +08:00
fix(#717): implement agents show/info/describe and list filter commands, mirror skills handler parity
This commit is contained in:
parent
6a007344ae
commit
a0b375c157
@ -7599,3 +7599,5 @@ Original filing (2026-04-18): the session emitted `SessionStart hook (completed)
|
|||||||
715. **Resume-path slash commands (`/compact`, `/clear`, `/cost`, `/stats`, `/history`, `/session exists`, `/session delete`, `memory`) JSON responses missing `action` and `status` fields** — dogfooded 2026-05-26 on `590b5b61`. The `assert_non_empty_action` guardrail added by #3109 only covers `assert_json_command` (top-level CLI surfaces); resume-path commands that emit JSON via `ResumeCommandOutcome.json` were not covered. 8 resume-path JSON sites all lacked `action` and `status`. Fix: added `action` + `status:"ok"` to `compact`, `clear`, `cost`, `stats`, `history`, `session_exists`, `session_delete`, `memory`, and `restored`. Source: Jobdori dogfood on `590b5b61`, 2026-05-26.
|
715. **Resume-path slash commands (`/compact`, `/clear`, `/cost`, `/stats`, `/history`, `/session exists`, `/session delete`, `memory`) JSON responses missing `action` and `status` fields** — dogfooded 2026-05-26 on `590b5b61`. The `assert_non_empty_action` guardrail added by #3109 only covers `assert_json_command` (top-level CLI surfaces); resume-path commands that emit JSON via `ResumeCommandOutcome.json` were not covered. 8 resume-path JSON sites all lacked `action` and `status`. Fix: added `action` + `status:"ok"` to `compact`, `clear`, `cost`, `stats`, `history`, `session_exists`, `session_delete`, `memory`, and `restored`. Source: Jobdori dogfood on `590b5b61`, 2026-05-26.
|
||||||
|
|
||||||
716. **Resume-path error JSON used legacy `{type:"error", error:...}` shape instead of standard `{kind, action, status:"error", error_kind, exit_code}` envelope — 5 error paths affected** — dogfooded 2026-05-26 on `76c8d480`. Session load failure, unsupported command, unsupported resumed command, SlashCommand parse error, and broad-cwd abort all emitted the old two-key shape. Fix: aligned all 5 to `{kind, action:"resume"|"abort", status:"error", error_kind, error, exit_code}`. Updated `resumed_stub_command_emits_not_implemented_json` test to assert `status:"error"` + `kind:"unsupported_command"`. Source: Jobdori dogfood on `76c8d480`, 2026-05-26.
|
716. **Resume-path error JSON used legacy `{type:"error", error:...}` shape instead of standard `{kind, action, status:"error", error_kind, exit_code}` envelope — 5 error paths affected** — dogfooded 2026-05-26 on `76c8d480`. Session load failure, unsupported command, unsupported resumed command, SlashCommand parse error, and broad-cwd abort all emitted the old two-key shape. Fix: aligned all 5 to `{kind, action:"resume"|"abort", status:"error", error_kind, error, exit_code}`. Updated `resumed_stub_command_emits_not_implemented_json` test to assert `status:"error"` + `kind:"unsupported_command"`. Source: Jobdori dogfood on `76c8d480`, 2026-05-26.
|
||||||
|
|
||||||
|
717. **`claw agents show <name>` missing — `handle_agents_slash_command_json` only accepted `list`; `show/info/describe` was unimplemented unlike skills which had parity** — dogfooded 2026-05-26 on `6a007344`. `claw agents show claw-code --output-format json` returned `unknown_agents_subcommand` error. Fix: added `show/info/describe` and `list <filter>` arms to both `handle_agents_slash_command` and `handle_agents_slash_command_json`, mirroring the skills handler; renamed `render_agents_report_json` → `render_agents_report_json_with_action`; not-found path returns `{kind:"agents", action:"show", status:"error", error_kind:"agent_not_found", requested:"<name>"}` + Ok; added `classify_error_kind` branch for `agent_not_found`. Updated 2 tests. Source: Jobdori dogfood on `6a007344`, 2026-05-26.
|
||||||
|
|||||||
@ -2323,10 +2323,50 @@ pub fn handle_agents_slash_command(args: Option<&str>, cwd: &Path) -> std::io::R
|
|||||||
let agents = load_agents_from_roots(&roots)?;
|
let agents = load_agents_from_roots(&roots)?;
|
||||||
Ok(render_agents_report(&agents))
|
Ok(render_agents_report(&agents))
|
||||||
}
|
}
|
||||||
|
Some(args) if args.starts_with("list ") => {
|
||||||
|
let filter = args["list ".len()..].trim().to_lowercase();
|
||||||
|
let roots = discover_definition_roots(cwd, "agents");
|
||||||
|
let agents = load_agents_from_roots(&roots)?;
|
||||||
|
let filtered: Vec<_> = agents
|
||||||
|
.into_iter()
|
||||||
|
.filter(|a| a.name.to_lowercase().contains(&filter))
|
||||||
|
.collect();
|
||||||
|
Ok(render_agents_report(&filtered))
|
||||||
|
}
|
||||||
|
Some("show" | "info" | "describe") => {
|
||||||
|
let roots = discover_definition_roots(cwd, "agents");
|
||||||
|
let agents = load_agents_from_roots(&roots)?;
|
||||||
|
Ok(render_agents_report(&agents))
|
||||||
|
}
|
||||||
|
Some(args)
|
||||||
|
if args.starts_with("show ")
|
||||||
|
|| args.starts_with("info ")
|
||||||
|
|| args.starts_with("describe ") =>
|
||||||
|
{
|
||||||
|
let name = args
|
||||||
|
.split_once(' ')
|
||||||
|
.map(|(_, name)| name)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.trim()
|
||||||
|
.to_lowercase();
|
||||||
|
let roots = discover_definition_roots(cwd, "agents");
|
||||||
|
let agents = load_agents_from_roots(&roots)?;
|
||||||
|
let matched: Vec<_> = agents
|
||||||
|
.into_iter()
|
||||||
|
.filter(|a| a.name.to_lowercase() == name)
|
||||||
|
.collect();
|
||||||
|
if matched.is_empty() {
|
||||||
|
return Err(std::io::Error::new(
|
||||||
|
std::io::ErrorKind::NotFound,
|
||||||
|
format!("agent not found: {name}"),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(render_agents_report(&matched))
|
||||||
|
}
|
||||||
Some(args) if is_help_arg(args) => Ok(render_agents_usage(None)),
|
Some(args) if is_help_arg(args) => Ok(render_agents_usage(None)),
|
||||||
Some(args) => Err(std::io::Error::new(
|
Some(args) => Err(std::io::Error::new(
|
||||||
std::io::ErrorKind::InvalidInput,
|
std::io::ErrorKind::InvalidInput,
|
||||||
format!("unknown agents subcommand: {args}. Supported: list, help"),
|
format!("unknown agents subcommand: {args}. Supported: list, show, help"),
|
||||||
)),
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2347,10 +2387,53 @@ pub fn handle_agents_slash_command_json(args: Option<&str>, cwd: &Path) -> std::
|
|||||||
let agents = load_agents_from_roots(&roots)?;
|
let agents = load_agents_from_roots(&roots)?;
|
||||||
Ok(render_agents_report_json(cwd, &agents))
|
Ok(render_agents_report_json(cwd, &agents))
|
||||||
}
|
}
|
||||||
|
Some(args) if args.starts_with("list ") => {
|
||||||
|
let filter = args["list ".len()..].trim().to_lowercase();
|
||||||
|
let roots = discover_definition_roots(cwd, "agents");
|
||||||
|
let agents = load_agents_from_roots(&roots)?;
|
||||||
|
let filtered: Vec<_> = agents
|
||||||
|
.into_iter()
|
||||||
|
.filter(|a| a.name.to_lowercase().contains(&filter))
|
||||||
|
.collect();
|
||||||
|
Ok(render_agents_report_json(cwd, &filtered))
|
||||||
|
}
|
||||||
|
Some("show" | "info" | "describe") => {
|
||||||
|
let roots = discover_definition_roots(cwd, "agents");
|
||||||
|
let agents = load_agents_from_roots(&roots)?;
|
||||||
|
Ok(render_agents_report_json_with_action(cwd, &agents, "show"))
|
||||||
|
}
|
||||||
|
Some(args)
|
||||||
|
if args.starts_with("show ")
|
||||||
|
|| args.starts_with("info ")
|
||||||
|
|| args.starts_with("describe ") =>
|
||||||
|
{
|
||||||
|
let name = args
|
||||||
|
.split_once(' ')
|
||||||
|
.map(|(_, name)| name)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.trim()
|
||||||
|
.to_lowercase();
|
||||||
|
let roots = discover_definition_roots(cwd, "agents");
|
||||||
|
let agents = load_agents_from_roots(&roots)?;
|
||||||
|
let matched: Vec<_> = agents
|
||||||
|
.into_iter()
|
||||||
|
.filter(|a| a.name.to_lowercase() == name)
|
||||||
|
.collect();
|
||||||
|
if matched.is_empty() {
|
||||||
|
return Ok(serde_json::json!({
|
||||||
|
"kind": "agents",
|
||||||
|
"action": "show",
|
||||||
|
"status": "error",
|
||||||
|
"error_kind": "agent_not_found",
|
||||||
|
"requested": name,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
Ok(render_agents_report_json_with_action(cwd, &matched, "show"))
|
||||||
|
}
|
||||||
Some(args) if is_help_arg(args) => Ok(render_agents_usage_json(None)),
|
Some(args) if is_help_arg(args) => Ok(render_agents_usage_json(None)),
|
||||||
Some(args) => Err(std::io::Error::new(
|
Some(args) => Err(std::io::Error::new(
|
||||||
std::io::ErrorKind::InvalidInput,
|
std::io::ErrorKind::InvalidInput,
|
||||||
format!("unknown agents subcommand: {args}. Supported: list, help"),
|
format!("unknown agents subcommand: {args}. Supported: list, show, help"),
|
||||||
)),
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -3636,6 +3719,14 @@ fn render_agents_report(agents: &[AgentSummary]) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn render_agents_report_json(cwd: &Path, agents: &[AgentSummary]) -> Value {
|
fn render_agents_report_json(cwd: &Path, agents: &[AgentSummary]) -> Value {
|
||||||
|
render_agents_report_json_with_action(cwd, agents, "list")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_agents_report_json_with_action(
|
||||||
|
cwd: &Path,
|
||||||
|
agents: &[AgentSummary],
|
||||||
|
action: &str,
|
||||||
|
) -> Value {
|
||||||
let active = agents
|
let active = agents
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|agent| agent.shadowed_by.is_none())
|
.filter(|agent| agent.shadowed_by.is_none())
|
||||||
@ -3643,7 +3734,7 @@ fn render_agents_report_json(cwd: &Path, agents: &[AgentSummary]) -> Value {
|
|||||||
json!({
|
json!({
|
||||||
"kind": "agents",
|
"kind": "agents",
|
||||||
"status": "ok",
|
"status": "ok",
|
||||||
"action": "list",
|
"action": action,
|
||||||
"working_directory": cwd.display().to_string(),
|
"working_directory": cwd.display().to_string(),
|
||||||
"count": agents.len(),
|
"count": agents.len(),
|
||||||
"summary": {
|
"summary": {
|
||||||
@ -5360,13 +5451,23 @@ mod tests {
|
|||||||
assert_eq!(help["status"], "ok");
|
assert_eq!(help["status"], "ok");
|
||||||
assert_eq!(help["usage"]["direct_cli"], "claw agents [list|help]");
|
assert_eq!(help["usage"]["direct_cli"], "claw agents [list|help]");
|
||||||
|
|
||||||
// Unknown agents subcommands now return Err so CLI layer can exit 1.
|
// `show <name>` is now valid. Known agent returns ok with matching entry.
|
||||||
let unexpected_err = handle_agents_slash_command_json(Some("show planner"), &workspace);
|
let show_planner = handle_agents_slash_command_json(Some("show planner"), &workspace)
|
||||||
|
.expect("show planner should return Ok");
|
||||||
|
assert_eq!(show_planner["status"], "ok");
|
||||||
|
let show_agents = show_planner["agents"].as_array().expect("agents array");
|
||||||
|
assert_eq!(show_agents.len(), 1, "show by exact name returns one entry");
|
||||||
|
assert_eq!(show_agents[0]["name"], "planner");
|
||||||
|
// Missing agent returns Ok(json error) with error_kind:agent_not_found.
|
||||||
|
let show_missing =
|
||||||
|
handle_agents_slash_command_json(Some("show nonexistent-xyz"), &workspace)
|
||||||
|
.expect("show missing agent should return Ok");
|
||||||
|
assert_eq!(show_missing["status"], "error");
|
||||||
|
assert_eq!(show_missing["error_kind"], "agent_not_found");
|
||||||
|
assert_eq!(show_missing["requested"], "nonexistent-xyz");
|
||||||
|
// Truly unknown subcommands still Err.
|
||||||
|
let unexpected_err = handle_agents_slash_command_json(Some("frobnicate"), &workspace);
|
||||||
assert!(unexpected_err.is_err());
|
assert!(unexpected_err.is_err());
|
||||||
assert!(unexpected_err
|
|
||||||
.unwrap_err()
|
|
||||||
.to_string()
|
|
||||||
.contains("show planner"));
|
|
||||||
|
|
||||||
let _ = fs::remove_dir_all(workspace);
|
let _ = fs::remove_dir_all(workspace);
|
||||||
let _ = fs::remove_dir_all(user_home);
|
let _ = fs::remove_dir_all(user_home);
|
||||||
@ -5518,14 +5619,23 @@ mod tests {
|
|||||||
assert!(agents_help
|
assert!(agents_help
|
||||||
.contains("Sources .claw/agents, ~/.claw/agents, $CLAW_CONFIG_HOME/agents"));
|
.contains("Sources .claw/agents, ~/.claw/agents, $CLAW_CONFIG_HOME/agents"));
|
||||||
|
|
||||||
// Unknown agents subcommands now return Err (typed error) instead of Ok+help text
|
// `show <name>` is now valid. For an agent that doesn't exist it returns Err(NotFound).
|
||||||
// so that the CLI layer can exit 1. The error message names the unexpected input.
|
let agents_show_missing = super::handle_agents_slash_command(Some("show planner"), &cwd);
|
||||||
let agents_unexpected_err = super::handle_agents_slash_command(Some("show planner"), &cwd);
|
assert!(
|
||||||
assert!(agents_unexpected_err.is_err());
|
agents_show_missing.is_err(),
|
||||||
assert!(agents_unexpected_err
|
"show of a missing agent should Err"
|
||||||
.unwrap_err()
|
);
|
||||||
.to_string()
|
assert_eq!(
|
||||||
.contains("show planner"));
|
agents_show_missing.unwrap_err().kind(),
|
||||||
|
std::io::ErrorKind::NotFound
|
||||||
|
);
|
||||||
|
// Truly unknown subcommands still Err with InvalidInput.
|
||||||
|
let agents_unknown_err = super::handle_agents_slash_command(Some("frobnicate"), &cwd);
|
||||||
|
assert!(agents_unknown_err.is_err());
|
||||||
|
assert_eq!(
|
||||||
|
agents_unknown_err.unwrap_err().kind(),
|
||||||
|
std::io::ErrorKind::InvalidInput
|
||||||
|
);
|
||||||
|
|
||||||
let skills_help =
|
let skills_help =
|
||||||
super::handle_skills_slash_command(Some("--help"), &cwd).expect("skills help");
|
super::handle_skills_slash_command(Some("--help"), &cwd).expect("skills help");
|
||||||
|
|||||||
@ -302,6 +302,8 @@ fn classify_error_kind(message: &str) -> &'static str {
|
|||||||
"interactive_only"
|
"interactive_only"
|
||||||
} else if message.starts_with("unknown agents subcommand:") {
|
} else if message.starts_with("unknown agents subcommand:") {
|
||||||
"unknown_agents_subcommand"
|
"unknown_agents_subcommand"
|
||||||
|
} else if message.starts_with("agent not found:") {
|
||||||
|
"agent_not_found"
|
||||||
} else if message.contains("is not installed") {
|
} else if message.contains("is not installed") {
|
||||||
"plugin_not_found"
|
"plugin_not_found"
|
||||||
} else if (message.contains("skill source") && message.contains("not found"))
|
} else if (message.contains("skill source") && message.contains("not found"))
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user