prjct-cli 0.10.0 → 0.10.3

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 (43) hide show
  1. package/CHANGELOG.md +41 -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/chain-of-thought.js +578 -0
  5. package/core/agentic/command-executor.js +238 -4
  6. package/core/agentic/context-builder.js +208 -8
  7. package/core/agentic/ground-truth.js +591 -0
  8. package/core/agentic/loop-detector.js +406 -0
  9. package/core/agentic/memory-system.js +850 -0
  10. package/core/agentic/parallel-tools.js +366 -0
  11. package/core/agentic/plan-mode.js +572 -0
  12. package/core/agentic/prompt-builder.js +76 -1
  13. package/core/agentic/response-templates.js +290 -0
  14. package/core/agentic/semantic-compression.js +517 -0
  15. package/core/agentic/think-blocks.js +657 -0
  16. package/core/agentic/tool-registry.js +32 -0
  17. package/core/agentic/validation-rules.js +380 -0
  18. package/core/command-registry.js +48 -0
  19. package/core/commands.js +65 -1
  20. package/core/context-sync.js +183 -0
  21. package/package.json +7 -15
  22. package/templates/commands/done.md +7 -0
  23. package/templates/commands/feature.md +8 -0
  24. package/templates/commands/ship.md +8 -0
  25. package/templates/commands/spec.md +128 -0
  26. package/templates/global/CLAUDE.md +17 -0
  27. package/core/__tests__/agentic/agent-router.test.js +0 -398
  28. package/core/__tests__/agentic/command-executor.test.js +0 -223
  29. package/core/__tests__/agentic/context-builder.test.js +0 -160
  30. package/core/__tests__/agentic/context-filter.test.js +0 -494
  31. package/core/__tests__/agentic/prompt-builder.test.js +0 -204
  32. package/core/__tests__/agentic/template-loader.test.js +0 -164
  33. package/core/__tests__/agentic/tool-registry.test.js +0 -243
  34. package/core/__tests__/domain/agent-generator.test.js +0 -289
  35. package/core/__tests__/domain/agent-loader.test.js +0 -179
  36. package/core/__tests__/domain/analyzer.test.js +0 -324
  37. package/core/__tests__/infrastructure/author-detector.test.js +0 -103
  38. package/core/__tests__/infrastructure/config-manager.test.js +0 -454
  39. package/core/__tests__/infrastructure/path-manager.test.js +0 -412
  40. package/core/__tests__/setup.test.js +0 -15
  41. package/core/__tests__/utils/date-helper.test.js +0 -169
  42. package/core/__tests__/utils/file-helper.test.js +0 -258
  43. package/core/__tests__/utils/jsonl-helper.test.js +0 -387
@@ -1,289 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach } from 'vitest'
2
- import AgentGenerator from '../../domain/agent-generator.js'
3
- import fs from 'fs/promises'
4
- import os from 'os'
5
- import path from 'path'
6
-
7
- describe('Agent Generator', () => {
8
- const testProjectId = 'test-agent-gen-' + Date.now()
9
- const agentsDir = path.join(os.homedir(), '.prjct-cli', 'projects', testProjectId, 'agents')
10
- const generator = new AgentGenerator(testProjectId)
11
-
12
- afterEach(async () => {
13
- // Cleanup test files
14
- try {
15
- await fs.rm(agentsDir, { recursive: true, force: true })
16
- } catch (error) {
17
- // Ignore cleanup errors
18
- }
19
- })
20
-
21
- describe('Constructor', () => {
22
- it('should create generator with project ID', () => {
23
- expect(generator.projectId).toBe(testProjectId)
24
- expect(generator.outputDir).toContain(testProjectId)
25
- })
26
-
27
- it('should use fallback directory without project ID', () => {
28
- const fallbackGenerator = new AgentGenerator()
29
- expect(fallbackGenerator.outputDir).toContain('.prjct-cli/agents')
30
- expect(fallbackGenerator.outputDir).not.toContain('projects')
31
- })
32
-
33
- it('should construct correct output path', () => {
34
- expect(generator.outputDir).toBe(agentsDir)
35
- })
36
- })
37
-
38
- describe('generateDynamicAgent()', () => {
39
- it('should generate agent file', async () => {
40
- await generator.generateDynamicAgent('test-agent', {
41
- role: 'Test Agent Role',
42
- expertise: 'Test Technologies',
43
- responsibilities: 'Test Responsibilities',
44
- })
45
-
46
- const agentFile = path.join(agentsDir, 'test-agent.md')
47
- const exists = await fs
48
- .access(agentFile)
49
- .then(() => true)
50
- .catch(() => false)
51
-
52
- expect(exists).toBe(true)
53
- })
54
-
55
- it('should create agent with correct content', async () => {
56
- await generator.generateDynamicAgent('backend-agent', {
57
- role: 'Backend Developer',
58
- expertise: 'Node.js, Express, PostgreSQL',
59
- responsibilities: 'API development and database management',
60
- })
61
-
62
- const content = await fs.readFile(path.join(agentsDir, 'backend-agent.md'), 'utf-8')
63
-
64
- expect(content).toContain('# AGENT: BACKEND-AGENT')
65
- expect(content).toContain('Role: Backend Developer')
66
- expect(content).toContain('## META-INSTRUCTION')
67
- // Expertise and Responsibilities are now part of the context or analysis instructions
68
- })
69
-
70
- it('should include project context in agent file', async () => {
71
- await generator.generateDynamicAgent('context-agent', {
72
- role: 'Agent with Context',
73
- expertise: 'Testing',
74
- responsibilities: 'Test things',
75
- projectContext: {
76
- framework: 'React',
77
- version: '18.0',
78
- },
79
- })
80
-
81
- const content = await fs.readFile(path.join(agentsDir, 'context-agent.md'), 'utf-8')
82
-
83
- expect(content).toContain('## PROJECT CONTEXT')
84
- expect(content).toContain('framework')
85
- expect(content).toContain('React')
86
- expect(content).toContain('version')
87
- expect(content).toContain('18.0')
88
- })
89
-
90
- it('should handle missing optional fields', async () => {
91
- await generator.generateDynamicAgent('minimal-agent', {
92
- role: 'Minimal Role',
93
- })
94
-
95
- const content = await fs.readFile(path.join(agentsDir, 'minimal-agent.md'), 'utf-8')
96
-
97
- expect(content).toContain('# AGENT: MINIMAL-AGENT')
98
- expect(content).toContain('## META-INSTRUCTION')
99
- expect(content).toContain('ANALYZE the provided PROJECT CONTEXT')
100
- expect(content).toContain('No specific project context provided')
101
- })
102
-
103
- it('should create output directory if not exists', async () => {
104
- const newProjectId = 'new-project-' + Date.now()
105
- const newGenerator = new AgentGenerator(newProjectId)
106
- const newAgentsDir = path.join(os.homedir(), '.prjct-cli', 'projects', newProjectId, 'agents')
107
-
108
- await newGenerator.generateDynamicAgent('auto-create', {
109
- role: 'Auto Created Agent',
110
- })
111
-
112
- const exists = await fs
113
- .access(newAgentsDir)
114
- .then(() => true)
115
- .catch(() => false)
116
-
117
- expect(exists).toBe(true)
118
-
119
- // Cleanup
120
- await fs.rm(path.join(os.homedir(), '.prjct-cli', 'projects', newProjectId), {
121
- recursive: true,
122
- force: true,
123
- })
124
- })
125
-
126
- it('should create multiple agents', async () => {
127
- await generator.generateDynamicAgent('agent-1', { role: 'Agent One' })
128
- await generator.generateDynamicAgent('agent-2', { role: 'Agent Two' })
129
- await generator.generateDynamicAgent('agent-3', { role: 'Agent Three' })
130
-
131
- const agents = await generator.listAgents()
132
-
133
- expect(agents).toHaveLength(3)
134
- expect(agents).toContain('agent-1')
135
- expect(agents).toContain('agent-2')
136
- expect(agents).toContain('agent-3')
137
- })
138
-
139
- it('should use agent name as fallback for role', async () => {
140
- await generator.generateDynamicAgent('fallback-agent', {})
141
-
142
- const content = await fs.readFile(path.join(agentsDir, 'fallback-agent.md'), 'utf-8')
143
-
144
- expect(content).toContain('# AGENT: FALLBACK-AGENT')
145
- })
146
- })
147
-
148
- describe('cleanupObsoleteAgents()', () => {
149
- beforeEach(async () => {
150
- // Create some test agents
151
- await generator.generateDynamicAgent('agent-1', { role: 'Agent 1' })
152
- await generator.generateDynamicAgent('agent-2', { role: 'Agent 2' })
153
- await generator.generateDynamicAgent('agent-3', { role: 'Agent 3' })
154
- })
155
-
156
- it('should remove obsolete agents', async () => {
157
- const removed = await generator.cleanupObsoleteAgents(['agent-1', 'agent-2'])
158
-
159
- expect(removed).toContain('agent-3')
160
- expect(removed).toHaveLength(1)
161
- })
162
-
163
- it('should keep required agents', async () => {
164
- await generator.cleanupObsoleteAgents(['agent-1', 'agent-2'])
165
-
166
- const agents = await generator.listAgents()
167
-
168
- expect(agents).toContain('agent-1')
169
- expect(agents).toContain('agent-2')
170
- expect(agents).not.toContain('agent-3')
171
- })
172
-
173
- it('should remove multiple obsolete agents', async () => {
174
- await generator.generateDynamicAgent('agent-4', { role: 'Agent 4' })
175
-
176
- const removed = await generator.cleanupObsoleteAgents(['agent-1'])
177
-
178
- expect(removed).toHaveLength(3)
179
- expect(removed).toContain('agent-2')
180
- expect(removed).toContain('agent-3')
181
- expect(removed).toContain('agent-4')
182
- })
183
-
184
- it('should return empty array if all agents are required', async () => {
185
- const removed = await generator.cleanupObsoleteAgents(['agent-1', 'agent-2', 'agent-3'])
186
-
187
- expect(removed).toEqual([])
188
- })
189
-
190
- it('should handle non-existent directory gracefully', async () => {
191
- const emptyGenerator = new AgentGenerator('empty-' + Date.now())
192
-
193
- const removed = await emptyGenerator.cleanupObsoleteAgents(['agent-1'])
194
-
195
- expect(Array.isArray(removed)).toBe(true)
196
- })
197
- })
198
-
199
- describe('listAgents()', () => {
200
- it('should list all agents', async () => {
201
- await generator.generateDynamicAgent('frontend', { role: 'Frontend' })
202
- await generator.generateDynamicAgent('backend', { role: 'Backend' })
203
-
204
- const agents = await generator.listAgents()
205
-
206
- expect(agents).toHaveLength(2)
207
- expect(agents).toContain('frontend')
208
- expect(agents).toContain('backend')
209
- })
210
-
211
- it('should return empty array for no agents', async () => {
212
- const agents = await generator.listAgents()
213
-
214
- expect(agents).toEqual([])
215
- })
216
-
217
- it('should ignore non-.md files', async () => {
218
- await generator.generateDynamicAgent('valid-agent', { role: 'Valid' })
219
- await fs.writeFile(path.join(agentsDir, 'not-agent.txt'), 'text file')
220
- await fs.writeFile(path.join(agentsDir, 'config.json'), '{}')
221
-
222
- const agents = await generator.listAgents()
223
-
224
- expect(agents).toHaveLength(1)
225
- expect(agents).toContain('valid-agent')
226
- })
227
-
228
- it('should ignore hidden files', async () => {
229
- await generator.generateDynamicAgent('visible', { role: 'Visible' })
230
- await fs.writeFile(path.join(agentsDir, '.hidden.md'), 'hidden')
231
-
232
- const agents = await generator.listAgents()
233
-
234
- expect(agents).toHaveLength(1)
235
- expect(agents).toContain('visible')
236
- })
237
- })
238
-
239
- describe('Integration', () => {
240
- it('should create, list, and cleanup agents', async () => {
241
- // Create agents
242
- await generator.generateDynamicAgent('keep-me', { role: 'Keep' })
243
- await generator.generateDynamicAgent('remove-me', { role: 'Remove' })
244
-
245
- // Verify they exist
246
- const initialAgents = await generator.listAgents()
247
- expect(initialAgents).toHaveLength(2)
248
-
249
- // Cleanup obsolete
250
- const removed = await generator.cleanupObsoleteAgents(['keep-me'])
251
- expect(removed).toContain('remove-me')
252
-
253
- // Verify cleanup
254
- const finalAgents = await generator.listAgents()
255
- expect(finalAgents).toHaveLength(1)
256
- expect(finalAgents).toContain('keep-me')
257
- })
258
-
259
- it('should handle agent file content correctly', async () => {
260
- await generator.generateDynamicAgent('full-agent', {
261
- role: 'Full Stack Developer',
262
- expertise: 'React, Node.js, PostgreSQL, Docker',
263
- responsibilities: 'Build and deploy full stack applications',
264
- projectContext: {
265
- stack: 'MERN',
266
- deployment: 'AWS',
267
- },
268
- })
269
-
270
- const content = await fs.readFile(path.join(agentsDir, 'full-agent.md'), 'utf-8')
271
-
272
- // Should have all sections
273
- expect(content).toContain('# AGENT: FULL-AGENT')
274
- expect(content).toContain('Role: Full Stack Developer')
275
- expect(content).toContain('## META-INSTRUCTION')
276
- expect(content).toContain('## DOMAIN AUTHORITY')
277
- expect(content).toContain('## DYNAMIC STANDARDS')
278
- expect(content).toContain('## ORCHESTRATION PROTOCOL')
279
- expect(content).toContain('## PROJECT CONTEXT')
280
-
281
- // Should have context content
282
- expect(content).toContain('MERN')
283
- expect(content).toContain('AWS')
284
-
285
- // Should NOT have hardcoded tech lists anymore
286
- // expect(content).toContain('React, Node.js, PostgreSQL, Docker') // This is no longer explicitly listed in EXPERTISE section as that section is gone
287
- })
288
- })
289
- })
@@ -1,179 +0,0 @@
1
- /**
2
- * Tests for AgentLoader
3
- * Verifies that agents are loaded correctly from project files
4
- */
5
-
6
- const fs = require('fs').promises
7
- const path = require('path')
8
- const os = require('os')
9
- const { describe, it, expect, beforeEach, afterEach } = require('vitest')
10
- const AgentLoader = require('../../domain/agent-loader')
11
-
12
- describe('AgentLoader', () => {
13
- let testProjectId
14
- let testAgentsDir
15
- let loader
16
-
17
- beforeEach(async () => {
18
- // Create unique test project ID
19
- testProjectId = `test-${Date.now()}`
20
- testAgentsDir = path.join(os.homedir(), '.prjct-cli', 'projects', testProjectId, 'agents')
21
- await fs.mkdir(testAgentsDir, { recursive: true })
22
- loader = new AgentLoader(testProjectId)
23
- })
24
-
25
- afterEach(async () => {
26
- // Cleanup: Remove test agents directory
27
- try {
28
- await fs.rm(testAgentsDir, { recursive: true, force: true })
29
- } catch (error) {
30
- // Ignore cleanup errors
31
- }
32
- })
33
-
34
- describe('loadAgent', () => {
35
- it('should load an existing agent from file', async () => {
36
- // Create test agent file
37
- const agentName = 'frontend-specialist'
38
- const agentContent = `# AGENT: FRONTEND-SPECIALIST
39
- Role: Frontend Development Specialist
40
-
41
- ## META-INSTRUCTION
42
- You are a frontend specialist.
43
-
44
- ## DOMAIN AUTHORITY
45
- You are the owner of the frontend domain.
46
- `
47
-
48
- const agentPath = path.join(testAgentsDir, `${agentName}.md`)
49
- await fs.writeFile(agentPath, agentContent, 'utf-8')
50
-
51
- // Load agent
52
- const agent = await loader.loadAgent(agentName)
53
-
54
- // Verify
55
- expect(agent).not.toBeNull()
56
- expect(agent.name).toBe(agentName)
57
- expect(agent.content).toBe(agentContent)
58
- expect(agent.role).toBe('Frontend Development Specialist')
59
- expect(agent.domain).toBe('frontend')
60
- })
61
-
62
- it('should return null for non-existent agent', async () => {
63
- const agent = await loader.loadAgent('non-existent-agent')
64
- expect(agent).toBeNull()
65
- })
66
-
67
- it('should cache loaded agents', async () => {
68
- // Create test agent
69
- const agentName = 'backend-specialist'
70
- const agentPath = path.join(testAgentsDir, `${agentName}.md`)
71
- await fs.writeFile(agentPath, '# AGENT: BACKEND-SPECIALIST\nRole: Backend Specialist', 'utf-8')
72
-
73
- // Load twice
74
- const agent1 = await loader.loadAgent(agentName)
75
- const agent2 = await loader.loadAgent(agentName)
76
-
77
- // Should be same object (cached)
78
- expect(agent1).toBe(agent2)
79
- })
80
-
81
- it('should extract skills from agent content', async () => {
82
- const agentName = 'react-specialist'
83
- const agentContent = `# AGENT: REACT-SPECIALIST
84
- Role: React Development Specialist
85
-
86
- This agent specializes in React, TypeScript, and Next.js.
87
- `
88
-
89
- const agentPath = path.join(testAgentsDir, `${agentName}.md`)
90
- await fs.writeFile(agentPath, agentContent, 'utf-8')
91
-
92
- const agent = await loader.loadAgent(agentName)
93
-
94
- expect(agent.skills).toContain('React')
95
- expect(agent.skills).toContain('TypeScript')
96
- expect(agent.skills).toContain('Next.js')
97
- })
98
- })
99
-
100
- describe('loadAllAgents', () => {
101
- it('should load all agents in the directory', async () => {
102
- // Create multiple agent files
103
- const agents = [
104
- { name: 'frontend-specialist', content: '# AGENT: FRONTEND-SPECIALIST\nRole: Frontend' },
105
- { name: 'backend-specialist', content: '# AGENT: BACKEND-SPECIALIST\nRole: Backend' },
106
- { name: 'qa-specialist', content: '# AGENT: QA-SPECIALIST\nRole: QA' }
107
- ]
108
-
109
- for (const agent of agents) {
110
- const agentPath = path.join(testAgentsDir, `${agent.name}.md`)
111
- await fs.writeFile(agentPath, agent.content, 'utf-8')
112
- }
113
-
114
- // Load all
115
- const loadedAgents = await loader.loadAllAgents()
116
-
117
- expect(loadedAgents).toHaveLength(3)
118
- expect(loadedAgents.map(a => a.name)).toContain('frontend-specialist')
119
- expect(loadedAgents.map(a => a.name)).toContain('backend-specialist')
120
- expect(loadedAgents.map(a => a.name)).toContain('qa-specialist')
121
- })
122
-
123
- it('should return empty array if no agents exist', async () => {
124
- const agents = await loader.loadAllAgents()
125
- expect(agents).toEqual([])
126
- })
127
-
128
- it('should ignore non-markdown files', async () => {
129
- // Create agent file and non-agent file
130
- const agentPath = path.join(testAgentsDir, 'frontend-specialist.md')
131
- await fs.writeFile(agentPath, '# AGENT', 'utf-8')
132
-
133
- const otherFile = path.join(testAgentsDir, 'config.json')
134
- await fs.writeFile(otherFile, '{}', 'utf-8')
135
-
136
- const agents = await loader.loadAllAgents()
137
-
138
- expect(agents).toHaveLength(1)
139
- expect(agents[0].name).toBe('frontend-specialist')
140
- })
141
- })
142
-
143
- describe('agentExists', () => {
144
- it('should return true for existing agent', async () => {
145
- const agentName = 'test-agent'
146
- const agentPath = path.join(testAgentsDir, `${agentName}.md`)
147
- await fs.writeFile(agentPath, '# AGENT', 'utf-8')
148
-
149
- const exists = await loader.agentExists(agentName)
150
- expect(exists).toBe(true)
151
- })
152
-
153
- it('should return false for non-existent agent', async () => {
154
- const exists = await loader.agentExists('non-existent')
155
- expect(exists).toBe(false)
156
- })
157
- })
158
-
159
- describe('clearCache', () => {
160
- it('should clear the agent cache', async () => {
161
- const agentName = 'test-agent'
162
- const agentPath = path.join(testAgentsDir, `${agentName}.md`)
163
- await fs.writeFile(agentPath, '# AGENT', 'utf-8')
164
-
165
- // Load and cache
166
- const agent1 = await loader.loadAgent(agentName)
167
- expect(agent1).not.toBeNull()
168
-
169
- // Clear cache
170
- loader.clearCache()
171
-
172
- // Load again - should still work but be new object
173
- const agent2 = await loader.loadAgent(agentName)
174
- expect(agent2).not.toBeNull()
175
- // Note: In real usage, they might be same due to file system, but cache is cleared
176
- })
177
- })
178
- })
179
-