ADR-0028 — Deux bibliotheques de skills, jamais une avec un drapeau
Le depot appelait deja trois choses differentes « skill », sans un seul import entre elles : les skills @Atlas sur disque (src/features/ai/skills/) : un prompt + ; les store skills (StoreContext.modules.skills) : les…
Statut
Accepté · 2026-09-20
Piliers : ai-platform, security-identity
Contexte
Le depot appelait deja trois choses differentes « skill », sans un seul import entre elles :
- les skills @Atlas sur disque (
src/features/ai/skills/) : un prompt + une liste d'outils, un seul choisi par tour de chat ; - les store skills (
StoreContext.modules.skills) : les memes champs, ecrits par le marchand, charges par requete ; - les wizard skills (
src/features/ai/wizard-skills/) : des fonctions TypeScript deterministes qui executent un jalon de lancement.
Plus un quatrieme homonyme purement editorial, le catalogue
/marketplace/skills, qui ne route rien.
Les onze bundles de la famille 1 sont tous des skills marchand : ils
s'executent dans un tour scope sur une boutique, filtrent des outils
Shopify / Studio / Intelligence, et repondent a « comment j'opere MA
boutique ». Le seul axe de classement existant etait tier
(standard / premium), qui est un axe de facturation.
Il n'existait donc aucune facon d'ecrire une methode interne. Tout ce
qui se repete de notre cote — la relecture de la file Postiz, le passage
du radar bulletin a une edition, la QC creative, la fabrication d'une
GrowthUnit — vivait dans la tete de qui le faisait, ou dans la copy
d'une page /ops.
Décision
Une seconde famille, src/features/ai/platform-skills/, avec son
repertoire, son loader, son registre, son routeur et sa porte. Un bundle
y est gate par une permission platform.*, jamais par un plan. Un tour
de chat appartient a exactement une des deux bibliotheques : il est
interne quand l'acteur tient au moins un mandat plateforme et qu'il
n'y a pas de boutique dans le scope. Les deux pools ne sont jamais
unionnes.
Alternatives écartées
Un champ audience: "platform" | "store" sur SkillMetadata.
C'etait l'option la moins chere, et elle a ete rejetee pour la raison
exacte qui fait de content/docs-internal/ un repertoire et non un
internal: true de front-matter : un drapeau est a une ligne oubliee de
publier une methode interne a des clients, et aucune faute de frappe ne
defait deux repertoires. Le precedent est dans ce depot, avec sa garde.
Gater par tier en ajoutant un troisieme palier. Cela aurait voulu
dire qu'un marchand sur Max 20x atteint la methode editoriale du
bulletin, et qu'un freelance qu'on recrute pour la QC creative ne
l'atteint pas : ni l'un ni l'autre n'a de plan qui dise quoi que ce soit
sur ce qu'on l'a embauche a faire. La question « ont-ils paye » et la
question « les a-t-on recrutes » n'ont pas la meme reponse et ne doivent
pas partager un champ.
Reutiliser skills/router.ts. Son pool part TOUJOURS de
registry.list(), la bibliotheque marchand, et compose les skills du
store par-dessus. L'appeler pour un tour interne aurait melange les deux
familles exactement la ou elles ne doivent pas se rencontrer : un
operateur interne serait routable vers pdp-conversion, et le menu du
classificateur aurait propose les skills d'un marchand a cote de la
methode du bulletin.
Un classificateur Haiku pour la famille plateforme. Ecarte pour l'instant : la bibliotheque est petite, fermee, et adressee par des gens qui travaillent ici dans un vocabulaire qu'on a choisi. Un appel paye par tour interne ambigu pour arbitrer entre cinq bundles, plus une seconde implementation de routage a tenir d'accord avec la premiere, ne se justifie pas. Si la bibliotheque grossit au point que ca ne tienne plus, on ajoute le classificateur ici, deliberement ; on n'emprunte pas celui du store.
Exposer ces skills en MCP. Non : aucun scope MCP ne les nomme et aucun outil ne les charge. Une methode interne atteignable par un client MCP serait exactement la fuite que la separation existe pour empecher.
Conséquences
- Une ambiguite de trigger se resout vers le fallback, jamais vers une
supposition : ces methodes portent des regles bloquantes (« publie ne
s'ecrit que d'une URL publique observee »), et la mauvaise a moitie
appliquee est pire que rien.
families-never-mix.test.tsrefuse donc qu'un trigger soit partage. - Un bundle dont la
permissionne resout pas n'est pas charge. Le sens sur est inverse de celui dutier: un tier mal orthographie retombe surstandardet reste disponible pour qui payait deja ; une permission mal orthographiee ne gaterait rien du tout. - Le tour de chat fait une lecture de plus en base
(
platformPermissionsFor) avant de choisir un skill. Assume, et de la meme nature que celle que le contexte d'outils documente deja : une permission mise en cache est une revocation qui n'existe pas. - Il reste vrai qu'un seul skill est actif par tour. Cette decision n'ouvre pas la composition.