prjct-cli 0.9.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,12 +10,34 @@
10
10
  const fs = require('fs').promises;
11
11
  const path = require('path');
12
12
  const AgentGenerator = require('../domain/agent-generator');
13
+ const configManager = require('../infrastructure/config-manager');
14
+ const TaskAnalyzer = require('../domain/task-analyzer');
15
+ const AgentMatcher = require('../domain/agent-matcher');
16
+ const SmartCache = require('../domain/smart-cache');
17
+ const AgentValidator = require('../domain/agent-validator');
13
18
 
14
19
  class MandatoryAgentRouter {
15
20
  constructor() {
16
- this.agentGenerator = new AgentGenerator();
17
- this.agentCache = new Map();
21
+ this.agentGenerator = null; // Will be initialized with projectId
22
+ this.agentCache = null; // SmartCache instance
18
23
  this.usageLog = [];
24
+ this.projectId = null;
25
+ this.taskAnalyzer = null;
26
+ this.agentMatcher = new AgentMatcher();
27
+ this.agentValidator = new AgentValidator();
28
+ }
29
+
30
+ /**
31
+ * Initialize with project context
32
+ * @param {string} projectPath - Path to the project
33
+ */
34
+ async initialize(projectPath) {
35
+ this.projectId = await configManager.getProjectId(projectPath);
36
+ this.agentGenerator = new AgentGenerator(this.projectId);
37
+ this.agentCache = new SmartCache(this.projectId);
38
+ await this.agentCache.initialize();
39
+ this.taskAnalyzer = new TaskAnalyzer(projectPath);
40
+ await this.taskAnalyzer.initialize();
19
41
  }
20
42
 
21
43
  /**
@@ -23,11 +45,16 @@ class MandatoryAgentRouter {
23
45
  * @throws {Error} If no agent can be assigned
24
46
  */
25
47
  async executeTask(task, context, projectPath) {
26
- // STEP 1: Analyze task to determine required expertise
27
- const taskAnalysis = this.analyzeTask(task);
48
+ // Initialize if needed
49
+ if (!this.agentGenerator) {
50
+ await this.initialize(projectPath);
51
+ }
52
+
53
+ // STEP 1: Deep semantic task analysis (NEW)
54
+ const taskAnalysis = await this.taskAnalyzer.analyzeTask(task);
28
55
 
29
- // STEP 2: Select or generate specialized agent (MANDATORY)
30
- const agent = await this.assignAgent(taskAnalysis, context);
56
+ // STEP 2: Select or generate specialized agent (MANDATORY) - with intelligent matching
57
+ const agent = await this.assignAgent(taskAnalysis, context, projectPath);
31
58
 
32
59
  // STEP 3: Validate agent assignment
33
60
  if (!agent || !agent.name) {
@@ -44,8 +71,9 @@ class MandatoryAgentRouter {
44
71
  taskAnalysis
45
72
  );
46
73
 
47
- // STEP 5: Log agent usage for tracking
74
+ // STEP 5: Log agent usage for tracking and learning
48
75
  this.logAgentUsage(task, agent, filteredContext);
76
+ this.agentMatcher.recordSuccess(agent, taskAnalysis, true); // Learn from assignment
49
77
 
50
78
  // STEP 6: Return agent with filtered context
51
79
  return {
@@ -62,145 +90,241 @@ class MandatoryAgentRouter {
62
90
 
63
91
  /**
64
92
  * Analyze task to determine what type of expertise is needed
93
+ * DEPRECATED: Now uses TaskAnalyzer for deep semantic analysis
94
+ * Kept for backward compatibility
65
95
  */
66
- analyzeTask(task) {
96
+ async analyzeTask(task, projectPath = null) {
67
97
  const description = task.description?.toLowerCase() || '';
68
98
  const type = task.type?.toLowerCase() || '';
69
99
 
70
- // Keywords for different domains
100
+ // Get project technologies for better matching
101
+ let projectTech = null
102
+ if (projectPath) {
103
+ try {
104
+ const TechDetector = require('../domain/tech-detector');
105
+ const detector = new TechDetector(projectPath);
106
+ projectTech = await detector.detectAll();
107
+ } catch (error) {
108
+ // If detection fails, continue with keyword-based analysis
109
+ }
110
+ }
111
+
112
+ // Semantic patterns - broader, more flexible
71
113
  const patterns = {
72
114
  frontend: [
73
- 'component', 'ui', 'react', 'vue', 'angular', 'style',
74
- 'css', 'layout', 'responsive', 'user interface', 'frontend'
115
+ 'component', 'ui', 'user interface', 'frontend', 'client',
116
+ 'style', 'css', 'layout', 'responsive', 'design',
117
+ 'page', 'view', 'template', 'render', 'display'
75
118
  ],
76
119
  backend: [
77
120
  'api', 'server', 'endpoint', 'route', 'middleware',
78
- 'auth', 'authentication', 'jwt', 'session', 'backend'
121
+ 'auth', 'authentication', 'authorization', 'jwt', 'session',
122
+ 'backend', 'service', 'controller', 'handler'
79
123
  ],
80
124
  database: [
81
- 'database', 'query', 'migration', 'schema', 'model',
82
- 'sql', 'postgres', 'mysql', 'mongo', 'index'
125
+ 'database', 'db', 'query', 'migration', 'schema', 'model',
126
+ 'sql', 'data', 'table', 'collection', 'index', 'relation'
83
127
  ],
84
128
  devops: [
85
- 'deploy', 'docker', 'kubernetes', 'ci/cd', 'pipeline',
86
- 'build', 'ship', 'release', 'production'
129
+ 'deploy', 'deployment', 'docker', 'kubernetes', 'k8s',
130
+ 'ci/cd', 'pipeline', 'build', 'ship', 'release',
131
+ 'production', 'infrastructure', 'container', 'orchestration'
87
132
  ],
88
133
  qa: [
89
- 'test', 'bug', 'error', 'fix', 'debug', 'issue',
90
- 'quality', 'coverage', 'unit test', 'integration'
134
+ 'test', 'testing', 'bug', 'error', 'fix', 'debug', 'issue',
135
+ 'quality', 'coverage', 'unit test', 'integration test',
136
+ 'e2e', 'spec', 'assertion', 'validation'
91
137
  ],
92
138
  architecture: [
93
139
  'design', 'architecture', 'pattern', 'structure',
94
- 'refactor', 'organize', 'plan', 'feature'
140
+ 'refactor', 'refactoring', 'organize', 'plan',
141
+ 'feature', 'system', 'module', 'component design'
95
142
  ]
96
143
  };
97
144
 
98
- // Detect primary domain
99
- let detectedDomain = 'generalist';
100
- let confidence = 0;
101
- let matchedKeywords = [];
145
+ // If we have project tech, enhance patterns with actual technologies
146
+ if (projectTech) {
147
+ // Add detected frontend frameworks to frontend patterns
148
+ const frontendTech = [
149
+ ...projectTech.frameworks.filter(f => ['react', 'vue', 'angular', 'svelte', 'next', 'nuxt'].includes(f.toLowerCase())),
150
+ ...projectTech.buildTools.filter(t => ['vite', 'webpack'].includes(t.toLowerCase()))
151
+ ];
152
+ if (frontendTech.length > 0) {
153
+ patterns.frontend.push(...frontendTech.map(t => t.toLowerCase()));
154
+ }
102
155
 
103
- for (const [domain, keywords] of Object.entries(patterns)) {
104
- const matches = keywords.filter(keyword =>
105
- description.includes(keyword) || type.includes(keyword)
156
+ // Add detected backend frameworks to backend patterns
157
+ const backendTech = projectTech.frameworks.filter(f =>
158
+ ['express', 'fastify', 'django', 'flask', 'rails', 'phoenix'].includes(f.toLowerCase())
106
159
  );
107
-
108
- if (matches.length > confidence) {
109
- confidence = matches.length;
110
- detectedDomain = domain;
111
- matchedKeywords = matches;
160
+ if (backendTech.length > 0) {
161
+ patterns.backend.push(...backendTech.map(t => t.toLowerCase()));
112
162
  }
113
163
  }
114
164
 
115
- // Detect technology stack
116
- const techStack = this.detectTechnology(task, description);
165
+ // Detect primary domain
166
+ const { detectedDomain, matchedKeywords, confidence } = this.detectDomain(description, type, patterns);
117
167
 
118
168
  return {
119
169
  domain: detectedDomain,
120
- confidence: confidence > 0 ? (confidence / 3) : 0.3, // confidence score
170
+ confidence: confidence > 0 ? Math.min(confidence / 3, 1.0) : 0.3,
121
171
  matchedKeywords,
122
- techStack,
123
172
  reason: `Detected ${detectedDomain} task based on: ${matchedKeywords.join(', ')}`,
124
- alternatives: this.getSimilarDomains(detectedDomain)
173
+ alternatives: this.getSimilarDomains(detectedDomain),
174
+ projectTechnologies: projectTech
125
175
  };
126
176
  }
127
177
 
128
178
  /**
129
- * Detect specific technologies mentioned or implied
179
+ * Detect domain based on patterns
130
180
  */
131
- detectTechnology(task, description) {
132
- const technologies = {
133
- languages: [],
134
- frameworks: [],
135
- databases: [],
136
- tools: []
137
- };
138
-
139
- // Language detection
140
- const languages = [
141
- 'javascript', 'typescript', 'python', 'ruby', 'go',
142
- 'rust', 'java', 'csharp', 'php', 'elixir', 'swift'
143
- ];
144
-
145
- const frameworks = [
146
- 'react', 'vue', 'angular', 'express', 'django', 'rails',
147
- 'spring', 'laravel', 'phoenix', 'gin', 'fastapi', 'nextjs'
148
- ];
149
-
150
- const databases = [
151
- 'postgres', 'mysql', 'mongodb', 'redis', 'elasticsearch',
152
- 'dynamodb', 'firebase', 'supabase', 'sqlite'
153
- ];
154
-
155
- // Check for each technology
156
- languages.forEach(lang => {
157
- if (description.includes(lang)) {
158
- technologies.languages.push(lang);
159
- }
181
+ detectDomain(description, type, patterns) {
182
+ // Simple domain detection based on keywords
183
+ const matches = Object.entries(patterns).map(([domain, keywords]) => {
184
+ const found = keywords.filter(keyword =>
185
+ description.includes(keyword) || type.includes(keyword)
186
+ );
187
+ return { domain, keywords: found, count: found.length };
160
188
  });
161
189
 
162
- frameworks.forEach(fw => {
163
- if (description.includes(fw)) {
164
- technologies.frameworks.push(fw);
165
- }
166
- });
190
+ // Sort by count descending
191
+ const sorted = matches.sort((a, b) => b.count - a.count);
192
+ const bestMatch = sorted[0];
167
193
 
168
- databases.forEach(db => {
169
- if (description.includes(db)) {
170
- technologies.databases.push(db);
171
- }
172
- });
194
+ if (bestMatch && bestMatch.count > 0) {
195
+ return {
196
+ detectedDomain: bestMatch.domain,
197
+ matchedKeywords: bestMatch.keywords,
198
+ confidence: bestMatch.count
199
+ };
200
+ }
173
201
 
174
- return technologies;
202
+ return {
203
+ detectedDomain: 'generalist',
204
+ matchedKeywords: [],
205
+ confidence: 0.5
206
+ };
175
207
  }
176
208
 
177
209
  /**
178
210
  * Assign the best agent for the task
179
- * Creates a new one if needed
211
+ * IMPROVED: Uses intelligent matching with scoring
180
212
  */
181
- async assignAgent(taskAnalysis, context) {
182
- const { domain, techStack } = taskAnalysis;
213
+ async assignAgent(taskAnalysis, context, projectPath, overrideAgent = null) {
214
+ // Respect override
215
+ if (overrideAgent) {
216
+ const existing = await this.agentGenerator.loadAgent(overrideAgent);
217
+ if (existing) {
218
+ return existing;
219
+ }
220
+ return this.generateSpecializedAgent(overrideAgent, {}, context);
221
+ }
222
+
223
+ const primaryDomain = taskAnalysis.primaryDomain;
224
+ const projectTech = taskAnalysis.projectTechnologies || {};
225
+
226
+ // Generate cache key with tech stack
227
+ const techStack = {
228
+ languages: projectTech.languages || [],
229
+ frameworks: projectTech.frameworks || []
230
+ };
231
+ const cacheKey = this.agentCache.generateKey(this.projectId, primaryDomain, techStack);
232
+
233
+ // Check smart cache first
234
+ const cached = await this.agentCache.get(cacheKey);
235
+ if (cached) {
236
+ return cached;
237
+ }
238
+
239
+ // STEP 1: Load all existing agents
240
+ const allAgents = await this.agentGenerator.loadAllAgents();
241
+
242
+ // STEP 2: Use intelligent matching to find best agent
243
+ const match = this.agentMatcher.findBestAgent(allAgents, taskAnalysis);
244
+
245
+ if (match && match.score > 0.5) {
246
+ // Good match found - use it
247
+ await this.agentCache.set(cacheKey, match.agent);
248
+ return match.agent;
249
+ }
250
+
251
+ // STEP 3: Try to load domain-specific agent
252
+ const agentType = this.getAgentTypeForDomain(primaryDomain);
253
+ const existingAgent = await this.agentGenerator.loadAgent(agentType);
254
+
255
+ if (existingAgent) {
256
+ await this.agentCache.set(cacheKey, existingAgent);
257
+ return existingAgent;
258
+ }
259
+
260
+ // STEP 4: Validate before generating new agent
261
+ const config = {
262
+ domain: primaryDomain,
263
+ projectContext: context.projectSummary || context.projectContext || '',
264
+ expertise: this.buildExpertiseFromTech(projectTech, primaryDomain)
265
+ };
183
266
 
184
- // Check cache first
185
- const cacheKey = `${domain}-${techStack.languages.join('-')}`;
186
- if (this.agentCache.has(cacheKey)) {
187
- return this.agentCache.get(cacheKey);
267
+ const validation = this.agentValidator.validateBeforeGeneration(
268
+ agentType,
269
+ config,
270
+ allAgents
271
+ );
272
+
273
+ if (!validation.valid && validation.similarAgent) {
274
+ // Similar agent exists - use it instead
275
+ await this.agentCache.set(cacheKey, validation.similarAgent);
276
+ return validation.similarAgent;
188
277
  }
189
278
 
190
- // Generate specialized agent based on detection
191
- const agent = await this.generateSpecializedAgent(domain, techStack, context);
279
+ // STEP 5: Generate new agent only if validated
280
+ const agent = await this.generateSpecializedAgent(primaryDomain, techStack, context);
281
+
282
+ // Validate after generation
283
+ const postValidation = this.agentValidator.validateAfterGeneration(agent);
284
+ if (!postValidation.valid) {
285
+ console.warn(`⚠️ Agent validation issues: ${postValidation.issues.join(', ')}`);
286
+ }
192
287
 
193
288
  // Cache for reuse
194
- this.agentCache.set(cacheKey, agent);
289
+ await this.agentCache.set(cacheKey, agent);
195
290
 
196
291
  return agent;
197
292
  }
198
293
 
199
294
  /**
200
- * Generate a specialized agent for the detected domain and tech
295
+ * Build expertise string from tech stack
201
296
  */
202
- async generateSpecializedAgent(domain, techStack, context) {
203
- // Map domain to agent type
297
+ buildExpertiseFromTech(projectTech, domain) {
298
+ const parts = []
299
+
300
+ if (projectTech.languages && projectTech.languages.length > 0) {
301
+ parts.push(projectTech.languages.join(', '))
302
+ }
303
+
304
+ if (projectTech.frameworks && projectTech.frameworks.length > 0) {
305
+ const relevantFrameworks = projectTech.frameworks.filter(f => {
306
+ const fLower = f.toLowerCase()
307
+ if (domain === 'frontend') {
308
+ return ['react', 'vue', 'angular', 'svelte', 'next', 'nuxt'].some(tech => fLower.includes(tech))
309
+ }
310
+ if (domain === 'backend') {
311
+ return ['express', 'fastify', 'django', 'flask', 'rails', 'phoenix'].some(tech => fLower.includes(tech))
312
+ }
313
+ return true
314
+ })
315
+ if (relevantFrameworks.length > 0) {
316
+ parts.push(relevantFrameworks.join(', '))
317
+ }
318
+ }
319
+
320
+ return parts.join(', ') || `${domain} development`
321
+ }
322
+
323
+ /**
324
+ * Get agent type name for a domain
325
+ * @private
326
+ */
327
+ getAgentTypeForDomain(domain) {
204
328
  const agentTypes = {
205
329
  frontend: 'frontend-specialist',
206
330
  backend: 'backend-specialist',
@@ -210,85 +334,43 @@ class MandatoryAgentRouter {
210
334
  architecture: 'architect',
211
335
  generalist: 'full-stack'
212
336
  };
337
+ return agentTypes[domain] || 'full-stack';
338
+ }
213
339
 
214
- const agentType = agentTypes[domain] || 'full-stack';
215
-
216
- // Generate with detected technologies
217
- const config = {
218
- domain,
219
- techStack,
220
- projectContext: context.projectSummary || '',
221
- bestPractices: await this.getBestPractices(domain, techStack)
222
- };
223
-
224
- return this.agentGenerator.generateDynamicAgent(agentType, config);
340
+ /**
341
+ * Find similar agent from existing agents
342
+ * DEPRECATED: Now uses AgentMatcher for intelligent matching
343
+ * @private
344
+ */
345
+ findSimilarAgent(allAgents, domain, taskAnalysis) {
346
+ // Use AgentMatcher instead
347
+ const match = this.agentMatcher.findBestAgent(allAgents, taskAnalysis);
348
+ return match ? match.agent : null;
225
349
  }
226
350
 
227
351
  /**
228
- * Get best practices for the domain and tech stack
352
+ * Generate a specialized agent for the detected domain
353
+ * Only called when no existing agent is found
229
354
  */
230
- async getBestPractices(domain, techStack) {
231
- const practices = [];
355
+ async generateSpecializedAgent(domain, techStack, context) {
356
+ // Map domain to agent type
357
+ const agentType = this.getAgentTypeForDomain(domain);
232
358
 
233
- // Domain-specific best practices
234
- const domainPractices = {
235
- frontend: [
236
- 'Component composition over inheritance',
237
- 'State management patterns',
238
- 'Responsive design principles',
239
- 'Accessibility standards',
240
- 'Performance optimization (lazy loading, memoization)'
241
- ],
242
- backend: [
243
- 'RESTful API design principles',
244
- 'Authentication and authorization patterns',
245
- 'Error handling and logging',
246
- 'Rate limiting and caching',
247
- 'Database connection pooling'
248
- ],
249
- database: [
250
- 'Normalization principles',
251
- 'Index optimization',
252
- 'Query performance tuning',
253
- 'Transaction management',
254
- 'Backup and recovery strategies'
255
- ],
256
- devops: [
257
- 'CI/CD pipeline best practices',
258
- 'Container orchestration',
259
- 'Infrastructure as Code',
260
- 'Monitoring and alerting',
261
- 'Security scanning'
262
- ],
263
- qa: [
264
- 'Test pyramid (unit, integration, e2e)',
265
- 'Test coverage standards',
266
- 'Mocking and stubbing',
267
- 'Performance testing',
268
- 'Security testing'
269
- ]
359
+ // Generate with minimal config - let the Agent figure it out
360
+ const config = {
361
+ domain,
362
+ projectContext: context.projectSummary || context.projectContext || '',
363
+ // No hardcoded best practices passed here
270
364
  };
271
365
 
272
- practices.push(...(domainPractices[domain] || []));
273
-
274
- // Technology-specific practices
275
- if (techStack.languages.includes('javascript') || techStack.languages.includes('typescript')) {
276
- practices.push('ES6+ features', 'Async/await patterns', 'Module system');
277
- }
278
-
279
- if (techStack.frameworks.includes('react')) {
280
- practices.push('Hooks patterns', 'Context API', 'Component lifecycle');
281
- }
282
-
283
- if (techStack.languages.includes('ruby')) {
284
- practices.push('Ruby idioms', 'Metaprogramming carefully', 'Convention over configuration');
285
- }
286
-
287
- if (techStack.languages.includes('go')) {
288
- practices.push('Goroutines and channels', 'Error handling patterns', 'Interface design');
289
- }
290
-
291
- return practices;
366
+ // Generate the agent file
367
+ await this.agentGenerator.generateDynamicAgent(agentType, config);
368
+
369
+ // Load it immediately so we return the full agent object
370
+ const agent = await this.agentGenerator.loadAgent(agentType);
371
+
372
+ // If loading failed, return minimal object
373
+ return agent || { name: agentType, content: '', domain };
292
374
  }
293
375
 
294
376
  /**
@@ -349,31 +431,18 @@ class MandatoryAgentRouter {
349
431
  filterFiles(files, pattern) {
350
432
  return files.filter(file => {
351
433
  // Check if file should be excluded
352
- for (const exclude of pattern.exclude) {
353
- if (file.includes(exclude)) return false;
354
- }
434
+ const isExcluded = pattern.exclude.some(exclude => file.includes(exclude));
435
+ if (isExcluded) return false;
355
436
 
356
437
  // Check if file matches include patterns
357
438
  if (pattern.include.length > 0) {
358
- let matches = false;
359
- for (const include of pattern.include) {
360
- if (file.includes(include)) {
361
- matches = true;
362
- break;
363
- }
364
- }
365
- if (!matches) return false;
439
+ const isIncluded = pattern.include.some(include => file.includes(include));
440
+ if (!isIncluded) return false;
366
441
  }
367
442
 
368
443
  // Check extensions if specified
369
444
  if (pattern.extensions.length > 0) {
370
- let hasValidExtension = false;
371
- for (const ext of pattern.extensions) {
372
- if (file.endsWith(ext)) {
373
- hasValidExtension = true;
374
- break;
375
- }
376
- }
445
+ const hasValidExtension = pattern.extensions.some(ext => file.endsWith(ext));
377
446
  if (!hasValidExtension) return false;
378
447
  }
379
448
 
@@ -467,13 +536,11 @@ class MandatoryAgentRouter {
467
536
  });
468
537
 
469
538
  // Find most used agent
470
- let maxUsage = 0;
471
- for (const [agent, count] of Object.entries(stats.byAgent)) {
472
- if (count > maxUsage) {
473
- maxUsage = count;
474
- stats.mostUsedAgent = agent;
475
- }
476
- }
539
+ const mostUsed = Object.entries(stats.byAgent).reduce((max, [agent, count]) => {
540
+ return count > max.count ? { agent, count } : max;
541
+ }, { agent: null, count: 0 });
542
+
543
+ stats.mostUsedAgent = mostUsed.agent;
477
544
 
478
545
  return stats;
479
546
  }