370 lines
14 KiB
JavaScript
370 lines
14 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
/**
|
|
* health-monitor.js - Automated health monitoring system for .agents
|
|
* Part of LCBP3-DMS Phase 3 enhancements
|
|
*/
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const { execSync } = require('child_process');
|
|
|
|
// Configuration
|
|
const BASE_DIR = path.resolve(__dirname, '../..');
|
|
const AGENTS_DIR = path.join(BASE_DIR, '.agents');
|
|
const HEALTH_LOG_PATH = path.join(AGENTS_DIR, 'logs', 'health.log');
|
|
const HEALTH_REPORT_PATH = path.join(AGENTS_DIR, 'reports', 'health-report.json');
|
|
|
|
// Ensure directories exist
|
|
[ path.dirname(HEALTH_LOG_PATH), path.dirname(HEALTH_REPORT_PATH) ].forEach(dir => {
|
|
if (!fs.existsSync(dir)) {
|
|
fs.mkdirSync(dir, { recursive: true });
|
|
}
|
|
});
|
|
|
|
// Health monitoring class
|
|
class HealthMonitor {
|
|
constructor() {
|
|
this.startTime = new Date();
|
|
this.metrics = {
|
|
timestamp: this.startTime.toISOString(),
|
|
version: '1.8.6',
|
|
checks: {},
|
|
summary: {
|
|
total_checks: 0,
|
|
passed_checks: 0,
|
|
failed_checks: 0,
|
|
warnings: 0,
|
|
overall_health: 'unknown'
|
|
}
|
|
};
|
|
}
|
|
|
|
log(message, level = 'info') {
|
|
const timestamp = new Date().toISOString();
|
|
const logEntry = `[${timestamp}] [${level.toUpperCase()}] ${message}\n`;
|
|
|
|
// Console output with colors
|
|
const colors = {
|
|
info: '\x1b[36m', // Cyan
|
|
pass: '\x1b[32m', // Green
|
|
fail: '\x1b[31m', // Red
|
|
warn: '\x1b[33m', // Yellow
|
|
reset: '\x1b[0m'
|
|
};
|
|
|
|
const color = colors[level] || colors.info;
|
|
console.log(`${color}${logEntry.trim()}${colors.reset}`);
|
|
|
|
// File logging
|
|
fs.appendFileSync(HEALTH_LOG_PATH, logEntry);
|
|
}
|
|
|
|
checkDirectoryExists(dirPath, checkName) {
|
|
this.metrics.summary.total_checks++;
|
|
const exists = fs.existsSync(dirPath);
|
|
|
|
this.metrics.checks[checkName] = {
|
|
type: 'directory_exists',
|
|
status: exists ? 'pass' : 'fail',
|
|
path: dirPath,
|
|
message: exists ? 'Directory exists' : 'Directory missing'
|
|
};
|
|
|
|
if (exists) {
|
|
this.metrics.summary.passed_checks++;
|
|
this.log(`${checkName}: PASS - Directory exists`, 'pass');
|
|
} else {
|
|
this.metrics.summary.failed_checks++;
|
|
this.log(`${checkName}: FAIL - Directory missing: ${dirPath}`, 'fail');
|
|
}
|
|
|
|
return exists;
|
|
}
|
|
|
|
checkFileExists(filePath, checkName) {
|
|
this.metrics.summary.total_checks++;
|
|
const exists = fs.existsSync(filePath);
|
|
|
|
this.metrics.checks[checkName] = {
|
|
type: 'file_exists',
|
|
status: exists ? 'pass' : 'fail',
|
|
path: filePath,
|
|
message: exists ? 'File exists' : 'File missing'
|
|
};
|
|
|
|
if (exists) {
|
|
this.metrics.summary.passed_checks++;
|
|
this.log(`${checkName}: PASS - File exists`, 'pass');
|
|
} else {
|
|
this.metrics.summary.failed_checks++;
|
|
this.log(`${checkName}: FAIL - File missing: ${filePath}`, 'fail');
|
|
}
|
|
|
|
return exists;
|
|
}
|
|
|
|
checkFileVersion(filePath, expectedVersion, checkName) {
|
|
this.metrics.summary.total_checks++;
|
|
|
|
if (!fs.existsSync(filePath)) {
|
|
this.metrics.summary.failed_checks++;
|
|
this.metrics.checks[checkName] = {
|
|
type: 'version_check',
|
|
status: 'fail',
|
|
path: filePath,
|
|
message: 'File does not exist'
|
|
};
|
|
this.log(`${checkName}: FAIL - File not found: ${filePath}`, 'fail');
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
const versionMatch = content.match(/v?(\d+\.\d+\.\d+)/);
|
|
const actualVersion = versionMatch ? versionMatch[1] : 'not_found';
|
|
const versionMatches = actualVersion === expectedVersion;
|
|
|
|
this.metrics.checks[checkName] = {
|
|
type: 'version_check',
|
|
status: versionMatches ? 'pass' : 'fail',
|
|
path: filePath,
|
|
expected_version: expectedVersion,
|
|
actual_version: actualVersion,
|
|
message: versionMatches ? 'Version matches' : `Version mismatch (expected ${expectedVersion}, found ${actualVersion})`
|
|
};
|
|
|
|
if (versionMatches) {
|
|
this.metrics.summary.passed_checks++;
|
|
this.log(`${checkName}: PASS - Version ${actualVersion}`, 'pass');
|
|
} else {
|
|
this.metrics.summary.failed_checks++;
|
|
this.log(`${checkName}: FAIL - Version mismatch (expected ${expectedVersion}, found ${actualVersion})`, 'fail');
|
|
}
|
|
|
|
return versionMatches;
|
|
} catch (error) {
|
|
this.metrics.summary.failed_checks++;
|
|
this.metrics.checks[checkName] = {
|
|
type: 'version_check',
|
|
status: 'fail',
|
|
path: filePath,
|
|
message: `Error reading file: ${error.message}`
|
|
};
|
|
this.log(`${checkName}: FAIL - Error reading file: ${error.message}`, 'fail');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
checkSkillHealth() {
|
|
this.log('Checking skill health...', 'info');
|
|
const skillsDir = path.join(AGENTS_DIR, 'skills');
|
|
|
|
if (!fs.existsSync(skillsDir)) {
|
|
this.log('Skills directory not found', 'fail');
|
|
return;
|
|
}
|
|
|
|
const skillDirs = fs.readdirSync(skillsDir).filter(item => {
|
|
const itemPath = path.join(skillsDir, item);
|
|
return fs.statSync(itemPath).isDirectory();
|
|
});
|
|
|
|
this.metrics.checks['skill_count'] = {
|
|
type: 'skill_count',
|
|
status: skillDirs.length >= 20 ? 'pass' : 'warn',
|
|
count: skillDirs.length,
|
|
expected: 20,
|
|
message: `Found ${skillDirs.length} skills (expected at least 20)`
|
|
};
|
|
|
|
if (skillDirs.length >= 20) {
|
|
this.metrics.summary.passed_checks++;
|
|
this.log(`Skill count: PASS - Found ${skillDirs.length} skills`, 'pass');
|
|
} else {
|
|
this.metrics.summary.warnings++;
|
|
this.log(`Skill count: WARN - Only ${skillDirs.length} skills found (expected at least 20)`, 'warn');
|
|
}
|
|
|
|
// Check individual skills
|
|
let healthySkills = 0;
|
|
skillDirs.forEach(skillDir => {
|
|
const skillPath = path.join(skillsDir, skillDir);
|
|
const skillMdPath = path.join(skillPath, 'SKILL.md');
|
|
|
|
if (fs.existsSync(skillMdPath)) {
|
|
try {
|
|
const content = fs.readFileSync(skillMdPath, 'utf8');
|
|
const hasName = content.includes('name:');
|
|
const hasDescription = content.includes('description:');
|
|
const hasVersion = content.includes('version:');
|
|
const hasRole = content.includes('## Role');
|
|
const hasTask = content.includes('## Task');
|
|
|
|
const isHealthy = hasName && hasDescription && hasVersion && hasRole && hasTask;
|
|
if (isHealthy) healthySkills++;
|
|
|
|
this.metrics.checks[`skill_${skillDir}_health`] = {
|
|
type: 'skill_health',
|
|
status: isHealthy ? 'pass' : 'fail',
|
|
skill: skillDir,
|
|
has_name: hasName,
|
|
has_description: hasDescription,
|
|
has_version: hasVersion,
|
|
has_role: hasRole,
|
|
has_task: hasTask,
|
|
message: isHealthy ? 'Skill is healthy' : 'Skill has missing sections'
|
|
};
|
|
} catch (error) {
|
|
this.metrics.checks[`skill_${skillDir}_health`] = {
|
|
type: 'skill_health',
|
|
status: 'fail',
|
|
skill: skillDir,
|
|
message: `Error reading skill: ${error.message}`
|
|
};
|
|
}
|
|
}
|
|
});
|
|
|
|
this.metrics.summary.total_checks++;
|
|
if (healthySkills === skillDirs.length) {
|
|
this.metrics.summary.passed_checks++;
|
|
this.log(`Individual skills: PASS - All ${healthySkills} skills are healthy`, 'pass');
|
|
} else {
|
|
this.metrics.summary.failed_checks++;
|
|
this.log(`Individual skills: FAIL - Only ${healthySkills}/${skillDirs.length} skills are healthy`, 'fail');
|
|
}
|
|
}
|
|
|
|
checkWorkflowHealth() {
|
|
this.log('Checking workflow health...', 'info');
|
|
const workflowsDir = path.join(BASE_DIR, '.windsurf', 'workflows');
|
|
|
|
if (!fs.existsSync(workflowsDir)) {
|
|
this.log('Workflows directory not found', 'fail');
|
|
return;
|
|
}
|
|
|
|
const workflowFiles = fs.readdirSync(workflowsDir).filter(file => file.endsWith('.md'));
|
|
|
|
this.metrics.checks['workflow_count'] = {
|
|
type: 'workflow_count',
|
|
status: workflowFiles.length >= 20 ? 'pass' : 'warn',
|
|
count: workflowFiles.length,
|
|
expected: 20,
|
|
message: `Found ${workflowFiles.length} workflows (expected at least 20)`
|
|
};
|
|
|
|
if (workflowFiles.length >= 20) {
|
|
this.metrics.summary.passed_checks++;
|
|
this.log(`Workflow count: PASS - Found ${workflowFiles.length} workflows`, 'pass');
|
|
} else {
|
|
this.metrics.summary.warnings++;
|
|
this.log(`Workflow count: WARN - Only ${workflowFiles.length} workflows found (expected at least 20)`, 'warn');
|
|
}
|
|
}
|
|
|
|
calculateOverallHealth() {
|
|
const { total_checks, passed_checks, failed_checks, warnings } = this.metrics.summary;
|
|
|
|
if (failed_checks === 0) {
|
|
this.metrics.summary.overall_health = warnings === 0 ? 'excellent' : 'good';
|
|
} else if (failed_checks <= total_checks * 0.1) {
|
|
this.metrics.summary.overall_health = 'fair';
|
|
} else {
|
|
this.metrics.summary.overall_health = 'poor';
|
|
}
|
|
|
|
this.log(`Overall health: ${this.metrics.summary.overall_health}`, 'info');
|
|
}
|
|
|
|
generateReport() {
|
|
const report = {
|
|
...this.metrics,
|
|
duration: new Date() - this.startTime,
|
|
environment: {
|
|
node_version: process.version,
|
|
platform: process.platform,
|
|
agents_dir: AGENTS_DIR
|
|
}
|
|
};
|
|
|
|
fs.writeFileSync(HEALTH_REPORT_PATH, JSON.stringify(report, null, 2));
|
|
this.log(`Health report saved to: ${HEALTH_REPORT_PATH}`, 'info');
|
|
|
|
return report;
|
|
}
|
|
|
|
async runFullHealthCheck() {
|
|
this.log('Starting comprehensive health check...', 'info');
|
|
this.log(`Base directory: ${BASE_DIR}`, 'info');
|
|
|
|
// Core directory checks
|
|
this.checkDirectoryExists(AGENTS_DIR, 'agents_directory');
|
|
this.checkDirectoryExists(path.join(AGENTS_DIR, 'skills'), 'skills_directory');
|
|
this.checkDirectoryExists(path.join(AGENTS_DIR, 'scripts'), 'scripts_directory');
|
|
this.checkDirectoryExists(path.join(AGENTS_DIR, 'rules'), 'rules_directory');
|
|
this.checkDirectoryExists(path.join(BASE_DIR, '.windsurf', 'workflows'), 'workflows_directory');
|
|
|
|
// Core file checks
|
|
this.checkFileExists(path.join(AGENTS_DIR, 'README.md'), 'readme_file');
|
|
this.checkFileExists(path.join(AGENTS_DIR, 'skills', 'VERSION'), 'skills_version_file');
|
|
this.checkFileExists(path.join(AGENTS_DIR, 'skills', 'skills.md'), 'skills_documentation');
|
|
|
|
// Version consistency checks
|
|
this.checkFileVersion(path.join(AGENTS_DIR, 'README.md'), '1.8.6', 'readme_version');
|
|
this.checkFileVersion(path.join(AGENTS_DIR, 'skills', 'VERSION'), '1.8.6', 'skills_version_file_version');
|
|
this.checkFileVersion(path.join(AGENTS_DIR, 'skills', 'skills.md'), '1.8.6', 'skills_documentation_version');
|
|
this.checkFileVersion(path.join(AGENTS_DIR, 'rules', '00-project-context.md'), '1.8.6', 'project_context_version');
|
|
|
|
// Script availability checks
|
|
this.checkFileExists(path.join(AGENTS_DIR, 'scripts', 'bash', 'validate-versions.sh'), 'bash_version_script');
|
|
this.checkFileExists(path.join(AGENTS_DIR, 'scripts', 'bash', 'audit-skills.sh'), 'bash_audit_script');
|
|
this.checkFileExists(path.join(AGENTS_DIR, 'scripts', 'bash', 'sync-workflows.sh'), 'bash_sync_script');
|
|
this.checkFileExists(path.join(AGENTS_DIR, 'scripts', 'powershell', 'validate-versions.ps1'), 'powershell_version_script');
|
|
this.checkFileExists(path.join(AGENTS_DIR, 'scripts', 'powershell', 'audit-skills.ps1'), 'powershell_audit_script');
|
|
|
|
// Detailed health checks
|
|
this.checkSkillHealth();
|
|
this.checkWorkflowHealth();
|
|
|
|
// Calculate overall health
|
|
this.calculateOverallHealth();
|
|
|
|
// Generate report
|
|
const report = this.generateReport();
|
|
|
|
// Summary
|
|
this.log('=== Health Check Summary ===', 'info');
|
|
this.log(`Total checks: ${this.metrics.summary.total_checks}`, 'info');
|
|
this.log(`Passed: ${this.metrics.summary.passed_checks}`, 'pass');
|
|
this.log(`Failed: ${this.metrics.summary.failed_checks}`, this.metrics.summary.failed_checks > 0 ? 'fail' : 'info');
|
|
this.log(`Warnings: ${this.metrics.summary.warnings}`, 'warn');
|
|
this.log(`Overall health: ${this.metrics.summary.overall_health}`, 'info');
|
|
this.log(`Duration: ${new Date() - this.startTime}ms`, 'info');
|
|
|
|
return report;
|
|
}
|
|
}
|
|
|
|
// CLI interface
|
|
async function main() {
|
|
const monitor = new HealthMonitor();
|
|
|
|
try {
|
|
const report = await monitor.runFullHealthCheck();
|
|
process.exit(report.summary.failed_checks > 0 ? 1 : 0);
|
|
} catch (error) {
|
|
console.error('Health check failed:', error);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
// Export for use in other modules
|
|
module.exports = { HealthMonitor };
|
|
|
|
// Run if called directly
|
|
if (require.main === module) {
|
|
main();
|
|
}
|