generate_skill
Generate Claude skills by analyzing repeated feedback patterns. Clusters failures by tags and creates SKILL.md files with actionable DO/INSTEAD rules.
Instructions
Auto-generate Claude skills from repeated feedback patterns. Clusters failure patterns by tags and produces SKILL.md files with DO/INSTEAD rules.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| minOccurrences | No | Minimum pattern occurrences to trigger skill generation (default 3) | |
| tags | No | Filter to specific tags |
Implementation Reference
- adapters/mcp/server-stdio.js:400-408 (handler)The handler for 'generate_skill' which calls the `generateSkills` function imported from `scripts/skill-generator.js`.
case 'generate_skill': return toTextResult({ skills: generateSkills({ minClusterSize: Number(args.minOccurrences || 3), }).filter((entry) => { if (!Array.isArray(args.tags) || args.tags.length === 0) return true; return args.tags.some((tag) => entry.skillName.includes(String(tag))); }), }); - scripts/skill-generator.js:334-421 (handler)The core implementation of the skill generation logic, which processes feedback, clusters it, and writes SKILL.md files.
function generateSkills(options) { if (!options) options = {}; const feedbackDir = options.feedbackDir || discoverFeedbackDir(); const minClusterSize = options.minClusterSize || MIN_CLUSTER_SIZE; const minTagOverlap = options.minTagOverlap || MIN_TAG_OVERLAP; const dryRun = options.dryRun || false; const logPath = path.join(feedbackDir, 'feedback-log.jsonl'); const outputDir = path.join(feedbackDir, 'generated-skills'); const auditLogPath = path.join(feedbackDir, 'skill-generation-audit.jsonl'); const entries = parseFeedbackFile(logPath); if (entries.length === 0) return []; // Separate positive and negative entries const posEntries = []; const negEntries = []; for (const entry of entries) { const cls = classifySignal(entry); if (cls === 'positive') posEntries.push(entry); else if (cls === 'negative') negEntries.push(entry); } if (negEntries.length === 0) return []; // Cluster negative feedback by tag overlap const clusters = clusterByTags(negEntries, minTagOverlap); const results = []; for (const [key, cluster] of clusters) { if (cluster.entries.length < minClusterSize) continue; // Compute domain-scoped approval rate const clusterTags = cluster.tags; let domainPos = 0; const domainNeg = cluster.entries.length; for (const pe of posEntries) { if (tagOverlap(extractTags(pe), clusterTags) >= 1) domainPos++; } const domainTotal = domainPos + domainNeg; const approvalRate = domainTotal > 0 ? `${((domainPos / domainTotal) * 100).toFixed(1)}%` : '0.0%'; const doRules = buildDoRules(posEntries, clusterTags); const insteadRules = buildInsteadRules(cluster.entries); const ruleCount = doRules.length + insteadRules.length; const skillContent = generateSkillFromCluster({ tags: clusterTags, entries: cluster.entries, doRules, insteadRules, approvalRate, }); const skillName = slugify(clusterTags.slice(0, 3).join('-')) || 'unnamed'; const fileName = `${skillName}.SKILL.md`; const filePath = path.join(outputDir, fileName); if (!dryRun) { ensureDir(outputDir); fs.writeFileSync(filePath, skillContent, 'utf8'); // Audit log appendJSONL(auditLogPath, { event: 'skill_generated', skillName, filePath, ruleCount, evidenceCount: cluster.entries.length, tags: clusterTags, approvalRate, timestamp: new Date().toISOString(), }); } results.push({ skillName, filePath, ruleCount, evidenceCount: cluster.entries.length, }); } return results; }