@devilnside/pi-auto-improve 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,65 @@
1
+ # @ajoye/pi-auto-improve
2
+
3
+ Système d'auto-amélioration pour [Pi Coding Agent](https://pi.dev). Permet à l'agent d'apprendre de ses erreurs via un système de feedback utilisateur.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pi install npm:@ajoye/pi-auto-improve
9
+ ```
10
+
11
+ Ou essayer sans installer :
12
+
13
+ ```bash
14
+ pi -e npm:@ajoye/pi-auto-improve
15
+ ```
16
+
17
+ Puis redémarre Pi ou tape `/reload`.
18
+
19
+ ## Fonctionnalités
20
+
21
+ - **Feedback manuel** — Commandes `/good` et `/bad` pour évaluer les réponses
22
+ - **Boutons Telegram** — 👍/👎 sur chaque réponse en mode Telegram
23
+ - **Analyse d'échec** — Sur feedback négatif, l'agent analyse ce qui a mal tourné et propose une leçon
24
+ - **Leçons validées** — Chaque leçon doit être validée par l'utilisateur avant stockage
25
+ - **3 scopes** — Global, par domaine (debug, refactoring, feature, review, general), par projet
26
+ - **Injection automatique** — Les leçons globales sont injectées dans le prompt système à chaque tour
27
+
28
+ ## Usage
29
+
30
+ ### Feedback
31
+
32
+ - `/good [commentaire]` — Enregistrer un feedback positif
33
+ - `/bad [commentaire]` — Feedback négatif + déclenche l'analyse
34
+
35
+ ### Leçons
36
+
37
+ - `/lessons [scope]` — Afficher les leçons (`global`, `domain`, `project`, `all`)
38
+ - `/lesson-add <domain> <règle>` — Ajouter manuellement une leçon
39
+ - `/lesson-remove <id>` — Désactiver une leçon
40
+
41
+ ### Domaines
42
+
43
+ - `debug` — Tests, erreurs, stack traces
44
+ - `refactoring` — Modifications multiples de fichiers
45
+ - `feature` — Nouveaux fichiers, nouvelles fonctionnalités
46
+ - `review` — Lecture de code
47
+ - `general` — Par défaut
48
+
49
+ ## Architecture
50
+
51
+ Les données sont stockées dans `~/.pi/agent/data/auto-improve/` :
52
+
53
+ ```
54
+ data/auto-improve/
55
+ ├── feedback.jsonl # Historique des feedbacks
56
+ ├── lessons/
57
+ │ ├── global.json # Leçons globales (toujours chargées)
58
+ │ ├── domain-debug.json # Leçons par domaine
59
+ │ └── ...
60
+ └── projects/ # Leçons par projet (hash du chemin)
61
+ ```
62
+
63
+ ## Design
64
+
65
+ Voir [docs/features/2026-05-10-auto-improve-design.md](docs/features/2026-05-10-auto-improve-design.md) pour le spec complet.
@@ -0,0 +1,208 @@
1
+ # Pi Auto-Improve — Design
2
+
3
+ **Date :** 2026-05-10
4
+ **Statut :** Validé
5
+
6
+ ## Résumé
7
+
8
+ Système d'auto-amélioration pour Pi composé d'un skill (logique métier) et d'une extension légère (intégration Pi). L'utilisateur donne du feedback manuel (CLI ou boutons Telegram 👍/👎). Sur feedback négatif, Pi analyse l'échec, propose une leçon à valider. Les leçons sont stockées en JSON structuré, avec des règles globales toujours chargées au démarrage et des leçons domaine/projet consultées à la demande.
9
+
10
+ ## Approche retenue
11
+
12
+ Skill + Extension légère (Approche C) :
13
+ - **Skill** = logique métier (analyse, génération de leçons, consolidation)
14
+ - **Extension** = intégration Pi (commandes, hooks, Telegram, chargement contexte)
15
+
16
+ ## Architecture
17
+
18
+ ```
19
+ ~/Projects/pi-self-improve/
20
+ ├── README.md
21
+ ├── docs/features/ # Design docs
22
+ ├── skill/
23
+ │ └── auto-improve/
24
+ │ └── SKILL.md # Logique d'analyse et leçons
25
+ ├── extension/
26
+ │ └── auto-improve.ts # Commandes, hooks, Telegram
27
+ ├── data/
28
+ │ ├── feedback.jsonl # Historique brut des feedbacks
29
+ │ ├── lessons/
30
+ │ │ ├── global.json # Leçons globales
31
+ │ │ ├── domain-debug.json
32
+ │ │ ├── domain-refactoring.json
33
+ │ │ ├── domain-feature.json
34
+ │ │ ├── domain-review.json
35
+ │ │ └── domain-general.json
36
+ │ └── projects/
37
+ │ └── <project-hash>.json # Leçons spécifiques par projet
38
+ └── scripts/
39
+ └── install.sh # Installation dans ~/.pi/agent/
40
+ ```
41
+
42
+ ## Format de stockage
43
+
44
+ ### feedback.jsonl
45
+
46
+ Un JSON par ligne, append-only.
47
+
48
+ ```json
49
+ {
50
+ "id": "abc123",
51
+ "timestamp": "2026-05-10T14:30:00Z",
52
+ "type": "positive | negative",
53
+ "source": "cli | telegram",
54
+ "session": "sess-456",
55
+ "project": "/var/home/ajoye/Projects/my-app",
56
+ "domain": "refactoring",
57
+ "context": "Refactor du module auth",
58
+ "user_comment": "Trop de changements d'un coup"
59
+ }
60
+ ```
61
+
62
+ Champs :
63
+ - `id` — identifiant unique (timestamp-based)
64
+ - `timestamp` — ISO 8601
65
+ - `type` — `positive` ou `negative`
66
+ - `source` — `cli` (commande `/good`/`/bad`) ou `telegram` (bouton)
67
+ - `session` — ID de la session Pi
68
+ - `project` — chemin du projet (cwd au moment du feedback)
69
+ - `domain` — domaine détecté ou `general` par défaut
70
+ - `context` — résumé court de la tâche en cours
71
+ - `user_comment` — texte libre optionnel
72
+
73
+ ### lessons/*.json et projects/*.json
74
+
75
+ Même structure, scope différent.
76
+
77
+ ```json
78
+ {
79
+ "project": "global",
80
+ "updated": "2026-05-10T14:35:00Z",
81
+ "lessons": [
82
+ {
83
+ "id": "l-001",
84
+ "created": "2026-05-10T14:35:00Z",
85
+ "domain": "refactoring",
86
+ "rule": "Toujours découper les refactors larges en petites étapes incrémentales, une fonction ou un fichier à la fois",
87
+ "rationale": "L'utilisateur préfère voir chaque étape validée avant de continuer",
88
+ "source_feedback": ["abc123"],
89
+ "positive_examples": 2,
90
+ "violations": 0,
91
+ "active": true
92
+ }
93
+ ]
94
+ }
95
+ ```
96
+
97
+ Champs :
98
+ - `id` — identifiant unique de la leçon
99
+ - `created` — date de création
100
+ - `domain` — `debug`, `refactoring`, `feature`, `review`, `general`
101
+ - `rule` — la leçon en langage naturel, concise et actionnable
102
+ - `rationale` — pourquoi cette règle existe
103
+ - `source_feedback` — IDs des feedbacks ayant généré cette leçon
104
+ - `positive_examples` — compteur de fois où la règle a été suivie avec succès
105
+ - `violations` — compteur de fois où la règle a été enfreinte
106
+ - `active` — `false` = désactivée sans suppression
107
+
108
+ Hash projet : hash court (6-8 chars) du chemin absolu du projet.
109
+
110
+ ## Extension (auto-improve.ts)
111
+
112
+ ### Commandes
113
+
114
+ | Commande | Description |
115
+ |----------|-------------|
116
+ | `/good [commentaire]` | Enregistre un feedback positif |
117
+ | `/bad [commentaire]` | Feedback négatif + déclenche l'analyse |
118
+ | `/lessons [scope]` | Affiche les leçons (global, domain, project) |
119
+ | `/lesson-add <domain> <règle>` | Ajout manuel d'une leçon |
120
+ | `/lesson-remove <id>` | Désactive une leçon |
121
+
122
+ ### Hooks
123
+
124
+ - **`on("session_start")`** — Charge `global.json` et l'injecte dans le contexte
125
+ - **`on("tool_call", filter="bash")`** — Détecte le domaine courant basé sur les actions
126
+
127
+ ### Intégration Telegram
128
+
129
+ Après chaque réponse en mode Telegram, ajout de boutons cachés :
130
+ ```
131
+ <!-- telegram_button label="👍" prompt="Feedback positif pour la dernière réponse." -->
132
+ <!-- telegram_button label="👎" prompt="Feedback négatif pour la dernière réponse." -->
133
+ ```
134
+
135
+ Le feedback négatif via bouton déclenche le même flux que `/bad`.
136
+
137
+ ### Chargement au démarrage
138
+
139
+ Le fichier `global.json` est lu, formaté en Markdown, injecté dans le contexte :
140
+
141
+ ```markdown
142
+ ## Leçons apprises (auto-improve)
143
+ - [refactoring] Toujours découper les refactors en petites étapes incrémentales
144
+ - [debug] Commencer par reproduire le bug avant de proposer des fixes
145
+ ```
146
+
147
+ ## Skill (auto-improve/SKILL.md)
148
+
149
+ ### Frontmatter
150
+
151
+ ```yaml
152
+ ---
153
+ name: auto-improve
154
+ description: "Use when the user gives negative feedback (👎, /bad) or asks to analyze a failure. Handles failure analysis, lesson generation, and lesson consolidation."
155
+ ---
156
+ ```
157
+
158
+ ### Responsabilités
159
+
160
+ 1. **Analyser l'échec** — Examiner la conversation récente, identifier la cause
161
+ 2. **Formuler une leçon** — Règle concise, actionnable, en français
162
+ 3. **Détecter le domaine** — `debug`, `refactoring`, `feature`, `review`, `general`
163
+ 4. **Proposer à l'utilisateur** — Validation avant stockage
164
+ 5. **Consolider** — Fusionner avec les leçons existantes si similaire
165
+
166
+ ### Processus
167
+
168
+ ```
169
+ Feedback négatif reçu
170
+ → Relire les derniers échanges
171
+ → Identifier la cause probable
172
+ → Vérifier si une leçon similaire existe
173
+ → Si oui : proposer une fusion
174
+ → Si non : proposer une nouvelle leçon
175
+ → Présenter à l'utilisateur : règle + justification
176
+ → Si validé → écrire dans le JSON
177
+ → Si refusé → ne rien stocker
178
+ ```
179
+
180
+ ### Consultation à la demande
181
+
182
+ `/lessons domain debug` ou `/lessons project` :
183
+ - Lire le JSON correspondant
184
+ - Filtrer `active: true`
185
+ - Présenter en liste concise
186
+
187
+ ## Chargement hybride
188
+
189
+ - **Toujours chargé** : `global.json` → injecté au démarrage de chaque session
190
+ - **À la demande** : leçons domaine et projet → consultées via `/lessons` ou par le skill quand c'est pertinent
191
+
192
+ ## Domaines supportés
193
+
194
+ | Domaine | Détection |
195
+ |---------|-----------|
196
+ | `debug` | Commandes de test, grep d'erreurs, lecture de stack traces |
197
+ | `refactoring` | Modifications multiples de fichiers existants |
198
+ | `feature` | Création de nouveaux fichiers, ajout de fonctionnalités |
199
+ | `review` | Lecture de code sans modification |
200
+ | `general` | Par défaut, quand aucun domaine clair |
201
+
202
+ ## Décisions de design
203
+
204
+ 1. **Pas d'auto-correction immédiate** — Sur feedback négatif, analyse + proposition de leçon uniquement. L'utilisateur valide.
205
+ 2. **Feedback positif stocké aussi** — Sert pour les compteurs `positive_examples` et pour renforcer les leçons existantes.
206
+ 3. **Leçons désactivables** — `active: false` plutôt que suppression, pour garder l'historique.
207
+ 4. **Pas de ML** — Tout repose sur l'analyse par le LLM et la validation humaine.
208
+ 5. **Hash court pour les projets** — Pas de chemins complets dans les noms de fichiers.
@@ -0,0 +1,554 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { Type } from "typebox";
3
+ import { createHash } from "node:crypto";
4
+ import { appendFile, readFile, writeFile, mkdir } from "node:fs/promises";
5
+ import { existsSync } from "node:fs";
6
+ import { resolve, dirname, join } from "node:path";
7
+
8
+ // ── Configuration ──────────────────────────────────────────────────────────
9
+ const DATA_DIR = resolve(process.env.HOME!, ".pi/agent/data/auto-improve");
10
+ const FEEDBACK_FILE = join(DATA_DIR, "feedback.jsonl");
11
+ const LESSONS_DIR = join(DATA_DIR, "lessons");
12
+ const PROJECTS_DIR = join(DATA_DIR, "projects");
13
+
14
+ const DOMAINS = ["debug", "refactoring", "feature", "review", "general"] as const;
15
+ type Domain = (typeof DOMAINS)[number];
16
+
17
+ // ── Helpers ────────────────────────────────────────────────────────────────
18
+ function generateId(): string {
19
+ return Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
20
+ }
21
+
22
+ function projectHash(cwd: string): string {
23
+ return createHash("sha256").update(cwd).digest("hex").slice(0, 8);
24
+ }
25
+
26
+ function timestamp(): string {
27
+ return new Date().toISOString();
28
+ }
29
+
30
+ async function ensureDir(path: string) {
31
+ if (!existsSync(path)) await mkdir(path, { recursive: true });
32
+ }
33
+
34
+ async function ensureFile(path: string, defaultContent: string) {
35
+ if (!existsSync(path)) await writeFile(path, defaultContent, "utf8");
36
+ }
37
+
38
+ function domainFromActions(recentActions: string[]): Domain {
39
+ const combined = recentActions.join(" ").toLowerCase();
40
+ if (/test|spec|grep.*error|stack.?trace|fail|exception/.test(combined)) return "debug";
41
+ if (/edit|modify|refactor|rename|move/.test(combined)) return "refactoring";
42
+ if (/create|new file|add.*feature|implement|write.*new/.test(combined)) return "feature";
43
+ if (/review|read.*only|audit|check/.test(combined)) return "review";
44
+ return "general";
45
+ }
46
+
47
+ // ── Feedback ───────────────────────────────────────────────────────────────
48
+ interface FeedbackEntry {
49
+ id: string;
50
+ timestamp: string;
51
+ type: "positive" | "negative";
52
+ source: "cli" | "telegram";
53
+ session: string;
54
+ project: string;
55
+ domain: string;
56
+ context: string;
57
+ user_comment?: string;
58
+ }
59
+
60
+ async function appendFeedback(entry: FeedbackEntry): Promise<void> {
61
+ await ensureDir(DATA_DIR);
62
+ await appendFile(FEEDBACK_FILE, JSON.stringify(entry) + "\n", "utf8");
63
+ }
64
+
65
+ // ── Lessons ────────────────────────────────────────────────────────────────
66
+ interface Lesson {
67
+ id: string;
68
+ created: string;
69
+ domain: Domain;
70
+ rule: string;
71
+ rationale: string;
72
+ source_feedback: string[];
73
+ positive_examples: number;
74
+ violations: number;
75
+ active: boolean;
76
+ }
77
+
78
+ interface LessonFile {
79
+ project: string;
80
+ updated: string;
81
+ lessons: Lesson[];
82
+ }
83
+
84
+ function emptyLessonFile(project: string): LessonFile {
85
+ return { project, updated: timestamp(), lessons: [] };
86
+ }
87
+
88
+ async function readLessonFile(path: string): Promise<LessonFile> {
89
+ await ensureDir(dirname(path));
90
+ await ensureFile(path, JSON.stringify(emptyLessonFile("unknown"), null, 2));
91
+ const raw = await readFile(path, "utf8");
92
+ return JSON.parse(raw);
93
+ }
94
+
95
+ async function writeLessonFile(path: string, data: LessonFile): Promise<void> {
96
+ data.updated = timestamp();
97
+ await writeFile(path, JSON.stringify(data, null, 2), "utf8");
98
+ }
99
+
100
+ function globalPath(): string {
101
+ return join(LESSONS_DIR, "global.json");
102
+ }
103
+
104
+ function domainPath(domain: Domain): string {
105
+ return join(LESSONS_DIR, `domain-${domain}.json`);
106
+ }
107
+
108
+ function projectPath(cwd: string): string {
109
+ return join(PROJECTS_DIR, `${projectHash(cwd)}.json`);
110
+ }
111
+
112
+ // ── Format lessons as markdown for injection ───────────────────────────────
113
+ function formatLessonsMarkdown(lessons: Lesson[]): string {
114
+ if (lessons.length === 0) return "";
115
+ const active = lessons.filter((l) => l.active);
116
+ if (active.length === 0) return "";
117
+ const lines = active.map((l) => `- [${l.domain}] ${l.rule}`);
118
+ return "## Leçons apprises (auto-improve)\n" + lines.join("\n");
119
+ }
120
+
121
+ async function loadGlobalLessons(): Promise<string> {
122
+ const data = await readLessonFile(globalPath());
123
+ return formatLessonsMarkdown(data.lessons);
124
+ }
125
+
126
+ // ── Track recent bash actions for domain detection ─────────────────────────
127
+ const recentActions: string[] = [];
128
+ const MAX_ACTIONS = 10;
129
+
130
+ function trackAction(command: string) {
131
+ recentActions.push(command);
132
+ if (recentActions.length > MAX_ACTIONS) recentActions.shift();
133
+ }
134
+
135
+ // ── Context summary from session ───────────────────────────────────────────
136
+ function getContextSummary(entries: any[]): string {
137
+ // Get last few user messages as context
138
+ const userMsgs = entries
139
+ .filter(
140
+ (e: any) =>
141
+ e.type === "message" &&
142
+ e.message?.role === "user" &&
143
+ typeof e.message?.content === "string"
144
+ )
145
+ .slice(-3);
146
+ return userMsgs
147
+ .map((e: any) => e.message.content.slice(0, 100))
148
+ .join(" | ");
149
+ }
150
+
151
+ // ── Extension ──────────────────────────────────────────────────────────────
152
+ export default function (pi: ExtensionAPI) {
153
+ // Ensure data structure on startup
154
+ pi.on("session_start", async (_event, ctx) => {
155
+ await ensureDir(LESSONS_DIR);
156
+ await ensureDir(PROJECTS_DIR);
157
+ await ensureFile(
158
+ globalPath(),
159
+ JSON.stringify(emptyLessonFile("global"), null, 2)
160
+ );
161
+ for (const d of DOMAINS) {
162
+ await ensureFile(
163
+ domainPath(d),
164
+ JSON.stringify(emptyLessonFile(`domain-${d}`), null, 2)
165
+ );
166
+ }
167
+ });
168
+
169
+ // Track bash actions for domain detection
170
+ pi.on("tool_call", async (event) => {
171
+ if (event.toolName === "bash" && event.input?.command) {
172
+ trackAction(event.input.command);
173
+ }
174
+ });
175
+
176
+ // Inject global lessons into system prompt
177
+ pi.on("before_agent_start", async (event) => {
178
+ const md = await loadGlobalLessons();
179
+ if (md) {
180
+ return { systemPrompt: event.systemPrompt + "\n\n" + md };
181
+ }
182
+ });
183
+
184
+ // ── Command: /good ─────────────────────────────────────────────────────
185
+ pi.registerCommand("good", {
186
+ description: "Enregistrer un feedback positif sur la dernière réponse",
187
+ handler: async (args, ctx) => {
188
+ const sessionId =
189
+ ctx.sessionManager.getSessionFile()?.split("/").pop() ?? "unknown";
190
+ const domain = domainFromActions(recentActions);
191
+ const context = getContextSummary(ctx.sessionManager.getEntries());
192
+
193
+ await appendFeedback({
194
+ id: generateId(),
195
+ timestamp: timestamp(),
196
+ type: "positive",
197
+ source: "cli",
198
+ session: sessionId,
199
+ project: ctx.cwd,
200
+ domain,
201
+ context,
202
+ user_comment: args || undefined,
203
+ });
204
+
205
+ ctx.ui.notify("👍 Feedback positif enregistré", "info");
206
+ },
207
+ });
208
+
209
+ // ── Command: /bad ──────────────────────────────────────────────────────
210
+ pi.registerCommand("bad", {
211
+ description:
212
+ "Enregistrer un feedback négatif et déclencher l'analyse d'auto-amélioration",
213
+ handler: async (args, ctx) => {
214
+ const sessionId =
215
+ ctx.sessionManager.getSessionFile()?.split("/").pop() ?? "unknown";
216
+ const domain = domainFromActions(recentActions);
217
+ const context = getContextSummary(ctx.sessionManager.getEntries());
218
+ const feedbackId = generateId();
219
+
220
+ await appendFeedback({
221
+ id: feedbackId,
222
+ timestamp: timestamp(),
223
+ type: "negative",
224
+ source: "cli",
225
+ session: sessionId,
226
+ project: ctx.cwd,
227
+ domain,
228
+ context,
229
+ user_comment: args || undefined,
230
+ });
231
+
232
+ ctx.ui.notify("👎 Feedback négatif enregistré — analyse en cours...", "warning");
233
+
234
+ // Send analysis prompt to the agent
235
+ const comment = args ? ` Commentaire de l'utilisateur : "${args}"` : "";
236
+ pi.sendUserMessage(
237
+ `Feedback négatif reçu (id: ${feedbackId}, domaine détecté: ${domain}).${comment}\n\n` +
238
+ `Utilise le skill auto-improve pour analyser ce qui n'a pas fonctionné dans tes dernières réponses. ` +
239
+ `Propose une leçon à l'utilisateur et attends sa validation avant de la sauvegarder via l'outil save_lesson.`,
240
+ { deliverAs: "steer" }
241
+ );
242
+ },
243
+ });
244
+
245
+ // ── Command: /lessons ──────────────────────────────────────────────────
246
+ pi.registerCommand("lessons", {
247
+ description:
248
+ "Afficher les leçons apprises. Usage: /lessons [global|domain|project|all]",
249
+ handler: async (args, ctx) => {
250
+ const scope = args?.trim().toLowerCase() || "global";
251
+ let output = "";
252
+
253
+ try {
254
+ if (scope === "global" || scope === "all") {
255
+ const data = await readLessonFile(globalPath());
256
+ const active = data.lessons.filter((l) => l.active);
257
+ output += `📚 Leçons globales (${active.length}) :\n`;
258
+ for (const l of active) {
259
+ output += ` [${l.domain}] ${l.rule}\n`;
260
+ }
261
+ }
262
+
263
+ if (scope === "domain" || scope === "all") {
264
+ for (const d of DOMAINS) {
265
+ const data = await readLessonFile(domainPath(d));
266
+ const active = data.lessons.filter((l) => l.active);
267
+ if (active.length > 0) {
268
+ output += `\n📚 Leçons ${d} (${active.length}) :\n`;
269
+ for (const l of active) {
270
+ output += ` ${l.rule}\n`;
271
+ }
272
+ }
273
+ }
274
+ }
275
+
276
+ if (scope === "project" || scope === "all") {
277
+ const pPath = projectPath(ctx.cwd);
278
+ const data = await readLessonFile(pPath);
279
+ const active = data.lessons.filter((l) => l.active);
280
+ output += `\n📚 Leçons projet (${active.length}) :\n`;
281
+ for (const l of active) {
282
+ output += ` [${l.domain}] ${l.rule}\n`;
283
+ }
284
+ }
285
+
286
+ ctx.ui.notify(output || "Aucune leçon trouvée.", "info");
287
+ } catch (err: any) {
288
+ ctx.ui.notify(`Erreur: ${err.message}`, "error");
289
+ }
290
+ },
291
+ });
292
+
293
+ // ── Command: /lesson-add ───────────────────────────────────────────────
294
+ pi.registerCommand("lesson-add", {
295
+ description:
296
+ "Ajouter manuellement une leçon. Usage: /lesson-add <domain> <règle>",
297
+ handler: async (args, ctx) => {
298
+ const parts = args?.trim().split(/\s+/);
299
+ if (!parts || parts.length < 2) {
300
+ ctx.ui.notify(
301
+ "Usage: /lesson-add <domain> <règle>\nDomaines: debug, refactoring, feature, review, general",
302
+ "warning"
303
+ );
304
+ return;
305
+ }
306
+
307
+ const domain = parts[0] as Domain;
308
+ if (!DOMAINS.includes(domain)) {
309
+ ctx.ui.notify(
310
+ `Domaine invalide: ${domain}. Utilise: ${DOMAINS.join(", ")}`,
311
+ "error"
312
+ );
313
+ return;
314
+ }
315
+
316
+ const rule = parts.slice(1).join(" ");
317
+ const scope = ctx.hasUI ? "global" : "global"; // default to global for manual adds
318
+
319
+ const path = globalPath();
320
+ const data = await readLessonFile(path);
321
+ const lesson: Lesson = {
322
+ id: `l-${generateId()}`,
323
+ created: timestamp(),
324
+ domain,
325
+ rule,
326
+ rationale: "Ajoutée manuellement par l'utilisateur",
327
+ source_feedback: [],
328
+ positive_examples: 0,
329
+ violations: 0,
330
+ active: true,
331
+ };
332
+ data.lessons.push(lesson);
333
+ await writeLessonFile(path, data);
334
+
335
+ ctx.ui.notify(`✅ Leçon ajoutée: [${domain}] ${rule}`, "info");
336
+ },
337
+ });
338
+
339
+ // ── Command: /lesson-remove ────────────────────────────────────────────
340
+ pi.registerCommand("lesson-remove", {
341
+ description:
342
+ "Désactiver une leçon. Usage: /lesson-remove <id>",
343
+ handler: async (args, ctx) => {
344
+ const id = args?.trim();
345
+ if (!id) {
346
+ ctx.ui.notify("Usage: /lesson-remove <id>", "warning");
347
+ return;
348
+ }
349
+
350
+ // Search across all lesson files
351
+ const filesToSearch = [
352
+ globalPath(),
353
+ ...DOMAINS.map(domainPath),
354
+ projectPath(ctx.cwd),
355
+ ];
356
+
357
+ let found = false;
358
+ for (const path of filesToSearch) {
359
+ if (!existsSync(path)) continue;
360
+ const data = await readLessonFile(path);
361
+ const lesson = data.lessons.find((l) => l.id === id);
362
+ if (lesson) {
363
+ lesson.active = false;
364
+ await writeLessonFile(path, data);
365
+ ctx.ui.notify(`🗑️ Leçon désactivée: ${lesson.rule}`, "info");
366
+ found = true;
367
+ break;
368
+ }
369
+ }
370
+
371
+ if (!found) {
372
+ ctx.ui.notify(`Leçon ${id} non trouvée.`, "warning");
373
+ }
374
+ },
375
+ });
376
+
377
+ // ── Tool: save_lesson ──────────────────────────────────────────────────
378
+ // Used by the agent (guided by the skill) to save a validated lesson
379
+ pi.registerTool({
380
+ name: "save_lesson",
381
+ label: "Save Lesson",
382
+ description:
383
+ "Sauvegarder une leçon validée par l'utilisateur. Spécifier le scope, domaine, règle et justification.",
384
+ promptSnippet: "Sauvegarder une leçon d'auto-amélioration validée",
385
+ parameters: Type.Object({
386
+ scope: Type.Union([
387
+ Type.Literal("global"),
388
+ Type.Literal("domain"),
389
+ Type.Literal("project"),
390
+ ], { description: "Scope de la leçon: global, domain, ou project" }),
391
+ domain: Type.Union([
392
+ Type.Literal("debug"),
393
+ Type.Literal("refactoring"),
394
+ Type.Literal("feature"),
395
+ Type.Literal("review"),
396
+ Type.Literal("general"),
397
+ ], { description: "Domaine de la leçon" }),
398
+ rule: Type.String({ description: "La leçon en français, concise et actionnable" }),
399
+ rationale: Type.String({ description: "Pourquoi cette règle existe" }),
400
+ source_feedback_id: Type.Optional(
401
+ Type.String({ description: "ID du feedback ayant généré cette leçon" })
402
+ ),
403
+ merge_with: Type.Optional(
404
+ Type.String({ description: "ID d'une leçon existante à fusionner" })
405
+ ),
406
+ }),
407
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
408
+ let path: string;
409
+ if (params.scope === "global") {
410
+ path = globalPath();
411
+ } else if (params.scope === "domain") {
412
+ path = domainPath(params.domain);
413
+ } else {
414
+ path = projectPath(ctx.cwd);
415
+ }
416
+
417
+ const data = await readLessonFile(path);
418
+
419
+ // Check for merge
420
+ if (params.merge_with) {
421
+ const existing = data.lessons.find((l) => l.id === params.merge_with);
422
+ if (existing) {
423
+ existing.rule = params.rule;
424
+ existing.rationale = params.rationale;
425
+ existing.domain = params.domain;
426
+ if (params.source_feedback_id) {
427
+ existing.source_feedback.push(params.source_feedback_id);
428
+ }
429
+ await writeLessonFile(path, data);
430
+ return {
431
+ content: [
432
+ {
433
+ type: "text",
434
+ text: `Leçon fusionnée: [${params.domain}] ${params.rule}`,
435
+ },
436
+ ],
437
+ details: { merged: true, id: existing.id },
438
+ };
439
+ }
440
+ }
441
+
442
+ // New lesson
443
+ const lesson: Lesson = {
444
+ id: `l-${generateId()}`,
445
+ created: timestamp(),
446
+ domain: params.domain,
447
+ rule: params.rule,
448
+ rationale: params.rationale,
449
+ source_feedback: params.source_feedback_id
450
+ ? [params.source_feedback_id]
451
+ : [],
452
+ positive_examples: 0,
453
+ violations: 0,
454
+ active: true,
455
+ };
456
+ data.lessons.push(lesson);
457
+ await writeLessonFile(path, data);
458
+
459
+ return {
460
+ content: [
461
+ {
462
+ type: "text",
463
+ text: `Leçon sauvegardée (${params.scope}): [${params.domain}] ${params.rule}`,
464
+ },
465
+ ],
466
+ details: { id: lesson.id, scope: params.scope },
467
+ };
468
+ },
469
+ });
470
+
471
+ // ── Tool: list_lessons ─────────────────────────────────────────────────
472
+ // Let the agent query lessons programmatically
473
+ pi.registerTool({
474
+ name: "list_lessons",
475
+ label: "List Lessons",
476
+ description:
477
+ "Lister les leçons apprises pour un scope donné. Utile pour vérifier les leçons existantes avant d'en proposer une nouvelle.",
478
+ promptSnippet: "Lister les leçons d'auto-amélioration existantes",
479
+ parameters: Type.Object({
480
+ scope: Type.Union([
481
+ Type.Literal("global"),
482
+ Type.Literal("domain"),
483
+ Type.Literal("project"),
484
+ Type.Literal("all"),
485
+ ], { description: "Scope à lister" }),
486
+ domain: Type.Optional(
487
+ Type.Union([
488
+ Type.Literal("debug"),
489
+ Type.Literal("refactoring"),
490
+ Type.Literal("feature"),
491
+ Type.Literal("review"),
492
+ Type.Literal("general"),
493
+ ], { description: "Filtrer par domaine" })
494
+ ),
495
+ }),
496
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
497
+ const results: { scope: string; lessons: Lesson[] }[] = [];
498
+
499
+ if (params.scope === "global" || params.scope === "all") {
500
+ const data = await readLessonFile(globalPath());
501
+ const filtered = params.domain
502
+ ? data.lessons.filter(
503
+ (l) => l.active && l.domain === params.domain
504
+ )
505
+ : data.lessons.filter((l) => l.active);
506
+ if (filtered.length > 0) results.push({ scope: "global", lessons: filtered });
507
+ }
508
+
509
+ if (params.scope === "domain" || params.scope === "all") {
510
+ if (params.domain) {
511
+ const data = await readLessonFile(domainPath(params.domain));
512
+ const filtered = data.lessons.filter((l) => l.active);
513
+ if (filtered.length > 0)
514
+ results.push({ scope: `domain-${params.domain}`, lessons: filtered });
515
+ } else {
516
+ for (const d of DOMAINS) {
517
+ const data = await readLessonFile(domainPath(d));
518
+ const filtered = data.lessons.filter((l) => l.active);
519
+ if (filtered.length > 0)
520
+ results.push({ scope: `domain-${d}`, lessons: filtered });
521
+ }
522
+ }
523
+ }
524
+
525
+ if (params.scope === "project" || params.scope === "all") {
526
+ const data = await readLessonFile(projectPath(ctx.cwd));
527
+ const filtered = params.domain
528
+ ? data.lessons.filter(
529
+ (l) => l.active && l.domain === params.domain
530
+ )
531
+ : data.lessons.filter((l) => l.active);
532
+ if (filtered.length > 0) results.push({ scope: "project", lessons: filtered });
533
+ }
534
+
535
+ if (results.length === 0) {
536
+ return {
537
+ content: [{ type: "text", text: "Aucune leçon trouvée pour ce scope/domaine." }],
538
+ details: {},
539
+ };
540
+ }
541
+
542
+ const lines = results.flatMap((r) =>
543
+ r.lessons.map(
544
+ (l) => `[${r.scope}][${l.domain}] ${l.id}: ${l.rule}`
545
+ )
546
+ );
547
+
548
+ return {
549
+ content: [{ type: "text", text: lines.join("\n") }],
550
+ details: { results },
551
+ };
552
+ },
553
+ });
554
+ }
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@devilnside/pi-auto-improve",
3
+ "version": "1.0.0",
4
+ "description": "Auto-improvement skill for Pi Coding Agent — feedback analysis, lesson generation and consolidation",
5
+ "keywords": ["pi-package"],
6
+ "license": "MIT",
7
+ "author": "ajoye",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/devilnside/pi-auto-improve"
11
+ },
12
+ "pi": {
13
+ "extensions": ["./extensions"],
14
+ "skills": ["./skills"]
15
+ },
16
+ "peerDependencies": {
17
+ "@earendil-works/pi-coding-agent": "*",
18
+ "typebox": "*"
19
+ }
20
+ }
@@ -0,0 +1,103 @@
1
+ ---
2
+ name: auto-improve
3
+ description: "Use when the user gives negative feedback (👎, /bad) or asks to analyze a failure. Handles failure analysis, lesson generation, and lesson consolidation."
4
+ ---
5
+
6
+ # Auto-Improve
7
+
8
+ ## Overview
9
+
10
+ Analyse les échecs de l'agent, génère des leçons actionnables, et les consolide avec les leçons existantes. L'utilisateur valide chaque leçon avant stockage.
11
+
12
+ ## When to Use
13
+
14
+ - L'utilisateur donne un feedback négatif (`/bad`, bouton 👎)
15
+ - L'utilisateur donne un feedback positif (`/good`, bouton 👍)
16
+ - L'utilisateur demande une analyse d'échec
17
+ - L'utilisateur utilise `/lesson-add` ou `/lesson-remove`
18
+
19
+ ## Boutons Telegram — OBLIGATOIRE
20
+
21
+ **À CHAQUE réponse** (sauf si la réponse EST un feedback `/good`/`/bad`), ajouter les boutons à la fin :
22
+
23
+ ```
24
+ <!-- telegram_button label="👍" prompt="/good" -->
25
+ <!-- telegram_button label="👎" prompt="/bad" -->
26
+ ```
27
+
28
+ Ne PAS ajouter les boutons si :
29
+ - La réponse est elle-même un traitement de feedback
30
+ - L'utilisateur vient de cliquer 👍 ou 👎 (ça ferait une boucle)
31
+
32
+ ## Gestion des feedbacks entrants
33
+
34
+ ### Feedback positif (`/good`)
35
+
36
+ 1. Utiliser l'outil `save_lesson` avec `scope: "global"`, `domain: "general"`, une règle positive si pertinent, ou simplement accuser réception
37
+ 2. Répondre brièvement : "Merci ! 👍"
38
+ 3. NE PAS ajouter les boutons 👍/👎 à cette réponse
39
+
40
+ ### Feedback négatif (`/bad`)
41
+
42
+ Suivre le processus d'analyse ci-dessous. NE PAS ajouter les boutons 👍/👎 à la réponse de validation de leçon.
43
+
44
+ ## Processus d'analyse (feedback négatif)
45
+
46
+ ```dot
47
+ digraph analyze {
48
+ "Feedback négatif reçu" -> "Relire les derniers échanges (5-10 derniers messages)";
49
+ "Relire les derniers échanges" -> "Identifier la cause probable de l'insatisfaction";
50
+ "Identifier la cause probable" -> "Vérifier leçons existantes via list_lessons";
51
+ "Vérifier leçons existantes" -> "Leçon similaire?";
52
+ "Leçon similaire?" -> "Oui: Proposer fusion (affiner règle existante)" [label="Oui"];
53
+ "Leçon similaire?" -> "Non: Proposer nouvelle leçon" [label="Non"];
54
+ "Proposer fusion" -> "Présenter à l'utilisateur";
55
+ "Proposer nouvelle leçon" -> "Présenter à l'utilisateur";
56
+ "Présenter à l'utilisateur" -> "Validé?";
57
+ "Validé?" -> "Oui: Sauvegarder via save_lesson" [label="Oui"];
58
+ "Validé?" -> "Non: Ne rien stocker" [label="Non"];
59
+ }
60
+ ```
61
+
62
+ ## Domaines
63
+
64
+ | Domaine | Détection |
65
+ |---------|-----------|
66
+ | `debug` | Commandes de test, grep d'erreurs, stack traces |
67
+ | `refactoring` | Modifications multiples de fichiers existants |
68
+ | `feature` | Création de nouveaux fichiers, ajout de fonctionnalités |
69
+ | `review` | Lecture de code sans modification |
70
+ | `general` | Par défaut |
71
+
72
+ ## Format de leçon proposée
73
+
74
+ Quand une leçon est proposée, la présenter ainsi :
75
+
76
+ ```
77
+ 📝 Nouvelle leçon proposée :
78
+ Domaine : [domaine]
79
+ Règle : [règle concise en français]
80
+ Justification : [pourquoi]
81
+
82
+ Valider ? (oui/non)
83
+ ```
84
+
85
+ ## Format de fusion proposée
86
+
87
+ ```
88
+ 🔄 Fusion de leçons proposée :
89
+ Leçon existante : [ancienne règle]
90
+ Nouvelle règle fusionnée : [règle mise à jour]
91
+ Justification : [pourquoi la fusion]
92
+
93
+ Valider ? (oui/non)
94
+ ```
95
+
96
+ ## Règles
97
+
98
+ 1. Toujours formuler les leçons en français, de manière concise et actionnable
99
+ 2. Toujours valider avec l'utilisateur avant de stocker via `save_lesson`
100
+ 3. Préférer fusionner plutôt que dupliquer
101
+ 4. Détecter le domaine automatiquement si possible, sinon `general`
102
+ 5. Ne jamais stocker sans validation explicite de l'utilisateur
103
+ 6. Sur Telegram, toujours inclure les boutons 👍/👎 sauf si la réponse est un traitement de feedback