/** * pi-evolve — Evolutionary self-improvement for Pi * * Inspired by AlphaEvolve (DeepMind), Sakana AI's ShinkaEvolve, * and the Darwin Gödel Machine. Applies evolutionary algorithms * to prompts, skills, extensions, and agent behavior. * * Core loop: Mutate → Evaluate → Select → Archive → Repeat * * Commands: * /evolve — evolve a skill or prompt file * /evolve status — show evolution state * /evolve archive — list evolved variants with scores * /evolve restore — restore a variant * /evolve compare — diff two variants * /mutate [goal] — generate N variants of a file * /darwin [gens] [goal] — full evolutionary loop * * Strategies: * - Token compression: reduce size while preserving quality * - Quality improvement: improve output accuracy/completeness * - Hybrid: optimize both simultaneously (Pareto front) */ import type { ExtensionAPI } from '@anthropic-ai/claude-code' import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync } from 'fs' import { join, basename, dirname, extname } from 'path' import { homedir } from 'os' const EVOLVE_DIR = join(homedir(), '.pi', 'evolve') const ARCHIVE_FILE = join(EVOLVE_DIR, 'archive.json') interface Variant { id: number generation: number parentId: number | null filePath: string content: string fitness: number // 0-100 tokenCount: number strategy: string timestamp: number notes: string survived: boolean } interface EvolutionRun { id: string targetFile: string goal: string strategy: string generations: number currentGen: number populationSize: number bestFitness: number bestVariantId: number startedAt: number status: 'running' | 'complete' | 'paused' } interface Archive { variants: Variant[] runs: EvolutionRun[] nextId: number } function ensureDir() { mkdirSync(EVOLVE_DIR, { recursive: true }) } function loadArchive(): Archive { ensureDir() try { return JSON.parse(readFileSync(ARCHIVE_FILE, 'utf-8')) } catch { return { variants: [], runs: [], nextId: 1 } } } function saveArchive(archive: Archive) { ensureDir() writeFileSync(ARCHIVE_FILE, JSON.stringify(archive, null, 2)) } function estimateTokens(text: string): number { return Math.ceil(text.length / 4) } // --- Mutation strategies --- const MUTATION_PROMPTS: Record = { compress: `You are an expert prompt/code compressor. Your task: 1. Read the content below carefully 2. Rewrite it to be SHORTER while preserving ALL functionality and meaning 3. Remove redundancy, verbose explanations, and unnecessary examples 4. Use concise language, bullet points, and terse phrasing 5. Preserve all code blocks, file paths, and technical details exactly 6. Target: 30-50% size reduction IMPORTANT: The output must be a COMPLETE replacement — not a summary.`, quality: `You are an expert prompt/code improver. Your task: 1. Read the content below carefully 2. Rewrite it to be MORE EFFECTIVE while keeping similar length 3. Add missing edge cases, clearer instructions, better structure 4. Fix any logical gaps, ambiguities, or unclear phrasing 5. Improve the organization and flow 6. Make it more actionable and less theoretical IMPORTANT: The output must be a COMPLETE replacement — not commentary.`, hybrid: `You are an evolutionary optimizer. Your task: 1. Read the content below carefully 2. Make it BOTH shorter AND better 3. Remove what doesn't earn its tokens (verbose explanations, redundant examples) 4. Strengthen what matters (core instructions, critical details, decision rules) 5. Restructure for maximum information density 6. Target: 20-40% shorter, same or better effectiveness IMPORTANT: The output must be a COMPLETE replacement — not a summary.`, radical: `You are a creative mutator. Your task: 1. Read the content below carefully 2. REIMAGINE it from scratch with a completely different structure 3. Same goals and information, but radically different organization 4. Try: tables instead of prose, numbered rules instead of paragraphs, inverted structure 5. Be bold — the point is divergent exploration IMPORTANT: The output must be a COMPLETE replacement that achieves the same goals.`, } function buildMutationPrompt(content: string, strategy: string, goal?: string): string { const base = MUTATION_PROMPTS[strategy] || MUTATION_PROMPTS.hybrid const goalLine = goal ? `\n\nAdditional goal: ${goal}` : '' return `${base}${goalLine}\n\n---\nCONTENT TO EVOLVE:\n---\n\n${content}` } // --- Fitness evaluation --- function evaluateFitness(original: string, variant: string, strategy: string): number { const origTokens = estimateTokens(original) const varTokens = estimateTokens(variant) const compression = 1 - (varTokens / origTokens) // Basic heuristics (real fitness would use LLM evaluation) let score = 50 // baseline // Token efficiency if (strategy === 'compress' || strategy === 'hybrid') { if (compression > 0.5) score += 25 else if (compression > 0.3) score += 20 else if (compression > 0.15) score += 10 else if (compression < 0) score -= 15 // got bigger = bad } // Structure quality signals const hasHeaders = (variant.match(/^#+\s/gm) || []).length const hasBullets = (variant.match(/^[-*]\s/gm) || []).length const hasCodeBlocks = (variant.match(/```/g) || []).length const hasTable = variant.includes('|') && variant.includes('---') if (hasHeaders > 0) score += 5 if (hasBullets > 3) score += 5 if (hasCodeBlocks > 0) score += 5 if (hasTable) score += 5 // Penalize if too short (likely lost content) if (varTokens < origTokens * 0.2) score -= 20 // Penalize if variant is mostly the mutation prompt itself if (variant.includes('Your task:') || variant.includes('CONTENT TO EVOLVE')) score -= 30 return Math.max(0, Math.min(100, score)) } // --- Formatting --- function formatArchive(archive: Archive): string { if (archive.variants.length === 0) return 'No evolved variants yet. Use `/evolve ` or `/darwin ` to start.' const lines = ['## Evolution Archive', ''] // Group by file const byFile = new Map() for (const v of archive.variants) { const existing = byFile.get(v.filePath) || [] existing.push(v) byFile.set(v.filePath, existing) } for (const [file, variants] of byFile) { const best = variants.reduce((a, b) => a.fitness > b.fitness ? a : b) const origTokens = variants.find(v => v.generation === 0)?.tokenCount || 0 lines.push(`### ${basename(file)}`) lines.push(`- Variants: ${variants.length}`) lines.push(`- Best fitness: **${best.fitness}/100** (gen ${best.generation})`) if (origTokens > 0) { const reduction = Math.round((1 - best.tokenCount / origTokens) * 100) lines.push(`- Token reduction: **${reduction}%** (${origTokens} → ${best.tokenCount})`) } lines.push('') } // Recent runs if (archive.runs.length > 0) { lines.push('### Recent Runs') for (const run of archive.runs.slice(-5)) { lines.push(`- ${run.id}: ${basename(run.targetFile)} — gen ${run.currentGen}/${run.generations} — best ${run.bestFitness}/100 — ${run.status}`) } } return lines.join('\n') } function formatStatus(archive: Archive): string { const activeRuns = archive.runs.filter(r => r.status === 'running') const totalVariants = archive.variants.length const totalRuns = archive.runs.length const bestEver = archive.variants.length > 0 ? archive.variants.reduce((a, b) => a.fitness > b.fitness ? a : b) : null const lines = [ '## Evolution Status', '', `**Total variants:** ${totalVariants}`, `**Total runs:** ${totalRuns}`, `**Active runs:** ${activeRuns.length}`, ] if (bestEver) { lines.push(`**Best ever:** ${bestEver.fitness}/100 on ${basename(bestEver.filePath)} (gen ${bestEver.generation})`) } lines.push('', '**Strategies:** compress, quality, hybrid, radical') lines.push('', '**Commands:**') lines.push('- `/evolve ` — quick evolution (3 variants)') lines.push('- `/darwin [gens] [goal]` — full evolutionary loop') lines.push('- `/mutate [goal]` — single mutation pass') lines.push('- `/evolve archive` — list all variants') lines.push('- `/evolve restore ` — restore a variant') return lines.join('\n') } export default function init(pi: ExtensionAPI) { pi.addCommand({ name: 'evolve', description: 'Evolutionary self-improvement — mutate, evaluate, select prompts/skills/code', handler: async (args) => { const parts = args.trim().split(/\s+/) const sub = parts[0]?.toLowerCase() const archive = loadArchive() if (!sub || sub === 'status') { pi.sendMessage({ content: formatStatus(archive), display: true }, { triggerTurn: false }) return } if (sub === 'archive') { pi.sendMessage({ content: formatArchive(archive), display: true }, { triggerTurn: false }) return } if (sub === 'restore') { const id = parseInt(parts[1], 10) const variant = archive.variants.find(v => v.id === id) if (!variant) { pi.sendMessage({ content: `Variant ${id} not found.`, display: true }, { triggerTurn: false }); return } writeFileSync(variant.filePath, variant.content, 'utf-8') pi.sendMessage({ content: `Restored variant ${id} (fitness ${variant.fitness}) to ${variant.filePath}`, display: true }, { triggerTurn: false }) return } if (sub === 'compare') { const idA = parseInt(parts[1], 10) const idB = parseInt(parts[2], 10) const a = archive.variants.find(v => v.id === idA) const b = archive.variants.find(v => v.id === idB) if (!a || !b) { pi.sendMessage({ content: 'Variant not found.', display: true }, { triggerTurn: false }); return } pi.sendMessage({ content: `## Compare ${idA} vs ${idB}\n\n| Metric | #${idA} | #${idB} |\n|--------|-------|-------|\n| Fitness | ${a.fitness} | ${b.fitness} |\n| Tokens | ${a.tokenCount} | ${b.tokenCount} |\n| Gen | ${a.generation} | ${b.generation} |\n| Strategy | ${a.strategy} | ${b.strategy} |`, display: true, }, { triggerTurn: false }) return } // Default: evolve a file const filePath = sub if (!existsSync(filePath)) { pi.sendMessage({ content: `File not found: ${filePath}. Provide a path to a skill, prompt, or code file.`, display: true }, { triggerTurn: false }) return } const goal = parts.slice(1).join(' ') || undefined pi.sendMessage({ content: `Starting evolution of **${basename(filePath)}**...\n\nThis will generate variants using the LLM, evaluate fitness, and archive the best.\n\nGoal: ${goal || 'optimize (compress + improve)'}`, display: true, }, { triggerTurn: true }) }, }) pi.addCommand({ name: 'mutate', description: 'Generate variants of a file using evolutionary mutation', handler: async (args) => { const parts = args.trim().split(/\s+/) const filePath = parts[0] const goal = parts.slice(1).join(' ') || undefined if (!filePath || !existsSync(filePath)) { pi.sendMessage({ content: 'Usage: /mutate [goal]\n\nGenerates 3 variants using different strategies.', display: true }, { triggerTurn: false }) return } const content = readFileSync(filePath, 'utf-8') const archive = loadArchive() // Archive original const origVariant: Variant = { id: archive.nextId++, generation: 0, parentId: null, filePath, content, fitness: 50, tokenCount: estimateTokens(content), strategy: 'original', timestamp: Date.now(), notes: 'Original file', survived: true, } archive.variants.push(origVariant) saveArchive(archive) pi.sendMessage({ content: `Archived original (${origVariant.tokenCount} tokens, id #${origVariant.id}).\n\nNow mutate this file with the following prompt. Generate 3 variants using these strategies:\n\n**1. Compress:** ${buildMutationPrompt('', 'compress', goal).split('\n')[0]}\n**2. Quality:** ${buildMutationPrompt('', 'quality', goal).split('\n')[0]}\n**3. Radical:** ${buildMutationPrompt('', 'radical', goal).split('\n')[0]}\n\nRead the file at \`${filePath}\` and produce 3 complete rewrites.`, display: true, }, { triggerTurn: true }) }, }) pi.addCommand({ name: 'darwin', description: 'Full evolutionary loop — mutate, evaluate, select over multiple generations', handler: async (args) => { const parts = args.trim().split(/\s+/) const filePath = parts[0] const gens = parseInt(parts[1], 10) || 5 const goal = parts.slice(2).join(' ') || 'optimize for token efficiency and quality' if (!filePath || !existsSync(filePath)) { pi.sendMessage({ content: 'Usage: /darwin [generations=5] [goal]\n\nRuns a full evolutionary loop.', display: true }, { triggerTurn: false }) return } const content = readFileSync(filePath, 'utf-8') const tokens = estimateTokens(content) const archive = loadArchive() const runId = `run-${Date.now()}` const run: EvolutionRun = { id: runId, targetFile: filePath, goal, strategy: 'hybrid', generations: gens, currentGen: 0, populationSize: 3, bestFitness: 50, bestVariantId: 0, startedAt: Date.now(), status: 'running', } archive.runs.push(run) saveArchive(archive) pi.sendMessage({ content: [ `## Darwin Evolution Started`, ``, `**Target:** ${basename(filePath)} (${tokens} tokens)`, `**Generations:** ${gens}`, `**Goal:** ${goal}`, `**Strategy:** hybrid (compress + quality + radical)`, `**Run ID:** ${runId}`, ``, `Starting generation 1/${gens}...`, ``, `Read \`${filePath}\`, generate 3 variants (compress, quality, radical), evaluate each, select the best, and repeat for ${gens} generations. After each generation, save the best variant and report fitness scores.`, ].join('\n'), display: true, }, { triggerTurn: true }) }, }) // Tools pi.addTool({ name: 'evolve_archive', description: 'Save an evolved variant to the archive with fitness score.', parameters: { type: 'object', properties: { filePath: { type: 'string', description: 'Original file path' }, content: { type: 'string', description: 'Evolved content' }, fitness: { type: 'number', description: 'Fitness score 0-100' }, generation: { type: 'number', description: 'Generation number' }, strategy: { type: 'string', description: 'Mutation strategy used' }, notes: { type: 'string', description: 'Notes about this variant' }, parentId: { type: 'number', description: 'Parent variant ID' }, }, required: ['filePath', 'content', 'fitness'], }, handler: async (params: any) => { const archive = loadArchive() const variant: Variant = { id: archive.nextId++, generation: params.generation || 1, parentId: params.parentId || null, filePath: params.filePath, content: params.content, fitness: params.fitness, tokenCount: estimateTokens(params.content), strategy: params.strategy || 'unknown', timestamp: Date.now(), notes: params.notes || '', survived: params.fitness > 50, } archive.variants.push(variant) // Update run if exists const activeRun = archive.runs.find(r => r.status === 'running' && r.targetFile === params.filePath) if (activeRun) { activeRun.currentGen = params.generation || activeRun.currentGen + 1 if (params.fitness > activeRun.bestFitness) { activeRun.bestFitness = params.fitness activeRun.bestVariantId = variant.id } if (activeRun.currentGen >= activeRun.generations) { activeRun.status = 'complete' } } saveArchive(archive) const origVariant = archive.variants.find(v => v.filePath === params.filePath && v.generation === 0) const reduction = origVariant ? Math.round((1 - variant.tokenCount / origVariant.tokenCount) * 100) : 0 return `Archived variant #${variant.id} — fitness: ${variant.fitness}/100, tokens: ${variant.tokenCount} (${reduction > 0 ? `-${reduction}%` : `+${Math.abs(reduction)}%`}), strategy: ${variant.strategy}` }, }) pi.addTool({ name: 'evolve_status', description: 'Show evolution archive status — total variants, runs, best scores.', parameters: { type: 'object', properties: {} }, handler: async () => formatStatus(loadArchive()), }) pi.addTool({ name: 'evolve_list', description: 'List all evolved variants with fitness scores and token counts.', parameters: { type: 'object', properties: {} }, handler: async () => formatArchive(loadArchive()), }) pi.addTool({ name: 'evolve_restore', description: 'Restore an evolved variant by ID — writes it back to the original file.', parameters: { type: 'object', properties: { id: { type: 'number', description: 'Variant ID to restore' }, }, required: ['id'], }, handler: async (params: { id: number }) => { const archive = loadArchive() const variant = archive.variants.find(v => v.id === params.id) if (!variant) return `Variant ${params.id} not found.` writeFileSync(variant.filePath, variant.content, 'utf-8') return `Restored variant #${params.id} (fitness ${variant.fitness}, ${variant.tokenCount} tokens) → ${variant.filePath}` }, }) }