diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index f6879c58e6e5..35af22d6363f 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -2794,6 +2794,7 @@ dependencies = [ "semver", "serde", "serde_json", + "serde_yaml", "tar", "tempfile", "thiserror 2.0.18", diff --git a/codex-rs/app-server/src/config/external_agent_config.rs b/codex-rs/app-server/src/config/external_agent_config.rs index ca1023e52918..3100717fc3b0 100644 --- a/codex-rs/app-server/src/config/external_agent_config.rs +++ b/codex-rs/app-server/src/config/external_agent_config.rs @@ -3,19 +3,19 @@ use codex_core::config::Config; use codex_core::config::ConfigBuilder; use codex_core_plugins::PluginInstallRequest; use codex_core_plugins::PluginsManager; +use codex_core_plugins::command_migration::count_missing_commands; +use codex_core_plugins::command_migration::import_commands; +use codex_core_plugins::command_migration::missing_command_names; use codex_core_plugins::marketplace::MarketplacePluginInstallPolicy; use codex_core_plugins::marketplace::find_marketplace_manifest_path; use codex_core_plugins::marketplace_add::MarketplaceAddRequest; use codex_core_plugins::marketplace_add::add_marketplace; use codex_core_plugins::marketplace_add::is_local_marketplace_source; use codex_external_agent_migration::build_mcp_config_from_external; -use codex_external_agent_migration::count_missing_commands; use codex_external_agent_migration::count_missing_subagents; use codex_external_agent_migration::hook_migration_event_names; -use codex_external_agent_migration::import_commands; use codex_external_agent_migration::import_hooks; use codex_external_agent_migration::import_subagents; -use codex_external_agent_migration::missing_command_names; use codex_external_agent_migration::missing_subagent_names; use codex_external_agent_sessions::ExternalAgentSessionMigration; use codex_external_agent_sessions::detect_recent_sessions; diff --git a/codex-rs/core-plugins/Cargo.toml b/codex-rs/core-plugins/Cargo.toml index 81cfe2f89cc9..3d3f05d32599 100644 --- a/codex-rs/core-plugins/Cargo.toml +++ b/codex-rs/core-plugins/Cargo.toml @@ -41,6 +41,7 @@ regex = { workspace = true } semver = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } +serde_yaml = { workspace = true } tar = { workspace = true } tempfile = { workspace = true } thiserror = { workspace = true } diff --git a/codex-rs/core-plugins/src/command_migration.rs b/codex-rs/core-plugins/src/command_migration.rs new file mode 100644 index 000000000000..75edf4a7730e --- /dev/null +++ b/codex-rs/core-plugins/src/command_migration.rs @@ -0,0 +1,348 @@ +use codex_utils_absolute_path::AbsolutePathBuf; +use serde_yaml::Value as YamlValue; +use std::collections::BTreeMap; +use std::fs; +use std::io; +use std::path::Path; +use std::path::PathBuf; + +const SOURCE_EXTERNAL_AGENT_NAME: &str = "claude"; +const COMMAND_SKILL_PREFIX: &str = "source-command"; +const MAX_SKILL_NAME_LEN: usize = 64; +const PLUGIN_COMMANDS_DIR: &str = "commands"; +const PLUGIN_METADATA_DIR: &str = ".codex-plugin"; +const MIGRATED_COMMAND_SKILLS_DIR: &str = "migrated-command-skills"; + +#[derive(Debug)] +struct ParsedCommand { + description: Option, + body: String, +} + +pub fn count_missing_commands(source_commands: &Path, target_skills: &Path) -> io::Result { + Ok(missing_command_names(source_commands, target_skills)?.len()) +} + +pub fn missing_command_names( + source_commands: &Path, + target_skills: &Path, +) -> io::Result> { + Ok(unique_supported_command_sources(source_commands)? + .into_iter() + .filter(|(_source_file, name)| !target_skills.join(name).exists()) + .map(|(_source_file, name)| name) + .collect()) +} + +pub fn import_commands(source_commands: &Path, target_skills: &Path) -> io::Result> { + if !source_commands.is_dir() { + return Ok(Vec::new()); + } + + fs::create_dir_all(target_skills)?; + let mut imported = Vec::new(); + for (source_file, name) in unique_supported_command_sources(source_commands)? { + let document = parse_command(&source_file)?; + let target_dir = target_skills.join(&name); + if target_dir.exists() { + continue; + } + fs::create_dir_all(&target_dir)?; + let source_name = command_source_name(source_commands, &source_file); + let Some(description) = document.description.as_deref() else { + continue; + }; + fs::write( + target_dir.join("SKILL.md"), + render_command_skill(&document.body, &name, description, &source_name), + )?; + imported.push(name); + } + + Ok(imported) +} + +pub(crate) fn migrate_plugin_commands(plugin_root: &Path) -> io::Result<()> { + import_commands( + &plugin_root.join(PLUGIN_COMMANDS_DIR), + &plugin_root + .join(PLUGIN_METADATA_DIR) + .join(MIGRATED_COMMAND_SKILLS_DIR), + )?; + Ok(()) +} + +pub(crate) fn migrated_command_skills_root(plugin_root: &AbsolutePathBuf) -> AbsolutePathBuf { + plugin_root + .join(PLUGIN_METADATA_DIR) + .join(MIGRATED_COMMAND_SKILLS_DIR) +} + +fn unique_supported_command_sources(source_commands: &Path) -> io::Result> { + let mut by_name = BTreeMap::>::new(); + for source_file in command_source_files(source_commands)? { + let document = parse_command(&source_file)?; + let Some(name) = command_skill_name_if_supported(source_commands, &source_file, &document) + else { + continue; + }; + by_name.entry(name).or_default().push(source_file); + } + + Ok(by_name + .into_iter() + .filter_map(|(name, source_files)| { + let [source_file] = source_files.as_slice() else { + return None; + }; + Some((source_file.clone(), name)) + }) + .collect()) +} + +fn command_source_files(source_commands: &Path) -> io::Result> { + let mut files = Vec::new(); + collect_markdown_files(source_commands, &mut files)?; + files.sort(); + Ok(files) +} + +fn collect_markdown_files(dir: &Path, files: &mut Vec) -> io::Result<()> { + if !dir.is_dir() { + return Ok(()); + } + + for entry in fs::read_dir(dir)? { + let entry = entry?; + let path = entry.path(); + let file_type = entry.file_type()?; + if file_type.is_dir() { + collect_markdown_files(&path, files)?; + } else if file_type.is_file() && path.extension().and_then(|ext| ext.to_str()) == Some("md") + { + files.push(path); + } + } + Ok(()) +} + +fn parse_command(source_file: &Path) -> io::Result { + Ok(parse_command_content(&fs::read_to_string(source_file)?)) +} + +fn parse_command_content(content: &str) -> ParsedCommand { + let Some(rest) = content + .strip_prefix("---\n") + .or_else(|| content.strip_prefix("---\r\n")) + else { + return ParsedCommand { + description: None, + body: content.to_string(), + }; + }; + let Some((end, body_start)) = frontmatter_end(rest) else { + return ParsedCommand { + description: None, + body: content.to_string(), + }; + }; + + ParsedCommand { + description: parse_command_description(&rest[..end]), + body: rest[body_start..].to_string(), + } +} + +fn frontmatter_end(rest: &str) -> Option<(usize, usize)> { + [ + "\r\n---\r\n", + "\r\n---\n", + "\n---\r\n", + "\n---\n", + "\r\n---", + "\n---", + ] + .into_iter() + .filter_map(|delimiter| rest.find(delimiter).map(|end| (end, end + delimiter.len()))) + .min_by_key(|(end, _body_start)| *end) +} + +fn parse_command_description(raw_frontmatter: &str) -> Option { + let parsed: YamlValue = serde_yaml::from_str(raw_frontmatter).ok()?; + let mapping = parsed.as_mapping()?; + mapping.iter().find_map(|(key, value)| { + if key.as_str()?.trim() == "description" { + yaml_scalar(value) + } else { + None + } + }) +} + +fn yaml_scalar(value: &YamlValue) -> Option { + match value { + YamlValue::String(value) => Some(value.trim().to_string()), + YamlValue::Bool(value) => Some(value.to_string()), + YamlValue::Number(value) => Some(value.to_string()), + YamlValue::Null | YamlValue::Sequence(_) | YamlValue::Mapping(_) | YamlValue::Tagged(_) => { + None + } + } +} + +fn command_skill_name(source_commands: &Path, source_file: &Path) -> String { + slugify_name(&format!( + "{COMMAND_SKILL_PREFIX}-{}", + command_source_name(source_commands, source_file) + )) +} + +fn command_skill_name_if_supported( + source_commands: &Path, + source_file: &Path, + document: &ParsedCommand, +) -> Option { + if source_file.file_stem().and_then(|stem| stem.to_str()) == Some("README") { + return None; + } + document + .description + .as_deref() + .filter(|description| !description.trim().is_empty())?; + let name = command_skill_name(source_commands, source_file); + if name.chars().count() > MAX_SKILL_NAME_LEN + || has_unsupported_command_template_features(&document.body) + { + return None; + } + Some(name) +} + +fn command_source_name(source_commands: &Path, source_file: &Path) -> String { + source_file + .strip_prefix(source_commands) + .unwrap_or(source_file) + .with_extension("") + .components() + .filter_map(|component| component.as_os_str().to_str()) + .collect::>() + .join("-") +} + +fn render_command_skill(body: &str, name: &str, description: &str, source_name: &str) -> String { + let body = rewrite_external_agent_terms(body.trim()); + let template_body = if body.is_empty() { + "No command template body was found.".to_string() + } else { + body + }; + format!( + "---\nname: {}\ndescription: {}\n---\n\n# {name}\n\nUse this skill when the user asks to run the migrated source command `{source_name}`.\n\n## Command Template\n\n{template_body}\n", + yaml_string(name), + yaml_string(&rewrite_external_agent_terms(description)), + ) +} + +fn has_unsupported_command_template_features(template: &str) -> bool { + template.contains("$ARGUMENTS") + || contains_numbered_argument_placeholder(template) + || (template.contains("{{") && template.contains("}}")) + || template.contains("!`") + || template.contains("! `") + || template + .split_whitespace() + .any(|token| token.strip_prefix('@').is_some_and(|rest| !rest.is_empty())) +} + +fn contains_numbered_argument_placeholder(template: &str) -> bool { + template + .as_bytes() + .windows(2) + .any(|window| window[0] == b'$' && window[1].is_ascii_digit()) +} + +fn yaml_string(value: &str) -> String { + format!("\"{}\"", value.replace('\\', "\\\\").replace('"', "\\\"")) +} + +fn slugify_name(value: &str) -> String { + let mut slug = String::new(); + let mut last_was_dash = false; + for ch in value.chars() { + if ch.is_ascii_alphanumeric() { + slug.push(ch.to_ascii_lowercase()); + last_was_dash = false; + } else if !last_was_dash { + slug.push('-'); + last_was_dash = true; + } + } + + let slug = slug.trim_matches('-').to_string(); + if slug.is_empty() { + "migrated".to_string() + } else { + slug + } +} + +fn rewrite_external_agent_terms(content: &str) -> String { + let mut rewritten = replace_case_insensitive_with_boundaries( + content, + &format!("{SOURCE_EXTERNAL_AGENT_NAME}.md"), + "AGENTS.md", + ); + for from in [ + format!("{SOURCE_EXTERNAL_AGENT_NAME} code"), + format!("{SOURCE_EXTERNAL_AGENT_NAME}-code"), + format!("{SOURCE_EXTERNAL_AGENT_NAME}_code"), + format!("{SOURCE_EXTERNAL_AGENT_NAME}code"), + SOURCE_EXTERNAL_AGENT_NAME.to_string(), + ] { + rewritten = replace_case_insensitive_with_boundaries(&rewritten, &from, "Codex"); + } + rewritten +} + +fn replace_case_insensitive_with_boundaries( + input: &str, + needle: &str, + replacement: &str, +) -> String { + let needle_lower = needle.to_ascii_lowercase(); + if needle_lower.is_empty() { + return input.to_string(); + } + + let haystack_lower = input.to_ascii_lowercase(); + let bytes = input.as_bytes(); + let mut output = String::with_capacity(input.len()); + let mut last_emitted = 0usize; + let mut search_start = 0usize; + + while let Some(relative_pos) = haystack_lower[search_start..].find(&needle_lower) { + let start = search_start + relative_pos; + let end = start + needle_lower.len(); + let boundary_before = start == 0 || !is_word_byte(bytes[start - 1]); + let boundary_after = end == bytes.len() || !is_word_byte(bytes[end]); + + if boundary_before && boundary_after { + output.push_str(&input[last_emitted..start]); + output.push_str(replacement); + last_emitted = end; + } + + search_start = start + 1; + } + + if last_emitted == 0 { + return input.to_string(); + } + + output.push_str(&input[last_emitted..]); + output +} + +fn is_word_byte(byte: u8) -> bool { + byte.is_ascii_alphanumeric() || byte == b'_' +} diff --git a/codex-rs/core-plugins/src/lib.rs b/codex-rs/core-plugins/src/lib.rs index df30305c38c0..71f47981a5de 100644 --- a/codex-rs/core-plugins/src/lib.rs +++ b/codex-rs/core-plugins/src/lib.rs @@ -1,4 +1,5 @@ mod app_mcp_routing; +pub mod command_migration; mod discoverable; pub mod installed_marketplaces; pub mod loader; diff --git a/codex-rs/core-plugins/src/loader.rs b/codex-rs/core-plugins/src/loader.rs index 2b5c4782e81d..bd426f2b9b98 100644 --- a/codex-rs/core-plugins/src/loader.rs +++ b/codex-rs/core-plugins/src/loader.rs @@ -1,5 +1,6 @@ use crate::app_mcp_routing::apply_app_mcp_routing_policy; use crate::app_mcp_routing::apps_route_available; +use crate::command_migration::migrated_command_skills_root; use crate::is_openai_curated_marketplace_name; use crate::manifest::PluginManifest; use crate::manifest::PluginManifestHooks; @@ -967,6 +968,10 @@ fn plugin_skill_roots( } else { manifest_paths.skills.clone() }; + let migrated_command_skills = migrated_command_skills_root(plugin_root); + if migrated_command_skills.is_dir() { + paths.push(migrated_command_skills); + } paths.sort_unstable(); paths.dedup(); paths diff --git a/codex-rs/core-plugins/src/manager_tests.rs b/codex-rs/core-plugins/src/manager_tests.rs index 5e743416f2aa..7d11bcac5693 100644 --- a/codex-rs/core-plugins/src/manager_tests.rs +++ b/codex-rs/core-plugins/src/manager_tests.rs @@ -1885,6 +1885,56 @@ async fn load_plugins_uses_manifest_configured_component_paths() { } } +#[tokio::test] +async fn load_plugins_includes_migrated_command_skills_with_explicit_skill_paths() { + let codex_home = TempDir::new().unwrap(); + let source_root = codex_home.path().join("source/sample"); + + write_file( + &source_root.join(".codex-plugin/plugin.json"), + r#"{ + "name": "sample", + "skills": "./custom-skills/" +}"#, + ); + fs::create_dir_all(source_root.join("custom-skills")).unwrap(); + write_file( + &source_root.join("commands/pr/review.md"), + "---\ndescription: Review a pull request\n---\nInspect the proposed changes.\n", + ); + let result = PluginStore::new(codex_home.path().to_path_buf()) + .install( + source_root.abs(), + PluginId::parse("sample@test").expect("plugin id should parse"), + ) + .unwrap(); + let migrated_skill = result + .installed_path + .join(".codex-plugin/migrated-command-skills/source-command-pr-review/SKILL.md"); + assert_eq!( + fs::read_to_string(migrated_skill).unwrap(), + "---\nname: \"source-command-pr-review\"\ndescription: \"Review a pull request\"\n---\n\n# source-command-pr-review\n\nUse this skill when the user asks to run the migrated source command `pr-review`.\n\n## Command Template\n\nInspect the proposed changes.\n" + ); + + let outcome = load_plugins_from_config( + &plugin_config_toml(/*enabled*/ true, /*plugins_feature_enabled*/ true), + codex_home.path(), + Some(AuthMode::Chatgpt), + ) + .await; + + assert_eq!( + outcome.plugins()[0].skill_roots, + vec![ + result + .installed_path + .join(".codex-plugin/migrated-command-skills"), + result.installed_path.join("custom-skills"), + ] + ); + assert!(outcome.plugins()[0].has_enabled_skills); +} + #[tokio::test] async fn load_plugin_skills_dedupes_overlapping_manifest_roots() { let codex_home = TempDir::new().unwrap(); diff --git a/codex-rs/core-plugins/src/store.rs b/codex-rs/core-plugins/src/store.rs index bb4929dedbfe..271d54fd2b45 100644 --- a/codex-rs/core-plugins/src/store.rs +++ b/codex-rs/core-plugins/src/store.rs @@ -1,3 +1,4 @@ +use crate::command_migration::migrate_plugin_commands; use crate::manifest::PluginManifest; use crate::manifest::load_plugin_manifest; use crate::manifest::parse_plugin_manifest; @@ -552,6 +553,9 @@ fn replace_plugin_root_atomically( fs::write(&manifest_path, contents) .map_err(|err| PluginStoreError::io("failed to write fallback plugin manifest", err))?; } + migrate_plugin_commands(&staged_version_root).map_err(|err| { + PluginStoreError::io("failed to migrate plugin commands into skills", err) + })?; let target_version_root = target_root.join(plugin_version); if target_root.exists() && !target_version_root.exists() { diff --git a/codex-rs/external-agent-migration/src/lib.rs b/codex-rs/external-agent-migration/src/lib.rs index 7e5f626d8d01..c9ab217f76d2 100644 --- a/codex-rs/external-agent-migration/src/lib.rs +++ b/codex-rs/external-agent-migration/src/lib.rs @@ -16,8 +16,6 @@ const SOURCE_EXTERNAL_AGENT_NAME: &str = "claude"; const EXTERNAL_AGENT_MCP_CONFIG_FILE: &str = ".mcp.json"; const EXTERNAL_AGENT_HOOKS_SUBDIR: &str = "hooks"; const EXTERNAL_AGENT_MIGRATED_HOOKS_SUBDIR: &str = "hooks"; -const COMMAND_SKILL_PREFIX: &str = "source-command"; -const MAX_SKILL_NAME_LEN: usize = 64; #[derive(Debug)] struct ParsedDocument { @@ -179,49 +177,6 @@ pub fn import_subagents(source_agents: &Path, target_agents: &Path) -> io::Resul Ok(imported) } -pub fn count_missing_commands(source_commands: &Path, target_skills: &Path) -> io::Result { - Ok(missing_command_names(source_commands, target_skills)?.len()) -} - -pub fn missing_command_names( - source_commands: &Path, - target_skills: &Path, -) -> io::Result> { - Ok(unique_supported_command_sources(source_commands)? - .into_iter() - .filter(|(_source_file, name)| !target_skills.join(name).exists()) - .map(|(_source_file, name)| name) - .collect()) -} - -pub fn import_commands(source_commands: &Path, target_skills: &Path) -> io::Result> { - if !source_commands.is_dir() { - return Ok(Vec::new()); - } - - fs::create_dir_all(target_skills)?; - let mut imported = Vec::new(); - for (source_file, name) in unique_supported_command_sources(source_commands)? { - let document = parse_document(&source_file)?; - let target_dir = target_skills.join(&name); - if target_dir.exists() { - continue; - } - fs::create_dir_all(&target_dir)?; - let source_name = command_source_name(source_commands, &source_file); - let Some(description) = command_skill_description(&document, &source_name) else { - continue; - }; - fs::write( - target_dir.join("SKILL.md"), - render_command_skill(&document.body, &name, &description, &source_name), - )?; - imported.push(name); - } - - Ok(imported) -} - fn read_external_mcp_servers( source_root: &Path, external_agent_home: Option<&Path>, @@ -911,54 +866,6 @@ fn subagent_target_file(source_file: &Path, target_agents: &Path) -> Option io::Result> { - let mut files = Vec::new(); - collect_markdown_files(source_commands, &mut files)?; - files.sort(); - Ok(files) -} - -fn unique_supported_command_sources(source_commands: &Path) -> io::Result> { - let mut by_name = BTreeMap::>::new(); - for source_file in command_source_files(source_commands)? { - let document = parse_document(&source_file)?; - let Some(name) = command_skill_name_if_supported(source_commands, &source_file, &document) - else { - continue; - }; - by_name.entry(name).or_default().push(source_file); - } - - Ok(by_name - .into_iter() - .filter_map(|(name, source_files)| { - let [source_file] = source_files.as_slice() else { - return None; - }; - Some((source_file.clone(), name)) - }) - .collect()) -} - -fn collect_markdown_files(dir: &Path, files: &mut Vec) -> io::Result<()> { - if !dir.is_dir() { - return Ok(()); - } - - for entry in fs::read_dir(dir)? { - let entry = entry?; - let path = entry.path(); - let file_type = entry.file_type()?; - if file_type.is_dir() { - collect_markdown_files(&path, files)?; - } else if file_type.is_file() && path.extension().and_then(|ext| ext.to_str()) == Some("md") - { - files.push(path); - } - } - Ok(()) -} - fn parse_document(source_file: &Path) -> io::Result { let content = fs::read_to_string(source_file)?; Ok(parse_document_content(&content)) @@ -1113,85 +1020,6 @@ fn render_agent_body(body: &str) -> String { } } -fn command_skill_name(source_commands: &Path, source_file: &Path) -> String { - slugify_name(&format!( - "{COMMAND_SKILL_PREFIX}-{}", - command_source_name(source_commands, source_file) - )) -} - -fn command_skill_name_if_supported( - source_commands: &Path, - source_file: &Path, - document: &ParsedDocument, -) -> Option { - if source_file.file_stem().and_then(|stem| stem.to_str()) == Some("README") { - return None; - } - let source_name = command_source_name(source_commands, source_file); - command_skill_description(document, &source_name)?; - let name = command_skill_name(source_commands, source_file); - if name.chars().count() > MAX_SKILL_NAME_LEN { - return None; - } - if has_unsupported_command_template_features(&document.body) { - return None; - } - Some(name) -} - -fn command_skill_description(document: &ParsedDocument, _source_name: &str) -> Option { - document - .frontmatter - .get("description") - .and_then(FrontmatterValue::as_scalar) - .filter(|value| !value.trim().is_empty()) - .map(ToOwned::to_owned) -} - -fn command_source_name(source_commands: &Path, source_file: &Path) -> String { - source_file - .strip_prefix(source_commands) - .unwrap_or(source_file) - .with_extension("") - .components() - .filter_map(|component| component.as_os_str().to_str()) - .collect::>() - .join("-") -} - -fn render_command_skill(body: &str, name: &str, description: &str, source_name: &str) -> String { - let body = rewrite_external_agent_terms(body.trim()); - let template_body = if body.is_empty() { - "No command template body was found.".to_string() - } else { - body - }; - format!( - "---\nname: {}\ndescription: {}\n---\n\n# {name}\n\nUse this skill when the user asks to run the migrated source command `{source_name}`.\n\n## Command Template\n\n{template_body}\n", - yaml_string(name), - yaml_string(&rewrite_external_agent_terms(description)), - ) -} - -fn has_unsupported_command_template_features(template: &str) -> bool { - template.contains("$ARGUMENTS") - || contains_numbered_argument_placeholder(template) - || (template.contains("{{") && template.contains("}}")) - || template.contains("!`") - || template.contains("! `") - || template - .split_whitespace() - .any(|token| token.strip_prefix('@').is_some_and(|rest| !rest.is_empty())) -} - -fn contains_numbered_argument_placeholder(template: &str) -> bool { - let bytes = template.as_bytes(); - bytes - .windows(2) - .any(|window| window[0] == b'$' && window[1].is_ascii_digit()) -} - fn frontmatter_string( frontmatter: &BTreeMap, key: &str, @@ -1246,31 +1074,6 @@ fn json_u64(value: &JsonValue) -> Option { value.as_u64().or_else(|| value.as_str()?.parse().ok()) } -fn yaml_string(value: &str) -> String { - format!("\"{}\"", value.replace('\\', "\\\\").replace('"', "\\\"")) -} - -fn slugify_name(value: &str) -> String { - let mut slug = String::new(); - let mut last_was_dash = false; - for ch in value.chars() { - if ch.is_ascii_alphanumeric() { - slug.push(ch.to_ascii_lowercase()); - last_was_dash = false; - } else if !last_was_dash { - slug.push('-'); - last_was_dash = true; - } - } - - let slug = slug.trim_matches('-').to_string(); - if slug.is_empty() { - "migrated".to_string() - } else { - slug - } -} - impl FrontmatterValue { fn as_scalar(&self) -> Option<&str> { match self { @@ -1384,8 +1187,6 @@ mod tests { use super::*; use pretty_assertions::assert_eq; - const MAX_SKILL_DESCRIPTION_LEN: usize = 1024; - fn source_path(relative_path: &str) -> PathBuf { Path::new("/repo") .join(external_agent_config_dir()) @@ -1703,94 +1504,6 @@ command = "enabled-server" ); } - #[test] - fn command_skill_names_include_nested_paths() { - let root = source_path("commands"); - let file = source_path("commands/pr/review.md"); - - assert_eq!(command_skill_name(&root, &file), "source-command-pr-review"); - } - - #[test] - fn command_skill_names_must_fit_codex_skill_loader_limit() { - let root = source_path("commands"); - let file = source_path("commands/this/is/a/deeply/nested/command/with/a/very/long/name.md"); - let document = parse_document_content("---\ndescription: Review PR\n---\nReview\n"); - - assert!(command_skill_name_if_supported(&root, &file, &document).is_none()); - } - - #[test] - fn commands_with_overlong_descriptions_are_preserved() { - let root = source_path("commands"); - let file = source_path("commands/review.md"); - let description = "x".repeat(MAX_SKILL_DESCRIPTION_LEN + 1); - let document = - parse_document_content(&format!("---\ndescription: {description}\n---\nReview\n")); - - assert_eq!( - command_skill_name_if_supported(&root, &file, &document), - Some("source-command-review".to_string()) - ); - - let rendered = render_command_skill( - &document.body, - "source-command-review", - &description, - "review", - ); - let rendered_document = parse_document_content(&rendered); - assert_eq!( - rendered_document - .frontmatter - .get("description") - .and_then(FrontmatterValue::as_scalar), - Some(description.as_str()) - ); - } - - #[test] - fn commands_with_provider_runtime_expansion_are_skipped() { - let root = source_path("commands"); - let file = source_path("commands/deploy.md"); - let document = parse_document_content( - "---\ndescription: Deploy\n---\nDeploy $ARGUMENTS from @release.yaml\n", - ); - - assert!(command_skill_name_if_supported(&root, &file, &document).is_none()); - } - - #[test] - fn commands_without_description_are_skipped() { - let root = source_path("commands"); - let file = source_path("commands/README.md"); - let document = parse_document_content("# Notes\n\nThis documents commands.\n"); - - assert!(command_skill_name_if_supported(&root, &file, &document).is_none()); - } - - #[test] - fn command_slug_collisions_are_skipped() { - let root = tempfile::TempDir::new().expect("tempdir"); - let commands = root.path().join("commands"); - fs::create_dir_all(&commands).expect("create commands"); - fs::write( - commands.join("foo-bar.md"), - "---\ndescription: First\n---\nRun the first command.\n", - ) - .expect("write first command"); - fs::write( - commands.join("foo_bar.md"), - "---\ndescription: Second\n---\nRun the second command.\n", - ) - .expect("write second command"); - - assert_eq!( - unique_supported_command_sources(&commands).unwrap(), - Vec::<(PathBuf, String)>::new() - ); - } - #[test] fn subagent_accepts_yaml_block_lists_by_ignoring_unsupported_fields() { let document = parse_document_content(