diff options
Diffstat (limited to 'modules')
| -rw-r--r-- | modules/core/core-bundle.js | 53 | ||||
| -rw-r--r-- | modules/core/module-aiassist.js | 47 | ||||
| -rw-r--r-- | modules/core/module-loader.js | 2 | ||||
| -rw-r--r-- | modules/core/module-system.js | 6 | ||||
| -rw-r--r-- | modules/test/test-help-and-lessons.js | 75 |
5 files changed, 148 insertions, 35 deletions
diff --git a/modules/core/core-bundle.js b/modules/core/core-bundle.js index c06af5f..d7d9d61 100644 --- a/modules/core/core-bundle.js +++ b/modules/core/core-bundle.js @@ -6431,7 +6431,11 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm if (helpEntry) { this.terminal.print(helpEntry, 'info'); } else { - this.terminal.print(`No help available for ${cmd}`, 'warning'); + // MAINT-019: a registered command with no help line answered "No help + // available", which reads as if the command does not exist. Name the + // module that owns it so the user knows where to look. + const owner = allCommands[cmd]; + this.terminal.print(`${cmd} — registered by ${owner}, which publishes no help line for it. Try: help`, 'warning'); } } else { this.terminal.print(`No help available for ${cmd}`, 'warning'); @@ -10045,13 +10049,21 @@ buildSystemPrompt() { try { var fs = this.terminal.fs; if (fs) { - var index = fs.fsReadJSON('/skills/index.json') || {}; - var lessonKeys = Object.keys(index).filter(function(k) { return k.indexOf('loop-lesson-') === 0; }); + // MAINT-020: lessons live in /lessons, not in /skills — writing them into + // the skills directory made auto-generated lessons show up as skills. + var index = fs.fsReadJSON('/lessons/index.json') || {}; + var lessonKeys = Object.keys(index); + if (lessonKeys.length === 0) { + // one-release fallback for lessons written by older builds + var legacy = fs.fsReadJSON('/skills/index.json') || {}; + lessonKeys = Object.keys(legacy).filter(function(k) { return k.indexOf('loop-lesson-') === 0; }); + } if (lessonKeys.length > 0) { var recentLessons = lessonKeys.sort().slice(-3); var lessonTexts = []; for (var li = 0; li < recentLessons.length; li++) { - var lc = fs.readFile('/skills/' + recentLessons[li] + '.md'); + var lc = fs.readFile('/lessons/' + recentLessons[li] + '.md') + || fs.readFile('/skills/' + recentLessons[li] + '.md'); if (lc) { var excerpt = lc.substring(0, 300); lessonTexts.push('[Lesson: ' + recentLessons[li] + '] ' + excerpt); @@ -13187,23 +13199,30 @@ buildSystemPrompt() { var suggestion = suggestions.join('\n'); try { var fs = this.terminal.fs; - var index = fs.fsReadJSON('/skills/index.json') || {}; - var autoSkills = Object.entries(index).filter(function(e) { return (e[1].tags || []).indexOf('auto-improvement') !== -1; }); - // One-time cleanup: remove all existing loop-lesson files on first run + // MAINT-020: lessons are kept in /lessons with their own index; /skills is + // reserved for real skills the user or the agent creates deliberately. + try { fs.mkdirRecursive('/lessons'); } catch (ex) {} + var index = fs.fsReadJSON('/lessons/index.json') || {}; + var autoSkills = Object.entries(index); + // One-time migration: earlier builds wrote loop-lesson-* into /skills. if (!this._loopLessonCleaned) { this._loopLessonCleaned = true; - autoSkills.forEach(function(e) { - try { fs.unlink('/skills/' + e[0] + '.md'); } catch (ex) {} - delete index[e[0]]; - }); - autoSkills = []; + try { + var sIdx = fs.fsReadJSON('/skills/index.json') || {}; + Object.keys(sIdx).filter(function(k) { return k.indexOf('loop-lesson-') === 0; }) + .forEach(function(k) { + try { fs.unlink('/skills/' + k + '.md'); } catch (ex) {} + delete sIdx[k]; + }); + fs.fsWriteJSON('/skills/index.json', sIdx); + } catch (ex) {} } // Age limit: remove lessons older than 24h var now = Date.now(); autoSkills = autoSkills.filter(function(e) { var created = new Date(e[1].created).getTime(); if (now - created > 86400000) { - try { fs.unlink('/skills/' + e[0] + '.md'); } catch (ex) {} + try { fs.unlink('/lessons/' + e[0] + '.md'); } catch (ex) {} delete index[e[0]]; return false; } @@ -13215,17 +13234,17 @@ buildSystemPrompt() { // FIFO rotation: if at cap, delete oldest before creating if (!isDuplicate && autoSkills.length >= 3) { var oldest = autoSkills[0]; - try { fs.unlink('/skills/' + oldest[0] + '.md'); } catch (ex) {} + try { fs.unlink('/lessons/' + oldest[0] + '.md'); } catch (ex) {} delete index[oldest[0]]; autoSkills.shift(); } if (!isDuplicate) { var skillName = 'loop-lesson-' + Date.now(); var content = '---\nname: ' + skillName + '\ndescription: Auto-saved from agent loop anomaly detection\ntags: [auto-improvement, agent-loop, auto]\ncategory: system\n---\n## Anomalies Detected\n\n' + JSON.stringify(anomalies, null, 2) + '\n\n## Suggestions\n\n' + suggestion + '\n\n## Context\n- Profile: ' + (this._activeLoopProfile ? this._activeLoopProfile.name : 'none') + '\n- Turns: ' + result.toolTurns + '\n- Tools used: ' + toolLog.map(function(t) { return t.name; }).join(', '); - fs.mkdirRecursive('/skills'); - fs.writeFile('/skills/' + skillName + '.md', content); + fs.mkdirRecursive('/lessons'); + fs.writeFile('/lessons/' + skillName + '.md', content); index[skillName] = { name: skillName, description: 'Auto-saved loop lesson', tags: ['auto-improvement', 'agent-loop', 'auto'], category: 'system', created: new Date().toISOString(), updated: new Date().toISOString(), anomalyTypes: anomalyTypes }; - fs.fsWriteJSON('/skills/index.json', index); + fs.fsWriteJSON('/lessons/index.json', index); } } catch (e) { /* VFS not available */ } var msg = '💡 Self-improvement: ' + anomalies.length + ' anomaly(ies)'; diff --git a/modules/core/module-aiassist.js b/modules/core/module-aiassist.js index 9965e6a..be5ebce 100644 --- a/modules/core/module-aiassist.js +++ b/modules/core/module-aiassist.js @@ -1530,13 +1530,21 @@ buildSystemPrompt() { try { var fs = this.terminal.fs; if (fs) { - var index = fs.fsReadJSON('/skills/index.json') || {}; - var lessonKeys = Object.keys(index).filter(function(k) { return k.indexOf('loop-lesson-') === 0; }); + // MAINT-020: lessons live in /lessons, not in /skills — writing them into + // the skills directory made auto-generated lessons show up as skills. + var index = fs.fsReadJSON('/lessons/index.json') || {}; + var lessonKeys = Object.keys(index); + if (lessonKeys.length === 0) { + // one-release fallback for lessons written by older builds + var legacy = fs.fsReadJSON('/skills/index.json') || {}; + lessonKeys = Object.keys(legacy).filter(function(k) { return k.indexOf('loop-lesson-') === 0; }); + } if (lessonKeys.length > 0) { var recentLessons = lessonKeys.sort().slice(-3); var lessonTexts = []; for (var li = 0; li < recentLessons.length; li++) { - var lc = fs.readFile('/skills/' + recentLessons[li] + '.md'); + var lc = fs.readFile('/lessons/' + recentLessons[li] + '.md') + || fs.readFile('/skills/' + recentLessons[li] + '.md'); if (lc) { var excerpt = lc.substring(0, 300); lessonTexts.push('[Lesson: ' + recentLessons[li] + '] ' + excerpt); @@ -4672,23 +4680,30 @@ buildSystemPrompt() { var suggestion = suggestions.join('\n'); try { var fs = this.terminal.fs; - var index = fs.fsReadJSON('/skills/index.json') || {}; - var autoSkills = Object.entries(index).filter(function(e) { return (e[1].tags || []).indexOf('auto-improvement') !== -1; }); - // One-time cleanup: remove all existing loop-lesson files on first run + // MAINT-020: lessons are kept in /lessons with their own index; /skills is + // reserved for real skills the user or the agent creates deliberately. + try { fs.mkdirRecursive('/lessons'); } catch (ex) {} + var index = fs.fsReadJSON('/lessons/index.json') || {}; + var autoSkills = Object.entries(index); + // One-time migration: earlier builds wrote loop-lesson-* into /skills. if (!this._loopLessonCleaned) { this._loopLessonCleaned = true; - autoSkills.forEach(function(e) { - try { fs.unlink('/skills/' + e[0] + '.md'); } catch (ex) {} - delete index[e[0]]; - }); - autoSkills = []; + try { + var sIdx = fs.fsReadJSON('/skills/index.json') || {}; + Object.keys(sIdx).filter(function(k) { return k.indexOf('loop-lesson-') === 0; }) + .forEach(function(k) { + try { fs.unlink('/skills/' + k + '.md'); } catch (ex) {} + delete sIdx[k]; + }); + fs.fsWriteJSON('/skills/index.json', sIdx); + } catch (ex) {} } // Age limit: remove lessons older than 24h var now = Date.now(); autoSkills = autoSkills.filter(function(e) { var created = new Date(e[1].created).getTime(); if (now - created > 86400000) { - try { fs.unlink('/skills/' + e[0] + '.md'); } catch (ex) {} + try { fs.unlink('/lessons/' + e[0] + '.md'); } catch (ex) {} delete index[e[0]]; return false; } @@ -4700,17 +4715,17 @@ buildSystemPrompt() { // FIFO rotation: if at cap, delete oldest before creating if (!isDuplicate && autoSkills.length >= 3) { var oldest = autoSkills[0]; - try { fs.unlink('/skills/' + oldest[0] + '.md'); } catch (ex) {} + try { fs.unlink('/lessons/' + oldest[0] + '.md'); } catch (ex) {} delete index[oldest[0]]; autoSkills.shift(); } if (!isDuplicate) { var skillName = 'loop-lesson-' + Date.now(); var content = '---\nname: ' + skillName + '\ndescription: Auto-saved from agent loop anomaly detection\ntags: [auto-improvement, agent-loop, auto]\ncategory: system\n---\n## Anomalies Detected\n\n' + JSON.stringify(anomalies, null, 2) + '\n\n## Suggestions\n\n' + suggestion + '\n\n## Context\n- Profile: ' + (this._activeLoopProfile ? this._activeLoopProfile.name : 'none') + '\n- Turns: ' + result.toolTurns + '\n- Tools used: ' + toolLog.map(function(t) { return t.name; }).join(', '); - fs.mkdirRecursive('/skills'); - fs.writeFile('/skills/' + skillName + '.md', content); + fs.mkdirRecursive('/lessons'); + fs.writeFile('/lessons/' + skillName + '.md', content); index[skillName] = { name: skillName, description: 'Auto-saved loop lesson', tags: ['auto-improvement', 'agent-loop', 'auto'], category: 'system', created: new Date().toISOString(), updated: new Date().toISOString(), anomalyTypes: anomalyTypes }; - fs.fsWriteJSON('/skills/index.json', index); + fs.fsWriteJSON('/lessons/index.json', index); } } catch (e) { /* VFS not available */ } var msg = '💡 Self-improvement: ' + anomalies.length + ' anomaly(ies)'; diff --git a/modules/core/module-loader.js b/modules/core/module-loader.js index 3204af0..836672d 100644 --- a/modules/core/module-loader.js +++ b/modules/core/module-loader.js @@ -4,7 +4,7 @@ // Core module bundle loaded synchronously in main.html. // This loader only handles version cache-busting, splash, terminal init, and optional ext modules. - const LATEST_VER = 'v4.6.107'; + const LATEST_VER = 'v4.6.108'; const CACHE = '?v=' + LATEST_VER; const stored = localStorage.getItem('webx11_cache_ver'); if (stored !== LATEST_VER) { diff --git a/modules/core/module-system.js b/modules/core/module-system.js index 40b0861..43bb0b1 100644 --- a/modules/core/module-system.js +++ b/modules/core/module-system.js @@ -743,7 +743,11 @@ if (helpEntry) { this.terminal.print(helpEntry, 'info'); } else { - this.terminal.print(`No help available for ${cmd}`, 'warning'); + // MAINT-019: a registered command with no help line answered "No help + // available", which reads as if the command does not exist. Name the + // module that owns it so the user knows where to look. + const owner = allCommands[cmd]; + this.terminal.print(`${cmd} — registered by ${owner}, which publishes no help line for it. Try: help`, 'warning'); } } else { this.terminal.print(`No help available for ${cmd}`, 'warning'); diff --git a/modules/test/test-help-and-lessons.js b/modules/test/test-help-and-lessons.js new file mode 100644 index 0000000..c4eb5ae --- /dev/null +++ b/modules/test/test-help-and-lessons.js @@ -0,0 +1,75 @@ +// modules/test/test-help-and-lessons.js — run: node +// MAINT-019: `help <cmd>` must not answer "No help available" for a command that exists. +// MAINT-020: loop lessons must be written to /lessons, never into /skills. +const fsMod = require('fs'), path = require('path'); +const ROOT = path.join(__dirname, '..'); +const store = {}; +global.localStorage = { getItem: k => (k in store ? store[k] : null), setItem: (k, v) => { store[k] = String(v); }, removeItem: k => { delete store[k]; } }; +global.indexedDB = { open() { throw new Error('no idb'); }, deleteDatabase() { throw new Error('no idb'); } }; +global.window = global; + +const SystemModule = new Function(fsMod.readFileSync(path.join(ROOT, 'core', 'module-system.js'), 'utf8') + '\nreturn SystemModule;')(); + +let fails = 0, passes = 0; +function ok(name, cond, detail) { + if (cond) { passes++; return; } + fails++; + console.log('FAIL ' + name + (detail ? ' — ' + detail : '')); +} + +function mkHelp(commands, help) { + const out = []; + const sys = Object.create(SystemModule.prototype); + sys.terminal = { + print: (m, t) => out.push({ text: String(m), type: t || '' }), + log: () => {}, + moduleManager: { + getAllCommands: () => commands, + getAllHelp: () => help, + }, + }; + return { sys, out }; +} +const text = out => out.map(o => o.text).join(' | '); + +// A. a documented command prints its help line +{ + const { sys, out } = mkHelp({ ls: 'FileSystemModule' }, { ls: 'ls [path] — List files' }); + sys.handleHelp(['ls']); + ok('A1 documented command shows its help', /ls \[path\]/.test(text(out)), text(out)); +} + +// B. an undocumented but registered command names its owner instead of denying it exists +{ + const { sys, out } = mkHelp({ 'agent-register': 'AgentRegistryModule' }, {}); + sys.handleHelp(['agent-register']); + const t = text(out); + ok('B1 no "No help available" for a registered command', !/No help available/.test(t), t); + ok('B2 the owning module is named', /AgentRegistryModule/.test(t), t); + ok('B3 it still says there is no help line', /no help line/i.test(t), t); +} + +// C. a command that really does not exist still says so +{ + const { sys, out } = mkHelp({ ls: 'FileSystemModule' }, { ls: 'ls — List' }); + sys.handleHelp(['definitely-not-a-command']); + ok('C1 unknown command still reports no help', /No help available/.test(text(out)), text(out)); +} + +// D. MAINT-020: no lesson is written into /skills, and /lessons is used instead +{ + const src = fsMod.readFileSync(path.join(ROOT, 'core', 'module-aiassist.js'), 'utf8'); + const writesSkils = /writeFile\(\s*'\/skills\/'\s*\+\s*skillName/.test(src); + ok('D1 no lesson file is written into /skills', !writesSkils); + ok('D2 lesson files are written into /lessons', /writeFile\(\s*'\/lessons\/'\s*\+\s*skillName/.test(src)); + ok('D3 the lessons index is written to /lessons', /fsWriteJSON\(\s*'\/lessons\/index\.json'/.test(src)); + ok('D4 the reader still handles legacy /skills lessons', /fs\.readFile\('\/skills\/' \+ recentLessons\[li\] \+ '\.md'\)/.test(src)); + ok('D5 legacy lessons are cleaned out of the skills index once', /indexOf\('loop-lesson-'\) === 0/.test(src)); +} + +if (fails === 0) { + console.log('test-help-and-lessons: ' + passes + ' passed, 0 failed'); +} else { + console.log('test-help-and-lessons: ' + passes + ' passed, ' + fails + ' FAILED'); + process.exit(1); +} |