Merge branch 'main' into tag

This commit is contained in:
Ahmed Ibrahim
2025-12-18 21:31:14 -08:00
committed by GitHub
8 changed files with 84 additions and 26 deletions

View File

@@ -1081,6 +1081,7 @@ pub enum SkillScope {
User,
Repo,
System,
Admin,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
@@ -1131,6 +1132,7 @@ impl From<CoreSkillScope> for SkillScope {
CoreSkillScope::User => Self::User,
CoreSkillScope::Repo => Self::Repo,
CoreSkillScope::System => Self::System,
CoreSkillScope::Admin => Self::Admin,
}
}
}

View File

@@ -395,7 +395,7 @@ pub const FEATURES: &[FeatureSpec] = &[
id: Feature::Skills,
key: "skills",
stage: Stage::Experimental,
default_enabled: false,
default_enabled: true,
},
FeatureSpec {
id: Feature::Tui2,

View File

@@ -2,7 +2,7 @@
name: plan
description: Generate a plan for how an agent should accomplish a complex coding task. Use when a user asks for a plan, and optionally when they want to save, find, read, update, or delete plan files in $CODEX_HOME/plans (default ~/.codex/plans).
metadata:
short-description: Create and manage plan markdown files under $CODEX_HOME/plans.
short-description: Generate a plan for a complex task
---
# Plan

View File

@@ -1,6 +1,8 @@
---
name: skill-creator
description: Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends Codex's capabilities with specialized knowledge, workflows, or tool integrations.
metadata:
short-description: Create or update a skill
---
# Skill Creator

View File

@@ -33,6 +33,7 @@ struct SkillFrontmatterMetadata {
const SKILLS_FILENAME: &str = "SKILL.md";
const SKILLS_DIR_NAME: &str = "skills";
const REPO_ROOT_CONFIG_DIR_NAME: &str = ".codex";
const ADMIN_SKILLS_ROOT: &str = "/etc/codex/skills";
const MAX_NAME_LEN: usize = 64;
const MAX_DESCRIPTION_LEN: usize = 1024;
const MAX_SHORT_DESCRIPTION_LEN: usize = MAX_DESCRIPTION_LEN;
@@ -108,6 +109,13 @@ pub(crate) fn system_skills_root(codex_home: &Path) -> SkillRoot {
}
}
pub(crate) fn admin_skills_root() -> SkillRoot {
SkillRoot {
path: PathBuf::from(ADMIN_SKILLS_ROOT),
scope: SkillScope::Admin,
}
}
pub(crate) fn repo_skills_root(cwd: &Path) -> Option<SkillRoot> {
let base = if cwd.is_dir() { cwd } else { cwd.parent()? };
let base = normalize_path(base).unwrap_or_else(|_| base.to_path_buf());
@@ -140,21 +148,28 @@ pub(crate) fn repo_skills_root(cwd: &Path) -> Option<SkillRoot> {
})
}
fn skill_roots(config: &Config) -> Vec<SkillRoot> {
pub(crate) fn skill_roots_for_cwd(codex_home: &Path, cwd: &Path) -> Vec<SkillRoot> {
let mut roots = Vec::new();
if let Some(repo_root) = repo_skills_root(&config.cwd) {
if let Some(repo_root) = repo_skills_root(cwd) {
roots.push(repo_root);
}
// Load order matters: we dedupe by name, keeping the first occurrence.
// This makes repo/user skills win over system skills.
roots.push(user_skills_root(&config.codex_home));
roots.push(system_skills_root(&config.codex_home));
// Priority order: repo, user, system, then admin.
roots.push(user_skills_root(codex_home));
roots.push(system_skills_root(codex_home));
if cfg!(unix) {
roots.push(admin_skills_root());
}
roots
}
fn skill_roots(config: &Config) -> Vec<SkillRoot> {
skill_roots_for_cwd(&config.codex_home, &config.cwd)
}
fn discover_skills_under_root(root: &Path, scope: SkillScope, outcome: &mut SkillLoadOutcome) {
let Ok(root) = normalize_path(root) else {
return;
@@ -622,7 +637,7 @@ mod tests {
}
#[tokio::test]
async fn loads_system_skills_with_lowest_priority() {
async fn loads_system_skills_when_present() {
let codex_home = tempfile::tempdir().expect("tempdir");
write_system_skill(&codex_home, "system", "dupe-skill", "from system");
@@ -764,6 +779,51 @@ mod tests {
assert_eq!(outcome.skills[0].scope, SkillScope::System);
}
#[tokio::test]
async fn skill_roots_include_admin_with_lowest_priority_on_unix() {
let codex_home = tempfile::tempdir().expect("tempdir");
let cfg = make_config(&codex_home).await;
let scopes: Vec<SkillScope> = skill_roots(&cfg)
.into_iter()
.map(|root| root.scope)
.collect();
let mut expected = vec![SkillScope::User, SkillScope::System];
if cfg!(unix) {
expected.push(SkillScope::Admin);
}
assert_eq!(scopes, expected);
}
#[tokio::test]
async fn deduplicates_by_name_preferring_system_over_admin() {
let system_dir = tempfile::tempdir().expect("tempdir");
let admin_dir = tempfile::tempdir().expect("tempdir");
write_skill_at(system_dir.path(), "system", "dupe-skill", "from system");
write_skill_at(admin_dir.path(), "admin", "dupe-skill", "from admin");
let outcome = load_skills_from_roots([
SkillRoot {
path: system_dir.path().to_path_buf(),
scope: SkillScope::System,
},
SkillRoot {
path: admin_dir.path().to_path_buf(),
scope: SkillScope::Admin,
},
]);
assert!(
outcome.errors.is_empty(),
"unexpected errors: {:?}",
outcome.errors
);
assert_eq!(outcome.skills.len(), 1);
assert_eq!(outcome.skills[0].name, "dupe-skill");
assert_eq!(outcome.skills[0].scope, SkillScope::System);
}
#[tokio::test]
async fn deduplicates_by_name_preferring_user_over_system() {
let codex_home = tempfile::tempdir().expect("tempdir");

View File

@@ -5,9 +5,7 @@ use std::sync::RwLock;
use crate::skills::SkillLoadOutcome;
use crate::skills::loader::load_skills_from_roots;
use crate::skills::loader::repo_skills_root;
use crate::skills::loader::system_skills_root;
use crate::skills::loader::user_skills_root;
use crate::skills::loader::skill_roots_for_cwd;
use crate::skills::system::install_system_skills;
pub struct SkillsManager {
codex_home: PathBuf,
@@ -39,12 +37,7 @@ impl SkillsManager {
return outcome;
}
let mut roots = Vec::new();
if let Some(repo_root) = repo_skills_root(cwd) {
roots.push(repo_root);
}
roots.push(user_skills_root(&self.codex_home));
roots.push(system_skills_root(&self.codex_home));
let roots = skill_roots_for_cwd(&self.codex_home, cwd);
let outcome = load_skills_from_roots(roots);
match self.cache_by_cwd.write() {
Ok(mut cache) => {

View File

@@ -1721,6 +1721,7 @@ pub enum SkillScope {
User,
Repo,
System,
Admin,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]

View File

@@ -1284,9 +1284,9 @@ async fn unified_exec_end_after_task_complete_is_suppressed() {
);
}
#[test]
fn unified_exec_waiting_multiple_empty_snapshots() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None);
#[tokio::test]
async fn unified_exec_waiting_multiple_empty_snapshots() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None).await;
begin_unified_exec_startup(&mut chat, "call-wait-1", "proc-1", "just fix");
terminal_interaction(&mut chat, "call-wait-1a", "proc-1", "");
@@ -1311,9 +1311,9 @@ fn unified_exec_waiting_multiple_empty_snapshots() {
assert_snapshot!("unified_exec_waiting_multiple_empty_after", combined);
}
#[test]
fn unified_exec_empty_then_non_empty_snapshot() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None);
#[tokio::test]
async fn unified_exec_empty_then_non_empty_snapshot() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None).await;
begin_unified_exec_startup(&mut chat, "call-wait-2", "proc-2", "just fix");
terminal_interaction(&mut chat, "call-wait-2a", "proc-2", "");
@@ -1327,9 +1327,9 @@ fn unified_exec_empty_then_non_empty_snapshot() {
assert_snapshot!("unified_exec_empty_then_non_empty_after", combined);
}
#[test]
fn unified_exec_non_empty_then_empty_snapshots() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None);
#[tokio::test]
async fn unified_exec_non_empty_then_empty_snapshots() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None).await;
begin_unified_exec_startup(&mut chat, "call-wait-3", "proc-3", "just fix");
terminal_interaction(&mut chat, "call-wait-3a", "proc-3", "pwd\n");