Merge pull request #599 from proffesor-for-testing/working-july

chore(release): prepare v3.13.5
This commit is contained in:
Dragan Spiridonov
2026-08-03 13:29:21 +02:00
committed by GitHub
41 changed files with 3969 additions and 3197 deletions
+1 -1
View File
@@ -1 +1 @@
3.32.2
3.32.34
+430 -430
View File
@@ -1,430 +1,430 @@
#!/usr/bin/env node
/**
* Auto Memory Bridge Hook (ADR-048/049)
*
* Wires AutoMemoryBridge + LearningBridge + MemoryGraph into Claude Code
* session lifecycle. Called by settings.json SessionStart/SessionEnd hooks.
*
* Usage:
* node auto-memory-hook.mjs import # SessionStart: import auto memory files into backend
* node auto-memory-hook.mjs sync # SessionEnd: sync insights back to MEMORY.md
* node auto-memory-hook.mjs status # Show bridge status
*/
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const PROJECT_ROOT = join(__dirname, '../..');
const DATA_DIR = join(PROJECT_ROOT, '.claude-flow', 'data');
const STORE_PATH = join(DATA_DIR, 'auto-memory-store.json');
// Colors
const GREEN = '\x1b[0;32m';
const CYAN = '\x1b[0;36m';
const DIM = '\x1b[2m';
const RESET = '\x1b[0m';
const YELLOW = '\x1b[0;33m';
const log = (msg) => console.log(`${CYAN}[AutoMemory] ${msg}${RESET}`);
const success = (msg) => console.log(`${GREEN}[AutoMemory] ✓ ${msg}${RESET}`);
const dim = (msg) => console.log(` ${DIM}${msg}${RESET}`);
// #2545: fail LOUD instead of a silent dim skip. When @claude-flow/memory cannot
// be resolved, self-learning imports are a no-op — the user must see this and be
// told exactly how to fix it (on both stdout, so it shows in the Claude Code hook
// transcript, and stderr, per the issue's requested channel).
function warnMemoryUnavailable() {
const line1 = `[AutoMemory] @claude-flow/memory not resolvable from ${PROJECT_ROOT} — self-learning imports are DISABLED.`;
const line2 = ' Fix: npm i -D @claude-flow/memory (or re-run: npx ruflo@latest init, then npx ruflo@latest doctor --fix)';
console.log(`${YELLOW}${line1}${RESET}`);
console.log(`${YELLOW}${line2}${RESET}`);
process.stderr.write(`${line1}\n${line2}\n`);
}
const DEBUG = !!(process.env.RUFLO_DEBUG || process.env.DEBUG);
// ── Graceful shutdown (FIX 3) ───────────────────────────────────────────────
// Track the backend in use so a SIGTERM/SIGINT mid-run can still flush it
// (the JSON backend persists; a SQLite-backed one closes/flushes WAL) instead
// of leaving a half-written store or a stale lock behind.
let activeBackend = null;
let shuttingDown = false;
function trackBackend(b) { activeBackend = b; return b; }
async function gracefulExit(signal) {
if (shuttingDown) return;
shuttingDown = true;
if (DEBUG) process.stderr.write(`[AutoMemory] received ${signal}, flushing backend before exit\n`);
try {
if (activeBackend && typeof activeBackend.shutdown === 'function') await activeBackend.shutdown();
} catch { /* best effort — never block exit on cleanup */ }
process.exit(0);
}
process.on('SIGTERM', () => { gracefulExit('SIGTERM'); });
process.on('SIGINT', () => { gracefulExit('SIGINT'); });
// Ensure data dir
if (!existsSync(DATA_DIR)) mkdirSync(DATA_DIR, { recursive: true });
// ============================================================================
// Simple JSON File Backend (implements IMemoryBackend interface)
// ============================================================================
class JsonFileBackend {
constructor(filePath) {
this.filePath = filePath;
this.entries = new Map();
}
async initialize() {
if (existsSync(this.filePath)) {
try {
const data = JSON.parse(readFileSync(this.filePath, 'utf-8'));
if (Array.isArray(data)) {
for (const entry of data) this.entries.set(entry.id, entry);
}
} catch { /* start fresh */ }
}
}
async shutdown() { this._persist(); }
async store(entry) { this.entries.set(entry.id, entry); this._persist(); }
async get(id) { return this.entries.get(id) ?? null; }
async getByKey(key, ns) {
for (const e of this.entries.values()) {
if (e.key === key && (!ns || e.namespace === ns)) return e;
}
return null;
}
async update(id, updates) {
const e = this.entries.get(id);
if (!e) return null;
if (updates.metadata) Object.assign(e.metadata, updates.metadata);
if (updates.content !== undefined) e.content = updates.content;
if (updates.tags) e.tags = updates.tags;
e.updatedAt = Date.now();
this._persist();
return e;
}
async delete(id) { return this.entries.delete(id); }
async query(opts) {
let results = [...this.entries.values()];
if (opts?.namespace) results = results.filter(e => e.namespace === opts.namespace);
if (opts?.type) results = results.filter(e => e.type === opts.type);
if (opts?.limit) results = results.slice(0, opts.limit);
return results;
}
async search() { return []; } // No vector search in JSON backend
async bulkInsert(entries) { for (const e of entries) this.entries.set(e.id, e); this._persist(); }
async bulkDelete(ids) { let n = 0; for (const id of ids) { if (this.entries.delete(id)) n++; } this._persist(); return n; }
async count() { return this.entries.size; }
async listNamespaces() {
const ns = new Set();
for (const e of this.entries.values()) ns.add(e.namespace || 'default');
return [...ns];
}
async clearNamespace(ns) {
let n = 0;
for (const [id, e] of this.entries) {
if (e.namespace === ns) { this.entries.delete(id); n++; }
}
this._persist();
return n;
}
async getStats() {
return {
totalEntries: this.entries.size,
entriesByNamespace: {},
entriesByType: { semantic: 0, episodic: 0, procedural: 0, working: 0, cache: 0 },
memoryUsage: 0, avgQueryTime: 0, avgSearchTime: 0,
};
}
async healthCheck() {
return {
status: 'healthy',
components: {
storage: { status: 'healthy', latency: 0 },
index: { status: 'healthy', latency: 0 },
cache: { status: 'healthy', latency: 0 },
},
timestamp: Date.now(), issues: [], recommendations: [],
};
}
_persist() {
try {
writeFileSync(this.filePath, JSON.stringify([...this.entries.values()], null, 2), 'utf-8');
} catch { /* best effort */ }
}
}
// ============================================================================
// Resolve memory package path (local dev or npm installed)
// ============================================================================
async function loadMemoryPackage() {
// Strategy 0 (#2545): sidecar recorded by `init` / `doctor --fix`. On the
// documented `npx ruflo` path @claude-flow/memory (an optionalDependency of
// the CLI) lands in the npx cache, which is NOT on the walk-up path from the
// project — so init resolves it from the CLI's own context and records the
// absolute path here. This is the only strategy that works on that install.
try {
const sidecar = join(PROJECT_ROOT, '.claude-flow', 'memory-package.json');
if (existsSync(sidecar)) {
const rec = JSON.parse(readFileSync(sidecar, 'utf-8'));
if (rec?.distPath && existsSync(rec.distPath)) {
return await import(`file://${rec.distPath}`);
}
}
} catch { /* fall through */ }
// Strategy 1: Local dev (built dist)
const localDist = join(PROJECT_ROOT, 'v3/@claude-flow/memory/dist/index.js');
if (existsSync(localDist)) {
try {
return await import(`file://${localDist}`);
} catch { /* fall through */ }
}
// Strategy 2: Use createRequire for CJS-style resolution (handles nested node_modules
// when installed as a transitive dependency via npx ruflo / npx claude-flow)
try {
const { createRequire } = await import('module');
const require = createRequire(join(PROJECT_ROOT, 'package.json'));
return require('@claude-flow/memory');
} catch { /* fall through */ }
// Strategy 3: ESM import (works when @claude-flow/memory is a direct dependency)
try {
return await import('@claude-flow/memory');
} catch { /* fall through */ }
// Strategy 4: Walk up from PROJECT_ROOT looking for @claude-flow/memory in any node_modules
let searchDir = PROJECT_ROOT;
const { parse } = await import('path');
while (searchDir !== parse(searchDir).root) {
const candidate = join(searchDir, 'node_modules', '@claude-flow', 'memory', 'dist', 'index.js');
if (existsSync(candidate)) {
try {
return await import(`file://${candidate}`);
} catch { /* fall through */ }
}
searchDir = dirname(searchDir);
}
return null;
}
// ============================================================================
// Read config from .claude-flow/config.yaml
// ============================================================================
function readConfig() {
const configPath = join(PROJECT_ROOT, '.claude-flow', 'config.yaml');
const defaults = {
learningBridge: { enabled: true, sonaMode: 'balanced', confidenceDecayRate: 0.005, accessBoostAmount: 0.03, consolidationThreshold: 10 },
memoryGraph: { enabled: true, pageRankDamping: 0.85, maxNodes: 5000, similarityThreshold: 0.8 },
agentScopes: { enabled: true, defaultScope: 'project' },
};
if (!existsSync(configPath)) return defaults;
try {
const yaml = readFileSync(configPath, 'utf-8');
// Simple YAML parser for the memory section
const getBool = (key) => {
const match = yaml.match(new RegExp(`${key}:\\s*(true|false)`, 'i'));
return match ? match[1] === 'true' : undefined;
};
const lbEnabled = getBool('learningBridge[\\s\\S]*?enabled');
if (lbEnabled !== undefined) defaults.learningBridge.enabled = lbEnabled;
const mgEnabled = getBool('memoryGraph[\\s\\S]*?enabled');
if (mgEnabled !== undefined) defaults.memoryGraph.enabled = mgEnabled;
const asEnabled = getBool('agentScopes[\\s\\S]*?enabled');
if (asEnabled !== undefined) defaults.agentScopes.enabled = asEnabled;
return defaults;
} catch {
return defaults;
}
}
// ============================================================================
// Commands
// ============================================================================
async function doImport() {
log('Importing auto memory files into bridge...');
const memPkg = await loadMemoryPackage();
if (!memPkg || !memPkg.AutoMemoryBridge) {
warnMemoryUnavailable();
return;
}
const config = readConfig();
const backend = trackBackend(new JsonFileBackend(STORE_PATH));
await backend.initialize();
const bridgeConfig = {
workingDir: PROJECT_ROOT,
syncMode: 'on-session-end',
};
// Wire learning if enabled and available
if (config.learningBridge.enabled && memPkg.LearningBridge) {
bridgeConfig.learning = {
sonaMode: config.learningBridge.sonaMode,
confidenceDecayRate: config.learningBridge.confidenceDecayRate,
accessBoostAmount: config.learningBridge.accessBoostAmount,
consolidationThreshold: config.learningBridge.consolidationThreshold,
};
}
// Wire graph if enabled and available
if (config.memoryGraph.enabled && memPkg.MemoryGraph) {
bridgeConfig.graph = {
pageRankDamping: config.memoryGraph.pageRankDamping,
maxNodes: config.memoryGraph.maxNodes,
similarityThreshold: config.memoryGraph.similarityThreshold,
};
}
const bridge = new memPkg.AutoMemoryBridge(backend, bridgeConfig);
try {
const result = await bridge.importFromAutoMemory();
success(`Imported ${result.imported} entries (${result.skipped} skipped)`);
dim(`├─ Backend entries: ${await backend.count()}`);
dim(`├─ Learning: ${config.learningBridge.enabled ? 'active' : 'disabled'}`);
dim(`├─ Graph: ${config.memoryGraph.enabled ? 'active' : 'disabled'}`);
dim(`└─ Agent scopes: ${config.agentScopes.enabled ? 'active' : 'disabled'}`);
} catch (err) {
dim(`Import failed (non-critical): ${err.message}`);
}
await backend.shutdown();
}
async function doSync() {
log('Syncing insights to auto memory files...');
const memPkg = await loadMemoryPackage();
if (!memPkg || !memPkg.AutoMemoryBridge) {
warnMemoryUnavailable();
return;
}
const config = readConfig();
const backend = trackBackend(new JsonFileBackend(STORE_PATH));
await backend.initialize();
const entryCount = await backend.count();
if (entryCount === 0) {
dim('No entries to sync');
await backend.shutdown();
return;
}
const bridgeConfig = {
workingDir: PROJECT_ROOT,
syncMode: 'on-session-end',
};
if (config.learningBridge.enabled && memPkg.LearningBridge) {
bridgeConfig.learning = {
sonaMode: config.learningBridge.sonaMode,
confidenceDecayRate: config.learningBridge.confidenceDecayRate,
consolidationThreshold: config.learningBridge.consolidationThreshold,
};
}
if (config.memoryGraph.enabled && memPkg.MemoryGraph) {
bridgeConfig.graph = {
pageRankDamping: config.memoryGraph.pageRankDamping,
maxNodes: config.memoryGraph.maxNodes,
};
}
const bridge = new memPkg.AutoMemoryBridge(backend, bridgeConfig);
try {
const syncResult = await bridge.syncToAutoMemory();
success(`Synced ${syncResult.synced} entries to auto memory`);
dim(`├─ Categories updated: ${syncResult.categories?.join(', ') || 'none'}`);
dim(`└─ Backend entries: ${entryCount}`);
// Curate MEMORY.md index with graph-aware ordering
await bridge.curateIndex();
success('Curated MEMORY.md index');
} catch (err) {
dim(`Sync failed (non-critical): ${err.message}`);
}
if (bridge.destroy) bridge.destroy();
await backend.shutdown();
}
async function doStatus() {
const memPkg = await loadMemoryPackage();
const config = readConfig();
const sidecar = join(PROJECT_ROOT, '.claude-flow', 'memory-package.json');
const hasSidecar = existsSync(sidecar);
console.log('\n=== Auto Memory Bridge Status ===\n');
console.log(` Package: ${memPkg ? '✅ Available' : '❌ Not found — self-learning DISABLED (fix: npm i -D @claude-flow/memory)'}`);
console.log(` Resolver: ${hasSidecar ? '✅ .claude-flow/memory-package.json' : '⏸ no sidecar (run: npx ruflo@latest doctor --fix)'}`);
console.log(` Store: ${existsSync(STORE_PATH) ? '✅ ' + STORE_PATH : '⏸ Not initialized'}`);
console.log(` LearningBridge: ${config.learningBridge.enabled ? '✅ Enabled' : '⏸ Disabled'}`);
console.log(` MemoryGraph: ${config.memoryGraph.enabled ? '✅ Enabled' : '⏸ Disabled'}`);
console.log(` AgentScopes: ${config.agentScopes.enabled ? '✅ Enabled' : '⏸ Disabled'}`);
if (existsSync(STORE_PATH)) {
try {
const data = JSON.parse(readFileSync(STORE_PATH, 'utf-8'));
console.log(` Entries: ${Array.isArray(data) ? data.length : 0}`);
} catch { /* ignore */ }
}
console.log('');
}
// ============================================================================
// Main
// ============================================================================
const command = process.argv[2] || 'status';
// Dynamic import() failures can surface as unhandled rejections on a later
// microtask even when the awaiting call site already caught them, which would
// otherwise force a non-zero exit. Swallow to keep hooks exit-0, but surface the
// reason under RUFLO_DEBUG/DEBUG so genuine async bugs aren't silently hidden
// (FIX 2 — the previous `() => {}` discarded every rejection process-wide).
process.on('unhandledRejection', (reason) => {
if (DEBUG) {
const detail = reason && reason.message ? reason.message : String(reason);
process.stderr.write(`[AutoMemory] unhandledRejection (suppressed): ${detail}\n`);
}
});
try {
switch (command) {
case 'import': await doImport(); break;
case 'sync': await doSync(); break;
case 'status': await doStatus(); break;
default:
console.log('Usage: auto-memory-hook.mjs <import|sync|status>');
break;
}
} catch (err) {
// Hooks must never crash Claude Code - fail silently
try { dim(`Error (non-critical): ${err.message}`); } catch (_) {}
}
// Force clean exit — process.exitCode alone isn't enough if async errors override it
process.exit(0);
#!/usr/bin/env node
/**
* Auto Memory Bridge Hook (ADR-048/049)
*
* Wires AutoMemoryBridge + LearningBridge + MemoryGraph into Claude Code
* session lifecycle. Called by settings.json SessionStart/SessionEnd hooks.
*
* Usage:
* node auto-memory-hook.mjs import # SessionStart: import auto memory files into backend
* node auto-memory-hook.mjs sync # SessionEnd: sync insights back to MEMORY.md
* node auto-memory-hook.mjs status # Show bridge status
*/
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const PROJECT_ROOT = join(__dirname, '../..');
const DATA_DIR = join(PROJECT_ROOT, '.claude-flow', 'data');
const STORE_PATH = join(DATA_DIR, 'auto-memory-store.json');
// Colors
const GREEN = '\x1b[0;32m';
const CYAN = '\x1b[0;36m';
const DIM = '\x1b[2m';
const RESET = '\x1b[0m';
const YELLOW = '\x1b[0;33m';
const log = (msg) => console.log(`${CYAN}[AutoMemory] ${msg}${RESET}`);
const success = (msg) => console.log(`${GREEN}[AutoMemory] ✓ ${msg}${RESET}`);
const dim = (msg) => console.log(` ${DIM}${msg}${RESET}`);
// #2545: fail LOUD instead of a silent dim skip. When @claude-flow/memory cannot
// be resolved, self-learning imports are a no-op — the user must see this and be
// told exactly how to fix it (on both stdout, so it shows in the Claude Code hook
// transcript, and stderr, per the issue's requested channel).
function warnMemoryUnavailable() {
const line1 = `[AutoMemory] @claude-flow/memory not resolvable from ${PROJECT_ROOT} — self-learning imports are DISABLED.`;
const line2 = ' Fix: npm i -D @claude-flow/memory (or re-run: npx ruflo@latest init, then npx ruflo@latest doctor --fix)';
console.log(`${YELLOW}${line1}${RESET}`);
console.log(`${YELLOW}${line2}${RESET}`);
process.stderr.write(`${line1}\n${line2}\n`);
}
const DEBUG = !!(process.env.RUFLO_DEBUG || process.env.DEBUG);
// ── Graceful shutdown (FIX 3) ───────────────────────────────────────────────
// Track the backend in use so a SIGTERM/SIGINT mid-run can still flush it
// (the JSON backend persists; a SQLite-backed one closes/flushes WAL) instead
// of leaving a half-written store or a stale lock behind.
let activeBackend = null;
let shuttingDown = false;
function trackBackend(b) { activeBackend = b; return b; }
async function gracefulExit(signal) {
if (shuttingDown) return;
shuttingDown = true;
if (DEBUG) process.stderr.write(`[AutoMemory] received ${signal}, flushing backend before exit\n`);
try {
if (activeBackend && typeof activeBackend.shutdown === 'function') await activeBackend.shutdown();
} catch { /* best effort — never block exit on cleanup */ }
process.exit(0);
}
process.on('SIGTERM', () => { gracefulExit('SIGTERM'); });
process.on('SIGINT', () => { gracefulExit('SIGINT'); });
// Ensure data dir
if (!existsSync(DATA_DIR)) mkdirSync(DATA_DIR, { recursive: true });
// ============================================================================
// Simple JSON File Backend (implements IMemoryBackend interface)
// ============================================================================
class JsonFileBackend {
constructor(filePath) {
this.filePath = filePath;
this.entries = new Map();
}
async initialize() {
if (existsSync(this.filePath)) {
try {
const data = JSON.parse(readFileSync(this.filePath, 'utf-8'));
if (Array.isArray(data)) {
for (const entry of data) this.entries.set(entry.id, entry);
}
} catch { /* start fresh */ }
}
}
async shutdown() { this._persist(); }
async store(entry) { this.entries.set(entry.id, entry); this._persist(); }
async get(id) { return this.entries.get(id) ?? null; }
async getByKey(key, ns) {
for (const e of this.entries.values()) {
if (e.key === key && (!ns || e.namespace === ns)) return e;
}
return null;
}
async update(id, updates) {
const e = this.entries.get(id);
if (!e) return null;
if (updates.metadata) Object.assign(e.metadata, updates.metadata);
if (updates.content !== undefined) e.content = updates.content;
if (updates.tags) e.tags = updates.tags;
e.updatedAt = Date.now();
this._persist();
return e;
}
async delete(id) { return this.entries.delete(id); }
async query(opts) {
let results = [...this.entries.values()];
if (opts?.namespace) results = results.filter(e => e.namespace === opts.namespace);
if (opts?.type) results = results.filter(e => e.type === opts.type);
if (opts?.limit) results = results.slice(0, opts.limit);
return results;
}
async search() { return []; } // No vector search in JSON backend
async bulkInsert(entries) { for (const e of entries) this.entries.set(e.id, e); this._persist(); }
async bulkDelete(ids) { let n = 0; for (const id of ids) { if (this.entries.delete(id)) n++; } this._persist(); return n; }
async count() { return this.entries.size; }
async listNamespaces() {
const ns = new Set();
for (const e of this.entries.values()) ns.add(e.namespace || 'default');
return [...ns];
}
async clearNamespace(ns) {
let n = 0;
for (const [id, e] of this.entries) {
if (e.namespace === ns) { this.entries.delete(id); n++; }
}
this._persist();
return n;
}
async getStats() {
return {
totalEntries: this.entries.size,
entriesByNamespace: {},
entriesByType: { semantic: 0, episodic: 0, procedural: 0, working: 0, cache: 0 },
memoryUsage: 0, avgQueryTime: 0, avgSearchTime: 0,
};
}
async healthCheck() {
return {
status: 'healthy',
components: {
storage: { status: 'healthy', latency: 0 },
index: { status: 'healthy', latency: 0 },
cache: { status: 'healthy', latency: 0 },
},
timestamp: Date.now(), issues: [], recommendations: [],
};
}
_persist() {
try {
writeFileSync(this.filePath, JSON.stringify([...this.entries.values()], null, 2), 'utf-8');
} catch { /* best effort */ }
}
}
// ============================================================================
// Resolve memory package path (local dev or npm installed)
// ============================================================================
async function loadMemoryPackage() {
// Strategy 0 (#2545): sidecar recorded by `init` / `doctor --fix`. On the
// documented `npx ruflo` path @claude-flow/memory (an optionalDependency of
// the CLI) lands in the npx cache, which is NOT on the walk-up path from the
// project — so init resolves it from the CLI's own context and records the
// absolute path here. This is the only strategy that works on that install.
try {
const sidecar = join(PROJECT_ROOT, '.claude-flow', 'memory-package.json');
if (existsSync(sidecar)) {
const rec = JSON.parse(readFileSync(sidecar, 'utf-8'));
if (rec?.distPath && existsSync(rec.distPath)) {
return await import(`file://${rec.distPath}`);
}
}
} catch { /* fall through */ }
// Strategy 1: Local dev (built dist)
const localDist = join(PROJECT_ROOT, 'v3/@claude-flow/memory/dist/index.js');
if (existsSync(localDist)) {
try {
return await import(`file://${localDist}`);
} catch { /* fall through */ }
}
// Strategy 2: Use createRequire for CJS-style resolution (handles nested node_modules
// when installed as a transitive dependency via npx ruflo / npx claude-flow)
try {
const { createRequire } = await import('module');
const require = createRequire(join(PROJECT_ROOT, 'package.json'));
return require('@claude-flow/memory');
} catch { /* fall through */ }
// Strategy 3: ESM import (works when @claude-flow/memory is a direct dependency)
try {
return await import('@claude-flow/memory');
} catch { /* fall through */ }
// Strategy 4: Walk up from PROJECT_ROOT looking for @claude-flow/memory in any node_modules
let searchDir = PROJECT_ROOT;
const { parse } = await import('path');
while (searchDir !== parse(searchDir).root) {
const candidate = join(searchDir, 'node_modules', '@claude-flow', 'memory', 'dist', 'index.js');
if (existsSync(candidate)) {
try {
return await import(`file://${candidate}`);
} catch { /* fall through */ }
}
searchDir = dirname(searchDir);
}
return null;
}
// ============================================================================
// Read config from .claude-flow/config.yaml
// ============================================================================
function readConfig() {
const configPath = join(PROJECT_ROOT, '.claude-flow', 'config.yaml');
const defaults = {
learningBridge: { enabled: true, sonaMode: 'balanced', confidenceDecayRate: 0.005, accessBoostAmount: 0.03, consolidationThreshold: 10 },
memoryGraph: { enabled: true, pageRankDamping: 0.85, maxNodes: 5000, similarityThreshold: 0.8 },
agentScopes: { enabled: true, defaultScope: 'project' },
};
if (!existsSync(configPath)) return defaults;
try {
const yaml = readFileSync(configPath, 'utf-8');
// Simple YAML parser for the memory section
const getBool = (key) => {
const match = yaml.match(new RegExp(`${key}:\\s*(true|false)`, 'i'));
return match ? match[1] === 'true' : undefined;
};
const lbEnabled = getBool('learningBridge[\\s\\S]*?enabled');
if (lbEnabled !== undefined) defaults.learningBridge.enabled = lbEnabled;
const mgEnabled = getBool('memoryGraph[\\s\\S]*?enabled');
if (mgEnabled !== undefined) defaults.memoryGraph.enabled = mgEnabled;
const asEnabled = getBool('agentScopes[\\s\\S]*?enabled');
if (asEnabled !== undefined) defaults.agentScopes.enabled = asEnabled;
return defaults;
} catch {
return defaults;
}
}
// ============================================================================
// Commands
// ============================================================================
async function doImport() {
log('Importing auto memory files into bridge...');
const memPkg = await loadMemoryPackage();
if (!memPkg || !memPkg.AutoMemoryBridge) {
warnMemoryUnavailable();
return;
}
const config = readConfig();
const backend = trackBackend(new JsonFileBackend(STORE_PATH));
await backend.initialize();
const bridgeConfig = {
workingDir: PROJECT_ROOT,
syncMode: 'on-session-end',
};
// Wire learning if enabled and available
if (config.learningBridge.enabled && memPkg.LearningBridge) {
bridgeConfig.learning = {
sonaMode: config.learningBridge.sonaMode,
confidenceDecayRate: config.learningBridge.confidenceDecayRate,
accessBoostAmount: config.learningBridge.accessBoostAmount,
consolidationThreshold: config.learningBridge.consolidationThreshold,
};
}
// Wire graph if enabled and available
if (config.memoryGraph.enabled && memPkg.MemoryGraph) {
bridgeConfig.graph = {
pageRankDamping: config.memoryGraph.pageRankDamping,
maxNodes: config.memoryGraph.maxNodes,
similarityThreshold: config.memoryGraph.similarityThreshold,
};
}
const bridge = new memPkg.AutoMemoryBridge(backend, bridgeConfig);
try {
const result = await bridge.importFromAutoMemory();
success(`Imported ${result.imported} entries (${result.skipped} skipped)`);
dim(`├─ Backend entries: ${await backend.count()}`);
dim(`├─ Learning: ${config.learningBridge.enabled ? 'active' : 'disabled'}`);
dim(`├─ Graph: ${config.memoryGraph.enabled ? 'active' : 'disabled'}`);
dim(`└─ Agent scopes: ${config.agentScopes.enabled ? 'active' : 'disabled'}`);
} catch (err) {
dim(`Import failed (non-critical): ${err.message}`);
}
await backend.shutdown();
}
async function doSync() {
log('Syncing insights to auto memory files...');
const memPkg = await loadMemoryPackage();
if (!memPkg || !memPkg.AutoMemoryBridge) {
warnMemoryUnavailable();
return;
}
const config = readConfig();
const backend = trackBackend(new JsonFileBackend(STORE_PATH));
await backend.initialize();
const entryCount = await backend.count();
if (entryCount === 0) {
dim('No entries to sync');
await backend.shutdown();
return;
}
const bridgeConfig = {
workingDir: PROJECT_ROOT,
syncMode: 'on-session-end',
};
if (config.learningBridge.enabled && memPkg.LearningBridge) {
bridgeConfig.learning = {
sonaMode: config.learningBridge.sonaMode,
confidenceDecayRate: config.learningBridge.confidenceDecayRate,
consolidationThreshold: config.learningBridge.consolidationThreshold,
};
}
if (config.memoryGraph.enabled && memPkg.MemoryGraph) {
bridgeConfig.graph = {
pageRankDamping: config.memoryGraph.pageRankDamping,
maxNodes: config.memoryGraph.maxNodes,
};
}
const bridge = new memPkg.AutoMemoryBridge(backend, bridgeConfig);
try {
const syncResult = await bridge.syncToAutoMemory();
success(`Synced ${syncResult.synced} entries to auto memory`);
dim(`├─ Categories updated: ${syncResult.categories?.join(', ') || 'none'}`);
dim(`└─ Backend entries: ${entryCount}`);
// Curate MEMORY.md index with graph-aware ordering
await bridge.curateIndex();
success('Curated MEMORY.md index');
} catch (err) {
dim(`Sync failed (non-critical): ${err.message}`);
}
if (bridge.destroy) bridge.destroy();
await backend.shutdown();
}
async function doStatus() {
const memPkg = await loadMemoryPackage();
const config = readConfig();
const sidecar = join(PROJECT_ROOT, '.claude-flow', 'memory-package.json');
const hasSidecar = existsSync(sidecar);
console.log('\n=== Auto Memory Bridge Status ===\n');
console.log(` Package: ${memPkg ? '✅ Available' : '❌ Not found — self-learning DISABLED (fix: npm i -D @claude-flow/memory)'}`);
console.log(` Resolver: ${hasSidecar ? '✅ .claude-flow/memory-package.json' : '⏸ no sidecar (run: npx ruflo@latest doctor --fix)'}`);
console.log(` Store: ${existsSync(STORE_PATH) ? '✅ ' + STORE_PATH : '⏸ Not initialized'}`);
console.log(` LearningBridge: ${config.learningBridge.enabled ? '✅ Enabled' : '⏸ Disabled'}`);
console.log(` MemoryGraph: ${config.memoryGraph.enabled ? '✅ Enabled' : '⏸ Disabled'}`);
console.log(` AgentScopes: ${config.agentScopes.enabled ? '✅ Enabled' : '⏸ Disabled'}`);
if (existsSync(STORE_PATH)) {
try {
const data = JSON.parse(readFileSync(STORE_PATH, 'utf-8'));
console.log(` Entries: ${Array.isArray(data) ? data.length : 0}`);
} catch { /* ignore */ }
}
console.log('');
}
// ============================================================================
// Main
// ============================================================================
const command = process.argv[2] || 'status';
// Dynamic import() failures can surface as unhandled rejections on a later
// microtask even when the awaiting call site already caught them, which would
// otherwise force a non-zero exit. Swallow to keep hooks exit-0, but surface the
// reason under RUFLO_DEBUG/DEBUG so genuine async bugs aren't silently hidden
// (FIX 2 — the previous `() => {}` discarded every rejection process-wide).
process.on('unhandledRejection', (reason) => {
if (DEBUG) {
const detail = reason && reason.message ? reason.message : String(reason);
process.stderr.write(`[AutoMemory] unhandledRejection (suppressed): ${detail}\n`);
}
});
try {
switch (command) {
case 'import': await doImport(); break;
case 'sync': await doSync(); break;
case 'status': await doStatus(); break;
default:
console.log('Usage: auto-memory-hook.mjs <import|sync|status>');
break;
}
} catch (err) {
// Hooks must never crash Claude Code - fail silently
try { dim(`Error (non-critical): ${err.message}`); } catch (_) {}
}
// Force clean exit — process.exitCode alone isn't enough if async errors override it
process.exit(0);
+6 -6
View File
@@ -1,13 +1,13 @@
{
"manifest": {
"version": "3.32.2",
"version": "3.32.34",
"files": {
"auto-memory-hook.mjs": "e3e1033b24704992ddef6b31c7fa9dd7fcd9e1af7935dd77ef73402b916b31e6",
"hook-handler.cjs": "f87ec28684bfc5fd0a54c46bd2a53a3fb037defe71d0ea4b8a5cb88a2cd87a3f",
"intelligence.cjs": "5a55d979cb7ba5c8c4f27f3b2e6d686fbb1045d180023b803da672a37e05b915",
"statusline.cjs": "f6fcaedad7b521248ba57c494eb1a0ce696679fa3f8709f81edf3fdecef369c2"
"auto-memory-hook.mjs": "68be7e9a9eba7bf9c4e8a230db7bf61a243b965639f8504842799d6c6ca28762",
"hook-handler.cjs": "50ea92a72651bdc95634f7588d56a5870963168eef5226f66ed14af3b47c8d9a",
"intelligence.cjs": "bd1f8e4b034944aee1df0391dc47ac2e8cc4b3aa542c65407a59d49620bbf76b",
"statusline.cjs": "d2a0eac56d1267d8dbed2d70b09feb592299469196ec4b215909f2b052144882"
}
},
"signature": "r7TaWKVLZ6gxssGRM31J6usZrRm1Y5TAZXNmOfeLn+7VzWMrH6pZZPGTfSv6WMW49j8Ko3aamDBtahasWybSAw==",
"signature": "i0qilACTabRlI6VORA0QyP0r4EuvJsrvtkucxia3TStgq/la250iesyKjlcHbLV/6fshZiGtPMLq1nmXLf+ZAw==",
"algorithm": "ed25519"
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+48
View File
@@ -5,6 +5,54 @@ All notable changes to the Agentic QE project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [3.13.5] - 2026-08-03
Quality decisions and generated tests now carry evidence that reflects what AQE
actually measured or executed, while initialization and embedding workflows are
more reliable across supported environments.
### Added
- **Measured quality-gate evidence** ([#596]). CLI and MCP quality evaluation
now share canonical thresholds and require fresh, attributed measurements for
coverage, passing tests, critical bugs, code smells, security findings,
technical debt, and duplication. Missing, malformed, or stale evidence fails
closed instead of silently receiving a fabricated passing value.
- **Optional status-line installation** ([#591]). `aqe init --no-statusline`
skips AQE's status line, removes an existing AQE-owned status line during an
upgrade, and preserves a project-authored status line.
### Fixed
- **Generated tests are trustworthy and executable** ([#577]). Test generation
preserves framework imports, validates evidence before reporting success, and
no longer treats structurally incomplete output as a passing quality result.
- **Node's built-in test runner is honored end to end** ([#592]). Test discovery
filters Node test files correctly, execution uses the selected framework, and
retry confidence remains bounded.
- **Batch embedding semantics are consistent** ([#585]). Mixed batches send only
semantic content to the embedding provider, nonsemantic entries receive zero
vectors in their original positions, and all-nonsemantic batches avoid
provider initialization entirely.
- **HNSW initialization works in native ESM** ([#586]). Runtime dependency
loading no longer assumes a CommonJS global `require`.
- **Learning-engine tests use isolated persistence** ([#588]), preventing shared
worker databases from accumulating duplicate genesis data and causing long
test runs.
### Changed
- Refreshed bundled Claude helpers and development-only Ruflo dependencies,
including upstream security and compatibility fixes.
[#577]: https://github.com/proffesor-for-testing/agentic-qe/issues/577
[#585]: https://github.com/proffesor-for-testing/agentic-qe/issues/585
[#586]: https://github.com/proffesor-for-testing/agentic-qe/issues/586
[#588]: https://github.com/proffesor-for-testing/agentic-qe/issues/588
[#591]: https://github.com/proffesor-for-testing/agentic-qe/issues/591
[#592]: https://github.com/proffesor-for-testing/agentic-qe/issues/592
[#596]: https://github.com/proffesor-for-testing/agentic-qe/issues/596
## [3.13.4] - 2026-08-02
Codex users now receive a complete, upgradeable Agentic QE workflow instead of
+1 -1
View File
@@ -940,7 +940,7 @@
},
"metadata": {
"generatedBy": "Agentic QE Fleet",
"fleetVersion": "3.13.4",
"fleetVersion": "3.13.5",
"manifestVersion": "1.4.0",
"lastUpdated": "2026-04-13T00:00:00.000Z",
"contributors": [
+12 -3
View File
@@ -234,7 +234,7 @@ aqe security --compliance gdpr,hipaa,soc2 # Compliance checks
aqe security --url-validate https://example.com # URL + PII exposure validation
# Quality gates
aqe quality --gate # Evaluate quality gate (pass/fail with exit codes)
aqe quality --gate # Measured coverage/tests: exit 0 pass, 1 fail, 2 near threshold
# Code intelligence
aqe code index src/ # Index codebase into knowledge graph
@@ -492,5 +492,14 @@ aqe test generate src/
aqe coverage src/ --risk --gaps
aqe security --sast -t src/
aqe quality --gate
aqe fleet init --wizard
```
aqe fleet init --wizard
```
`aqe quality --gate` currently evaluates only measured line coverage and test
pass-rate evidence stored by the coverage and test commands. It deliberately
does not invent values for security vulnerabilities, critical bugs, code
smells, technical debt, or duplication; restoring those measured checks is
tracked in [#596](https://github.com/proffesor-for-testing/agentic-qe/issues/596).
The command exits `0` when all measured checks pass with more than five
percentage points of headroom, `1` when a check fails, and `2` when the gate
passes but a check has less than five percentage points of headroom.
+1
View File
@@ -4,6 +4,7 @@ All Agentic QE release notes organized by version.
| Version | Date | Highlights |
|---------|------|------------|
| [v3.13.5](v3.13.5.md) | 2026-08-03 | Measured quality gates and trustworthy cross-framework test execution. |
| [v3.13.4](v3.13.4.md) | 2026-08-02 | Reliable Codex fleet setup, upgrades, hooks, and verification. |
| [v3.13.3](v3.13.3.md) | 2026-07-29 | Learning capture no longer stops silently under `AQE_DISABLE_WAL` (it now refuses to write in an unsafe journal mode and names what is holding the DB), and QE-Court's shipped panel no longer violates its own `writerIsNeverJuror` rule (#576). |
| [v3.13.2](v3.13.2.md) | 2026-07-24 | Stops hook-driven `brain.rvf`/`patterns.rvf` corruption loops by closing native stores after every hook and protecting same-process live locks (#574). |
+61
View File
@@ -0,0 +1,61 @@
# v3.13.5 Release Notes
**Release Date:** 2026-08-03
## Highlights
AQE now bases quality decisions on fresh, attributed measurements and produces
more trustworthy, executable tests across Jest, Vitest, and Node's built-in test
runner. Initialization and embeddings are also more reliable in real projects.
## Added
- Canonical quality evidence for coverage, passing tests, bugs, code smells,
security findings, technical debt, and duplication.
- Shared CLI and MCP quality thresholds that fail closed when measurements are
missing, malformed, or stale.
- `aqe init --no-statusline` for installations that do not want AQE's status
line, without disturbing a project-authored status line.
## Fixed
- Generated tests retain the correct framework imports and must satisfy
evidence-backed validation before AQE reports success.
- Node test discovery, execution, and retry behavior now honor the selected
framework consistently.
- Mixed embedding batches preserve output order while excluding nonsemantic
content from provider calls; entirely nonsemantic batches make no provider
call.
- HNSW runtime loading works when AQE is imported as native ESM.
- Learning-engine unit tests no longer share persistence across test cases.
## Changed
- Refreshed bundled Claude helpers.
- Updated development-only Ruflo UI and tooling dependencies, including upstream
security fixes.
## Upgrade notes
No configuration migration is required. Existing installations can upgrade and
refresh their managed assets normally:
```bash
npx agentic-qe init --auto --upgrade
```
Projects that want to remove only AQE's managed status line can run:
```bash
npx agentic-qe init --auto --upgrade --no-statusline
```
Project-authored status-line configuration is preserved.
Tracking: [#577](https://github.com/proffesor-for-testing/agentic-qe/issues/577),
[#585](https://github.com/proffesor-for-testing/agentic-qe/issues/585),
[#586](https://github.com/proffesor-for-testing/agentic-qe/issues/586),
[#588](https://github.com/proffesor-for-testing/agentic-qe/issues/588),
[#591](https://github.com/proffesor-for-testing/agentic-qe/issues/591),
[#592](https://github.com/proffesor-for-testing/agentic-qe/issues/592), and
[#596](https://github.com/proffesor-for-testing/agentic-qe/issues/596).
+4 -3
View File
@@ -1,12 +1,12 @@
{
"name": "agentic-qe",
"version": "3.13.4",
"version": "3.13.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "agentic-qe",
"version": "3.13.4",
"version": "3.13.5",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
@@ -28,7 +28,6 @@
"pg": "^8.17.2",
"prime-radiant-advanced-wasm": "^0.1.3",
"secure-json-parse": "^4.1.0",
"typescript": "^5.9.3",
"uuid": "^14.0.0",
"vibium": "^0.1.2",
"web-tree-sitter": "~0.26.8",
@@ -60,6 +59,7 @@
"glob": "^13.0.0",
"msw": "^2.12.7",
"tsx": "^4.21.0",
"typescript": "^5.9.3",
"vitest": "^4.0.16"
},
"engines": {
@@ -11248,6 +11248,7 @@
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "agentic-qe",
"version": "3.13.4",
"version": "3.13.5",
"description": "Agentic Quality Engineering V3 - Domain-Driven Design Architecture with 13 Bounded Contexts, O(log n) coverage analysis, ReasoningBank learning, 60 specialized QE agents, mathematical Coherence verification, deep Claude Flow integration",
"type": "module",
"main": "./dist/index.js",
@@ -71,7 +71,7 @@
"test:unit:fast": "vitest run --exclude='**/qe-reasoning-bank.test.ts' --exclude='**/qe-reasoning-bank-feedback-loop.test.ts' --exclude='**/aqe-learning-engine.test.ts' --exclude='**/aqe-learning-engine-patterns.test.ts' tests/unit/adapters tests/unit/shared tests/unit/cli tests/unit/learning tests/unit/kernel tests/unit/workers tests/unit/routing tests/unit/strange-loop tests/unit/sync tests/unit/feedback tests/unit/error-paths tests/unit/early-exit tests/unit/causal-discovery tests/unit/neural-optimizer tests/unit/test-scheduling tests/unit/logging tests/unit/validation tests/unit/memory tests/unit/performance tests/unit/scripts tests/unit/planning",
"test:unit:heavy": "vitest run tests/unit/coordination tests/unit/domains tests/unit/integrations tests/unit/optimization tests/unit/init --exclude='**/browser/**' --exclude='**/*.e2e.test.ts' --exclude='**/vibium/**' --exclude='**/browser-swarm-coordinator.test.ts'",
"test:unit:mcp": "vitest run tests/unit/mcp --exclude='**/mcp/handlers/domain-handlers.test.ts' --fileParallelism=false",
"test:ci": "vitest run --exclude='**/browser/**' --exclude='**/*.e2e.test.ts' --exclude='**/vibium/**' --exclude='**/integration/browser/**' --exclude='**/browser-swarm-coordinator.test.ts' --exclude='**/mcp/handlers/domain-handlers.test.ts'",
"test:ci": "vitest run --exclude='**/browser/**' --exclude='**/*.e2e.test.ts' --exclude='**/vibium/**' --exclude='**/integration/browser/**' --exclude='**/browser-swarm-coordinator.test.ts' --exclude='**/mcp/handlers/domain-handlers.test.ts' --exclude='**/fixtures/init-corpus/**'",
"test:e2e": "vitest run tests/integration/browser --testTimeout=120000",
"test:safe": "NODE_OPTIONS='--max-old-space-size=768 --expose-gc' vitest run --maxForks=1",
"test:dev": "npm run test:unit:fast",
@@ -183,7 +183,6 @@
"pg": "^8.17.2",
"prime-radiant-advanced-wasm": "^0.1.3",
"secure-json-parse": "^4.1.0",
"typescript": "^5.9.3",
"uuid": "^14.0.0",
"vibium": "^0.1.2",
"web-tree-sitter": "~0.26.8",
@@ -260,6 +259,7 @@
"glob": "^13.0.0",
"msw": "^2.12.7",
"tsx": "^4.21.0",
"typescript": "^5.9.3",
"vitest": "^4.0.16"
},
"engines": {
+32 -47
View File
@@ -10,57 +10,42 @@ import chalk from 'chalk';
import type { MemoryBackend } from '../../kernel/interfaces.js';
import type { CLIContext } from '../handlers/interfaces.js';
import { type OutputFormat, type QualityGateResult, writeOutput, toJSON, qualityGateToMarkdown } from '../utils/ci-output.js';
import {
evaluateQualityEvidence,
loadQualityEvidence as loadCanonicalQualityEvidence,
type QualityEvidenceValues,
} from '../../domains/quality-assessment/quality-evidence.js';
export async function loadQualityEvidence(memory: MemoryBackend): Promise<{
coverage: number;
testsPassing: number;
}> {
const coverage = await memory.get<{ line?: number }>('coverage:latest');
const tests = await memory.get<{ passed?: number; failed?: number; skipped?: number }>(
'test-run:latest',
{ namespace: 'test-execution' }
);
if (!coverage || !tests) {
throw new Error(
'No measured quality evidence found in AgentDB. Run coverage and `aqe test execute` before `aqe quality --gate`.'
);
}
const total = (tests.passed ?? 0) + (tests.failed ?? 0) + (tests.skipped ?? 0);
if (!Number.isFinite(coverage.line) || total <= 0 || !Number.isFinite(tests.passed)) {
throw new Error('Measured quality evidence is malformed or incomplete.');
}
export async function loadQualityEvidence(memory: MemoryBackend): Promise<QualityEvidenceValues> {
return loadCanonicalQualityEvidence(memory);
}
export function evaluateMeasuredQualityEvidence(measured: QualityEvidenceValues): QualityGateResult {
const evaluation = evaluateQualityEvidence(measured);
return {
coverage: coverage.line!,
testsPassing: ((tests.passed ?? 0) / total) * 100,
passed: evaluation.passed,
score: 'N/A',
checks: evaluation.checks,
recommendations: evaluation.recommendations,
};
}
export function evaluateMeasuredQualityEvidence(measured: {
coverage: number;
testsPassing: number;
}): QualityGateResult {
const checks = [
{
name: 'coverage',
passed: measured.coverage >= 80,
value: measured.coverage,
threshold: 80,
},
{
name: 'testsPassing',
passed: measured.testsPassing >= 95,
value: measured.testsPassing,
threshold: 95,
},
];
return {
passed: checks.every(check => check.passed),
score: 'N/A',
checks,
recommendations: checks
.filter(check => !check.passed)
.map(check => `${check.name} is below its measured threshold.`),
};
/**
* Published quality command exit contract:
* 0 = passed with more than five percentage points of headroom
* 1 = one or more measured checks failed
* 2 = passed, but at least one measured check has less than five points of headroom
*/
export function getMeasuredQualityExitCode(result: QualityGateResult): 0 | 1 | 2 {
if (!result.passed) return 1;
return result.checks.some(check => (
(check as typeof check & { direction?: string }).direction !== 'max'
&&
typeof check.value === 'number'
&& typeof check.threshold === 'number'
&& check.value >= check.threshold
&& check.value < check.threshold + 5
)) ? 2 : 0;
}
export function createQualityCommand(
@@ -111,7 +96,7 @@ export function createQualityCommand(
console.log('');
}
await cleanupAndExit(gateResult.passed ? 0 : 1);
await cleanupAndExit(getMeasuredQualityExitCode(gateResult));
}
} catch (error) {
+8 -1
View File
@@ -8,7 +8,8 @@ import { Command } from 'commander';
import chalk from 'chalk';
import type { CLIContext } from '../handlers/interfaces.js';
import { filterTestFilesForFramework, walkSourceFiles } from '../utils/file-discovery.js';
import { type OutputFormat, writeOutput, toJSON, toJUnit, testRunToMarkdown, type TestRunSummary } from '../utils/ci-output.js';
import { type OutputFormat, writeOutput, toJSON, toJUnit, testRunToMarkdown, type TestRunSummary } from '../utils/ci-output.js';
import { writeQualityEvidence } from '../../domains/quality-assessment/quality-evidence.js';
export function createTestCommand(
context: CLIContext,
@@ -170,6 +171,12 @@ export function createTestCommand(
duration: run.duration,
measuredAt: new Date().toISOString(),
}, { namespace: 'test-execution', persist: true });
await writeQualityEvidence(context.kernel!.memory, {
testsPassing: total > 0 ? (run.passed / total) * 100 : 0,
}, {
measuredAt: new Date().toISOString(),
source: 'aqe test execute',
});
if (format === 'json') {
writeOutput(toJSON(run), options.output);
+13 -8
View File
@@ -45,7 +45,8 @@ export class InitHandler implements ICommandHandler {
.option('-d, --domains <domains>', 'Comma-separated list of domains to enable', 'all')
.option('-m, --max-agents <number>', 'Maximum concurrent agents', '15')
.option('--memory <backend>', 'Memory backend (sqlite|agentdb|hybrid|memory). "memory" = database-free, in-memory only.', 'hybrid')
.option('--no-database', 'Database-free install: alias for `--memory memory`. Skips the SQLite database phase and runs any MCP server in-memory — nothing is written to .agentic-qe/.')
.option('--no-database', 'Database-free install: alias for `--memory memory`. Skips the SQLite database phase and runs any MCP server in-memory — nothing is written to .agentic-qe/.')
.option('--no-statusline', 'Do not install AQE\'s Claude Code status line; remove an existing AQE-owned status line while preserving custom ones')
.option('--lazy', 'Enable lazy loading of domains')
.option('--wizard', 'Run interactive setup wizard')
.option('--auto', 'Auto-configure based on project analysis')
@@ -145,9 +146,10 @@ export class InitHandler implements ICommandHandler {
// only honored by the modular orchestrator. Route there too, or the flag
// silently falls through to runStandardInit and installs the full Claude
// surface it was meant to suppress.
const noClaudeRequested = options.claude === false;
if (options.modular || platformRequested || databaseFree || noClaudeRequested) {
const noClaudeRequested = options.claude === false;
const noStatusLineRequested = options.statusline === false;
if (options.modular || platformRequested || databaseFree || noClaudeRequested || noStatusLineRequested) {
console.log(chalk.blue('\n Agentic QE v3 Initialization\n'));
await this.runModularInit(options, context);
return;
@@ -214,8 +216,9 @@ export class InitHandler implements ICommandHandler {
withContinueDev: options.withContinuedev,
noMcp: options.noMcp && !options.withMcp,
noGovernance: options.noGovernance,
noClaude,
memoryBackend: memoryOnly ? 'memory' : undefined,
noClaude,
noStatusLine: options.statusline === false,
memoryBackend: memoryOnly ? 'memory' : undefined,
});
console.log(chalk.white(' Analyzing project...\n'));
@@ -648,8 +651,10 @@ interface InitOptions {
withClaudeFlow?: boolean;
skipClaudeFlow?: boolean;
noGovernance?: boolean;
/** commander negatable flag: `--no-claude` sets this to false (#532). */
claude?: boolean;
/** commander negatable flag: `--no-claude` sets this to false (#532). */
claude?: boolean;
/** commander negatable flag: `--no-statusline` sets this to false (#591). */
statusline?: boolean;
modular?: boolean;
}
@@ -23,6 +23,7 @@ import {
import type { HybridRouter, ChatResponse } from '../../../shared/llm';
import { toError } from '../../../shared/error-utils.js';
import { safeJsonParse } from '../../../shared/safe-json.js';
import { writeQualityEvidence } from '../../quality-assessment/quality-evidence.js';
// ============================================================================
// Service Interface
@@ -613,6 +614,10 @@ Provide thoughtful, specific analysis based on the coverage data. Do not include
// Store latest snapshot
await this.memory.set('coverage:latest', summary, { persist: true });
await writeQualityEvidence(this.memory, { coverage: summary.line }, {
measuredAt: new Date().toISOString(),
source: 'coverage-analysis',
});
// Store historical snapshot
const timestamp = Date.now();
+15
View File
@@ -50,6 +50,21 @@ export {
type DeploymentAccuracy,
} from './services/deployment-advisor';
export {
DEFAULT_QUALITY_EVIDENCE_MAX_AGE_MS,
DEFAULT_QUALITY_THRESHOLDS,
QUALITY_EVIDENCE_NAMESPACE,
QUALITY_METRICS,
evaluateQualityEvidence,
loadQualityEvidence,
writeQualityEvidence,
type EvaluatedQualityCheck,
type QualityEvidenceRecord,
type QualityEvidenceValues,
type QualityMetricName,
type QualityThreshold,
} from './quality-evidence.js';
// ============================================================================
// Coherence-Gated Quality Gates (ADR-030)
// ============================================================================
@@ -0,0 +1,167 @@
import type { MemoryBackend } from '../../kernel/interfaces.js';
export const QUALITY_EVIDENCE_NAMESPACE = 'quality-assessment';
export const DEFAULT_QUALITY_EVIDENCE_MAX_AGE_MS = 24 * 60 * 60 * 1000;
export const QUALITY_METRICS = [
'coverage',
'testsPassing',
'criticalBugs',
'codeSmells',
'securityVulnerabilities',
'technicalDebt',
'duplications',
] as const;
export type QualityMetricName = typeof QUALITY_METRICS[number];
export type QualityEvidenceValues = Record<QualityMetricName, number>;
export interface QualityEvidenceRecord {
schemaVersion: 1;
metric: QualityMetricName;
value: number;
measuredAt: string;
source: string;
}
export interface QualityThreshold {
direction: 'min' | 'max';
value: number;
severity: 'critical' | 'high' | 'medium' | 'low';
}
export const DEFAULT_QUALITY_THRESHOLDS: Record<QualityMetricName, QualityThreshold> = {
coverage: { direction: 'min', value: 80, severity: 'high' },
testsPassing: { direction: 'min', value: 95, severity: 'critical' },
criticalBugs: { direction: 'max', value: 0, severity: 'critical' },
codeSmells: { direction: 'max', value: 20, severity: 'medium' },
securityVulnerabilities: { direction: 'max', value: 0, severity: 'critical' },
technicalDebt: { direction: 'max', value: 5, severity: 'medium' },
duplications: { direction: 'max', value: 5, severity: 'low' },
};
export interface EvaluatedQualityCheck {
name: QualityMetricName;
passed: boolean;
value: number;
threshold: number;
direction: 'min' | 'max';
severity: QualityThreshold['severity'];
}
function evidenceKey(metric: QualityMetricName): string {
return `quality-evidence:${metric}:latest`;
}
function parseMeasuredAt(measuredAt: unknown, now: number, maxAgeMs: number): number {
if (typeof measuredAt !== 'string') {
throw new Error('Measured quality evidence is malformed: measuredAt is required.');
}
const timestamp = Date.parse(measuredAt);
if (!Number.isFinite(timestamp)) {
throw new Error('Measured quality evidence is malformed: measuredAt is invalid.');
}
if (timestamp > now + 60_000) {
throw new Error('Measured quality evidence is malformed: measuredAt is in the future.');
}
if (now - timestamp > maxAgeMs) {
throw new Error('Measured quality evidence is stale. Run the relevant quality analyzers again.');
}
return timestamp;
}
function validateRecord(
record: unknown,
metric: QualityMetricName,
now: number,
maxAgeMs: number,
): asserts record is QualityEvidenceRecord {
if (!record || typeof record !== 'object') {
throw new Error(`No measured quality evidence found for ${metric}.`);
}
const candidate = record as Partial<QualityEvidenceRecord>;
if (
candidate.schemaVersion !== 1
|| candidate.metric !== metric
|| !Number.isFinite(candidate.value)
|| candidate.value! < 0
|| typeof candidate.source !== 'string'
|| candidate.source.trim().length === 0
) {
throw new Error(`Measured quality evidence for ${metric} is malformed or incomplete.`);
}
parseMeasuredAt(candidate.measuredAt, now, maxAgeMs);
}
export async function writeQualityEvidence(
memory: MemoryBackend,
values: Partial<QualityEvidenceValues>,
metadata: { measuredAt: string; source: string },
): Promise<void> {
const now = Date.now();
parseMeasuredAt(metadata.measuredAt, now, Number.POSITIVE_INFINITY);
if (metadata.source.trim().length === 0) {
throw new Error('Quality evidence source is required.');
}
for (const metric of QUALITY_METRICS) {
const value = values[metric];
if (value === undefined) continue;
if (!Number.isFinite(value) || value < 0) {
throw new Error(`Quality evidence value for ${metric} must be a non-negative finite number.`);
}
const record: QualityEvidenceRecord = {
schemaVersion: 1,
metric,
value,
measuredAt: metadata.measuredAt,
source: metadata.source,
};
await memory.set(evidenceKey(metric), record, {
namespace: QUALITY_EVIDENCE_NAMESPACE,
persist: true,
});
}
}
export async function loadQualityEvidence(
memory: MemoryBackend,
options: { now?: number; maxAgeMs?: number } = {},
): Promise<QualityEvidenceValues> {
const now = options.now ?? Date.now();
const maxAgeMs = options.maxAgeMs ?? DEFAULT_QUALITY_EVIDENCE_MAX_AGE_MS;
const values = {} as QualityEvidenceValues;
for (const metric of QUALITY_METRICS) {
const record = await memory.get<QualityEvidenceRecord>(evidenceKey(metric), {
namespace: QUALITY_EVIDENCE_NAMESPACE,
});
validateRecord(record, metric, now, maxAgeMs);
values[metric] = record.value;
}
return values;
}
export function evaluateQualityEvidence(
values: QualityEvidenceValues,
): { passed: boolean; checks: EvaluatedQualityCheck[]; recommendations: string[] } {
const checks = QUALITY_METRICS.map((name) => {
const threshold = DEFAULT_QUALITY_THRESHOLDS[name];
const value = values[name];
return {
name,
value,
threshold: threshold.value,
direction: threshold.direction,
severity: threshold.severity,
passed: threshold.direction === 'min' ? value >= threshold.value : value <= threshold.value,
};
});
return {
passed: checks.every((check) => check.passed),
checks,
recommendations: checks
.filter((check) => !check.passed)
.map((check) => `${check.name} failed its measured ${check.direction} threshold.`),
};
}
@@ -22,6 +22,7 @@ import { TEST_EXECUTION_CONSTANTS, LLM_ANALYSIS_CONSTANTS } from '../../constant
import { toErrorMessage, toError } from '../../../shared/error-utils.js';
import { safeJsonParse } from '../../../shared/safe-json.js';
import { secureRandom, secureRandomInt } from '../../../shared/utils/crypto-random.js';
import { writeQualityEvidence } from '../../quality-assessment/quality-evidence.js';
// ============================================================================
// Configuration
@@ -1296,6 +1297,14 @@ Provide:
statement: result.coverage.statement ?? 0,
files: result.fileCoverages?.length ?? 0,
}, { persist: true });
if (Number.isFinite(result.coverage.line)) {
await writeQualityEvidence(this.memory, {
coverage: result.coverage.line,
}, {
measuredAt: new Date().toISOString(),
source: 'test-execution coverage',
});
}
// Store per-file coverage via memory.set() (not storeVector) so that
// quality-analyzer's getStoredCoverage() can read it with memory.get()
@@ -85,17 +85,22 @@ export function getGeneratedTestSyntaxIssues(
): TestQualityIssue[] {
if (!isJavaScriptFamily(sourceFilePath)) return [];
const sourceFile = parseGeneratedTest(testCode, sourceFilePath);
const diagnostics = (
sourceFile as ts.SourceFile & { parseDiagnostics?: readonly ts.Diagnostic[] }
).parseDiagnostics ?? [];
const diagnostics = ts.transpileModule(testCode, {
fileName: sourceFilePath,
reportDiagnostics: true,
compilerOptions: {
jsx: ts.JsxEmit.Preserve,
module: ts.ModuleKind.ESNext,
target: ts.ScriptTarget.Latest,
},
}).diagnostics ?? [];
return diagnostics.map((diagnostic) => {
const position = sourceFile.getLineAndCharacterOfPosition(diagnostic.start ?? 0);
const position = diagnostic.file?.getLineAndCharacterOfPosition(diagnostic.start ?? 0);
return {
type: 'syntax-error' as const,
severity: 'error' as const,
line: position.line + 1,
line: position ? position.line + 1 : undefined,
description: `Generated test has invalid syntax: ${ts.flattenDiagnosticMessageText(diagnostic.messageText, ' ')}`,
suggestion: 'Repair the generated test syntax before accepting or executing it.',
};
+1
View File
@@ -72,6 +72,7 @@ export class ModularInitOrchestrator {
noGovernance: options.noGovernance,
noMcp: options.noMcp,
noClaude: options.noClaude,
noStatusLine: options.noStatusLine,
memoryBackend: options.memoryBackend,
},
config: {},
+3 -1
View File
@@ -119,7 +119,9 @@ export class HooksPhase extends BasePhase<HooksResult> {
// - statusLine / includeCoAuthoredBy preserved when user-set (#362 follow-up)
// - AQE-owned sections deep-merged so user additions survive
const v3Sections = generateV3SettingsSections(config, projectRoot);
applyV3Sections(settings, v3Sections);
applyV3Sections(settings, v3Sections, {
statusLine: !context.options.noStatusLine,
});
// Enable MCP servers (deduplicate, replace old 'aqe' with 'agentic-qe')
let existingMcp = (settings.enabledMcpjsonServers as string[]) || [];
+3 -1
View File
@@ -151,7 +151,9 @@ export interface InitOptions {
* OpenCode-only install). Opt-in via `--no-claude`; default install is
* unchanged. Pairs naturally with `--no-database`.
*/
noClaude?: boolean;
noClaude?: boolean;
/** Skip AQE's Claude Code status line and remove a previously AQE-owned one. */
noStatusLine?: boolean;
/** @deprecated Use default behavior instead — MCP is now enabled by default */
withMcp?: boolean;
/**
+19 -12
View File
@@ -198,18 +198,27 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
*
* Mutates and returns `settings`.
*/
export function applyV3Sections(
settings: Record<string, unknown>,
sections: Record<string, unknown>,
): Record<string, unknown> {
export function applyV3Sections(
settings: Record<string, unknown>,
sections: Record<string, unknown>,
options: { statusLine?: boolean } = {},
): Record<string, unknown> {
for (const [key, value] of Object.entries(sections)) {
if (key === '_aqePermissions') {
const existingPerms = (settings.permissions as { allow?: string[]; deny?: string[] }) || {};
const existingAllow = existingPerms.allow || [];
const merged = [...new Set([...existingAllow, ...(value as string[])])];
settings.permissions = { ...existingPerms, allow: merged };
} else if (key === 'statusLine') {
// Preserve a user's (or another tool's) custom status line.
} else if (key === 'statusLine') {
if (options.statusLine === false) {
// Opting out removes only AQE-owned configuration. A status line from
// the user or another tool remains outside AQE's ownership boundary.
if (isAqeStatusLine(settings.statusLine)) {
delete settings.statusLine;
}
continue;
}
// Preserve a user's (or another tool's) custom status line.
if (settings.statusLine === undefined || isAqeStatusLine(settings.statusLine)) {
settings.statusLine = value;
}
@@ -238,12 +247,10 @@ export function generateV3SettingsSections(config: AQEInitConfig, projectRoot?:
initialized: new Date().toISOString(),
hooksConfigured: true,
},
statusLine: {
type: 'command',
command: 'sh -c \'node "${CLAUDE_PROJECT_DIR:-.}/.claude/helpers/statusline-v3.cjs" 2>/dev/null || echo "▊ Agentic QE v3"\'',
refreshMs: 5000,
enabled: true,
},
statusLine: {
type: 'command',
command: 'sh -c \'node "${CLAUDE_PROJECT_DIR:-.}/.claude/helpers/statusline-v3.cjs" 2>/dev/null || echo "▊ Agentic QE v3"\'',
},
// permissions are union-merged in 07-hooks.ts — not set here to avoid overwriting user entries (#362)
_aqePermissions: [
'Bash(npx agentic-qe:*)',
@@ -10,6 +10,8 @@
* @module integrations/embeddings/index/HNSWIndex
*/
import { createRequire } from 'node:module';
import type {
IEmbedding,
IHNSWConfig,
@@ -17,6 +19,8 @@ import type {
ISearchOptions,
} from '../base/types.js';
const require = createRequire(import.meta.url);
// hnswlib-node is an OPTIONAL dependency (issue #439, ADR-090 amendment).
// Top-level static import would crash module load on platforms where the
// native binary failed to compile (e.g. Windows without VS Build Tools).
+30 -13
View File
@@ -346,11 +346,28 @@ export async function computeBatchEmbeddings(
const fullConfig = { ...DEFAULT_EMBEDDING_CONFIG, ...config };
// Check which texts need computation
const uncachedTexts: string[] = [];
const uncachedIndices: number[] = [];
const results: (number[] | null)[] = new Array(texts.length).fill(null);
// Init BEFORE cache lookup so the cache key namespace is stable.
const uncachedTexts: string[] = [];
const uncachedIndices: number[] = [];
const results: (number[] | null)[] = new Array(texts.length).fill(null);
// Match computeRealEmbedding(): non-semantic inputs are represented by a
// zero vector and must never reach the model or its cache (#585).
const semanticIndices: number[] = [];
for (let i = 0; i < texts.length; i++) {
if (isNonSemanticText(texts[i])) {
results[i] = new Array(getEmbeddingDimension()).fill(0);
} else {
semanticIndices.push(i);
}
}
// Preserve the single-item API's no-initialization behavior when every
// input is non-semantic.
if (semanticIndices.length === 0) {
return results as number[][];
}
// Init BEFORE cache lookup so the cache key namespace is stable.
if (!embeddingModel) {
await initializeModel(config);
}
@@ -358,9 +375,9 @@ export async function computeBatchEmbeddings(
throw new Error('Embedding model failed to initialize');
}
if (fullConfig.enableCache) {
for (let i = 0; i < texts.length; i++) {
const cached = embeddingCache.get(cacheKey(texts[i]));
if (fullConfig.enableCache) {
for (const i of semanticIndices) {
const cached = embeddingCache.get(cacheKey(texts[i]));
if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) {
results[i] = cached.embedding;
} else {
@@ -368,11 +385,11 @@ export async function computeBatchEmbeddings(
uncachedIndices.push(i);
}
}
} else {
uncachedTexts.push(...texts);
for (let i = 0; i < texts.length; i++) {
uncachedIndices.push(i);
}
} else {
for (const i of semanticIndices) {
uncachedTexts.push(texts[i]);
uncachedIndices.push(i);
}
}
// Compute uncached embeddings
+81 -21
View File
@@ -9,6 +9,12 @@
import { MCPToolBase, MCPToolConfig, MCPToolContext, MCPToolSchema } from '../base';
import { ToolResult } from '../../types';
import { toErrorMessage } from '../../../shared/error-utils.js';
import { getMemoryBackend } from '../base.js';
import {
DEFAULT_QUALITY_THRESHOLDS,
loadQualityEvidence,
writeQualityEvidence,
} from '../../../domains/quality-assessment/quality-evidence.js';
// ============================================================================
// Types
@@ -20,6 +26,7 @@ export interface QualityEvaluateParams {
thresholds?: GateThresholds;
includeAdvice?: boolean;
riskTolerance?: 'low' | 'medium' | 'high';
evidence?: { measuredAt: string; source: string };
[key: string]: unknown;
}
@@ -41,6 +48,7 @@ export interface GateThresholds {
codeSmells?: { max: number };
securityVulnerabilities?: { max: number };
technicalDebt?: { max: number };
duplications?: { max: number };
}
export interface QualityEvaluateResult {
@@ -97,7 +105,7 @@ export class QualityEvaluateTool extends MCPToolBase<QualityEvaluateParams, Qual
context: MCPToolContext
): Promise<ToolResult<QualityEvaluateResult>> {
const {
metrics = getDefaultMetrics(),
metrics: suppliedMetrics,
gateName = 'default',
thresholds = getDefaultThresholds(),
includeAdvice = true,
@@ -105,6 +113,36 @@ export class QualityEvaluateTool extends MCPToolBase<QualityEvaluateParams, Qual
} = params;
try {
const memory = await getMemoryBackend(context);
if (suppliedMetrics && !params.evidence) {
return {
success: false,
error: 'Timestamped evidence provenance is required when supplying quality metrics.',
};
}
if (suppliedMetrics && params.evidence) {
await writeQualityEvidence(memory, suppliedMetrics, params.evidence);
}
const metrics = suppliedMetrics ?? await loadQualityEvidence(memory);
const configuredMetrics = [
['coverage', thresholds.coverage],
['testsPassing', thresholds.testsPassing],
['criticalBugs', thresholds.criticalBugs],
['codeSmells', thresholds.codeSmells],
['securityVulnerabilities', thresholds.securityVulnerabilities],
['technicalDebt', thresholds.technicalDebt],
['duplications', thresholds.duplications],
] as const;
const missingMetrics = configuredMetrics
.filter(([name, threshold]) => threshold !== undefined && metrics[name] === undefined)
.map(([name]) => name);
if (missingMetrics.length > 0) {
return {
success: false,
error: `Measured quality evidence missing configured metrics: ${missingMetrics.join(', ')}.`,
};
}
this.emitStream(context, {
status: 'evaluating',
message: `Evaluating quality gate: ${gateName}`,
@@ -187,12 +225,38 @@ export class QualityEvaluateTool extends MCPToolBase<QualityEvaluateParams, Qual
});
}
if (thresholds.technicalDebt && metrics.technicalDebt !== undefined) {
checks.push({
name: 'Technical Debt',
passed: metrics.technicalDebt <= thresholds.technicalDebt.max,
value: metrics.technicalDebt,
threshold: thresholds.technicalDebt.max,
severity: 'medium',
message: metrics.technicalDebt <= thresholds.technicalDebt.max
? `${metrics.technicalDebt}h technical debt within threshold`
: `${metrics.technicalDebt}h technical debt exceeds threshold of ${thresholds.technicalDebt.max}h`,
});
}
if (thresholds.duplications && metrics.duplications !== undefined) {
checks.push({
name: 'Duplications',
passed: metrics.duplications <= thresholds.duplications.max,
value: metrics.duplications,
threshold: thresholds.duplications.max,
severity: 'low',
message: metrics.duplications <= thresholds.duplications.max
? `${metrics.duplications}% duplication within threshold`
: `${metrics.duplications}% duplication exceeds threshold of ${thresholds.duplications.max}%`,
});
}
// Calculate overall score and grade
const passedChecks = checks.filter(c => c.passed).length;
const totalChecks = checks.length;
const score = totalChecks > 0 ? Math.round((passedChecks / totalChecks) * 100) : 100;
const grade = calculateGrade(score, checks);
const passed = checks.every(c => c.passed || c.severity !== 'critical');
const passed = checks.every(c => c.passed);
// Generate deployment advice
const deploymentAdvice: DeploymentAdvice | undefined = includeAdvice
@@ -269,6 +333,14 @@ const QUALITY_EVALUATE_SCHEMA: MCPToolSchema = {
enum: ['low', 'medium', 'high'],
default: 'medium',
},
evidence: {
type: 'object',
description: 'Provenance required to persist supplied metrics as canonical measured evidence',
properties: {
measuredAt: { type: 'string', description: 'ISO-8601 measurement timestamp' },
source: { type: 'string', description: 'Scanner or analyzer that produced the metrics' },
},
},
},
};
@@ -276,27 +348,15 @@ const QUALITY_EVALUATE_SCHEMA: MCPToolSchema = {
// Helper Functions
// ============================================================================
function getDefaultMetrics(): QualityMetrics {
return {
coverage: 80,
testsPassing: 95,
criticalBugs: 0,
codeSmells: 15,
securityVulnerabilities: 0,
technicalDebt: 4,
duplications: 3,
complexity: 8,
};
}
function getDefaultThresholds(): GateThresholds {
return {
coverage: { min: 80 },
testsPassing: { min: 95 },
criticalBugs: { max: 0 },
securityVulnerabilities: { max: 0 },
codeSmells: { max: 50 },
technicalDebt: { max: 8 },
coverage: { min: DEFAULT_QUALITY_THRESHOLDS.coverage.value },
testsPassing: { min: DEFAULT_QUALITY_THRESHOLDS.testsPassing.value },
criticalBugs: { max: DEFAULT_QUALITY_THRESHOLDS.criticalBugs.value },
securityVulnerabilities: { max: DEFAULT_QUALITY_THRESHOLDS.securityVulnerabilities.value },
codeSmells: { max: DEFAULT_QUALITY_THRESHOLDS.codeSmells.value },
technicalDebt: { max: DEFAULT_QUALITY_THRESHOLDS.technicalDebt.value },
duplications: { max: DEFAULT_QUALITY_THRESHOLDS.duplications.value },
};
}
+9 -1
View File
@@ -9,7 +9,7 @@
*/
import * as fs from 'fs';
import { MCPToolBase, MCPToolConfig, MCPToolContext, MCPToolSchema } from '../base';
import { MCPToolBase, MCPToolConfig, MCPToolContext, MCPToolSchema, getMemoryBackend } from '../base';
import { ToolResult } from '../../types';
import { toErrorMessage } from '../../../shared/error-utils.js';
import { safeJsonParse } from '../../../shared/safe-json.js';
@@ -17,6 +17,7 @@ import {
ALL_SECURITY_PATTERNS,
SECRET_PATTERNS,
} from '../../../domains/security-compliance/services/scanners/security-patterns.js';
import { writeQualityEvidence } from '../../../domains/quality-assessment/quality-evidence.js';
// ============================================================================
// Types
@@ -231,6 +232,13 @@ export class SecurityScanTool extends MCPToolBase<SecurityScanParams, SecuritySc
: severityOrder.length;
const passed = worstSeverity > failThreshold;
await writeQualityEvidence(await getMemoryBackend(context), {
securityVulnerabilities: vulnerabilities.length,
}, {
measuredAt: new Date().toISOString(),
source: 'qe/security/scan',
});
this.emitStream(context, {
status: 'complete',
message: `Scan complete: ${vulnerabilities.length} vulnerabilities found in ${files.length} files`,
@@ -13,7 +13,7 @@
* in production, written by sentence-transformers/all-MiniLM-L6-v2-style
* models) and verifies:
*
* 1. Self-query returns id=self with score 1.0 (exact-match recall)
* 1. Self-query returns an exact vector match with score 1.0
* 2. Top-10 recall vs brute-force ground truth = 100%
* 3. Repeated for several random query vectors to rule out one-off luck
* 4. Vectors persisted in memory.db deserialize correctly into the backend
@@ -183,7 +183,7 @@ describe('NativeHnswBackend — real qe-kernel fixture (#399)', () => {
);
it.runIf(fixture !== null && fixture.length >= 100)(
'should hit recall@10 >= 0.9 and top-1 == self on real qe-kernel embeddings',
'should hit recall@10 >= 0.9 with an exact top match on real qe-kernel embeddings',
() => {
const pool = fixture!;
const backend = new NativeHnswBackend({
@@ -209,13 +209,12 @@ describe('NativeHnswBackend — real qe-kernel fixture (#399)', () => {
// them based on entry-point luck. 90-100% recall@10 is the
// standard approximate-HNSW guarantee.
//
// The TOP-1 self-match assertion is the strict bug-detector — it's
// mathematically impossible to fail unless HNSW is fundamentally
// broken (which is exactly what @ruvector/router 0.1.28 was: it
// returned random non-neighbors with recall@10 = 0% to 10%). Anything
// above 0.5 recall@10 here would already be a clear pass. We hold
// the bar at 0.9 to catch tuning regressions while accepting the
// legitimate plateau-region behavior of real embeddings.
// The TOP-1 exact-vector assertion is the strict bug-detector. Several
// real fixture rows can contain byte-identical embeddings, so requiring
// the queried row's numeric id would make the result depend on arbitrary
// tie-breaking. @ruvector/router 0.1.28 returned random non-neighbors
// with recall@10 = 0% to 10%; requiring an exact top result still catches
// that failure while accepting indistinguishable duplicate vectors.
const RECALL_FLOOR = 0.9;
for (const queryIdx of [
@@ -234,7 +233,8 @@ describe('NativeHnswBackend — real qe-kernel fixture (#399)', () => {
const recallAt10 =
[...groundTruthIds].filter((id) => hitIds.has(id)).length / groundTruthIds.size;
expect(results[0].id).toBe(queryIdx); // self MUST be top-1 (exact-match guarantee)
expect(pool[results[0].id].vector).toEqual(queryVector);
expect(results[0].score).toBeGreaterThanOrEqual(0.999);
expect(recallAt10).toBeGreaterThanOrEqual(RECALL_FLOOR);
}
@@ -0,0 +1,31 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
const originalRequire = Object.getOwnPropertyDescriptor(globalThis, 'require');
afterEach(() => {
if (originalRequire) {
Object.defineProperty(globalThis, 'require', originalRequire);
} else {
Reflect.deleteProperty(globalThis, 'require');
}
vi.resetModules();
});
describe('HNSWIndex ESM compatibility', () => {
it('should initialize when CommonJS require is unavailable globally', async () => {
// Arrange: reproduce the published unbundled ESM environment from #586.
Reflect.deleteProperty(globalThis, 'require');
vi.resetModules();
// Act
const { HNSWEmbeddingIndex } = await import(
'../../../src/integrations/embeddings/index/HNSWIndex.js'
);
const index = new HNSWEmbeddingIndex({ dimension: 4 });
index.initializeIndex('test');
// Assert
expect(index.isInitialized('test')).toBe(true);
index.clearAll();
});
});
+18
View File
@@ -7,6 +7,9 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { InitOrchestrator, type InitOrchestratorOptions } from '../../../src/init/init-wizard';
import * as fs from 'fs';
import * as path from 'path';
import { Command } from 'commander';
import { InitHandler } from '../../../src/cli/handlers/init-handler';
import type { CLIContext } from '../../../src/cli/handlers/interfaces';
// Mock fs module
vi.mock('fs', async () => {
@@ -89,6 +92,21 @@ describe('Init Command', () => {
});
describe('Init Options Validation', () => {
it('should parse --no-statusline as an explicit opt-out', async () => {
const program = new Command();
const handler = new InitHandler(async () => undefined as never);
const context = {} as CLIContext;
const execute = vi.spyOn(handler, 'execute').mockResolvedValue(undefined);
handler.register(program, context);
await program.parseAsync(['node', 'aqe', 'init', '--no-statusline']);
expect(execute).toHaveBeenCalledWith(
expect.objectContaining({ statusline: false }),
context
);
});
it('should accept valid domain list', () => {
const domains = 'test-generation,coverage-analysis,quality-assessment';
const domainList = domains.split(',');
+35 -17
View File
@@ -1,10 +1,21 @@
import { describe, expect, it } from 'vitest';
import {
evaluateMeasuredQualityEvidence,
getMeasuredQualityExitCode,
loadQualityEvidence,
} from '../../../src/cli/commands/quality.js';
describe('quality command evidence loading', () => {
const passing = {
coverage: 90,
testsPassing: 100,
criticalBugs: 0,
codeSmells: 10,
securityVulnerabilities: 0,
technicalDebt: 2,
duplications: 3,
};
it('refuses to invent zero metrics when AgentDB has no measured evidence', async () => {
const memory = { get: async () => undefined };
await expect(loadQualityEvidence(memory as never)).rejects.toThrow(/no measured quality evidence/i);
@@ -12,28 +23,35 @@ describe('quality command evidence loading', () => {
it('loads measured coverage and test pass rate from canonical AgentDB keys', async () => {
const memory = {
get: async (key: string) => key === 'coverage:latest'
? { line: 81.25 }
: { passed: 38, failed: 2, skipped: 0 },
get: async (key: string) => {
const metric = key.split(':')[1] as keyof typeof passing;
return {
schemaVersion: 1,
metric,
value: passing[metric],
measuredAt: new Date().toISOString(),
source: 'test',
};
},
};
const evidence = await loadQualityEvidence(memory as never);
expect(evidence.coverage).toBe(81.25);
expect(evidence.testsPassing).toBe(95);
expect(evidence).toEqual(passing);
});
it('reports only measured checks and does not invent security or debt zeros', () => {
const result = evaluateMeasuredQualityEvidence({
coverage: 81.25,
testsPassing: 95,
});
it('reports all configured checks from measured evidence', () => {
const result = evaluateMeasuredQualityEvidence(passing);
expect(result.passed).toBe(true);
expect(result.checks.map(check => check.name)).toEqual(['coverage', 'testsPassing']);
expect(result.checks).not.toEqual(
expect.arrayContaining([
expect.objectContaining({ name: 'securityVulnerabilities' }),
expect.objectContaining({ name: 'technicalDebt' }),
]),
);
expect(result.checks.map(check => check.name)).toEqual(Object.keys(passing));
});
it('preserves pass, fail, and near-threshold warning exit codes', () => {
const failed = evaluateMeasuredQualityEvidence({ ...passing, coverage: 79.9 });
const warning = evaluateMeasuredQualityEvidence({ ...passing, coverage: 82 });
const passed = evaluateMeasuredQualityEvidence(passing);
expect(getMeasuredQualityExitCode(failed)).toBe(1);
expect(getMeasuredQualityExitCode(warning)).toBe(2);
expect(getMeasuredQualityExitCode(passed)).toBe(0);
});
});
+6
View File
@@ -15,6 +15,7 @@ describe('test command', () => {
const context = {
kernel: {
getDomainAPIAsync: vi.fn().mockResolvedValue({ runTests }),
memory: { set: vi.fn().mockResolvedValue(undefined) },
},
} as unknown as CLIContext;
const command = createTestCommand(
@@ -34,5 +35,10 @@ describe('test command', () => {
expect(runTests).toHaveBeenCalledWith(expect.objectContaining({
framework: 'node',
}));
expect(context.kernel.memory.set).toHaveBeenCalledWith(
'test-run:latest',
expect.objectContaining({ passed: 1, failed: 0, skipped: 0 }),
{ namespace: 'test-execution', persist: true },
);
});
});
@@ -0,0 +1,98 @@
import { describe, expect, it, vi } from 'vitest';
import {
DEFAULT_QUALITY_THRESHOLDS,
QUALITY_EVIDENCE_NAMESPACE,
evaluateQualityEvidence,
loadQualityEvidence,
writeQualityEvidence,
type QualityEvidenceValues,
} from '../../../../src/domains/quality-assessment/quality-evidence.js';
function createMemory() {
const values = new Map<string, unknown>();
return {
values,
get: vi.fn(async (key: string, options?: { namespace?: string }) =>
values.get(`${options?.namespace ?? 'default'}:${key}`)),
set: vi.fn(async (key: string, value: unknown, options?: { namespace?: string }) => {
values.set(`${options?.namespace ?? 'default'}:${key}`, value);
}),
};
}
const passingValues: QualityEvidenceValues = {
coverage: 90,
testsPassing: 100,
criticalBugs: 0,
codeSmells: 10,
securityVulnerabilities: 0,
technicalDebt: 2,
duplications: 3,
};
describe('quality evidence contract', () => {
it('should round-trip timestamped evidence through the canonical namespace', async () => {
const memory = createMemory();
const measuredAt = '2026-08-03T08:00:00.000Z';
await writeQualityEvidence(memory as never, passingValues, {
measuredAt,
source: 'quality-analyzer',
});
const loaded = await loadQualityEvidence(memory as never, {
now: Date.parse(measuredAt) + 1_000,
});
expect(loaded).toEqual(passingValues);
expect(memory.set).toHaveBeenCalledWith(
'quality-evidence:criticalBugs:latest',
expect.objectContaining({ measuredAt, source: 'quality-analyzer' }),
expect.objectContaining({ namespace: QUALITY_EVIDENCE_NAMESPACE, persist: true }),
);
});
it('should fail closed when configured evidence is missing', async () => {
await expect(loadQualityEvidence(createMemory() as never)).rejects.toThrow(
/no measured quality evidence/i,
);
});
it('should fail closed when evidence is malformed', async () => {
const memory = createMemory();
memory.values.set(`${QUALITY_EVIDENCE_NAMESPACE}:quality-evidence:coverage:latest`, {
schemaVersion: 1,
metric: 'coverage',
value: Number.NaN,
measuredAt: new Date().toISOString(),
source: 'coverage-analysis',
});
await expect(loadQualityEvidence(memory as never)).rejects.toThrow(/malformed/i);
});
it('should fail closed when evidence is stale', async () => {
const memory = createMemory();
await writeQualityEvidence(memory as never, passingValues, {
measuredAt: '2026-08-01T00:00:00.000Z',
source: 'quality-analyzer',
});
await expect(loadQualityEvidence(memory as never, {
now: Date.parse('2026-08-03T00:00:00.000Z'),
maxAgeMs: 60_000,
})).rejects.toThrow(/stale/i);
});
it('should evaluate every metric against the shared thresholds', () => {
const result = evaluateQualityEvidence({ ...passingValues, technicalDebt: 6 });
expect(result.passed).toBe(false);
expect(result.checks).toHaveLength(7);
expect(result.checks.find((check) => check.name === 'technicalDebt')).toEqual(
expect.objectContaining({
passed: false,
threshold: DEFAULT_QUALITY_THRESHOLDS.technicalDebt.value,
}),
);
});
});
@@ -24,6 +24,20 @@ describe('TestExecutorService runner command', () => {
expect(command.args).not.toContain('--coverage');
});
it('passes a Node worker shard to one native test-runner process', () => {
const executor = new TestExecutorService({ memory: {} as never });
const command = (
executor as unknown as {
buildTestCommand(files: string[], framework: string): { command: string; args: string[] };
}
).buildTestCommand(['one.test.cjs', 'two.test.cjs'], 'node');
expect(command).toEqual({
command: process.execPath,
args: ['--test', 'one.test.cjs', 'two.test.cjs'],
});
});
it('runs each worker shard as one bounded runner batch', async () => {
const executor = new TestExecutorService({ memory: {} as never });
const internals = executor as unknown as {
+13 -3
View File
@@ -82,7 +82,7 @@ describe('ModularInitOrchestrator', () => {
expect(orchestrator.getPhases()).toHaveLength(0);
});
it('should create context with provided options', () => {
it('should create context with provided options', () => {
const orchestrator = new ModularInitOrchestrator({
projectRoot: '/tmp/test',
autoMode: true,
@@ -95,8 +95,18 @@ describe('ModularInitOrchestrator', () => {
expect(ctx.projectRoot).toBe('/tmp/test');
expect(ctx.options.autoMode).toBe(true);
expect(ctx.options.upgrade).toBe(true);
expect(ctx.options.minimal).toBe(true);
});
expect(ctx.options.minimal).toBe(true);
});
it('should propagate the status line opt-out to init phases', () => {
const orchestrator = new ModularInitOrchestrator({
projectRoot: '/tmp/test',
noStatusLine: true,
customPhases: [],
});
expect(orchestrator.getContext().options.noStatusLine).toBe(true);
});
it('should initialize context with empty results map', () => {
const orchestrator = new ModularInitOrchestrator({
+39 -9
View File
@@ -407,10 +407,12 @@ describe('Settings Merge Utilities', () => {
agents: { maxConcurrent: 10 },
} as any;
const sections = generateV3SettingsSections(config);
expect(sections.statusLine).toBeDefined();
expect((sections.statusLine as any).enabled).toBe(true);
const sections = generateV3SettingsSections(config);
expect(sections.statusLine).toBeDefined();
expect(sections.statusLine).toEqual(expect.objectContaining({ type: 'command' }));
expect(sections.statusLine).not.toHaveProperty('enabled');
expect(sections.statusLine).not.toHaveProperty('refreshMs');
expect(sections._aqePermissions).toBeDefined();
expect(sections._aqePermissions).toContain('mcp__agentic-qe__*');
@@ -454,17 +456,45 @@ describe('Settings Merge Utilities', () => {
expect(settings.statusLine).toEqual(userStatusLine);
});
it('should update AQE-owned statusLine and set it when absent', () => {
it('should update AQE-owned statusLine and set it when absent', () => {
const fresh: Record<string, unknown> = {};
applyV3Sections(fresh, generateV3SettingsSections(config));
expect((fresh.statusLine as any).command).toContain('statusline-v3.cjs');
const stale: Record<string, unknown> = {
statusLine: { type: 'command', command: 'node old/statusline-v3.cjs', enabled: false },
};
applyV3Sections(stale, generateV3SettingsSections(config));
expect((stale.statusLine as any).enabled).toBe(true); // refreshed
});
};
applyV3Sections(stale, generateV3SettingsSections(config));
expect((stale.statusLine as any).command).toContain('statusline-v3.cjs');
expect(stale.statusLine).not.toHaveProperty('enabled');
});
it('should omit a statusLine when explicitly disabled', () => {
const settings: Record<string, unknown> = {};
applyV3Sections(settings, generateV3SettingsSections(config), { statusLine: false });
expect(settings.statusLine).toBeUndefined();
});
it('should remove an AQE-owned statusLine when explicitly disabled', () => {
const settings: Record<string, unknown> = {
statusLine: { type: 'command', command: 'node .claude/helpers/statusline-v3.cjs' },
};
applyV3Sections(settings, generateV3SettingsSections(config), { statusLine: false });
expect(settings.statusLine).toBeUndefined();
});
it('should preserve a custom statusLine when AQE statusLine is disabled', () => {
const custom = { type: 'command', command: 'my-prompt --powerline' };
const settings: Record<string, unknown> = { statusLine: custom };
applyV3Sections(settings, generateV3SettingsSections(config), { statusLine: false });
expect(settings.statusLine).toEqual(custom);
});
it('should respect an explicit includeCoAuthoredBy=false', () => {
const settings: Record<string, unknown> = { includeCoAuthoredBy: false };
@@ -15,6 +15,7 @@ import { describe, it, expect, beforeEach, afterEach, afterAll, vi } from 'vites
import { setRuVectorFeatureFlags, resetRuVectorFeatureFlags } from '../../../src/integrations/ruvector/feature-flags.js';
import { clearEmbeddingCache, resetInitialization } from '../../../src/learning/real-embeddings';
import { _resetWitnessChainForTests } from '../../../src/audit/witness-chain';
import { resetUnifiedPersistence } from '../../../src/kernel/unified-persistence';
// Ensure these tests exercise the in-memory PatternStore, not the RVF variant
beforeEach(() => { setRuVectorFeatureFlags({ useRVFPatternStore: false }); });
@@ -45,11 +46,17 @@ import { createMockMemoryBackend, createMockEventBus } from './_aqe-engine-test-
// ============================================================================
describe('AQELearningEngine', () => {
let memory: MemoryBackend;
let eventBus: EventBus;
let engine: AQELearningEngine;
beforeEach(async () => {
const originalMemoryBackend = process.env.AQE_MEMORY_BACKEND;
let memory: MemoryBackend;
let eventBus: EventBus;
let engine: AQELearningEngine;
beforeEach(async () => {
// QEReasoningBank owns a SQLitePatternStore in addition to the injected
// MemoryBackend. Keep this unit suite database-free and reset that shared
// singleton so patterns cannot accumulate across test cases (#588).
process.env.AQE_MEMORY_BACKEND = 'memory';
resetUnifiedPersistence();
memory = createMockMemoryBackend();
eventBus = createMockEventBus();
engine = createAQELearningEngine(
@@ -63,6 +70,12 @@ describe('AQELearningEngine', () => {
afterEach(async () => {
vi.clearAllMocks();
await engine.dispose();
resetUnifiedPersistence();
if (originalMemoryBackend === undefined) {
delete process.env.AQE_MEMORY_BACKEND;
} else {
process.env.AQE_MEMORY_BACKEND = originalMemoryBackend;
}
});
describe('Initialization', () => {
@@ -41,14 +41,20 @@ function fakeEmbedding(text: string, dim = 384): number[] {
return v;
}
async function startServer(): Promise<{ url: string; close: () => Promise<void>; calls: number }> {
const handle = { url: '', calls: 0, close: async () => {} };
async function startServer(): Promise<{
url: string;
close: () => Promise<void>;
calls: number;
inputs: string[][];
}> {
const handle = { url: '', calls: 0, inputs: [] as string[][], close: async () => {} };
const server = http.createServer((req, res) => {
handle.calls++;
let raw = '';
req.on('data', (c) => (raw += c));
req.on('end', () => {
const { input } = JSON.parse(raw) as { input: string[] };
handle.inputs.push(input);
const data = input.map((t, i) => ({
index: i,
embedding: fakeEmbedding(t),
@@ -107,6 +113,28 @@ describe('real-embeddings.ts — ADR-097 endpoint branch', () => {
}
});
it('returns zero vectors for non-semantic items in a mixed batch', async () => {
const nonSemantic = '{"metrics":{"tasksReceived":1}}';
const vecs = await computeBatchEmbeddings(['semantic text', nonSemantic], {
endpoint: server.url,
enableCache: false,
});
expect(vecs[1].every((value) => value === 0)).toBe(true);
expect(server.inputs.at(-1)).toEqual(['semantic text']);
});
it('does not initialize the embedder for an entirely non-semantic batch', async () => {
const vecs = await computeBatchEmbeddings(
['{"metrics":{"tasksReceived":1}}', '1234567890-1234567890-1234567890'],
{ endpoint: server.url, enableCache: false }
);
expect(vecs).toHaveLength(2);
expect(server.calls).toBe(0);
});
it('returns unit-length vectors', async () => {
const vec = await computeRealEmbedding('unit length check', {
endpoint: server.url,
+35 -2
View File
@@ -279,6 +279,16 @@ describe('CoverageGapsTool', () => {
describe('QualityEvaluateTool', () => {
let tool: QualityEvaluateTool;
const measuredMetrics = {
coverage: 90,
testsPassing: 100,
criticalBugs: 0,
codeSmells: 10,
securityVulnerabilities: 0,
technicalDebt: 2,
duplications: 3,
};
const evidence = { measuredAt: new Date().toISOString(), source: 'unit-test analyzer' };
beforeEach(() => {
tool = new QualityEvaluateTool();
@@ -295,17 +305,40 @@ describe('QualityEvaluateTool', () => {
it('should evaluate quality gates', async () => {
const result = await tool.invoke({
gateName: 'default',
metrics: measuredMetrics,
evidence,
});
expect(result.success).toBe(true);
expect(result.data?.passed).toBeDefined();
expect(result.data?.checks).toBeDefined();
expect(result.data?.checks).toHaveLength(7);
expect(result.data?.checks).toEqual(expect.arrayContaining([
expect.objectContaining({ name: 'Code Smells', threshold: 20 }),
expect.objectContaining({ name: 'Technical Debt', threshold: 5 }),
expect.objectContaining({ name: 'Duplications', threshold: 5 }),
]));
});
it('should fail closed when no metrics or stored evidence are available', async () => {
const result = await tool.invoke({ gateName: 'default' });
expect(result.success).toBe(false);
expect(result.error).toMatch(/no measured quality evidence/i);
});
it('should reject supplied metrics without timestamped provenance', async () => {
const result = await tool.invoke({ metrics: measuredMetrics });
expect(result.success).toBe(false);
expect(result.error).toMatch(/timestamped evidence provenance is required/i);
});
it('should include deployment advice', async () => {
const result = await tool.invoke({
gateName: 'production',
includeDeploymentAdvice: true,
metrics: measuredMetrics,
evidence,
includeAdvice: true,
});
expect(result.success).toBe(true);