/** * pi-gepa v1.2 — Skill scanner + quality scorer for Pi * * The SCANNER component of the pi-evolve ecosystem. * /gepa scans and scores skills. /evolve (from pi-evolve) mutates them. * Together: scan → identify weak skills → evolve them. * * /gepa scan — score all skills * /gepa top [N] — best N skills * /gepa bottom [N] — worst N skills (candidates for pi-evolve) * /gepa analyze — deep analysis of one skill * * Pair with: pi install npm:@artale/pi-evolve * Then: /gepa bottom 5 → pick worst → /darwin SKILL.md 5 */ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; import { readFileSync, existsSync, readdirSync, statSync } from "node:fs"; import { join, basename, dirname } from "node:path"; // ANSI colors const B = "\x1b[1m", C = "\x1b[36m", G = "\x1b[32m", Y = "\x1b[33m"; const R = "\x1b[31m", D = "\x1b[2m", RST = "\x1b[0m"; interface SkillStats { name: string; path: string; lines: number; words: number; sections: number; codeBlocks: number; sizeBytes: number; score: number; } function scoreSkill(content: string): number { const lines = content.trim().split("\n"); const words = content.split(/\s+/).length; const sections = lines.filter(l => l.startsWith("#")).length; const codeBlocks = Math.floor((content.match(/```/g) || []).length / 2); let score = 0; if (sections >= 3) score += 0.1; if (codeBlocks >= 1) score += 0.1; if (content.includes("## ")) score += 0.1; if (words < 2000) score += 0.15; if (words < 1000) score += 0.15; else if (words > 3000) score -= 0.1; const keys = ["trigger", "when", "use", "example", "step", "workflow", "output"]; const found = keys.filter(k => content.toLowerCase().includes(k)).length; score += 0.4 * (found / keys.length); return Math.max(0, Math.min(1, score)); } function analyzeSkill(skillPath: string): SkillStats { const content = readFileSync(skillPath, "utf-8"); const lines = content.trim().split("\n"); const words = content.split(/\s+/).length; return { name: basename(dirname(skillPath)), path: skillPath, lines: lines.length, words, sections: lines.filter(l => l.startsWith("#")).length, codeBlocks: Math.floor((content.match(/```/g) || []).length / 2), sizeBytes: Buffer.byteLength(content), score: scoreSkill(content), }; } function findSkills(dir: string): string[] { const results: string[] = []; if (!existsSync(dir)) return results; try { for (const entry of readdirSync(dir, { withFileTypes: true })) { const full = join(dir, entry.name); if (entry.isDirectory()) { const skill = join(full, "SKILL.md"); if (existsSync(skill)) results.push(skill); try { for (const sub of readdirSync(full, { withFileTypes: true })) { if (sub.isDirectory()) { const subSkill = join(full, sub.name, "SKILL.md"); if (existsSync(subSkill)) results.push(subSkill); } } } catch {} } } } catch {} return results; } function formatScore(score: number): string { if (score >= 0.85) return `${G}${score.toFixed(2)}${RST}`; if (score >= 0.65) return `${Y}${score.toFixed(2)}${RST}`; return `${R}${score.toFixed(2)}${RST}`; } function dashboard(skills: SkillStats[]): string { const sorted = [...skills].sort((a, b) => b.score - a.score); const avg = sorted.reduce((s, sk) => s + sk.score, 0) / Math.max(1, sorted.length); let out = `${B}${C}GEPA Skill Analysis${RST}\n\n`; out += ` Skills: ${sorted.length} | Avg: ${formatScore(avg)} | `; out += `Best: ${sorted[0]?.name} (${sorted[0]?.score.toFixed(2)}) | `; out += `Worst: ${sorted[sorted.length - 1]?.name} (${sorted[sorted.length - 1]?.score.toFixed(2)})\n\n`; out += ` ${D}${"Skill".padEnd(30)} Score Words Sects Code${RST}\n`; out += ` ${D}${"─".repeat(60)}${RST}\n`; for (const sk of sorted) { out += ` ${sk.name.padEnd(30)} ${formatScore(sk.score)} ${String(sk.words).padStart(5)} ${String(sk.sections).padStart(5)} ${String(sk.codeBlocks).padStart(4)}\n`; } const candidates = sorted.filter(s => s.score < 0.7).reverse(); if (candidates.length > 0) { out += `\n${B}${Y}Evolution Candidates (< 0.70):${RST}\n`; for (const c of candidates.slice(0, 5)) { out += ` ${R}${c.name}${RST} (${c.score.toFixed(2)}): ${c.words}w\n`; } } return out; } function analyzeOne(skillPath: string): string { const stats = analyzeSkill(skillPath); const content = readFileSync(skillPath, "utf-8"); let out = `${B}${C}GEPA: ${stats.name}${RST}\n\n`; out += ` Score: ${formatScore(stats.score)} | Words: ${stats.words} | Sections: ${stats.sections} | Code: ${stats.codeBlocks}\n\n`; const keys = ["trigger", "when", "use", "example", "step", "workflow", "output"]; const found = keys.filter(k => content.toLowerCase().includes(k)); const missing = keys.filter(k => !content.toLowerCase().includes(k)); out += ` ${B}Completeness:${RST} ${found.map(k => `${G}${k}${RST}`).join(" ")}`; if (missing.length) out += ` ${missing.map(k => `${R}${k}${RST}`).join(" ")}`; out += `\n\n ${B}Mutations:${RST}\n`; if (stats.words > 2000) out += ` ${Y}trim${RST} — ${stats.words} -> <1000 words\n`; if (stats.sections < 3) out += ` ${Y}restructure${RST} — add sections\n`; if (stats.codeBlocks < 1) out += ` ${Y}sharpen${RST} — add code examples\n`; if (missing.length > 0) out += ` ${Y}complete${RST} — add ${missing.join(", ")}\n`; return out; } export default function init(pi: ExtensionAPI) { pi.addCommand({ name: 'gepa', description: "GEPA skill evolution dashboard", handler: async (args: string) => { const parts = (args || "").trim().split(/\s+/); const cmd = parts[0] || "scan"; const home = process.env.HOME || process.env.USERPROFILE || ""; const skillDirs = [ join(home, ".pi", "agent", "skills"), join(home, "Projects", "pi-doodlestein", "skills"), ].filter(d => existsSync(d)); const allSkillPaths = skillDirs.flatMap(findSkills); const allSkills = allSkillPaths.map(analyzeSkill); if (cmd === "scan" || cmd === "all") return dashboard(allSkills); if (cmd === "top") { const n = parseInt(parts[1]) || 10; const sorted = [...allSkills].sort((a, b) => b.score - a.score).slice(0, n); return sorted.map(s => ` ${formatScore(s.score)} ${s.name.padEnd(30)} ${D}${s.words}w${RST}`).join("\n"); } if (cmd === "bottom") { const n = parseInt(parts[1]) || 10; const sorted = [...allSkills].sort((a, b) => a.score - b.score).slice(0, n); return sorted.map(s => ` ${formatScore(s.score)} ${s.name.padEnd(30)} ${D}${s.words}w${RST}`).join("\n"); } if (cmd === "analyze" || cmd === "check") { const name = parts[1]; if (!name) return `${Y}Usage:${RST} /gepa analyze `; const skill = allSkillPaths.find(p => basename(dirname(p)) === name); if (!skill) return `${R}Not found:${RST} ${name}`; return analyzeOne(skill); } if (cmd === "help") { return `${B}GEPA${RST} — /gepa [scan|top|bottom|analyze |help]`; } // Default: treat as skill name const skill = allSkillPaths.find(p => basename(dirname(p)) === cmd); if (skill) return analyzeOne(skill); return `${Y}Unknown:${RST} ${cmd}. Try /gepa help`; }}); }