Skip to main content
Glama
BMADService.ts•28.1 kB
import { marked } from 'marked'; import * as yaml from 'js-yaml'; import matter from 'gray-matter'; import { ParsedSpec, SpecSection, Requirement, Task, Agent, Assignment, ValidationResult, UserStory, UseCase, DependencyGraph, ParseSpecRequest, ParseSpecResponse } from '../types/bmad.types.js'; export class BMADService { private agents: Agent[] = []; private tasks: Task[] = []; private assignments: Assignment[] = []; constructor() { this.initializeDefaultAgents(); } private initializeDefaultAgents(): void { this.agents = [ { id: 'dev-001', name: 'Full-Stack Developer', type: 'developer', capabilities: ['typescript', 'react', 'nodejs', 'database', 'api'], availability: 'available', performance: { tasksCompleted: 0, averageScore: 0, specialties: ['frontend', 'backend'] } }, { id: 'ui-001', name: 'UI/UX Designer', type: 'designer', capabilities: ['figma', 'ui-design', 'prototyping', 'accessibility'], availability: 'available', performance: { tasksCompleted: 0, averageScore: 0, specialties: ['design', 'user-experience'] } }, { id: 'qa-001', name: 'QA Engineer', type: 'tester', capabilities: ['manual-testing', 'automation', 'performance', 'security'], availability: 'available', performance: { tasksCompleted: 0, averageScore: 0, specialties: ['testing', 'quality-assurance'] } }, { id: 'arch-001', name: 'Solution Architect', type: 'architect', capabilities: ['system-design', 'scalability', 'security', 'documentation'], availability: 'available', performance: { tasksCompleted: 0, averageScore: 0, specialties: ['architecture', 'documentation'] } } ]; } async parseSpecification(request: ParseSpecRequest): Promise<ParseSpecResponse> { const { content, format, options = { generateTasks: false, autoAssign: false, validate: false } } = request; let spec: ParsedSpec; try { switch (format) { case 'markdown': spec = await this.parseMarkdown(content); break; case 'yaml': spec = await this.parseYAML(content); break; case 'plain': spec = await this.parsePlainText(content); break; default: throw new Error(`Unsupported format: ${format}`); } let tasks: Task[] = []; let assignments: Assignment[] = []; let validation: ValidationResult[] = []; if (options.generateTasks) { tasks = await this.generateTasks(spec); this.tasks.push(...tasks); } if (options.autoAssign && tasks.length > 0) { assignments = await this.assignTasks(tasks); this.assignments.push(...assignments); } if (options.validate && tasks.length > 0) { validation = await this.validateTasks(tasks); } return { spec, tasks, assignments, validation: validation.length > 0 ? validation : undefined }; } catch (error: any) { throw new Error(`Failed to parse specification: ${error.message}`); } } private async parseMarkdown(content: string): Promise<ParsedSpec> { const { data: frontMatter, content: markdown } = matter(content); const tokens = marked.lexer(markdown); // console.log('Markdown Tokens:', JSON.stringify(tokens, null, 2)); const spec: ParsedSpec = { id: this.generateId(), title: frontMatter.title || this.extractTitle(tokens), summary: frontMatter.summary || this.extractSummary(tokens), sections: this.extractSections(tokens), requirements: [], metadata: frontMatter, timestamp: new Date().toISOString() }; // Extract requirements from sections spec.requirements = this.extractRequirements(spec.sections); // Extract user stories if present spec.userStories = this.extractUserStories(spec.sections); // Extract use cases if present spec.useCases = this.extractUseCases(spec.sections); return spec; } private async parseYAML(content: string): Promise<ParsedSpec> { try { const data = yaml.load(content) as any; return { id: this.generateId(), title: data.title || 'Untitled Specification', summary: data.summary || 'No summary provided', sections: this.convertYAMLToSections(data), requirements: data.requirements || [], metadata: data.metadata || {}, timestamp: new Date().toISOString(), userStories: data.userStories || [], useCases: data.useCases || [] }; } catch (error: any) { throw new Error(`Invalid YAML format: ${error.message}`); } } private async parsePlainText(content: string): Promise<ParsedSpec> { const lines = content.split('\n').filter(line => line.trim()); const title = lines[0] || 'Untitled Specification'; const summary = lines[1] || 'No summary provided'; const sections: SpecSection[] = [{ title: 'Content', content: content, level: 1 }]; return { id: this.generateId(), title, summary, sections, requirements: this.extractRequirementsFromText(content), metadata: {}, timestamp: new Date().toISOString() }; } private extractTitle(tokens: any[]): string { const heading = tokens.find(token => token.type === 'heading' && token.depth === 1); return heading ? heading.text : 'Untitled Specification'; } private extractSummary(tokens: any[]): string { const firstParagraph = tokens.find(token => token.type === 'paragraph'); return firstParagraph ? firstParagraph.text.substring(0, 200) + '...' : 'No summary provided'; } private extractSections(tokens: any[]): SpecSection[] { const sections: SpecSection[] = []; let currentSection: SpecSection | null = null; let currentContent: string[] = []; for (const token of tokens) { if (token.type === 'heading') { if (currentSection) { currentSection.content = currentContent.join('\n'); sections.push(currentSection); } currentSection = { title: token.text, content: '', level: token.depth }; currentContent = []; } else if (currentSection && (token.type === 'paragraph' || token.type === 'list_item')) { currentContent.push(token.text); } else if (currentSection && token.type === 'list') { // Handle list tokens by extracting text from nested list items if (token.items) { for (const item of token.items) { if (item.text) { currentContent.push(item.text); } } } } } if (currentSection) { currentSection.content = currentContent.join('\n'); sections.push(currentSection); } return sections; } private extractRequirements(sections: SpecSection[]): Requirement[] { const requirements: Requirement[] = []; for (const section of sections) { const sectionRequirements = this.extractRequirementsFromText(section.content); requirements.push(...sectionRequirements.map(req => ({ ...req, source: section.title }))); } return requirements; } private extractRequirementsFromText(text: string): Requirement[] { const requirements: Requirement[] = []; const lines = text.split('\n'); for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); // Look for requirement patterns if (this.isRequirement(line)) { requirements.push({ id: `req-${requirements.length + 1}`, description: this.cleanRequirementText(line), type: this.classifyRequirement(line), priority: this.determinePriority(line), acceptance_criteria: this.extractAcceptanceCriteria(lines, i) }); } } return requirements; } private extractUserStories(sections: SpecSection[]): UserStory[] { const stories: UserStory[] = []; for (const section of sections) { if (section.title.toLowerCase().includes('user stor') || section.title.toLowerCase().includes('story')) { const sectionStories = this.parseUserStories(section.content); stories.push(...sectionStories); } } return stories; } private extractUseCases(sections: SpecSection[]): UseCase[] { const useCases: UseCase[] = []; for (const section of sections) { // Check if this section is a use case section or contains use cases if (section.title.toLowerCase().includes('use case') || section.title.toLowerCase().includes('usecase')) { // If this section itself is a specific use case (e.g., "Use Case: User Login") if (section.title.match(/use\s+case:\s*(.+)/i)) { const sectionUseCases = this.parseUseCases(section.title + '\n' + section.content); useCases.push(...sectionUseCases); } else { // This is a general use cases section, parse its content const sectionUseCases = this.parseUseCases(section.content); useCases.push(...sectionUseCases); } } // Also check if this section looks like a use case based on content structure else if (section.content.toLowerCase().includes('description:') && (section.content.toLowerCase().includes('actors:') || section.content.toLowerCase().includes('preconditions:'))) { // This section looks like a use case, treat the title as the use case name const useCase: UseCase = { id: `uc-${useCases.length + 1}`, name: section.title, description: '', actors: [], preconditions: [], postconditions: [], mainFlow: [], alternativeFlows: [] }; // Parse the content to extract use case details const lines = section.content.split('\n'); for (const line of lines) { const trimmedLine = line.trim(); if (trimmedLine.startsWith('Description:')) { useCase.description = trimmedLine.substring(12).trim(); } else if (trimmedLine.startsWith('Actors:')) { const actorsText = trimmedLine.substring(7).trim(); useCase.actors = actorsText.split(',').map(a => a.trim()).filter(a => a); } else if (trimmedLine.startsWith('Preconditions:')) { const preconditionsText = trimmedLine.substring(14).trim(); useCase.preconditions = [preconditionsText]; } else if (trimmedLine.startsWith('Postconditions:')) { const postconditionsText = trimmedLine.substring(15).trim(); useCase.postconditions = [postconditionsText]; } } useCases.push(useCase); } } return useCases; } private parseUserStories(content: string): UserStory[] { const stories: UserStory[] = []; const storyPattern = /As\s+(?:a|an)\s+(.+?),\s*I\s+want\s+(.+?)\s+so\s+that\s+(.+?)(?:\.|\$)/gi; let match; while ((match = storyPattern.exec(content)) !== null) { stories.push({ id: `story-${stories.length + 1}`, actor: match[1].trim(), action: match[2].trim(), benefit: match[3].trim(), acceptanceCriteria: [], priority: 'medium' }); } return stories; } private parseUseCases(content: string): UseCase[] { const useCases: UseCase[] = []; const lines = content.split('\n'); let currentUseCase: Partial<UseCase> | null = null; let useCaseCounter = 1; for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); // Match use case headers like "### Use Case: User Login" or "Use Case: User Login" const explicitUseCaseMatch = line.match(/^#+\s*Use\s+Case:\s*(.+)$/i) || line.match(/^Use\s+Case:\s*(.+)$/i); // Also match general headers if the section content looks like a use case const headerMatch = line.match(/^#+\s*(.+)$/i); const isUseCaseLikeSection = headerMatch && (content.toLowerCase().includes('description:') && (content.toLowerCase().includes('actors:') || content.toLowerCase().includes('preconditions:'))); if (explicitUseCaseMatch || isUseCaseLikeSection) { // Save previous use case if exists if (currentUseCase && currentUseCase.name) { useCases.push({ id: currentUseCase.id || `uc-${useCaseCounter++}`, name: currentUseCase.name, description: currentUseCase.description || '', actors: currentUseCase.actors || [], preconditions: currentUseCase.preconditions || [], postconditions: currentUseCase.postconditions || [], mainFlow: currentUseCase.mainFlow || [], alternativeFlows: currentUseCase.alternativeFlows || [] }); } // Start new use case const useCaseName = explicitUseCaseMatch ? explicitUseCaseMatch[1].trim() : (headerMatch?.[1]?.trim() || 'Unnamed Use Case'); currentUseCase = { id: `uc-${useCaseCounter}`, name: useCaseName, description: '', actors: [], preconditions: [], postconditions: [], mainFlow: [], alternativeFlows: [] }; continue; } // Parse use case fields if we're inside a use case if (currentUseCase) { if (line.startsWith('Description:')) { currentUseCase.description = line.substring(12).trim(); } else if (line.startsWith('Actors:')) { const actorsText = line.substring(7).trim(); currentUseCase.actors = actorsText.split(',').map(a => a.trim()).filter(a => a); } else if (line.startsWith('Preconditions:')) { const preconditionsText = line.substring(14).trim(); currentUseCase.preconditions = [preconditionsText]; } else if (line.startsWith('Postconditions:')) { const postconditionsText = line.substring(15).trim(); currentUseCase.postconditions = [postconditionsText]; } } } // Add the last use case if exists if (currentUseCase && currentUseCase.name) { useCases.push({ id: currentUseCase.id || `uc-${useCaseCounter}`, name: currentUseCase.name, description: currentUseCase.description || '', actors: currentUseCase.actors || [], preconditions: currentUseCase.preconditions || [], postconditions: currentUseCase.postconditions || [], mainFlow: currentUseCase.mainFlow || [], alternativeFlows: currentUseCase.alternativeFlows || [] }); } return useCases; } private convertYAMLToSections(data: any): SpecSection[] { const sections: SpecSection[] = []; for (const [key, value] of Object.entries(data)) { if (typeof value === 'string' || typeof value === 'object') { sections.push({ title: key, content: typeof value === 'string' ? value : JSON.stringify(value, null, 2), level: 1 }); } } return sections; } private isRequirement(line: string): boolean { const trimmedLine = line.trim(); // Skip empty lines if (!trimmedLine) return false; // Skip markdown headers if (trimmedLine.startsWith('#')) return false; // Skip lines that are just section dividers or markdown syntax if (trimmedLine.match(/^[-=]+$/)) return false; // Look for requirement patterns const requirementPatterns = [ // Bullet points that contain requirement keywords /^-\s+.*(requirement|must|shall|should|will|system|feature|function)/i, // Numbered list items that contain requirement keywords /^\d+\.\s+.*(requirement|must|shall|should|will|system|feature|function)/i, // Direct requirement statements with modal verbs /^(requirement|req-|the system|system)\s+.*(must|shall|should|will)/i, // Plain "Requirement:" statements /^requirement:\s+.*/i, // Natural language requirements (sentences with needs/requirements) /.*(system|application|platform)\s+(needs?|requires?|should|must).*/i, // Performance and security requirements /.*(performance|security|reliability)\s+(should|must|needs?|requires?).*/i ]; // Additional check: line should be substantial (not just acceptance criteria) const isSubstantial = trimmedLine.length > 15; // Exclude lines that look like acceptance criteria (typically short and start with dash) const isAcceptanceCriteria = trimmedLine.match(/^-\s+[A-Z][a-z]+\s+\d+$/) || trimmedLine.match(/^-\s+(criteria|criterion)/i); return isSubstantial && !isAcceptanceCriteria && requirementPatterns.some(pattern => pattern.test(trimmedLine)); } private cleanRequirementText(text: string): string { return text.replace(/^[-\d\.\s]+/, '').trim(); } private classifyRequirement(text: string): 'functional' | 'non-functional' | 'technical' | 'business' { const lowerText = text.toLowerCase(); if (lowerText.includes('performance') || lowerText.includes('scalability') || lowerText.includes('security') || lowerText.includes('usability')) { return 'non-functional'; } if (lowerText.includes('technology') || lowerText.includes('framework') || lowerText.includes('architecture') || lowerText.includes('implementation')) { return 'technical'; } if (lowerText.includes('business') || lowerText.includes('revenue') || lowerText.includes('cost') || lowerText.includes('roi')) { return 'business'; } return 'functional'; } private determinePriority(text: string): 'low' | 'medium' | 'high' | 'critical' { const lowerText = text.toLowerCase(); if (lowerText.includes('critical') || lowerText.includes('must') || lowerText.includes('required')) { return 'critical'; } if (lowerText.includes('important') || lowerText.includes('should')) { return 'high'; } if (lowerText.includes('nice to have') || lowerText.includes('may')) { return 'low'; } return 'medium'; } private extractAcceptanceCriteria(lines: string[], startIndex: number): string[] { const criteria: string[] = []; let i = startIndex + 1; while (i < lines.length && lines[i].trim().startsWith('-')) { criteria.push(lines[i].trim().substring(1).trim()); i++; } return criteria; } private async generateTasks(spec: ParsedSpec): Promise<Task[]> { const tasks: Task[] = []; let taskCounter = 1; // Generate tasks from requirements for (const requirement of spec.requirements) { const task: Task = { id: `task-${taskCounter++}`, title: `Implement: ${requirement.description.substring(0, 50)}...`, description: requirement.description, type: this.mapRequirementToTaskType(requirement.type), priority: requirement.priority, status: 'pending', requirements: [requirement.id], estimatedHours: requirement.effort || this.estimateEffort(requirement), createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }; tasks.push(task); } // Generate tasks from user stories if (spec.userStories) { for (const story of spec.userStories) { const task: Task = { id: `task-${taskCounter++}`, title: `Story: ${story.action}`, description: `As ${story.actor}, I want ${story.action} so that ${story.benefit}`, type: 'development', priority: story.priority, status: 'pending', requirements: [], estimatedHours: story.effort || 8, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }; tasks.push(task); } } return tasks; } private mapRequirementToTaskType(requirementType: string): Task['type'] { switch (requirementType) { case 'functional': return 'development'; case 'non-functional': return 'testing'; case 'technical': return 'development'; case 'business': return 'documentation'; default: return 'development'; } } private estimateEffort(requirement: Requirement): number { const complexity = requirement.description.length > 100 ? 'high' : requirement.description.length > 50 ? 'medium' : 'low'; const baseHours = { low: 4, medium: 8, high: 16 }; const priorityMultiplier = { low: 0.8, medium: 1.0, high: 1.2, critical: 1.5 }; return Math.round(baseHours[complexity] * priorityMultiplier[requirement.priority]); } private async assignTasks(tasks: Task[]): Promise<Assignment[]> { const assignments: Assignment[] = []; for (const task of tasks) { const bestAgent = this.findBestAgent(task); if (bestAgent) { const assignment: Assignment = { taskId: task.id, agentId: bestAgent.agent.id, agentName: bestAgent.agent.name, confidence: bestAgent.confidence, reason: bestAgent.reason, assignedAt: new Date().toISOString() }; assignments.push(assignment); // Update task task.assignedAgent = bestAgent.agent.id; task.status = 'assigned'; task.updatedAt = new Date().toISOString(); } } return assignments; } private findBestAgent(task: Task): { agent: Agent; confidence: number; reason: string } | null { const availableAgents = this.agents.filter(agent => agent.availability === 'available'); if (availableAgents.length === 0) { return null; } let bestMatch: { agent: Agent; confidence: number; reason: string } | null = null; for (const agent of availableAgents) { const match = this.calculateAgentMatch(agent, task); if (!bestMatch || match.confidence > bestMatch.confidence) { bestMatch = match; } } return bestMatch; } private calculateAgentMatch(agent: Agent, task: Task): { agent: Agent; confidence: number; reason: string } { let confidence = 0; const reasons: string[] = []; // Check agent type compatibility const typeCompatibility = this.getTypeCompatibility(agent.type, task.type); confidence += typeCompatibility * 0.4; if (typeCompatibility > 0.7) { reasons.push(`Agent type (${agent.type}) matches task type (${task.type})`); } // Check capabilities const capabilityMatch = this.getCapabilityMatch(agent.capabilities, task.description); confidence += capabilityMatch * 0.3; if (capabilityMatch > 0.5) { reasons.push('Has relevant technical capabilities'); } // Check workload const workload = this.getAgentWorkload(agent.id); const workloadScore = Math.max(0, 1 - (workload / 10)); // Prefer agents with less workload confidence += workloadScore * 0.2; // Check performance if (agent.performance && agent.performance.averageScore > 0) { confidence += (agent.performance.averageScore / 10) * 0.1; reasons.push(`Good performance history (${agent.performance.averageScore}/10)`); } return { agent, confidence: Math.min(confidence, 1), reason: reasons.join(', ') || 'Basic compatibility match' }; } private getTypeCompatibility(agentType: string, taskType: string): number { const compatibility: Record<string, Record<string, number>> = { developer: { development: 1.0, testing: 0.7, documentation: 0.5, design: 0.3, review: 0.8 }, designer: { design: 1.0, development: 0.4, testing: 0.2, documentation: 0.6, review: 0.7 }, tester: { testing: 1.0, development: 0.6, documentation: 0.4, design: 0.2, review: 0.9 }, reviewer: { review: 1.0, documentation: 0.8, testing: 0.7, development: 0.5, design: 0.5 }, architect: { development: 0.8, design: 0.7, documentation: 0.9, testing: 0.6, review: 1.0 } }; return compatibility[agentType]?.[taskType] || 0.3; } private getCapabilityMatch(capabilities: string[], taskDescription: string): number { const taskLower = taskDescription.toLowerCase(); let matches = 0; for (const capability of capabilities) { if (taskLower.includes(capability.toLowerCase())) { matches++; } } return Math.min(matches / capabilities.length, 1); } private getAgentWorkload(agentId: string): number { return this.assignments.filter(assignment => assignment.agentId === agentId && this.tasks.find(task => task.id === assignment.taskId)?.status !== 'completed' ).length; } private async validateTasks(tasks: Task[]): Promise<ValidationResult[]> { const results: ValidationResult[] = []; for (const task of tasks) { const validation = this.validateTask(task); results.push(validation); } return results; } private validateTask(task: Task): ValidationResult { const issues: any[] = []; let score = 100; // Check title if (!task.title || task.title.length < 5) { issues.push({ severity: 'error', category: 'title', message: 'Task title is too short or missing' }); score -= 20; } // Check description if (!task.description || task.description.length < 10) { issues.push({ severity: 'warning', category: 'description', message: 'Task description should be more detailed' }); score -= 10; } // Check estimation if (!task.estimatedHours || task.estimatedHours < 1) { issues.push({ severity: 'warning', category: 'estimation', message: 'Task should have time estimation' }); score -= 5; } return { id: `validation-${task.id}`, taskId: task.id, passed: issues.filter(i => i.severity === 'error').length === 0, score: Math.max(score, 0), issues, suggestions: this.generateSuggestions(task, issues), timestamp: new Date().toISOString() }; } private generateSuggestions(task: Task, issues: any[]): string[] { const suggestions: string[] = []; if (issues.some(i => i.category === 'title')) { suggestions.push('Consider adding more descriptive title with action verb'); } if (issues.some(i => i.category === 'description')) { suggestions.push('Add acceptance criteria and technical details'); } if (issues.some(i => i.category === 'estimation')) { suggestions.push('Break down the task and estimate hours based on complexity'); } return suggestions; } // Public API methods async getTasks(): Promise<Task[]> { return [...this.tasks]; } async getAgents(): Promise<Agent[]> { return [...this.agents]; } async getAssignments(): Promise<Assignment[]> { return [...this.assignments]; } async updateTaskStatus(taskId: string, status: Task['status']): Promise<boolean> { const task = this.tasks.find(t => t.id === taskId); if (task) { task.status = status; task.updatedAt = new Date().toISOString(); return true; } return false; } private generateId(): string { return `spec-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Ghostseller/CastPlan_mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server