import * as fs from "node:fs"; import * as path from "node:path"; import * as os from "node:os"; import type { SkillDefinition, SkillStep } from "../core/types.js"; import { StateStore } from "../core/state-store.js"; import { stripHiddenUnicode } from "../core/unicode-safety.js"; const PAI_SKILLS_DIR = path.join(os.homedir(), ".claude", "skills"); export interface PaiSkillManifest { name: string; description: string; steps: string[]; tags: string[]; } export class SkillSync { private paiSkillsDir: string; constructor(paiSkillsDir?: string) { this.paiSkillsDir = paiSkillsDir ?? PAI_SKILLS_DIR; } findAllPaiSkills(): PaiSkillManifest[] { if (!fs.existsSync(this.paiSkillsDir)) return []; const skills: PaiSkillManifest[] = []; const entries = fs.readdirSync(this.paiSkillsDir, { withFileTypes: true }); for (const entry of entries) { if (!entry.isDirectory()) continue; const skillMd = path.join(this.paiSkillsDir, entry.name, "SKILL.md"); if (!fs.existsSync(skillMd)) continue; const content = stripHiddenUnicode(fs.readFileSync(skillMd, "utf-8")); const description = this.extractDescription(content); const steps = this.extractSteps(content); const tags = this.extractTags(content); skills.push({ name: entry.name, description, steps, tags }); } return skills; } toSkillDefinitions(skills: PaiSkillManifest[]): SkillDefinition[] { return skills.map((s, i) => ({ id: `pai-${s.name.toLowerCase().replace(/[^a-z0-9]/g, "-")}`, name: s.name, description: s.description, version: "1.0.0", tags: [...new Set(["pai-imported", ...s.tags])], contextTemplate: {}, steps: this.buildSteps(s), metrics: { totalExecutions: 0, avgDurationMs: 0, avgQualityScore: 0, successRate: 1.0, lastExecuted: null, evolutionCount: 0, }, history: [], dependencies: [], createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), })); } private buildSteps(skill: PaiSkillManifest): SkillStep[] { if (skill.steps.length > 0) { return skill.steps.map((label) => ({ id: StateStore.uid(), label, instruction: `Execute step: ${label}`, expectedOutcome: `Completed: ${label}`, validationCriteria: [], })); } return [ { id: StateStore.uid(), label: "Analyze", instruction: "Analyze the task using this PAI skill", expectedOutcome: "Analysis complete", validationCriteria: [] }, { id: StateStore.uid(), label: "Execute", instruction: `Execute: ${skill.description}`, expectedOutcome: "Execution complete", validationCriteria: [] }, { id: StateStore.uid(), label: "Verify", instruction: "Verify results against expected outcomes", expectedOutcome: "Verification complete", validationCriteria: [] }, ]; } private extractDescription(content: string): string { const descMatch = content.match(/description:\s*"([^"]+)"/); if (descMatch) return descMatch[1]; const lines = content.split("\n").slice(0, 10); return lines.find((l) => l.length > 20 && !l.startsWith("#"))?.trim() ?? "PAI skill"; } private extractSteps(content: string): string[] { const steps: string[] = []; const stepHeadings = content.match(/### .+Step[^:]*:/gi) ?? []; const numberedSteps = content.match(/^\d+\.\s+(.+)$/gm) ?? []; for (const h of stepHeadings) { steps.push(h.replace(/### /, "").trim()); } for (const ns of numberedSteps) { steps.push(ns.replace(/^\d+\.\s+/, "").trim()); } return steps; } /** * Compound a lesson into a PAI skill's SKILL.md. * Appends a "Lessons Learned" section entry so the skill sharpens over time. * This implements the "write the lesson into the Skill" pattern from the Fable 5 playbook. * * @returns the skill file path that was updated, or null if skill not found */ compoundLesson(skillName: string, lesson: string, source: string): string | null { const skillPath = path.join(this.paiSkillsDir, skillName, "SKILL.md"); if (!fs.existsSync(skillPath)) return null; let content = stripHiddenUnicode(fs.readFileSync(skillPath, "utf-8")); // Find or create a Lessons Learned section const lessonsHeader = "## Lessons Learned"; const entry = `- ${new Date().toISOString().slice(0, 10)}: ${lesson} (from ${source})`; if (content.includes(lessonsHeader)) { // Append after the Lessons Learned header const idx = content.indexOf(lessonsHeader); const afterHeader = content.indexOf("\n", idx); content = content.slice(0, afterHeader + 1) + "\n" + entry + content.slice(afterHeader + 1); } else { // Append before EOF — create a new Lessons Learned section const trimmed = content.trimEnd(); content = trimmed + "\n\n" + lessonsHeader + "\n\n" + entry + "\n"; } fs.writeFileSync(skillPath, content, "utf-8"); return skillPath; } /** * Find the most relevant PAI skill for a given task or failure context. * Scores skills by tag overlap with the context. */ findRelevantSkill(context: string): { name: string; score: number } | null { const skills = this.findAllPaiSkills(); if (skills.length === 0) return null; const ctxLower = context.toLowerCase(); let best: { name: string; score: number } | null = null; for (const skill of skills) { let score = 0; for (const tag of skill.tags) { if (ctxLower.includes(tag.toLowerCase())) score += 3; } for (const step of skill.steps) { if (ctxLower.includes(step.toLowerCase())) score += 1; } if (ctxLower.includes(skill.name.toLowerCase())) score += 5; if (ctxLower.includes(skill.description.toLowerCase().slice(0, 20))) score += 2; if (!best || score > best.score) { best = { name: skill.name, score }; } } return best && best.score > 0 ? best : null; } private extractTags(content: string): string[] { const tags: string[] = []; const tagMatch = content.match(/tags:\s*\[([^\]]+)\]/); if (tagMatch) { tags.push(...tagMatch[1].split(",").map((t) => t.trim().replace(/"/g, ""))); } const keywordMatch = content.match(/(?:USE WHEN|Keywords?):\s*(.+)/i); if (keywordMatch) { tags.push(...keywordMatch[1].split(",").map((t) => t.trim().toLowerCase().replace(/\s+/g, "-"))); } return tags; } }