mirror of
https://github.com/openai/codex.git
synced 2026-09-07 15:40:00 +00:00
core-skills: detect skill doc reads with parsed commands
This commit is contained in:
1
codex-rs/Cargo.lock
generated
1
codex-rs/Cargo.lock
generated
@@ -2584,6 +2584,7 @@ dependencies = [
|
||||
"codex-model-provider",
|
||||
"codex-otel",
|
||||
"codex-protocol",
|
||||
"codex-shell-command",
|
||||
"codex-skills",
|
||||
"codex-utils-absolute-path",
|
||||
"codex-utils-output-truncation",
|
||||
|
||||
@@ -22,6 +22,7 @@ codex-login = { workspace = true }
|
||||
codex-model-provider = { workspace = true }
|
||||
codex-otel = { workspace = true }
|
||||
codex-protocol = { workspace = true }
|
||||
codex-shell-command = { workspace = true }
|
||||
codex-skills = { workspace = true }
|
||||
codex-utils-absolute-path = { workspace = true }
|
||||
codex-utils-output-truncation = { workspace = true }
|
||||
|
||||
@@ -3,6 +3,8 @@ use std::path::Path;
|
||||
|
||||
use crate::SkillLoadOutcome;
|
||||
use crate::SkillMetadata;
|
||||
use codex_protocol::parse_command::ParsedCommand;
|
||||
use codex_shell_command::parse_command::parse_command_impl;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
|
||||
pub(crate) fn build_implicit_skill_path_indexes(
|
||||
@@ -101,15 +103,11 @@ fn detect_skill_doc_read(
|
||||
tokens: &[String],
|
||||
workdir: &AbsolutePathBuf,
|
||||
) -> Option<SkillMetadata> {
|
||||
if !command_reads_file(tokens) {
|
||||
return None;
|
||||
}
|
||||
|
||||
for token in tokens.iter().skip(1) {
|
||||
if token.starts_with('-') {
|
||||
// Keep partially parsed reads from pipelines such as `cat SKILL.md | head`.
|
||||
for parsed in parse_command_impl(&tokens_with_command_basename(tokens)) {
|
||||
let ParsedCommand::Read { path, .. } = parsed else {
|
||||
continue;
|
||||
}
|
||||
let path = Path::new(token);
|
||||
};
|
||||
let candidate_path = canonicalize_if_exists(&workdir.join(path));
|
||||
if let Some(candidate) = outcome.implicit_skills_by_doc_path.get(&candidate_path) {
|
||||
return Some(candidate.clone());
|
||||
@@ -119,13 +117,14 @@ fn detect_skill_doc_read(
|
||||
None
|
||||
}
|
||||
|
||||
fn command_reads_file(tokens: &[String]) -> bool {
|
||||
const READERS: [&str; 8] = ["cat", "sed", "head", "tail", "less", "more", "bat", "awk"];
|
||||
let Some(program) = tokens.first() else {
|
||||
return false;
|
||||
fn tokens_with_command_basename(tokens: &[String]) -> Vec<String> {
|
||||
let Some((program, rest)) = tokens.split_first() else {
|
||||
return Vec::new();
|
||||
};
|
||||
let program = command_basename(program).to_ascii_lowercase();
|
||||
READERS.contains(&program.as_str())
|
||||
let mut normalized = Vec::with_capacity(tokens.len());
|
||||
normalized.push(command_basename(program).to_ascii_lowercase());
|
||||
normalized.extend_from_slice(rest);
|
||||
normalized
|
||||
}
|
||||
|
||||
fn command_basename(command: &str) -> String {
|
||||
|
||||
@@ -75,6 +75,29 @@ fn skill_doc_read_detection_matches_absolute_path() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skill_doc_read_detection_uses_parsed_read_commands() {
|
||||
let skill_doc_path = test_path_buf("/tmp/skill-test/SKILL.md").abs();
|
||||
let normalized_skill_doc_path = canonicalize_if_exists(&skill_doc_path);
|
||||
let skill = test_skill_metadata(skill_doc_path);
|
||||
let outcome = SkillLoadOutcome {
|
||||
implicit_skills_by_scripts_dir: Arc::new(HashMap::new()),
|
||||
implicit_skills_by_doc_path: Arc::new(HashMap::from([(normalized_skill_doc_path, skill)])),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let tokens = vec![
|
||||
"nl".to_string(),
|
||||
test_path_display("/tmp/skill-test/SKILL.md"),
|
||||
];
|
||||
let found = detect_skill_doc_read(&outcome, &tokens, &test_path_buf("/tmp").abs());
|
||||
|
||||
assert_eq!(
|
||||
found.map(|value| value.name),
|
||||
Some("test-skill".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skill_script_run_detection_matches_relative_path_from_skill_root() {
|
||||
let skill_doc_path = test_path_buf("/tmp/skill-test/SKILL.md").abs();
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
use anyhow::Result;
|
||||
use codex_core::ThreadManager;
|
||||
use codex_core::skills::SkillsLoadInput;
|
||||
use codex_core::skills::detect_implicit_skill_invocation_for_command;
|
||||
use codex_core::thread_store_from_config;
|
||||
use codex_exec_server::CreateDirectoryOptions;
|
||||
use codex_exec_server::EnvironmentManager;
|
||||
@@ -149,6 +151,47 @@ async fn user_turn_includes_skill_instructions() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn parsed_skill_doc_reads_detect_loaded_repo_skill() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
let mut builder = test_codex().with_workspace_setup(|cwd, fs| async move {
|
||||
write_repo_skill(cwd, fs, "demo", "demo skill", "# Body").await
|
||||
});
|
||||
let test = builder.build_remote_aware(&server).await?;
|
||||
let skills_input = SkillsLoadInput::new(
|
||||
test.config.cwd.clone(),
|
||||
Vec::new(),
|
||||
test.config.config_layer_stack.clone(),
|
||||
test.config.bundled_skills_enabled(),
|
||||
);
|
||||
let outcome = test
|
||||
.thread_manager
|
||||
.skills_manager()
|
||||
.skills_for_config(&skills_input, Some(test.fs()))
|
||||
.await;
|
||||
let skill_path = test.config.cwd.join(".agents/skills/demo/SKILL.md");
|
||||
let skill_path_str = skill_path.to_string_lossy();
|
||||
|
||||
let parsed_read = detect_implicit_skill_invocation_for_command(
|
||||
&outcome,
|
||||
&format!("nl {skill_path_str}"),
|
||||
&test.config.cwd,
|
||||
)
|
||||
.map(|skill| skill.name);
|
||||
let non_read = detect_implicit_skill_invocation_for_command(
|
||||
&outcome,
|
||||
&format!("echo {skill_path_str}"),
|
||||
&test.config.cwd,
|
||||
)
|
||||
.map(|skill| skill.name);
|
||||
|
||||
assert_eq!((parsed_read, non_read), (Some("demo".to_string()), None));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn list_skills_includes_repo_and_home_skills_remote_aware() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
Reference in New Issue
Block a user