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.
351 lines (305 loc) • 11.2 kB
text/typescript
import * as fs from 'fs';
import * as path from 'path';
/**
* Task classification result
*/
export interface TaskClassification {
category: string;
confidence: number;
keywords: string[];
}
/**
* Agent selection result with Loop 3, Loop 2, and Product Owner
*/
export interface AgentSelection {
loop3: string[];
loop2: string[];
product_owner: string;
category: string;
confidence: number;
}
/**
* Agent mapping configuration for a single category
*/
export interface CategoryMapping {
loop3: string[];
loop2: string[];
keywords: string[];
confidence: number;
}
/**
* Full agent mappings configuration
*/
export interface AgentMappings {
categories: {
[key: string]: Omit<CategoryMapping, 'keywords'>;
};
product_owner: string;
agent_aliases: {
[key: string]: string;
};
}
/**
* Task classification keywords for each category
*/
const CLASSIFICATION_KEYWORDS: Record<string, string[]> = {
security: [
'security', 'vulnerability', 'exploit', 'encryption', 'ssl', 'tls',
'certificate', 'oauth', 'saml', 'rbac', 'permission', 'auth', 'jwt',
'authentication', 'authorization'
],
infrastructure: [
'docker', 'kubernetes', 'k8s', 'helm', 'deployment', 'ci/cd', 'pipeline',
'aws', 'gcp', 'azure', 'cloud', 'terraform', 'ansible', 'infrastructure'
],
mobile: [
'mobile', 'ios', 'android', 'react-native', 'react native', 'swift',
'kotlin', 'flutter', 'app store', 'play store'
],
frontend: [
'react', 'vue', 'angular', 'svelte', 'typescript', 'javascript', 'css',
'scss', 'tailwind', 'ui', 'ux', 'design', 'component', 'frontend',
'next.js', 'remix'
],
database: [
'schema', 'migration', 'index', 'database', 'sql', 'nosql', 'postgres',
'mongodb', 'redis', 'mysql', 'table design', 'create table'
],
'backend-api': [
'api', 'rest', 'graphql', 'endpoint', 'middleware', 'express', 'fastify',
'nest.js', 'backend', 'server', 'microservice'
],
performance: [
'performance', 'benchmark', 'latency', 'throughput', 'profiling',
'memory leak', 'cpu usage', 'slow', 'optimize'
],
fullstack: [
'fullstack', 'full-stack', 'full stack'
],
default: []
};
/**
* Keyword categories sorted by priority (highest first)
*/
const CATEGORY_PRIORITY = [
'security',
'infrastructure',
'mobile',
'fullstack',
'performance',
'database',
'frontend',
'backend-api',
'default'
];
/**
* Agent Selection class for task classification and agent selection
*/
export class AgentSelector {
private agentMappings: AgentMappings | null = null;
private mappingsPath: string;
private agentsDir: string;
constructor(
mappingsPath: string = '.claude/skills/cfn-agent-selection-with-fallback/agent-mappings.json',
agentsDir: string = '.claude/agents/cfn-dev-team'
) {
this.mappingsPath = mappingsPath;
this.agentsDir = agentsDir;
}
/**
* Load agent mappings from JSON file
*/
async loadMappings(): Promise<AgentMappings> {
if (this.agentMappings !== null) {
return this.agentMappings;
}
try {
const mappingsContent = await fs.promises.readFile(this.mappingsPath, 'utf-8');
const parsed = JSON.parse(mappingsContent) as AgentMappings;
this.agentMappings = parsed;
return parsed;
} catch (error) {
console.error(`Failed to load agent mappings from ${this.mappingsPath}:`, error);
throw new Error(`Cannot load agent mappings: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Classify a task description into a category
* Returns the category with highest confidence score
*/
async classifyTask(description: string): Promise<TaskClassification> {
if (!description || description.trim().length === 0) {
return {
category: 'default',
confidence: 0.0,
keywords: []
};
}
const lowerDesc = description.toLowerCase();
const scores: Record<string, { count: number; keywords: string[] }> = {};
// Initialize scores for all categories
for (const category of CATEGORY_PRIORITY) {
scores[category] = { count: 0, keywords: [] };
}
// Special handling for fullstack (requires both frontend AND backend keywords)
const hasFrontend = this.hasKeywords(lowerDesc, ['react', 'vue', 'angular', 'svelte', 'typescript', 'javascript', 'css', 'scss', 'tailwind', 'ui', 'component', 'frontend']);
const hasBackend = this.hasKeywords(lowerDesc, ['api', 'backend', 'server', 'database', 'endpoint']);
const hasFullstackKeyword = this.hasKeywords(lowerDesc, ['fullstack', 'full-stack', 'full stack']);
if (hasFullstackKeyword || (hasFrontend && hasBackend)) {
scores['fullstack'].count = 3; // High score for fullstack
scores['fullstack'].keywords = ['fullstack'];
}
// Score other categories
for (const category of CATEGORY_PRIORITY) {
if (category === 'fullstack' || category === 'default') continue;
const keywords = CLASSIFICATION_KEYWORDS[category] || [];
for (const keyword of keywords) {
if (lowerDesc.includes(keyword)) {
scores[category].count++;
scores[category].keywords.push(keyword);
}
}
}
// Find category with highest score
let bestCategory = 'default';
let bestScore = 0;
let bestKeywords: string[] = [];
for (const category of CATEGORY_PRIORITY) {
if (scores[category].count > bestScore) {
bestScore = scores[category].count;
bestCategory = category;
bestKeywords = scores[category].keywords;
}
}
// Map bestScore to confidence (normalize to 0.0-1.0)
const confidence = bestCategory === 'default' ? 0.0 : Math.min(1.0, 0.5 + (bestScore * 0.1));
return {
category: bestCategory,
confidence: Math.round(confidence * 100) / 100,
keywords: bestKeywords
};
}
/**
* Check if description contains any of the given keywords
*/
private hasKeywords(description: string, keywords: string[]): boolean {
return keywords.some(keyword => description.includes(keyword));
}
/**
* Select agents for the given task description
*/
async selectAgents(
description: string,
minValidators: number = 3
): Promise<AgentSelection> {
const mappings = await this.loadMappings();
const classification = await this.classifyTask(description);
// Validate category exists in mappings
let category = classification.category;
if (!mappings.categories[category]) {
console.warn(`[WARN] Invalid category '${category}', falling back to 'default'`);
category = 'default';
}
const categoryMapping = mappings.categories[category];
if (!categoryMapping) {
throw new Error(`Category '${category}' not found in mappings`);
}
// Extract agents for category
let loop3Agents = Array.isArray(categoryMapping.loop3) ? [...categoryMapping.loop3] : [];
let loop2Agents = Array.isArray(categoryMapping.loop2) ? [...categoryMapping.loop2] : [];
// Fallback if empty Loop 3 agents
if (loop3Agents.length === 0) {
console.warn(`[WARN] Empty Loop 3 agents for category '${category}', using default`);
const defaultMapping = mappings.categories['default'];
loop3Agents = Array.isArray(defaultMapping.loop3) ? [...defaultMapping.loop3] : [];
}
// Fallback if empty Loop 2 agents
if (loop2Agents.length === 0) {
console.warn(`[WARN] Empty Loop 2 agents for category '${category}', using default`);
const defaultMapping = mappings.categories['default'];
loop2Agents = Array.isArray(defaultMapping.loop2) ? [...defaultMapping.loop2] : [];
}
// Validate agents exist in agent profiles
const validatedLoop3 = await this.validateAndFixAgents(loop3Agents, 'backend-developer');
const validatedLoop2 = await this.validateAndFixAgents(loop2Agents, 'tester');
// Ensure minimum 2 Loop 3 agents
if (validatedLoop3.length < 2) {
console.warn('[WARN] Less than 2 Loop 3 agents, adding devops-engineer');
validatedLoop3.push('devops-engineer');
}
// Ensure minimum validators for Loop 2
while (validatedLoop2.length < minValidators) {
console.warn(`[WARN] Less than ${minValidators} Loop 2 validators, adding code-quality-validator`);
validatedLoop2.push('code-quality-validator');
}
const productOwner = mappings.product_owner || 'product-owner';
return {
loop3: validatedLoop3,
loop2: validatedLoop2,
product_owner: productOwner,
category,
confidence: categoryMapping.confidence || 0.70
};
}
/**
* Validate agents and replace invalid ones with fallback
*/
private async validateAndFixAgents(
agents: string[],
fallbackAgent: string
): Promise<string[]> {
const mappings = await this.loadMappings();
const validated: string[] = [];
for (const agent of agents) {
if (await this.validateAgent(agent, mappings)) {
validated.push(agent);
} else {
console.warn(`[WARN] Invalid agent '${agent}', replacing with ${fallbackAgent}`);
validated.push(fallbackAgent);
}
}
return validated.length > 0 ? validated : [fallbackAgent];
}
/**
* Validate that an agent exists in the agent profiles
*/
private async validateAgent(agentName: string, mappings: AgentMappings): Promise<boolean> {
const agentAliases = mappings.agent_aliases || {};
const agentPath = agentAliases[agentName];
if (!agentPath) {
return false;
}
const fullPath = path.join(this.agentsDir, agentPath);
try {
await fs.promises.access(fullPath, fs.constants.F_OK);
// Verify path stays within agents directory (security check)
const realPath = await fs.promises.realpath(fullPath);
const realAgentsDir = await fs.promises.realpath(this.agentsDir);
return realPath.startsWith(realAgentsDir);
} catch {
return false;
}
}
/**
* Validate agents exist in agent profiles
*/
async validateAgents(agents: string[]): Promise<string[]> {
const mappings = await this.loadMappings();
const validated: string[] = [];
for (const agent of agents) {
if (await this.validateAgent(agent, mappings)) {
validated.push(agent);
}
}
return validated;
}
}
/**
* Create a default agent selector with standard paths
*/
export async function createDefaultSelector(): Promise<AgentSelector> {
const projectRoot = process.env.PROJECT_ROOT || process.cwd();
const mappingsPath = path.join(projectRoot, '.claude/skills/cfn-agent-selection-with-fallback/agent-mappings.json');
const agentsDir = path.join(projectRoot, '.claude/agents/cfn-dev-team');
return new AgentSelector(mappingsPath, agentsDir);
}