UNPKG

claude-flow-novice

Version:

Claude Flow Novice - Advanced orchestration platform for multi-agent AI workflows with CFN Loop architecture Includes Local RuVector Accelerator and all CFN skills for complete functionality.

288 lines (287 loc) 11 kB
/** * Promotion SLA Enforcer * * Background job to enforce 48-hour SLA for skill promotion. * Checks for stale skills in staging and auto-promotes or notifies based on config. * Part of Task 1.2: Staging → Production Promotion Workflow * * Features: * - Detects skills >48h in staging * - Auto-promotes stale skills (optional) * - Sends notifications for SLA breaches * - Cron-job compatible * - Monitoring dashboard data * * @example * ```typescript * // Run as cron job * const enforcer = new PromotionSLAEnforcer(dbService, { * autoPromote: true, * notifyStale: true, * }); * * await enforcer.enforceSLA(); * ``` * * @example * ```bash * # Cron job (daily at 9am) * 0 9 * * * cd /path/to/project && npx ts-node src/jobs/promotion-sla-enforcer.ts * ``` */ import { DatabaseService } from '../lib/database-service.js'; import { createLogger } from '../lib/logging.js'; import { SkillPromotionService } from '../services/skill-promotion.js'; const logger = createLogger('promotion-sla-enforcer'); /** * Promotion SLA Enforcer */ export class PromotionSLAEnforcer { dbService; promotionService; config; constructor(dbService, config = {}){ this.dbService = dbService; this.promotionService = new SkillPromotionService(dbService, { slaThresholdHours: config.slaThresholdHours || 48 }); // Default configuration this.config = { autoPromote: config.autoPromote ?? false, notifyStale: config.notifyStale ?? true, slaThresholdHours: config.slaThresholdHours ?? 48, dryRun: config.dryRun ?? false, enableGitCommit: config.enableGitCommit ?? true, enableAutoDeploy: config.enableAutoDeploy ?? false }; logger.info('SLA Enforcer initialized', { config: this.config }); } /** * Enforce SLA for skill promotions */ async enforceSLA() { const startTime = Date.now(); logger.info('Starting SLA enforcement', { slaThresholdHours: this.config.slaThresholdHours, autoPromote: this.config.autoPromote, dryRun: this.config.dryRun }); try { // 1. Find stale skills (>48 hours in staging) const staleSkills = await this.promotionService.checkStaleness(); if (staleSkills.length === 0) { logger.info('No stale skills found'); return { staleSkillsFound: 0, promoted: 0, notified: 0, enforcedAt: new Date(), durationMs: Date.now() - startTime }; } logger.warn('Stale skills detected', { count: staleSkills.length, skills: staleSkills.map((s)=>({ name: s.name, ageHours: s.ageHours, slaBreachHours: s.slaBreachHours })) }); // 2. Process each stale skill let promoted = 0; let notified = 0; const errors = []; for (const skill of staleSkills){ try { await this.processStaleSkill(skill); // Track promotion count if (this.config.autoPromote && !this.config.dryRun) { promoted++; } // Track notification count if (this.config.notifyStale) { notified++; } } catch (error) { logger.error('Failed to process stale skill', { skillName: skill.name, error }); errors.push({ skillName: skill.name, error: error instanceof Error ? error.message : String(error) }); } } // 3. Record enforcement in database await this.recordEnforcement(staleSkills.length, promoted, notified); const result = { staleSkillsFound: staleSkills.length, promoted, notified, errors: errors.length > 0 ? errors : undefined, enforcedAt: new Date(), durationMs: Date.now() - startTime }; logger.info('SLA enforcement complete', result); // 4. Generate summary report this.printSummary(result); return result; } catch (error) { logger.error('SLA enforcement failed', { error }); throw error; } } /** * Process a single stale skill */ async processStaleSkill(skill) { logger.info('Processing stale skill', { skillName: skill.name, ageHours: skill.ageHours, slaBreachHours: skill.slaBreachHours }); // 1. Send notification if enabled if (this.config.notifyStale) { await this.notifyStaleSkill(skill); } // 2. Auto-promote if enabled if (this.config.autoPromote) { if (this.config.dryRun) { logger.info('DRY RUN: Would auto-promote stale skill', { skillName: skill.name }); } else { logger.info('Auto-promoting stale skill', { skillName: skill.name }); const result = await this.promotionService.promoteSkill(skill.stagingPath, { autoDeploy: this.config.enableAutoDeploy, gitCommit: this.config.enableGitCommit, notify: false, promotedBy: 'sla-enforcer' }); if (!result.success) { logger.error('Auto-promotion failed', { skillName: skill.name, error: result.error }); throw new Error(result.error); } logger.info('Auto-promotion successful', { skillName: skill.name, productionPath: result.productionPath, deploymentId: result.deploymentId }); } } } /** * Send notification for stale skill */ async notifyStaleSkill(skill) { // Console notification console.log(` ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ⚠️ SLA BREACH DETECTED ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Skill: ${skill.name} Age: ${skill.ageHours} hours SLA Threshold: ${this.config.slaThresholdHours} hours SLA Breach: ${skill.slaBreachHours} hours over threshold Location: ${skill.stagingPath} Auto-Promote: ${this.config.autoPromote ? 'YES' : 'NO'} ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ `); // Future: Add webhook, email, Slack notifications logger.info('Stale skill notification sent', { skillName: skill.name }); } /** * Record enforcement in database */ async recordEnforcement(staleSkillsFound, promoted, notified) { try { const query = ` INSERT INTO sla_enforcements (enforced_at, stale_skills_found, promoted, notified) VALUES (?, ?, ?, ?) `; const adapter = this.dbService.getAdapter('sqlite'); await adapter.query(query, [ new Date().toISOString(), staleSkillsFound, promoted, notified ]); logger.debug('Enforcement recorded in database', { staleSkillsFound, promoted, notified }); } catch (error) { // Log error but don't fail enforcement logger.error('Failed to record enforcement in database', { error }); } } /** * Print summary report */ printSummary(result) { console.log(` ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ SLA ENFORCEMENT SUMMARY ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Stale Skills Found: ${result.staleSkillsFound} Auto-Promoted: ${result.promoted} Notifications Sent: ${result.notified} Errors: ${result.errors?.length || 0} Duration: ${result.durationMs}ms Enforced At: ${result.enforcedAt.toISOString()} ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ `); if (result.errors && result.errors.length > 0) { console.log('Errors:'); for (const error of result.errors){ console.log(` - ${error.skillName}: ${error.error}`); } console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); } } } /** * CLI entry point for running as standalone job */ async function main() { // Parse command line arguments const args = process.argv.slice(2); const config = { autoPromote: args.includes('--auto-promote'), notifyStale: !args.includes('--no-notify'), dryRun: args.includes('--dry-run'), enableGitCommit: !args.includes('--no-git-commit'), enableAutoDeploy: args.includes('--auto-deploy') }; // Initialize database service const dbService = new DatabaseService({ type: 'sqlite', path: process.env.CFN_DB_PATH || './data/cfn.db' }); // Run SLA enforcer const enforcer = new PromotionSLAEnforcer(dbService, config); try { const result = await enforcer.enforceSLA(); process.exit(result.errors && result.errors.length > 0 ? 1 : 0); } catch (error) { logger.error('SLA enforcer failed', { error }); process.exit(1); } } // Run if executed directly if (require.main === module) { main().catch((error)=>{ console.error('Fatal error:', error); process.exit(1); }); } //# sourceMappingURL=promotion-sla-enforcer.js.map