diff options
| -rw-r--r-- | main.html | 6 | ||||
| -rw-r--r-- | modules/core/core-bundle.js | 131 | ||||
| -rw-r--r-- | modules/core/module-aiassist.js | 17 | ||||
| -rw-r--r-- | modules/core/module-loader.js | 2 | ||||
| -rw-r--r-- | modules/core/module-world-model.js | 114 | ||||
| -rw-r--r-- | modules/test/test-world-model-ask.js | 47 |
6 files changed, 287 insertions, 30 deletions
@@ -1349,13 +1349,13 @@ </script> <!-- v4.5.63: A2A scratchpad fixes + agent-register arg fix --> -<script src="modules/core/core-bundle.js?v=v4.6.49"></script> +<script src="modules/core/core-bundle.js?v=v4.6.50"></script> <!-- Async dependency-ordered module loader now only initializes terminal and loads optional ext modules --> -<script src="modules/core/module-loader.js?v=v4.6.49"></script> +<script src="modules/core/module-loader.js?v=v4.6.50"></script> <!-- Critical ext bundle loads AFTER webx11 exists so registerModule calls work --> -<script src="modules/ext/critical-ext-bundle.js?v=v4.6.49"></script> +<script src="modules/ext/critical-ext-bundle.js?v=v4.6.50"></script> <script> // ═══ ONE TRUE VERSION SOURCE ═══ diff --git a/modules/core/core-bundle.js b/modules/core/core-bundle.js index f467f84..2264ef0 100644 --- a/modules/core/core-bundle.js +++ b/modules/core/core-bundle.js @@ -8260,7 +8260,7 @@ buildSystemPrompt() { + 'FS: list/read/write/delete | NVFS: nvfs-resolve | Web: search/fetch [POST=method+body] | Mod: view/edit/test/reload | Config | Skills | VaultMind | wallet-lookup | get_usage_stats\\n' + 'web_fetch: GET for pages, POST with method+body for JSON APIs. JS: node <file.js> or node -e "expr" (browser JS only).\n' + 'Other: floating, webdav, cron, vault, sandbox, agents, wpm, mindmap, memgraph, world-model, predict, guardrail, tar, secret-* — via help.\n' - + 'predict_task: estimate duration before complex tasks. world_model_query: ask about system state, changes, health.'; + + 'predict_task: estimate duration before complex tasks. world_model_query: read the system snapshot (modules, files, agents, telemetry, recent errors) and answer from it.'; if (profilePrompt) { base = profilePrompt + '\n\n' + base; } @@ -8811,11 +8811,12 @@ buildSystemPrompt() { type: 'function', function: { name: 'world_model_query', - description: 'Query the system world model for self-awareness: modules, skills, files, agents, health, memory graph, recent changes, active projects, telemetry. Ask questions like "what changed" or "how many modules".', + description: 'Read the synthesized system world model (modules, files, skills, agents, memory graph, telemetry, recent changes). Returns a JSON snapshot you can reason over; `facts` additionally carries the instant keyword answer. Ask detail:"full" for the complete model (~3 KB).', parameters: { type: 'object', properties: { - question: { type: 'string', description: 'What to ask: modules, skills, files, agents, health, memory, changes, projects, telemetry, summary' } + question: { type: 'string', description: 'What you want to know (used for the fast facts line)' }, + detail: { type: 'string', description: '"digest" (default) or "full"' } }, required: ['question'] } @@ -11197,8 +11198,14 @@ buildSystemPrompt() { var wm = this.terminal.moduleManager.modules['WorldModelModule']; if (!wm) return JSON.stringify({ error: 'WorldModelModule not loaded' }); if (!wm._model) return JSON.stringify({ error: 'World model not synthesized yet — run world-model-refresh' }); - var wmAnswer = wm._answerQuery(a.question || ''); - return JSON.stringify({ question: a.question, answer: wmAnswer, version: wm._model.version, synthesized: wm._model.synthesized }); + var wmOut = { + question: a.question, + facts: wm._answerQuery(a.question || ''), + version: wm._model.version, + synthesized: wm._model.synthesized, + snapshot: (a.detail === 'full') ? wm._model : JSON.parse(wm._modelDigest()) + }; + return JSON.stringify(wmOut); } case 'health_check': case 'diff_module': @@ -17912,7 +17919,7 @@ if (typeof window !== 'undefined') { /* === module-world-model.js === */ // module-world-model.js — Global World Model: system self-awareness synthesis engine // Aggregates from memory graph, neural VFS, skills index, telemetry -// Commands: world-model, world-model-summary, world-model-ask, world-model-refresh, world-model-config +// Commands: world-model, world-model-summary, world-model-ask (LLM), world-model-refresh, world-model-config class WorldModelModule { constructor() { this.commands = {}; @@ -17933,7 +17940,7 @@ class WorldModelModule { return { 'world-model': 'world-model — Show full world model as structured output', 'world-model-summary': 'world-model-summary — One-line system self-awareness statement', - 'world-model-ask': 'world-model-ask <question> — Query the model (e.g. "what changed since yesterday")', +'world-model-ask': 'world-model-ask <question> — Ask the LLM about this system (grounded in the world model; --facts for the instant keyword lookup)', 'world-model-refresh': 'world-model-refresh — Force re-synthesis from all subsystems', 'world-model-config': 'world-model-config <key> <value> — Tune: refresh-interval, max-projects', }; @@ -18285,13 +18292,30 @@ class WorldModelModule { return summary; } - ask(question) { - if (!question) return this.terminal.print('Usage: world-model-ask <question>', 'error'); + async ask(question) { + if (!question) return this.terminal.print('Usage: world-model-ask <question> (--facts = fast keyword lookup)', 'error'); + const factsOnly = /(^|\s)--facts\b/.test(question); + if (factsOnly) question = question.replace(/--facts\b/, '').trim(); if (!this._model) return this.terminal.print('No model — run world-model-refresh first', 'warning'); - const answer = this._answerQuery(question); this.terminal.print(`❓ ${question}`, 'info'); - this.terminal.print(`💡 ${answer}`, 'success'); + + if (!factsOnly) { + const om = this.terminal.ollamaManager; + try { + if (om && !om.connected) { try { await om.connect(); } catch (e) {} } + if (om && om.connected) { + const answer = await om.chat(this._buildAskPrompt(question)); + if (answer && String(answer).trim()) { + this.terminal.print(`💡 ${String(answer).trim()}`, 'success'); + return; + } + } + } catch (e) { /* fall through to the facts path */ } + this.terminal.print('⚠️ LLM unavailable — showing the factual lookup instead', 'warning'); + } + + this.terminal.print(`💡 ${this._answerQuery(question)}`, 'success'); } // Shared query routing — used by both ask() (terminal) and world_model_query (AI tool) @@ -18313,8 +18337,12 @@ class WorldModelModule { } else if (q.includes('memory') || q.includes('graph')) { return `Memory Graph: ${m.memory.nodes} nodes, ${m.memory.edges} edges (${m.memory.active ? 'active' : 'inactive'})`; } else if (q.includes('change') || q.includes('yesterday') || q.includes('recent')) { - const recent = m.recent; - return recent.length > 0 ? `Recent changes: ${recent.join('; ')}` : 'No recent changes detected'; + const diff = this._diffSincePrev(); + const recent = m.recent || []; + const parts = []; + if (diff.length) parts.push('Since the last snapshot: ' + diff.join('; ')); + if (recent.length) parts.push('Current: ' + recent.join('; ')); + return parts.length ? parts.join(' · ') : 'No previous snapshot yet — run world-model-refresh twice'; } else if (q.includes('project') || q.includes('work')) { const projects = m.projects.activeProjects; return projects.length > 0 @@ -18329,6 +18357,80 @@ class WorldModelModule { } } + // 2026-10-21: ask() used to be a keyword table only, so any real question got + // "Unknown query". Whitelist the fields we are willing to send to a model + // (never the raw model object: it is persisted alongside other VFS data). + _modelDigest(maxChars) { + const m = this._model; + if (!m) return ''; + const cap = maxChars || 4000; + const pick = { + version: m.version, + synthesized: m.synthesized, + system: m.system, + modules: m.modules, + files: m.files, + skills: { count: m.skills && m.skills.count, skills: ((m.skills && m.skills.skills) || []).slice(0, 40) }, + memory: m.memory, + agents: m.agents, + telemetry: m.telemetry, + projects: m.projects, + recent: (m.recent || []).slice(0, 20), + // 2026-10-21: the model only ever exposed a COUNT ("9 recent errors") with no detail, + // so neither the user nor the LLM could say what failed. Carry the last few. + recentErrorSample: this._errorSample ? this._errorSample() : [], + changesSinceLastSnapshot: this._diffSincePrev ? this._diffSincePrev() : [] + }; + let json = JSON.stringify(pick); + if (json.length > cap) json = json.slice(0, cap) + '…(digest truncated)'; + return json; + } + + // Last few tool/agent errors with their tool name, for the digest (bounded). + _errorSample() { + try { + const ds = this.terminal.moduleManager.modules['DebugSupervisorModule']; + const db = ds && ds._errorDB && ds._errorDB.errors; + if (!db) return []; + return Object.keys(db).slice(-5).map(k => { + const e = db[k] || {}; + return String(e.tool || e.module || k).slice(0, 40) + ': ' + String(e.error || e.message || '').slice(0, 80); + }); + } catch (e) { return []; } + } + + _buildAskPrompt(question) { + return [ + 'You are the newOS4 world model interface. Answer ONLY from the JSON snapshot below.', + 'If the snapshot does not contain the answer, say what is missing and name the command that', + 'would produce it (world-model-refresh, world-model, modules, test). Never invent numbers.', + 'Answer in 1-3 short sentences, plain text, no markdown headers.', + '', + 'SNAPSHOT: ' + this._modelDigest(), + '', + 'QUESTION: ' + String(question || '').trim() + ].join('\n'); + } + + _diffSincePrev() { + try { + const fs = this.terminal.fs; + const prev = fs && fs.fsReadJSON('/system/world-model.prev.json'); + const cur = this._model; + if (!prev || !cur) return []; + const out = []; + if (prev.version && cur.version && prev.version !== cur.version) out.push(`version ${prev.version} → ${cur.version}`); + const fields = [['modules', 'loaded'], ['skills', 'count'], ['files', 'total'], ['agents', 'agents'], ['memory', 'nodes']]; + fields.forEach(function (f) { + const a = prev[f[0]] && prev[f[0]][f[1]]; + const b = cur[f[0]] && cur[f[0]][f[1]]; + if (typeof a === 'number' && typeof b === 'number' && a !== b) out.push(`${f[0]} ${a} → ${b}`); + }); + if (prev.synthesized) out.push('previous snapshot: ' + prev.synthesized); + return out; + } catch (e) { return []; } + } + refresh() { this._synthesize(); this.terminal.print(`✅ World Model refreshed at ${new Date().toLocaleTimeString()}`, 'success'); @@ -18392,6 +18494,9 @@ class WorldModelModule { _saveToVFS(fs) { try { if (!fs.readDir('/system')) fs.mkdir('system'); + // 2026-10-21: keep one previous model so "what changed" can be a real diff. + const prevModel = fs.fsReadJSON('/system/world-model.json'); + if (prevModel) { try { fs.fsWriteJSON('/system/world-model.prev.json', prevModel); } catch (_) {} } // v4.5.89b fix: Clean stale temp from previous crash (P1 from review) try { fs.deleteFile('/system/world-model.tmp.json'); } catch (_) {} fs.fsWriteJSON('/system/world-model.tmp.json', this._model); diff --git a/modules/core/module-aiassist.js b/modules/core/module-aiassist.js index f69494e..a8c0ae1 100644 --- a/modules/core/module-aiassist.js +++ b/modules/core/module-aiassist.js @@ -98,7 +98,7 @@ buildSystemPrompt() { + 'FS: list/read/write/delete | NVFS: nvfs-resolve | Web: search/fetch [POST=method+body] | Mod: view/edit/test/reload | Config | Skills | VaultMind | wallet-lookup | get_usage_stats\\n' + 'web_fetch: GET for pages, POST with method+body for JSON APIs. JS: node <file.js> or node -e "expr" (browser JS only).\n' + 'Other: floating, webdav, cron, vault, sandbox, agents, wpm, mindmap, memgraph, world-model, predict, guardrail, tar, secret-* — via help.\n' - + 'predict_task: estimate duration before complex tasks. world_model_query: ask about system state, changes, health.'; + + 'predict_task: estimate duration before complex tasks. world_model_query: read the system snapshot (modules, files, agents, telemetry, recent errors) and answer from it.'; if (profilePrompt) { base = profilePrompt + '\n\n' + base; } @@ -649,11 +649,12 @@ buildSystemPrompt() { type: 'function', function: { name: 'world_model_query', - description: 'Query the system world model for self-awareness: modules, skills, files, agents, health, memory graph, recent changes, active projects, telemetry. Ask questions like "what changed" or "how many modules".', + description: 'Read the synthesized system world model (modules, files, skills, agents, memory graph, telemetry, recent changes). Returns a JSON snapshot you can reason over; `facts` additionally carries the instant keyword answer. Ask detail:"full" for the complete model (~3 KB).', parameters: { type: 'object', properties: { - question: { type: 'string', description: 'What to ask: modules, skills, files, agents, health, memory, changes, projects, telemetry, summary' } + question: { type: 'string', description: 'What you want to know (used for the fast facts line)' }, + detail: { type: 'string', description: '"digest" (default) or "full"' } }, required: ['question'] } @@ -3035,8 +3036,14 @@ buildSystemPrompt() { var wm = this.terminal.moduleManager.modules['WorldModelModule']; if (!wm) return JSON.stringify({ error: 'WorldModelModule not loaded' }); if (!wm._model) return JSON.stringify({ error: 'World model not synthesized yet — run world-model-refresh' }); - var wmAnswer = wm._answerQuery(a.question || ''); - return JSON.stringify({ question: a.question, answer: wmAnswer, version: wm._model.version, synthesized: wm._model.synthesized }); + var wmOut = { + question: a.question, + facts: wm._answerQuery(a.question || ''), + version: wm._model.version, + synthesized: wm._model.synthesized, + snapshot: (a.detail === 'full') ? wm._model : JSON.parse(wm._modelDigest()) + }; + return JSON.stringify(wmOut); } case 'health_check': case 'diff_module': diff --git a/modules/core/module-loader.js b/modules/core/module-loader.js index 8330eaf..64ece7c 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.49'; + const LATEST_VER = 'v4.6.50'; const CACHE = '?v=' + LATEST_VER; const stored = localStorage.getItem('webx11_cache_ver'); if (stored !== LATEST_VER) { diff --git a/modules/core/module-world-model.js b/modules/core/module-world-model.js index 61eecfc..cbfd1fb 100644 --- a/modules/core/module-world-model.js +++ b/modules/core/module-world-model.js @@ -1,6 +1,6 @@ // module-world-model.js — Global World Model: system self-awareness synthesis engine // Aggregates from memory graph, neural VFS, skills index, telemetry -// Commands: world-model, world-model-summary, world-model-ask, world-model-refresh, world-model-config +// Commands: world-model, world-model-summary, world-model-ask (LLM), world-model-refresh, world-model-config class WorldModelModule { constructor() { this.commands = {}; @@ -21,7 +21,7 @@ class WorldModelModule { return { 'world-model': 'world-model — Show full world model as structured output', 'world-model-summary': 'world-model-summary — One-line system self-awareness statement', - 'world-model-ask': 'world-model-ask <question> — Query the model (e.g. "what changed since yesterday")', +'world-model-ask': 'world-model-ask <question> — Ask the LLM about this system (grounded in the world model; --facts for the instant keyword lookup)', 'world-model-refresh': 'world-model-refresh — Force re-synthesis from all subsystems', 'world-model-config': 'world-model-config <key> <value> — Tune: refresh-interval, max-projects', }; @@ -373,13 +373,30 @@ class WorldModelModule { return summary; } - ask(question) { - if (!question) return this.terminal.print('Usage: world-model-ask <question>', 'error'); + async ask(question) { + if (!question) return this.terminal.print('Usage: world-model-ask <question> (--facts = fast keyword lookup)', 'error'); + const factsOnly = /(^|\s)--facts\b/.test(question); + if (factsOnly) question = question.replace(/--facts\b/, '').trim(); if (!this._model) return this.terminal.print('No model — run world-model-refresh first', 'warning'); - const answer = this._answerQuery(question); this.terminal.print(`❓ ${question}`, 'info'); - this.terminal.print(`💡 ${answer}`, 'success'); + + if (!factsOnly) { + const om = this.terminal.ollamaManager; + try { + if (om && !om.connected) { try { await om.connect(); } catch (e) {} } + if (om && om.connected) { + const answer = await om.chat(this._buildAskPrompt(question)); + if (answer && String(answer).trim()) { + this.terminal.print(`💡 ${String(answer).trim()}`, 'success'); + return; + } + } + } catch (e) { /* fall through to the facts path */ } + this.terminal.print('⚠️ LLM unavailable — showing the factual lookup instead', 'warning'); + } + + this.terminal.print(`💡 ${this._answerQuery(question)}`, 'success'); } // Shared query routing — used by both ask() (terminal) and world_model_query (AI tool) @@ -401,8 +418,12 @@ class WorldModelModule { } else if (q.includes('memory') || q.includes('graph')) { return `Memory Graph: ${m.memory.nodes} nodes, ${m.memory.edges} edges (${m.memory.active ? 'active' : 'inactive'})`; } else if (q.includes('change') || q.includes('yesterday') || q.includes('recent')) { - const recent = m.recent; - return recent.length > 0 ? `Recent changes: ${recent.join('; ')}` : 'No recent changes detected'; + const diff = this._diffSincePrev(); + const recent = m.recent || []; + const parts = []; + if (diff.length) parts.push('Since the last snapshot: ' + diff.join('; ')); + if (recent.length) parts.push('Current: ' + recent.join('; ')); + return parts.length ? parts.join(' · ') : 'No previous snapshot yet — run world-model-refresh twice'; } else if (q.includes('project') || q.includes('work')) { const projects = m.projects.activeProjects; return projects.length > 0 @@ -417,6 +438,80 @@ class WorldModelModule { } } + // 2026-10-21: ask() used to be a keyword table only, so any real question got + // "Unknown query". Whitelist the fields we are willing to send to a model + // (never the raw model object: it is persisted alongside other VFS data). + _modelDigest(maxChars) { + const m = this._model; + if (!m) return ''; + const cap = maxChars || 4000; + const pick = { + version: m.version, + synthesized: m.synthesized, + system: m.system, + modules: m.modules, + files: m.files, + skills: { count: m.skills && m.skills.count, skills: ((m.skills && m.skills.skills) || []).slice(0, 40) }, + memory: m.memory, + agents: m.agents, + telemetry: m.telemetry, + projects: m.projects, + recent: (m.recent || []).slice(0, 20), + // 2026-10-21: the model only ever exposed a COUNT ("9 recent errors") with no detail, + // so neither the user nor the LLM could say what failed. Carry the last few. + recentErrorSample: this._errorSample ? this._errorSample() : [], + changesSinceLastSnapshot: this._diffSincePrev ? this._diffSincePrev() : [] + }; + let json = JSON.stringify(pick); + if (json.length > cap) json = json.slice(0, cap) + '…(digest truncated)'; + return json; + } + + // Last few tool/agent errors with their tool name, for the digest (bounded). + _errorSample() { + try { + const ds = this.terminal.moduleManager.modules['DebugSupervisorModule']; + const db = ds && ds._errorDB && ds._errorDB.errors; + if (!db) return []; + return Object.keys(db).slice(-5).map(k => { + const e = db[k] || {}; + return String(e.tool || e.module || k).slice(0, 40) + ': ' + String(e.error || e.message || '').slice(0, 80); + }); + } catch (e) { return []; } + } + + _buildAskPrompt(question) { + return [ + 'You are the newOS4 world model interface. Answer ONLY from the JSON snapshot below.', + 'If the snapshot does not contain the answer, say what is missing and name the command that', + 'would produce it (world-model-refresh, world-model, modules, test). Never invent numbers.', + 'Answer in 1-3 short sentences, plain text, no markdown headers.', + '', + 'SNAPSHOT: ' + this._modelDigest(), + '', + 'QUESTION: ' + String(question || '').trim() + ].join('\n'); + } + + _diffSincePrev() { + try { + const fs = this.terminal.fs; + const prev = fs && fs.fsReadJSON('/system/world-model.prev.json'); + const cur = this._model; + if (!prev || !cur) return []; + const out = []; + if (prev.version && cur.version && prev.version !== cur.version) out.push(`version ${prev.version} → ${cur.version}`); + const fields = [['modules', 'loaded'], ['skills', 'count'], ['files', 'total'], ['agents', 'agents'], ['memory', 'nodes']]; + fields.forEach(function (f) { + const a = prev[f[0]] && prev[f[0]][f[1]]; + const b = cur[f[0]] && cur[f[0]][f[1]]; + if (typeof a === 'number' && typeof b === 'number' && a !== b) out.push(`${f[0]} ${a} → ${b}`); + }); + if (prev.synthesized) out.push('previous snapshot: ' + prev.synthesized); + return out; + } catch (e) { return []; } + } + refresh() { this._synthesize(); this.terminal.print(`✅ World Model refreshed at ${new Date().toLocaleTimeString()}`, 'success'); @@ -480,6 +575,9 @@ class WorldModelModule { _saveToVFS(fs) { try { if (!fs.readDir('/system')) fs.mkdir('system'); + // 2026-10-21: keep one previous model so "what changed" can be a real diff. + const prevModel = fs.fsReadJSON('/system/world-model.json'); + if (prevModel) { try { fs.fsWriteJSON('/system/world-model.prev.json', prevModel); } catch (_) {} } // v4.5.89b fix: Clean stale temp from previous crash (P1 from review) try { fs.deleteFile('/system/world-model.tmp.json'); } catch (_) {} fs.fsWriteJSON('/system/world-model.tmp.json', this._model); diff --git a/modules/test/test-world-model-ask.js b/modules/test/test-world-model-ask.js new file mode 100644 index 0000000..4a0c21a --- /dev/null +++ b/modules/test/test-world-model-ask.js @@ -0,0 +1,47 @@ +// node modules/test/test-world-model-ask.js +// Executes the real class from module-world-model.js (no imports, no DOM at class level). +const fs = require('fs'); +const path = require('path'); +const src = fs.readFileSync(path.join(__dirname, '../core/module-world-model.js'), 'utf8'); +const WorldModelModule = new Function(src + '\nreturn WorldModelModule;')(); + +const wm = new WorldModelModule(); +const failures = []; +const check = (name, cond, detail) => { + if (!cond) failures.push(name + (detail ? ' — ' + detail : '')); + console.log((cond ? 'PASS' : 'FAIL'), name, detail || ''); +}; + +// no model yet +check('empty model digest is empty', wm._modelDigest() === ''); + +wm._model = { + version: 'v4.6.47', synthesized: '2026-10-21T20:00:00.000Z', + system: { health: 'ok', memory: '3 MB' }, + modules: { loaded: 30, total: 30, modifiedCount: 0, modified: [] }, + files: { total: 42, directories: 8, size: '12 KB' }, + skills: { count: 23, skills: Array.from({ length: 60 }, (_, i) => 'skill-' + i) }, + memory: { nodes: 120, edges: 200, active: true }, + agents: { agents: 4, active: 1 }, + telemetry: { toolLatencyDataPoints: 9, recentErrors: 0 }, + projects: { totalTasks: 3, activeProjects: [] }, + recent: ['Skills directory: 23 files'] +}; + +const digest = wm._modelDigest(); +check('digest is valid JSON', (() => { try { JSON.parse(digest); return true; } catch (e) { return false; } })()); +check('digest is capped at 4000', digest.length <= 4000, digest.length + ' chars'); +check('digest keeps counts', /"count":23/.test(digest) && /"loaded":30/.test(digest)); +check('digest trims the skill list to 40', (JSON.parse(digest).skills.skills || []).length === 40, + String((JSON.parse(digest).skills.skills || []).length)); + +const prompt = wm._buildAskPrompt(' why did the build break? '); +check('prompt carries the question', /QUESTION: why did the build break\?$/.test(prompt), prompt.slice(-60)); +check('prompt carries the snapshot', prompt.indexOf('SNAPSHOT: {') > 0); +check('prompt forbids invention', /Never invent numbers/.test(prompt)); + +check('facts path still answers a keyword', /30 modules/.test(wm._answerQuery('how many modules'))); +check('facts path still refuses nonsense', /Unknown query/.test(wm._answerQuery('why did it break'))); + +console.log(failures.length ? '\n' + failures.length + ' FAILED' : '\nall PASS'); +process.exit(failures.length ? 1 : 0); |