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,169 lines • 206 kB
JavaScript
/**
* V3 CLI Hooks Command
* Self-learning hooks system for intelligent workflow automation
*/
import { output } from '../output.js';
import { confirm } from '../prompt.js';
import { callMCPTool, MCPClientError } from '../mcp-client.js';
import { storeCommand } from './transfer-store.js';
import { existsSync, readFileSync, statSync } from 'node:fs';
import { join } from 'node:path';
/**
* #1686 — `?? 0` only defaults null/undefined; NaN slips through and
* surfaces as `"NaN"` (or earlier crashed `.toFixed`) in the metrics
* dashboard and pretrain output. Coerce to a finite number, fall back
* to `fallback` when the input is null/undefined/non-numeric/NaN/Infinity.
*/
function safeNum(value, fallback = 0) {
const n = typeof value === 'number' ? value : Number(value);
return Number.isFinite(n) ? n : fallback;
}
/**
* Read coverage data from disk. Checks these locations in order:
* 1. coverage/coverage-summary.json (Jest/Istanbul)
* 2. coverage/lcov.info (lcov format)
* 3. .nyc_output/out.json (nyc)
*/
function readCoverageFromDisk() {
const cwd = process.cwd();
const noData = {
found: false,
source: 'none',
entries: [],
summary: { totalFiles: 0, overallLineCoverage: 0, overallBranchCoverage: 0, overallFunctionCoverage: 0, overallStatementCoverage: 0 },
};
// 1. Try coverage-summary.json (Jest/Istanbul)
for (const relPath of ['coverage/coverage-summary.json', 'coverage-summary.json']) {
const summaryPath = join(cwd, relPath);
if (existsSync(summaryPath)) {
try {
const raw = JSON.parse(readFileSync(summaryPath, 'utf-8'));
return parseCoverageSummaryJson(raw, relPath);
}
catch {
// malformed, try next
}
}
}
// 2. Try lcov.info
for (const relPath of ['coverage/lcov.info', 'lcov.info']) {
const lcovPath = join(cwd, relPath);
if (existsSync(lcovPath)) {
try {
const raw = readFileSync(lcovPath, 'utf-8');
return parseLcovInfo(raw, relPath);
}
catch {
// malformed, try next
}
}
}
// 3. Try .nyc_output/out.json
const nycPath = join(cwd, '.nyc_output', 'out.json');
if (existsSync(nycPath)) {
try {
const raw = JSON.parse(readFileSync(nycPath, 'utf-8'));
return parseCoverageSummaryJson(raw, '.nyc_output/out.json');
}
catch {
// malformed
}
}
return noData;
}
function parseCoverageSummaryJson(data, source) {
const entries = [];
let totalLines = 0, coveredLines = 0;
let totalBranches = 0, coveredBranches = 0;
let totalFunctions = 0, coveredFunctions = 0;
let totalStatements = 0, coveredStatements = 0;
for (const [filePath, metrics] of Object.entries(data)) {
if (filePath === 'total')
continue;
const m = metrics;
if (!m || typeof m !== 'object')
continue;
const linePct = m.lines?.pct ?? m.lines?.covered != null ? ((m.lines?.covered ?? 0) / Math.max(m.lines?.total ?? 1, 1)) * 100 : 0;
const branchPct = m.branches?.pct ?? (m.branches?.total ? ((m.branches?.covered ?? 0) / m.branches.total) * 100 : 100);
const funcPct = m.functions?.pct ?? (m.functions?.total ? ((m.functions?.covered ?? 0) / m.functions.total) * 100 : 100);
const stmtPct = m.statements?.pct ?? (m.statements?.total ? ((m.statements?.covered ?? 0) / m.statements.total) * 100 : 100);
entries.push({ filePath, lines: linePct, branches: branchPct, functions: funcPct, statements: stmtPct });
totalLines += m.lines?.total ?? 0;
coveredLines += m.lines?.covered ?? 0;
totalBranches += m.branches?.total ?? 0;
coveredBranches += m.branches?.covered ?? 0;
totalFunctions += m.functions?.total ?? 0;
coveredFunctions += m.functions?.covered ?? 0;
totalStatements += m.statements?.total ?? 0;
coveredStatements += m.statements?.covered ?? 0;
}
// Also read the total key if present
const total = data['total'];
const overallLine = total?.lines?.pct ?? (totalLines > 0 ? (coveredLines / totalLines) * 100 : 0);
const overallBranch = total?.branches?.pct ?? (totalBranches > 0 ? (coveredBranches / totalBranches) * 100 : 0);
const overallFunction = total?.functions?.pct ?? (totalFunctions > 0 ? (coveredFunctions / totalFunctions) * 100 : 0);
const overallStatement = total?.statements?.pct ?? (totalStatements > 0 ? (coveredStatements / totalStatements) * 100 : 0);
// Sort by lowest line coverage
entries.sort((a, b) => a.lines - b.lines);
return {
found: true,
source,
entries,
summary: {
totalFiles: entries.length,
overallLineCoverage: overallLine,
overallBranchCoverage: overallBranch,
overallFunctionCoverage: overallFunction,
overallStatementCoverage: overallStatement,
},
};
}
function parseLcovInfo(raw, source) {
const entries = [];
let currentFile = '';
let linesHit = 0, linesFound = 0;
let branchesHit = 0, branchesFound = 0;
let functionsHit = 0, functionsFound = 0;
const flushRecord = () => {
if (currentFile) {
entries.push({
filePath: currentFile,
lines: linesFound > 0 ? (linesHit / linesFound) * 100 : 0,
branches: branchesFound > 0 ? (branchesHit / branchesFound) * 100 : 100,
functions: functionsFound > 0 ? (functionsHit / functionsFound) * 100 : 100,
statements: linesFound > 0 ? (linesHit / linesFound) * 100 : 0,
});
}
};
for (const line of raw.split('\n')) {
const trimmed = line.trim();
if (trimmed.startsWith('SF:')) {
currentFile = trimmed.slice(3);
linesHit = 0;
linesFound = 0;
branchesHit = 0;
branchesFound = 0;
functionsHit = 0;
functionsFound = 0;
}
else if (trimmed.startsWith('LH:')) {
linesHit = parseInt(trimmed.slice(3), 10) || 0;
}
else if (trimmed.startsWith('LF:')) {
linesFound = parseInt(trimmed.slice(3), 10) || 0;
}
else if (trimmed.startsWith('BRH:')) {
branchesHit = parseInt(trimmed.slice(4), 10) || 0;
}
else if (trimmed.startsWith('BRF:')) {
branchesFound = parseInt(trimmed.slice(4), 10) || 0;
}
else if (trimmed.startsWith('FNH:')) {
functionsHit = parseInt(trimmed.slice(4), 10) || 0;
}
else if (trimmed.startsWith('FNF:')) {
functionsFound = parseInt(trimmed.slice(4), 10) || 0;
}
else if (trimmed === 'end_of_record') {
flushRecord();
currentFile = '';
}
}
flushRecord();
entries.sort((a, b) => a.lines - b.lines);
let totalLH = 0, totalLF = 0, totalBH = 0, totalBF = 0;
for (const e of entries) {
// Approximate from percentages (we lost exact counts after flush, but summaries are okay)
totalLH += e.lines;
totalLF += 100;
totalBH += e.branches;
totalBF += 100;
}
const n = entries.length || 1;
return {
found: true,
source,
entries,
summary: {
totalFiles: entries.length,
overallLineCoverage: totalLH / n,
overallBranchCoverage: totalBH / n,
overallFunctionCoverage: 0,
overallStatementCoverage: totalLH / n,
},
};
}
/**
* Classify a coverage gap by priority type based on coverage percentage and threshold
*/
function classifyCoverageGap(coveragePct, threshold) {
if (coveragePct < threshold * 0.25)
return { gapType: 'critical', priority: 10 };
if (coveragePct < threshold * 0.5)
return { gapType: 'high', priority: 7 };
if (coveragePct < threshold * 0.75)
return { gapType: 'medium', priority: 5 };
if (coveragePct < threshold)
return { gapType: 'low', priority: 3 };
return { gapType: 'ok', priority: 0 };
}
/**
* Suggest agents for a file based on its path
*/
function suggestAgentsForFile(filePath) {
const lower = filePath.toLowerCase();
if (lower.includes('test') || lower.includes('spec'))
return ['tester'];
if (lower.includes('security') || lower.includes('auth'))
return ['security-auditor', 'tester'];
if (lower.includes('api') || lower.includes('route') || lower.includes('controller'))
return ['coder', 'tester'];
if (lower.includes('model') || lower.includes('schema') || lower.includes('entity'))
return ['coder', 'tester'];
return ['tester', 'coder'];
}
// Hook types
const HOOK_TYPES = [
{ value: 'pre-edit', label: 'Pre-Edit', hint: 'Get context before editing files' },
{ value: 'post-edit', label: 'Post-Edit', hint: 'Record editing outcomes' },
{ value: 'pre-command', label: 'Pre-Command', hint: 'Assess risk before commands' },
{ value: 'post-command', label: 'Post-Command', hint: 'Record command outcomes' },
{ value: 'route', label: 'Route', hint: 'Route tasks to optimal agents' },
{ value: 'explain', label: 'Explain', hint: 'Explain routing decisions' }
];
// Agent routing options
const AGENT_TYPES = [
'coder', 'researcher', 'tester', 'reviewer', 'architect',
'security-architect', 'security-auditor', 'memory-specialist',
'swarm-specialist', 'performance-engineer', 'core-architect',
'test-architect', 'coordinator', 'analyst', 'optimizer'
];
// Pre-edit subcommand
const preEditCommand = {
name: 'pre-edit',
description: 'Get context and agent suggestions before editing a file',
options: [
{
name: 'file',
short: 'f',
description: 'File path to edit',
type: 'string',
required: false
},
{
name: 'operation',
short: 'o',
description: 'Type of edit operation (create, update, delete, refactor)',
type: 'string',
default: 'update'
},
{
name: 'context',
short: 'c',
description: 'Additional context about the edit',
type: 'string'
}
],
examples: [
{ command: 'claude-flow hooks pre-edit -f src/utils.ts', description: 'Get context before editing' },
{ command: 'claude-flow hooks pre-edit -f src/api.ts -o refactor', description: 'Pre-edit with operation type' }
],
action: async (ctx) => {
// Default file to 'unknown' for backward compatibility (env var may be empty)
const filePath = ctx.flags.file || ctx.args[0] || 'unknown';
const operation = ctx.flags.operation || 'update';
output.printInfo(`Analyzing context for: ${output.highlight(filePath)}`);
try {
// Call MCP tool for pre-edit hook
const result = await callMCPTool('hooks_pre-edit', {
filePath,
operation,
context: ctx.flags.context,
includePatterns: true,
includeRisks: true,
});
if (ctx.flags.format === 'json') {
output.printJson(result);
return { success: true, data: result };
}
output.writeln();
output.printBox([
`File: ${result.filePath}`,
`Operation: ${result.operation}`,
`Type: ${result.context.fileType}`,
`Exists: ${result.context.fileExists ? 'Yes' : 'No'}`
].join('\n'), 'File Context');
if (result.context.suggestedAgents.length > 0) {
output.writeln();
output.writeln(output.bold('Suggested Agents'));
output.printList(result.context.suggestedAgents.map(a => output.highlight(a)));
}
if (result.context.relatedFiles.length > 0) {
output.writeln();
output.writeln(output.bold('Related Files'));
output.printList(result.context.relatedFiles.slice(0, 5).map(f => output.dim(f)));
}
if (result.context.patterns.length > 0) {
output.writeln();
output.writeln(output.bold('Learned Patterns'));
output.printTable({
columns: [
{ key: 'pattern', header: 'Pattern', width: 40 },
{ key: 'confidence', header: 'Confidence', width: 12, align: 'right', format: (v) => `${(Number(v) * 100).toFixed(1)}%` }
],
data: result.context.patterns
});
}
if (result.context.risks.length > 0) {
output.writeln();
output.writeln(output.bold(output.error('Potential Risks')));
output.printList(result.context.risks.map(r => output.warning(r)));
}
if (result.recommendations.length > 0) {
output.writeln();
output.writeln(output.bold('Recommendations'));
output.printList(result.recommendations.map(r => output.success(`• ${r}`)));
}
return { success: true, data: result };
}
catch (error) {
if (error instanceof MCPClientError) {
output.printError(`Pre-edit hook failed: ${error.message}`);
}
else {
output.printError(`Unexpected error: ${String(error)}`);
}
return { success: false, exitCode: 1 };
}
}
};
// Post-edit subcommand
const postEditCommand = {
name: 'post-edit',
description: 'Record editing outcome for learning',
options: [
{
name: 'file',
short: 'f',
description: 'File path that was edited',
type: 'string',
required: false
},
{
name: 'success',
short: 's',
description: 'Whether the edit was successful',
type: 'boolean',
required: false
},
{
name: 'outcome',
short: 'o',
description: 'Outcome description',
type: 'string'
},
{
name: 'metrics',
short: 'm',
description: 'Performance metrics (e.g., "time:500ms,quality:0.95")',
type: 'string'
}
],
examples: [
{ command: 'claude-flow hooks post-edit -f src/utils.ts --success true', description: 'Record successful edit' },
{ command: 'claude-flow hooks post-edit -f src/api.ts --success false -o "Type error"', description: 'Record failed edit' }
],
action: async (ctx) => {
// Default file to 'unknown' for backward compatibility (env var may be empty)
const filePath = ctx.flags.file || ctx.args[0] || 'unknown';
// Default success to true for backward compatibility (PostToolUse = success, PostToolUseFailure = failure)
const success = ctx.flags.success !== undefined ? ctx.flags.success : true;
output.printInfo(`Recording outcome for: ${output.highlight(filePath)}`);
try {
// Parse metrics if provided
const metrics = {};
if (ctx.flags.metrics) {
const metricsStr = ctx.flags.metrics;
metricsStr.split(',').forEach(pair => {
const [key, value] = pair.split(':');
if (key && value) {
metrics[key.trim()] = parseFloat(value);
}
});
}
// Call MCP tool for post-edit hook
const result = await callMCPTool('hooks_post-edit', {
filePath,
success,
outcome: ctx.flags.outcome,
metrics,
timestamp: Date.now(),
});
if (ctx.flags.format === 'json') {
output.printJson(result);
return { success: true, data: result };
}
output.writeln();
output.printSuccess(`Outcome recorded for ${filePath}`);
if (result.learningUpdates) {
output.writeln();
output.writeln(output.bold('Learning Updates'));
output.printTable({
columns: [
{ key: 'metric', header: 'Metric', width: 25 },
{ key: 'value', header: 'Value', width: 15, align: 'right' }
],
data: [
{ metric: 'Patterns Updated', value: result.learningUpdates.patternsUpdated },
{ metric: 'Confidence Adjusted', value: result.learningUpdates.confidenceAdjusted },
{ metric: 'New Patterns', value: result.learningUpdates.newPatterns }
]
});
}
return { success: true, data: result };
}
catch (error) {
if (error instanceof MCPClientError) {
output.printError(`Post-edit hook failed: ${error.message}`);
}
else {
output.printError(`Unexpected error: ${String(error)}`);
}
return { success: false, exitCode: 1 };
}
}
};
// Pre-command subcommand
const preCommandCommand = {
name: 'pre-command',
description: 'Assess risk before executing a command',
options: [
{
name: 'command',
short: 'c',
description: 'Command to execute',
type: 'string',
required: true
},
{
name: 'dry-run',
short: 'd',
description: 'Only analyze, do not execute',
type: 'boolean',
default: true
}
],
examples: [
{ command: 'claude-flow hooks pre-command -c "rm -rf dist"', description: 'Assess command risk' },
{ command: 'claude-flow hooks pre-command -c "npm install lodash"', description: 'Check package install' }
],
action: async (ctx) => {
const command = ctx.flags.command || ctx.args[0];
if (!command) {
output.printError('Command is required. Use --command or -c flag.');
return { success: false, exitCode: 1 };
}
output.printInfo(`Analyzing command: ${output.highlight(command)}`);
try {
// Call MCP tool for pre-command hook
const result = await callMCPTool('hooks_pre-command', {
command,
includeAlternatives: true,
});
if (ctx.flags.format === 'json') {
output.printJson(result);
return { success: true, data: result };
}
output.writeln();
// Risk level indicator
let riskIndicator;
switch (result.riskLevel) {
case 'critical':
riskIndicator = output.error('CRITICAL');
break;
case 'high':
riskIndicator = output.error('HIGH');
break;
case 'medium':
riskIndicator = output.warning('MEDIUM');
break;
default:
riskIndicator = output.success('LOW');
}
output.printBox([
`Risk Level: ${riskIndicator}`,
`Should Proceed: ${result.shouldProceed ? output.success('Yes') : output.error('No')}`
].join('\n'), 'Risk Assessment');
if (result.risks.length > 0) {
output.writeln();
output.writeln(output.bold('Identified Risks'));
output.printTable({
columns: [
{ key: 'type', header: 'Type', width: 15 },
{ key: 'severity', header: 'Severity', width: 10 },
{ key: 'description', header: 'Description', width: 40 }
],
data: result.risks
});
}
if (result.safeAlternatives && result.safeAlternatives.length > 0) {
output.writeln();
output.writeln(output.bold('Safe Alternatives'));
output.printList(result.safeAlternatives.map(a => output.success(a)));
}
if (result.recommendations.length > 0) {
output.writeln();
output.writeln(output.bold('Recommendations'));
output.printList(result.recommendations);
}
return { success: true, data: result };
}
catch (error) {
if (error instanceof MCPClientError) {
output.printError(`Pre-command hook failed: ${error.message}`);
}
else {
output.printError(`Unexpected error: ${String(error)}`);
}
return { success: false, exitCode: 1 };
}
}
};
// Post-command subcommand
const postCommandCommand = {
name: 'post-command',
description: 'Record command execution outcome',
options: [
{
name: 'command',
short: 'c',
description: 'Command that was executed',
type: 'string',
required: true
},
{
name: 'success',
short: 's',
description: 'Whether the command succeeded',
type: 'boolean',
required: false
},
{
name: 'exit-code',
short: 'e',
description: 'Command exit code',
type: 'number',
default: 0
},
{
name: 'duration',
short: 'd',
description: 'Execution duration in milliseconds',
type: 'number'
}
],
examples: [
{ command: 'claude-flow hooks post-command -c "npm test" --success true', description: 'Record successful test run' },
{ command: 'claude-flow hooks post-command -c "npm build" --success false -e 1', description: 'Record failed build' }
],
action: async (ctx) => {
const command = ctx.flags.command || ctx.args[0];
// Default success to true for backward compatibility
const success = ctx.flags.success !== undefined ? ctx.flags.success : true;
if (!command) {
output.printError('Command is required. Use --command or -c flag.');
return { success: false, exitCode: 1 };
}
output.printInfo(`Recording command outcome: ${output.highlight(command)}`);
try {
// Call MCP tool for post-command hook
const result = await callMCPTool('hooks_post-command', {
command,
success,
exitCode: ctx.flags.exitCode || 0,
duration: ctx.flags.duration,
timestamp: Date.now(),
});
if (ctx.flags.format === 'json') {
output.printJson(result);
return { success: true, data: result };
}
output.writeln();
output.printSuccess('Command outcome recorded');
if (result.learningUpdates) {
output.writeln();
output.writeln(output.dim(`Patterns updated: ${result.learningUpdates.commandPatternsUpdated}`));
output.writeln(output.dim(`Risk assessment: ${result.learningUpdates.riskAssessmentUpdated ? 'Updated' : 'No change'}`));
}
return { success: true, data: result };
}
catch (error) {
if (error instanceof MCPClientError) {
output.printError(`Post-command hook failed: ${error.message}`);
}
else {
output.printError(`Unexpected error: ${String(error)}`);
}
return { success: false, exitCode: 1 };
}
}
};
// Route subcommand
const routeCommand = {
name: 'route',
description: 'Route task to optimal agent using learned patterns',
options: [
{
name: 'task',
short: 't',
description: 'Task description',
type: 'string',
required: true
},
{
name: 'context',
short: 'c',
description: 'Additional context',
type: 'string'
},
{
name: 'top-k',
short: 'K',
description: 'Number of top agent suggestions',
type: 'number',
default: 3
}
],
examples: [
{ command: 'claude-flow hooks route -t "Fix authentication bug"', description: 'Route task to optimal agent' },
{ command: 'claude-flow hooks route -t "Optimize database queries" -K 5', description: 'Get top 5 suggestions' }
],
action: async (ctx) => {
const task = ctx.flags.task || ctx.args[0];
const topK = ctx.flags.topK || 3;
if (!task) {
output.printError('Task description is required. Use --task or -t flag.');
return { success: false, exitCode: 1 };
}
output.printInfo(`Routing task: ${output.highlight(task)}`);
try {
// Call MCP tool for routing
const result = await callMCPTool('hooks_route', {
task,
context: ctx.flags.context,
topK,
includeEstimates: true,
});
if (ctx.flags.format === 'json') {
output.printJson(result);
return { success: true, data: result };
}
// Show routing method info
if (result.routing) {
output.writeln();
output.writeln(output.bold('Routing Method'));
const methodDisplay = result.routing.method.startsWith('semantic')
? output.success(`${result.routing.method} (${result.routing.backend || 'semantic'})`)
: 'keyword';
output.printList([
`Method: ${methodDisplay}`,
result.routing.backend ? `Backend: ${result.routing.backend}` : null,
`Latency: ${result.routing.latencyMs.toFixed(3)}ms`,
result.matchedPattern ? `Matched Pattern: ${result.matchedPattern}` : null,
].filter(Boolean));
// Show semantic matches if available
if (result.semanticMatches && result.semanticMatches.length > 0) {
output.writeln();
output.writeln(output.dim('Semantic Matches:'));
result.semanticMatches.forEach(m => {
output.writeln(` ${m.pattern}: ${(m.score * 100).toFixed(1)}%`);
});
}
}
output.writeln();
output.printBox([
`Agent: ${output.highlight(result.primaryAgent.type)}`,
`Confidence: ${(result.primaryAgent.confidence * 100).toFixed(1)}%`,
`Reason: ${result.primaryAgent.reason}`
].join('\n'), 'Primary Recommendation');
if (result.alternativeAgents.length > 0) {
output.writeln();
output.writeln(output.bold('Alternative Agents'));
output.printTable({
columns: [
{ key: 'type', header: 'Agent Type', width: 20 },
{ key: 'confidence', header: 'Confidence', width: 12, align: 'right', format: (v) => `${(Number(v) * 100).toFixed(1)}%` },
{ key: 'reason', header: 'Reason', width: 35 }
],
data: result.alternativeAgents
});
}
if (result.estimatedMetrics) {
output.writeln();
output.writeln(output.bold('Estimated Metrics'));
output.printList([
`Success Probability: ${(result.estimatedMetrics.successProbability * 100).toFixed(1)}%`,
`Estimated Duration: ${result.estimatedMetrics.estimatedDuration}`,
`Complexity: ${result.estimatedMetrics.complexity.toUpperCase()}`
]);
}
return { success: true, data: result };
}
catch (error) {
if (error instanceof MCPClientError) {
output.printError(`Routing failed: ${error.message}`);
}
else {
output.printError(`Unexpected error: ${String(error)}`);
}
return { success: false, exitCode: 1 };
}
}
};
// Explain subcommand
const explainCommand = {
name: 'explain',
description: 'Explain routing decision with transparency',
options: [
{
name: 'task',
short: 't',
description: 'Task description',
type: 'string',
required: true
},
{
name: 'agent',
short: 'a',
description: 'Agent type to explain',
type: 'string'
},
{
name: 'verbose',
short: 'v',
description: 'Verbose explanation',
type: 'boolean',
default: false
}
],
examples: [
{ command: 'claude-flow hooks explain -t "Fix authentication bug"', description: 'Explain routing decision' },
{ command: 'claude-flow hooks explain -t "Optimize queries" -a coder --verbose', description: 'Verbose explanation for specific agent' }
],
action: async (ctx) => {
const task = ctx.flags.task || ctx.args[0];
if (!task) {
output.printError('Task description is required. Use --task or -t flag.');
return { success: false, exitCode: 1 };
}
output.printInfo(`Explaining routing for: ${output.highlight(task)}`);
try {
// Call MCP tool for explanation
const result = await callMCPTool('hooks_explain', {
task,
agent: ctx.flags.agent,
verbose: ctx.flags.verbose || false,
});
if (ctx.flags.format === 'json') {
output.printJson(result);
return { success: true, data: result };
}
output.writeln();
output.writeln(output.bold('Decision Explanation'));
output.writeln();
output.writeln(result.explanation);
output.writeln();
output.printBox([
`Agent: ${output.highlight(result.decision.agent)}`,
`Confidence: ${(result.decision.confidence * 100).toFixed(1)}%`
].join('\n'), 'Final Decision');
if (result.decision.reasoning.length > 0) {
output.writeln();
output.writeln(output.bold('Reasoning Steps'));
output.printList(result.decision.reasoning.map((r, i) => `${i + 1}. ${r}`));
}
if (result.factors.length > 0) {
output.writeln();
output.writeln(output.bold('Decision Factors'));
output.printTable({
columns: [
{ key: 'factor', header: 'Factor', width: 20 },
{ key: 'weight', header: 'Weight', width: 10, align: 'right', format: (v) => `${(Number(v) * 100).toFixed(0)}%` },
{ key: 'value', header: 'Value', width: 10, align: 'right', format: (v) => Number(v).toFixed(2) },
{ key: 'impact', header: 'Impact', width: 25 }
],
data: result.factors
});
}
if (result.patterns.length > 0 && ctx.flags.verbose) {
output.writeln();
output.writeln(output.bold('Matched Patterns'));
result.patterns.forEach((p, i) => {
output.writeln();
output.writeln(`${i + 1}. ${output.highlight(p.pattern)} (${(p.matchScore * 100).toFixed(1)}% match)`);
if (p.examples.length > 0) {
output.printList(p.examples.slice(0, 3).map(e => output.dim(` ${e}`)));
}
});
}
return { success: true, data: result };
}
catch (error) {
if (error instanceof MCPClientError) {
output.printError(`Explanation failed: ${error.message}`);
}
else {
output.printError(`Unexpected error: ${String(error)}`);
}
return { success: false, exitCode: 1 };
}
}
};
// Pretrain subcommand
const pretrainCommand = {
name: 'pretrain',
description: 'Bootstrap intelligence from repository (4-step pipeline + embeddings)',
options: [
{
name: 'path',
short: 'p',
description: 'Repository path',
type: 'string',
default: '.'
},
{
name: 'depth',
short: 'd',
description: 'Analysis depth (shallow, medium, deep)',
type: 'string',
default: 'medium',
choices: ['shallow', 'medium', 'deep']
},
{
name: 'skip-cache',
description: 'Skip cached analysis',
type: 'boolean',
default: false
},
{
name: 'with-embeddings',
description: 'Index documents for semantic search during pretraining',
type: 'boolean',
default: true
},
{
name: 'embedding-model',
description: 'ONNX embedding model',
type: 'string',
default: 'Xenova/all-MiniLM-L6-v2',
choices: ['Xenova/all-MiniLM-L6-v2', 'Xenova/all-mpnet-base-v2']
},
{
name: 'file-types',
description: 'File extensions to index (comma-separated)',
type: 'string',
default: 'ts,js,py,md,json'
}
],
examples: [
{ command: 'claude-flow hooks pretrain', description: 'Pretrain with embeddings indexing' },
{ command: 'claude-flow hooks pretrain -p ../my-project --depth deep', description: 'Deep analysis of specific project' },
{ command: 'claude-flow hooks pretrain --no-with-embeddings', description: 'Skip embedding indexing' },
{ command: 'claude-flow hooks pretrain --file-types ts,tsx,js', description: 'Index only TypeScript/JS files' }
],
action: async (ctx) => {
const repoPath = ctx.flags.path || '.';
const depth = ctx.flags.depth || 'medium';
const withEmbeddings = ctx.flags['with-embeddings'] !== false && ctx.flags.withEmbeddings !== false;
const embeddingModel = (ctx.flags['embedding-model'] || ctx.flags.embeddingModel || 'Xenova/all-MiniLM-L6-v2');
const fileTypes = (ctx.flags['file-types'] || ctx.flags.fileTypes || 'ts,js,py,md,json');
output.writeln();
output.writeln(output.bold('Pretraining Intelligence (4-Step Pipeline + Embeddings)'));
output.writeln();
const steps = [
{ name: 'RETRIEVE', desc: 'Top-k memory injection with MMR diversity' },
{ name: 'JUDGE', desc: 'LLM-as-judge trajectory evaluation' },
{ name: 'DISTILL', desc: 'Extract strategy memories from trajectories' },
{ name: 'CONSOLIDATE', desc: 'Dedup, detect contradictions, prune old patterns' }
];
// Add embedding steps if enabled
if (withEmbeddings) {
steps.push({ name: 'EMBED', desc: `Index documents with ${embeddingModel} (ONNX)` }, { name: 'HYPERBOLIC', desc: 'Project to Poincaré ball for hierarchy preservation' });
}
const spinner = output.createSpinner({ text: 'Starting pretraining...', spinner: 'dots' });
try {
spinner.start();
// Display progress for each step
for (const step of steps) {
spinner.setText(`${step.name}: ${step.desc}`);
await new Promise(resolve => setTimeout(resolve, 800));
}
// Call MCP tool for pretraining. The tool currently returns
// `{ statistics: { ..., executionTime }, ... }` but earlier CLI
// versions read `result.stats` and `result.duration` (#1686). Accept
// either shape so the dashboard works whether you upgraded the tool
// or the CLI first.
const rawResult = await callMCPTool('hooks_pretrain', {
path: repoPath,
depth,
skipCache: ctx.flags.skipCache || false,
withEmbeddings,
embeddingModel,
fileTypes: fileTypes.split(',').map((t) => t.trim()),
});
spinner.succeed('Pretraining completed');
// Normalize shape: prefer `statistics`, fall back to `stats` for older tools.
// #1686 — coerce duration through safeNum so a NaN from the underlying
// pretrain pipeline surfaces as `0.0s` rather than `NaNs`.
const stats = (rawResult.statistics ?? rawResult.stats ?? {});
const durationMs = safeNum(rawResult.duration ?? rawResult.statistics?.executionTime);
const result = { ...rawResult, stats, duration: durationMs };
if (ctx.flags.format === 'json') {
output.printJson(result);
return { success: true, data: result };
}
output.writeln();
// Base stats — use ?? 0 fallbacks to keep the table readable even when
// the tool omits a counter rather than crashing on undefined.
const tableData = [
{ metric: 'Files Analyzed', value: stats.filesAnalyzed ?? 0 },
{ metric: 'Patterns Extracted', value: stats.patternsExtracted ?? 0 },
{ metric: 'Strategies Learned', value: stats.strategiesLearned ?? 0 },
{ metric: 'Trajectories Evaluated', value: stats.trajectoriesEvaluated ?? 0 },
{ metric: 'Contradictions Resolved', value: stats.contradictionsResolved ?? 0 },
];
// Add embedding stats if available
if (withEmbeddings && stats.documentsIndexed !== undefined) {
tableData.push({ metric: 'Documents Indexed', value: stats.documentsIndexed }, { metric: 'Embeddings Generated', value: stats.embeddingsGenerated ?? 0 }, { metric: 'Hyperbolic Projections', value: stats.hyperbolicProjections ?? 0 });
}
tableData.push({ metric: 'Duration', value: `${(durationMs / 1000).toFixed(1)}s` });
output.printTable({
columns: [
{ key: 'metric', header: 'Metric', width: 30 },
{ key: 'value', header: 'Value', width: 15, align: 'right' }
],
data: tableData
});
output.writeln();
output.printSuccess('Repository intelligence bootstrapped successfully');
if (withEmbeddings) {
output.writeln(output.dim(' Semantic search enabled: Use "embeddings search -q <query>" to search'));
}
output.writeln(output.dim(' Next step: Run "claude-flow hooks build-agents" to generate optimized configs'));
return { success: true, data: result };
}
catch (error) {
spinner.fail('Pretraining failed');
if (error instanceof MCPClientError) {
output.printError(`Pretraining error: ${error.message}`);
}
else {
output.printError(`Unexpected error: ${String(error)}`);
}
return { success: false, exitCode: 1 };
}
}
};
// Build agents subcommand
const buildAgentsCommand = {
name: 'build-agents',
description: 'Generate optimized agent configs from pretrain data',
options: [
{
name: 'output',
short: 'o',
description: 'Output directory for agent configs',
type: 'string',
default: './agents'
},
{
name: 'focus',
short: 'f',
description: 'Focus area (v3-implementation, security, performance, all)',
type: 'string',
default: 'all'
},
{
name: 'config-format',
description: 'Config format (yaml, json)',
type: 'string',
default: 'yaml',
choices: ['yaml', 'json']
}
],
examples: [
{ command: 'claude-flow hooks build-agents', description: 'Build all agent configs' },
{ command: 'claude-flow hooks build-agents --focus security -o ./config/agents', description: 'Build security-focused configs' }
],
action: async (ctx) => {
const output_dir = ctx.flags.output || './agents';
const focus = ctx.flags.focus || 'all';
const configFormat = ctx.flags.configFormat || 'yaml';
output.printInfo(`Building agent configs (focus: ${output.highlight(focus)})`);
const spinner = output.createSpinner({ text: 'Generating configs...', spinner: 'dots' });
try {
spinner.start();
// Call MCP tool for building agents
const result = await callMCPTool('hooks_build-agents', {
outputDir: output_dir,
focus,
format: configFormat,
includePretrained: true,
});
spinner.succeed(`Generated ${result.agents.length} agent configs`);
if (ctx.flags.format === 'json') {
output.printJson(result);
return { success: true, data: result };
}
output.writeln();
output.writeln(output.bold('Generated Agent Configs'));
output.printTable({
columns: [
{ key: 'type', header: 'Agent Type', width: 20 },
{ key: 'configFile', header: 'Config File', width: 30 },
{ key: 'capabilities', header: 'Capabilities', width: 10, align: 'right', format: (v) => String(Array.isArray(v) ? v.length : 0) }
],
data: result.agents
});
output.writeln();
output.printTable({
columns: [
{ key: 'metric', header: 'Metric', width: 30 },
{ key: 'value', header: 'Value', width: 15, align: 'right' }
],
data: [
{ metric: 'Configs Generated', value: result.stats.configsGenerated },
{ metric: 'Patterns Applied', value: result.stats.patternsApplied },
{ metric: 'Optimizations Included', value: result.stats.optimizationsIncluded }
]
});
output.writeln();
output.printSuccess(`Agent configs saved to ${output_dir}`);
return { success: true, data: result };
}
catch (error) {
spinner.fail('Agent config generation failed');
if (error instanceof MCPClientError) {
output.printError(`Build agents error: ${error.message}`);
}
else {
output.printError(`Unexpected error: ${String(error)}`);
}
return { success: false, exitCode: 1 };
}
}
};
// Metrics subcommand
const metricsCommand = {
name: 'metrics',
description: 'View learning metrics dashboard',
options: [
{
name: 'period',
short: 'p',
description: 'Time period (1h, 24h, 7d, 30d, all)',
type: 'string',
default: '24h'
},
{
name: 'v3-dashboard',
description: 'Show V3 performance dashboard',
type: 'boolean',
default: false
},
{
name: 'category',
short: 'c',
description: 'Metric category (patterns, agents, commands, performance)',
type: 'string'
}
],
examples: [
{ command: 'claude-flow hooks metrics', description: 'View 24h metrics' },
{ command: 'claude-flow hooks metrics --period 7d --v3-dashboard', description: 'V3 metrics for 7 days' }
],
action: async (ctx) => {
const period = ctx.flags.period || '24h';
const v3Dashboard = ctx.flags.v3Dashboard;
output.writeln();
output.writeln(output.bold(`Learning Metrics Dashboard (${period})`));
output.writeln();
try {
// Call MCP tool for metrics. The tool returns `{ summary, routing,
// edits, commands }` (see MetricsResult in v3/mcp/tools/hooks-tools.ts)
// but earlier CLI versions expected `{ patterns, agents, commands.avgRiskScore }`.
// Accept the union and normalize below — without the `?? 0` guards the
// dashboard crashed with "Cannot read properties of null (reading 'toFixed')"
// whenever a counter was missing (#1686).
const rawMetrics = await callMCPTool('hooks_metrics', {
period,
includeV3: v3Dashboard,
category: ctx.flags.category,
});
// Normalize across both shapes; default every numeric to 0 so toFixed
// never sees null/undefined. #1686 — also coerce NaN through `safeNum`
// because `?? 0` only catches null/undefined; an upstream NaN would
// still land in `.toFixed(...)` and surface as `"NaN"`.
const totalPatterns = safeNum(rawMetrics.patterns?.total ?? rawMetrics.summary?.patternsLearned);
const successfulPatterns = safeNum(rawMetrics.patterns?.successful ?? Math.round(safeNum(rawMetrics.summary?.successRate) * totalPatterns));
const failedPatterns = Math.max(0, safeNum(rawMetrics.patterns?.failed ?? totalPatterns - successfulPatterns));
const avgConfidence = safeNum(rawMetrics.patterns?.avgConfidence ?? rawMetrics.summary?.avgQuality);
const routingAccuracy = safeNum(rawMetrics.agents?.routingAccuracy ?? rawMetrics.routing?.avgConfidence);
const totalRoutes = safeNum(rawMetrics.agents?.totalRoutes ?? rawMetrics.routing?.totalRoutes);
const topAgent = rawMetrics.agents?.topAgent ?? rawMetrics.routing?.topAgents?.[0]?.agent ?? 'n/a';
const totalCommands = safeNum(rawMetrics.commands?.totalExecuted ?? rawMetrics.commands?.totalCommands);
const commandSuccessRate = safeNum(rawMetrics.commands?.successRate);
const avgRiskScore = safeNum(rawMetrics.commands?.avgRiskScore ?? rawMetrics.commands?.avgExecutionTime);
const result = {
...rawMetrics,
patterns: { total: totalPatterns, successful: successfulPatterns, failed: failedPatterns, avgConfidence },
agents: { routingAccuracy, totalRoutes, topAgent },
commands: { totalExecuted: totalCommands, successRate: commandSuccessRate, avgRiskScore },
};
if (ctx.flags.format === 'json') {
output.printJson(result);
return { success: true, data: result };
}
// Patterns section
output.writeln(output.bold('📊 Pattern Learning'));
output.printTable({
columns: [
{ key: 'metric', header: 'Metric', width: 25 },
{ key: 'value', header: 'Value', width: 20, align: 'right' }
],
data: [
{ metric: 'Total Patterns', value: totalPatterns },
{ metric: 'Successful', value: output.success(String(successfulPatterns)) },
{ metric: 'Failed', value: output.error(String(failedPatterns)) },
{ metric: 'Avg Confidence', value: `${(avgConfidence * 100).toFixed(1)}%` }
]
});
output.writeln();
// Agent routing section
output.writeln(output.bold('🤖 Agent Routing'));
output.printTable({
columns: [
{ key: 'metric', header: 'Metric', width: 25 },
{ key: 'value', header: 'Value', width: 20, align: 'right' }
],
data: [
{ metric: 'Routing Accuracy', value: `${(routingAccuracy * 100).toFixed(1)}%` },
{ metric: 'Total Routes', value: totalRoutes },
{ metric: 'Top Agent', value: output.highlight(topAgent) }
]
});
output.writeln();
// Command execution section
output.writeln(output.bold('⚡ Command Execution'));
output.printTable({
columns: [
{ key: 'metric', header: 'Metric', width: 25 },
{ key: 'value', header: 'Value', width: 20, align: 'right' }
],
data: [
{ metric: 'Total Executed', value: totalCommands },
{ metric: 'Success Rate', value: `${(commandSuccessRate * 100).toFixed(1)}%` },
{ metric: 'Avg Risk Score', value: avgRiskScore.toFixed(2) }
]
});
if