Le format SKILL.md
Une équipe finance dépose dans le dossier partagé un skill dont le frontmatter contient description: Use when: the user sends receipts. Le parseur YAML du harness lève une exception sur le deux-points, le chargeur s'arrête, et les cinq autres skills disparaissent avec lui. Personne ne le voit avant que le support ne signale que l'assistant « ne trie plus les tickets ». Lire un SKILL.md paraît trivial ; le lire sans faire tomber les voisins demande des règles écrites.
Le mécanisme
Un skill est un dossier qui contient au minimum un fichier SKILL.md : un frontmatter YAML entre deux lignes ---, puis un corps Markdown libre. La spécification (agentskills.io/specification) recommande trois sous-dossiers facultatifs : scripts/ (code exécutable), references/ (documentation lue à la demande) et assets/ (gabarits, données). Le dépôt anthropics/skills publie des exemples et un dossier spec/ ; son README renvoie vers agentskills.io pour le standard.
flowchart TB D["expense-report-checker/"] --> S["SKILL.md : frontmatter + corps"] D --> SC["scripts/check_expenses.py"] D --> R["references/politique-frais.md"] D --> A["assets/bareme.json, assets/rapport.md"] S --> F["name, description (obligatoires)"] S --> O["license, compatibility, metadata, allowed-tools (facultatifs)"]
Les contraintes, relevées sur la spécification le 2026-10-15 :
| Champ | Obligatoire | Contrainte |
|---|---|---|
name | oui | 1 à 64 caractères, a-z, 0-9 et tirets ; pas de tiret en tête, en fin ni doublé ; égal au nom du dossier |
description | oui | 1 à 1 024 caractères ; dit ce que fait le skill et quand l'utiliser |
license | non | nom de licence ou fichier de licence joint |
compatibility | non | 1 à 500 caractères (produit visé, paquets système, accès réseau) |
metadata | non | dictionnaire de chaînes vers chaînes (version: "1.2.0") |
allowed-tools | non | liste d'outils pré-approuvés séparés par des espaces, expérimental |
Pour le corps, la spécification recommande moins de 500 lignes et moins de 5 000 tokens, avec des références à un seul niveau de profondeur depuis SKILL.md.
La mise en œuvre
Le dépôt applique deux politiques différentes, comme le recommande le guide d'implémentation client : strict en CI, tolérant au chargement. Un nom qui ne correspond pas au dossier est une faute de l'auteur, à refuser en revue ; en production, on charge quand même et on journalise. Seules deux fautes écartent un skill : YAML illisible après réparation, ou description absente (sans elle, le modèle ne peut pas savoir quand l'utiliser).
def split_frontmatter(text: str) -> tuple[dict, str]:
m = re.match(r"^---\s*\n(.*?)\n---\s*(?:\n|$)(.*)", text, re.S)
...
try:
fm = yaml.safe_load(raw) or {}
except yaml.YAMLError:
# « description: Use when: ... » non guillemeté : on guillemette et on réessaie.
fixed = re.sub(r"^(\w[\w-]*):\s+(.*:.*)$", lambda x: f'{x[1]}: "{x[2]}"', raw, flags=re.M)
fm = yaml.safe_load(fixed) or {}La découverte parcourt les dossiers, ignore .git, node_modules, .venv, borne la profondeur, et applique une règle de collision déterministe : le premier répertoire passé gagne (projet avant utilisateur), avec un avertissement.
def discover(*roots: Path, max_depth: int = 4) -> dict[str, Skill]:
for root in roots:
for md in sorted(root.rglob("SKILL.md")):
...
if s.name in found:
log.warning("collision : %s (%s) masqué par %s", s.name, md.parent, found[s.name].path)
continueLes trois skills nouveaux suivent la même structure de description que ceux de la bibliothèque : ce que le skill produit, « Use whenever… », une ligne « Not for » qui renvoie vers le skill voisin, puis des phrases déclencheuses en anglais et en français. Cette ligne « Not for » pèsera lourd dans la leçon sur le déclenchement : hr-policy-qa parle aussi de frais et de congés.
Résultat obtenu
Sortie réelle de python run.py validate, 2026-10-15 :
OK csv-data-profiler v0.9.0 corps 25 lignes, 2 ressource(s), description 707 car.
OK expense-report-checker v1.2.0 corps 27 lignes, 4 ressource(s), description 689 car.
OK hr-policy-qa v0.0.0 corps 81 lignes, 0 ressource(s), description 781 car.
OK leave-balance-calculator v1.0.3 corps 31 lignes, 2 ressource(s), description 696 car.
OK meeting-notes-to-actions v0.0.0 corps 85 lignes, 0 ressource(s), description 669 car.
OK ticket-triage v0.0.0 corps 91 lignes, 0 ressource(s), description 752 car.Ce qui casse en production
| Symptôme | Cause | Parade |
|---|---|---|
| Tous les skills disparaissent d'un coup | une exception YAML non rattrapée dans la boucle de découverte | try par skill, réparation des deux-points, journal d'erreur, on continue |
| Un skill « n'existe pas » alors que le fichier est là | name différent du dossier ; ADK refuse de le charger (load_skill_from_dir lève une erreur) | validation stricte en CI, avant la fusion |
| Deux comportements selon le poste | deux skills de même nom, l'un dans le projet, l'autre dans ~/.agents/skills/ | règle de précédence fixe et avertissement de collision visible |
version: 1.10 devient le nombre 1.1, le verrou ne voit plus la montée de version | YAML convertit en nombre | la spécification impose des chaînes : guillemets, et le validateur le vérifie |
| Un dépôt cloné injecte des instructions | les skills de projet viennent d'un dépôt non fiable | ne charger les skills de projet que si le dossier est marqué de confiance |
La règle de décision
Validez strictement en CI et chargez avec tolérance en production. N'ajoutez des champs hors spécification que dans metadata, jamais à la racine du frontmatter : ADK les accepte, d'autres clients les refusent. Gardez SKILL.md sous 300 lignes ; au-delà, déplacez le détail dans references/. Surveillez dans les journaux le nombre de skills chargés au démarrage : une baisse soudaine signale un skill rejeté.
Pratique
Un auteur livre un skill dont le frontmatter contient « version: 2.0 » à la racine, à côté de name et description. Le chargeur tolérant l'accepte. Que faites-vous ?
La spécification n'autorise que six champs à la racine ; d'autres clients peuvent refuser le fichier. La version a sa place dans metadata, sous forme de chaîne. Accepter en silence reporte le problème sur un autre client ; supprimer sans prévenir masque l'erreur ; renommer le dossier casse l'égalité name = dossier.
Au démarrage, le journal affiche « collision : ticket-triage (~/.agents/skills/ticket-triage) masqué par ./skills/ticket-triage ». Le support se plaint d'un comportement différent sur le poste d'un collègue. Première vérification ?
La précédence dépend du répertoire de travail : hors du projet, seule la copie utilisateur existe. Le comportement diffère parce que le contenu diffère. Charger les deux crée un nom ambigu ; la profondeur et la description sont sans rapport avec la collision.
Exercice. Ajoutez au validateur une règle : un SKILL.md qui cite un fichier (scripts/x.py, references/y.md) absent du dossier est une erreur. Testez-la sur expense-report-checker en renommant assets/rapport.md.
Voir la solution
def missing_refs(skill: Skill) -> list[str]:
cited = set(re.findall(r"`((?:scripts|references|assets)/[\w./-]+)`", skill.body))
return sorted(c for c in cited if c not in skill.resources)Appelez-la dans cmd_validate et ajoutez un test qui copie le skill dans tmp_path, renomme assets/rapport.md et vérifie que missing_refs renvoie ["assets/rapport.md"]. Le motif ne couvre que les chemins entre accents graves : c'est la convention des six skills, à écrire dans votre guide d'auteur.
- Je sais lister les six champs du frontmatter et leurs contraintes
- Je sais pourquoi valider strictement en CI et charger avec tolérance
- Je sais réparer un frontmatter avec deux-points non guillemetés
- Je sais définir une règle de précédence entre skills de projet et d'utilisateur