mirror of
https://github.com/ultraworkers/claw-code.git
synced 2026-05-07 13:22:12 +08:00
fix(skills): route show/info/list-filter to local, not model invoke (#2988)
`claw skills show <name>`, `claw skills info <name>`, and `claw skills list <filter>` were all falling through to SkillSlashDispatch::Invoke, which spawned a real model session, consumed tokens, and created session files. Root cause: classify_skills_slash_command had no guards for these discovery prefixes; every non-reserved arg became Invoke. Fix: - Add "show", "info" as Local-only bare tokens - Add starts_with guards for "show ", "info ", "list " args - handle_skills_slash_command: filter skill list by name/substring for show/info/list-filter paths (no model call, no session) - handle_skills_slash_command_json: same structured filtering Test: skills_show_and_list_filter_do_not_invoke_model asserts classify_skills_slash_command returns Local for all discovery patterns and still returns Invoke for bare skill names. Pinpoint: ROADMAP #502
This commit is contained in:
parent
9b97c4d832
commit
94b80a05d3
@ -2371,6 +2371,40 @@ pub fn handle_skills_slash_command(args: Option<&str>, cwd: &Path) -> std::io::R
|
|||||||
let skills = load_skills_from_roots(&roots)?;
|
let skills = load_skills_from_roots(&roots)?;
|
||||||
Ok(render_skills_report(&skills))
|
Ok(render_skills_report(&skills))
|
||||||
}
|
}
|
||||||
|
Some(args) if args.starts_with("list ") => {
|
||||||
|
let filter = args["list ".len()..].trim().to_lowercase();
|
||||||
|
let roots = discover_skill_roots(cwd);
|
||||||
|
let skills = load_skills_from_roots(&roots)?;
|
||||||
|
let filtered: Vec<_> = skills
|
||||||
|
.into_iter()
|
||||||
|
.filter(|s| s.name.to_lowercase().contains(&filter))
|
||||||
|
.collect();
|
||||||
|
Ok(render_skills_report(&filtered))
|
||||||
|
}
|
||||||
|
Some("show" | "info" | "describe") => {
|
||||||
|
let roots = discover_skill_roots(cwd);
|
||||||
|
let skills = load_skills_from_roots(&roots)?;
|
||||||
|
Ok(render_skills_report(&skills))
|
||||||
|
}
|
||||||
|
Some(args)
|
||||||
|
if args.starts_with("show ")
|
||||||
|
|| args.starts_with("info ")
|
||||||
|
|| args.starts_with("describe ") =>
|
||||||
|
{
|
||||||
|
let name = args
|
||||||
|
.splitn(2, ' ')
|
||||||
|
.nth(1)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.trim()
|
||||||
|
.to_lowercase();
|
||||||
|
let roots = discover_skill_roots(cwd);
|
||||||
|
let skills = load_skills_from_roots(&roots)?;
|
||||||
|
let matched: Vec<_> = skills
|
||||||
|
.into_iter()
|
||||||
|
.filter(|s| s.name.to_lowercase() == name)
|
||||||
|
.collect();
|
||||||
|
Ok(render_skills_report(&matched))
|
||||||
|
}
|
||||||
Some("install") => Ok(render_skills_usage(Some("install"))),
|
Some("install") => Ok(render_skills_usage(Some("install"))),
|
||||||
Some(args) if args.starts_with("install ") => {
|
Some(args) if args.starts_with("install ") => {
|
||||||
let target = args["install ".len()..].trim();
|
let target = args["install ".len()..].trim();
|
||||||
@ -2402,6 +2436,40 @@ pub fn handle_skills_slash_command_json(args: Option<&str>, cwd: &Path) -> std::
|
|||||||
let skills = load_skills_from_roots(&roots)?;
|
let skills = load_skills_from_roots(&roots)?;
|
||||||
Ok(render_skills_report_json(&skills))
|
Ok(render_skills_report_json(&skills))
|
||||||
}
|
}
|
||||||
|
Some(args) if args.starts_with("list ") => {
|
||||||
|
let filter = args["list ".len()..].trim().to_lowercase();
|
||||||
|
let roots = discover_skill_roots(cwd);
|
||||||
|
let skills = load_skills_from_roots(&roots)?;
|
||||||
|
let filtered: Vec<_> = skills
|
||||||
|
.into_iter()
|
||||||
|
.filter(|s| s.name.to_lowercase().contains(&filter))
|
||||||
|
.collect();
|
||||||
|
Ok(render_skills_report_json(&filtered))
|
||||||
|
}
|
||||||
|
Some("show" | "info" | "describe") => {
|
||||||
|
let roots = discover_skill_roots(cwd);
|
||||||
|
let skills = load_skills_from_roots(&roots)?;
|
||||||
|
Ok(render_skills_report_json(&skills))
|
||||||
|
}
|
||||||
|
Some(args)
|
||||||
|
if args.starts_with("show ")
|
||||||
|
|| args.starts_with("info ")
|
||||||
|
|| args.starts_with("describe ") =>
|
||||||
|
{
|
||||||
|
let name = args
|
||||||
|
.splitn(2, ' ')
|
||||||
|
.nth(1)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.trim()
|
||||||
|
.to_lowercase();
|
||||||
|
let roots = discover_skill_roots(cwd);
|
||||||
|
let skills = load_skills_from_roots(&roots)?;
|
||||||
|
let matched: Vec<_> = skills
|
||||||
|
.into_iter()
|
||||||
|
.filter(|s| s.name.to_lowercase() == name)
|
||||||
|
.collect();
|
||||||
|
Ok(render_skills_report_json(&matched))
|
||||||
|
}
|
||||||
Some("install") => Ok(render_skills_usage_json(Some("install"))),
|
Some("install") => Ok(render_skills_usage_json(Some("install"))),
|
||||||
Some(args) if args.starts_with("install ") => {
|
Some(args) if args.starts_with("install ") => {
|
||||||
let target = args["install ".len()..].trim();
|
let target = args["install ".len()..].trim();
|
||||||
@ -2419,10 +2487,20 @@ pub fn handle_skills_slash_command_json(args: Option<&str>, cwd: &Path) -> std::
|
|||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn classify_skills_slash_command(args: Option<&str>) -> SkillSlashDispatch {
|
pub fn classify_skills_slash_command(args: Option<&str>) -> SkillSlashDispatch {
|
||||||
match normalize_optional_args(args) {
|
match normalize_optional_args(args) {
|
||||||
None | Some("list" | "help" | "-h" | "--help") => SkillSlashDispatch::Local,
|
None | Some("list" | "help" | "-h" | "--help" | "show" | "info" | "describe") => {
|
||||||
|
SkillSlashDispatch::Local
|
||||||
|
}
|
||||||
Some(args) if args == "install" || args.starts_with("install ") => {
|
Some(args) if args == "install" || args.starts_with("install ") => {
|
||||||
SkillSlashDispatch::Local
|
SkillSlashDispatch::Local
|
||||||
}
|
}
|
||||||
|
Some(args)
|
||||||
|
if args.starts_with("list ")
|
||||||
|
|| args.starts_with("show ")
|
||||||
|
|| args.starts_with("info ")
|
||||||
|
|| args.starts_with("describe ") =>
|
||||||
|
{
|
||||||
|
SkillSlashDispatch::Local
|
||||||
|
}
|
||||||
Some(args) => SkillSlashDispatch::Invoke(format!("${}", args.trim_start_matches('/'))),
|
Some(args) => SkillSlashDispatch::Invoke(format!("${}", args.trim_start_matches('/'))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -4619,6 +4697,32 @@ mod tests {
|
|||||||
assert!(agents_error.contains(" Usage /agents [list|help]"));
|
assert!(agents_error.contains(" Usage /agents [list|help]"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn skills_show_and_list_filter_do_not_invoke_model() {
|
||||||
|
// `show`, `info`, `list <filter>` must route to Local, not Invoke.
|
||||||
|
// Regression for: `claw skills show plan` unexpectedly spawned a model session.
|
||||||
|
for token in &["show", "info", "describe"] {
|
||||||
|
assert_eq!(
|
||||||
|
classify_skills_slash_command(Some(token)),
|
||||||
|
SkillSlashDispatch::Local,
|
||||||
|
"`skills {token}` alone must be Local"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for prefix in &["show ", "info ", "list ", "describe "] {
|
||||||
|
let arg = format!("{prefix}plan");
|
||||||
|
assert_eq!(
|
||||||
|
classify_skills_slash_command(Some(&arg)),
|
||||||
|
SkillSlashDispatch::Local,
|
||||||
|
"`skills {arg}` must be Local, not Invoke"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Bare invocable tokens still dispatch to Invoke.
|
||||||
|
assert_eq!(
|
||||||
|
classify_skills_slash_command(Some("plan")),
|
||||||
|
SkillSlashDispatch::Invoke("$plan".to_string()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn accepts_skills_invocation_arguments_for_prompt_dispatch() {
|
fn accepts_skills_invocation_arguments_for_prompt_dispatch() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user