prjct-cli 0.9.2 → 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.
- package/CHANGELOG.md +111 -0
- package/core/__tests__/agentic/context-filter.test.js +2 -2
- package/core/__tests__/agentic/prompt-builder.test.js +39 -47
- package/core/__tests__/domain/agent-generator.test.js +29 -36
- package/core/__tests__/domain/agent-loader.test.js +179 -0
- package/core/agentic/agent-router.js +253 -186
- package/core/agentic/command-executor.js +61 -13
- package/core/agentic/context-filter.js +83 -83
- package/core/agentic/prompt-builder.js +51 -1
- package/core/commands.js +85 -59
- 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 +2 -2
|
@@ -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
|
+
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ContextEstimator - Pre-filter files before building context
|
|
3
|
+
*
|
|
4
|
+
* Estimates which files are needed based on task analysis
|
|
5
|
+
* BEFORE building full context - saves I/O
|
|
6
|
+
*
|
|
7
|
+
* @version 1.0.0
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const path = require('path')
|
|
11
|
+
const { glob } = require('glob')
|
|
12
|
+
|
|
13
|
+
class ContextEstimator {
|
|
14
|
+
/**
|
|
15
|
+
* Estimate which files are needed for a task
|
|
16
|
+
* @param {Object} taskAnalysis - Task analysis result
|
|
17
|
+
* @param {string} projectPath - Project path
|
|
18
|
+
* @returns {Promise<Array<string>>} Estimated file paths
|
|
19
|
+
*/
|
|
20
|
+
async estimateFiles(taskAnalysis, projectPath) {
|
|
21
|
+
const domain = taskAnalysis.primaryDomain
|
|
22
|
+
const projectTech = taskAnalysis.projectTechnologies || {}
|
|
23
|
+
const semantic = taskAnalysis.semantic || {}
|
|
24
|
+
|
|
25
|
+
// Get patterns for this domain
|
|
26
|
+
const patterns = this.getPatternsForDomain(domain, projectTech)
|
|
27
|
+
|
|
28
|
+
// Expand with semantic understanding
|
|
29
|
+
if (semantic.requiresMultipleAgents && semantic.agents) {
|
|
30
|
+
// Multi-agent task - combine patterns
|
|
31
|
+
const allPatterns = semantic.agents.reduce((acc, agentDomain) => {
|
|
32
|
+
const agentPatterns = this.getPatternsForDomain(agentDomain, projectTech)
|
|
33
|
+
return {
|
|
34
|
+
include: [...acc.include, ...agentPatterns.include],
|
|
35
|
+
extensions: [...acc.extensions, ...agentPatterns.extensions]
|
|
36
|
+
}
|
|
37
|
+
}, { include: [], extensions: [] })
|
|
38
|
+
|
|
39
|
+
patterns.include = [...new Set(allPatterns.include)]
|
|
40
|
+
patterns.extensions = [...new Set(allPatterns.extensions)]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Find files matching patterns
|
|
44
|
+
const files = await this.findMatchingFiles(projectPath, patterns)
|
|
45
|
+
|
|
46
|
+
// Limit to reasonable number
|
|
47
|
+
const maxFiles = 200
|
|
48
|
+
return files.slice(0, maxFiles)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Get file patterns for a domain
|
|
53
|
+
*/
|
|
54
|
+
getPatternsForDomain(domain, projectTech) {
|
|
55
|
+
const patterns = {
|
|
56
|
+
include: [],
|
|
57
|
+
extensions: [],
|
|
58
|
+
exclude: ['node_modules', 'dist', 'build', '.git']
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
switch (domain) {
|
|
62
|
+
case 'frontend':
|
|
63
|
+
patterns.include = ['src', 'components', 'pages', 'views', 'app']
|
|
64
|
+
patterns.extensions = ['.tsx', '.jsx', '.vue', '.svelte', '.css', '.scss', '.styled.js']
|
|
65
|
+
|
|
66
|
+
// Add framework-specific patterns
|
|
67
|
+
if (projectTech.frameworks) {
|
|
68
|
+
if (projectTech.frameworks.some(f => f.toLowerCase().includes('next'))) {
|
|
69
|
+
patterns.include.push('pages', 'app', 'components')
|
|
70
|
+
}
|
|
71
|
+
if (projectTech.frameworks.some(f => f.toLowerCase().includes('react'))) {
|
|
72
|
+
patterns.extensions.push('.tsx', '.jsx')
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
break
|
|
76
|
+
|
|
77
|
+
case 'backend':
|
|
78
|
+
patterns.include = ['src', 'lib', 'api', 'routes', 'controllers', 'services', 'app']
|
|
79
|
+
patterns.extensions = ['.js', '.ts', '.py', '.rb', '.go', '.rs']
|
|
80
|
+
|
|
81
|
+
// Framework-specific
|
|
82
|
+
if (projectTech.frameworks) {
|
|
83
|
+
if (projectTech.frameworks.some(f => f.toLowerCase().includes('express'))) {
|
|
84
|
+
patterns.include.push('routes', 'middleware', 'controllers')
|
|
85
|
+
}
|
|
86
|
+
if (projectTech.frameworks.some(f => f.toLowerCase().includes('django'))) {
|
|
87
|
+
patterns.include.push('views', 'urls', 'models')
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
break
|
|
91
|
+
|
|
92
|
+
case 'database':
|
|
93
|
+
patterns.include = ['migrations', 'models', 'schemas', 'db', 'database']
|
|
94
|
+
patterns.extensions = ['.sql', '.js', '.ts', '.rb', '.py']
|
|
95
|
+
break
|
|
96
|
+
|
|
97
|
+
case 'qa':
|
|
98
|
+
patterns.include = ['tests', '__tests__', 'spec', 'test']
|
|
99
|
+
patterns.extensions = ['.test.js', '.test.ts', '.spec.js', '.spec.ts']
|
|
100
|
+
break
|
|
101
|
+
|
|
102
|
+
case 'devops':
|
|
103
|
+
patterns.include = ['.github', '.gitlab', 'docker', 'k8s', 'kubernetes', 'terraform']
|
|
104
|
+
patterns.extensions = ['.yml', '.yaml', '.dockerfile', '.sh', '.tf']
|
|
105
|
+
break
|
|
106
|
+
|
|
107
|
+
default:
|
|
108
|
+
// General - include common source directories
|
|
109
|
+
patterns.include = ['src', 'lib', 'app']
|
|
110
|
+
patterns.extensions = ['.js', '.ts', '.jsx', '.tsx', '.py', '.rb']
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return patterns
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Find files matching patterns
|
|
118
|
+
*/
|
|
119
|
+
async findMatchingFiles(projectPath, patterns) {
|
|
120
|
+
const files = []
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
// Build glob patterns
|
|
124
|
+
const globPatterns = []
|
|
125
|
+
|
|
126
|
+
// Add include patterns
|
|
127
|
+
for (const include of patterns.include) {
|
|
128
|
+
for (const ext of patterns.extensions) {
|
|
129
|
+
globPatterns.push(`${include}/**/*${ext}`)
|
|
130
|
+
globPatterns.push(`**/${include}/**/*${ext}`)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Also search root level
|
|
135
|
+
for (const ext of patterns.extensions) {
|
|
136
|
+
globPatterns.push(`*${ext}`)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Execute glob searches
|
|
140
|
+
for (const pattern of globPatterns) {
|
|
141
|
+
try {
|
|
142
|
+
const matches = await glob(pattern, {
|
|
143
|
+
cwd: projectPath,
|
|
144
|
+
ignore: patterns.exclude.map(ex => `**/${ex}/**`),
|
|
145
|
+
nodir: true,
|
|
146
|
+
follow: false
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
if (Array.isArray(matches)) {
|
|
150
|
+
files.push(...matches)
|
|
151
|
+
}
|
|
152
|
+
} catch (error) {
|
|
153
|
+
// Skip invalid patterns
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Remove duplicates and sort
|
|
158
|
+
return [...new Set(files)].sort()
|
|
159
|
+
} catch (error) {
|
|
160
|
+
console.error('Error finding files:', error.message)
|
|
161
|
+
return []
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Estimate context size (number of files)
|
|
167
|
+
*/
|
|
168
|
+
async estimateContextSize(taskAnalysis, projectPath) {
|
|
169
|
+
const files = await this.estimateFiles(taskAnalysis, projectPath)
|
|
170
|
+
return files.length
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
module.exports = ContextEstimator
|
|
175
|
+
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Modern Product Standards
|
|
3
|
+
* Defines what "Good" looks like for each domain.
|
|
4
|
+
* Used to inject high standards into agent prompts.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const ProductStandards = {
|
|
8
|
+
// General standards applicable to all agents
|
|
9
|
+
general: [
|
|
10
|
+
'SHIP IT: Bias for action. Better to ship and iterate than perfect and delay.',
|
|
11
|
+
'USER CENTRIC: Always ask "How does this help the user?"',
|
|
12
|
+
'CLEAN CODE: Write code that is easy to read, test, and maintain.',
|
|
13
|
+
'NO BS: Avoid over-engineering. Simple is better than complex.'
|
|
14
|
+
],
|
|
15
|
+
|
|
16
|
+
// Domain-specific standards
|
|
17
|
+
domains: {
|
|
18
|
+
frontend: {
|
|
19
|
+
title: 'Modern Frontend Standards',
|
|
20
|
+
rules: [
|
|
21
|
+
'PERFORMANCE: Core Web Vitals matter. Optimize LCP, CLS, FID.',
|
|
22
|
+
'ACCESSIBILITY: Semantic HTML, ARIA labels, keyboard navigation (WCAG 2.1 AA).',
|
|
23
|
+
'RESPONSIVE: Mobile-first design. Works on all devices.',
|
|
24
|
+
'UX/UI: Smooth transitions, loading states, error boundaries. No dead clicks.',
|
|
25
|
+
'STATE: Local state for UI, Global state (Context/Zustand) for data. No prop drilling.'
|
|
26
|
+
]
|
|
27
|
+
},
|
|
28
|
+
backend: {
|
|
29
|
+
title: 'Robust Backend Standards',
|
|
30
|
+
rules: [
|
|
31
|
+
'SECURITY: Validate ALL inputs. Sanitize outputs. OWASP Top 10 awareness.',
|
|
32
|
+
'SCALABILITY: Stateless services. Caching strategies (Redis/CDN).',
|
|
33
|
+
'RELIABILITY: Graceful error handling. Structured logging. Health checks.',
|
|
34
|
+
'API DESIGN: RESTful or GraphQL best practices. Consistent response envelopes.',
|
|
35
|
+
'DB: Indexed queries. Migrations for schema changes. No N+1 queries.'
|
|
36
|
+
]
|
|
37
|
+
},
|
|
38
|
+
database: {
|
|
39
|
+
title: 'Data Integrity Standards',
|
|
40
|
+
rules: [
|
|
41
|
+
'INTEGRITY: Foreign keys, constraints, transactions.',
|
|
42
|
+
'PERFORMANCE: Index usage analysis. Query optimization.',
|
|
43
|
+
'BACKUPS: Point-in-time recovery awareness.',
|
|
44
|
+
'MIGRATIONS: Idempotent scripts. Zero-downtime changes.'
|
|
45
|
+
]
|
|
46
|
+
},
|
|
47
|
+
devops: {
|
|
48
|
+
title: 'Modern Ops Standards',
|
|
49
|
+
rules: [
|
|
50
|
+
'AUTOMATION: CI/CD for everything. No manual deployments.',
|
|
51
|
+
'IaC: Infrastructure as Code (Terraform/Pulumi).',
|
|
52
|
+
'OBSERVABILITY: Metrics, Logs, Traces (OpenTelemetry).',
|
|
53
|
+
'SECURITY: Least privilege access. Secrets management.'
|
|
54
|
+
]
|
|
55
|
+
},
|
|
56
|
+
qa: {
|
|
57
|
+
title: 'Quality Assurance Standards',
|
|
58
|
+
rules: [
|
|
59
|
+
'PYRAMID: Many unit tests, some integration, few E2E.',
|
|
60
|
+
'COVERAGE: Critical paths must be tested.',
|
|
61
|
+
'REALISM: Test with realistic data and scenarios.',
|
|
62
|
+
'SPEED: Fast feedback loops. Parallel execution.'
|
|
63
|
+
]
|
|
64
|
+
},
|
|
65
|
+
architecture: {
|
|
66
|
+
title: 'System Architecture Standards',
|
|
67
|
+
rules: [
|
|
68
|
+
'MODULARITY: High cohesion, low coupling.',
|
|
69
|
+
'EVOLVABILITY: Easy to change. Hard to break.',
|
|
70
|
+
'SIMPLICITY: Choose boring technology. Innovation tokens are limited.',
|
|
71
|
+
'DOCS: Architecture Decision Records (ADRs).'
|
|
72
|
+
]
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get standards for a specific domain
|
|
78
|
+
* @param {string} domain - The domain (frontend, backend, etc.)
|
|
79
|
+
* @returns {Object} The standards object
|
|
80
|
+
*/
|
|
81
|
+
getStandards(domain) {
|
|
82
|
+
const key = domain?.toLowerCase();
|
|
83
|
+
const specific = this.domains[key] || { title: 'General Standards', rules: [] };
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
title: specific.title,
|
|
87
|
+
rules: [...this.general, ...specific.rules]
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
module.exports = ProductStandards;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SmartCache - Intelligent Persistent Cache for Agents
|
|
3
|
+
*
|
|
4
|
+
* Cache with specific keys: {projectId}-{domain}-{techStack}
|
|
5
|
+
* Persists to disk for cross-session caching
|
|
6
|
+
* Invalidates only when stack changes
|
|
7
|
+
*
|
|
8
|
+
* @version 1.0.0
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require('fs').promises
|
|
12
|
+
const path = require('path')
|
|
13
|
+
const os = require('os')
|
|
14
|
+
const crypto = require('crypto')
|
|
15
|
+
|
|
16
|
+
class SmartCache {
|
|
17
|
+
constructor(projectId = null) {
|
|
18
|
+
this.projectId = projectId
|
|
19
|
+
this.memoryCache = new Map()
|
|
20
|
+
this.cacheDir = path.join(os.homedir(), '.prjct-cli', 'cache')
|
|
21
|
+
this.cacheFile = projectId
|
|
22
|
+
? path.join(this.cacheDir, `agents-${projectId}.json`)
|
|
23
|
+
: path.join(this.cacheDir, 'agents-global.json')
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Initialize cache - load from disk
|
|
28
|
+
*/
|
|
29
|
+
async initialize() {
|
|
30
|
+
try {
|
|
31
|
+
await fs.mkdir(this.cacheDir, { recursive: true })
|
|
32
|
+
await this.loadFromDisk()
|
|
33
|
+
} catch (error) {
|
|
34
|
+
// Cache file doesn't exist yet - that's ok
|
|
35
|
+
this.memoryCache = new Map()
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Generate cache key
|
|
41
|
+
* Format: {projectId}-{domain}-{techStackHash}
|
|
42
|
+
*/
|
|
43
|
+
generateKey(projectId, domain, techStack = {}) {
|
|
44
|
+
const techString = JSON.stringify(techStack)
|
|
45
|
+
const techHash = crypto.createHash('md5').update(techString).digest('hex').substring(0, 8)
|
|
46
|
+
return `${projectId || 'global'}-${domain}-${techHash}`
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get from cache
|
|
51
|
+
*/
|
|
52
|
+
async get(key) {
|
|
53
|
+
// Check memory first
|
|
54
|
+
if (this.memoryCache.has(key)) {
|
|
55
|
+
return this.memoryCache.get(key)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Load from disk if not in memory
|
|
59
|
+
await this.loadFromDisk()
|
|
60
|
+
return this.memoryCache.get(key) || null
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Set in cache
|
|
65
|
+
*/
|
|
66
|
+
async set(key, value) {
|
|
67
|
+
// Set in memory
|
|
68
|
+
this.memoryCache.set(key, value)
|
|
69
|
+
|
|
70
|
+
// Persist to disk (async, don't wait)
|
|
71
|
+
this.persistToDisk().catch(err => {
|
|
72
|
+
console.error('Cache persist error:', err.message)
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Check if key exists
|
|
78
|
+
*/
|
|
79
|
+
async has(key) {
|
|
80
|
+
if (this.memoryCache.has(key)) {
|
|
81
|
+
return true
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
await this.loadFromDisk()
|
|
85
|
+
return this.memoryCache.has(key)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Clear cache
|
|
90
|
+
*/
|
|
91
|
+
async clear() {
|
|
92
|
+
this.memoryCache.clear()
|
|
93
|
+
try {
|
|
94
|
+
await fs.unlink(this.cacheFile)
|
|
95
|
+
} catch {
|
|
96
|
+
// File doesn't exist - that's ok
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Invalidate cache for a project (when stack changes)
|
|
102
|
+
*/
|
|
103
|
+
async invalidateProject(projectId) {
|
|
104
|
+
const keysToDelete = []
|
|
105
|
+
for (const key of this.memoryCache.keys()) {
|
|
106
|
+
if (key.startsWith(`${projectId}-`)) {
|
|
107
|
+
keysToDelete.push(key)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
keysToDelete.forEach(key => this.memoryCache.delete(key))
|
|
112
|
+
await this.persistToDisk()
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Load cache from disk
|
|
117
|
+
*/
|
|
118
|
+
async loadFromDisk() {
|
|
119
|
+
try {
|
|
120
|
+
const content = await fs.readFile(this.cacheFile, 'utf-8')
|
|
121
|
+
const data = JSON.parse(content)
|
|
122
|
+
|
|
123
|
+
// Restore to memory cache
|
|
124
|
+
for (const [key, value] of Object.entries(data)) {
|
|
125
|
+
this.memoryCache.set(key, value)
|
|
126
|
+
}
|
|
127
|
+
} catch {
|
|
128
|
+
// File doesn't exist or invalid - start fresh
|
|
129
|
+
this.memoryCache = new Map()
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Persist cache to disk
|
|
135
|
+
*/
|
|
136
|
+
async persistToDisk() {
|
|
137
|
+
try {
|
|
138
|
+
const data = Object.fromEntries(this.memoryCache)
|
|
139
|
+
await fs.writeFile(this.cacheFile, JSON.stringify(data, null, 2), 'utf-8')
|
|
140
|
+
} catch (error) {
|
|
141
|
+
// Fail silently - cache is best effort
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Get cache statistics
|
|
147
|
+
*/
|
|
148
|
+
getStats() {
|
|
149
|
+
return {
|
|
150
|
+
size: this.memoryCache.size,
|
|
151
|
+
keys: Array.from(this.memoryCache.keys())
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
module.exports = SmartCache
|
|
157
|
+
|