UNPKG

claude-flow-novice

Version:

Claude Flow Novice - Advanced orchestration platform for multi-agent AI workflows with CFN Loop architecture Includes CodeSearch (hybrid SQLite + pgvector), mem0/memgraph specialists, and all CFN skills.

275 lines (239 loc) 9.48 kB
/** * Main Skill Propagator - orchestrates the skill update propagation process */ import type { SkillPropagator as ISkillPropagator, SkillPropagationOptions, PropagationResult, FileSystemAdapter, DatabaseAdapter, Logger, } from './types'; import { SkillValidator } from './skill-validator'; import { SkillMetadataParser } from './metadata-parser'; import { VersionManager } from './version-manager'; export class SkillPropagator implements ISkillPropagator { private validator: SkillValidator; private metadataParser: SkillMetadataParser; constructor( private fs: FileSystemAdapter, private db: DatabaseAdapter, private logger: Logger ) { this.validator = new SkillValidator(fs, db); this.metadataParser = new SkillMetadataParser(); } /** * Main propagation workflow */ async propagate(options: SkillPropagationOptions): Promise<PropagationResult> { const startTime = new Date().toISOString(); this.logger.info('========================================'); this.logger.info('Propagate Skill Update'); this.logger.info('========================================'); this.logger.info(`Skill: ${options.skillName}`); this.logger.info(`New Version: ${options.newVersion}`); this.logger.info(`Update Path: ${options.updatePath}`); this.logger.info(`Change Type: ${options.changeType || 'patch'}`); this.logger.info('========================================'); // Step 1: Validate inputs this.logger.info('Step 1: Validating inputs'); const validationResult = await this.validator.validateParameters(options); if (!validationResult.valid) { throw new Error(`Validation failed:\n${validationResult.errors.join('\n')}`); } this.logger.success('Input validation passed'); // Step 2: Look up existing skill this.logger.info('Step 2: Looking up existing skill'); const skillInfo = await this.getSkillInfo(options.skillName); if (!skillInfo) { throw new Error(`Skill not found in database: ${options.skillName}`); } this.logger.info(`Found skill: ID=${skillInfo.id}, Current Version=${skillInfo.version}`); // Step 3: Validate version increment this.logger.info('Step 3: Validating version increment'); const changeType = options.changeType || 'patch'; const versionValidation = await this.validator.validateVersionIncrement( skillInfo.version, options.newVersion, changeType ); if (!versionValidation.valid) { throw new Error(`Version validation failed:\n${versionValidation.errors.join('\n')}`); } this.logger.success(`Version increment validated: ${skillInfo.version} → ${options.newVersion} (${changeType})`); // Step 4: Calculate new content hash this.logger.info('Step 4: Calculating new content hash'); const newHash = await this.fs.calculateHash(options.updatePath); this.logger.debug(`New Hash: ${newHash}`); const hashValidation = this.validator.validateContentHashChanged(skillInfo.content_hash, newHash); if (!hashValidation.valid) { throw new Error(`Content validation failed:\n${hashValidation.errors.join('\n')}`); } this.logger.success('Content hash calculated and differs from current'); // Step 5: Parse frontmatter metadata this.logger.info('Step 5: Parsing frontmatter metadata from updated skill'); const updateFileContent = await this.fs.readFile(options.updatePath); const metadata = this.metadataParser.parse(updateFileContent); const metadataValidation = this.metadataParser.validate(metadata); if (!metadataValidation.valid) { this.logger.warning(`Metadata validation warnings:\n${metadataValidation.errors.join('\n')}`); } // Use new metadata or fall back to existing const newTags = metadata.tags ? JSON.stringify(metadata.tags) : skillInfo.tags; const newCategory = metadata.category || skillInfo.category; const newOwner = metadata.owner || skillInfo.owner; const newApprovalLevel = metadata.approval_level || skillInfo.approval_level; // Step 6: Update skill record this.logger.info('Step 6: Updating skill record in database'); await this.updateSkillRecord(skillInfo.id, { version: options.newVersion, content_hash: newHash, content_path: options.updatePath, tags: newTags, category: newCategory, owner: newOwner, approval_level: newApprovalLevel, }); this.logger.success('Skill record updated with refreshed metadata'); // Step 7: Record approval history this.logger.info('Step 7: Recording approval history'); await this.recordApprovalHistory(skillInfo.id, options.newVersion, changeType); this.logger.success('Approval history recorded'); // Step 8: Notify affected agents (optional) let affectedAgents: string[] = []; if (options.notifyAgents || options.enableAgentNotifications) { this.logger.info('Step 8: Notifying affected agents'); affectedAgents = await this.notifyAffectedAgents(skillInfo.id, options.skillName); } else { this.logger.info('Step 8: Agent notification disabled (skipping)'); } // Success summary this.logger.info('=========================================='); this.logger.success('Skill Update Propagated Successfully'); this.logger.info('=========================================='); this.logger.info(`Skill Name: ${options.skillName}`); this.logger.info(`Version: ${skillInfo.version} → ${options.newVersion}`); this.logger.info(`Change Type: ${changeType}`); this.logger.info(`Content Hash: ${newHash.substring(0, 16)}...`); this.logger.info(`Update Path: ${options.updatePath}`); this.logger.info('Metadata Refreshed: Yes'); this.logger.info(` Tags: ${newTags}`); this.logger.info(` Category: ${newCategory}`); this.logger.info(` Owner: ${newOwner}`); this.logger.info(` Approval Level: ${newApprovalLevel}`); this.logger.info('=========================================='); return { success: true, skillName: options.skillName, newVersion: options.newVersion, oldVersion: skillInfo.version, changeType: VersionManager.compareVersions(skillInfo.version, options.newVersion), contentHash: newHash, metadata: { tags: newTags, category: newCategory, owner: newOwner, approval_level: newApprovalLevel, }, affectedAgents, timestamp: startTime, }; } /** * Retrieve skill info from database */ private async getSkillInfo(skillName: string) { const result = await this.db.selectOne( 'SELECT id, version, content_hash, content_path, tags, category, owner, approval_level FROM skills WHERE name = ?1', [skillName] ); if (!result) { return null; } // Handle different result formats if (Array.isArray(result)) { return { id: result[0], version: result[1], content_hash: result[2], content_path: result[3], tags: result[4], category: result[5], owner: result[6], approval_level: result[7], }; } return result; } /** * Update skill record in database */ private async updateSkillRecord( skillId: number, updates: Record<string, any> ): Promise<void> { const setClauses = Object.keys(updates) .map((key) => `${key} = ?`) .join(', '); const values = Object.values(updates); values.push(skillId); const sql = `UPDATE skills SET ${setClauses}, updated_at = datetime('now') WHERE id = ?`; await this.db.execute(sql, values); } /** * Record approval history entry */ private async recordApprovalHistory( skillId: number, newVersion: string, changeType: string ): Promise<void> { const metadata = JSON.stringify({ change_type: changeType, source: 'propagate-skill-update', propagated_at: new Date().toISOString(), }); const sql = ` INSERT INTO approval_history ( skill_id, version, approval_level, approver, decision, reasoning, approval_criteria_check, timestamp ) VALUES ( ?, ?, ?, ?, ?, ?, ?, datetime('now') ) `; const reasoning = 'Skill update propagated through automated workflow'; await this.db.execute(sql, [ skillId, newVersion, 'auto', 'propagate-skill-update', 'approved', reasoning, metadata, ]); } /** * Get and notify affected agents */ private async notifyAffectedAgents(skillId: number, skillName: string): Promise<string[]> { const agents = await this.db.getAffectedAgents(skillId); if (agents.length === 0) { this.logger.info('No agents using this skill'); return []; } this.logger.info(`Agents using this skill (${agents.length}):`); for (const agent of agents) { this.logger.info(` - ${agent}`); } this.logger.success(`Notification: Skill '${skillName}' has been updated`); this.logger.info('Affected agents should reload skill content on next invocation'); return agents; } }