938 lines
33 KiB
JavaScript
938 lines
33 KiB
JavaScript
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
class HandoffManager {
|
|
constructor(workspacePath = null) {
|
|
this.workspacePath = workspacePath || path.join(process.cwd(), '.workspace');
|
|
this.handoffsPath = path.join(this.workspacePath, 'handoffs');
|
|
this.contextPath = path.join(this.workspacePath, 'context');
|
|
|
|
// Initialize directories
|
|
this.initialize();
|
|
|
|
// Agent-specific filtering rules with multi-role support
|
|
this.agentFilters = {
|
|
'dev': {
|
|
includePatterns: ['technical', 'implementation', 'code', 'architecture', 'bug', 'feature'],
|
|
excludePatterns: ['business', 'stakeholder', 'marketing'],
|
|
requiredSections: ['technical details', 'code references', 'implementation requirements']
|
|
},
|
|
'qa': {
|
|
includePatterns: ['testing', 'validation', 'quality', 'acceptance', 'bug', 'criteria'],
|
|
excludePatterns: ['implementation details', 'code specifics'],
|
|
requiredSections: ['acceptance criteria', 'testing requirements', 'quality standards']
|
|
},
|
|
'architect': {
|
|
includePatterns: ['design', 'architecture', 'system', 'integration', 'technical', 'pattern'],
|
|
excludePatterns: ['implementation specifics', 'testing details'],
|
|
requiredSections: ['design decisions', 'technical constraints', 'system architecture']
|
|
},
|
|
'pm': {
|
|
includePatterns: ['requirements', 'business', 'stakeholder', 'scope', 'timeline', 'priority'],
|
|
excludePatterns: ['technical implementation', 'code details'],
|
|
requiredSections: ['business requirements', 'stakeholder decisions', 'scope changes']
|
|
},
|
|
'ux-expert': {
|
|
includePatterns: ['user', 'interface', 'experience', 'design', 'usability', 'interaction'],
|
|
excludePatterns: ['backend', 'database', 'server'],
|
|
requiredSections: ['user requirements', 'design specifications', 'usability considerations']
|
|
},
|
|
'analyst': {
|
|
includePatterns: ['data', 'analysis', 'metrics', 'trends', 'insights', 'research', 'patterns', 'statistics'],
|
|
excludePatterns: ['implementation', 'specific code'],
|
|
requiredSections: ['data analysis', 'insights and trends', 'research findings']
|
|
},
|
|
'brainstorming': {
|
|
includePatterns: ['creative', 'ideation', 'brainstorm', 'innovation', 'alternative', 'possibility', 'exploration'],
|
|
excludePatterns: ['constraints', 'limitations', 'final decisions'],
|
|
requiredSections: ['creative exploration', 'alternative approaches', 'innovative solutions']
|
|
},
|
|
'research': {
|
|
includePatterns: ['research', 'investigation', 'study', 'benchmark', 'industry', 'best-practice', 'standards'],
|
|
excludePatterns: ['implementation', 'specific solutions'],
|
|
requiredSections: ['research methodology', 'findings and insights', 'recommendations']
|
|
}
|
|
};
|
|
|
|
// Multi-role combinations for complex scenarios
|
|
this.multiRoleFilters = {
|
|
'dev-analyst': {
|
|
primary: 'dev',
|
|
secondary: 'analyst',
|
|
description: 'Development with data analysis capabilities'
|
|
},
|
|
'qa-research': {
|
|
primary: 'qa',
|
|
secondary: 'research',
|
|
description: 'Quality assurance with research methodologies'
|
|
},
|
|
'architect-brainstorming': {
|
|
primary: 'architect',
|
|
secondary: 'brainstorming',
|
|
description: 'Architecture design with creative exploration'
|
|
},
|
|
'pm-analyst': {
|
|
primary: 'pm',
|
|
secondary: 'analyst',
|
|
description: 'Project management with data analysis'
|
|
},
|
|
'ux-research': {
|
|
primary: 'ux-expert',
|
|
secondary: 'research',
|
|
description: 'UX design with user research capabilities'
|
|
}
|
|
};
|
|
}
|
|
|
|
initialize() {
|
|
if (!fs.existsSync(this.handoffsPath)) {
|
|
fs.mkdirSync(this.handoffsPath, { recursive: true });
|
|
}
|
|
}
|
|
|
|
async createHandoff(sourceAgent, targetAgent, context = {}) {
|
|
try {
|
|
const timestamp = new Date().toISOString();
|
|
const handoffId = `${sourceAgent}-to-${targetAgent}-${timestamp.replace(/[:.]/g, '-')}`;
|
|
const handoffFile = path.join(this.handoffsPath, `${handoffId}.md`);
|
|
|
|
// Load workspace context using our ContextManager integration
|
|
const workspaceContext = await this.loadWorkspaceContext();
|
|
|
|
// Filter context for target agent
|
|
const filteredContext = this.filterContextForAgent(workspaceContext, targetAgent);
|
|
|
|
// Generate handoff package
|
|
const handoffContent = await this.generateHandoffPackage({
|
|
handoffId,
|
|
sourceAgent,
|
|
targetAgent,
|
|
timestamp,
|
|
context: filteredContext,
|
|
customContext: context
|
|
});
|
|
|
|
// Validate handoff completeness
|
|
const validation = this.validateHandoff(handoffContent, targetAgent);
|
|
|
|
// Write handoff file
|
|
fs.writeFileSync(handoffFile, handoffContent);
|
|
|
|
// Update handoff registry
|
|
await this.updateHandoffRegistry(handoffId, sourceAgent, targetAgent, validation);
|
|
|
|
// Log handoff in audit trail
|
|
await this.logHandoffEvent({
|
|
handoffId,
|
|
sourceAgent,
|
|
targetAgent,
|
|
timestamp,
|
|
status: 'created',
|
|
validationScore: validation.score,
|
|
filePath: handoffFile
|
|
});
|
|
|
|
return {
|
|
handoffId,
|
|
filePath: handoffFile,
|
|
validation,
|
|
success: true
|
|
};
|
|
|
|
} catch (error) {
|
|
console.error('Failed to create handoff:', error.message);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async loadWorkspaceContext() {
|
|
try {
|
|
const context = {
|
|
shared: {},
|
|
decisions: [],
|
|
progress: {},
|
|
quality: {}
|
|
};
|
|
|
|
// Load shared context
|
|
const sharedContextFile = path.join(this.contextPath, 'shared-context.md');
|
|
if (fs.existsSync(sharedContextFile)) {
|
|
context.shared = this.parseSharedContext(fs.readFileSync(sharedContextFile, 'utf8'));
|
|
}
|
|
|
|
// Load decisions
|
|
const decisionsFile = path.join(this.workspacePath, 'decisions', 'decisions-log.md');
|
|
if (fs.existsSync(decisionsFile)) {
|
|
context.decisions = this.parseDecisions(fs.readFileSync(decisionsFile, 'utf8'));
|
|
}
|
|
|
|
// Load progress
|
|
const progressFile = path.join(this.workspacePath, 'progress', 'progress-summary.md');
|
|
if (fs.existsSync(progressFile)) {
|
|
context.progress = this.parseProgress(fs.readFileSync(progressFile, 'utf8'));
|
|
}
|
|
|
|
// Load quality metrics
|
|
const qualityFile = path.join(this.workspacePath, 'quality', 'quality-metrics.md');
|
|
if (fs.existsSync(qualityFile)) {
|
|
context.quality = this.parseQualityMetrics(fs.readFileSync(qualityFile, 'utf8'));
|
|
}
|
|
|
|
return context;
|
|
} catch (error) {
|
|
console.error('Failed to load workspace context:', error.message);
|
|
return { shared: {}, decisions: [], progress: {}, quality: {} };
|
|
}
|
|
}
|
|
|
|
parseSharedContext(content) {
|
|
const context = {};
|
|
|
|
try {
|
|
const lastUpdatedMatch = content.match(/\*\*Last Updated:\*\* (.+)/);
|
|
if (lastUpdatedMatch) context.lastUpdated = lastUpdatedMatch[1];
|
|
|
|
const primaryAgentMatch = content.match(/\*\*Primary Agent:\*\* (.+)/);
|
|
if (primaryAgentMatch) context.primaryAgent = primaryAgentMatch[1];
|
|
|
|
const currentFocusMatch = content.match(/## Current Focus\n([\s\S]*?)(?=\n## |$)/);
|
|
if (currentFocusMatch) context.currentFocus = currentFocusMatch[1].trim();
|
|
|
|
const nextStepsMatch = content.match(/## Next Steps\n([\s\S]*?)(?=\n## |$)/);
|
|
if (nextStepsMatch) {
|
|
context.nextSteps = nextStepsMatch[1]
|
|
.split('\n')
|
|
.filter(line => line.startsWith('- '))
|
|
.map(line => line.substring(2).trim())
|
|
.filter(step => step.length > 0);
|
|
}
|
|
|
|
const sessionNotesMatch = content.match(/## Session Notes\n([\s\S]*?)$/);
|
|
if (sessionNotesMatch) context.sessionNotes = sessionNotesMatch[1].trim();
|
|
|
|
} catch (error) {
|
|
console.warn('Failed to parse shared context:', error.message);
|
|
}
|
|
|
|
return context;
|
|
}
|
|
|
|
parseDecisions(content) {
|
|
const decisions = [];
|
|
const decisionBlocks = content.split(/## Decision \d+:/);
|
|
|
|
for (let i = 1; i < decisionBlocks.length; i++) {
|
|
try {
|
|
const block = decisionBlocks[i];
|
|
const lines = block.split('\n');
|
|
|
|
const decision = {
|
|
id: `${i.toString().padStart(3, '0')}`,
|
|
title: lines[0].trim(),
|
|
date: this.extractField(block, 'Date'),
|
|
agent: this.extractField(block, 'Agent'),
|
|
context: this.extractField(block, 'Context'),
|
|
decision: this.extractField(block, 'Decision'),
|
|
rationale: this.extractField(block, 'Rationale'),
|
|
impact: this.extractField(block, 'Impact'),
|
|
status: this.extractField(block, 'Status')
|
|
};
|
|
|
|
decisions.push(decision);
|
|
} catch (error) {
|
|
console.warn(`Failed to parse decision block ${i}:`, error.message);
|
|
}
|
|
}
|
|
|
|
return decisions.slice(-10); // Last 10 decisions for handoff
|
|
}
|
|
|
|
parseProgress(content) {
|
|
const progress = {};
|
|
|
|
try {
|
|
const currentStoryMatch = content.match(/\*\*Current Story:\*\* (.+)/);
|
|
if (currentStoryMatch) progress.currentStory = currentStoryMatch[1];
|
|
|
|
const qualityScoreMatch = content.match(/\*\*Quality Score:\*\* (.+)/);
|
|
if (qualityScoreMatch) progress.qualityScore = qualityScoreMatch[1];
|
|
|
|
const completedMatch = content.match(/## Completed Tasks\n([\s\S]*?)(?=\n## |$)/);
|
|
if (completedMatch) {
|
|
progress.completedTasks = completedMatch[1]
|
|
.split('\n')
|
|
.filter(line => line.startsWith('- ✅'))
|
|
.map(line => line.substring(4).trim())
|
|
.filter(task => task.length > 0);
|
|
}
|
|
|
|
const pendingMatch = content.match(/## Pending Tasks\n([\s\S]*?)(?=\n## |$)/);
|
|
if (pendingMatch) {
|
|
progress.pendingTasks = pendingMatch[1]
|
|
.split('\n')
|
|
.filter(line => line.startsWith('- ⏳'))
|
|
.map(line => line.substring(4).trim())
|
|
.filter(task => task.length > 0);
|
|
}
|
|
|
|
const blockersMatch = content.match(/## Blockers\n([\s\S]*?)$/);
|
|
if (blockersMatch) {
|
|
progress.blockers = blockersMatch[1]
|
|
.split('\n')
|
|
.filter(line => line.startsWith('- 🚫'))
|
|
.map(line => line.substring(4).trim())
|
|
.filter(blocker => blocker.length > 0);
|
|
}
|
|
|
|
} catch (error) {
|
|
console.warn('Failed to parse progress:', error.message);
|
|
}
|
|
|
|
return progress;
|
|
}
|
|
|
|
parseQualityMetrics(content) {
|
|
const quality = {};
|
|
|
|
try {
|
|
// Get the most recent quality assessment
|
|
const assessments = content.split('## Quality Assessment -');
|
|
if (assessments.length > 1) {
|
|
const latest = assessments[1];
|
|
quality.timestamp = latest.split('\n')[0].trim();
|
|
quality.agent = this.extractField(latest, 'Agent');
|
|
quality.story = this.extractField(latest, 'Story');
|
|
quality.realityAuditScore = this.extractField(latest, 'Reality Audit Score');
|
|
quality.overallQuality = this.extractField(latest, 'Overall Quality');
|
|
}
|
|
} catch (error) {
|
|
console.warn('Failed to parse quality metrics:', error.message);
|
|
}
|
|
|
|
return quality;
|
|
}
|
|
|
|
extractField(content, fieldName) {
|
|
const regex = new RegExp(`\\*\\*${fieldName}:\\*\\* (.+)`, 'i');
|
|
const match = content.match(regex);
|
|
return match ? match[1].trim() : '';
|
|
}
|
|
|
|
filterContextForAgent(context, targetAgent) {
|
|
const agentType = this.getAgentType(targetAgent);
|
|
|
|
// Handle multi-role filtering
|
|
if (this.multiRoleFilters[agentType]) {
|
|
return this.filterMultiRoleContext(context, agentType);
|
|
}
|
|
|
|
// Handle single role filtering
|
|
const filter = this.agentFilters[agentType] || this.agentFilters['dev']; // Default to dev filter
|
|
|
|
const filtered = {
|
|
shared: context.shared,
|
|
decisions: this.filterDecisions(context.decisions, filter),
|
|
progress: context.progress,
|
|
quality: context.quality,
|
|
relevantContent: this.extractRelevantContent(context, filter),
|
|
roleType: 'single',
|
|
primaryRole: agentType
|
|
};
|
|
|
|
return filtered;
|
|
}
|
|
|
|
filterMultiRoleContext(context, multiRoleType) {
|
|
const multiRole = this.multiRoleFilters[multiRoleType];
|
|
const primaryFilter = this.agentFilters[multiRole.primary];
|
|
const secondaryFilter = this.agentFilters[multiRole.secondary];
|
|
|
|
// Combine include patterns from both roles
|
|
const combinedIncludePatterns = [
|
|
...primaryFilter.includePatterns,
|
|
...secondaryFilter.includePatterns
|
|
];
|
|
|
|
// Use primary role's exclude patterns but remove conflicts with secondary role
|
|
const combinedExcludePatterns = primaryFilter.excludePatterns.filter(
|
|
pattern => !secondaryFilter.includePatterns.includes(pattern)
|
|
);
|
|
|
|
const combinedFilter = {
|
|
includePatterns: combinedIncludePatterns,
|
|
excludePatterns: combinedExcludePatterns,
|
|
requiredSections: [
|
|
...primaryFilter.requiredSections,
|
|
...secondaryFilter.requiredSections
|
|
]
|
|
};
|
|
|
|
const filtered = {
|
|
shared: context.shared,
|
|
decisions: this.filterDecisions(context.decisions, combinedFilter),
|
|
progress: context.progress,
|
|
quality: context.quality,
|
|
relevantContent: this.extractRelevantContent(context, combinedFilter),
|
|
roleType: 'multi',
|
|
primaryRole: multiRole.primary,
|
|
secondaryRole: multiRole.secondary,
|
|
roleDescription: multiRole.description
|
|
};
|
|
|
|
return filtered;
|
|
}
|
|
|
|
getAgentType(agentName) {
|
|
const lowerName = agentName.toLowerCase();
|
|
|
|
// Check for multi-role patterns first (e.g., "dev-analyst", "qa+research")
|
|
const multiRolePatterns = [
|
|
{ pattern: ['dev', 'analyst'], type: 'dev-analyst' },
|
|
{ pattern: ['qa', 'research'], type: 'qa-research' },
|
|
{ pattern: ['architect', 'brainstorm'], type: 'architect-brainstorming' },
|
|
{ pattern: ['pm', 'analyst'], type: 'pm-analyst' },
|
|
{ pattern: ['ux', 'research'], type: 'ux-research' }
|
|
];
|
|
|
|
for (const multiRole of multiRolePatterns) {
|
|
if (multiRole.pattern.every(part => lowerName.includes(part))) {
|
|
return multiRole.type;
|
|
}
|
|
}
|
|
|
|
// Check for specialized roles
|
|
if (lowerName.includes('analyst') || lowerName.includes('analysis')) return 'analyst';
|
|
if (lowerName.includes('brainstorm') || lowerName.includes('creative')) return 'brainstorming';
|
|
if (lowerName.includes('research') || lowerName.includes('investigat')) return 'research';
|
|
|
|
// Check for primary roles
|
|
if (lowerName.includes('dev') || lowerName.includes('developer')) return 'dev';
|
|
if (lowerName.includes('qa') || lowerName.includes('test')) return 'qa';
|
|
if (lowerName.includes('arch') || lowerName.includes('architect')) return 'architect';
|
|
if (lowerName.includes('pm') || lowerName.includes('manager')) return 'pm';
|
|
if (lowerName.includes('ux') || lowerName.includes('design')) return 'ux-expert';
|
|
|
|
return 'dev'; // Default fallback
|
|
}
|
|
|
|
filterDecisions(decisions, filter) {
|
|
return decisions.filter(decision => {
|
|
const decisionText = `${decision.title} ${decision.decision} ${decision.rationale} ${decision.impact}`.toLowerCase();
|
|
|
|
// Check if decision matches include patterns
|
|
const matchesInclude = filter.includePatterns.some(pattern =>
|
|
decisionText.includes(pattern.toLowerCase())
|
|
);
|
|
|
|
// Check if decision matches exclude patterns
|
|
const matchesExclude = filter.excludePatterns.some(pattern =>
|
|
decisionText.includes(pattern.toLowerCase())
|
|
);
|
|
|
|
return matchesInclude && !matchesExclude;
|
|
});
|
|
}
|
|
|
|
extractRelevantContent(context, filter) {
|
|
const relevant = [];
|
|
|
|
// Extract relevant next steps
|
|
if (context.shared.nextSteps) {
|
|
context.shared.nextSteps.forEach(step => {
|
|
const stepText = step.toLowerCase();
|
|
const isRelevant = filter.includePatterns.some(pattern =>
|
|
stepText.includes(pattern.toLowerCase())
|
|
);
|
|
|
|
if (isRelevant) {
|
|
relevant.push(`Next Step: ${step}`);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Extract relevant progress items
|
|
if (context.progress.pendingTasks) {
|
|
context.progress.pendingTasks.forEach(task => {
|
|
const taskText = task.toLowerCase();
|
|
const isRelevant = filter.includePatterns.some(pattern =>
|
|
taskText.includes(pattern.toLowerCase())
|
|
);
|
|
|
|
if (isRelevant) {
|
|
relevant.push(`Pending Task: ${task}`);
|
|
}
|
|
});
|
|
}
|
|
|
|
return relevant;
|
|
}
|
|
|
|
async generateHandoffPackage(params) {
|
|
const {
|
|
handoffId,
|
|
sourceAgent,
|
|
targetAgent,
|
|
timestamp,
|
|
context,
|
|
customContext
|
|
} = params;
|
|
|
|
const agentType = this.getAgentType(targetAgent);
|
|
const nextActions = this.generateNextActions(context, agentType);
|
|
const fileReferences = this.generateFileReferences(context);
|
|
const blockers = this.extractBlockers(context);
|
|
|
|
return `# Agent Handoff: ${sourceAgent} → ${targetAgent}
|
|
|
|
**Created:** ${timestamp}
|
|
**Handoff ID:** ${handoffId}
|
|
**Source Agent:** ${sourceAgent}
|
|
**Target Agent:** ${targetAgent}
|
|
**Target Agent Type:** ${agentType}
|
|
|
|
## Context Summary
|
|
${context.shared.currentFocus || 'No current focus available.'}
|
|
|
|
${customContext.summary || ''}
|
|
|
|
## Key Decisions Made
|
|
${context.decisions.map(d => `- **${d.title}** (${d.agent}, ${d.date}): ${d.decision}`).join('\n') || '- No relevant decisions for this agent type'}
|
|
|
|
## Current Progress
|
|
**Story:** ${context.progress.currentStory || 'No active story'}
|
|
**Quality Score:** ${context.progress.qualityScore || 'Not assessed'}
|
|
|
|
**Completed Tasks:**
|
|
${context.progress.completedTasks ? context.progress.completedTasks.map(task => `- ✅ ${task}`).join('\n') : '- No completed tasks'}
|
|
|
|
**Pending Tasks:**
|
|
${context.progress.pendingTasks ? context.progress.pendingTasks.map(task => `- ⏳ ${task}`).join('\n') : '- No pending tasks'}
|
|
|
|
## Next Actions for ${targetAgent}
|
|
${nextActions.map(action => `- [ ] ${action}`).join('\n')}
|
|
|
|
## Files and References
|
|
${fileReferences.join('\n') || '- No specific file references available'}
|
|
|
|
## Blockers and Dependencies
|
|
${blockers.join('\n') || '- No blockers identified'}
|
|
|
|
## Quality Metrics
|
|
${context.quality.overallQuality ? `**Latest Quality Score:** ${context.quality.overallQuality}` : 'No quality metrics available'}
|
|
${context.quality.story ? `**Last Assessed Story:** ${context.quality.story}` : ''}
|
|
|
|
## Relevant Context
|
|
${context.relevantContent.map(item => `- ${item}`).join('\n') || '- No additional relevant context'}
|
|
|
|
## Handoff Validation
|
|
- [ ] Context completeness verified
|
|
- [ ] Decisions documented and relevant
|
|
- [ ] Next actions clearly defined for ${agentType} role
|
|
- [ ] References included
|
|
- [ ] Quality metrics current
|
|
- [ ] Agent-specific filtering applied
|
|
- [ ] Blockers and dependencies identified
|
|
|
|
## Handoff Notes
|
|
${customContext.notes || 'No additional notes provided.'}
|
|
|
|
---
|
|
*Generated by BMAD Agent Handoff System v1.0*
|
|
*Handoff Quality Score: ${this.calculateHandoffScore(context, agentType)}/100*
|
|
`;
|
|
}
|
|
|
|
generateNextActions(context, agentType) {
|
|
const actions = [];
|
|
|
|
// Handle multi-role actions
|
|
if (this.multiRoleFilters[agentType]) {
|
|
return this.generateMultiRoleActions(context, agentType);
|
|
}
|
|
|
|
// Handle single-role actions
|
|
switch (agentType) {
|
|
case 'dev':
|
|
actions.push('Review technical requirements and architecture decisions');
|
|
actions.push('Examine current code implementation status');
|
|
actions.push('Address any pending technical tasks or bugs');
|
|
if (context.progress.blockers && context.progress.blockers.length > 0) {
|
|
actions.push('Resolve identified blockers and technical dependencies');
|
|
}
|
|
break;
|
|
|
|
case 'qa':
|
|
actions.push('Review acceptance criteria and testing requirements');
|
|
actions.push('Validate completed functionality against requirements');
|
|
actions.push('Execute test cases and identify quality issues');
|
|
actions.push('Update quality metrics and provide feedback');
|
|
break;
|
|
|
|
case 'architect':
|
|
actions.push('Review system design and architectural decisions');
|
|
actions.push('Validate technical approach and integration points');
|
|
actions.push('Assess scalability and performance implications');
|
|
actions.push('Document any new architectural requirements');
|
|
break;
|
|
|
|
case 'pm':
|
|
actions.push('Review project scope and timeline status');
|
|
actions.push('Assess stakeholder requirements and priority changes');
|
|
actions.push('Update project planning and resource allocation');
|
|
actions.push('Communicate progress to stakeholders');
|
|
break;
|
|
|
|
case 'ux-expert':
|
|
actions.push('Review user experience requirements and design specifications');
|
|
actions.push('Validate interface design and usability considerations');
|
|
actions.push('Assess user interaction patterns and feedback');
|
|
actions.push('Update design documentation and prototypes');
|
|
break;
|
|
|
|
case 'analyst':
|
|
actions.push('Analyze available data and identify key patterns');
|
|
actions.push('Generate insights from metrics and performance data');
|
|
actions.push('Create data visualization and trend analysis');
|
|
actions.push('Provide data-driven recommendations');
|
|
break;
|
|
|
|
case 'brainstorming':
|
|
actions.push('Explore creative alternatives and innovative approaches');
|
|
actions.push('Generate multiple solution options without constraints');
|
|
actions.push('Challenge existing assumptions and think outside the box');
|
|
actions.push('Facilitate ideation sessions and creative problem-solving');
|
|
break;
|
|
|
|
case 'research':
|
|
actions.push('Conduct comprehensive research on relevant topics');
|
|
actions.push('Investigate industry best practices and standards');
|
|
actions.push('Gather evidence and benchmark against competitors');
|
|
actions.push('Synthesize research findings into actionable insights');
|
|
break;
|
|
|
|
default:
|
|
actions.push('Review handoff context and understand current state');
|
|
actions.push('Identify specific tasks relevant to your role');
|
|
actions.push('Address any pending items in your domain');
|
|
}
|
|
|
|
// Add context-specific actions
|
|
if (context.shared.nextSteps) {
|
|
context.shared.nextSteps.forEach(step => {
|
|
if (!actions.some(action => action.toLowerCase().includes(step.toLowerCase().substring(0, 20)))) {
|
|
actions.push(step);
|
|
}
|
|
});
|
|
}
|
|
|
|
return actions.slice(0, 8); // Limit to 8 actions for readability
|
|
}
|
|
|
|
generateMultiRoleActions(context, multiRoleType) {
|
|
const multiRole = this.multiRoleFilters[multiRoleType];
|
|
const actions = [];
|
|
|
|
switch (multiRoleType) {
|
|
case 'dev-analyst':
|
|
actions.push('Analyze current system performance and identify optimization opportunities');
|
|
actions.push('Review code metrics and technical debt patterns');
|
|
actions.push('Implement data-driven development improvements');
|
|
actions.push('Create performance monitoring and analysis dashboards');
|
|
actions.push('Research and apply evidence-based development practices');
|
|
break;
|
|
|
|
case 'qa-research':
|
|
actions.push('Research industry testing standards and compliance frameworks');
|
|
actions.push('Investigate best practices for quality assurance methodologies');
|
|
actions.push('Analyze quality trends and benchmark against industry standards');
|
|
actions.push('Design comprehensive testing strategies based on research findings');
|
|
actions.push('Validate testing approaches through evidence-based research');
|
|
break;
|
|
|
|
case 'architect-brainstorming':
|
|
actions.push('Explore creative architectural patterns and innovative design approaches');
|
|
actions.push('Brainstorm multiple system design alternatives without constraints');
|
|
actions.push('Challenge conventional architecture assumptions');
|
|
actions.push('Generate innovative solutions for complex integration challenges');
|
|
actions.push('Facilitate collaborative design exploration sessions');
|
|
break;
|
|
|
|
case 'pm-analyst':
|
|
actions.push('Analyze project data to identify trends and optimization opportunities');
|
|
actions.push('Research stakeholder feedback and user behavior patterns');
|
|
actions.push('Create data-driven project prioritization and resource allocation');
|
|
actions.push('Generate insights from project metrics and timeline analysis');
|
|
actions.push('Develop evidence-based project planning and risk assessment');
|
|
break;
|
|
|
|
case 'ux-research':
|
|
actions.push('Conduct user research and usability studies');
|
|
actions.push('Investigate accessibility standards and inclusive design practices');
|
|
actions.push('Analyze user behavior data and interaction patterns');
|
|
actions.push('Research industry UX trends and best practices');
|
|
actions.push('Validate design decisions through evidence-based user research');
|
|
break;
|
|
|
|
default:
|
|
actions.push('Apply multi-role perspective to current challenges');
|
|
actions.push('Integrate primary and secondary role capabilities');
|
|
actions.push('Provide comprehensive analysis from multiple viewpoints');
|
|
}
|
|
|
|
// Add context-specific actions
|
|
if (context.shared.nextSteps) {
|
|
context.shared.nextSteps.forEach(step => {
|
|
if (!actions.some(action => action.toLowerCase().includes(step.toLowerCase().substring(0, 20)))) {
|
|
actions.push(step);
|
|
}
|
|
});
|
|
}
|
|
|
|
return actions.slice(0, 10); // Allow more actions for multi-role scenarios
|
|
}
|
|
|
|
generateFileReferences(context) {
|
|
const references = [];
|
|
|
|
// Add standard workspace references
|
|
references.push('📁 `.workspace/context/shared-context.md` - Current workspace context');
|
|
references.push('📋 `.workspace/decisions/decisions-log.md` - Architectural decisions');
|
|
references.push('📈 `.workspace/progress/progress-summary.md` - Development progress');
|
|
references.push('📊 `.workspace/quality/quality-metrics.md` - Quality assessments');
|
|
|
|
// Add story-specific references if available
|
|
if (context.progress.currentStory) {
|
|
references.push(`📖 Story documentation for: ${context.progress.currentStory}`);
|
|
}
|
|
|
|
return references;
|
|
}
|
|
|
|
extractBlockers(context) {
|
|
const blockers = [];
|
|
|
|
if (context.progress.blockers && context.progress.blockers.length > 0) {
|
|
context.progress.blockers.forEach(blocker => {
|
|
blockers.push(`🚫 ${blocker}`);
|
|
});
|
|
}
|
|
|
|
// Check for decision-based blockers
|
|
context.decisions.forEach(decision => {
|
|
if (decision.status === 'pending' || decision.impact.toLowerCase().includes('blocker')) {
|
|
blockers.push(`⚠️ Decision pending: ${decision.title}`);
|
|
}
|
|
});
|
|
|
|
return blockers;
|
|
}
|
|
|
|
validateHandoff(handoffContent, targetAgent) {
|
|
const validation = {
|
|
score: 0,
|
|
maxScore: 100,
|
|
issues: [],
|
|
strengths: []
|
|
};
|
|
|
|
const agentType = this.getAgentType(targetAgent);
|
|
const requiredSections = this.agentFilters[agentType]?.requiredSections || [];
|
|
|
|
// Check required sections (30 points)
|
|
let sectionsFound = 0;
|
|
requiredSections.forEach(section => {
|
|
if (handoffContent.toLowerCase().includes(section.toLowerCase())) {
|
|
sectionsFound++;
|
|
validation.strengths.push(`Required section present: ${section}`);
|
|
} else {
|
|
validation.issues.push(`Missing required section: ${section}`);
|
|
}
|
|
});
|
|
|
|
if (requiredSections.length > 0) {
|
|
validation.score += (sectionsFound / requiredSections.length) * 30;
|
|
} else {
|
|
validation.score += 30; // No specific requirements
|
|
}
|
|
|
|
// Check context completeness (25 points)
|
|
const hasContext = handoffContent.includes('## Context Summary') &&
|
|
handoffContent.length > 500;
|
|
if (hasContext) {
|
|
validation.score += 25;
|
|
validation.strengths.push('Comprehensive context summary provided');
|
|
} else {
|
|
validation.issues.push('Context summary incomplete or missing');
|
|
}
|
|
|
|
// Check decisions documentation (20 points)
|
|
const hasDecisions = handoffContent.includes('## Key Decisions Made');
|
|
if (hasDecisions) {
|
|
validation.score += 20;
|
|
validation.strengths.push('Key decisions documented');
|
|
} else {
|
|
validation.issues.push('Key decisions not documented');
|
|
}
|
|
|
|
// Check next actions (15 points)
|
|
const hasNextActions = handoffContent.includes('## Next Actions for') &&
|
|
handoffContent.includes('- [ ]');
|
|
if (hasNextActions) {
|
|
validation.score += 15;
|
|
validation.strengths.push('Clear next actions defined');
|
|
} else {
|
|
validation.issues.push('Next actions unclear or missing');
|
|
}
|
|
|
|
// Check references (10 points)
|
|
const hasReferences = handoffContent.includes('## Files and References');
|
|
if (hasReferences) {
|
|
validation.score += 10;
|
|
validation.strengths.push('File references provided');
|
|
} else {
|
|
validation.issues.push('File references missing');
|
|
}
|
|
|
|
validation.grade = this.scoreToGrade(validation.score);
|
|
|
|
return validation;
|
|
}
|
|
|
|
scoreToGrade(score) {
|
|
if (score >= 90) return 'A';
|
|
if (score >= 80) return 'B';
|
|
if (score >= 70) return 'C';
|
|
if (score >= 60) return 'D';
|
|
return 'F';
|
|
}
|
|
|
|
calculateHandoffScore(context, agentType) {
|
|
let score = 50; // Base score
|
|
|
|
// Add points for context richness
|
|
if (context.shared.currentFocus) score += 10;
|
|
if (context.decisions.length > 0) score += 15;
|
|
if (context.progress.currentStory) score += 10;
|
|
if (context.quality.overallQuality) score += 10;
|
|
if (context.relevantContent.length > 0) score += 5;
|
|
|
|
return Math.min(score, 100);
|
|
}
|
|
|
|
async updateHandoffRegistry(handoffId, sourceAgent, targetAgent, validation) {
|
|
try {
|
|
const registryFile = path.join(this.handoffsPath, 'handoff-registry.json');
|
|
let registry = [];
|
|
|
|
if (fs.existsSync(registryFile)) {
|
|
const content = fs.readFileSync(registryFile, 'utf8');
|
|
registry = JSON.parse(content);
|
|
}
|
|
|
|
registry.push({
|
|
handoffId,
|
|
sourceAgent,
|
|
targetAgent,
|
|
timestamp: new Date().toISOString(),
|
|
validationScore: validation.score,
|
|
grade: validation.grade,
|
|
status: 'pending'
|
|
});
|
|
|
|
// Keep only last 100 handoffs
|
|
if (registry.length > 100) {
|
|
registry = registry.slice(-100);
|
|
}
|
|
|
|
fs.writeFileSync(registryFile, JSON.stringify(registry, null, 2));
|
|
} catch (error) {
|
|
console.error('Failed to update handoff registry:', error.message);
|
|
}
|
|
}
|
|
|
|
async logHandoffEvent(event) {
|
|
try {
|
|
const auditFile = path.join(this.handoffsPath, 'audit-trail.md');
|
|
let auditContent = '';
|
|
|
|
if (fs.existsSync(auditFile)) {
|
|
auditContent = fs.readFileSync(auditFile, 'utf8');
|
|
} else {
|
|
auditContent = '# Handoff Audit Trail\n\n';
|
|
}
|
|
|
|
const logEntry = `## Handoff ${event.handoffId}
|
|
**Timestamp:** ${event.timestamp}
|
|
**Source:** ${event.sourceAgent}
|
|
**Target:** ${event.targetAgent}
|
|
**Status:** ${event.status}
|
|
**Validation Score:** ${event.validationScore}/100
|
|
**File:** ${event.filePath}
|
|
|
|
---
|
|
|
|
`;
|
|
|
|
auditContent += logEntry;
|
|
fs.writeFileSync(auditFile, auditContent);
|
|
} catch (error) {
|
|
console.error('Failed to log handoff event:', error.message);
|
|
}
|
|
}
|
|
|
|
async getPendingHandoffs(targetAgent = null) {
|
|
try {
|
|
const registryFile = path.join(this.handoffsPath, 'handoff-registry.json');
|
|
|
|
if (!fs.existsSync(registryFile)) {
|
|
return [];
|
|
}
|
|
|
|
const content = fs.readFileSync(registryFile, 'utf8');
|
|
const registry = JSON.parse(content);
|
|
|
|
let pending = registry.filter(handoff => handoff.status === 'pending');
|
|
|
|
if (targetAgent) {
|
|
pending = pending.filter(handoff => handoff.targetAgent === targetAgent);
|
|
}
|
|
|
|
return pending.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
|
|
} catch (error) {
|
|
console.error('Failed to get pending handoffs:', error.message);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
async getHandoffStats() {
|
|
try {
|
|
const registryFile = path.join(this.handoffsPath, 'handoff-registry.json');
|
|
|
|
if (!fs.existsSync(registryFile)) {
|
|
return { total: 0, pending: 0, avgScore: 0, gradeDistribution: {} };
|
|
}
|
|
|
|
const content = fs.readFileSync(registryFile, 'utf8');
|
|
const registry = JSON.parse(content);
|
|
|
|
const stats = {
|
|
total: registry.length,
|
|
pending: registry.filter(h => h.status === 'pending').length,
|
|
avgScore: registry.reduce((sum, h) => sum + h.validationScore, 0) / registry.length,
|
|
gradeDistribution: {}
|
|
};
|
|
|
|
// Calculate grade distribution
|
|
registry.forEach(handoff => {
|
|
stats.gradeDistribution[handoff.grade] = (stats.gradeDistribution[handoff.grade] || 0) + 1;
|
|
});
|
|
|
|
return stats;
|
|
} catch (error) {
|
|
console.error('Failed to get handoff stats:', error.message);
|
|
return { total: 0, pending: 0, avgScore: 0, gradeDistribution: {} };
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = HandoffManager; |