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.
Files changed (53) hide show
  1. package/CHANGELOG.md +142 -0
  2. package/core/__tests__/agentic/memory-system.test.js +263 -0
  3. package/core/__tests__/agentic/plan-mode.test.js +336 -0
  4. package/core/agentic/agent-router.js +253 -186
  5. package/core/agentic/chain-of-thought.js +578 -0
  6. package/core/agentic/command-executor.js +299 -17
  7. package/core/agentic/context-builder.js +208 -8
  8. package/core/agentic/context-filter.js +83 -83
  9. package/core/agentic/ground-truth.js +591 -0
  10. package/core/agentic/loop-detector.js +406 -0
  11. package/core/agentic/memory-system.js +850 -0
  12. package/core/agentic/parallel-tools.js +366 -0
  13. package/core/agentic/plan-mode.js +572 -0
  14. package/core/agentic/prompt-builder.js +127 -2
  15. package/core/agentic/response-templates.js +290 -0
  16. package/core/agentic/semantic-compression.js +517 -0
  17. package/core/agentic/think-blocks.js +657 -0
  18. package/core/agentic/tool-registry.js +32 -0
  19. package/core/agentic/validation-rules.js +380 -0
  20. package/core/command-registry.js +48 -0
  21. package/core/commands.js +128 -60
  22. package/core/context-sync.js +183 -0
  23. package/core/domain/agent-generator.js +77 -46
  24. package/core/domain/agent-loader.js +183 -0
  25. package/core/domain/agent-matcher.js +217 -0
  26. package/core/domain/agent-validator.js +217 -0
  27. package/core/domain/context-estimator.js +175 -0
  28. package/core/domain/product-standards.js +92 -0
  29. package/core/domain/smart-cache.js +157 -0
  30. package/core/domain/task-analyzer.js +353 -0
  31. package/core/domain/tech-detector.js +365 -0
  32. package/package.json +8 -16
  33. package/templates/commands/done.md +7 -0
  34. package/templates/commands/feature.md +8 -0
  35. package/templates/commands/ship.md +8 -0
  36. package/templates/commands/spec.md +128 -0
  37. package/templates/global/CLAUDE.md +17 -0
  38. package/core/__tests__/agentic/agent-router.test.js +0 -398
  39. package/core/__tests__/agentic/command-executor.test.js +0 -223
  40. package/core/__tests__/agentic/context-builder.test.js +0 -160
  41. package/core/__tests__/agentic/context-filter.test.js +0 -494
  42. package/core/__tests__/agentic/prompt-builder.test.js +0 -212
  43. package/core/__tests__/agentic/template-loader.test.js +0 -164
  44. package/core/__tests__/agentic/tool-registry.test.js +0 -243
  45. package/core/__tests__/domain/agent-generator.test.js +0 -296
  46. package/core/__tests__/domain/analyzer.test.js +0 -324
  47. package/core/__tests__/infrastructure/author-detector.test.js +0 -103
  48. package/core/__tests__/infrastructure/config-manager.test.js +0 -454
  49. package/core/__tests__/infrastructure/path-manager.test.js +0 -412
  50. package/core/__tests__/setup.test.js +0 -15
  51. package/core/__tests__/utils/date-helper.test.js +0 -169
  52. package/core/__tests__/utils/file-helper.test.js +0 -258
  53. 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
+