ADRADR-0028 · Deux bibliotheques de skills, jamais une avec un drapeau

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 :

  1. les skills @Atlas sur disque (src/features/ai/skills/) : un prompt + une liste d'outils, un seul choisi par tour de chat ;
  2. les store skills (StoreContext.modules.skills) : les memes champs, ecrits par le marchand, charges par requete ;
  3. 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.ts refuse donc qu'un trigger soit partage.
  • Un bundle dont la permission ne resout pas n'est pas charge. Le sens sur est inverse de celui du tier : un tier mal orthographie retombe sur standard et 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.