Orchestration multi-agents & distribution en package
Le dernier chapitre de l'annexe : des sous-agents pi bornés par le code — chaîne plan → build livrée en JSON —, puis le nœud pi de plume-factory empaqueté en package pi, installable dans un dépôt vierge d'une seule commande. La configuration voyage désormais comme l'usine.
Au chapitre 27, vous avez tamponné l’usine dans un dépôt vierge : stamp.py a copié adws/, les
rosters, le justfile et le skill, et le premier ADW a tourné dix minutes plus tard. Puis vous avez
ouvert pi dans ce dépôt d’essai, et rien de l’annexe n’était là. Pas de pied de page qui parle
la langue de l’usine, pas de damage control, pas de trace harnais : le manifeste du stamp ne
connaît pas .pi/, et le nœud pi que vous avez équipé pendant quatre chapitres est resté à la
maison. À la fin de ce chapitre, vous saurez deux choses. D’abord, comment un agent pi peut en
lancer d’autres, en sous-agents, équipes, chaînes ou pair-à-pair, et pourquoi la seule forme que
l’usine retient est celle où le code tient la séquence : une chaîne plan → build décrite en
JSON, exécutée par une extension, un sous-processus pi -p par étape. Ensuite, comment tout ce
qui vit sous .pi/ devient un package pi, installable d’une commande dans n’importe quel
dépôt, le miroir côté harnais du stamp du chapitre 27. La pièce du jour ferme l’annexe : le
runner Python reste le propriétaire du graphe, et le nœud pi voyage désormais avec lui.
Subagents, équipes, chaînes et pair-à-pair
L’idée en une phrase
Un sous-agent est un second processus pi -p lancé par une extension, avec son propre
système prompt, sa propre allowlist d’outils, son propre modèle et sa propre fenêtre de
contexte. Ce qui distingue une chaîne, une équipe et le pair-à-pair, c’est qui
décide de l’ordre, et dans l’usine cette décision revient au code déterministe de
l’extension, jamais au modèle qui parle.
Points clés
- Un sous-agent, c’est un processus. L’extension appelle
pi.exec("pi", args)avec-p,--mode json,--model,--tools,--append-system-promptet le prompt. Le sous-agent démarre vide : il ne voit ni votre conversation, ni les tours précédents. C’est le prix et l’intérêt de l’isolation : un contexte propre par tâche, une facture par tâche. - Trois formes, une différence : qui séquence. Une chaîne est une liste d’étapes fixée
dans un fichier, où la sortie de l’étape N devient
$INPUTde l’étape N+1, et$ORIGINALrappelle la demande de départ. Une équipe confie au modèle principal le choix du spécialiste à appeler, via un outildispatch_agent. Le pair-à-pair met deux agents à égalité, qui s’envoient des messages sans orchestrateur. Seule la chaîne est déterministe. - L’usine garde la chaîne. Le runner du chapitre 13 est une chaîne, en Python, avec des
gates. La chaîne pi du jour en est la version de poche pour la session interactive : même loi,
même enveloppe, sans gate
bun test, et c’est pour ça qu’elle sert à explorer, jamais à livrer. - Les outils du sous-agent sont bornés par
--tools. Unplannerreçoitread,grep,find,lset ne peut physiquement pas écrire. L’outilrun_chainlui-même n’est jamais dans la liste, donc un sous-agent ne relance pas de chaîne. Le damage control du chapitre A3 se charge en plus dans chaque sous-processus, puisque vous ne passez pas--no-extensions. - La sortie est une enveloppe, pas un souvenir. Le texte final de l’étape (le dernier
message_endd’assistant du flux JSON) traverse vers l’étape suivante avec son coût, ses jetons et sa durée. Rien d’autre ne passe : pas de session partagée, pas de mémoire implicite.
Exemple concret
Dans une session interactive de plume-factory, vous tapez : « lance la chaîne plan-build :
ajouter un compteur de mots dans la barre d’état de Plume ». Le modèle principal appelle
run_chain. L’extension lit .pi/agents/factory-chain.json, trouve deux étapes, et lance un
premier pi -p : le planner, sur le workhorse du roster, outils en lecture seule. Une
trentaine de secondes, quelques milliers de jetons, moins d’un centime, et il rend un plan
numéroté. L’extension substitue ce plan dans le prompt du builder, en $INPUT, et lance un
second pi -p, outils d’écriture cette fois. Deux minutes, quelques dizaines de milliers de
jetons, quelques centimes, et le builder modifie deux fichiers et lance bun test. Le résultat
revient dans votre session principale sous forme d’une enveloppe : deux étapes, deux factures, le
texte final. Le même travail confié à votre session principale aurait mêlé le plan, le code et
votre historique dans une seule fenêtre. Ici chaque étape a démarré propre, et vous savez ce que
chacune a coûté. Ce que vous n’avez pas : un verdict. Pour ça, il y a uv run adws/adw_sdlc.py.
Trois façons de faire travailler plusieurs agents
| Forme | Qui décide de l’ordre | Ce qui traverse | Reproductible | Place dans l’usine |
|---|---|---|---|---|
| Chaîne | le fichier de chaîne (code) | $INPUT / $ORIGINAL, texte + facture | oui, à la variabilité du modèle près | la version de poche du runner, pour explorer |
| Équipe | le modèle principal, via dispatch_agent | le prompt que le modèle rédige | non : le dispatch change d’un run à l’autre | à éviter ; c’est l’agent qui séquence |
| Pair-à-pair | personne : les deux agents négocient | des messages libres | non | un outil de recherche, pas une pièce d’usine |
| Runner Python (ch. 13) | runner.py, avec gates | enveloppes JSON typées | oui, verdict compris | la référence |
Config — .pi/agents/factory-chain.json, la forme d’une chaîne
Le fichier décrit deux choses : les agents (un système prompt, une allowlist d’outils) et les chaînes (des étapes, chacune avec son agent et son gabarit de prompt). Ni YAML ni Markdown à frontmatter : du JSON, comme toutes les enveloppes du livre, lisible sans dépendance depuis TypeScript comme depuis Python.
{
"agents": {
"planner": {
"tools": "read,grep,find,ls",
"system": "Vous planifiez, vous n'ecrivez jamais de fichier. Rendez un plan numerote."
}
},
"chains": {
"plan-only": {
"description": "Un plan, rien d'autre",
"steps": [
{ "agent": "planner", "prompt": "Planifiez : $INPUT" }
]
}
}
}
Commande — ce que l’extension lance pour une étape
Collez mentalement ce que run_chain exécute pour l’étape planner. C’est le même pi -p que
l’adaptateur _run_pi du chapitre 15, avec un système prompt en plus et sans --no-extensions.
# Un sous-agent = un processus : contexte vide, outils bornés, facture séparée.
pi -p --mode json --thinking off \
--model openrouter/z-ai/glm-5.3 \
--tools read,grep,find,ls \
--append-system-prompt "Vous planifiez, vous n'ecrivez jamais de fichier. Rendez un plan numerote." \
"Planifiez : ajouter un compteur de mots dans la barre d'etat de Plume"
Piège courant : « un sous-agent partage le contexte de l’agent qui l’a lancé » est inexact. Un sous-agent est un processus neuf qui ne sait que ce que son prompt lui dit. Tout ce que vous voulez qu’il sache doit traverser explicitement, dans le gabarit de l’étape. C’est une contrainte, et c’est exactement la loi de la couture du chapitre 8.
Empaqueter sa configuration pi en package
L’idée en une phrase
Un package pi est un dossier avec un package.json portant un manifeste pi qui dit quels
dossiers contiennent des extensions, des skills, des gabarits de prompt et des thèmes.
pi install l’enregistre dans settings.json et pi le charge à chaque démarrage. C’est la pièce
de la zone distribution de l’usine, côté code déterministe, le pendant harnais du stamp du
chapitre 27.
Points clés
- Le manifeste dit où chercher, pas quoi copier.
"pi": { "extensions": [".pi/extensions"], "skills": [".claude/skills"] }: des chemins relatifs à la racine du package. Sans manifeste, pi retombe sur des dossiers conventionnels (extensions/,skills/,prompts/,themes/) qui ne sont pas les vôtres, d’où le manifeste. - Trois sources, une même commande.
pi install npm:@scope/pkg@1.2.3,pi install git:github.com/user/repo@v1,pi install /chemin/vers/dossier. npm et git sont copiés (etnpm installest lancé). Un chemin local est référencé sans copie, parfait pour un dépôt à côté du vôtre, à réserver au poste de travail. -lécrit dans le projet. Sans drapeau,pi installmodifie~/.pi/agent/settings.json. Avec-l, il écritpackagesdans.pi/settings.jsondu dépôt courant, que vous commitez, et pi installe les packages manquants au démarrage, une fois le projet approuvé.- Les réglages ne voyagent pas dans le package. Un package transporte des extensions, des
skills, des prompts et des thèmes.
defaultModel,compactionouretrydu chapitre A1 restent des réglages du projet qui les reçoit. Le dépôt cible garde ses choix, le package lui apporte des capacités. - Les dépendances cœur sont des
peerDependencies.@earendil-works/pi-coding-agent,typeboxet les autres paquets que pi embarque se déclarent enpeerDependenciesavec"*", jamais endependencies: pi les fournit au chargement. Ce que le Bun de la gate a besoin de résoudre pour tester le fichier à sec vit endevDependencies.
Exemple concret
Vous ouvrez un nouveau dépôt, ../pi-essai, vierge. Depuis lui : pi install -l /chemin/vers/plume-factory. Une seconde, zéro jeton : .pi/settings.json apparaît avec une
entrée packages. Vous lancez pi -p --approve --mode json "ping", le flux JSON répond, et un
fichier adws/adw_data/traces/harness/…jsonl vient d’apparaître dans pi-essai : la trace du
chapitre A4 a tourné, donc les extensions du package ont été chargées, dans un dépôt qui n’a ni
adws/ ni runner. Le damage control est chargé aussi, avec zéro règle puisque le fichier de
règles est absent : il ne bloque rien mais ne plante pas. Ce que le package n’a pas apporté :
le modèle par défaut, c’est le global qui a répondu. Vous le voyez dans le champ model de la
première ligne du flux. Pour un dépôt qui doit ressembler à plume-factory, le stamp copie
adws/ et le package apporte .pi/ : deux commandes, une minute, et les deux côtés de la
couture sont en place.
Ce que chaque canal transporte
| Canal | Ce qui voyage | Ce qui reste | Commande |
|---|---|---|---|
| Stamp (ch. 27) | adws/, rosters, justfile, .env.sample, skills | payload, .env, adw_data/, .pi/ | uv run .claude/skills/factory/scripts/stamp.py --into … |
| Package pi (A5) | extensions, skills, prompts, thèmes | settings.json, règles de damage control, secrets | pi install -l <source> |
| Extension à l’essai | un fichier ou un package, pour un run | tout : rien n’est écrit dans les réglages | pi -e <source> |
| Dépôt cloné | tout, y compris ce qui ne devrait pas | rien | git clone |
Config — le manifeste minimal d’un package
Le package.json que vous poserez tout à l’heure suit exactement cette forme. Ici, la version
la plus courte qui fonctionne.
{
"name": "mon-noeud-pi",
"version": "0.1.0",
"private": true,
"keywords": ["pi-package"],
"pi": {
"extensions": [".pi/extensions"],
"skills": [".claude/skills"]
},
"peerDependencies": {
"@earendil-works/pi-coding-agent": "*"
}
}
Commande — installer, essayer, filtrer
# Enregistrer le package dans le projet courant (écrit .pi/settings.json, à commiter)
pi install -l /chemin/absolu/vers/plume-factory
# L'essayer sans rien écrire : chargé pour ce run seulement
pi -e ../plume-factory -p --approve "ping"
# Ne charger qu'une partie : la forme objet dans .pi/settings.json
# { "packages": [ { "source": "/chemin/vers/plume-factory", "extensions": [".pi/extensions/factory-obs.ts"], "skills": [] } ] }
Piège courant : « le package emporte ma configuration pi » est inexact. Il emporte vos capacités (extensions, skills), pas vos réglages : le dépôt qui l’installe garde son modèle par défaut, sa compaction, ses règles de damage control. Si vous voulez les mêmes réglages partout, c’est un
.pi/settings.jsonde plus à copier, et le stamp sait le faire.
Fil rouge — la pièce posée aujourd’hui
Sur le plan de l’usine, la pièce du jour ferme la zone distribution par le côté harnais :
package.json à la racine, avec son manifeste pi, à côté du skill factory du chapitre 26 et
du stamp.py du chapitre 27. Elle ajoute aussi, au poste de pilotage, une quatrième extension,
factory-chain.ts, et son fichier de chaînes sous .pi/agents/. La couture ne bouge pas, et
c’est le message de toute l’annexe : le runner Python possède le graphe des phases, et l’extension
de chaîne possède le graphe d’une exploration interactive, avec la même loi (le fichier
séquence, le modèle exécute une étape bornée, une enveloppe traverse : texte, coût, jetons,
durée) mais sans gate : elle propose, elle ne dispose pas. Le package, lui, est entièrement
déterministe : un manifeste, une commande, zéro jeton. Coût d’usage : une chaîne plan-build
sur le workhorse coûte quelques centimes et deux à trois minutes, installer le package
coûte une seconde. Ce qu’ils économisent : la session principale qui gonfle en mélangeant
plan, code et historique, et la demi-journée de « copie ce qu’il faut de .pi/ » chaque fois que
l’usine change de dépôt. Rappel de A1, tenu jusqu’au bout : l’annexe a rendu le nœud pi plus sûr
(A3), plus observable (A4) et, aujourd’hui, transportable, sans jamais déplacer la couture.
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. Aujourd’hui, trois fichiers : le fichier de chaînes,
l’extension qui les exécute, et le package.json qui fait voyager tout le dossier .pi/.
Pièce — .pi/agents/factory-chain.json
Deux agents et une chaîne. Le planner ne peut pas écrire, le builder peut, et doit lancer
bun test avant de rendre la main. Sans gate, c’est une consigne, pas une garantie, ce que le
runner du chapitre 13 corrige. Côté code déterministe : ce fichier est la séquence. Aucun outil
run_chain dans les allowlists, par construction.
{
"agents": {
"planner": {
"description": "Planifie sans ecrire : un plan numerote, les fichiers a toucher, les risques.",
"tools": "read,grep,find,ls",
"system": "Vous etes le planner de l'usine plume-factory. Vous lisez le depot, vous n'ecrivez jamais de fichier. Rendez un plan numerote, court, qui nomme chaque fichier a modifier et le test qui prouvera le resultat. Repondez en francais."
},
"builder": {
"description": "Implemente un plan donne, lance bun test, rend un compte rendu.",
"tools": "read,write,edit,bash,grep,find,ls",
"system": "Vous etes le builder de l'usine plume-factory. Vous implementez exactement le plan recu, sans l'etendre. Le payload vit dans apps/plume ; ses tests se lancent avec bun test depuis apps/plume. Avant de rendre la main, lancez les tests et rapportez leur resultat tel quel. Repondez en francais : fichiers touches, resultat des tests, ce qui reste ouvert."
}
},
"chains": {
"plan-build": {
"description": "Plan puis implementation, en deux sous-agents isoles",
"steps": [
{ "agent": "planner", "prompt": "Planifiez l'implementation de : $INPUT" },
{ "agent": "builder", "prompt": "Implementez ce plan :\n\n$INPUT\n\nDemande d'origine : $ORIGINAL" }
]
},
"plan-only": {
"description": "Un plan, rien d'autre — pour reflechir sans toucher au depot",
"steps": [
{ "agent": "planner", "prompt": "Planifiez l'implementation de : $INPUT" }
]
}
}
}
Pièce — .pi/extensions/factory-chain.ts
L’extension enregistre l’outil run_chain et la commande /chains. Elle lit le fichier de
chaînes du projet et, s’il est absent, celui du package (à côté d’elle, sous ../agents/), et c’est
ce qui rend le package autonome dans un dépôt vierge. Chaque étape est un pi -p --mode json
lancé par pi.exec, sur le modèle de la session courante, sans --no-extensions : le damage
control du chapitre A3 et la trace du chapitre A4 se chargent dans chaque sous-agent. Le fichier
porte sa propre gate sous Bun : elle vérifie la résolution des gabarits et la forme des arguments,
sans lancer un seul processus. Sous Windows, si pi n’est pas trouvé par exec, exportez
FACTORY_PI_BIN=pi.cmd.
// .pi/extensions/factory-chain.ts — des sous-agents bornés, une séquence tenue par un fichier.
// La loi ne change pas : le fichier de chaînes séquence, le modèle exécute une étape bornée,
// et seule une enveloppe (texte, coût, jetons, durée) traverse d'une étape à l'autre.
import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { Type } from "typebox";
const PROJECT_CHAINS = join(".pi", "agents", "factory-chain.json");
const PACKAGED_CHAINS = join(import.meta.dirname, "..", "agents", "factory-chain.json");
const STEP_TIMEOUT_MS = 10 * 60 * 1000; // dix minutes par étape : au-delà, on rend la main
const PI_BIN = process.env.FACTORY_PI_BIN ?? "pi";
// ---------- le contrat : ce que le fichier de chaînes doit contenir ----------
export interface AgentDef { description?: string; tools: string; system: string }
export interface ChainStep { agent: string; prompt: string }
export interface ChainDef { description?: string; steps: ChainStep[] }
export interface ChainConfig { agents: Record<string, AgentDef>; chains: Record<string, ChainDef> }
// L'enveloppe d'une étape : ce qui traverse, et rien d'autre.
export interface StepEnvelope {
step: number;
agent: string;
model: string;
output: string;
tokens: number;
cost_usd: number;
elapsed_ms: number;
exit_code: number;
}
export function loadConfig(cwd: string): { config: ChainConfig; source: string } {
const projectFile = join(cwd, PROJECT_CHAINS);
const file = existsSync(projectFile) ? projectFile : PACKAGED_CHAINS;
const config = JSON.parse(readFileSync(file, "utf8")) as ChainConfig;
for (const [name, chain] of Object.entries(config.chains)) {
for (const step of chain.steps) {
if (!config.agents[step.agent]) throw new Error(`chaîne ${name} : agent inconnu « ${step.agent} »`);
if (/\brun_chain\b/.test(config.agents[step.agent].tools)) throw new Error(`agent ${step.agent} : run_chain interdit dans un sous-agent`);
}
}
return { config, source: file };
}
// $INPUT = la sortie de l'étape précédente (ou la demande, à la première) ; $ORIGINAL = la demande.
export function resolvePrompt(template: string, input: string, original: string): string {
return template.replace(/\$INPUT/g, input).replace(/\$ORIGINAL/g, original);
}
// Les arguments d'un sous-agent : headless, JSON, sans réflexion, outils bornés, prompt en dernier.
export function argsFor(agent: AgentDef, model: string, prompt: string): string[] {
return [
"-p", "--mode", "json", "--thinking", "off",
"--model", model,
"--tools", agent.tools,
"--append-system-prompt", agent.system,
"--", prompt,
];
}
// Lire le flux JSON d'un `pi -p --mode json` : le dernier message d'assistant est la réponse,
// et chaque message d'assistant porte sa facture (même source que la trace du chapitre A4).
export function parseStream(stdout: string): { output: string; tokens: number; cost_usd: number } {
let output = "";
let tokens = 0;
let cost = 0;
for (const line of stdout.split("\n")) {
if (!line.trim()) continue;
let event: { type?: string; message?: { role?: string; content?: unknown; usage?: { totalTokens?: number; cost?: { total?: number } } } };
try { event = JSON.parse(line); } catch { continue; }
if (event.type !== "message_end" || event.message?.role !== "assistant") continue;
const parts = Array.isArray(event.message.content) ? event.message.content : [];
const text = parts
.filter((p) => (p as { type?: string })?.type === "text")
.map((p) => (p as { text?: string }).text ?? "")
.join("");
if (text.trim()) output = text; // la dernière réponse textuelle gagne
tokens += event.message.usage?.totalTokens ?? 0;
cost += event.message.usage?.cost?.total ?? 0;
}
return { output, tokens, cost_usd: cost };
}
function summarize(envelopes: StepEnvelope[]): string {
const cost = envelopes.reduce((s, e) => s + e.cost_usd, 0);
const tokens = envelopes.reduce((s, e) => s + e.tokens, 0);
const seconds = Math.round(envelopes.reduce((s, e) => s + e.elapsed_ms, 0) / 1000);
return `${envelopes.length} étape(s) · ${tokens} jetons · ${cost.toFixed(4)} $ · ${seconds} s`;
}
// ---------- l'extension : un outil, une commande, aucune décision confiée au modèle ----------
export default function (pi: ExtensionAPI) {
pi.registerTool({
name: "run_chain",
label: "Run Chain",
description: "Exécute une chaîne d'agents définie dans .pi/agents/factory-chain.json : chaque étape est un sous-agent pi isolé, la sortie d'une étape nourrit la suivante. Sans gate : pour explorer, pas pour livrer.",
parameters: Type.Object({
chain: Type.String({ description: "Nom de la chaîne (ex. plan-build, plan-only)" }),
task: Type.String({ description: "La demande à traiter" }),
}),
async execute(_toolCallId, params, signal, onUpdate, ctx) {
const { chain: chainName, task } = params as { chain: string; task: string };
const { config } = loadConfig(ctx.cwd);
const chain = config.chains[chainName];
// Une erreur se signale en levant : c'est ainsi que pi marque le résultat isError pour le modèle.
if (!chain) throw new Error(`Chaîne inconnue « ${chainName} ». Disponibles : ${Object.keys(config.chains).join(", ")}`);
const model = ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "";
if (!model) throw new Error("Aucun modèle actif : impossible de lancer un sous-agent.");
const envelopes: StepEnvelope[] = [];
let input = task;
for (const [index, step] of chain.steps.entries()) {
const agent = config.agents[step.agent];
onUpdate?.({ content: [{ type: "text", text: `étape ${index + 1}/${chain.steps.length} · ${step.agent} · en cours` }], details: { envelopes } });
const started = Date.now();
const result = await pi.exec(PI_BIN, argsFor(agent, model, resolvePrompt(step.prompt, input, task)),
{ cwd: ctx.cwd, signal, timeout: STEP_TIMEOUT_MS });
const parsed = parseStream(result.stdout);
const envelope: StepEnvelope = {
step: index + 1, agent: step.agent, model, output: parsed.output,
tokens: parsed.tokens, cost_usd: parsed.cost_usd,
elapsed_ms: Date.now() - started, exit_code: result.code,
};
envelopes.push(envelope);
// Échec fermé : une étape rouge ou muette arrête la chaîne, l'enveloppe dit laquelle.
if (envelope.exit_code !== 0 || !envelope.output.trim()) {
const why = result.stderr.trim().split("\n").pop() || "sortie vide";
throw new Error(`[chaîne ${chainName}] arrêt à l'étape ${envelope.step} (${step.agent}) : ${why} · ${summarize(envelopes)}`);
}
input = envelope.output;
}
return {
content: [{ type: "text", text: `[chaîne ${chainName}] terminée · ${summarize(envelopes)}\n\n${input}` }],
details: { chain: chainName, envelopes },
};
},
});
pi.registerCommand("chains", {
description: "Lister les chaînes d'agents disponibles",
handler: async (_args, ctx) => {
if (!ctx.hasUI) return;
const { config, source } = loadConfig(ctx.cwd);
const lines = Object.entries(config.chains)
.map(([name, c]) => `${name} : ${c.steps.map((s) => s.agent).join(" → ")}${c.description ? ` — ${c.description}` : ""}`);
ctx.ui.notify(`${lines.join("\n")}\n(${source})`, "info");
},
});
}
// ---------- la gate du fichier : `bun .pi/extensions/factory-chain.ts`, zéro jeton, zéro processus ----------
if (import.meta.main) {
const { config, source } = loadConfig(process.cwd());
const chain = config.chains["plan-build"];
const prompt = resolvePrompt(chain.steps[1].prompt, "1. Lire apps/plume/src/editor.ts", "un compteur de mots");
const args = argsFor(config.agents["builder"], "openrouter/z-ai/glm-5.3", prompt);
const stream = [
JSON.stringify({ type: "session", id: "x" }),
JSON.stringify({ type: "message_end", message: { role: "assistant", content: [{ type: "text", text: "Plan : 1. editor.ts" }], usage: { totalTokens: 900, cost: { total: 0.001 } } } }),
JSON.stringify({ type: "message_end", message: { role: "assistant", content: [{ type: "text", text: "Fait. bun test : 6 pass." }], usage: { totalTokens: 2100, cost: { total: 0.003 } } } }),
].join("\n");
const parsed = parseStream(stream);
const agents = Object.keys(config.agents).length;
const chains = Object.keys(config.chains).length;
const ok =
chain.steps.length === 2 &&
prompt.includes("1. Lire apps/plume/src/editor.ts") && prompt.includes("Demande d'origine : un compteur de mots") &&
args[0] === "-p" && args.includes("--tools") && args[args.length - 1] === prompt &&
!config.agents["planner"].tools.includes("write") && config.agents["builder"].tools.includes("bash") &&
parsed.output === "Fait. bun test : 6 pass." && parsed.tokens === 3000 && Math.abs(parsed.cost_usd - 0.004) < 1e-9;
console.log(`factory-chain ${ok ? "OK" : "KO"} — ${agents} agents, ${chains} chaînes, gabarits résolus, outils bornés, flux JSON lu (${source})`);
process.exit(ok ? 0 : 1);
}
Pièce — package.json
À la racine de plume-factory, où il n’y en avait pas encore : celui de Plume vit dans
apps/plume/. Le manifeste pi pointe vers les extensions de l’annexe et les skills des
chapitres 25-26. Les paquets cœur sont en peerDependencies (pi les fournit), et typebox est
aussi en devDependencies pour que la gate Bun puisse importer l’extension à sec. private
empêche une publication npm accidentelle. Le jour où vous voudrez publier, retirez-le et
versionnez.
{
"name": "plume-factory-pi",
"version": "0.1.0",
"private": true,
"type": "module",
"description": "Le noeud pi de l'usine plume-factory : pied de page, damage control, trace harnais, chaines d'agents et skills de l'usine.",
"keywords": ["pi-package"],
"pi": {
"extensions": [".pi/extensions"],
"skills": [".claude/skills"]
},
"peerDependencies": {
"@earendil-works/pi-coding-agent": "*",
"typebox": "*"
},
"peerDependenciesMeta": {
"@earendil-works/pi-coding-agent": { "optional": true },
"typebox": { "optional": true }
},
"devDependencies": {
"typebox": "1.3.7"
}
}
Les peerDependenciesMeta marquent les paquets cœur comme optionnels : bun install n’ira pas
télécharger pi entier à la racine de votre dépôt, seulement typebox pour la gate.
La gate du TP
Depuis la racine de plume-factory, projet approuvé (chapitre A1). Une commande par ligne,
identiques dans bash et PowerShell. Les trois premières ne coûtent rien, la quatrième lance la
chaîne plan-only, un seul sous-agent en lecture seule, et les suivantes installent le package
dans un dépôt vierge et prouvent que ses extensions s’y chargent. Remplacez le chemin absolu par
celui de votre dépôt.
bun install
bun .pi/extensions/factory-chain.ts
pi -p --approve --mode json --tools run_chain "Appelle run_chain avec chain=plan-only et task=ajouter un compteur de mots dans la barre d'etat de Plume. Rends le resultat tel quel."
git init ../pi-essai
cd ../pi-essai
pi install -l /chemin/absolu/vers/plume-factory
pi -p --approve --mode json "ping"
ls adws/adw_data/traces/harness
cd ../plume-factory
Résultat attendu : factory-chain OK — 2 agents, 2 chaînes, gabarits résolus, outils bornés, flux JSON lu, puis, dans le flux JSON de la troisième commande, un tool_execution_end pour
run_chain dont le texte commence par [chaîne plan-only] terminée · 1 étape(s) suivi d’un plan
numéroté, pour moins d’un centime, ~30 s sur le workhorse du chapitre A1. Dans pi-essai,
pi install -l écrit .pi/settings.json avec une entrée packages, et le ls final liste un
fichier .jsonl : la trace du chapitre A4 a tourné dans un dépôt qui n’a ni adws/ ni runner,
donc les extensions du package ont été chargées (zéro jeton pour l’installation, un ping
à moins d’un centime). Variante éco : elle est déjà dans la pièce, les sous-agents prennent
le modèle de la session, celui du roster. Pour voir la chaîne complète, ouvrez pi dans
plume-factory, tapez /chains, puis demandez plan-build sur une petite évolution de Plume :
quelques centimes, deux à trois minutes. Souvenez-vous qu’aucune gate n’a tranché, et que
c’est uv run adws/adw_sdlc.py qui dispose. Effacez ../pi-essai quand vous avez fini.