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
text/typescript
/**
* 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;
}
}