aboutsummaryrefslogtreecommitdiff
path: root/modules/ext/skill-pack-core.js
diff options
context:
space:
mode:
authorAgando <roduccomm@gmail.com>2026-07-09 06:00:32 +0200
committerAgando <roduccomm@gmail.com>2026-07-09 06:00:32 +0200
commit2538c91876455fe96febf441fc8dde20f2606023 (patch)
treeb74c949cdf4296d10103dd57ef8178ba427a5b39 /modules/ext/skill-pack-core.js
parentea4d09fb5ccba12ed774fcf7c913edfa1e6a851a (diff)
auto: 2026-07-09_06:00 changes
Diffstat (limited to 'modules/ext/skill-pack-core.js')
-rw-r--r--modules/ext/skill-pack-core.js298
1 files changed, 298 insertions, 0 deletions
diff --git a/modules/ext/skill-pack-core.js b/modules/ext/skill-pack-core.js
index 455a7f4..f204e70 100644
--- a/modules/ext/skill-pack-core.js
+++ b/modules/ext/skill-pack-core.js
@@ -35,6 +35,10 @@ class SkillPackCoreModule {
'skill-export': 'skill-export <name> — Export a skill as JSON',
'skill-import': 'skill-import <json> — Import a skill from JSON string',
'skill-sync': 'skill-sync — Rebuild skills index from /skills/ directory',
+ 'learn-skill': 'learn-skill <topic> — Reflect on topic, draft a skill from experience',
+ 'skill-validate': 'skill-validate <name> — Validate skill structure and get confidence score',
+ 'skill-audit': 'skill-audit — Audit skills for stale, duplicate, and broken references',
+ 'skill-restore': 'skill-restore <name> — Restore an archived skill',
'skill-webdav-sync': 'skill-webdav-sync — Push all skills to WebDAV as JSON files',
};
}
@@ -528,6 +532,11 @@ class SkillPackCoreModule {
this.commands['skill-export'] = (args) => this._cmdSkillExport(args);
this.commands['skill-import'] = (args) => this._cmdSkillImport(args);
this.commands['skill-sync'] = () => this._cmdSkillSync();
+ // v4.5.90: Agent Skills — learn, validate, audit, restore
+ this.commands['learn-skill'] = (args) => this._cmdLearnSkill(args);
+ this.commands['skill-validate'] = (args) => this._cmdSkillValidate(args);
+ this.commands['skill-audit'] = () => this._cmdSkillAudit();
+ this.commands['skill-restore'] = (args) => this._cmdSkillRestore(args);
this.commands['skill-webdav-sync'] = () => this._cmdSkillWebdavSync();
}
// ── Skill Commands ──
@@ -574,6 +583,9 @@ class SkillPackCoreModule {
this.terminal.print(line, '');
}
this.terminal.print(`=== End ${name} ===`, 'info');
+ // v4.5.90: Record skill use for decay tracking
+ const mg = this.terminal?.moduleManager?.modules?.['MemoryGraphModule'];
+ if (mg && typeof mg.recordSkillUse === 'function') mg.recordSkillUse(name, true);
}
_cmdSkillCreate(args) {
@@ -846,6 +858,292 @@ class SkillPackCoreModule {
}
this._seeded = true;
}
+
+ // ── v4.5.90: Agent Skills ──
+
+ _cmdLearnSkill(args) {
+ if (!args || args.length === 0) return this.terminal.print('Usage: learn-skill <topic>', 'warning');
+ const topic = args.join(' ');
+ this.terminal.print('🧠 Reflecting on: ' + topic + '...', 'info');
+
+ // 1. Pull context from memgraph
+ const mg = this.terminal?.moduleManager?.modules?.['MemoryGraphModule'];
+ let graphNodes = [];
+ if (mg) {
+ graphNodes = mg.findNodes(topic) || [];
+ this.terminal.print(' 📊 Found ' + graphNodes.length + ' graph nodes matching topic', 'info');
+ }
+
+ // 2. Pull recent sessions
+ const fs = this.terminal.fs;
+ let sessionInsights = [];
+ try {
+ const memDir = fs.readDir('/memory/') || [];
+ // v4.5.90b fix: readDir returns objects, filter to .json files (P0 from review)
+ const recent = memDir.filter(function(e) { return e.type === 'file' && e.name.endsWith('.json'); }).slice(-3);
+ for (const f of recent) {
+ const data = fs.fsReadJSON('/memory/' + f.name);
+ if (data && data.summary) sessionInsights.push(data.summary);
+ }
+ } catch (_) {}
+ this.terminal.print(' 📅 Loaded ' + sessionInsights.length + ' recent sessions', 'info');
+
+ // 3. Pull existing skills
+ const index = fs.fsReadJSON('/skills/index.json') || {};
+ const skillNames = Object.keys(index).filter(function(n) {
+ return n.toLowerCase().includes(topic.toLowerCase().split(' ')[0]);
+ });
+ this.terminal.print(' 📚 Found ' + skillNames.length + ' related skills', 'info');
+
+ // 4. Draft skill
+ const draft = this._draftSkill(topic, graphNodes, sessionInsights, skillNames);
+ this.terminal.print('', '');
+ this.terminal.print('📝 Draft skill:', 'header');
+ const lines = draft.split('\n');
+ for (const line of lines) this.terminal.print(line, '');
+ this.terminal.print('', '');
+
+ // 5. Validate
+ const validation = this._validateSkillContent(draft);
+ if (validation.errors.length > 0) {
+ this.terminal.print('❌ Validation errors:', 'error');
+ for (const e of validation.errors) this.terminal.print(' • ' + e, 'error');
+ this.terminal.print('Fix the issues and use skill-create to save manually.', 'info');
+ return;
+ }
+ this.terminal.print('✅ Validation passed. Confidence: ' + validation.confidence.toFixed(2), 'success');
+
+ // 6. Propose
+ this.terminal.print('Ship this skill? Use: skill-create ' + topic.replace(/\s+/g, '-') + ' "<desc>" "<content>"', 'info');
+ this.terminal.print('Or discard and continue.', 'text-muted');
+ }
+
+ _draftSkill(topic, graphNodes, sessions, existingSkills) {
+ var lines = [];
+ lines.push('---');
+ lines.push('name: ' + topic.toLowerCase().replace(/\s+/g, '-'));
+ lines.push('description: ' + topic + ' — auto-drafted from agent experience');
+ lines.push('tags: [auto-learned, ' + topic.toLowerCase().split(' ')[0] + ']');
+ lines.push('---');
+ lines.push('');
+ lines.push('# ' + topic);
+ lines.push('');
+ lines.push('## Steps');
+ lines.push('');
+ // Generate steps from graph node patterns
+ var stepNum = 1;
+ if (graphNodes && graphNodes.length > 0) {
+ for (var n of graphNodes.slice(0, 5)) {
+ lines.push(stepNum + '. ' + n.label + ' (type: ' + n.type + ', uses: ' + (n.access_count || 1) + ')');
+ stepNum++;
+ }
+ } else {
+ lines.push('1. Identify the core ' + topic + ' workflow');
+ lines.push('2. Check for existing tools and modules');
+ lines.push('3. Execute the task step by step');
+ lines.push('4. Record results in memory graph');
+ }
+ if (sessions && sessions.length > 0) {
+ lines.push('');
+ lines.push('## Insights from Recent Sessions');
+ for (var s of sessions.slice(0, 3)) {
+ lines.push('- ' + s.substring(0, 100));
+ }
+ }
+ if (existingSkills && existingSkills.length > 0) {
+ lines.push('');
+ lines.push('## Related Skills');
+ for (var sk of existingSkills) lines.push('- skill-view ' + sk);
+ }
+ lines.push('');
+ lines.push('## Pitfalls');
+ lines.push('- This skill was auto-drafted — validate before relying on it');
+ return lines.join('\n');
+ }
+
+ _cmdSkillValidate(args) {
+ if (!args || args.length === 0) return this.terminal.print('Usage: skill-validate <name>', 'warning');
+ const name = args[0].replace(/\.md$/, '');
+ const fs = this.terminal.fs;
+ const content = fs.readFile('/skills/' + name + '.md');
+ if (!content) return this.terminal.print('Skill not found: ' + name, 'error');
+
+ const result = this._validateSkillContent(content);
+ this.terminal.print('📊 Validation: ' + name, 'header');
+ if (result.errors.length === 0) {
+ this.terminal.print(' ✅ Valid', 'success');
+ } else {
+ this.terminal.print(' ❌ ' + result.errors.length + ' error(s):', 'error');
+ for (var e of result.errors) this.terminal.print(' • ' + e, 'error');
+ }
+ if (result.warnings.length > 0) {
+ this.terminal.print(' ⚠️ ' + result.warnings.length + ' warning(s):', 'warning');
+ for (var w of result.warnings) this.terminal.print(' • ' + w, 'warning');
+ }
+ this.terminal.print(' 📈 Confidence: ' + result.confidence.toFixed(2), 'info');
+ }
+
+ _validateSkillContent(content) {
+ var errors = [];
+ var warnings = [];
+ var confidence = 0;
+
+ // v4.5.90b fix: Normalize CRLF to LF (P2 from review)
+ content = content.replace(/\r\n/g, '\n');
+
+ // Check YAML frontmatter
+ var fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
+ if (!fmMatch) {
+ errors.push('Missing YAML frontmatter (--- ... ---)');
+ } else {
+ var fm = fmMatch[1];
+ if (!fm.match(/^name:\s*\S+/m)) errors.push('Missing "name" field in frontmatter');
+ if (!fm.match(/^description:\s*\S+/m)) errors.push('Missing "description" field in frontmatter');
+ var nameMatch = fm.match(/^name:\s*(\S+)/m);
+ if (nameMatch && !nameMatch[1].match(/^[a-z0-9-]+$/)) {
+ errors.push('Name must be kebab-case (lowercase, hyphens, numbers only)');
+ }
+ var tagsMatch = fm.match(/^tags:\s*\[(.+)\]/m);
+ if (tagsMatch && !tagsMatch[1].trim()) warnings.push('Tags field is empty');
+ }
+
+ // Check for ## Steps heading
+ if (!content.match(/##\s+Steps/i)) {
+ errors.push('Missing "## Steps" heading');
+ } else {
+ // Check for at least 1 numbered step
+ var steps = content.match(/^\d+\.\s+/gm);
+ if (!steps || steps.length === 0) errors.push('No numbered steps found under ## Steps');
+ }
+
+ // Check broken skill-view references
+ var refMatches = content.match(/skill-view\s+(\S+)/g) || [];
+ var fs = this.terminal.fs;
+ for (var ref of refMatches) {
+ var refName = ref.replace(/skill-view\s+/, '').replace(/\.md$/, '');
+ if (fs && !fs.readFile('/skills/' + refName + '.md')) {
+ warnings.push('Broken skill-view reference: ' + refName);
+ }
+ }
+
+ // v4.5.90b fix: Confidence = success_rate × (uses / (uses + 5)) — saturates at ~5 perfect uses, discriminates better (P2 from review)
+ var index = fs ? (fs.fsReadJSON('/skills/index.json') || {}) : {};
+ var fmName = fmMatch ? fmMatch[1].match(/^name:\s*(\S+)/m) : null;
+ var skillName = fmName ? fmName[1] : '';
+ var stats = index[skillName] || {};
+ var uses = stats.useCount || 0;
+ var successes = stats.successCount || 0;
+ if (uses > 0) {
+ var successRate = successes / uses;
+ confidence = successRate * (uses / (uses + 5));
+ }
+
+ return { valid: errors.length === 0, errors: errors, warnings: warnings, confidence: Math.min(confidence, 1.0) };
+ }
+
+ _cmdSkillAudit() {
+ const fs = this.terminal.fs;
+ const index = fs.fsReadJSON('/skills/index.json') || {};
+ const allNames = Object.keys(index);
+ const now = Date.now();
+
+ var stale = [];
+ var duplicates = [];
+ var brokenRefs = 0;
+
+ // 1. Stale check
+ const mg = this.terminal?.moduleManager?.modules?.['MemoryGraphModule'];
+ var staleSkills = [];
+ if (mg && typeof mg.getStaleSkills === 'function') {
+ staleSkills = mg.getStaleSkills();
+ }
+ for (var s of staleSkills) {
+ if (!index[s.name] || index[s.name].immune) continue;
+ stale.push(s.name + ' (' + s.daysUnused + 'd unused)');
+ }
+
+ // 2. Duplicate check (simple Levenshtein on name)
+ const auditCap = 200;
+ const auditNames = allNames.slice(0, auditCap);
+ for (var i = 0; i < auditNames.length; i++) {
+ for (var j = i + 1; j < auditNames.length; j++) {
+ if (this._levenshtein(auditNames[i], auditNames[j]) <= 2) {
+ duplicates.push(auditNames[i] + ' ≈ ' + auditNames[j]);
+ }
+ }
+ }
+ if (allNames.length > auditCap) {
+ warnings.push('Duplicate check only scanned first ' + auditCap + ' of ' + allNames.length + ' skills');
+ }
+
+ // 3. Reference check
+ for (var name of allNames) {
+ var content = fs.readFile('/skills/' + name + '.md');
+ if (!content) continue;
+ var refs = content.match(/skill-view\s+(\S+)/g) || [];
+ for (var ref of refs) {
+ var refName = ref.replace(/skill-view\s+/, '').replace(/\.md$/, '');
+ if (!fs.readFile('/skills/' + refName + '.md')) brokenRefs++;
+ }
+ }
+
+ var healthy = allNames.length - stale.length;
+ this.terminal.print('📊 Skill Audit', 'header');
+ this.terminal.print('────────────────────────────────', 'text-muted');
+ this.terminal.print('Total skills: ' + allNames.length, 'info');
+ this.terminal.print('Healthy: ' + healthy, 'success');
+ this.terminal.print('Stale: ' + stale.length + (stale.length > 0 ? ' (' + stale.join(', ') + ')' : ''), stale.length > 0 ? 'warning' : 'info');
+ this.terminal.print('Duplicates: ' + duplicates.length + (duplicates.length > 0 ? ' (' + duplicates.join(', ') + ')' : ''), duplicates.length > 0 ? 'warning' : 'info');
+ this.terminal.print('Broken refs: ' + brokenRefs, brokenRefs > 0 ? 'error' : 'info');
+ this.terminal.print('', '');
+
+ if (stale.length > 0 || duplicates.length > 0 || brokenRefs > 0) {
+ this.terminal.print('Suggestions:', 'info');
+ for (var st of stale) this.terminal.print('• Archive ' + st, 'warning');
+ for (var dup of duplicates) this.terminal.print('• Merge ' + dup, 'warning');
+ if (brokenRefs > 0) this.terminal.print('• Fix ' + brokenRefs + ' broken reference(s)', 'warning');
+ } else {
+ this.terminal.print('✅ All skills healthy', 'success');
+ }
+ }
+
+ _cmdSkillRestore(args) {
+ if (!args || args.length === 0) return this.terminal.print('Usage: skill-restore <name>', 'warning');
+ const name = args[0].replace(/\.md$/, '');
+ const fs = this.terminal.fs;
+ if (!fs.readDir('/skills/.archive/')) return this.terminal.print('No archive directory found', 'error');
+ const content = fs.readFile('/skills/.archive/' + name + '.md');
+ if (!content) return this.terminal.print('Skill not found in archive: ' + name, 'error');
+ // v4.5.90b fix: Don't overwrite a live skill (P1 from review)
+ const existing = fs.readFile('/skills/' + name + '.md');
+ if (existing) return this.terminal.print('Skill "' + name + '" already exists live. Use skill-patch or delete it first.', 'warning');
+ fs.writeFile('/skills/' + name + '.md', content);
+ fs.deleteFile('/skills/.archive/' + name + '.md');
+ const index = fs.fsReadJSON('/skills/index.json') || {};
+ index[name] = index[name] || {};
+ index[name].lastUsed = new Date().toISOString();
+ index[name].useCount = index[name].useCount || 0;
+ fs.fsWriteJSON('/skills/index.json', index);
+ this.terminal.print('✅ Restored: ' + name, 'success');
+ }
+
+ _levenshtein(a, b) {
+ if (a.length === 0) return b.length;
+ if (b.length === 0) return a.length;
+ var matrix = [];
+ for (var i = 0; i <= b.length; i++) matrix[i] = [i];
+ for (var j = 0; j <= a.length; j++) matrix[0][j] = j;
+ for (var i = 1; i <= b.length; i++) {
+ for (var j = 1; j <= a.length; j++) {
+ if (b.charAt(i - 1) === a.charAt(j - 1)) {
+ matrix[i][j] = matrix[i - 1][j - 1];
+ } else {
+ matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1);
+ }
+ }
+ }
+ return matrix[b.length][a.length];
+ }
}
if (typeof window.webx11 !== 'undefined' && window.webx11) {
webx11.moduleManager.registerModule(new SkillPackCoreModule(), 'https://webx11.duckdns.org/spaces/newos4/modules/ext/skill-pack-core.js');