UNPKG

claude-flow

Version:

Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration

1,131 lines (1,130 loc) 185 kB
/** * Hooks MCP Tools * Provides intelligent hooks functionality via MCP protocol */ import { mkdirSync, writeFileSync, existsSync, readFileSync, statSync, unlinkSync, readdirSync } from 'fs'; import { dirname, join, resolve } from 'path'; import { getProjectCwd } from './types.js'; import { validateIdentifier, validateText, validatePath } from './validate-input.js'; import { checkCommandLoop, recordCommandOutcome } from './tool-loop-guardrail.js'; // Real vector search functions - lazy loaded to avoid circular imports let searchEntriesFn = null; /** * Strip extended-thinking blocks from text before it enters a learning * trajectory (hermes-agent think_scrubber pattern). Claude models with extended * thinking emit <thinking>/<think>/<reasoning> blocks; if those land in a * trajectory's action/result text, the DISTILL step embeds reasoning-token * content that does not generalize, contaminating pattern confidence. Boundary- * gated: only strips well-formed paired tags, leaving prose that merely mentions * the tag names untouched. */ export function scrubReasoningBlocks(text) { if (typeof text !== 'string' || text.indexOf('<') === -1) return text; return text .replace(/<think>[\s\S]*?<\/think>/gi, '') .replace(/<thinking>[\s\S]*?<\/thinking>/gi, '') .replace(/<reasoning>[\s\S]*?<\/reasoning>/gi, '') .replace(/<thought>[\s\S]*?<\/thought>/gi, '') .replace(/<REASONING_SCRATCHPAD>[\s\S]*?<\/REASONING_SCRATCHPAD>/gi, '') .trim(); } async function getRealSearchFunction() { if (!searchEntriesFn) { try { const { searchEntries } = await import('../memory/memory-initializer.js'); searchEntriesFn = searchEntries; } catch { searchEntriesFn = null; } } return searchEntriesFn; } // Real store function - lazy loaded let storeEntryFn = null; async function getRealStoreFunction() { if (!storeEntryFn) { try { const { storeEntry } = await import('../memory/memory-initializer.js'); storeEntryFn = storeEntry; } catch { storeEntryFn = null; } } return storeEntryFn; } // ============================================================================= // Neural Module Lazy Loaders (SONA, EWC++, MoE, LoRA, Flash Attention) // ============================================================================= // SONA Optimizer - lazy loaded let sonaOptimizer = null; async function getSONAOptimizer() { if (!sonaOptimizer) { try { const { getSONAOptimizer: getSona } = await import('../memory/sona-optimizer.js'); sonaOptimizer = await getSona(); } catch { sonaOptimizer = null; } } return sonaOptimizer; } // EWC++ Consolidator - lazy loaded let ewcConsolidator = null; async function getEWCConsolidator() { if (!ewcConsolidator) { try { const { getEWCConsolidator: getEWC } = await import('../memory/ewc-consolidation.js'); ewcConsolidator = await getEWC(); } catch { ewcConsolidator = null; } } return ewcConsolidator; } // MoE Router - lazy loaded // #1773 item 4 — moe-router migrated to @claude-flow/neural let moeRouter = null; async function getMoERouter() { if (!moeRouter) { try { const { getMoERouter: getMoE } = await import('@claude-flow/neural'); moeRouter = await getMoE(); } catch { moeRouter = null; } } return moeRouter; } // Semantic Router - lazy loaded // Tries native VectorDb first (16k+ routes/s HNSW), falls back to pure JS (47k routes/s cosine) let semanticRouter = null; let nativeVectorDb = null; let semanticRouterInitialized = false; let routerBackend = 'none'; // Pre-computed embeddings for common task patterns (cached) const TASK_PATTERN_EMBEDDINGS = new Map(); function generateSimpleEmbedding(text, dimension = 384) { // Simple deterministic embedding based on character codes // This is for routing purposes where we need consistent, fast embeddings const embedding = new Float32Array(dimension); const normalized = text.toLowerCase().replace(/[^a-z0-9\s]/g, ''); const words = normalized.split(/\s+/).filter(w => w.length > 0); // Combine word-level and character-level features for (let i = 0; i < dimension; i++) { let value = 0; // Word-level features for (let w = 0; w < words.length; w++) { const word = words[w]; for (let c = 0; c < word.length; c++) { const charCode = word.charCodeAt(c); value += Math.sin((charCode * (i + 1) + w * 17 + c * 23) * 0.0137); } } // Character-level features for (let c = 0; c < text.length; c++) { value += Math.cos((text.charCodeAt(c) * (i + 1) + c * 7) * 0.0073); } embedding[i] = value / Math.max(1, text.length); } // Normalize let norm = 0; for (let i = 0; i < dimension; i++) { norm += embedding[i] * embedding[i]; } norm = Math.sqrt(norm); if (norm > 0) { for (let i = 0; i < dimension; i++) { embedding[i] /= norm; } } return embedding; } // ── Runtime routing outcome persistence ────────────────────────────── // Closes the learning loop: post-task records outcomes → route loads them. const ROUTING_OUTCOMES_PATH = join(resolve('.'), '.claude-flow/routing-outcomes.json'); const ROUTING_STOPWORDS = new Set([ 'the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been', 'being', 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could', 'should', 'may', 'might', 'shall', 'can', 'to', 'of', 'in', 'for', 'on', 'with', 'at', 'by', 'from', 'as', 'into', 'through', 'during', 'before', 'after', 'above', 'below', 'between', 'under', 'again', 'further', 'then', 'once', 'it', 'its', 'this', 'that', 'these', 'those', 'i', 'me', 'my', 'we', 'our', 'you', 'your', 'he', 'she', 'they', 'them', 'and', 'but', 'or', 'nor', 'not', 'no', 'so', 'if', 'when', 'than', 'very', 'just', 'also', 'only', 'both', 'each', 'all', 'any', 'few', 'more', 'most', 'other', 'some', 'such', 'same', 'new', 'now', 'here', 'there', 'where', 'how', 'what', 'which', 'who', ]); function extractKeywords(text) { if (!text) return []; return text.toLowerCase() .replace(/[^a-z0-9\s-]/g, ' ') .split(/\s+/) .filter(w => w.length > 2 && !ROUTING_STOPWORDS.has(w)); } function loadRoutingOutcomes() { try { if (existsSync(ROUTING_OUTCOMES_PATH)) { const data = JSON.parse(readFileSync(ROUTING_OUTCOMES_PATH, 'utf-8')); return data.outcomes || []; } } catch { /* corrupt file, start fresh */ } return []; } function saveRoutingOutcomes(outcomes) { try { const dir = dirname(ROUTING_OUTCOMES_PATH); if (!existsSync(dir)) mkdirSync(dir, { recursive: true }); // Cap at 500 entries to bound file size const capped = outcomes.slice(-500); writeFileSync(ROUTING_OUTCOMES_PATH, JSON.stringify({ outcomes: capped }, null, 2)); } catch { /* non-critical */ } } /** * Build learned routing patterns from successful task outcomes. * Returns patterns in the same shape as TASK_PATTERNS so they can be * merged into both the native HNSW and pure-JS semantic routers. */ function loadLearnedPatterns() { const outcomes = loadRoutingOutcomes(); const byAgent = {}; for (const o of outcomes) { if (!o.success || !o.agent || !o.keywords?.length) continue; if (!byAgent[o.agent]) byAgent[o.agent] = new Set(); for (const kw of o.keywords) byAgent[o.agent].add(kw); } const patterns = {}; for (const [agent, kwSet] of Object.entries(byAgent)) { patterns[`learned-${agent}`] = { keywords: [...kwSet].slice(0, 50), agents: [agent], }; } return patterns; } /** * Merge static TASK_PATTERNS with runtime-learned patterns. * Static patterns take precedence (learned patterns won't overwrite them). */ function getMergedTaskPatterns() { const merged = { ...TASK_PATTERNS }; const learned = loadLearnedPatterns(); for (const [key, pattern] of Object.entries(learned)) { if (!merged[key]) { merged[key] = pattern; } } return merged; } // ── Static task patterns (used by both native and pure-JS routers) ─── const TASK_PATTERNS = { 'security-task': { keywords: ['authentication', 'security', 'auth', 'password', 'encryption', 'vulnerability', 'cve', 'audit'], agents: ['security-architect', 'security-auditor', 'reviewer'], }, 'testing-task': { keywords: ['test', 'testing', 'spec', 'coverage', 'unit test', 'integration test', 'e2e'], agents: ['tester', 'reviewer'], }, 'api-task': { keywords: ['api', 'endpoint', 'rest', 'graphql', 'route', 'handler', 'controller'], agents: ['architect', 'coder', 'tester'], }, 'performance-task': { keywords: ['performance', 'optimize', 'speed', 'memory', 'benchmark', 'profiling', 'bottleneck'], agents: ['performance-engineer', 'coder', 'tester'], }, 'refactor-task': { keywords: ['refactor', 'restructure', 'clean', 'organize', 'modular', 'decouple'], agents: ['architect', 'coder', 'reviewer'], }, 'bugfix-task': { keywords: ['bug', 'fix', 'error', 'issue', 'broken', 'crash', 'debug'], agents: ['coder', 'tester', 'reviewer'], }, 'feature-task': { keywords: ['feature', 'implement', 'add', 'new', 'create', 'build'], agents: ['architect', 'coder', 'tester'], }, 'database-task': { keywords: ['database', 'sql', 'query', 'schema', 'migration', 'orm'], agents: ['architect', 'coder', 'tester'], }, 'frontend-task': { keywords: ['frontend', 'ui', 'component', 'react', 'css', 'style', 'layout'], agents: ['coder', 'reviewer', 'tester'], }, 'devops-task': { keywords: ['deploy', 'ci', 'cd', 'pipeline', 'docker', 'kubernetes', 'infrastructure'], agents: ['devops', 'coder', 'tester'], }, 'swarm-task': { keywords: ['swarm', 'agent', 'coordinator', 'hive', 'mesh', 'topology'], agents: ['swarm-specialist', 'coordinator', 'architect'], }, 'memory-task': { keywords: ['memory', 'cache', 'store', 'vector', 'embedding', 'persistence'], agents: ['memory-specialist', 'architect', 'coder'], }, }; /** * Get the semantic router with environment detection. * Tries native VectorDb first (HNSW, 16k routes/s), falls back to pure JS (47k routes/s cosine). */ async function getSemanticRouter() { if (semanticRouterInitialized) { return { router: semanticRouter, backend: routerBackend, native: nativeVectorDb }; } semanticRouterInitialized = true; // STEP 1: Try native VectorDb from @ruvector/router (HNSW-backed) // Note: Native VectorDb uses a persistent database file which can have lock issues // in concurrent environments. We try it first but fall back gracefully to pure JS. try { // Use createRequire for ESM compatibility with native modules const { createRequire } = await import('module'); const require = createRequire(import.meta.url); const router = require('@ruvector/router'); if (router.VectorDb && router.DistanceMetric) { // Try to create VectorDb - may fail with lock error in concurrent envs const db = new router.VectorDb({ dimensions: 384, distanceMetric: router.DistanceMetric.Cosine, hnswM: 16, hnswEfConstruction: 200, hnswEfSearch: 100, }); // Initialize with static + runtime-learned task patterns for (const [patternName, { keywords }] of Object.entries(getMergedTaskPatterns())) { for (const keyword of keywords) { const embedding = generateSimpleEmbedding(keyword); db.insert(`${patternName}:${keyword}`, embedding); TASK_PATTERN_EMBEDDINGS.set(`${patternName}:${keyword}`, embedding); } } nativeVectorDb = db; routerBackend = 'native'; console.log('[hooks] Semantic router initialized: native VectorDb (HNSW, 16k+ routes/s)'); return { router: null, backend: routerBackend, native: nativeVectorDb }; } } catch (err) { // Native not available or database locked - fall back to pure JS // Common errors: "Database already open. Cannot acquire lock." or "MODULE_NOT_FOUND" // This is expected in concurrent environments or when binary isn't installed } // STEP 2: Fall back to pure JS SemanticRouter try { const { SemanticRouter } = await import('../ruvector/semantic-router.js'); semanticRouter = new SemanticRouter({ dimension: 384 }); for (const [patternName, { keywords, agents }] of Object.entries(getMergedTaskPatterns())) { const embeddings = keywords.map(kw => generateSimpleEmbedding(kw)); semanticRouter.addIntentWithEmbeddings(patternName, embeddings, { agents, keywords }); // Cache embeddings for keywords keywords.forEach((kw, i) => { TASK_PATTERN_EMBEDDINGS.set(kw, embeddings[i]); }); } routerBackend = 'pure-js'; console.log('[hooks] Semantic router initialized: pure JS (cosine, 47k routes/s)'); } catch { semanticRouter = null; routerBackend = 'none'; console.log('[hooks] Semantic router initialized: none (no backend available)'); } return { router: semanticRouter, backend: routerBackend, native: nativeVectorDb }; } /** * Get router backend info for status display. */ function getRouterBackendInfo() { switch (routerBackend) { case 'native': return { backend: 'native VectorDb (HNSW)', speed: '16k+ routes/s' }; case 'pure-js': return { backend: 'pure JS (cosine)', speed: '47k routes/s' }; default: return { backend: 'none', speed: 'N/A' }; } } // Flash Attention - lazy loaded // #1773 item 4 — flash-attention migrated to @claude-flow/neural let flashAttention = null; async function getFlashAttention() { if (!flashAttention) { try { const { getFlashAttention: getFlash } = await import('@claude-flow/neural'); flashAttention = await getFlash(); } catch { flashAttention = null; } } return flashAttention; } // LoRA Adapter - lazy loaded let loraAdapter = null; async function getLoRAAdapter() { if (!loraAdapter) { try { const { getLoRAAdapter: getLora } = await import('../ruvector/lora-adapter.js'); loraAdapter = await getLora(); } catch { loraAdapter = null; } } return loraAdapter; } // In-memory trajectory tracking (persisted on end) const activeTrajectories = new Map(); const MEMORY_DIR = '.claude-flow/memory'; const MEMORY_FILE = 'store.json'; function getMemoryPath() { return resolve(join(MEMORY_DIR, MEMORY_FILE)); } function loadMemoryStore() { try { const path = getMemoryPath(); if (existsSync(path)) { const data = readFileSync(path, 'utf-8'); return JSON.parse(data); } } catch { // Return empty store on error } return { entries: {}, version: '3.0.0' }; } /** * Get real intelligence statistics from memory store */ function getIntelligenceStatsFromMemory() { const store = loadMemoryStore(); const entries = Object.values(store.entries); // Count trajectories (keys starting with "trajectory-" or containing trajectory data) const trajectoryEntries = entries.filter(e => e.key.includes('trajectory') || (e.metadata?.type === 'trajectory')); const successfulTrajectories = trajectoryEntries.filter(e => e.metadata?.success === true || (typeof e.value === 'object' && e.value !== null && e.value.success === true)); // Count patterns const patternEntries = entries.filter(e => e.key.includes('pattern') || e.metadata?.type === 'pattern' || e.key.startsWith('learned-')); // Categorize patterns const categories = {}; patternEntries.forEach(e => { const category = e.metadata?.category || 'general'; categories[category] = (categories[category] || 0) + 1; }); // Count routing decisions const routingEntries = entries.filter(e => e.key.includes('routing') || e.metadata?.type === 'routing-decision'); // Calculate average confidence from routing decisions let totalConfidence = 0; let confidenceCount = 0; routingEntries.forEach(e => { const confidence = e.metadata?.confidence; if (typeof confidence === 'number') { totalConfidence += confidence; confidenceCount++; } }); // Calculate total access count const totalAccessCount = entries.reduce((sum, e) => sum + (e.accessCount || 0), 0); // Calculate memory file size let memorySizeBytes = 0; try { const memPath = getMemoryPath(); if (existsSync(memPath)) { memorySizeBytes = statSync(memPath).size; } } catch { // Ignore } return { trajectories: { total: trajectoryEntries.length, successful: successfulTrajectories.length, }, patterns: { learned: patternEntries.length, categories, }, memory: { indexSize: entries.length, totalAccessCount, memorySizeBytes, }, routing: { decisions: routingEntries.length, avgConfidence: confidenceCount > 0 ? totalConfidence / confidenceCount : 0, }, }; } // Agent routing configuration - maps file types to recommended agents const AGENT_PATTERNS = { '.ts': ['coder', 'architect', 'tester'], '.tsx': ['coder', 'architect', 'reviewer'], '.test.ts': ['tester', 'reviewer'], '.spec.ts': ['tester', 'reviewer'], '.md': ['researcher', 'documenter'], '.json': ['coder', 'architect'], '.yaml': ['coder', 'devops'], '.yml': ['coder', 'devops'], '.sh': ['devops', 'coder'], '.py': ['coder', 'ml-developer', 'researcher'], '.sql': ['coder', 'architect'], '.css': ['coder', 'designer'], '.scss': ['coder', 'designer'], }; // Keyword patterns for fallback routing (when semantic routing doesn't match) const KEYWORD_PATTERNS = { 'authentication': { agents: ['security-architect', 'coder', 'tester'], confidence: 0.9 }, 'auth': { agents: ['security-architect', 'coder', 'tester'], confidence: 0.85 }, 'api': { agents: ['architect', 'coder', 'tester'], confidence: 0.85 }, 'test': { agents: ['tester', 'reviewer'], confidence: 0.95 }, 'refactor': { agents: ['architect', 'coder', 'reviewer'], confidence: 0.9 }, 'performance': { agents: ['performance-engineer', 'coder', 'tester'], confidence: 0.88 }, 'security': { agents: ['security-architect', 'security-auditor', 'reviewer'], confidence: 0.92 }, 'database': { agents: ['architect', 'coder', 'tester'], confidence: 0.85 }, 'frontend': { agents: ['coder', 'designer', 'tester'], confidence: 0.82 }, 'backend': { agents: ['architect', 'coder', 'tester'], confidence: 0.85 }, 'bug': { agents: ['coder', 'tester', 'reviewer'], confidence: 0.88 }, 'fix': { agents: ['coder', 'tester', 'reviewer'], confidence: 0.85 }, 'feature': { agents: ['architect', 'coder', 'tester'], confidence: 0.8 }, 'swarm': { agents: ['swarm-specialist', 'coordinator', 'architect'], confidence: 0.9 }, 'memory': { agents: ['memory-specialist', 'architect', 'coder'], confidence: 0.88 }, 'deploy': { agents: ['devops', 'coder', 'tester'], confidence: 0.85 }, 'ci/cd': { agents: ['devops', 'coder'], confidence: 0.9 }, }; function getFileExtension(filePath) { const match = filePath.match(/\.[a-zA-Z0-9]+$/); return match ? match[0] : ''; } function suggestAgentsForFile(filePath) { const ext = getFileExtension(filePath); // Check for test files first if (filePath.includes('.test.') || filePath.includes('.spec.')) { return AGENT_PATTERNS['.test.ts'] || ['tester', 'reviewer']; } return AGENT_PATTERNS[ext] || ['coder', 'architect']; } function suggestAgentsForTask(task) { const taskLower = task.toLowerCase(); // Check static keyword patterns first for (const [pattern, result] of Object.entries(KEYWORD_PATTERNS)) { if (taskLower.includes(pattern)) { return result; } } // Check runtime-learned patterns from successful task outcomes const taskKeywords = extractKeywords(task); if (taskKeywords.length > 0) { const outcomes = loadRoutingOutcomes(); let bestAgent = ''; let bestOverlap = 0; for (const outcome of outcomes) { if (!outcome.success || !outcome.agent || !outcome.keywords?.length) continue; const overlap = taskKeywords.filter(kw => outcome.keywords.includes(kw)).length; if (overlap > bestOverlap) { bestOverlap = overlap; bestAgent = outcome.agent; } } // Require at least 2 keyword overlap to prevent false positives if (bestAgent && bestOverlap >= 2) { return { agents: [bestAgent], confidence: Math.min(0.6 + bestOverlap * 0.05, 0.85) }; } } // Default fallback return { agents: ['coder', 'researcher', 'tester'], confidence: 0.7 }; } function assessCommandRisk(command) { const warnings = []; let level = 0; // High risk commands if (command.includes('rm -rf') || command.includes('rm -r')) { level = Math.max(level, 0.9); warnings.push('Recursive deletion detected - verify target path'); } if (command.includes('sudo')) { level = Math.max(level, 0.7); warnings.push('Elevated privileges requested'); } if (command.includes('> /') || command.includes('>> /')) { level = Math.max(level, 0.6); warnings.push('Writing to system path'); } if (command.includes('chmod') || command.includes('chown')) { level = Math.max(level, 0.5); warnings.push('Permission modification'); } if (command.includes('curl') && command.includes('|')) { level = Math.max(level, 0.8); warnings.push('Piping remote content to shell'); } // Safe commands if (command.startsWith('npm ') || command.startsWith('npx ')) { level = Math.min(level, 0.3); } if (command.startsWith('git ')) { level = Math.min(level, 0.2); } if (command.startsWith('ls ') || command.startsWith('cat ') || command.startsWith('echo ')) { level = Math.min(level, 0.1); } const risk = level >= 0.7 ? 'high' : level >= 0.4 ? 'medium' : 'low'; return { risk, level, warnings }; } // MCP Tool implementations - return raw data for direct CLI use export const hooksPreEdit = { name: 'hooks_pre-edit', description: 'Get context and agent suggestions before editing a file Use when native Bash hooks (via Claude Code\'s settings.json) are wrong because you need Ruflo-side state — pattern persistence, neural training signals, model-routing learning, cost tracking, audit chain. For one-off shell commands, plain Bash hooks are fine.', inputSchema: { type: 'object', properties: { filePath: { type: 'string', description: 'Path to the file being edited' }, operation: { type: 'string', description: 'Type of operation (create, update, delete, refactor)' }, context: { type: 'string', description: 'Additional context' }, }, required: ['filePath'], }, handler: async (params) => { const filePath = params.filePath; const operation = params.operation || 'update'; { const v = validatePath(filePath, 'filePath'); if (!v.valid) return { success: false, error: v.error }; } const suggestedAgents = suggestAgentsForFile(filePath); const ext = getFileExtension(filePath); return { filePath, operation, context: { fileExists: true, fileType: ext || 'unknown', relatedFiles: [], suggestedAgents, patterns: [ { pattern: `${ext} file editing`, confidence: 0.85 }, ], risks: operation === 'delete' ? ['File deletion is irreversible'] : [], }, recommendations: [ `Recommended agents: ${suggestedAgents.join(', ')}`, 'Run tests after changes', ], }; }, }; export const hooksPostEdit = { name: 'hooks_post-edit', description: 'Record editing outcome for learning Use when native Bash hooks (via Claude Code\'s settings.json) are wrong because you need Ruflo-side state — pattern persistence, neural training signals, model-routing learning, cost tracking, audit chain. For one-off shell commands, plain Bash hooks are fine.', inputSchema: { type: 'object', properties: { filePath: { type: 'string', description: 'Path to the edited file' }, success: { type: 'boolean', description: 'Whether the edit was successful' }, agent: { type: 'string', description: 'Agent that performed the edit' }, }, required: ['filePath'], }, handler: async (params) => { const filePath = params.filePath; const success = params.success !== false; const agent = params.agent; { const v = validatePath(filePath, 'filePath'); if (!v.valid) return { success: false, error: v.error }; } if (agent) { const v = validateIdentifier(agent, 'agent'); if (!v.valid) return { success: false, error: v.error }; } // Wire recordFeedback through bridge (issue #1209) let feedbackResult = null; try { const bridge = await import('../memory/memory-bridge.js'); feedbackResult = await bridge.bridgeRecordFeedback({ taskId: `edit-${filePath}-${Date.now()}`, success, quality: success ? 0.85 : 0.3, agent, }); } catch { // Bridge not available — continue with basic response } return { recorded: true, filePath, success, timestamp: new Date().toISOString(), learningUpdate: success ? 'pattern_reinforced' : 'pattern_adjusted', feedback: feedbackResult ? { recorded: feedbackResult.success, controller: feedbackResult.controller, updates: feedbackResult.updated, } : { recorded: false, controller: 'unavailable', updates: 0 }, }; }, }; export const hooksPreCommand = { name: 'hooks_pre-command', description: 'Assess risk before executing a command Use when native Bash hooks (via Claude Code\'s settings.json) are wrong because you need Ruflo-side state — pattern persistence, neural training signals, model-routing learning, cost tracking, audit chain. For one-off shell commands, plain Bash hooks are fine.', inputSchema: { type: 'object', properties: { command: { type: 'string', description: 'Command to execute' }, }, required: ['command'], }, handler: async (params) => { const command = params.command; { const v = validateText(command, 'command'); if (!v.valid) return { success: false, error: v.error }; } const assessment = assessCommandRisk(command); const riskLevel = assessment.level >= 0.8 ? 'critical' : assessment.level >= 0.6 ? 'high' : assessment.level >= 0.3 ? 'medium' : 'low'; // #6: tool-loop circuit breaker — warn/block when this exact command has // failed repeatedly in a row (an agent stuck looping on a failing call). const loop = checkCommandLoop(command); const recommendations = assessment.warnings.length > 0 ? ['Review warnings before proceeding', 'Consider using safer alternative'] : ['Command appears safe to execute']; if (loop.hint) recommendations.unshift(loop.hint); return { command, riskLevel, risks: assessment.warnings.map((warning, i) => ({ type: `risk-${i + 1}`, severity: assessment.level >= 0.6 ? 'high' : 'medium', description: warning, })), recommendations, loopGuard: { verdict: loop.verdict, consecutiveFailures: loop.consecutiveFailures }, safeAlternatives: [], // Don't proceed on a high-risk command OR a hard loop-block. shouldProceed: assessment.level < 0.7 && loop.verdict !== 'block', }; }, }; export const hooksPostCommand = { name: 'hooks_post-command', description: 'Record command execution outcome Use when native Bash hooks (via Claude Code\'s settings.json) are wrong because you need Ruflo-side state — pattern persistence, neural training signals, model-routing learning, cost tracking, audit chain. For one-off shell commands, plain Bash hooks are fine.', inputSchema: { type: 'object', properties: { command: { type: 'string', description: 'Executed command' }, exitCode: { type: 'number', description: 'Command exit code' }, }, required: ['command'], }, handler: async (params) => { const command = params.command; const exitCode = params.exitCode || 0; const success = exitCode === 0; { const v = validateText(command, 'command'); if (!v.valid) return { success: false, error: v.error }; } // #6: feed the tool-loop circuit breaker so pre-command can warn/block on // repeated consecutive failures of the same command. recordCommandOutcome(command, success); // Persist command outcome via AgentDB let _storedIn = 'none'; try { const bridge = await import('../memory/memory-bridge.js'); await bridge.bridgeStoreEntry({ key: `cmd-${Date.now()}`, value: JSON.stringify({ command, exitCode, success }), namespace: 'commands', tags: [success ? 'success' : 'error'], }); _storedIn = 'agentdb'; } catch { // AgentDB not available — store in JSON try { const store = loadMemoryStore(); const key = `cmd-${Date.now()}`; store.entries[key] = { key, value: JSON.stringify({ command, exitCode, success }), namespace: 'commands', createdAt: new Date().toISOString() }; const memDir = resolve(MEMORY_DIR); if (!existsSync(memDir)) mkdirSync(memDir, { recursive: true }); writeFileSync(getMemoryPath(), JSON.stringify(store, null, 2), 'utf-8'); _storedIn = 'json-store'; } catch { /* non-critical */ } } return { recorded: _storedIn !== 'none', command, exitCode, success, timestamp: new Date().toISOString(), _storedIn, }; }, }; export const hooksRoute = { name: 'hooks_route', description: 'Get a 3-tier routing recommendation for a task: Tier 1 (Agent Booster, 0ms / $0 — for var-to-const, add-types, etc.), Tier 2 (Haiku — simple), Tier 3 (Sonnet/Opus — complex). Use this BEFORE spawning an agent to avoid sending simple transforms to Sonnet. Native tools have no equivalent — Claude Code does not introspect its own model-selection cost. Returns the recommended model + a `[AGENT_BOOSTER_AVAILABLE]` literal when the WASM bypass applies. Use when native Bash hooks (via Claude Code\'s settings.json) are wrong because you need Ruflo-side state — pattern persistence, neural training signals, model-routing learning, cost tracking, audit chain. For one-off shell commands, plain Bash hooks are fine.', inputSchema: { type: 'object', properties: { task: { type: 'string', description: 'Task description' }, context: { type: 'string', description: 'Additional context' }, useSemanticRouter: { type: 'boolean', description: 'Use semantic similarity routing (default: true)' }, }, required: ['task'], }, handler: async (params) => { const task = params.task; const context = params.context; const useSemanticRouter = params.useSemanticRouter !== false; { const v = validateText(task, 'task'); if (!v.valid) return { success: false, error: v.error }; } if (context) { const v = validateText(context, 'context'); if (!v.valid) return { success: false, error: v.error }; } // Phase 5: Try AgentDB's SemanticRouter / LearningSystem first if (useSemanticRouter) { try { const bridge = await import('../memory/memory-bridge.js'); const agentdbRoute = await bridge.bridgeRouteTask({ task, context }); if (agentdbRoute && agentdbRoute.confidence > 0.5) { const agents = agentdbRoute.agents.length > 0 ? agentdbRoute.agents : ['coder', 'researcher']; const complexity = task.length > 200 ? 'high' : task.length < 50 ? 'low' : 'medium'; return { task, routing: { method: `agentdb-${agentdbRoute.controller}`, backend: agentdbRoute.controller, latencyMs: 0, throughput: 'N/A', }, matchedPattern: agentdbRoute.route, semanticMatches: [{ pattern: agentdbRoute.route, score: agentdbRoute.confidence }], primaryAgent: { type: agents[0], confidence: Math.round(agentdbRoute.confidence * 100) / 100, reason: `AgentDB ${agentdbRoute.controller}: "${agentdbRoute.route}" (${Math.round(agentdbRoute.confidence * 100)}%)`, }, alternativeAgents: agents.slice(1).map((agent, i) => ({ type: agent, confidence: Math.round((agentdbRoute.confidence - (0.1 * (i + 1))) * 100) / 100, reason: `Alternative from ${agentdbRoute.controller}`, })), estimatedMetrics: { successProbability: Math.round(agentdbRoute.confidence * 100) / 100, estimatedDuration: complexity === 'high' ? '2-4 hours' : complexity === 'medium' ? '30-60 min' : '10-30 min', complexity, }, swarmRecommendation: agents.length > 2 ? { topology: 'hierarchical', agents, coordination: 'queen-led' } : null, }; } } catch { // AgentDB router not available — fall through to local routing } } // Get router (tries native VectorDb first, falls back to pure JS) const { router, backend, native } = useSemanticRouter ? await getSemanticRouter() : { router: null, backend: 'none', native: null }; let semanticResult = []; let routingMethod = 'keyword'; let routingLatencyMs = 0; let backendInfo = ''; const queryText = context ? `${task} ${context}` : task; const queryEmbedding = generateSimpleEmbedding(queryText); // Try native VectorDb (HNSW-backed) if (native && backend === 'native') { const routeStart = performance.now(); try { // eslint-disable-next-line @typescript-eslint/no-explicit-any const results = native.search(queryEmbedding, 5); routingLatencyMs = performance.now() - routeStart; routingMethod = 'semantic-native'; backendInfo = 'native VectorDb (HNSW)'; // Convert results to semantic format const mergedPatterns = getMergedTaskPatterns(); semanticResult = results.map((r) => { const [patternName] = r.id.split(':'); const pattern = mergedPatterns[patternName]; return { intent: patternName, score: 1 - r.score, // Native uses distance (lower is better), convert to similarity metadata: { agents: pattern?.agents || (patternName.startsWith('learned-') ? [patternName.slice(8)] : ['coder']), }, }; }); } catch { // Native failed, try pure JS fallback } } // Try pure JS SemanticRouter fallback if (router && backend === 'pure-js' && semanticResult.length === 0) { const routeStart = performance.now(); semanticResult = router.routeWithEmbedding(queryEmbedding, 3); routingLatencyMs = performance.now() - routeStart; routingMethod = 'semantic-pure-js'; backendInfo = 'pure JS (cosine similarity)'; } // Get agents from semantic routing or fall back to keyword let agents; let confidence; let matchedPattern = ''; if (semanticResult.length > 0 && semanticResult[0].score > 0.4) { const topMatch = semanticResult[0]; agents = topMatch.metadata.agents || ['coder', 'researcher']; confidence = topMatch.score; matchedPattern = topMatch.intent; } else { // Fall back to keyword matching const suggestion = suggestAgentsForTask(task); agents = suggestion.agents; confidence = suggestion.confidence; matchedPattern = 'keyword-fallback'; routingMethod = 'keyword'; backendInfo = 'keyword matching'; } // Determine complexity const taskLower = task.toLowerCase(); const complexity = taskLower.includes('complex') || taskLower.includes('architecture') || task.length > 200 ? 'high' : taskLower.includes('simple') || taskLower.includes('fix') || task.length < 50 ? 'low' : 'medium'; return { task, routing: { method: routingMethod, backend: backendInfo, latencyMs: routingLatencyMs, throughput: routingLatencyMs > 0 ? `${Math.round(1000 / routingLatencyMs)} routes/s` : 'N/A', }, matchedPattern, semanticMatches: semanticResult.slice(0, 3).map(r => ({ pattern: r.intent, score: Math.round(r.score * 100) / 100, })), primaryAgent: { type: agents[0], confidence: Math.round(confidence * 100) / 100, reason: routingMethod.startsWith('semantic') ? `Semantic similarity to "${matchedPattern}" pattern (${Math.round(confidence * 100)}%)` : `Task contains keywords matching ${agents[0]} specialization`, }, alternativeAgents: agents.slice(1).map((agent, i) => ({ type: agent, confidence: Math.round((confidence - (0.1 * (i + 1))) * 100) / 100, reason: `Alternative agent for ${agent} capabilities`, })), estimatedMetrics: { successProbability: Math.round(confidence * 100) / 100, estimatedDuration: complexity === 'high' ? '2-4 hours' : complexity === 'medium' ? '30-60 min' : '10-30 min', complexity, }, swarmRecommendation: agents.length > 2 ? { topology: 'hierarchical', agents, coordination: 'queen-led', } : null, }; }, }; export const hooksMetrics = { name: 'hooks_metrics', description: 'View learning metrics dashboard Use when native Bash hooks (via Claude Code\'s settings.json) are wrong because you need Ruflo-side state — pattern persistence, neural training signals, model-routing learning, cost tracking, audit chain. For one-off shell commands, plain Bash hooks are fine.', inputSchema: { type: 'object', properties: { period: { type: 'string', description: 'Metrics period (1h, 24h, 7d, 30d)' }, includeV3: { type: 'boolean', description: 'Include V3 performance metrics' }, }, }, handler: async (params) => { const period = params.period || '24h'; // ADR-093 F1: read from the same trajectory/pattern store that // hooks_post-task and hooks_intelligence_stats write to. Previously // this handler key-substring-filtered the memory store for "pattern", // "route", "task" — none of which match the trajectory keys that // post-task actually writes — so counters stayed at 0 forever (#1686). const stats = getIntelligenceStatsFromMemory(); // Routing outcomes are persisted to a separate file (loadRoutingOutcomes) // by post-task; surface them so the dashboard sees command counters too. let routingOutcomes = []; try { routingOutcomes = loadRoutingOutcomes(); } catch { /* non-fatal */ } const totalCommands = routingOutcomes.length; const successfulCommands = routingOutcomes.filter(o => o.success).length; const successRate = totalCommands > 0 ? successfulCommands / totalCommands : null; // Compute top agent from routing outcomes const agentCounts = {}; for (const o of routingOutcomes) { if (o.agent) agentCounts[o.agent] = (agentCounts[o.agent] || 0) + 1; } const topAgent = Object.entries(agentCounts).sort((a, b) => b[1] - a[1])[0]?.[0] ?? null; const successful = stats.trajectories.successful; const total = stats.trajectories.total; const failed = Math.max(0, total - successful); return { _real: true, _dataSource: 'intelligence-stats + routing-outcomes', period, patterns: { total: stats.patterns.learned, successful, failed, avgConfidence: stats.routing.avgConfidence || null, }, agents: { routingAccuracy: stats.routing.avgConfidence || null, totalRoutes: stats.routing.decisions, topAgent, }, commands: { totalExecuted: totalCommands, successRate, avgRiskScore: null, }, _note: total === 0 && totalCommands === 0 ? 'No metrics data collected yet. Run hooks_post-task / hooks_intelligence_trajectory-end / hooks_route to populate.' : undefined, lastUpdated: new Date().toISOString(), }; }, }; export const hooksList = { name: 'hooks_list', description: 'List all registered hooks Use when native Bash hooks (via Claude Code\'s settings.json) are wrong because you need Ruflo-side state — pattern persistence, neural training signals, model-routing learning, cost tracking, audit chain. For one-off shell commands, plain Bash hooks are fine.', inputSchema: { type: 'object', properties: {}, }, handler: async () => { return { hooks: [ // Core hooks { name: 'pre-edit', type: 'PreToolUse', status: 'active' }, { name: 'post-edit', type: 'PostToolUse', status: 'active' }, { name: 'pre-command', type: 'PreToolUse', status: 'active' }, { name: 'post-command', type: 'PostToolUse', status: 'active' }, { name: 'pre-task', type: 'PreToolUse', status: 'active' }, { name: 'post-task', type: 'PostToolUse', status: 'active' }, // Routing hooks { name: 'route', type: 'intelligence', status: 'active' }, { name: 'explain', type: 'intelligence', status: 'active' }, // Session hooks { name: 'session-start', type: 'SessionStart', status: 'active' }, { name: 'session-end', type: 'SessionEnd', status: 'active' }, { name: 'session-restore', type: 'SessionStart', status: 'active' }, // Learning hooks { name: 'pretrain', type: 'intelligence', status: 'active' }, { name: 'build-agents', type: 'intelligence', status: 'active' }, { name: 'transfer', type: 'intelligence', status: 'active' }, { name: 'metrics', type: 'analytics', status: 'active' }, // System hooks { name: 'init', type: 'system', status: 'active' }, { name: 'notify', type: 'coordination', status: 'active' }, // Intelligence subcommands { name: 'intelligence', type: 'intelligence', status: 'active' }, { name: 'intelligence_trajectory-start', type: 'intelligence', status: 'active' }, { name: 'intelligence_trajectory-step', type: 'intelligence', status: 'active' }, { name: 'intelligence_trajectory-end', type: 'intelligence', status: 'active' }, { name: 'intelligence_pattern-store', type: 'intelligence', status: 'active' }, { name: 'intelligence_pattern-search', type: 'intelligence', status: 'active' }, { name: 'intelligence_stats', type: 'analytics', status: 'active' }, { name: 'intelligence_learn', type: 'intelligence', status: 'active' }, { name: 'intelligence_attention', type: 'intelligence', status: 'active' }, ], total: 26, }; }, }; export const hooksPreTask = { name: 'hooks_pre-task', description: 'Record task start and get agent suggestions with intelligent model routing (ADR-026) Use when native Bash hooks (via Claude Code\'s settings.json) are wrong because you need Ruflo-side state — pattern persistence, neural training signals, model-routing learning, cost tracking, audit chain. For one-off shell commands, plain Bash hooks are fine.', inputSchema: { type: 'object', properties: { taskId: { type: 'string', description: 'Task identifier' }, description: { type: 'string', description: 'Task description' }, filePath: { type: 'string', description: 'Optional file path for AST analysis' }, }, required: ['taskId', 'description'], }, handler: async (params) => { const taskId = params.taskId; const description = params.description; const filePath = params.filePath; { const v = validateIdentifier(taskId, 'taskId'); if (!v.valid) return { success: false, error: v.error }; } { const v = validateText(description, 'description'); if (!v.valid) return { success: false, error: v.error }; } if (filePath) { const v = validatePath(filePath, 'filePath'); if (!v.valid) return { success: false, error: v.error }; } const suggestion = suggestAgentsForTask(description); // Determine complexity const descLower = description.toLowerCase(); const complexity = descLower.includes('complex') || descLower.includes('architecture') || description.length > 200 ? 'high' : descLower.includes('simple') || descLower.includes('fix') || description.length < 50 ? 'low' : 'medium'; // Enhanced model routing with Agent Booster AST (ADR-026) let modelRouting; try { const { getEnhancedModelRouter } = await import('../ruvector/enhanced-model-router.js'); const router = getEnhancedModelRouter(); const routeResult = await router.route(description, { filePath }); if (routeResult.tier === 1) { // Agent Booster can handle this task modelRouting = { tier: 1,