Système de skills
skills.ts implémente la spec Agent Skills (voir https://agentskills.io/integrate-skills). Une skill est un fichier markdown avec un frontmatter qui décrit son usage ; le LLM voit la liste des skills dans le system prompt, puis utilise l'outil read pour lire le SKILL.md correspondant et obtenir des instructions détaillées. Ce fichier gère la découverte, le chargement, la validation, la déduplication et le formatage en bloc de prompt des skills.
Responsabilités
- Règles de découverte :
loadSkillsFromDirInternalscanne récursivement un répertoire — quand il rencontreSKILL.md, il le considère comme la racine d'une skill et ne descend plus ; sinon, il descend dans les sous-répertoires et scanne les fichiers.mdà la racine. Voirpackages/coding-agent/src/core/skills.ts:173-280. - Validation du frontmatter :
validateNamevérifie le nom (a-z0-9-, ≤ 64 caractères, ni ne commence ni ne finit par un tiret, pas de--, identique au nom du répertoire parent) ;validateDescriptionvérifie que la description est obligatoire et ≤ 1024 caractères. Voirpackages/coding-agent/src/core/skills.ts:93-117etpackages/coding-agent/src/core/skills.ts:122-132. - Chargement multi-source :
loadSkillscharge depuis trois sources — global utilisateur (~/.pi/agent/skills), projet (./.pi/skills), etskillPathsexplicites ; en cas de conflit, le premier enregistré l'emporte et un diagnosticcollisionest produit. Voirpackages/coding-agent/src/core/skills.ts:405-504. - Déduplication : utilise
realpathpour résoudre les liens symboliques ; un même fichier enregistré via des chemins différents ne compte qu'une fois ; en cas de skills homonymes, premier arrivé premier servi. Voirpackages/coding-agent/src/core/skills.ts:416-445. - Formatage du prompt :
formatSkillsForPromptgénère un bloc XML<available_skills>, avec un triplet<name>/<description>/<location>par skill ; les skillsdisableModelInvocation: truen'entrent pas dans le prompt (elles ne peuvent être appelées que via/skill:nameexplicite). Voirpackages/coding-agent/src/core/skills.ts:340-366. - Règles d'ignore : respecte
.gitignore/.ignore/.fdignore; le scan récursif préfixe les motifs par le chemin du répertoire. Voirpackages/coding-agent/src/core/skills.ts:48-66etpackages/coding-agent/src/core/skills.ts:25-46.
Motifs de conception
Pourquoi SKILL.md plutôt que de scanner tous les .md ? Parce qu'une skill peut contenir plusieurs fichiers (scripts, sous-docs) : SKILL.md est le marqueur d'entrée. Une fois SKILL.md rencontré, on arrête la récursion et on traite le répertoire entier comme un pack de skills, ce qui permet à un auteur d'organiser plusieurs fichiers sans qu'ils soient vus comme plusieurs skills indépendantes.
Pourquoi un frontmatter plutôt que des commentaires ? Parce que le frontmatter est du YAML, structuré, parsable, validable. Les champs name, description, disable-model-invocation doivent être lisibles tant par le LLM que par le programme. validateName force le nom à correspondre au nom du répertoire parent (name "x" does not match parent directory "y"), pour éviter que le nom ne dérive quand le fichier de skill est déplacé.
Pourquoi disableModelInvocation ? Certaines skills sont des templates de prompt à usage utilisateur et ne doivent pas être déclenchées automatiquement par le LLM (par ex. « quand tu écris du code, utilise ce style ») ; elles ne se chargent que lorsque l'utilisateur fait explicitement /skill:name. formatSkillsForPrompt filtre ces skills, le prompt n'expose que celles que le LLM peut appeler de lui-même.
Pourquoi dédupliquer via realpath ? Parce que l'utilisateur peut passer --skills ./my-skills tout en ayant symlinké ./my-skills vers ~/.pi/agent/skills/my-skills ; deux scans ramèneraient le même fichier via des chemins différents. canonicalizePath résout les liens symboliques avant comparaison, pour éviter de charger deux fois la même skill.
Fichiers clés
packages/coding-agent/src/core/skills.ts:11-19— constantes :MAX_NAME_LENGTH64,MAX_DESCRIPTION_LENGTH1024,IGNORE_FILE_NAMES.packages/coding-agent/src/core/skills.ts:21-46—toPosixPathetprefixIgnorePattern, qui préfixent les règles d'ignore des sous-répertoires par le chemin du répertoire.packages/coding-agent/src/core/skills.ts:75-87— interfaceSkill:name,description,filePath,baseDir,sourceInfo,disableModelInvocation.packages/coding-agent/src/core/skills.ts:93-117—validateName, 5 règles de validation.packages/coding-agent/src/core/skills.ts:173-280—loadSkillsFromDirInternal, scan récursif et filtrage ignore.packages/coding-agent/src/core/skills.ts:282-330—loadSkillFromFile, parsing et validation du frontmatter.packages/coding-agent/src/core/skills.ts:340-366—formatSkillsForPrompt, génération du bloc XML.packages/coding-agent/src/core/skills.ts:405-504—loadSkills, chargement multi-source et déduplication.
formatSkillsForPrompt enveloppe la liste des skills dans des balises XML et échappe les caractères spéciaux :
// 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 retourne immédiatement dès qu'il rencontre SKILL.md, sans récuser plus loin :
// 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 };
}Flux de données
Une skill du disque jusqu'au prompt :
Limites et cas d'échec
- Pas de description = refus de chargement : quand
frontmatter.descriptionest vide ou ne contient que des espaces, on renvoie{ skill: null }et la skill n'entre pas dans skillMap. Voirpackages/coding-agent/src/core/skills.ts:310-312. - Nom non conforme mais chargement quand même : la validation du nom ne produit qu'un diagnostic warning, n'empêche pas le chargement — l'utilisateur voit le problème mais la skill reste utilisable. Voir
packages/coding-agent/src/core/skills.ts:300-307. - Lien symbolique cassé : si
statSynclève, oncontinuesans interrompre le scan global. Voirpackages/coding-agent/src/core/skills.ts:207-213etpackages/coding-agent/src/core/skills.ts:241-252. - node_modules sauté : la récursion saute explicitement
node_modulespour ne pas scanner les.mddes dépendances. Voirpackages/coding-agent/src/core/skills.ts:233-236. - Résolution du type de path : un
skillPathsexplicite peut être un fichier ou un répertoire ; un répertoire passe parloadSkillsFromDirInternal, un fichier doit être.md. Voirpackages/coding-agent/src/core/skills.ts:478-498. - Identification de la source : un path explicite avec
includeDefaults: falseessaie quand même de s'identifier comme source user/project (basé sur le préfixe du chemin), sinon est étiquetépath. Voirpackages/coding-agent/src/core/skills.ts:464-470.
Synthèse
skills.ts implémente la spec Agent Skills : chargement depuis trois sources (global utilisateur, projet, chemin explicite), validation du frontmatter, déduplication par realpath, formatage en XML injecté dans le prompt. Une skill référencée par le system prompt est ensuite lue via l'outil read — voir construction du system prompt et ensemble d'outils read/bash/edit/write/grep/find/ls. L'entrée qui assemble les chemins de skills est dans entrée CLI et dispatch.