refactor(discovery): extracted skill loading logic into reusable helper to eliminate duplication

- Extracted skill loading logic into reusable `loadSkillsFromDir` helper function to eliminate code duplication across builtin, claude, and codex discovery providers.
- Refactored builtin provider to use new `loadSkillsFromDir` helper instead of inline `loadSkillsRecursive` function.
- Refactored claude provider to use new `loadSkillsFromDir` helper, reducing function from 78 lines to 25 lines.
- Refactored codex provider to use new `loadSkillsFromDir` helper, consolidating duplicate skill transformation and frontmatter parsing logic.
- Updated extension module discovery to skip `node_modules` directories in addition to hidden directories.
- Added guideline to Task tool documentation about isolating file scopes for concurrent agents to prevent conflicts.
This commit is contained in:
can1357
2026-01-11 03:07:28 +01:00
parent 2090f4345a
commit 2844d6ece8
6 changed files with 93 additions and 161 deletions
+4 -1
View File
@@ -1,7 +1,6 @@
# Changelog
## [Unreleased]
### Added
- Added automatic discovery and listing of AGENTS.md files in the system prompt, providing agents with an authoritative list of project-specific instruction files without runtime searching
@@ -9,10 +8,14 @@
### Changed
- Refactored skill discovery to use unified `loadSkillsFromDir` helper across all providers, reducing code duplication
- Updated skill discovery to scan only `skills/*/SKILL.md` entries instead of recursive walks in Codex provider
- Added guidance to Task tool documentation to isolate file scopes when assigning tasks to prevent agent conflicts
- Updated Task tool documentation to emphasize that subagents have no access to conversation history and require all relevant context to be explicitly passed
- Revised task agent prompt to clarify that subagents have full tool access and can make file edits, run commands, and create files
- OpenAI Codex: updated to use bundled system prompt from upstream
- Changed `complete` tool to make `data` parameter optional when aborting, while still requiring it for successful completions
- Skills discovery now scans only `skills/*/SKILL.md` entries instead of recursive walks
### Removed
+9 -54
View File
@@ -5,7 +5,7 @@
* .pi is an alias for backwards compatibility.
*/
import { basename, dirname, isAbsolute, join, resolve } from "path";
import { dirname, isAbsolute, join, resolve } from "path";
import { type ContextFile, contextFileCapability } from "../capability/context-file";
import { type Extension, type ExtensionManifest, extensionCapability } from "../capability/extension";
import { type ExtensionModule, extensionModuleCapability } from "../capability/extension-module";
@@ -16,7 +16,7 @@ import { type MCPServer, mcpCapability } from "../capability/mcp";
import { type Prompt, promptCapability } from "../capability/prompt";
import { type Rule, ruleCapability } from "../capability/rule";
import { type Settings, settingsCapability } from "../capability/settings";
import { type Skill, type SkillFrontmatter, skillCapability } from "../capability/skill";
import { type Skill, skillCapability } from "../capability/skill";
import { type SlashCommand, slashCommandCapability } from "../capability/slash-command";
import { type SystemPrompt, systemPromptCapability } from "../capability/system-prompt";
import { type CustomTool, toolCapability } from "../capability/tool";
@@ -27,6 +27,7 @@ import {
expandEnvVarsDeep,
getExtensionNameFromPath,
loadFilesFromDir,
loadSkillsFromDir,
parseFrontmatter,
parseJSON,
SOURCE_PATHS,
@@ -190,64 +191,18 @@ registerProvider<SystemPrompt>(systemPromptCapability.id, {
});
// Skills
function loadSkillFromFile(ctx: LoadContext, path: string, level: "user" | "project"): Skill | null {
const content = ctx.fs.readFile(path);
if (!content) return null;
const { frontmatter, body } = parseFrontmatter(content);
const skillDir = dirname(path);
const parentDirName = basename(skillDir);
const name = (frontmatter.name as string) || parentDirName;
if (!frontmatter.description) return null;
return {
name,
path,
content: body,
frontmatter: frontmatter as SkillFrontmatter,
level,
_source: createSourceMeta(PROVIDER_ID, path, level),
};
}
function loadSkillsRecursive(ctx: LoadContext, dir: string, level: "user" | "project"): LoadResult<Skill> {
const items: Skill[] = [];
const warnings: string[] = [];
if (!ctx.fs.isDir(dir)) return { items, warnings };
for (const name of ctx.fs.readDir(dir)) {
if (name.startsWith(".") || name === "node_modules") continue;
const path = join(dir, name);
if (ctx.fs.isDir(path)) {
const skillFile = join(path, "SKILL.md");
if (ctx.fs.isFile(skillFile)) {
const skill = loadSkillFromFile(ctx, skillFile, level);
if (skill) items.push(skill);
}
const sub = loadSkillsRecursive(ctx, path, level);
items.push(...sub.items);
if (sub.warnings) warnings.push(...sub.warnings);
} else if (name === "SKILL.md") {
const skill = loadSkillFromFile(ctx, path, level);
if (skill) items.push(skill);
}
}
return { items, warnings };
}
function loadSkills(ctx: LoadContext): LoadResult<Skill> {
const items: Skill[] = [];
const warnings: string[] = [];
for (const { dir, level } of getConfigDirs(ctx)) {
const skillsDir = join(dir, "skills");
const result = loadSkillsRecursive(ctx, skillsDir, level);
const result = loadSkillsFromDir(ctx, {
dir: skillsDir,
providerId: PROVIDER_ID,
level,
requireDescription: true,
});
items.push(...result.items);
if (result.warnings) warnings.push(...result.warnings);
}
+16 -69
View File
@@ -24,7 +24,7 @@ import {
expandEnvVarsDeep,
getExtensionNameFromPath,
loadFilesFromDir,
parseFrontmatter,
loadSkillsFromDir,
parseJSON,
} from "./helpers";
@@ -218,78 +218,25 @@ function loadSkills(ctx: LoadContext): LoadResult<Skill> {
const items: Skill[] = [];
const warnings: string[] = [];
// User-level: ~/.claude/skills/*/SKILL.md
const userBase = getUserClaude(ctx);
const userSkillsDir = join(userBase, "skills");
const userSkillsDir = join(getUserClaude(ctx), "skills");
const userResult = loadSkillsFromDir(ctx, {
dir: userSkillsDir,
providerId: PROVIDER_ID,
level: "user",
});
items.push(...userResult.items);
if (userResult.warnings) warnings.push(...userResult.warnings);
if (ctx.fs.isDir(userSkillsDir)) {
const skillDirs = ctx.fs.readDir(userSkillsDir);
for (const dirName of skillDirs) {
if (dirName.startsWith(".")) continue;
const skillDir = join(userSkillsDir, dirName);
if (!ctx.fs.isDir(skillDir)) continue;
const skillFile = join(skillDir, "SKILL.md");
if (!ctx.fs.isFile(skillFile)) continue;
const content = ctx.fs.readFile(skillFile);
if (!content) {
warnings.push(`Failed to read ${skillFile}`);
continue;
}
const { frontmatter, body } = parseFrontmatter(content);
const name = (frontmatter.name as string) || dirName;
items.push({
name,
path: skillFile,
content: body,
frontmatter,
level: "user",
_source: createSourceMeta(PROVIDER_ID, skillFile, "user"),
});
}
}
// Project-level: <project>/.claude/skills/*/SKILL.md
const projectBase = getProjectClaude(ctx);
if (projectBase) {
const projectSkillsDir = join(projectBase, "skills");
if (ctx.fs.isDir(projectSkillsDir)) {
const skillDirs = ctx.fs.readDir(projectSkillsDir);
for (const dirName of skillDirs) {
if (dirName.startsWith(".")) continue;
const skillDir = join(projectSkillsDir, dirName);
if (!ctx.fs.isDir(skillDir)) continue;
const skillFile = join(skillDir, "SKILL.md");
if (!ctx.fs.isFile(skillFile)) continue;
const content = ctx.fs.readFile(skillFile);
if (!content) {
warnings.push(`Failed to read ${skillFile}`);
continue;
}
const { frontmatter, body } = parseFrontmatter(content);
const name = (frontmatter.name as string) || dirName;
items.push({
name,
path: skillFile,
content: body,
frontmatter,
level: "project",
_source: createSourceMeta(PROVIDER_ID, skillFile, "project"),
});
}
}
const projectResult = loadSkillsFromDir(ctx, {
dir: projectSkillsDir,
providerId: PROVIDER_ID,
level: "project",
});
items.push(...projectResult.items);
if (projectResult.warnings) warnings.push(...projectResult.warnings);
}
return { items, warnings };
+11 -36
View File
@@ -33,6 +33,7 @@ import {
discoverExtensionModulePaths,
getExtensionNameFromPath,
loadFilesFromDir,
loadSkillsFromDir,
parseFrontmatter,
SOURCE_PATHS,
} from "./helpers";
@@ -209,51 +210,25 @@ function loadSkills(ctx: LoadContext): LoadResult<Skill> {
const items: Skill[] = [];
const warnings: string[] = [];
// User level: ~/.codex/skills/
const userSkillsDir = join(ctx.home, SOURCE_PATHS.codex.userBase, "skills");
const userResult = loadFilesFromDir(ctx, userSkillsDir, PROVIDER_ID, "user", {
extensions: ["md"],
recursive: true,
transform: (name, content, path, source) => {
const { frontmatter, body } = parseFrontmatter(content);
const skillName = frontmatter.name || name.replace(/\.md$/, "");
return {
name: String(skillName),
path,
content: body,
frontmatter,
level: "user" as const,
_source: source,
};
},
const userResult = loadSkillsFromDir(ctx, {
dir: userSkillsDir,
providerId: PROVIDER_ID,
level: "user",
});
items.push(...userResult.items);
warnings.push(...(userResult.warnings || []));
if (userResult.warnings) warnings.push(...userResult.warnings);
// Project level: .codex/skills/
const codexDir = ctx.fs.walkUp(".codex", { dir: true });
if (codexDir) {
const projectSkillsDir = join(codexDir, "skills");
const projectResult = loadFilesFromDir(ctx, projectSkillsDir, PROVIDER_ID, "project", {
extensions: ["md"],
recursive: true,
transform: (name, content, path, source) => {
const { frontmatter, body } = parseFrontmatter(content);
const skillName = frontmatter.name || name.replace(/\.md$/, "");
return {
name: String(skillName),
path,
content: body,
frontmatter,
level: "project" as const,
_source: source,
};
},
const projectResult = loadSkillsFromDir(ctx, {
dir: projectSkillsDir,
providerId: PROVIDER_ID,
level: "project",
});
items.push(...projectResult.items);
warnings.push(...(projectResult.warnings || []));
if (projectResult.warnings) warnings.push(...projectResult.warnings);
}
return { items, warnings };
+52 -1
View File
@@ -4,6 +4,7 @@
import { join, resolve } from "path";
import { parse as parseYAML } from "yaml";
import type { Skill, SkillFrontmatter } from "../capability/skill";
import type { LoadContext, LoadResult, SourceMeta } from "../capability/types";
/**
@@ -126,6 +127,56 @@ export function parseFrontmatter(content: string): {
}
}
export function loadSkillsFromDir(
ctx: LoadContext,
options: {
dir: string;
providerId: string;
level: "user" | "project";
requireDescription?: boolean;
},
): LoadResult<Skill> {
const items: Skill[] = [];
const warnings: string[] = [];
const { dir, level, providerId, requireDescription = false } = options;
if (!ctx.fs.isDir(dir)) {
return { items, warnings };
}
for (const name of ctx.fs.readDir(dir)) {
if (name.startsWith(".") || name === "node_modules") continue;
const skillDir = join(dir, name);
if (!ctx.fs.isDir(skillDir)) continue;
const skillFile = join(skillDir, "SKILL.md");
if (!ctx.fs.isFile(skillFile)) continue;
const content = ctx.fs.readFile(skillFile);
if (!content) {
warnings.push(`Failed to read ${skillFile}`);
continue;
}
const { frontmatter, body } = parseFrontmatter(content);
if (requireDescription && !frontmatter.description) {
continue;
}
items.push({
name: (frontmatter.name as string) || name,
path: skillFile,
content: body,
frontmatter: frontmatter as SkillFrontmatter,
level,
_source: createSourceMeta(providerId, skillFile, level),
});
}
return { items, warnings };
}
/**
* Expand environment variables in a string.
* Supports ${VAR} and ${VAR:-default} syntax.
@@ -286,7 +337,7 @@ export function discoverExtensionModulePaths(ctx: LoadContext, dir: string): str
const discovered: string[] = [];
for (const name of ctx.fs.readDir(dir)) {
if (name.startsWith(".")) continue;
if (name.startsWith(".") || name === "node_modules") continue;
const entryPath = join(dir, name);
@@ -33,6 +33,7 @@ If you discussed requirements, plans, schemas, or decisions with the user, you M
- **Minimize tool chatter**: Avoid repeating large context; use Output tool with output ids for full logs
- **Structured completion**: If `output` is provided, subagents must call `complete` to finish
- **Parallelize**: Launch multiple agents concurrently whenever possible
- **Isolate file scopes**: Assign each task distinct files or directories so agents don't conflict
- **Results are intermediate data**: Agent findings provide context for YOU to perform actual work. Do not treat agent reports as "task complete" signals.
- **Stateless invocations**: Subagents have zero memory of your conversation. Pass ALL relevant context: requirements discussed, decisions made, schemas agreed upon, file paths mentioned. If you reference something from earlier discussion without including it, the subagent will fail.
- **Trust outputs**: Agent results should generally be trusted