Sistema de skills
skills.ts implementa la especificación Agent Skills (ver https://agentskills.io/integrate-skills). Una skill es un archivo markdown con frontmatter que describe su uso; el LLM ve la lista de skills en el system prompt y luego lee el SKILL.md correspondiente con la herramienta read para obtener instrucciones detalladas. Este archivo gestiona el descubrimiento, carga, validación, deduplicación y formateo a párrafo de prompt.
Responsabilidades
- Reglas de descubrimiento:
loadSkillsFromDirInternalescanea recursivamente; al encontrarSKILL.mdlo trata como raíz de skill y no recursa más; si no, escanea subdirectorios y archivos.mdde la raíz. Verpackages/coding-agent/src/core/skills.ts:173-280. - Validación de frontmatter:
validateNamecomprueba el nombre (a-z0-9-, máx 64 chars, no empieza/termina con guion, sin--, coincide con el nombre del directorio padre);validateDescriptionexige descripción obligatoria y máx 1024 chars. Verpackages/coding-agent/src/core/skills.ts:93-117ypackages/coding-agent/src/core/skills.ts:122-132. - Carga multi-fuente:
loadSkillscarga desde el usuario global (~/.pi/agent/skills), el proyecto (./.pi/skills) y paths explícitosskillPaths; en conflictos gana el primero registrado y se genera un diagnósticocollision. Verpackages/coding-agent/src/core/skills.ts:405-504. - Deduplicación: usa
realpathpara resolver symlinks; el mismo archivo registrado por paths distintos cuenta una sola vez; para skills con el mismo nombre, gana el primero en llegar. Verpackages/coding-agent/src/core/skills.ts:416-445. - Formateo de prompt:
formatSkillsForPromptgenera un bloque XML<available_skills>con tripleta<name>/<description>/<location>por skill; las que tienendisableModelInvocation: trueno entran en el prompt (sólo se invocan con/skill:name). Verpackages/coding-agent/src/core/skills.ts:340-366. - Reglas de ignore: respeta
.gitignore/.ignore/.fdignore; al escanear recursivamente se concatenan los patrones por prefijo de directorio. Verpackages/coding-agent/src/core/skills.ts:48-66ypackages/coding-agent/src/core/skills.ts:25-46.
Motivación de diseño
¿Por qué SKILL.md en vez de escanear todos los .md? Porque una skill puede incluir varios archivos (scripts, sub-docs); SKILL.md es la marca de entrada. Al encontrarlo se para la recursión y se trata el directorio entero como paquete de skill, permitiendo al autor organizar varios archivos sin que se conviertan en skills independientes.
¿Por qué frontmatter y no comentarios? Porque el frontmatter es YAML, estructurado, parseable y validable. Los campos name, description, disable-model-invocation tienen que ser legibles tanto por el LLM como por el programa. validateName fuerza que el nombre coincida con el del directorio padre (name "x" does not match parent directory "y"), evitando que el nombre derive al mover el archivo.
¿Por qué disableModelInvocation? Algunas skills son plantillas de prompt para uso personal del usuario y no deberían dispararse automáticamente desde el LLM (por ejemplo, "usa este estilo al escribir código"); sólo cargan cuando el usuario escribe /skill:name. formatSkillsForPrompt filtra estas skills; el prompt sólo expone las que el LLM puede invocar.
¿Por qué deduplicar por realpath? Porque el usuario puede pasar --skills ./my-skills y a la vez tener ./my-skills enlazado simbólicamente a ~/.pi/agent/skills/my-skills; dos escaneos devolverían paths distintos del mismo archivo. canonicalizePath resuelve el symlink antes de comparar, evitando cargar la misma skill dos veces.
Archivos clave
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—toPosixPathyprefixIgnorePattern, antepone el prefijo de subdirectorio a las reglas ignore del subdirectorio.packages/coding-agent/src/core/skills.ts:75-87— InterfazSkill:name,description,filePath,baseDir,sourceInfo,disableModelInvocation.packages/coding-agent/src/core/skills.ts:93-117—validateName, 5 reglas.packages/coding-agent/src/core/skills.ts:173-280—loadSkillsFromDirInternal, escaneo recursivo con filtro ignore.packages/coding-agent/src/core/skills.ts:282-330—loadSkillFromFile, parseo y validación de frontmatter.packages/coding-agent/src/core/skills.ts:340-366—formatSkillsForPrompt, generación del bloque XML.packages/coding-agent/src/core/skills.ts:405-504—loadSkills, carga multi-fuente y deduplicación.
formatSkillsForPrompt envuelve la lista en etiquetas XML y escapa caracteres especiales:
// 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 al encontrar SKILL.md devuelve y no recursa más:
// 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 };
}Flujo de datos
Skills de disco a prompt:
Límites y fallos
- Sin descripción, se rechaza: si
frontmatter.descriptionestá vacío o trim vacío, devuelve{ skill: null }y no entra en skillMap. Verpackages/coding-agent/src/core/skills.ts:310-312. - Nombre no coincide pero carga igual: la validación del nombre sólo produce un diagnóstico de warning, no bloquea; el usuario ve el problema pero la skill sigue disponible. Ver
packages/coding-agent/src/core/skills.ts:300-307. - Symlink roto: si
statSynclanza, se hacecontinuey se salta, sin interrumpir el escaneo. Verpackages/coding-agent/src/core/skills.ts:207-213ypackages/coding-agent/src/core/skills.ts:241-252. - Salto de node_modules: al recursar se salta explícitamente
node_modules, para no escanear.mdde dependencias. Verpackages/coding-agent/src/core/skills.ts:233-236. - Resolución de path explícito: un
skillPathsexplícito puede ser archivo o directorio; el directorio va porloadSkillsFromDirInternal, el archivo debe ser.md. Verpackages/coding-agent/src/core/skills.ts:478-498. - Identificación de source: un path explícito con
includeDefaults: falsetodavía se intenta identificar como fuente user/project (por prefijo de path); si no, se marcapath. Verpackages/coding-agent/src/core/skills.ts:464-470.
Resumen
skills.ts implementa la especificación Agent Skills: carga desde tres fuentes (usuario global, proyecto, path explícito), valida frontmatter, deduplica por realpath y formatea el prompt en XML. Las skills se referencian en el system prompt y se leen con la herramienta read; ver construcción del system prompt y conjunto de herramientas read/bash/edit/write/grep/find/ls. La entrada que ensambla los paths de skills en entrada y dispatch del CLI.