Skip to content

Skill System

源码版本v0.73.1

skills.ts implements the Agent Skills spec (see https://agentskills.io/integrate-skills). A skill is a markdown file with frontmatter describing its purpose. After the LLM sees the skill list in the system prompt, it reads the corresponding SKILL.md via the read tool to get detailed instructions. This file handles skill discovery, loading, validation, deduplication, and formatting as a prompt section.

Responsibilities

  1. Discovery rules: loadSkillsFromDirInternal recursively scans a directory — encountering SKILL.md is treated as a skill root and recursion stops; otherwise it scans subdirectories and root-level .md files. See packages/coding-agent/src/core/skills.ts:173-280.
  2. Frontmatter validation: validateName checks the name (a-z0-9-, max 64 chars, no leading/trailing hyphen, no --, matches the parent directory name); validateDescription checks the description is required and max 1024 chars. See packages/coding-agent/src/core/skills.ts:93-117 and packages/coding-agent/src/core/skills.ts:122-132.
  3. Multi-source loading: loadSkills loads from the user global (~/.pi/agent/skills), the project (./.pi/skills), and explicit skillPaths. On collision it keeps the first registered and emits a collision diagnostic. See packages/coding-agent/src/core/skills.ts:405-504.
  4. Deduplication: uses realpath to resolve symlinks, so the same file registered via different paths only counts once; same-named skills are first-come-first-served. See packages/coding-agent/src/core/skills.ts:416-445.
  5. Prompt formatting: formatSkillsForPrompt generates an <available_skills> XML block, with each skill as a <name> / <description> / <location> triple. Skills with disableModelInvocation: true are excluded from the prompt (they can only be invoked explicitly via /skill:name). See packages/coding-agent/src/core/skills.ts:340-366.
  6. Ignore rules: respects .gitignore / .ignore / .fdignore, with directory-prefixed path patterns during recursive scans. See packages/coding-agent/src/core/skills.ts:48-66 and packages/coding-agent/src/core/skills.ts:25-46.

Design Motivation

Why use SKILL.md instead of scanning all .md files? A skill may contain multiple files (scripts, sub-docs); SKILL.md is the entry marker. Once encountered, recursion stops and the whole directory is treated as a skill package, letting authors organize multiple files without them being picked up as separate skills.

Why frontmatter instead of comments? Frontmatter is YAML — structured, parseable, validatable. Fields like name, description, disable-model-invocation need to be readable by both the LLM and programs. validateName enforces that the name matches the parent directory name (name "x" does not match parent directory "y"), so skills don't drift after being moved.

Why disableModelInvocation? Some skills are user-private prompt templates that shouldn't be auto-triggered by the LLM (e.g., "use this style when writing code"); they should only be loaded when the user explicitly runs /skill:name. formatSkillsForPrompt filters these out, so only skills allowed for LLM auto-invocation are exposed in the prompt.

Why dedupe via realpath? A user might pass --skills ./my-skills while also symlinking ./my-skills to ~/.pi/agent/skills/my-skills. Two scans would pick up the same file via different paths. canonicalizePath resolves symlinks before comparing, avoiding double-loading.

Key Files

formatSkillsForPrompt wraps the skill list in XML tags and escapes XML special characters:

typescript
// packages/coding-agent/src/core/skills.ts:340-366
export function formatSkillsForPrompt(skills: Skill[]): string {
	const visibleSkills = skills.filter((s) => !s.disableModelInvocation);

	if (visibleSkills.length === 0) {
		return "";
	}

	const lines = [
		"\n\nThe following skills provide specialized instructions for specific tasks.",
		"Use the read tool to load a skill's file when the task matches its description.",
		"When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.",
		"",
		"<available_skills>",
	];

	for (const skill of visibleSkills) {
		lines.push("  <skill>");
		lines.push(`    <name>${escapeXml(skill.name)}</name>`);
		lines.push(`    <description>${escapeXml(skill.description)}</description>`);
		lines.push(`    <location>${escapeXml(skill.filePath)}</location>`);
		lines.push("  </skill>");
	}
	lines.push("</available_skills>");
	return lines.join("\n");
}

loadSkillsFromDirInternal returns immediately upon encountering SKILL.md and does not recurse further:

typescript
// packages/coding-agent/src/core/skills.ts:199-226
for (const entry of entries) {
	if (entry.name !== "SKILL.md") {
		continue;
	}
	const fullPath = join(dir, entry.name);
	let isFile = entry.isFile();
	if (entry.isSymbolicLink()) {
		try {
			isFile = statSync(fullPath).isFile();
		} catch {
			continue;
		}
	}
	const relPath = toPosixPath(relative(root, fullPath));
	if (!isFile || ig.ignores(relPath)) {
		continue;
	}
	const result = loadSkillFromFile(fullPath, source);
	if (result.skill) {
		skills.push(result.skill);
	}
	diagnostics.push(...result.diagnostics);
	return { skills, diagnostics };
}

Data Flow

Skills from disk to prompt:

Edges and Failures

Summary

skills.ts implements the Agent Skills spec: three-source loading (user global, project, explicit path), frontmatter validation, realpath dedup, and XML formatting injected into the prompt. Once a skill is referenced by the system prompt, it's read via the read tool — see System Prompt Construction and Tools: read/bash/edit/write/grep/find/ls. For the entry point that wires up skill paths, see CLI Entry and Dispatch.