UNPKG

claude-buddy

Version:

Your friendly AI development companion for Claude Code - supercharge Claude Code with intelligent workflows and safety features

779 lines (692 loc) 25.2 kB
/** * Persona Manager - Central coordination for persona system * * Manages persona selection, activation, collaboration, and integration * with Claude Buddy's command system. */ import { promises as fs } from 'fs'; import path from 'path'; import PersonaActivationEngine from './auto-activation.js'; import type { PersonaConfig, ActivePersona, CollaborationPattern, ValidationStep, CollaborationPlan, DetectionResults, PersonaActivationResult, SessionMemory, PersonaInteraction, PersonaFeedback, PersonaManagerAnalytics, GeneratedPrompt, SuccessfulPattern, FeedbackRecord, ActivationHistory, PersonaContext, ActivationReason } from '../types/personas.js'; import type { InputContext } from '../types/context.js'; /** * Parsed persona with runtime state * @category Persona Management */ export interface ParsedPersona extends PersonaConfig { name: string; content: string; isActive: boolean; activationReason: string | null; confidence: number; reasoning?: string; } interface CollaborationMatrixEntry { description: string; synergy: 'positive' | 'neutral' | 'negative'; leadPersona: string; } class PersonaManager { private configDir: string; private specialistsDir: string; private activationEngine: PersonaActivationEngine; private config: any = null; public personas: Map<string, ParsedPersona> = new Map(); public activePersonas: ParsedPersona[] = []; private sessionMemory: SessionMemory = { interactions: [], preferences: {}, successfulPatterns: [], feedbackHistory: [], activationHistory: [] }; private collaborationMatrix: Map<string, CollaborationMatrixEntry> = new Map(); constructor(configDir?: string) { this.configDir = configDir || path.join(__dirname, 'config'); this.specialistsDir = path.join(__dirname, 'specialists'); this.activationEngine = new PersonaActivationEngine(); } /** * Initialize the persona manager with all components. * * Sets up activation engine, loads configuration and persona definitions, * and initializes collaboration matrix for multi-persona workflows. * * @returns Promise resolving to true if initialization succeeds, false otherwise * * @example * ```typescript * const manager = new PersonaManager(); * const success = await manager.initialize(); * if (success) { * console.log(`Loaded ${manager.personas.size} personas`); * } * ``` * * @category Core * @public */ async initialize(): Promise<boolean> { try { // Initialize activation engine await this.activationEngine.initialize(); // Load configuration await this.loadConfiguration(); // Load persona definitions await this.loadPersonaDefinitions(); // Initialize collaboration matrix this.initializeCollaborationMatrix(); console.log(`Persona manager initialized with ${this.personas.size} personas`); return true; } catch (error) { console.error('Failed to initialize persona manager:', error); return false; } } /** * Load persona configuration */ private async loadConfiguration(): Promise<void> { const configPath = path.join(this.configDir, 'personas-config.json'); const configContent = await fs.readFile(configPath, 'utf8'); this.config = JSON.parse(configContent); } /** * Load all persona definitions from markdown files */ private async loadPersonaDefinitions(): Promise<void> { const files = await fs.readdir(this.specialistsDir); for (const file of files) { if (path.extname(file) === '.md') { const personaName = path.basename(file, '.md'); const personaPath = path.join(this.specialistsDir, file); const personaContent = await fs.readFile(personaPath, 'utf8'); // Parse persona markdown content const persona = this.parsePersonaDefinition(personaName, personaContent); this.personas.set(personaName, persona); } } } /** * Parse persona definition from markdown content */ private parsePersonaDefinition(name: string, content: string): ParsedPersona { const config = this.config.personas?.[name] || {}; return { name, content, category: config.category || 'unknown', description: config.description || '', specializations: config.specializations || [], auto_activation: config.auto_activation || {}, compatible_with: config.compatible_with || [], priority_hierarchy: config.priority_hierarchy || [], isActive: false, activationReason: null, confidence: 0 }; } /** * Initialize collaboration patterns between personas */ private initializeCollaborationMatrix(): void { const patterns = this.config.collaboration_patterns || {}; for (const [pattern, description] of Object.entries(patterns)) { const [persona1, persona2] = pattern.split('_'); if (persona1 && persona2) { this.collaborationMatrix.set(`${persona1}-${persona2}`, { description: String(description), synergy: 'positive', leadPersona: persona1 }); // Add reverse mapping this.collaborationMatrix.set(`${persona2}-${persona1}`, { description: String(description), synergy: 'positive', leadPersona: persona1 }); } } } /** * Select appropriate personas for a given context. * * This is the main persona selection method that handles both manual and automatic * persona activation. It first checks for manual overrides in the user input, * then falls back to automatic detection via the activation engine. * * @param userInput - The user's input string, may contain persona flags * @param context - Additional context for persona selection * @returns Promise resolving to persona activation result * * @example * ```typescript * // Automatic selection * const result = await manager.selectPersonas( * "Review this security vulnerability", * { files: ["auth.ts"], command: "review" } * ); * * // Manual override * const result = await manager.selectPersonas( * "Help me --persona-security --persona-architect" * ); * ``` * * @category Core * @public */ async selectPersonas(userInput: string, context: Partial<InputContext> = {}): Promise<PersonaActivationResult> { // Check for manual overrides first const manualOverrides = this.parseManualOverrides(userInput); if (manualOverrides.length > 0) { return this.activateManualPersonas(manualOverrides, userInput, context); } // Use automatic detection const detectionResults = await this.activationEngine.detectPersonas(userInput, context); if (detectionResults.recommendations.length === 0) { return { activePersonas: [], reasoning: 'No personas met confidence threshold', detectionResults, fallbackMode: true }; } // Activate recommended personas return this.activateRecommendedPersonas(detectionResults, userInput, context); } /** * Parse manual persona overrides from user input */ private parseManualOverrides(userInput: string): string[] { const overrides: string[] = []; const input = userInput.toLowerCase(); // Look for --persona-[name] flags for (const personaName of this.personas.keys()) { if (input.includes(`--persona-${personaName}`) || input.includes(`--${personaName}`)) { overrides.push(personaName); } } return overrides; } /** * Activate manually specified personas */ activateManualPersonas( personaNames: string[], userInput: string, context: Partial<InputContext> ): PersonaActivationResult { const activePersonas: ActivePersona[] = []; for (const name of personaNames) { const persona = this.personas.get(name); if (persona) { persona.isActive = true; persona.activationReason = 'manual_override'; persona.confidence = 1.0; activePersonas.push({ name: persona.name, confidence: persona.confidence, activationReason: 'manual', reasoning: 'Manually activated by user', category: persona.category, specializations: persona.specializations }); } } this.activePersonas = activePersonas.map(ap => { const persona = this.personas.get(ap.name)!; return { ...persona, ...ap }; }); // Record usage this.recordInteraction({ input: userInput, personas: activePersonas.map(p => p.name), activationType: 'manual', context: context || {} }); return { activePersonas, reasoning: `Manually activated: ${personaNames.join(', ')}`, detectionResults: null, manualMode: true, collaboration: this.planCollaboration(activePersonas) }; } /** * Activate recommended personas from detection engine */ private activateRecommendedPersonas( detectionResults: DetectionResults, userInput: string, context: Partial<InputContext> ): PersonaActivationResult { const activePersonas: ActivePersona[] = []; for (const recommendation of detectionResults.recommendations) { const persona = this.personas.get(recommendation.persona); if (persona) { persona.isActive = true; persona.activationReason = 'automatic'; persona.confidence = recommendation.confidence; persona.reasoning = recommendation.reasoning; activePersonas.push({ name: persona.name, confidence: recommendation.confidence, activationReason: 'automatic', reasoning: recommendation.reasoning, category: persona.category, specializations: persona.specializations }); } } this.activePersonas = activePersonas.map(ap => { const persona = this.personas.get(ap.name)!; return { ...persona, ...ap }; }); // Record usage this.recordInteraction({ input: userInput, personas: activePersonas.map(p => p.name), activationType: 'automatic', confidence: detectionResults.recommendations.map(r => r.confidence), context: context || {} }); return { activePersonas, reasoning: this.generateActivationSummary(activePersonas), detectionResults, automaticMode: true, collaboration: this.planCollaboration(activePersonas) }; } /** * Plan collaboration between active personas */ planCollaboration(activePersonas: ActivePersona[]): CollaborationPlan { if (activePersonas.length <= 1) { return { strategy: 'single_persona', leadPersona: activePersonas[0]?.name || null, consultingPersonas: [], collaborationPatterns: [], validationChain: [] }; } // Determine lead persona (highest confidence or specific rules) const leadPersona = this.determineLead(activePersonas); // Find collaboration patterns const collaborationPatterns: CollaborationPattern[] = []; for (let i = 0; i < activePersonas.length; i++) { for (let j = i + 1; j < activePersonas.length; j++) { const pattern = this.getCollaborationPattern( activePersonas[i].name, activePersonas[j].name ); if (pattern) { collaborationPatterns.push({ personas: [activePersonas[i].name, activePersonas[j].name], description: pattern.description, synergy: pattern.synergy, leadPersona: pattern.leadPersona }); } } } return { strategy: 'multi_persona', leadPersona: leadPersona.name, consultingPersonas: activePersonas.filter(p => p !== leadPersona).map(p => p.name), collaborationPatterns, validationChain: this.planValidationChain(activePersonas) }; } /** * Determine lead persona based on confidence and domain rules */ determineLead(activePersonas: ActivePersona[]): ActivePersona { if (activePersonas.length === 0) { throw new Error('Cannot determine lead persona from empty array'); } // Priority rules for lead persona const leadPriority: Record<string, number> = { 'security': 10, // Security always leads when involved 'architect': 9, // Architecture has high priority for system changes 'analyzer': 8, // Analysis leads for investigation tasks 'qa': 7, // Quality assurance for testing contexts 'performance': 6, // Performance for optimization tasks 'backend': 5, 'frontend': 5, 'devops': 5, 'refactorer': 4, 'mentor': 3, 'scribe': 2 }; return activePersonas.reduce((lead, current) => { const leadScore = (leadPriority[lead.name] || 0) + (lead.confidence * 2); const currentScore = (leadPriority[current.name] || 0) + (current.confidence * 2); return currentScore > leadScore ? current : lead; }); } /** * Get collaboration pattern between two personas */ private getCollaborationPattern(persona1: string, persona2: string): CollaborationMatrixEntry | undefined { const key1 = `${persona1}-${persona2}`; const key2 = `${persona2}-${persona1}`; return this.collaborationMatrix.get(key1) || this.collaborationMatrix.get(key2); } /** * Plan validation chain based on active personas */ private planValidationChain(activePersonas: ActivePersona[]): ValidationStep[] { const validationConfig = this.config.validation_chain || {}; const activeNames = activePersonas.map(p => p.name); const validationSteps: ValidationStep[] = []; // Security validation if (validationConfig.security_validation?.some((p: string) => activeNames.includes(p))) { validationSteps.push({ type: 'security', personas: validationConfig.security_validation.filter((p: string) => activeNames.includes(p)), description: 'Security review and threat assessment' }); } // Quality validation if (validationConfig.quality_validation?.some((p: string) => activeNames.includes(p))) { validationSteps.push({ type: 'quality', personas: validationConfig.quality_validation.filter((p: string) => activeNames.includes(p)), description: 'Code quality and maintainability review' }); } // Performance validation if (validationConfig.performance_validation?.some((p: string) => activeNames.includes(p))) { validationSteps.push({ type: 'performance', personas: validationConfig.performance_validation.filter((p: string) => activeNames.includes(p)), description: 'Performance impact assessment' }); } return validationSteps; } /** * Generate human-readable activation summary */ private generateActivationSummary(activePersonas: ActivePersona[]): string { if (activePersonas.length === 0) return 'No personas activated'; if (activePersonas.length === 1) { const persona = activePersonas[0]; return `Activated ${persona.name} persona (${Math.round(persona.confidence * 100)}% confidence): ${persona.reasoning}`; } const lead = this.determineLead(activePersonas); const supporting = activePersonas.filter(p => p !== lead); return `Multi-persona activation: ${lead.name} leading (${Math.round(lead.confidence * 100)}%), supported by ${supporting.map(p => p.name).join(', ')}`; } /** * Generate persona-aware prompt for Claude Code. * * Creates a system prompt that incorporates active persona content and * collaboration instructions. Handles both single-persona and multi-persona * collaboration modes with appropriate context integration. * * @param activePersonas - Currently active personas * @param userInput - The user's original input * @param context - Additional context for prompt generation * @returns Generated prompt with persona content and collaboration plan * * @example * ```typescript * const prompt = manager.generatePersonaPrompt( * [{ name: "security", confidence: 0.9, ... }], * "Review this code", * { files: ["auth.ts"] } * ); * * console.log(prompt.systemPrompt); // Contains persona instructions * ``` * * @category Core * @public */ generatePersonaPrompt( activePersonas: ActivePersona[], userInput: string, context: Partial<InputContext> = {} ): GeneratedPrompt { if (activePersonas.length === 0) { return { systemPrompt: '', personaContext: null, collaboration: null }; } let systemPrompt = ''; if (activePersonas.length === 1) { // Single persona mode const persona = activePersonas[0]; const personaContent = this.personas.get(persona.name)?.content || ''; systemPrompt = `You are now operating as the **${persona.name} persona** for Claude Buddy.\n\n${personaContent}`; } else { // Multi-persona collaboration mode const collaboration = this.planCollaboration(activePersonas); const lead = activePersonas.find(p => p.name === collaboration.leadPersona); const supporting = activePersonas.filter(p => p.name !== collaboration.leadPersona); if (lead) { const leadContent = this.personas.get(lead.name)?.content || ''; systemPrompt = `You are operating in **multi-persona collaboration mode** for Claude Buddy.\n\n`; systemPrompt += `**Lead Persona: ${lead.name}**\n${leadContent}\n\n`; systemPrompt += `**Supporting Personas:**\n`; for (const persona of supporting) { const personaConfig = this.personas.get(persona.name); systemPrompt += `- **${persona.name}**: Focus on ${personaConfig?.description || 'specialized tasks'}\n`; } systemPrompt += `\n**Collaboration Strategy:**\n`; systemPrompt += `- ${lead.name} leads the response and decision-making\n`; systemPrompt += `- Supporting personas provide specialized input and validation\n`; if (collaboration.collaborationPatterns && collaboration.collaborationPatterns.length > 0) { systemPrompt += `- Known collaboration patterns: ${collaboration.collaborationPatterns.map(p => p.description).join('; ')}\n`; } } } const personaContext: PersonaContext = { active: activePersonas.map(p => ({ name: p.name, confidence: p.confidence, role: p.name === this.determineLead(activePersonas).name ? 'lead' : 'supporting' })), collaboration: this.planCollaboration(activePersonas) }; return { systemPrompt, personaContext, collaboration: this.planCollaboration(activePersonas), userInput, context }; } /** * Record interaction for learning and improvement */ private recordInteraction(interaction: Partial<PersonaInteraction>): void { const fullInteraction: PersonaInteraction = { input: interaction.input || '', personas: interaction.personas || [], activationType: interaction.activationType || 'automatic', confidence: Array.isArray(interaction.confidence) ? interaction.confidence : (interaction.confidence ? [interaction.confidence] : []), context: interaction.context || {}, timestamp: new Date().toISOString() }; this.sessionMemory.interactions.push(fullInteraction); // Keep only recent interactions if (this.sessionMemory.interactions.length > 100) { this.sessionMemory.interactions = this.sessionMemory.interactions.slice(-100); } // Update activation engine this.activationEngine.recordPersonaUsage(fullInteraction.personas); } /** * Get current session ID (simple implementation) */ private getSessionId(): string { // In a real implementation, this would be more sophisticated return Date.now().toString(); } /** * Provide feedback on persona performance for learning and improvement. * * Records feedback in session memory and updates the activation engine * with performance data. Positive feedback (rating >= 4) creates successful * patterns for future learning and recommendation improvements. * * @param feedback - Feedback object with personas, rating, and optional comments * * @example * ```typescript * manager.provideFeedback({ * personas: ["security", "architect"], * rating: 5, * comments: "Excellent security analysis and architecture recommendations", * context: { command: "review" } * }); * ``` * * @category Learning * @public */ provideFeedback(feedback: PersonaFeedback): void { const { personas, rating, comments, context } = feedback; // Record feedback in session memory const feedbackRecord: FeedbackRecord = { type: 'feedback', personas, rating, comments: comments || '', context: context || {}, timestamp: Date.now() }; this.sessionMemory.feedbackHistory.push(feedbackRecord); // Update activation engine with feedback const feedbackType = rating >= 4 ? 'positive' : rating <= 2 ? 'negative' : 'neutral'; this.activationEngine.recordPersonaUsage(personas, feedbackType); // Learn from successful patterns if (rating >= 4) { const successfulPattern: SuccessfulPattern = { personas, context, timestamp: Date.now() }; this.sessionMemory.successfulPatterns.push(successfulPattern); } } /** * Get analytics and insights about persona usage. * * Provides comprehensive statistics about persona activation patterns, * collaboration effectiveness, confidence levels, and successful patterns. * Used for monitoring system performance and optimization insights. * * @returns Analytics object with usage statistics and insights * * @example * ```typescript * const analytics = manager.getAnalytics(); * console.log(`Total interactions: ${analytics.totalInteractions}`); * console.log(`Average confidence: ${analytics.averageConfidence}`); * console.log(`Top persona: ${Object.keys(analytics.personaUsage)[0]}`); * ``` * * @category Analytics * @public */ getAnalytics(): PersonaManagerAnalytics { const interactions = this.sessionMemory.interactions; const analytics: PersonaManagerAnalytics = { totalInteractions: interactions.length, personaUsage: {}, collaborationPatterns: {}, averageConfidence: 0, successfulPatterns: this.sessionMemory.successfulPatterns.length }; // Calculate persona usage statistics for (const interaction of interactions) { if (interaction.personas) { for (const persona of interaction.personas) { if (!analytics.personaUsage[persona]) { analytics.personaUsage[persona] = { count: 0, totalConfidence: 0 }; } analytics.personaUsage[persona].count++; if (interaction.confidence && interaction.confidence.length > 0) { const confidence = interaction.confidence[0]; analytics.personaUsage[persona].totalConfidence += confidence; } } // Track collaboration patterns if (interaction.personas.length > 1) { const pattern = interaction.personas.sort().join('-'); analytics.collaborationPatterns[pattern] = (analytics.collaborationPatterns[pattern] || 0) + 1; } } } // Calculate average confidence const confidenceSum = interactions .filter(i => i.confidence && i.confidence.length > 0) .reduce((sum, i) => { const confidence = i.confidence[0]; return sum + confidence; }, 0); const confidenceCount = interactions.filter(i => i.confidence && i.confidence.length > 0).length; analytics.averageConfidence = confidenceCount > 0 ? confidenceSum / confidenceCount : 0; return analytics; } /** * Reset active personas (for new conversation/session) */ reset(): void { this.activePersonas.forEach(persona => { persona.isActive = false; persona.activationReason = null; persona.confidence = 0; }); this.activePersonas = []; } /** * Get current active personas */ getActivePersonas(): ActivePersona[] { return this.activePersonas.map(p => ({ name: p.name, confidence: p.confidence, activationReason: (p.activationReason === 'manual_override' ? 'manual' : 'automatic') as ActivationReason, reasoning: p.reasoning, category: p.category, specializations: p.specializations })); } /** * Check if specific persona is active */ isPersonaActive(personaName: string): boolean { return this.activePersonas.some(p => p.name === personaName); } } export default PersonaManager;