MagicCode/README.md
2026-10-15 23:49:58 +02:00

296 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

![MAIA](screenshot.png)
# MAIA
Assistant IA en ligne de commande : un seul agent (MAIA), le **LLM** se choisit parmi ceux de LiteLLM,
l'interface utilise **Ink** (React pour CLI).
## Démarrer
```bash
pnpm install
mkdir -p ~/.maia && cp settings.example.json ~/.maia/settings.json # puis renseigner l'URL de LiteLLM
pnpm dev # le modèle est choisi dans la liste au démarrage
pnpm dev -- --model litellm:gpt-4o # nom du modèle tel que LiteLLM le connaît
pnpm test # test de la boucle agent (provider factice)
pnpm build && node packages/cli/dist/index.js
```
## Lancer MAIA depuis un autre dossier
`pnpm dev` s'exécute depuis la racine du dépôt : le dossier de travail est alors cette racine (`./MAIA.md`, `./.maia/skills`… y sont cherchés).
Pour utiliser MAIA sur un autre projet `./X`, lancez-le depuis `./X` :
```bash
pnpm build
cd packages/cli && pnpm link --global # une fois : installe la commande `maia`
cd ~/X && maia # le dossier de travail est ~/X
# ou sans installer : cd ~/X && node /chemin/vers/maia/packages/cli/dist/index.js
# ou en développement : MAIA_CWD=~/X pnpm --dir /chemin/vers/maia dev
```
Pourquoi pas `pnpm --filter … dev` : pnpm exécute alors le script depuis `packages/cli`, pas depuis le dossier où vous avez tapé la commande. MAIA
tient compte de `MAIA_CWD`, puis de `INIT_CWD` (dossier où pnpm/npm a été lancé), puis de `process.cwd()`.
## Architecture
| Paquet | Rôle |
| --- | --- |
| `core` | Contrats (`LLMProvider`, `Tool`, `AgentUI`…), boucle agent, sessions, `!`, `@fichier`, `§skill` |
| `providers` | Adaptateurs LLM : `litellm` (provider unique : requêtes au format OpenAI vers LiteLLM, voir ci-dessous) |
| `ui` | Interface Ink/React (journal, saisie, listes de choix, confirmations, spinner) |
| `agent` | L'agent MAIA : contexte (MAIA.md / CLAUDE.md), skills `.maia/skills` + outils Read/Glob/Grep/Write/Edit/Bash/TodoWrite/WebFetch/CurrentModel |
| `cli` | Point d'entrée : configuration, boucle de conversation |
Résolution du modèle (pas de défaut : s'il n'est pas défini, il est demandé) : option `--model` > variable `MAIA_MODEL`
> `.maia/config.json` (projet) > `~/.maia/config.json` > défauts.
Il n'y a plus de notion de mode ni d'extensions : un seul agent, MAIA.
## Commandes
| Saisie | Effet |
| --- | --- |
| `/help` | aide (commandes, skills) |
| `/permissions` | affiche les règles de permissions chargées, par fichier : badges `ALLOW`, `ASK`, `DENY` suivis des règles |
| `/model` | recharge la liste des modèles LiteLLM et change de modèle (conversation conservée) |
| `/clear` | supprime le contexte de la session et recharge `MAIA.md` / `CLAUDE.md` et les skills, comme au démarrage (la session garde son nom) |
| `/rename <nom>` · `/load [nom]` · `/new` | sessions (voir plus bas) |
| `/exit` (ou Ctrl+D) | quitte (la session est sauvegardée) |
| `!commande` · `@fichier` · `§skill` | commande shell, fichier injecté, skill (voir plus bas) |
Option de lancement : `--model <spec>` (ex. `litellm:gpt-4o`).
## Interface (Ink)
L'interface est une application React rendue par [Ink](https://github.com/vadimdemedes/ink) (Ink 7 : React ≥ 19.2 ; vérifiez la version de Node qu'il exige).
Le CLI garde une façade impérative (`InkUI` : `info`, `select`, `readInput`, `confirm`, `withSpinner`…) au-dessus d'un `UIStore` sans dépendance à React,
ce qui rend la logique testable (`store.test.ts`). Les composants (saisie, liste, confirmation, spinner) sont écrits à la main ; seuls les badges viennent de [`@inkjs/ui`](https://github.com/vadimdemedes/ink-ui).
Un terminal interactif (TTY) est requis.
Présentation : la bannière d'accueil reprend le style « MAIA » : masque pixel-art à gauche (demi-blocs ▀▄█, 20 × 20 pixels), titre « Maia » en gros caractères pixel avec ombre et dégradé arc-en-ciel à droite, puis deux textes (vert : « Assistant IA en ligne de commande… », rose : « Un seul agent, plusieurs modèles »). Terminal d'au moins 46 colonnes pour la version complète ; entre 25 et 45 : sans le masque ; en dessous : cadre à double bordure compact, titre centré ;
une ligne vide sépare chaque message.
Messages du journal, avec le composant `Badge` de `@inkjs/ui` :
| Badge | Usage | Texte |
| --- | --- | --- |
| `INFO` (bleu) | information | bleu clair (`LIGHT_BLUE`) |
| `WARN` (jaune) | avertissement | par défaut |
| `FAIL` (rouge) | erreur | `#DC8ADD` (`ERROR_TEXT`) |
| `ASK` (vert) | question posée à l'utilisateur (liste de choix, confirmation o/n), à gauche de la question | gras |
Les couleurs sont dans `packages/ui/src/colors.ts`. Une fois la réponse validée, la question est remplacée par la ligne `◆ Question : choix` dans le journal.
La zone de saisie est encadrée par une bordure en pointillés (┈) **au-dessus et au-dessous** seulement, en `greenBright`, sans bords gauche ni droit.
À sa gauche, le spinner `arrow3` de [cli-spinners](https://github.com/sindresorhus/cli-spinners) s'anime en continu. Pendant l'attente de la réponse après validation de la saisie, « En réflexion » s'affiche avec le spinner `dots10` à sa gauche, en `yellowBright` (même spinner pour les autres attentes : chargement des modèles, exécution d'une commande `!`).
| Contexte | Touches |
| --- | --- |
| Saisie | Entrée : envoyer · ←/→, Ctrl+A/E : se déplacer · ↑/↓ : historique · Ctrl+U/W : effacer · Ctrl+C ou Ctrl+D (saisie vide) : quitter · le collage multi-lignes est conservé |
| Liste (`/model`, `/load`, choix initial) | ↑/↓ (ou j/k) · Entrée : valider · Échap : annuler |
| Confirmation d'une action (Write, Edit, Bash…) | `o` : autoriser · `n` ou Échap : refuser · Entrée seule est ignorée (réponse explicite exigée) |
| Pendant un tour de l'agent | Ctrl+C : interrompt la requête et restaure la conversation avant cette demande |
## Dossier de base
Le dossier où MAIA est lancé (ex. `./X`) est la base de **tous** les chemins relatifs : fichiers lus, créés ou modifiés par les outils
(Read, Write, Edit, Glob, Grep, Bash), `.maia/config.json`, `MAIA.md` / `CLAUDE.md` et `.maia/skills/`. Un chemin complet est utilisé tel quel
(`~/…` désigne le dossier personnel). Ce dossier est capturé une fois au démarrage, affiché (« Dossier de travail : … ») et transmis aux outils et aux providers ;
les règles de permissions (`Read(./.env)`, `Edit(src/**)`) sont elles aussi relatives à ce dossier. Seul `~/.maia` (config, `settings.json` et skills utilisateur) est lu depuis le dossier personnel.
## Commande `!` : exécuter une commande shell
`!commande` exécute la commande **directement**, sans passer par le modèle. Deux syntaxes :
| Saisie | Commande exécutée |
| --- | --- |
| `!ls` · `!ls -la src` | tout ce qui suit le `!` |
| `!'ls -la'` | le texte entre guillemets simples (délimite la commande avec ses paramètres) |
| `!'ls' -la src` · `!'ls -la' \| wc -l` | le texte entre guillemets, puis ce qui suit est ajouté comme paramètres : `ls -la src`, `ls -la \| wc -l` |
| `!'echo "l\'ami"'` | `\'` donne un guillemet simple littéral dans la commande |
| `!'ls -la` | erreur (guillemet non fermé) : rien n'est exécuté |
Le reste du comportement :
- elle tourne dans le dossier de lancement (voir « Dossier de base ») ; un `cd` n'a donc pas d'effet durable ;
- stdout et stderr sont affichés dans l'ordre d'arrivée, avec le code de sortie en cas d'échec ; durée maximale 2 minutes ;
- aucune confirmation n'est demandée (c'est vous qui la tapez) et les règles de permissions des outils ne s'appliquent pas ;
- le résultat est ajouté au contexte de **votre prochaine demande** (jusqu'à 30 000 caractères, milieu tronqué au-delà), pour que le modèle sache ce qui a été lancé. `/clear` l'efface ;
- Ctrl+C interrompt la commande ; `!` seul affiche l'aide. Les programmes interactifs (vim, less, saisie au clavier…) ne sont pas pris en charge.
## Références `@fichier`
`@chemin/vers/fichier` dans votre message **injecte le contenu du fichier** dans le contexte de la demande (en plus de le laisser dans le texte) :
| Saisie | Effet |
| --- | --- |
| `explique @src/a.ts` | injecte `src/a.ts` |
| `compare @a.ts et @ b.ts` | plusieurs fichiers ; la forme `@ chemin` (avec espace) est acceptée |
| `lis @'mon dossier/a b.txt'` | guillemets simples pour un chemin avec espaces (`\'` : guillemet littéral) |
| `résume @docs` | un dossier : la liste de ses entrées est injectée |
| `@/chemin/complet.txt` · `@~/notes.md` | chemin complet, ou depuis le dossier personnel |
- Chemins relatifs au dossier de lancement (voir « Dossier de base »). Aucune confirmation : c'est vous qui citez le fichier, les règles de permissions des outils ne s'appliquent pas.
- Un `@mot` qui ne désigne aucun fichier existant reste du texte ordinaire (`@mention`, `@Override`, `jean@exemple.fr`) ; un avertissement n'apparaît que s'il ressemble à un chemin (`@src/x.ts` introuvable).
- Limites : 2 Mo par fichier, 50 000 caractères injectés par fichier (milieu tronqué au-delà) et 150 000 par message ; les fichiers binaires sont ignorés ; un même fichier cité plusieurs fois n'est injecté qu'une fois.
- Le contenu est marqué comme donnée, pas comme instructions. Les `@fichier` écrits *dans* le corps d'une commande personnalisée ne sont pas encore traités (seuls ceux de votre saisie le sont).
- Pas d'autocomplétion des chemins à la saisie pour l'instant.
## Sessions (`./.maia/.sessions`)
Au lancement, `./.maia` et `./.maia/.sessions` sont créés dans le dossier d'exécution s'ils n'existent pas. Les sessions sont une fonctionnalité de l'application.
- **Sauvegarde automatique à la sortie** (`/exit`, Ctrl+D, SIGTERM/SIGHUP). Une session sans aucun message n'est pas enregistrée.
- **Nom par défaut** : `session_{date}_{heure}_{minutes}_{secondes}` (ex. `session_2026-10-13_01_04_05`).
- `/rename <nom>` : renomme la session courante ; ce nom sera utilisé à la sauvegarde.
- `/load` : sans argument, liste les sessions enregistrées (sélection) ; `/load <nom>` recharge cette session (le modèle courant est conservé).
- `/new` : nouvelle session, nouveau contexte (`MAIA.md` / `CLAUDE.md` et skills rechargés) ; l'utilisateur est averti que la session quittée est sauvegardée (sous son nom).
Chaque session est un fichier JSON : elle peut contenir des extraits de fichiers ou de sorties de commandes. `.maia/` est ajouté au `.gitignore`.
## Skills : appel direct avec `§`
`§nom_du_skill [arguments]` lance un skill de l'agent (ex. `§sk_test`) ; `§` seul liste les skills disponibles.
L'agent expose ses skills via `ModeInitResult.skills` (contrat `AgentSkill` : `name`, `description`, `invoke(args)`), ce qui
rend l'appel indépendant du format propre à l'agent. `invoke` retourne le prompt envoyé au LLM. Ce prompt contient
les instructions du `SKILL.md` (`.maia/skills/<nom>/`) (avec `$ARGUMENTS` / `ARGUMENTS:`), sans passer par l'outil `Skill` du modèle.
## Accès aux LLMs : LiteLLM
MAIA ne parle qu'à un serveur [LiteLLM](https://docs.litellm.ai) : **toutes les requêtes de tous les agents sont envoyées au format OpenAI**
(`chat/completions`, en streaming, avec appels d'outils) vers le endpoint OpenAI de LiteLLM.
Au démarrage, le fichier **`~/.maia/settings.json`** (dossier de l'utilisateur, pas celui de lancement) est chargé :
```json
{
"LiteLLM_URL": "http://localhost:4000",
"OpenAI_EndPoint": "/v1/chat/completions",
"LiteLLM_Models": "/v1/models"
}
```
- `LiteLLM_URL` : URL du serveur LiteLLM.
- `OpenAI_EndPoint` : suffixe ajouté à `LiteLLM_URL` pour envoyer une requête au format OpenAI.
- `LiteLLM_Models` : suffixe ajouté à `LiteLLM_URL` pour récupérer la liste des modèles disponibles.
- Optionnel : `LiteLLM_API_Key` (ou variable d'environnement `LITELLM_API_KEY`), envoyée en `Authorization: Bearer …`. Ce fichier peut donc contenir un secret : ne pas le versionner.
Si le fichier est absent ou incomplet, MAIA s'arrête avec un message qui en donne un exemple.
Choix du modèle : au démarrage (sauf si `--model` ou `MAIA_MODEL` le définit), MAIA récupère les modèles auprès de LiteLLM
et demande de choisir celui de la session. `/model` rouvre cette liste en cours de session (la conversation est conservée).
Spécification explicite : `litellm:<nom du modèle>` (ou le nom seul).
## Agent MAIA : contexte et skills
Au lancement, le contexte de départ est chargé depuis le **dossier de lancement** : `MAIA.md` s'il existe, sinon `CLAUDE.md` (s'il existe).
Le fichier chargé est indiqué par un message `INFO` ; si aucun des deux n'existe, un `WARN` le signale. Seul ce dossier est regardé (pas les dossiers parents).
Les dossiers `.claude` ne sont **plus lus** (ni `settings.json`, ni `commands/`, ni `skills/`, ni `.claude/CLAUDE.md`, ni `~/.claude`).
- **Skills** : `.maia/skills/<nom_du_skill>/SKILL.md` (ou `skill.md`), chargés depuis trois emplacements. Le plus spécifique l'emporte en cas de même nom :
1. le dossier utilisateur : `~/.maia/skills` ;
2. le dossier parent du dossier de lancement : ex. `/X/.maia/skills` pour un lancement dans `/X/Y` ;
3. le dossier de lancement : ex. `/X/Y/.maia/skills`.
Le fichier commence par un en-tête YAML : `description` (objectif du skill) et `name` (optionnel ; s'il est présent, il doit être identique au nom du dossier, qui fait foi).
Seule la liste (nom + description) est mise dans le prompt ; le contenu est chargé à la demande par l'outil `Skill`, ou lancé directement avec `§nom`.
```
---
name: sk_test # optionnel, = nom du dossier
description: Lance les tests du projet et résume les échecs
---
Instructions du skill… ($ARGUMENTS = arguments passés avec §sk_test)
```
Conséquence : il n'y a plus de variables d'environnement venant de la configuration de l'agent ni de slash-commandes personnalisées. Les permissions sont décrites dans la section suivante.
## Permissions
Les permissions s'appliquent aux **outils** (intégrés ou non). Trois types de règles :
| Type | Effet |
| --- | --- |
| `allow` | l'outil est utilisé sans approbation manuelle |
| `ask` | l'utilisateur doit confirmer chaque utilisation (badge `ASK`, réponse `o`/`n`) |
| `deny` | l'outil ne peut pas être utilisé (l'agent reçoit un refus) |
**Ordre d'évaluation : deny, puis ask, puis allow.** La première famille qui correspond décide, quelle que soit la précision des règles : un `allow` étroit comme `Bash(aws s3 ls)` ne fait pas exception à un `deny` large comme `Bash(aws *)`, ni à un `ask` plus large. **Sans règle applicable, c'est `ask` qui s'applique** (pour tous les outils, y compris Read, Glob, Grep…).
Où les définir : `~/.maia/settings.json` (utilisateur) et `./.maia/settings.json` (dossier de lancement). Les listes des deux fichiers s'additionnent. Le fichier `../.maia/settings.json` du dossier parent n'est **pas** pris en compte. Dans chaque fichier, la propriété `permissions` contient une liste par type, tous facultatifs :
```json
"permissions": {
"allow": ["Bash(allow stop *)"],
"ask": ["Bash(allow ask *)"],
"deny": ["Bash(allow deny *)", "Read(./.deny.md)"]
}
```
Format d'une règle : `Outil` (tous les appels de l'outil) ou `Outil(motif)` :
| Outil | Le motif porte sur | Exemples |
| --- | --- | --- |
| `Bash` | la commande ; `*` = n'importe quelle suite ; `cmd *` couvre `cmd` avec ou sans arguments (mais pas `cmdx`) ; sans `*` : commande exacte | `Bash(allow stop *)`, `Bash(git status)`, `Bash(npm run test:*)` |
| `Read`, `Write`, `Edit`, `Glob`, `Grep` | le chemin (relatif au dossier de lancement, motifs glob) | `Read(./.deny.md)`, `Edit(src/**)` |
| `WebFetch` | le domaine | `WebFetch(domain:docs.example.com)`, `WebFetch(domain:*.example.org)` |
| `Skill` | le nom du skill | `Skill(pdf)` |
| autres outils (ex. ajoutés plus tard) | une valeur texte de l'entrée de l'outil (`*` accepté) | `mcp__db__query(SELECT *)` |
Précisions sur `Bash` :
- commande composée (`&&`, `;`, `|`) : un `deny` ou un `ask` s'applique dès qu'**une** des parties correspond, y compris cachée dans `$(…)`, des backticks ou des parenthèses ; un `allow` ne s'applique que si **toutes** les parties sont couvertes ;
- un `allow` ne couvre jamais une commande avec redirection (`>`) ou substitution (`$(…)`, backticks) : elle repasse en `ask`.
Au démarrage, MAIA indique (`INFO`) les fichiers chargés et le nombre de règles, ou qu'aucune règle n'est définie ; un JSON invalide ou une règle mal formée donne un `WARN` et est ignoré. Les `deny` sont du « meilleur effort » : ils ne remplacent pas un vrai bac à sable. Les commandes `!` et les références `@fichier` tapées par l'utilisateur ne passent pas par ces règles.
Attention : `./.maia/settings.json` d'un dépôt tiers peut contenir des règles `allow` larges ; relisez-le avant de lancer MAIA dans un projet que vous ne connaissez pas.
La commande `/permissions` affiche les règles actives, fichier par fichier, avec un badge `ALLOW` (vert), `ASK` (jaune) et `DENY` (rouge) suivi des règles du type, une par ligne ; seuls les types non vides sont montrés. Après avoir modifié un `settings.json`, `/clear` recharge la configuration.
Exemple pour éviter les confirmations sur les outils en lecture seule : voir `settings.example.json`.
## Outils TodoWrite, WebFetch et CurrentModel
- **TodoWrite** : liste de tâches de la session (`pending` / `in_progress` / `completed`, forme `activeForm` pour la tâche en cours). Chaque appel remplace la liste ; le résultat (☐ ▶ ☑) est affiché à l'utilisateur et renvoyé au modèle. Un avertissement est ajouté si plusieurs tâches sont « en cours ».
- **CurrentModel** : renvoie le modèle LLM utilisé par la session (valeur à jour après un `/model`). Sans paramètre, sans confirmation ; le modèle est lu au moment de l'appel.
- **WebFetch** : télécharge une URL http(s) (HTML converti en Markdown avec `turndown`, JSON mis en forme, texte brut ; 5 Mo et 30 s maximum, `max_length` caractères renvoyés). Pas de JavaScript exécuté, pas de PDF ni d'images.
- Confirmation demandée par défaut ; règle `allow` possible : `WebFetch(domain:docs.example.com)` (voir « Permissions »).
- Une redirection vers un **autre domaine** n'est pas suivie : l'outil renvoie la nouvelle URL, que le modèle doit rappeler (ce qui repasse par la confirmation).
- Le contenu est signalé au modèle comme non fiable (risque d'injection de prompt) ; il ne passe par aucun second modèle de résumé.
## Reste à faire
- Provider LiteLLM : validation sur un vrai serveur (streaming SSE et appels d'outils testés seulement sur des flux simulés) ; compatibilité de `@inkjs/ui` avec Ink 7 à confirmer à l'usage (l'affichage des badges a été vu fonctionner par l'utilisateur).
- Agent : sous-agents + outil Task, hooks, serveurs MCP.
- Éventuellement rétablir sous `.maia` ce que supprimait l'abandon de `.claude` : variables d'environnement et slash-commandes personnalisées (le code des commandes existe encore dans `packages/agent/src/commands.ts`, il n'est plus appelé).
- `@fichier` dans le corps d'une commande personnalisée ; autocomplétion des chemins `@` à la saisie.
- Liste à cocher (choix multiples) : n'existe pas encore ; elle recevrait le badge `ASK`.
- Nerd Fonts (icônes) : proposé comme réglage optionnel dans `settings.json`, non réalisé.
- Tests : les suites vitest et le typecheck sont à lancer après `pnpm install` (écrits sans avoir pu être exécutés dans l'environnement de développement).
## Travaux réalisés
Journal des évolutions, dans l'ordre :
1. **Squelette** : monorepo pnpm (TypeScript strict, tsup, vitest), contrats `LLMProvider` / `Tool` / `AgentUI`, boucle agent avec appels d'outils, agent par défaut de type « Claude Code » (outils Read, Write, Edit, Bash).
2. **Provider SAP AI Core** puis **choix du modèle** au démarrage et commande `/model`. *(Remplacé ensuite par LiteLLM, voir 12.)*
3. **Outils Glob et Grep** (JavaScript pur, sans ripgrep).
4. **Configuration de l'agent (lot 2)** : fichiers de contexte, permissions, slash-commandes, skills. *(Abandonné avec les dossiers `.claude`, voir 13.)*
5. **`§skill`** : appel direct d'un skill de l'agent.
6. **Dossier de lancement** : tous les chemins relatifs partent du dossier où MAIA est lancé (`MAIA_CWD` > `INIT_CWD` > `cwd`).
7. **Interface Ink** (remplace Clack.js) : bannière « MAIA » avec masque pixel-art et titre arc-en-ciel, messages en bleu clair, zone de saisie en pointillés `greenBright` avec spinner `arrow3`, attente « En réflexion » avec `dots10` en `yellowBright`.
8. **Outils TodoWrite et WebFetch** (lot 3, première partie).
9. **`!commande`** (avec la syntaxe `!'commande avec paramètres'`) et **`@fichier`**.
10. **Renommage en MAIA** (titre « Maia ») et **sessions** : `./.maia/.sessions`, sauvegarde automatique, `/rename`, `/load` (avec réaffichage du contenu), `/new`.
11. **Corrections** : import manquant de `normalizeSessionName`, `session.tools` est une `Map`, en-tête de skill avec espace avant les deux-points (`description : …`).
12. **LiteLLM** : un seul provider, requêtes au format OpenAI en streaming vers le endpoint OpenAI de LiteLLM ; configuration `~/.maia/settings.json` (`LiteLLM_URL`, `OpenAI_EndPoint`, `LiteLLM_Models`, `LiteLLM_API_Key` optionnel). Suppression de SAP AI Core, Anthropic, OpenAI et Ollama.
13. **Renommage technique** `magiccode` → `maia` (packages `@maia/*`, commande `maia`, variables `MAIA_*`, dossier `.maia`) ; textes « MagicCode » → « MAIA » ; bannière « Un seul agent, plusieurs modèles ».
14. **Skills dans `.maia/skills`** (dossier de lancement, dossier parent, dossier utilisateur) ; les dossiers `.claude` ne sont plus lus.
15. **Badges `@inkjs/ui`** : `INFO`, `WARN`, `FAIL` (texte d'erreur `#DC8ADD`) et `ASK` devant les questions.
16. **Suppression des modes et des extensions** : un seul agent MAIA (`packages/agent`), contexte de départ `MAIA.md` sinon `CLAUDE.md` (message `INFO`, ou `WARN` si aucun).
17. **Outil `CurrentModel`** : indique au modèle quel LLM est utilisé dans la session.
18. **`/clear`** : supprime le contexte et recharge `MAIA.md` / `CLAUDE.md` (et les skills) comme au démarrage ; `/new` fait de même.
19. **Permissions granulaires** : règles `allow` / `ask` / `deny` par outil (`Outil` ou `Outil(motif)`), évaluées dans l'ordre deny > ask > allow, `ask` par défaut ; définies dans `~/.maia/settings.json` et `./.maia/settings.json` (pas le dossier parent).
20. **`/permissions`** : affiche les permissions chargées, par fichier et par type (badges ALLOW / ASK / DENY) ; les fichiers sont listés l'un sous l'autre au démarrage.