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,11 +10,13 @@ const promptBuilder = require('./prompt-builder')
10
10
  const toolRegistry = require('./tool-registry')
11
11
  const MandatoryAgentRouter = require('./agent-router')
12
12
  const ContextFilter = require('./context-filter')
13
+ const ContextEstimator = require('../domain/context-estimator')
13
14
 
14
15
  class CommandExecutor {
15
16
  constructor() {
16
17
  this.agentRouter = new MandatoryAgentRouter()
17
18
  this.contextFilter = new ContextFilter()
19
+ this.contextEstimator = null
18
20
  }
19
21
 
20
22
  /**
@@ -25,45 +27,74 @@ class CommandExecutor {
25
27
  // 1. Load template
26
28
  const template = await templateLoader.load(commandName)
27
29
 
28
- // 2. Build FULL context (before filtering)
29
- const fullContext = await contextBuilder.build(projectPath, params)
30
+ // 2. Build METADATA context only (lazy loading - no file reads yet)
31
+ const metadataContext = await contextBuilder.build(projectPath, params)
30
32
 
31
- // 3. Check if command requires agent
32
- const requiresAgent = template.metadata?.['required-agent'] ||
33
- this.isTaskCommand(commandName)
33
+ // 3. CRITICAL: Force agent assignment for ALL task-related commands
34
+ const requiresAgent = template.metadata?.['required-agent'] !== false &&
35
+ (template.metadata?.['required-agent'] === true ||
36
+ this.isTaskCommand(commandName) ||
37
+ this.shouldUseAgent(commandName))
34
38
 
35
- let context = fullContext
39
+ let context = metadataContext
36
40
  let assignedAgent = null
37
41
 
42
+ // MANDATORY: Assign specialized agent for task commands
38
43
  if (requiresAgent) {
39
- // 4. MANDATORY: Assign specialized agent
44
+ // 4. Create task object for analysis
40
45
  const task = {
41
46
  description: params.task || params.description || commandName,
42
47
  type: commandName
43
48
  }
44
49
 
50
+ // 5. LAZY CONTEXT: Analyze task FIRST, then estimate files needed
51
+ // This avoids reading all files before knowing what we need
45
52
  const agentAssignment = await this.agentRouter.executeTask(
46
53
  task,
47
- fullContext,
54
+ metadataContext, // Only metadata, no files yet
48
55
  projectPath
49
56
  )
50
57
 
51
58
  assignedAgent = agentAssignment.agent
59
+ const taskAnalysis = agentAssignment.taskAnalysis
60
+
61
+ // Validate agent was assigned
62
+ if (!assignedAgent || !assignedAgent.name) {
63
+ throw new Error(
64
+ `CRITICAL: Failed to assign agent for command "${commandName}". ` +
65
+ `System requires ALL task commands to use specialized agents.`
66
+ )
67
+ }
68
+
69
+ // 6. PRE-FILTER: Estimate which files are needed BEFORE reading
70
+ if (!this.contextEstimator) {
71
+ this.contextEstimator = new ContextEstimator()
72
+ }
73
+
74
+ const estimatedFiles = await this.contextEstimator.estimateFiles(
75
+ taskAnalysis,
76
+ projectPath
77
+ )
52
78
 
53
- // 5. Filter context for this specific agent (70-90% reduction)
79
+ // 7. Build context ONLY with estimated files (lazy loading)
54
80
  const filtered = await this.contextFilter.filterForAgent(
55
81
  assignedAgent,
56
82
  task,
57
83
  projectPath,
58
- fullContext
84
+ {
85
+ ...metadataContext,
86
+ estimatedFiles, // Pre-filtered file list
87
+ fileCount: estimatedFiles.length
88
+ }
59
89
  )
60
90
 
61
91
  context = {
62
92
  ...filtered,
63
93
  agent: assignedAgent,
64
- originalSize: fullContext.files?.length || 0,
94
+ originalSize: estimatedFiles.length, // Estimated, not actual full size
65
95
  filteredSize: filtered.files?.length || 0,
66
- reduction: filtered.metrics?.reductionPercent || 0
96
+ reduction: filtered.metrics?.reductionPercent || 0,
97
+ lazyLoaded: true // Flag indicating lazy loading was used
67
98
  }
68
99
  }
69
100
 
@@ -100,10 +131,27 @@ class CommandExecutor {
100
131
  * Check if command is task-related
101
132
  */
102
133
  isTaskCommand(commandName) {
103
- const taskCommands = ['work', 'now', 'build', 'feature', 'bug', 'done']
134
+ const taskCommands = [
135
+ 'work', 'now', 'build', 'feature', 'bug', 'done',
136
+ 'task', 'design', 'cleanup', 'fix', 'test'
137
+ ]
104
138
  return taskCommands.includes(commandName)
105
139
  }
106
140
 
141
+ /**
142
+ * Determine if command should use an agent
143
+ * Expanded list of commands that benefit from agent specialization
144
+ */
145
+ shouldUseAgent(commandName) {
146
+ // Commands that should ALWAYS use agents
147
+ const agentCommands = [
148
+ 'work', 'now', 'build', 'feature', 'bug', 'done',
149
+ 'task', 'design', 'cleanup', 'fix', 'test',
150
+ 'sync', 'analyze' // These analyze/modify code, need specialization
151
+ ]
152
+ return agentCommands.includes(commandName)
153
+ }
154
+
107
155
  /**
108
156
  * Execute tool with permission check
109
157
  * @param {string} toolName - Tool name
@@ -9,9 +9,7 @@
9
9
 
10
10
  const fs = require('fs').promises;
11
11
  const path = require('path');
12
- const glob = require('glob');
13
- const { promisify } = require('util');
14
- const globAsync = promisify(glob);
12
+ const { glob } = require('glob');
15
13
 
16
14
  class ContextFilter {
17
15
  constructor() {
@@ -27,10 +25,31 @@ class ContextFilter {
27
25
 
28
26
  /**
29
27
  * Main entry point - filters context based on agent and task
28
+ * IMPROVED: Supports pre-estimated files for lazy loading
30
29
  */
31
30
  async filterForAgent(agent, task, projectPath, fullContext = {}) {
32
31
  const startTime = Date.now();
33
32
 
33
+ // If files were pre-estimated (lazy loading), use them
34
+ if (fullContext.estimatedFiles && fullContext.estimatedFiles.length > 0) {
35
+ const filteredFiles = fullContext.estimatedFiles;
36
+
37
+ const metrics = this.calculateMetrics(
38
+ fullContext.fileCount || filteredFiles.length,
39
+ filteredFiles.length,
40
+ startTime
41
+ );
42
+
43
+ return {
44
+ files: filteredFiles,
45
+ patterns: { preEstimated: true },
46
+ metrics,
47
+ agent: agent.name,
48
+ filtered: true
49
+ };
50
+ }
51
+
52
+ // Fallback to traditional filtering if no pre-estimation
34
53
  // Determine what files this agent needs
35
54
  const relevantPatterns = await this.determineRelevantPatterns(
36
55
  agent,
@@ -270,93 +289,29 @@ class ContextFilter {
270
289
 
271
290
  /**
272
291
  * Detect technologies used in the project
292
+ * NOW USES TechDetector - NO HARDCODING
273
293
  */
274
294
  async detectProjectTechnologies(projectPath) {
275
- const detected = new Set();
276
-
277
295
  try {
278
- // Check package.json for JS/TS projects
279
- const packageJsonPath = path.join(projectPath, 'package.json');
280
- if (await this.fileExists(packageJsonPath)) {
281
- const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
282
-
283
- // Check dependencies
284
- const deps = {
285
- ...packageJson.dependencies,
286
- ...packageJson.devDependencies
287
- };
288
-
289
- // Detect frameworks
290
- if (deps.react) detected.add('react');
291
- if (deps.vue) detected.add('vue');
292
- if (deps['@angular/core']) detected.add('angular');
293
- if (deps.express) detected.add('express');
294
- if (deps.next) detected.add('nextjs');
295
- if (deps.fastify) detected.add('fastify');
296
-
297
- // Language
298
- if (deps.typescript) detected.add('typescript');
299
- else detected.add('javascript');
300
- }
301
-
302
- // Check Gemfile for Ruby projects
303
- const gemfilePath = path.join(projectPath, 'Gemfile');
304
- if (await this.fileExists(gemfilePath)) {
305
- detected.add('ruby');
306
- const gemfile = await fs.readFile(gemfilePath, 'utf8');
307
- if (gemfile.includes('rails')) detected.add('rails');
308
- }
309
-
310
- // Check requirements.txt or setup.py for Python
311
- const requirementsPath = path.join(projectPath, 'requirements.txt');
312
- const setupPyPath = path.join(projectPath, 'setup.py');
313
- if (await this.fileExists(requirementsPath) || await this.fileExists(setupPyPath)) {
314
- detected.add('python');
315
-
316
- if (await this.fileExists(requirementsPath)) {
317
- const requirements = await fs.readFile(requirementsPath, 'utf8');
318
- if (requirements.includes('django')) detected.add('django');
319
- if (requirements.includes('fastapi')) detected.add('fastapi');
320
- if (requirements.includes('flask')) detected.add('flask');
321
- }
322
- }
323
-
324
- // Check go.mod for Go projects
325
- const goModPath = path.join(projectPath, 'go.mod');
326
- if (await this.fileExists(goModPath)) {
327
- detected.add('go');
328
- }
329
-
330
- // Check Cargo.toml for Rust
331
- const cargoPath = path.join(projectPath, 'Cargo.toml');
332
- if (await this.fileExists(cargoPath)) {
333
- detected.add('rust');
334
- }
335
-
336
- // Check mix.exs for Elixir
337
- const mixPath = path.join(projectPath, 'mix.exs');
338
- if (await this.fileExists(mixPath)) {
339
- detected.add('elixir');
340
- }
341
-
342
- // Check for Java/Maven/Gradle
343
- const pomPath = path.join(projectPath, 'pom.xml');
344
- const gradlePath = path.join(projectPath, 'build.gradle');
345
- if (await this.fileExists(pomPath) || await this.fileExists(gradlePath)) {
346
- detected.add('java');
347
- }
348
-
349
- // Check composer.json for PHP
350
- const composerPath = path.join(projectPath, 'composer.json');
351
- if (await this.fileExists(composerPath)) {
352
- detected.add('php');
353
- }
354
-
296
+ const TechDetector = require('../domain/tech-detector');
297
+ const detector = new TechDetector(projectPath);
298
+ const tech = await detector.detectAll();
299
+
300
+ // Convert to array of all detected technologies
301
+ const all = [
302
+ ...tech.languages,
303
+ ...tech.frameworks,
304
+ ...tech.tools,
305
+ ...tech.databases,
306
+ ...tech.buildTools,
307
+ ...tech.testFrameworks
308
+ ];
309
+
310
+ return Array.from(new Set(all)); // Remove duplicates
355
311
  } catch (error) {
356
312
  console.error('Error detecting technologies:', error.message);
313
+ return [];
357
314
  }
358
-
359
- return Array.from(detected);
360
315
  }
361
316
 
362
317
  /**
@@ -445,27 +400,36 @@ class ContextFilter {
445
400
 
446
401
  // Execute glob searches
447
402
  for (const pattern of globPatterns) {
448
- const matches = await globAsync(pattern, {
403
+ const matches = await glob(pattern, {
449
404
  cwd: projectPath,
450
405
  ignore: patterns.exclude,
451
406
  nodir: true,
452
407
  follow: false
453
408
  });
454
409
 
455
- files.push(...matches);
410
+ // Ensure matches is always an array (glob v10+ returns array, but be defensive)
411
+ if (Array.isArray(matches)) {
412
+ files.push(...matches);
413
+ } else if (matches) {
414
+ // Convert iterable to array if needed
415
+ files.push(...Array.from(matches));
416
+ }
456
417
  }
457
418
 
458
419
  // Remove duplicates and sort
459
420
  const uniqueFiles = [...new Set(files)].sort();
460
421
 
461
422
  // Limit to reasonable number
462
- const maxFiles = 100;
423
+ const maxFiles = 300;
463
424
  if (uniqueFiles.length > maxFiles) {
464
425
  console.log(`Limiting context to ${maxFiles} most relevant files`);
465
426
  return uniqueFiles.slice(0, maxFiles);
466
427
  }
467
428
 
468
- return uniqueFiles;
429
+ // Expand context with related files
430
+ const expandedFiles = await this.expandContext(uniqueFiles);
431
+
432
+ return expandedFiles.slice(0, maxFiles);
469
433
 
470
434
  } catch (error) {
471
435
  console.error('Error loading files:', error.message);
@@ -530,6 +494,46 @@ class ContextFilter {
530
494
  }
531
495
  }
532
496
 
497
+ /**
498
+ * Expand context with related files (tests, styles, etc.)
499
+ */
500
+ async expandContext(files) {
501
+ const expanded = new Set(files);
502
+
503
+ for (const file of files) {
504
+ const ext = path.extname(file);
505
+ const basename = path.basename(file, ext);
506
+ const dirname = path.dirname(file);
507
+
508
+ // 1. Look for test files
509
+ const testPatterns = [
510
+ path.join(dirname, `${basename}.test${ext}`),
511
+ path.join(dirname, `${basename}.spec${ext}`),
512
+ path.join(dirname, '__tests__', `${basename}.test${ext}`),
513
+ path.join(dirname, 'tests', `${basename}.test${ext}`)
514
+ ];
515
+
516
+ // 2. Look for style files (for UI components)
517
+ const stylePatterns = [
518
+ path.join(dirname, `${basename}.css`),
519
+ path.join(dirname, `${basename}.scss`),
520
+ path.join(dirname, `${basename}.module.css`),
521
+ path.join(dirname, `${basename}.module.scss`)
522
+ ];
523
+
524
+ // Check if these related files exist
525
+ const potentialFiles = [...testPatterns, ...stylePatterns];
526
+
527
+ for (const potential of potentialFiles) {
528
+ if (!expanded.has(potential) && await this.fileExists(potential)) {
529
+ expanded.add(potential);
530
+ }
531
+ }
532
+ }
533
+
534
+ return Array.from(expanded).sort();
535
+ }
536
+
533
537
  /**
534
538
  * Get filter statistics
535
539
  */
@@ -7,13 +7,45 @@
7
7
  class PromptBuilder {
8
8
  /**
9
9
  * Build concise prompt - only essentials
10
+ * CRITICAL: Includes full agent content if agent is provided
10
11
  */
11
12
  build(template, context, state, agent = null) {
12
13
  const parts = []
13
14
 
14
15
  // Agent assignment (if applicable)
16
+ // CRITICAL: Include full agent content, not just name
15
17
  if (agent) {
16
- parts.push(`AGENT: ${agent.name}\n`)
18
+ parts.push(`# AGENT ASSIGNMENT\n`)
19
+ parts.push(`Agent: ${agent.name}\n`)
20
+
21
+ // Include role if available
22
+ if (agent.role) {
23
+ parts.push(`Role: ${agent.role}\n`)
24
+ }
25
+
26
+ // Include domain if available
27
+ if (agent.domain) {
28
+ parts.push(`Domain: ${agent.domain}\n`)
29
+ }
30
+
31
+ // Include skills if available
32
+ if (agent.skills && agent.skills.length > 0) {
33
+ parts.push(`Skills: ${agent.skills.join(', ')}\n`)
34
+ }
35
+
36
+ parts.push(`\n## AGENT INSTRUCTIONS\n`)
37
+
38
+ // CRITICAL: Include full agent content
39
+ // This is the specialized knowledge for this project
40
+ if (agent.content) {
41
+ parts.push(agent.content)
42
+ parts.push(`\n`)
43
+ } else if (agent.name) {
44
+ // Fallback if content not loaded
45
+ parts.push(`You are the ${agent.name} agent for this project.\n`)
46
+ parts.push(`Apply your specialized expertise to complete the task.\n\n`)
47
+ }
48
+
17
49
  parts.push(`CONTEXT: ${context.filteredSize || 'all'} files (${context.reduction || 0}% reduced)\n\n`)
18
50
  }
19
51
 
@@ -49,6 +81,9 @@ class PromptBuilder {
49
81
  parts.push('\n')
50
82
  }
51
83
 
84
+ // Enforcement (Strict Mode)
85
+ parts.push(this.buildEnforcement());
86
+
52
87
  // Simple execution directive
53
88
  parts.push('\nEXECUTE: Follow flow. Use tools. Decide.\n')
54
89
 
@@ -91,6 +126,21 @@ class PromptBuilder {
91
126
 
92
127
  return parts.join('')
93
128
  }
129
+
130
+ /**
131
+ * Build enforcement section
132
+ * Forces Claude to follow the process strictly
133
+ */
134
+ buildEnforcement() {
135
+ return `
136
+ ## PROCESS ENFORCEMENT
137
+ 1. FOLLOW the Flow strictly. Do not skip steps.
138
+ 2. USE the allowed tools only.
139
+ 3. IF you are stuck, use the "Ask" tool or stop.
140
+ 4. DO NOT hallucinate files or commands.
141
+ 5. ALWAYS verify your changes.
142
+ `;
143
+ }
94
144
  }
95
145
 
96
146
  module.exports = new PromptBuilder()
package/core/commands.js CHANGED
@@ -1133,7 +1133,6 @@ class PrjctCommands {
1133
1133
  */
1134
1134
  async _cleanupMemory(projectPath) {
1135
1135
  const projectId = await configManager.getProjectId(projectPath)
1136
- const globalPath = pathManager.getGlobalProjectPath(projectId)
1137
1136
 
1138
1137
  console.log('📊 Analyzing disk usage...\n')
1139
1138
 
@@ -2474,7 +2473,7 @@ Agent: ${agent}
2474
2473
  const AgentGenerator = require('./domain/agent-generator')
2475
2474
  const generator = new AgentGenerator(projectId)
2476
2475
 
2477
- const generatedAgents = await this._generateAgentsFromAnalysis(summaryContent, generator)
2476
+ const generatedAgents = await this._generateAgentsFromAnalysis(summaryContent, generator, projectPath)
2478
2477
 
2479
2478
  // Step 4: Log to memory
2480
2479
  await this.logToMemory(projectPath, 'agents_generated', {
@@ -2506,91 +2505,118 @@ Agent: ${agent}
2506
2505
 
2507
2506
  /**
2508
2507
  * Generate agents dynamically from analysis summary
2509
- * Claude decides based on what technologies are detected
2508
+ * 100% DYNAMIC - Uses TechDetector, NO HARDCODING
2509
+ * Claude decides based on actual detected technologies
2510
2510
  * @private
2511
2511
  */
2512
- async _generateAgentsFromAnalysis(summaryContent, generator) {
2512
+ async _generateAgentsFromAnalysis(summaryContent, generator, projectPath) {
2513
2513
  const agents = []
2514
+ const TechDetector = require('./domain/tech-detector')
2515
+ const detector = new TechDetector(projectPath)
2516
+ const tech = await detector.detectAll()
2514
2517
 
2515
- // Parse summary to identify technologies
2516
- // Simple detection based on sections (NOT predetermined patterns)
2518
+ // Generate agents based on ACTUAL detected technologies
2519
+ // No assumptions, no hardcoding - just what we found
2517
2520
 
2518
- // Detect languages/frameworks from summary
2519
- const hasJavaScript =
2520
- summaryContent.includes('JavaScript') || summaryContent.includes('TypeScript')
2521
- const hasNextJS = summaryContent.includes('Next.js')
2522
- const hasVite = summaryContent.includes('Vite')
2523
- const hasReact = summaryContent.includes('react')
2524
- const hasRust = summaryContent.includes('Rust')
2525
- const hasGo = summaryContent.includes('Go')
2526
- const hasPython = summaryContent.includes('Python')
2527
- const hasDocker = summaryContent.includes('Docker')
2528
-
2529
- // Generate agents based on detected stack
2530
- // Each agent is specific to THIS project's stack
2521
+ // Frontend agents - if we have frontend frameworks
2522
+ const frontendFrameworks = tech.frameworks.filter(f =>
2523
+ ['react', 'vue', 'angular', 'svelte', 'next', 'nuxt', 'sveltekit', 'remix'].includes(f.toLowerCase())
2524
+ )
2525
+ const frontendBuildTools = tech.buildTools.filter(t =>
2526
+ ['vite', 'webpack', 'rollup', 'esbuild'].includes(t.toLowerCase())
2527
+ )
2531
2528
 
2532
- if (hasJavaScript || hasNextJS || hasVite) {
2529
+ if (frontendFrameworks.length > 0 || frontendBuildTools.length > 0 || tech.languages.includes('JavaScript') || tech.languages.includes('TypeScript')) {
2530
+ const frameworkList = frontendFrameworks.length > 0
2531
+ ? frontendFrameworks.join(', ')
2532
+ : (frontendBuildTools.length > 0 ? frontendBuildTools.join(', ') : 'JavaScript/TypeScript')
2533
+
2533
2534
  await generator.generateDynamicAgent('frontend-specialist', {
2534
2535
  role: 'Frontend Development Specialist',
2535
- expertise: `${hasNextJS ? 'Next.js' : hasVite ? 'Vite' : 'JavaScript/TypeScript'}, ${hasReact ? 'React' : 'Modern JavaScript frameworks'}`,
2536
- responsibilities:
2537
- 'Handle UI components, state management, routing, and frontend architecture',
2536
+ expertise: `${frameworkList}, ${tech.languages.filter(l => ['JavaScript', 'TypeScript'].includes(l)).join(' or ') || 'Modern JavaScript'}`,
2537
+ responsibilities: 'Handle UI components, state management, routing, and frontend architecture',
2538
2538
  projectContext: {
2539
- detectedFramework: hasNextJS ? 'Next.js' : hasVite ? 'Vite + React' : 'JavaScript',
2539
+ detectedFrameworks: frontendFrameworks,
2540
+ buildTools: frontendBuildTools,
2541
+ languages: tech.languages.filter(l => ['JavaScript', 'TypeScript'].includes(l))
2540
2542
  },
2541
2543
  })
2542
2544
  agents.push('frontend-specialist')
2543
2545
  }
2544
2546
 
2545
- if (hasRust) {
2546
- await generator.generateDynamicAgent('rust-developer', {
2547
- role: 'Rust Development Specialist',
2548
- expertise: 'Rust, Cargo, performance optimization, memory safety',
2549
- responsibilities:
2550
- 'Handle Rust codebase, performance-critical components, systems programming',
2551
- projectContext: { language: 'Rust' },
2552
- })
2553
- agents.push('rust-developer')
2554
- }
2547
+ // Backend agents - if we have backend frameworks or languages
2548
+ const backendFrameworks = tech.frameworks.filter(f =>
2549
+ ['express', 'fastify', 'koa', 'hapi', 'nest', 'django', 'flask', 'fastapi', 'rails', 'phoenix', 'laravel'].includes(f.toLowerCase())
2550
+ )
2551
+ const backendLanguages = tech.languages.filter(l =>
2552
+ ['Go', 'Rust', 'Python', 'Ruby', 'Elixir', 'Java', 'PHP'].includes(l)
2553
+ )
2555
2554
 
2556
- if (hasGo) {
2557
- await generator.generateDynamicAgent('go-developer', {
2558
- role: 'Go Development Specialist',
2559
- expertise: 'Go, Go modules, concurrency, backend services',
2560
- responsibilities: 'Handle Go codebase, backend services, API development',
2561
- projectContext: { language: 'Go' },
2555
+ if (backendFrameworks.length > 0 || backendLanguages.length > 0) {
2556
+ const agentName = backendLanguages.length > 0
2557
+ ? `${backendLanguages[0].toLowerCase()}-developer`
2558
+ : 'backend-specialist'
2559
+
2560
+ const expertise = backendFrameworks.length > 0
2561
+ ? backendFrameworks.join(', ')
2562
+ : backendLanguages.join(', ')
2563
+
2564
+ await generator.generateDynamicAgent(agentName, {
2565
+ role: `${backendLanguages[0] || 'Backend'} Development Specialist`,
2566
+ expertise,
2567
+ responsibilities: 'Handle backend services, API development, server logic',
2568
+ projectContext: {
2569
+ detectedFrameworks: backendFrameworks,
2570
+ languages: backendLanguages
2571
+ },
2562
2572
  })
2563
- agents.push('go-developer')
2573
+ agents.push(agentName)
2564
2574
  }
2565
2575
 
2566
- if (hasPython) {
2567
- await generator.generateDynamicAgent('python-developer', {
2568
- role: 'Python Development Specialist',
2569
- expertise: 'Python, pip, Django/Flask, data processing',
2570
- responsibilities: 'Handle Python codebase, backend logic, data processing',
2571
- projectContext: { language: 'Python' },
2576
+ // Database specialist - if we have database tools
2577
+ if (tech.databases.length > 0) {
2578
+ await generator.generateDynamicAgent('database-specialist', {
2579
+ role: 'Database Specialist',
2580
+ expertise: tech.databases.join(', '),
2581
+ responsibilities: 'Handle database design, queries, migrations, data modeling',
2582
+ projectContext: {
2583
+ databases: tech.databases
2584
+ },
2572
2585
  })
2573
- agents.push('python-developer')
2586
+ agents.push('database-specialist')
2574
2587
  }
2575
2588
 
2576
- if (hasDocker) {
2589
+ // DevOps specialist - if we have DevOps tools
2590
+ if (tech.tools.some(t => ['Docker', 'Kubernetes', 'Terraform'].includes(t))) {
2577
2591
  await generator.generateDynamicAgent('devops-specialist', {
2578
2592
  role: 'DevOps & Infrastructure Specialist',
2579
- expertise: 'Docker, Docker Compose, containerization, deployment',
2593
+ expertise: tech.tools.filter(t => ['Docker', 'Kubernetes', 'Terraform'].includes(t)).join(', '),
2580
2594
  responsibilities: 'Handle containerization, deployment, infrastructure setup',
2581
- projectContext: { hasDocker: true },
2595
+ projectContext: {
2596
+ tools: tech.tools
2597
+ },
2582
2598
  })
2583
2599
  agents.push('devops-specialist')
2584
2600
  }
2585
2601
 
2586
- // Always generate a QA specialist if we have code
2587
- await generator.generateDynamicAgent('qa-specialist', {
2588
- role: 'Quality Assurance Specialist',
2589
- expertise: 'Testing frameworks, test automation, quality metrics',
2590
- responsibilities: 'Handle testing strategy, test creation, quality assurance',
2591
- projectContext: { role: 'QA' },
2592
- })
2593
- agents.push('qa-specialist')
2602
+ // QA specialist - always generate if we have test frameworks or any code
2603
+ if (tech.testFrameworks.length > 0 || tech.languages.length > 0) {
2604
+ const testExpertise = tech.testFrameworks.length > 0
2605
+ ? tech.testFrameworks.join(', ')
2606
+ : 'Testing frameworks, test automation'
2607
+
2608
+ await generator.generateDynamicAgent('qa-specialist', {
2609
+ role: 'Quality Assurance Specialist',
2610
+ expertise: testExpertise,
2611
+ responsibilities: 'Handle testing strategy, test creation, quality assurance',
2612
+ projectContext: {
2613
+ testFrameworks: tech.testFrameworks,
2614
+ languages: tech.languages,
2615
+ role: 'QA'
2616
+ },
2617
+ })
2618
+ agents.push('qa-specialist')
2619
+ }
2594
2620
 
2595
2621
  return agents
2596
2622
  }