prjct-cli 0.9.2 → 0.10.2
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.
- package/CHANGELOG.md +142 -0
- package/core/__tests__/agentic/memory-system.test.js +263 -0
- package/core/__tests__/agentic/plan-mode.test.js +336 -0
- package/core/agentic/agent-router.js +253 -186
- package/core/agentic/chain-of-thought.js +578 -0
- package/core/agentic/command-executor.js +299 -17
- package/core/agentic/context-builder.js +208 -8
- package/core/agentic/context-filter.js +83 -83
- package/core/agentic/ground-truth.js +591 -0
- package/core/agentic/loop-detector.js +406 -0
- package/core/agentic/memory-system.js +850 -0
- package/core/agentic/parallel-tools.js +366 -0
- package/core/agentic/plan-mode.js +572 -0
- package/core/agentic/prompt-builder.js +127 -2
- package/core/agentic/response-templates.js +290 -0
- package/core/agentic/semantic-compression.js +517 -0
- package/core/agentic/think-blocks.js +657 -0
- package/core/agentic/tool-registry.js +32 -0
- package/core/agentic/validation-rules.js +380 -0
- package/core/command-registry.js +48 -0
- package/core/commands.js +128 -60
- package/core/context-sync.js +183 -0
- package/core/domain/agent-generator.js +77 -46
- package/core/domain/agent-loader.js +183 -0
- package/core/domain/agent-matcher.js +217 -0
- package/core/domain/agent-validator.js +217 -0
- package/core/domain/context-estimator.js +175 -0
- package/core/domain/product-standards.js +92 -0
- package/core/domain/smart-cache.js +157 -0
- package/core/domain/task-analyzer.js +353 -0
- package/core/domain/tech-detector.js +365 -0
- package/package.json +8 -16
- package/templates/commands/done.md +7 -0
- package/templates/commands/feature.md +8 -0
- package/templates/commands/ship.md +8 -0
- package/templates/commands/spec.md +128 -0
- package/templates/global/CLAUDE.md +17 -0
- package/core/__tests__/agentic/agent-router.test.js +0 -398
- package/core/__tests__/agentic/command-executor.test.js +0 -223
- package/core/__tests__/agentic/context-builder.test.js +0 -160
- package/core/__tests__/agentic/context-filter.test.js +0 -494
- package/core/__tests__/agentic/prompt-builder.test.js +0 -212
- package/core/__tests__/agentic/template-loader.test.js +0 -164
- package/core/__tests__/agentic/tool-registry.test.js +0 -243
- package/core/__tests__/domain/agent-generator.test.js +0 -296
- package/core/__tests__/domain/analyzer.test.js +0 -324
- package/core/__tests__/infrastructure/author-detector.test.js +0 -103
- package/core/__tests__/infrastructure/config-manager.test.js +0 -454
- package/core/__tests__/infrastructure/path-manager.test.js +0 -412
- package/core/__tests__/setup.test.js +0 -15
- package/core/__tests__/utils/date-helper.test.js +0 -169
- package/core/__tests__/utils/file-helper.test.js +0 -258
- package/core/__tests__/utils/jsonl-helper.test.js +0 -387
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentLoader - Loads agents from project files
|
|
3
|
+
*
|
|
4
|
+
* CRITICAL: This ensures agents generated for a project are actually USED
|
|
5
|
+
*
|
|
6
|
+
* @version 1.0.0
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs').promises
|
|
10
|
+
const path = require('path')
|
|
11
|
+
const os = require('os')
|
|
12
|
+
|
|
13
|
+
class AgentLoader {
|
|
14
|
+
constructor(projectId = null) {
|
|
15
|
+
this.projectId = projectId
|
|
16
|
+
this.agentsDir = projectId
|
|
17
|
+
? path.join(os.homedir(), '.prjct-cli', 'projects', projectId, 'agents')
|
|
18
|
+
: path.join(os.homedir(), '.prjct-cli', 'agents')
|
|
19
|
+
this.cache = new Map()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Load an agent from its file
|
|
24
|
+
* @param {string} agentName - Name of the agent (without .md extension)
|
|
25
|
+
* @returns {Promise<Object|null>} - Agent object with name and content, or null if not found
|
|
26
|
+
*/
|
|
27
|
+
async loadAgent(agentName) {
|
|
28
|
+
// Check cache first
|
|
29
|
+
const cacheKey = `${this.projectId || 'global'}-${agentName}`
|
|
30
|
+
if (this.cache.has(cacheKey)) {
|
|
31
|
+
return this.cache.get(cacheKey)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const agentPath = path.join(this.agentsDir, `${agentName}.md`)
|
|
36
|
+
const content = await fs.readFile(agentPath, 'utf-8')
|
|
37
|
+
|
|
38
|
+
// Parse agent metadata from content
|
|
39
|
+
const agent = {
|
|
40
|
+
name: agentName,
|
|
41
|
+
content,
|
|
42
|
+
path: agentPath,
|
|
43
|
+
// Extract role if present in content
|
|
44
|
+
role: this.extractRole(content),
|
|
45
|
+
// Extract domain if present
|
|
46
|
+
domain: this.extractDomain(content),
|
|
47
|
+
// Extract skills/technologies mentioned
|
|
48
|
+
skills: this.extractSkills(content),
|
|
49
|
+
// Last modified time
|
|
50
|
+
modified: (await fs.stat(agentPath)).mtime
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Cache it
|
|
54
|
+
this.cache.set(cacheKey, agent)
|
|
55
|
+
|
|
56
|
+
return agent
|
|
57
|
+
} catch (error) {
|
|
58
|
+
if (error.code === 'ENOENT') {
|
|
59
|
+
return null // Agent file doesn't exist
|
|
60
|
+
}
|
|
61
|
+
throw error
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Load all agents for the project
|
|
67
|
+
* @returns {Promise<Array<Object>>} - Array of agent objects
|
|
68
|
+
*/
|
|
69
|
+
async loadAllAgents() {
|
|
70
|
+
try {
|
|
71
|
+
const files = await fs.readdir(this.agentsDir)
|
|
72
|
+
const agentFiles = files.filter(f => f.endsWith('.md') && !f.startsWith('.'))
|
|
73
|
+
|
|
74
|
+
const agents = []
|
|
75
|
+
for (const file of agentFiles) {
|
|
76
|
+
const agentName = file.replace('.md', '')
|
|
77
|
+
const agent = await this.loadAgent(agentName)
|
|
78
|
+
if (agent) {
|
|
79
|
+
agents.push(agent)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return agents
|
|
84
|
+
} catch (error) {
|
|
85
|
+
if (error.code === 'ENOENT') {
|
|
86
|
+
return [] // Agents directory doesn't exist yet
|
|
87
|
+
}
|
|
88
|
+
throw error
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Check if an agent exists
|
|
94
|
+
* @param {string} agentName - Name of the agent
|
|
95
|
+
* @returns {Promise<boolean>} - True if agent file exists
|
|
96
|
+
*/
|
|
97
|
+
async agentExists(agentName) {
|
|
98
|
+
try {
|
|
99
|
+
const agentPath = path.join(this.agentsDir, `${agentName}.md`)
|
|
100
|
+
await fs.access(agentPath)
|
|
101
|
+
return true
|
|
102
|
+
} catch {
|
|
103
|
+
return false
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Clear cache (useful after agent updates)
|
|
109
|
+
*/
|
|
110
|
+
clearCache() {
|
|
111
|
+
this.cache.clear()
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Extract role from agent content
|
|
116
|
+
* @private
|
|
117
|
+
*/
|
|
118
|
+
extractRole(content) {
|
|
119
|
+
const roleMatch = content.match(/Role:\s*(.+)/i)
|
|
120
|
+
return roleMatch ? roleMatch[1].trim() : null
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Extract domain from agent content
|
|
125
|
+
* @private
|
|
126
|
+
*/
|
|
127
|
+
extractDomain(content) {
|
|
128
|
+
const domainMatch = content.match(/DOMAIN AUTHORITY[\s\S]*?the\s+(\w+)\s+domain/i)
|
|
129
|
+
if (domainMatch) {
|
|
130
|
+
return domainMatch[1].toLowerCase()
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Fallback: try to detect from agent name
|
|
134
|
+
const name = this.projectId ? '' : ''
|
|
135
|
+
if (name.includes('frontend')) return 'frontend'
|
|
136
|
+
if (name.includes('backend')) return 'backend'
|
|
137
|
+
if (name.includes('database')) return 'database'
|
|
138
|
+
if (name.includes('devops')) return 'devops'
|
|
139
|
+
if (name.includes('qa')) return 'qa'
|
|
140
|
+
if (name.includes('architect')) return 'architecture'
|
|
141
|
+
|
|
142
|
+
return 'general'
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Extract skills/technologies mentioned in agent content
|
|
147
|
+
* @private
|
|
148
|
+
*/
|
|
149
|
+
extractSkills(content) {
|
|
150
|
+
const skills = []
|
|
151
|
+
|
|
152
|
+
// Look for common technology mentions
|
|
153
|
+
const techKeywords = [
|
|
154
|
+
'React', 'Vue', 'Angular', 'Svelte',
|
|
155
|
+
'Next.js', 'Nuxt', 'SvelteKit',
|
|
156
|
+
'TypeScript', 'JavaScript',
|
|
157
|
+
'Node.js', 'Express', 'Fastify',
|
|
158
|
+
'Python', 'Django', 'Flask', 'FastAPI',
|
|
159
|
+
'Go', 'Rust', 'Ruby', 'Rails',
|
|
160
|
+
'PostgreSQL', 'MySQL', 'MongoDB',
|
|
161
|
+
'Docker', 'Kubernetes', 'Terraform'
|
|
162
|
+
]
|
|
163
|
+
|
|
164
|
+
for (const tech of techKeywords) {
|
|
165
|
+
if (content.includes(tech)) {
|
|
166
|
+
skills.push(tech)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return skills
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Get agents directory path
|
|
175
|
+
* @returns {string} - Path to agents directory
|
|
176
|
+
*/
|
|
177
|
+
getAgentsDir() {
|
|
178
|
+
return this.agentsDir
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
module.exports = AgentLoader
|
|
183
|
+
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentMatcher - Intelligent Agent Matching with Scoring
|
|
3
|
+
*
|
|
4
|
+
* Matches tasks to agents using multi-factor scoring:
|
|
5
|
+
* - Agent skills and capabilities
|
|
6
|
+
* - Historical success rates
|
|
7
|
+
* - Project technologies
|
|
8
|
+
* - Task complexity
|
|
9
|
+
*
|
|
10
|
+
* @version 1.0.0
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
class AgentMatcher {
|
|
14
|
+
constructor() {
|
|
15
|
+
this.historyCache = new Map()
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Find best agent for a task using intelligent scoring
|
|
20
|
+
* @param {Array<Object>} availableAgents - All available agents
|
|
21
|
+
* @param {Object} taskAnalysis - Task analysis result
|
|
22
|
+
* @returns {Object|null} Best matching agent with score
|
|
23
|
+
*/
|
|
24
|
+
findBestAgent(availableAgents, taskAnalysis) {
|
|
25
|
+
if (!availableAgents || availableAgents.length === 0) {
|
|
26
|
+
return null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Score each agent
|
|
30
|
+
const scored = availableAgents.map(agent => {
|
|
31
|
+
const score = this.scoreAgent(agent, taskAnalysis)
|
|
32
|
+
return { agent, score }
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
// Sort by score descending
|
|
36
|
+
scored.sort((a, b) => b.score - a.score)
|
|
37
|
+
|
|
38
|
+
// Return best match if score is above threshold
|
|
39
|
+
const best = scored[0]
|
|
40
|
+
if (best && best.score > 0.3) {
|
|
41
|
+
return {
|
|
42
|
+
agent: best.agent,
|
|
43
|
+
score: best.score,
|
|
44
|
+
alternatives: scored.slice(1, 3).map(s => ({
|
|
45
|
+
agent: s.agent,
|
|
46
|
+
score: s.score
|
|
47
|
+
}))
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return null
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Score an agent for a specific task
|
|
56
|
+
* Multi-factor scoring system
|
|
57
|
+
*/
|
|
58
|
+
scoreAgent(agent, taskAnalysis) {
|
|
59
|
+
let score = 0
|
|
60
|
+
|
|
61
|
+
// Factor 1: Domain Match (40% weight)
|
|
62
|
+
const domainScore = this.scoreDomainMatch(agent, taskAnalysis)
|
|
63
|
+
score += domainScore * 0.4
|
|
64
|
+
|
|
65
|
+
// Factor 2: Skills Match (30% weight)
|
|
66
|
+
const skillsScore = this.scoreSkillsMatch(agent, taskAnalysis)
|
|
67
|
+
score += skillsScore * 0.3
|
|
68
|
+
|
|
69
|
+
// Factor 3: Historical Success (20% weight)
|
|
70
|
+
const historyScore = this.scoreHistoricalSuccess(agent, taskAnalysis)
|
|
71
|
+
score += historyScore * 0.2
|
|
72
|
+
|
|
73
|
+
// Factor 4: Complexity Match (10% weight)
|
|
74
|
+
const complexityScore = this.scoreComplexityMatch(agent, taskAnalysis)
|
|
75
|
+
score += complexityScore * 0.1
|
|
76
|
+
|
|
77
|
+
return Math.min(score, 1.0)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Score domain match
|
|
82
|
+
*/
|
|
83
|
+
scoreDomainMatch(agent, taskAnalysis) {
|
|
84
|
+
const agentDomain = agent.domain || ''
|
|
85
|
+
const taskDomain = taskAnalysis.primaryDomain || ''
|
|
86
|
+
|
|
87
|
+
// Exact match
|
|
88
|
+
if (agentDomain === taskDomain) {
|
|
89
|
+
return 1.0
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Partial match (agent name contains domain)
|
|
93
|
+
if (agent.name && agent.name.includes(taskDomain)) {
|
|
94
|
+
return 0.7
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Check alternatives
|
|
98
|
+
if (taskAnalysis.alternatives && taskAnalysis.alternatives.includes(agentDomain)) {
|
|
99
|
+
return 0.5
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return 0.1
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Score skills match
|
|
107
|
+
*/
|
|
108
|
+
scoreSkillsMatch(agent, taskAnalysis) {
|
|
109
|
+
if (!agent.skills || agent.skills.length === 0) {
|
|
110
|
+
return 0.2 // Generic agent penalty
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const projectTech = taskAnalysis.projectTechnologies || {}
|
|
114
|
+
const allTech = [
|
|
115
|
+
...(projectTech.languages || []),
|
|
116
|
+
...(projectTech.frameworks || []),
|
|
117
|
+
...(projectTech.tools || [])
|
|
118
|
+
]
|
|
119
|
+
|
|
120
|
+
// Count matching skills
|
|
121
|
+
const matchingSkills = agent.skills.filter(skill => {
|
|
122
|
+
const skillLower = skill.toLowerCase()
|
|
123
|
+
return allTech.some(tech => tech.toLowerCase().includes(skillLower) ||
|
|
124
|
+
skillLower.includes(tech.toLowerCase()))
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
if (matchingSkills.length === 0) {
|
|
128
|
+
return 0.1 // No matching skills
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Score based on match ratio
|
|
132
|
+
const matchRatio = matchingSkills.length / Math.max(agent.skills.length, allTech.length)
|
|
133
|
+
return Math.min(matchRatio * 2, 1.0) // Boost for good matches
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Score historical success
|
|
138
|
+
*/
|
|
139
|
+
scoreHistoricalSuccess(agent, taskAnalysis) {
|
|
140
|
+
// TODO: Load from persistent history
|
|
141
|
+
// For now, return neutral score
|
|
142
|
+
const cacheKey = `${agent.name}-${taskAnalysis.primaryDomain}`
|
|
143
|
+
const history = this.historyCache.get(cacheKey)
|
|
144
|
+
|
|
145
|
+
if (history) {
|
|
146
|
+
// Success rate from history
|
|
147
|
+
return history.successRate || 0.5
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return 0.5 // Neutral - no history
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Score complexity match
|
|
155
|
+
*/
|
|
156
|
+
scoreComplexityMatch(agent, taskAnalysis) {
|
|
157
|
+
const taskComplexity = taskAnalysis.complexity || 'medium'
|
|
158
|
+
|
|
159
|
+
// Generic agents are better for simple tasks
|
|
160
|
+
// Specialized agents are better for complex tasks
|
|
161
|
+
const isGeneric = !agent.skills || agent.skills.length === 0
|
|
162
|
+
|
|
163
|
+
if (taskComplexity === 'low' && isGeneric) {
|
|
164
|
+
return 0.8
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (taskComplexity === 'high' && !isGeneric) {
|
|
168
|
+
return 0.9
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return 0.5 // Neutral
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Record agent success for learning
|
|
176
|
+
*/
|
|
177
|
+
recordSuccess(agent, taskAnalysis, success = true) {
|
|
178
|
+
const cacheKey = `${agent.name}-${taskAnalysis.primaryDomain}`
|
|
179
|
+
const history = this.historyCache.get(cacheKey) || {
|
|
180
|
+
attempts: 0,
|
|
181
|
+
successes: 0,
|
|
182
|
+
successRate: 0.5
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
history.attempts++
|
|
186
|
+
if (success) {
|
|
187
|
+
history.successes++
|
|
188
|
+
}
|
|
189
|
+
history.successRate = history.successes / history.attempts
|
|
190
|
+
|
|
191
|
+
this.historyCache.set(cacheKey, history)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Get match explanation
|
|
196
|
+
*/
|
|
197
|
+
explainMatch(match) {
|
|
198
|
+
if (!match) {
|
|
199
|
+
return 'No suitable agent found'
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const reasons = []
|
|
203
|
+
|
|
204
|
+
if (match.score > 0.8) {
|
|
205
|
+
reasons.push('Excellent match')
|
|
206
|
+
} else if (match.score > 0.6) {
|
|
207
|
+
reasons.push('Good match')
|
|
208
|
+
} else {
|
|
209
|
+
reasons.push('Acceptable match')
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return reasons.join(', ')
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
module.exports = AgentMatcher
|
|
217
|
+
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentValidator - Validates agents before and after generation
|
|
3
|
+
*
|
|
4
|
+
* Ensures agents are useful and not generic
|
|
5
|
+
* Compares with existing agents before generating
|
|
6
|
+
*
|
|
7
|
+
* @version 1.0.0
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
class AgentValidator {
|
|
11
|
+
/**
|
|
12
|
+
* Validate if agent should be generated
|
|
13
|
+
* @param {string} agentName - Proposed agent name
|
|
14
|
+
* @param {Object} config - Agent configuration
|
|
15
|
+
* @param {Array<Object>} existingAgents - Existing agents
|
|
16
|
+
* @returns {Object} Validation result
|
|
17
|
+
*/
|
|
18
|
+
validateBeforeGeneration(agentName, config, existingAgents = []) {
|
|
19
|
+
const issues = []
|
|
20
|
+
const warnings = []
|
|
21
|
+
|
|
22
|
+
// Check if similar agent exists
|
|
23
|
+
const similar = this.findSimilarAgent(agentName, config, existingAgents)
|
|
24
|
+
if (similar) {
|
|
25
|
+
warnings.push(`Similar agent exists: ${similar.name}`)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Check if agent has specific skills
|
|
29
|
+
if (!config.expertise || config.expertise.length < 10) {
|
|
30
|
+
issues.push('Agent expertise is too generic')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Check if agent has project context
|
|
34
|
+
if (!config.projectContext || Object.keys(config.projectContext).length === 0) {
|
|
35
|
+
warnings.push('Agent has no project-specific context')
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Check if agent name is descriptive
|
|
39
|
+
if (agentName.includes('specialist') && !config.expertise) {
|
|
40
|
+
issues.push('Agent name suggests specialization but has no expertise defined')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
valid: issues.length === 0,
|
|
45
|
+
issues,
|
|
46
|
+
warnings,
|
|
47
|
+
similarAgent: similar
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Validate agent after generation
|
|
53
|
+
* @param {Object} agent - Generated agent
|
|
54
|
+
* @returns {Object} Validation result
|
|
55
|
+
*/
|
|
56
|
+
validateAfterGeneration(agent) {
|
|
57
|
+
const issues = []
|
|
58
|
+
const warnings = []
|
|
59
|
+
|
|
60
|
+
// Check if agent has content
|
|
61
|
+
if (!agent.content || agent.content.length < 100) {
|
|
62
|
+
issues.push('Agent content is too short or missing')
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Check if agent has skills extracted
|
|
66
|
+
if (!agent.skills || agent.skills.length === 0) {
|
|
67
|
+
warnings.push('Agent has no skills detected')
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Check if agent has domain
|
|
71
|
+
if (!agent.domain || agent.domain === 'general') {
|
|
72
|
+
warnings.push('Agent domain is generic')
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Check if agent is useful (not too generic)
|
|
76
|
+
const usefulness = this.calculateUsefulness(agent)
|
|
77
|
+
if (usefulness < 0.5) {
|
|
78
|
+
issues.push('Agent is too generic to be useful')
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
valid: issues.length === 0,
|
|
83
|
+
issues,
|
|
84
|
+
warnings,
|
|
85
|
+
usefulness
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Find similar existing agent
|
|
91
|
+
*/
|
|
92
|
+
findSimilarAgent(agentName, config, existingAgents) {
|
|
93
|
+
for (const existing of existingAgents) {
|
|
94
|
+
// Check name similarity
|
|
95
|
+
if (this.namesSimilar(agentName, existing.name)) {
|
|
96
|
+
return existing
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Check domain similarity
|
|
100
|
+
if (config.domain && existing.domain && config.domain === existing.domain) {
|
|
101
|
+
// Check if skills overlap significantly
|
|
102
|
+
const skillOverlap = this.calculateSkillOverlap(config, existing)
|
|
103
|
+
if (skillOverlap > 0.7) {
|
|
104
|
+
return existing
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return null
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Check if agent names are similar
|
|
114
|
+
*/
|
|
115
|
+
namesSimilar(name1, name2) {
|
|
116
|
+
const n1 = name1.toLowerCase()
|
|
117
|
+
const n2 = name2.toLowerCase()
|
|
118
|
+
|
|
119
|
+
// Exact match
|
|
120
|
+
if (n1 === n2) return true
|
|
121
|
+
|
|
122
|
+
// One contains the other
|
|
123
|
+
if (n1.includes(n2) || n2.includes(n1)) return true
|
|
124
|
+
|
|
125
|
+
// Check word overlap
|
|
126
|
+
const words1 = new Set(n1.split('-'))
|
|
127
|
+
const words2 = new Set(n2.split('-'))
|
|
128
|
+
const intersection = new Set([...words1].filter(w => words2.has(w)))
|
|
129
|
+
const union = new Set([...words1, ...words2])
|
|
130
|
+
|
|
131
|
+
return intersection.size / union.size > 0.5
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Calculate skill overlap between config and existing agent
|
|
136
|
+
*/
|
|
137
|
+
calculateSkillOverlap(config, existingAgent) {
|
|
138
|
+
if (!existingAgent.skills || existingAgent.skills.length === 0) {
|
|
139
|
+
return 0
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Extract skills from config expertise
|
|
143
|
+
const configSkills = this.extractSkillsFromText(config.expertise || '')
|
|
144
|
+
if (configSkills.length === 0) {
|
|
145
|
+
return 0
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Calculate overlap
|
|
149
|
+
const existingSet = new Set(existingAgent.skills.map(s => s.toLowerCase()))
|
|
150
|
+
const configSet = new Set(configSkills.map(s => s.toLowerCase()))
|
|
151
|
+
|
|
152
|
+
const intersection = new Set([...existingSet].filter(s => configSet.has(s)))
|
|
153
|
+
const union = new Set([...existingSet, ...configSet])
|
|
154
|
+
|
|
155
|
+
return intersection.size / union.size
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Extract skills from text
|
|
160
|
+
*/
|
|
161
|
+
extractSkillsFromText(text) {
|
|
162
|
+
// Common technology keywords
|
|
163
|
+
const techKeywords = [
|
|
164
|
+
'React', 'Vue', 'Angular', 'Svelte',
|
|
165
|
+
'Next.js', 'Nuxt', 'SvelteKit',
|
|
166
|
+
'TypeScript', 'JavaScript',
|
|
167
|
+
'Node.js', 'Express', 'Fastify',
|
|
168
|
+
'Python', 'Django', 'Flask', 'FastAPI',
|
|
169
|
+
'Go', 'Rust', 'Ruby', 'Rails',
|
|
170
|
+
'PostgreSQL', 'MySQL', 'MongoDB'
|
|
171
|
+
]
|
|
172
|
+
|
|
173
|
+
return techKeywords.filter(tech =>
|
|
174
|
+
text.toLowerCase().includes(tech.toLowerCase())
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Calculate agent usefulness score
|
|
180
|
+
*/
|
|
181
|
+
calculateUsefulness(agent) {
|
|
182
|
+
let score = 0
|
|
183
|
+
|
|
184
|
+
// Has skills
|
|
185
|
+
if (agent.skills && agent.skills.length > 0) {
|
|
186
|
+
score += 0.3
|
|
187
|
+
if (agent.skills.length > 3) {
|
|
188
|
+
score += 0.1 // Bonus for multiple skills
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Has specific domain
|
|
193
|
+
if (agent.domain && agent.domain !== 'general') {
|
|
194
|
+
score += 0.2
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Has content
|
|
198
|
+
if (agent.content && agent.content.length > 200) {
|
|
199
|
+
score += 0.2
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Has role
|
|
203
|
+
if (agent.role && agent.role.length > 10) {
|
|
204
|
+
score += 0.1
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Not generic name
|
|
208
|
+
if (agent.name && !agent.name.includes('generalist')) {
|
|
209
|
+
score += 0.1
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return Math.min(score, 1.0)
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
module.exports = AgentValidator
|
|
217
|
+
|