スキルシステム
skills.ts は Agent Skills 仕様(https://agentskills.io/integrate-skills 参照)を実装する。スキルは markdown ファイルで、frontmatter がスキルの用途を記述する。LLM はシステムプロンプト内のスキルリストを見て、read ツールで対応する SKILL.md を読んで詳細な指示を取る。このファイルはスキルの発見、ロード、検証、重複除去、プロンプトセクションへのフォーマットを担う。
責務
- 発見規則:
loadSkillsFromDirInternalはディレクトリを再帰的に走査する——SKILL.mdに出会ったらスキルのルートとみなし再帰しない。さもなくば子ディレクトリとルートの.mdファイルを走査する。packages/coding-agent/src/core/skills.ts:173-280参照。 - frontmatter 検証:
validateNameは名前を検査する(a-z0-9-、64 文字以内、ハイフンで始め/終わらない、--なし、親ディレクトリ名と一致)。validateDescriptionは描述が必須かつ 1024 文字以内かを検査する。packages/coding-agent/src/core/skills.ts:93-117、packages/coding-agent/src/core/skills.ts:122-132参照。 - 多来源ロード:
loadSkillsはユーザグローバル(~/.pi/agent/skills)、プロジェクト(./.pi/skills)、明示的skillPathsの三来源からロードする。衝突時は先に登録されたものを残しcollision診断を生成する。packages/coding-agent/src/core/skills.ts:405-504参照。 - 重複除去:
realpathでシンボリックリンクを解決し、同一ファイルを別パスで登録しても一回だけカウントする。同名スキルは先着順。packages/coding-agent/src/core/skills.ts:416-445参照。 - プロンプトフォーマット:
formatSkillsForPromptは<available_skills>XML セクションを生成する。各スキルは<name>/<description>/<location>の三組で表現し、disableModelInvocation: trueのスキルはプロンプトに入れない(!/skill:nameで明示的呼び出しのみ)。packages/coding-agent/src/core/skills.ts:340-366参照。 - ignore 規則:
.gitignore/.ignore/.fdignoreを尊重し、再帰走査時にディレクトリプレフィックスをパスパターンに追加する。packages/coding-agent/src/core/skills.ts:48-66、packages/coding-agent/src/core/skills.ts:25-46参照。
設計動機
なぜ SKILL.md なのか。すべての .md を直接走査するのではなく?一つのスキルが複数ファイル(スクリプト、サブドキュメント)を含み得るからだ。SKILL.md は入口のしるしで、これに出会ったら再帰を止め、ディレクトリ全体をスキルパッケージとみなす。これでスキル作者は複数ファイルを一つのスキルとしてまとめられるし、複数の独立スキルとして誤って分割されることもない。
なぜ frontmatter なのか。コメントではなく?frontmatter は YAML で、構造化され、解析でき、検証できる。名称、描述、disable-model-invocation といったフィールドは LLM とプログラムの両方が読める必要がある。validateName は名前と親ディレクトリ名の一致を強制し(name "x" does not match parent directory "y")、スキルファイルを移動した後の名前の drift を防ぐ。
なぜ disableModelInvocation が必要なのか。ユーザ自身で使うプロンプトテンプレートのようなスキルは LLM に自動発火してほしくない(「コードを書くときはこのスタイルで」など)場合があり、ユーザが明示的に /skill:name したときだけロードされるべきだ。formatSkillsForPrompt はこうしたスキルをフィルタし、プロンプトには LLM が自動呼び出しを許可されたものだけを晒す。
なぜ realpath で重複除去するのか。ユーザが --skills ./my-skills を指定しつつ ./my-skills を ~/.pi/agent/skills/my-skills にシンボリックリンクで張ることがある。二回の走査で同一ファイルの異なるパスが取れる。canonicalizePath がシンボリックリンクを解決してから比較し、同一スキルの重複ロードを防ぐ。
主要ファイル
packages/coding-agent/src/core/skills.ts:11-19— 定数:MAX_NAME_LENGTH64、MAX_DESCRIPTION_LENGTH1024、IGNORE_FILE_NAMES。packages/coding-agent/src/core/skills.ts:21-46—toPosixPathとprefixIgnorePattern。子ディレクトリの ignore 規則にディレクトリプレフィックスを付ける。packages/coding-agent/src/core/skills.ts:75-87—Skillインターフェース:name、description、filePath、baseDir、sourceInfo、disableModelInvocation。packages/coding-agent/src/core/skills.ts:93-117—validateName、5 条の検査規則。packages/coding-agent/src/core/skills.ts:173-280—loadSkillsFromDirInternal、再帰走査と ignore フィルタ。packages/coding-agent/src/core/skills.ts:282-330—loadSkillFromFile、frontmatter 解析と検証。packages/coding-agent/src/core/skills.ts:340-366—formatSkillsForPrompt、XML セクション生成。packages/coding-agent/src/core/skills.ts:405-504—loadSkills、多来源ロードと重複除去。
formatSkillsForPrompt は XML タグでスキルリストを包み、XML 特殊文字をエスケープする:
// 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 は SKILL.md に出会ったら即座に return し、再帰しない:
// 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 };
}データフロー
スキルがディスクからプロンプトに至るまで:
境界と失敗
- 描述なしはロード拒否:
frontmatter.descriptionが空、または trim 後空のときは{ skill: null }を返し、skillMap に入れない。packages/coding-agent/src/core/skills.ts:310-312参照。 - 名前不整合でもロード:名前検証は warning 診断を出すだけでロードを阻止しない。ユーザに問題を見せつつスキルを使えるようにする。
packages/coding-agent/src/core/skills.ts:300-307参照。 - 壊れたシンボリックリンク:
statSyncが throw したらcontinueでスキップし、走査全体を中断しない。packages/coding-agent/src/core/skills.ts:207-213、packages/coding-agent/src/core/skills.ts:241-252参照。 - node_modules をスキップ:再帰時に
node_modulesを明示的にスキップし、依存先の.mdを走査しない。packages/coding-agent/src/core/skills.ts:233-236参照。 - path 型の解決:明示的
skillPathsはファイルでもディレクトリでもよく、ディレクトリはloadSkillsFromDirInternalに回し、ファイルは.mdでなければならない。packages/coding-agent/src/core/skills.ts:478-498参照。 - source 識別:明示的 path は
includeDefaults: falseでも user/project 来源(パスプレフィックスに基づく)として識別を試み、さもなくばpathとする。packages/coding-agent/src/core/skills.ts:464-470参照。
まとめ
skills.ts は Agent Skills 仕様を実装し、三来源ロード(ユーザグローバル、プロジェクト、明示的パス)、frontmatter 検証、realpath 重複除去、XML フォーマットでプロンプトに注入する。スキルはシステムプロンプトから参照された後 read ツールで読まれる。システムプロンプト構築 と ツール集合 read/bash/edit/write/grep/find/ls 参照。スキルパスを装配する入口は CLI 入口とディスパッチ 参照。