Annexe — Harnais pi Chapitre A1 / 42

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: true et un projet compaction.reserveTokens: 8192 donnent les deux réglages. Une exception nette : un tableau defaultTools projet remplace le tableau global, il ne s’y ajoute pas.
  • Trois réglages font le modèle de démarrage : defaultProvider (par exemple openrouter), defaultModel (l’id tel que le fournisseur le nomme, z-ai/glm-5.3 au moment d’écrire) et defaultThinkingLevel (off à max). modelThinkingLevels affine par modèle, avec des clés provider/modelId. Ces réglages ne valent que pour le démarrage : --model et --thinking en 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 skills factory et factory-orchestrator des chapitres 25-26 sans --skill à chaque lancement.
  • Une clé se résout dans un ordre fixe : --api-key sur la ligne de commande, puis l’entrée du fichier ~/.pi/agent/auth.json (créé en 0600 par /login), puis la variable d’environnement (OPENROUTER_API_KEY pour la passerelle), puis les clés déclarées dans un models.json personnalisé. 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() dans harness.py qui charge .env dans 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 dans auth.json) ou d’un export de 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

QuestionGlobal ~/.pi/agent/settings.jsonProjet .pi/settings.jsonLigne de commande
Modèle et réflexion de démarrageoui, pour tous vos projetsoui, surcharge le global--model, --thinking gagnent
Skills, extensions, promptschemins 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’APIjamais (voir auth.json)jamais — un secret ne se commite pas--api-key gagne sur tout
Compaction, retryvaleurs par défaut de votre posteajusté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() dans harness.py. Et l’inverse vous guette sur votre poste : si /login openrouter a rangé une clé dans auth.json, elle passe devant la variable que .env a exportée. Une clé jetable du chapitre 23 mise dans .env peut 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ù aucun auth.json n’existe. Quand la trace vous montre un coût sur la mauvaise clé, regardez auth.json avant 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. /trust en 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énement project_trust, vous y reviendrez au chapitre A2.
  • Sans décision enregistrée, le headless ne demande rien. ask et never ignorent les ressources projet : .pi/settings.json n’est pas lu, les extensions ne se chargent pas, et pi ne le dit pas. always les charge. --approve (-a) et --no-approve (-na) tranchent pour un seul run. Votre adaptateur _run_pi ne 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 les keepRecentTokens (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 laisser retry.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)ouioui/trust une fois, en interactif
Aucune décision, global ask (défaut) ou nevernon, en silencenon— c’est l’état d’une machine neuve
Aucune décision, global alwaysouiouià réserver aux boîtes jetables
pi -p --approve …oui, pour ce runouiun run de diagnostic
pi -p --no-approve …non, pour ce runnonvé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: always dans .pi/settings.json du 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 premier pi -p. La confiance vient toujours de votre poste : /trust, le global, ou --approve pour 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.


Quiz — teste tes connaissances
Annexe — Harnais pi 7 questions Objectif : 5/7 minimum
0/7
bonnes reponses
Objectif non atteint (minimum 5/7 requis).
Remonte relire la fiche memo en pretant attention aux points manques, puis cliquer sur « Recommencer » pour retenter.