Configurer pi en profondeur & le trust en headless
L'annexe ouvre le capot du harnais pi : où vivent ses réglages, comment global et projet se fusionnent, par quel chemin une clé atteint le modèle — et pourquoi, en headless, un projet non approuvé ignore silencieusement vos réglages. La pièce du jour pose le premier fichier sous .pi/ de plume-factory.
Depuis le chapitre 7, votre usine appelle pi par un seul chemin, l’adaptateur de harness.py,
et tout ce que le harnais sait de vous tient dans quelques drapeaux : --model, --thinking,
--tools, --session-id. Pourtant, quand vous ouvrez pi à la main dans plume-factory pour
lire une trace ou lancer l’orchestrateur du chapitre 25, il démarre sur un modèle qui n’est pas
celui de votre roster, ne connaît pas vos skills, et vous demande si vous faites confiance au
dossier. Cette annexe fait de vous le propriétaire de votre harnais. À la fin de ce chapitre, vous
saurez où pi range ses réglages, comment le global et le projet se fusionnent, par quel chemin une
clé atteint un modèle, et surtout pourquoi un run headless dans un projet non approuvé ignore
silencieusement vos réglages projet, le piège qui rend un run non reproductible d’une machine à
l’autre. La pièce du jour est le premier fichier sous .pi/ de plume-factory :
.pi/settings.json. Elle ne déplace pas la couture : le runner Python des chapitres 1 à 27 reste
le propriétaire du graphe. L’annexe rend le nœud pi plus prévisible aujourd’hui, plus sûr (A3),
plus observable (A4) et plus transportable (A5). Une précision, valable pour les cinq chapitres de
l’annexe : ils se placent entièrement du côté pi de la couture. Claude Code a ses propres fichiers
(.claude/settings.json) et ses propres mécanismes. L’adaptateur _run_claude de harness.py
n’en a pas besoin pour fonctionner, et l’annexe ne le modifie pas.
Settings global et projet, providers et clés
L’idée en une phrase
pi lit deux fichiers JSON : ~/.pi/agent/settings.json pour le global, .pi/settings.json
pour le projet. Il fusionne le second par-dessus le premier, objet par objet. Les
credentials, eux, suivent un ordre de résolution séparé (drapeau, fichier d’auth, variable
d’environnement). Cette pièce vit dans le poste de pilotage, côté code déterministe :
elle est lue avant qu’un seul jeton ne soit dépensé.
Points clés
- Deux niveaux, une fusion. Le global s’applique à tous vos projets, le projet le surcharge.
Les objets imbriqués se fusionnent clé par clé : un global
compaction.enabled: trueet un projetcompaction.reserveTokens: 8192donnent les deux réglages. Une exception nette : un tableaudefaultToolsprojet remplace le tableau global, il ne s’y ajoute pas. - Trois réglages font le modèle de démarrage :
defaultProvider(par exempleopenrouter),defaultModel(l’id tel que le fournisseur le nomme,z-ai/glm-5.3au moment d’écrire) etdefaultThinkingLevel(offàmax).modelThinkingLevelsaffine par modèle, avec des clésprovider/modelId. Ces réglages ne valent que pour le démarrage :--modelet--thinkingen ligne de commande gagnent toujours, c’est ce qui permet à votre adaptateur d’imposer le roster. - Les ressources se déclarent ici aussi.
skills,extensions,prompts,themes,packages: des chemins résolus relativement à.pi/pour le fichier projet, à~/.pi/agent/pour le global."skills": ["../.claude/skills"]charge vos skillsfactoryetfactory-orchestratordes chapitres 25-26 sans--skillà chaque lancement. - Une clé se résout dans un ordre fixe :
--api-keysur la ligne de commande, puis l’entrée du fichier~/.pi/agent/auth.json(créé en0600par/login), puis la variable d’environnement (OPENROUTER_API_KEYpour la passerelle), puis les clés déclarées dans unmodels.jsonpersonnalisé. Retenez la surprise : le fichier d’auth passe avant l’environnement. - pi ne lit pas
.env. Vous le saviez depuis le chapitre 15 : c’est_load_env()dansharness.pyqui charge.envdans l’environnement du process avant de lancer le sous-process. En interactif, sans le runner, la clé doit venir de/login openrouter(option « clé d’API », rangée dansauth.json) ou d’unexportde session.
Exemple concret
Vous ouvrez pi dans plume-factory sans rien configurer. Il démarre sur le modèle de votre
global, disons un modèle frontier, et vous tapez « liste les rosters » : quelques milliers de
jetons, quelques centimes, sur un moteur dix fois plus cher que le workhorse de votre roster
éco. Rien de grave, mais ce n’est pas votre usine qui a choisi. Vous posez la pièce du jour :
defaultProvider: openrouter, defaultModel: z-ai/glm-5.3, defaultThinkingLevel: medium,
skills pointant sur ../.claude/skills. Vous relancez pi, approuvez le projet une fois
(/trust), et le même « liste les rosters » tourne sur GLM à 1,40 $ le million de jetons en
entrée, 4,40 $ en sortie au moment d’écrire, moins d’un centime, avec /skill:factory
disponible. Zéro jeton dépensé pour poser la pièce. Le gain se mesure sur chaque session
interactive qui suit.
Ce que chaque niveau décide
| Question | Global ~/.pi/agent/settings.json | Projet .pi/settings.json | Ligne de commande |
|---|---|---|---|
| Modèle et réflexion de démarrage | oui, pour tous vos projets | oui, surcharge le global | --model, --thinking gagnent |
| Skills, extensions, prompts | chemins relatifs à ~/.pi/agent/ | chemins relatifs à .pi/ | --skill, -e s’ajoutent |
Confiance dans un projet (defaultProjectTrust) | seul niveau autorisé | ignoré ici | --approve / --no-approve pour un run |
| Clé d’API | jamais (voir auth.json) | jamais — un secret ne se commite pas | --api-key gagne sur tout |
| Compaction, retry | valeurs par défaut de votre poste | ajustées au payload | — |
Config — le global de votre poste
Le fichier global n’entre pas dans le repo : il décrit votre poste, pas l’usine. Posez-y ce que
le projet n’a pas le droit de décider, la politique de confiance, et vos valeurs par défaut
personnelles. Créez-le si ~/.pi/agent/settings.json n’existe pas encore.
{
"defaultProvider": "openrouter",
"defaultModel": "z-ai/glm-5.3",
"defaultThinkingLevel": "medium",
"defaultProjectTrust": "ask",
"quietStartup": false
}
defaultProjectTrust: "ask" est la valeur par défaut. Vous la voyez écrite pour vous souvenir
qu’elle existe et qu’elle est globale uniquement. Un projet ne peut pas s’auto-approuver, et
c’est précisément ce que le second sous-thème exploite.
Piège courant : « ma clé est dans
.env, donc pi la voit » est inexact. pi ne lit aucun.env, seul le runner de l’usine le charge, par_load_env()dansharness.py. Et l’inverse vous guette sur votre poste : si/login openroutera rangé une clé dansauth.json, elle passe devant la variable que.enva exportée. Une clé jetable du chapitre 23 mise dans.envpeut donc être ignorée sur votre poste, alors qu’elle sera bien utilisée dans une boîte du hors-site (Podman ou exe.dev), où aucunauth.jsonn’existe. Quand la trace vous montre un coût sur la mauvaise clé, regardezauth.jsonavant de suspecter le roster.
Trust, compaction et retry en mode headless
L’idée en une phrase
Un projet qui contient .pi/settings.json, .pi/extensions/, .pi/skills/ ou
.pi/SYSTEM.md exige une approbation (project trust) avant que pi ne charge ses réglages
et n’exécute ses extensions. En headless (-p, --mode json, --mode rpc) pi ne pose
jamais la question et, sans décision enregistrée, suit defaultProjectTrust, dont la valeur
par défaut ask signifie ignorer le projet. La compaction et le retry sont les deux
autres réglages qui décident du comportement d’un nœud pi sans humain devant : ils vivent tous
trois côté code déterministe, dans le harnais, sous le runner.
Points clés
- La confiance est une décision enregistrée, pas un réglage projet.
/trusten interactif écrit la décision dans~/.pi/agent/trust.json, par dossier canonique. La décision la plus proche sur le chemin (dossier courant ou parent) s’applique ensuite à tous les modes, headless compris. Une extension globale peut aussi répondre à l’événementproject_trust, vous y reviendrez au chapitre A2. - Sans décision enregistrée, le headless ne demande rien.
asketneverignorent les ressources projet :.pi/settings.jsonn’est pas lu, les extensions ne se chargent pas, et pi ne le dit pas.alwaysles charge.--approve(-a) et--no-approve(-na) tranchent pour un seul run. Votre adaptateur_run_pine passe aucun des deux aujourd’hui : c’est un choix, la confiance est une décision du poste, pas du runner. - Ce qui se charge malgré tout : les fichiers de contexte (
AGENTS.md,CLAUDE.md), les extensions globales et celles passées par-e. Le trust n’est pas un bac à sable, la doc de pi le répète, il empêche seulement un dépôt de modifier votre harnais sans votre accord. - La compaction se déclenche quand les jetons du contexte dépassent la fenêtre moins
compaction.reserveTokens(16 384 par défaut). pi résume les anciens messages et garde leskeepRecentTokens(20 000) les plus récents. Dans une phase de l’usine, courte par construction, elle se déclenche rarement. Dans la boucle de correction du chapitre 13, même session et plusieurs allers-retours, elle peut s’inviter, et chaque résumé est un appel modèle. - Deux étages de retry, deux responsables.
retry.maxRetries(3, backoff 2 s, 4 s, 8 s) est celui du harnais : erreurs transitoires du fournisseur, quelques secondes. Le retry de l’usine, une gate rouge renvoyée en enveloppe ou une reprise en session vivante du chapitre 18, appartient au runner. La doc conseille de laisserretry.provider.maxRetriesà0: un SDK qui réessaie à votre place peut masquer une limite d’usage et bloquer l’agent jusqu’au reset du quota.
Exemple concret
Vous stampez l’usine dans une boîte du hors-site (chapitre 26) et lancez adw_sdlc : le run passe,
la trace est propre, mais les phases ont tourné sans le skill factory et sans votre
defaultThinkingLevel : dans la boîte, ~/.pi/agent/trust.json n’existe pas, le global est
vierge, defaultProjectTrust vaut ask, et le headless a ignoré .pi/settings.json sans un
mot. Sur votre poste, où /trust a enregistré la décision, le même run charge tout. Deux
machines, deux comportements, zéro message d’erreur : c’est le contraire de la reproductibilité
que l’usine promet. La correction tient en une ligne dans la recette setup de la boîte
(chapitre 22) : écrire un global minimal avec defaultProjectTrust: "always", acceptable parce
que la boîte est jetable et ne contient que votre dépôt. Coût : zéro jeton, quelques
millisecondes. Ce que vous évitez, c’est un run entier, quelques dizaines de centimes à
quelques dollars, dont les chiffres ne se comparent à rien.
Le headless selon la décision de confiance
| Situation | .pi/settings.json lu ? | Extensions projet ? | Comment y arriver |
|---|---|---|---|
Décision yes dans trust.json (dossier ou parent) | oui | oui | /trust une fois, en interactif |
Aucune décision, global ask (défaut) ou never | non, en silence | non | — c’est l’état d’une machine neuve |
Aucune décision, global always | oui | oui | à réserver aux boîtes jetables |
pi -p --approve … | oui, pour ce run | oui | un run de diagnostic |
pi -p --no-approve … | non, pour ce run | non | vérifier ce que vaut le global seul |
Commande — voir ce que le headless a vraiment chargé
Les deux commandes ci-dessous sont les mêmes dans bash et PowerShell. Seule la version pi
existe ici : Claude Code n’a pas cette notion d’approbation de projet en headless, et l’annexe
ne modifie pas l’adaptateur _run_claude.
# Avec la décision de confiance en place : provider et model sont ceux de .pi/settings.json
pi -p --mode json "ping"
# En ignorant le projet pour ce run : provider et model retombent sur le global
pi -p --mode json --no-approve "ping"
Dans le flux JSONL, l’événement message_end porte un message assistant avec ses champs
provider et model, les mêmes que votre adaptateur lit depuis le chapitre 7 pour le coût. Si
les deux commandes affichent le même modèle alors que le projet en déclare un autre, la décision
de confiance n’est pas enregistrée.
Piège courant : « je mets
defaultProjectTrust: alwaysdans.pi/settings.jsondu repo » est inexact. Ce réglage est global uniquement, et pi l’ignore dans un fichier projet. Sinon n’importe quel dépôt cloné s’approuverait lui-même et exécuterait ses extensions au premierpi -p. La confiance vient toujours de votre poste :/trust, le global, ou--approvepour un run.
Fil rouge — la pièce posée aujourd’hui
Sur le plan de l’usine, la pièce se pose dans le poste de pilotage, sous le port harnais du
chapitre 7 : .pi/settings.json décrit le nœud pi tel qu’il démarre quand personne ne lui passe
de drapeau : vos sessions interactives, l’orchestrateur du chapitre 25, un pi -p de diagnostic.
Le runner, lui, continue d’imposer le roster par --model, --thinking, --tools et
--session-id : la couture ne bouge pas, l’agent propose, le code dispose, et aucune enveloppe
nouvelle ne traverse. Ce que la pièce ajoute, c’est la reproductibilité du nœud : même modèle
de démarrage, mêmes skills, même politique de compaction et de retry, sur votre poste comme dans
une boîte, à condition que la décision de confiance soit enregistrée, ce que la gate du jour
vérifie. Coût : zéro jeton pour la pièce, moins d’un centime pour la gate. Ce qu’elle
économise, c’est chaque session interactive démarrée sur un moteur frontier par défaut, et chaque
run hors-site dont les réglages ne correspondaient pas à ceux du poste.
Travaux pratiques — la pièce du jour
Une pièce complète à poser dans le repo compagnon plume-factory, qui devient, chapitre après
chapitre, votre usine logicielle agentique.
Prérequis de l’annexe, annoncés une fois : pi installé (npm install -g --ignore-scripts @earendil-works/pi-coding-agent, doc sur https://pi.dev), Bun déjà présent depuis le
chapitre 2, et un TypeScript que vous savez lire. Les pièces des chapitres A2 à A5 sont livrées
complètes, comme toujours. Presque tous les TP de l’annexe coûtent zéro jeton, seules les gates
qui lancent un tour headless coûtent quelques centimes.
Pièce — .pi/settings.json
Le premier fichier sous .pi/ de plume-factory. Il fixe le modèle et la réflexion de démarrage
sur le workhorse de votre roster éco (chapitre 17), branche les skills des chapitres 25-26,
resserre la compaction pour un payload volontairement petit, et garde le retry du harnais court
pour laisser au runner ses reprises. Côté code déterministe, lu avant tout jeton. L’adaptateur
_run_pi du chapitre 15 continue de passer ses drapeaux, qui gagnent sur chaque valeur ci-dessous.
Les identifiants de modèle sont ceux vérifiés au moment d’écrire, remplacez-les par ceux de votre
roster du jour.
{
"defaultProvider": "openrouter",
"defaultModel": "z-ai/glm-5.3",
"defaultThinkingLevel": "medium",
"modelThinkingLevels": {
"openrouter/z-ai/glm-5.3": "medium",
"openrouter/deepseek/deepseek-v4-flash-0731": "low"
},
"enabledModels": [
"openrouter/z-ai/glm-5.3",
"openrouter/deepseek/deepseek-v4-flash-0731"
],
"skills": ["../.claude/skills"],
"quietStartup": true,
"compaction": {
"enabled": true,
"reserveTokens": 16384,
"keepRecentTokens": 12000
},
"retry": {
"enabled": true,
"maxRetries": 2,
"baseDelayMs": 2000,
"provider": {
"maxRetries": 0,
"maxRetryDelayMs": 60000
}
}
}
Pièce — .pi/README.md
Une note d’un écran, commitée à côté des réglages, pour que la personne qui clone le dépôt (ou la boîte qui le reçoit par le stamp du chapitre 26) sache ce que pi ne fera pas tout seul.
# Le nœud pi de l'usine
Ce dossier configure le harnais pi pour `plume-factory`. Le runner (`adws/`) reste le
propriétaire du graphe : il impose modèle, réflexion, outils et session par ses drapeaux.
Ce qui est ici vaut pour tout ce qui démarre SANS ces drapeaux : sessions interactives,
orchestrateur, diagnostics `pi -p`.
## Trois choses que pi ne fait pas seul
1. **Lire `.env`.** Seul le runner le charge (`_load_env()` dans `adws/adw_modules/harness.py`).
En interactif : `/login openrouter` puis « clé d'API » (rangée dans `~/.pi/agent/auth.json`),
ou un `export OPENROUTER_API_KEY=...` de session. Attention : `auth.json` passe AVANT la
variable d'environnement.
2. **Approuver ce dossier.** `.pi/settings.json` exige une décision de confiance. Sur un poste :
lancer `pi` ici une fois, puis `/trust`. En headless sans décision, pi IGNORE ce fichier
en silence (`defaultProjectTrust: ask` par défaut, réglage global uniquement).
Dans une boîte jetable : écrire `~/.pi/agent/settings.json` avec `"defaultProjectTrust": "always"`.
3. **Réessayer une gate rouge.** Le `retry` d'ici ne couvre que les erreurs transitoires du
fournisseur. Les reprises de l'usine appartiennent au runner (`adws/adw_modules/runner.py`).
## Vérifier
pi -p --mode json "ping" # provider/model = ceux de ce fichier
pi -p --mode json --no-approve "ping" # provider/model = ceux du global
La gate du TP
Depuis la racine de plume-factory, après avoir lancé pi une fois et répondu /trust. Quatre
commandes, une par ligne, identiques dans bash et PowerShell :
pi --version
pi -p --mode json "ping"
pi -p --mode json --no-approve "ping"
pi -p "Liste les skills disponibles, un par ligne, sans commentaire."
Résultat attendu : la deuxième commande émet un message_end dont le message porte
"provider":"openrouter" et "model":"z-ai/glm-5.3". La troisième affiche le modèle de votre
global, la quatrième nomme factory et factory-orchestrator. Coût : la première et la
troisième ligne ne dépendent pas de la pièce, les deux tours headless coûtent ensemble moins
d’un centime, en quelques secondes. Variante éco : elle est déjà dans la pièce, le
modèle de démarrage est le workhorse du roster éco.