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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prjct-cli",
3
- "version": "0.9.2",
3
+ "version": "0.10.2",
4
4
  "description": "Built for Claude - Ship fast, track progress, stay focused. Developer momentum tool for indie hackers.",
5
5
  "main": "core/index.js",
6
6
  "bin": {
@@ -15,18 +15,14 @@
15
15
  "update-commands": "node -e \"const installer = require('./core/infrastructure/command-installer'); installer.syncCommands().then(r => console.log('Commands updated:', r)).catch(e => console.error('Error:', e.message))\"",
16
16
  "install-global": "./scripts/install.sh",
17
17
  "update": "./scripts/update.sh",
18
- "test": "vitest run --workspace=vitest.workspace.js",
19
- "test:watch": "vitest --workspace=vitest.workspace.js",
20
- "test:coverage": "vitest run --coverage --workspace=vitest.workspace.js",
18
+ "test": "vitest run",
19
+ "test:watch": "vitest",
20
+ "test:coverage": "vitest run --coverage",
21
21
  "validate": "node scripts/validate-commands.js",
22
- "lint": "eslint \"**/*.js\" --ignore-pattern \"node_modules/**\" --ignore-pattern \"website/**\"",
23
- "lint:fix": "eslint \"**/*.js\" --fix --ignore-pattern \"node_modules/**\" --ignore-pattern \"website/**\"",
22
+ "lint": "eslint . --ignore-pattern node_modules",
23
+ "lint:fix": "eslint . --fix --ignore-pattern node_modules",
24
24
  "format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,css,md}\" --config .config/.prettierrc --ignore-path .config/.prettierignore",
25
- "format:check": "prettier --check \"**/*.{js,jsx,ts,tsx,json,css,md}\" --config .config/.prettierrc --ignore-path .config/.prettierignore",
26
- "website:dev": "cd website && npm run dev",
27
- "website:build": "cd website && npm run build",
28
- "website:preview": "cd website && npm run preview",
29
- "website:install": "cd website && npm install"
25
+ "format:check": "prettier --check \"**/*.{js,jsx,ts,tsx,json,css,md}\" --config .config/.prettierrc --ignore-path .config/.prettierignore"
30
26
  },
31
27
  "keywords": [
32
28
  "claude-code",
@@ -48,7 +44,6 @@
48
44
  "prompts": "^2.4.2"
49
45
  },
50
46
  "devDependencies": {
51
- "@types/node": "^20.0.0",
52
47
  "@vitest/coverage-v8": "^3.2.4",
53
48
  "eslint": "^8.57.1",
54
49
  "eslint-config-prettier": "^10.1.8",
@@ -56,10 +51,7 @@
56
51
  "eslint-plugin-import": "^2.32.0",
57
52
  "eslint-plugin-n": "^16.6.2",
58
53
  "eslint-plugin-promise": "^6.6.0",
59
- "jsdom": "^27.0.0",
60
54
  "prettier": "^3.6.2",
61
- "prettier-plugin-tailwindcss": "^0.6.14",
62
- "typescript": "^5.0.0",
63
55
  "vitest": "^3.2.4"
64
56
  },
65
57
  "repository": {
@@ -88,4 +80,4 @@
88
80
  "chalk",
89
81
  "prompts"
90
82
  ]
91
- }
83
+ }
@@ -1,10 +1,17 @@
1
1
  ---
2
2
  allowed-tools: [Read, Write]
3
3
  description: 'Complete task'
4
+ think-triggers: [report_complete]
4
5
  ---
5
6
 
6
7
  # /p:done
7
8
 
9
+ ## Think First
10
+ Before marking complete, verify:
11
+ 1. Is the task actually finished?
12
+ 2. Were all acceptance criteria met?
13
+ 3. Should this trigger a /p:ship?
14
+
8
15
  ## Check
9
16
  Requires: `core/now.md` has content
10
17
 
@@ -2,10 +2,18 @@
2
2
  allowed-tools: [Read, Write, Bash, GetTimestamp, GetDate]
3
3
  description: 'Value analysis + roadmap + task breakdown + auto-start'
4
4
  timestamp-rule: 'GetTimestamp() and GetDate() for ALL timestamps'
5
+ think-triggers: [explore_to_edit, complex_analysis]
5
6
  ---
6
7
 
7
8
  # /p:feature
8
9
 
10
+ ## Think First
11
+ Before creating feature, analyze:
12
+ 1. Is this a simple or complex feature?
13
+ 2. What files/components will be affected?
14
+ 3. Are there dependencies or blockers?
15
+ 4. Should I create a /p:spec first?
16
+
9
17
  ## Flow
10
18
 
11
19
  1. **Value**: Impact/effort/timing → do_now/defer/blocked
@@ -2,10 +2,18 @@
2
2
  allowed-tools: [Read, Write, Bash, GetTimestamp, GetDate]
3
3
  description: 'Ship feature workflow'
4
4
  timestamp-rule: 'GetTimestamp() and GetDate() for timestamps'
5
+ think-triggers: [git_decision, report_complete]
5
6
  ---
6
7
 
7
8
  # /p:ship
8
9
 
10
+ ## Think First
11
+ Before shipping, verify:
12
+ 1. Are there uncommitted changes to include?
13
+ 2. What version bump is needed (patch/minor/major)?
14
+ 3. Is the feature actually complete?
15
+ 4. Should tests run first?
16
+
9
17
  ## Workflow (non-blocking)
10
18
  1. Lint → Tests → Docs update
11
19
  2. Version bump → CHANGELOG
@@ -0,0 +1,128 @@
1
+ ---
2
+ allowed-tools: [Read, Write, Glob, GetTimestamp, GetDate]
3
+ description: 'Spec-driven development for complex features'
4
+ timestamp-rule: 'GetTimestamp() and GetDate() for ALL timestamps'
5
+ think-triggers: [explore_to_edit, complex_analysis]
6
+ ---
7
+
8
+ # /p:spec
9
+
10
+ Spec-Driven Development. Creates detailed specifications for complex features before implementation.
11
+
12
+ ## Think First
13
+ Before creating spec, analyze:
14
+ 1. Is this feature complex enough for a spec? (auth, payments, migrations = yes)
15
+ 2. What are the key architectural decisions to make?
16
+ 3. Are there multiple valid approaches? Document tradeoffs.
17
+ 4. What questions should I ask the user before proceeding?
18
+
19
+ ## Purpose
20
+
21
+ For features that require:
22
+ - Clear requirements before coding
23
+ - Design decisions documented
24
+ - Tasks broken into 20-30 min chunks
25
+ - User approval before starting
26
+
27
+ ## Flow
28
+
29
+ ### No params: Show template
30
+ ```
31
+ → Interactive spec template
32
+ → Ask for feature name
33
+ → Guide through requirements
34
+ ```
35
+
36
+ ### With feature name: Create spec
37
+ ```
38
+ /p:spec "Dark Mode"
39
+ 1. Analyze: Context, patterns, dependencies
40
+ 2. Propose: Requirements + Design + Tasks
41
+ 3. Write: `planning/specs/{slug}.md`
42
+ 4. Ask: User approval
43
+ 5. On approve: Add tasks to queue, start first
44
+ ```
45
+
46
+ ## Spec Structure
47
+
48
+ ```markdown
49
+ # Feature Spec: {name}
50
+
51
+ **Created**: {GetDate()}
52
+ **Status**: PENDING_APPROVAL | APPROVED | IN_PROGRESS | COMPLETED
53
+
54
+ ## Requirements (User approves)
55
+ - [ ] Requirement 1
56
+ - [ ] Requirement 2
57
+ - [ ] Requirement 3
58
+
59
+ ## Design (Claude proposes)
60
+ - **Approach**: {architecture decision}
61
+ - **Key decisions**: {list}
62
+ - **Dependencies**: {existing code/libs}
63
+
64
+ ## Tasks (20-30min each)
65
+ 1. [ ] Task 1 (20m) - {description}
66
+ 2. [ ] Task 2 (25m) - {description}
67
+ 3. [ ] Task 3 (30m) - {description}
68
+
69
+ **Total**: {n} tasks, ~{Xh}
70
+
71
+ ## Notes
72
+ - {implementation notes}
73
+ - {edge cases to consider}
74
+ ```
75
+
76
+ ## Validation
77
+
78
+ - Feature name required for creation
79
+ - Spec must have at least 1 requirement
80
+ - Each task should be 20-30 minutes
81
+ - Check for existing spec with same name
82
+
83
+ ## Data
84
+
85
+ Session: `{"ts":"{GetTimestamp()}","type":"spec_create","name":"{feature}","requirements":{n},"tasks":{n},"effort":"{Xh}"}`
86
+ Spec file: `planning/specs/{feature-slug}.md`
87
+
88
+ ## Response
89
+
90
+ ### On creation:
91
+ ```
92
+ 📋 Spec: {feature}
93
+
94
+ Requirements ({n}):
95
+ {numbered_list}
96
+
97
+ Design:
98
+ → {approach}
99
+ → {key_decision}
100
+
101
+ Tasks ({n}, ~{total_time}):
102
+ {numbered_list}
103
+
104
+ APPROVE? (y/n/edit)
105
+ ```
106
+
107
+ ### On approval:
108
+ ```
109
+ ✅ Spec approved: {feature}
110
+ → {n} tasks added to queue
111
+ → Starting: {task_1}
112
+
113
+ Use /p:done when complete
114
+ ```
115
+
116
+ ## Examples
117
+
118
+ ```
119
+ /p:spec "User Authentication"
120
+ → Creates spec with OAuth/JWT decisions
121
+ → Breaks into: setup, login, logout, session, tests
122
+ → Estimates ~4h total
123
+
124
+ /p:spec "Dark Mode"
125
+ → Creates spec with theme approach
126
+ → Breaks into: toggle, state, styles, persist, test
127
+ → Estimates ~3h total
128
+ ```
@@ -5,6 +5,23 @@ This section provides global context for all `/p:*` commands across any prjct pr
5
5
 
6
6
  **Auto-managed by prjct-cli** - This section is automatically updated when you install or update prjct.
7
7
 
8
+ ## 🤖 Project Context (OBLIGATORIO)
9
+
10
+ **ANTES de trabajar en un proyecto prjct, LEE el contexto del proyecto:**
11
+
12
+ 1. Lee `.prjct/prjct.config.json` → obtén `projectId`
13
+ 2. Lee `~/.prjct-cli/projects/{projectId}/CLAUDE.md` → contexto dinámico del proyecto
14
+ 3. Para detalles de implementación, lee los archivos en `agents/`
15
+
16
+ El archivo `CLAUDE.md` del proyecto contiene:
17
+ - Stack detectado del proyecto
18
+ - Agentes disponibles (varían por proyecto)
19
+ - Tarea actual
20
+ - Cola de prioridades
21
+ - Rutas a documentación detallada
22
+
23
+ **Si no existe CLAUDE.md**: Sugiere ejecutar `/p:sync` para generarlo.
24
+
8
25
  ## 🎯 Path Resolution for ALL /p:* Commands
9
26
 
10
27
  **CRITICAL**: Every `/p:*` command operates on **global storage**, NOT local files.
@@ -1,398 +0,0 @@
1
- import { describe, it, expect, beforeEach, vi } from 'vitest'
2
- import { createRequire } from 'module'
3
- import path from 'path'
4
- import fs from 'fs/promises'
5
- import os from 'os'
6
-
7
- const require = createRequire(import.meta.url)
8
-
9
- describe('Mandatory Agent Router', () => {
10
- let MandatoryAgentRouter
11
- let AgentGenerator
12
- let router
13
- let mockAgentGenerator
14
-
15
- beforeEach(() => {
16
- MandatoryAgentRouter = require('../../agentic/agent-router.js')
17
- AgentGenerator = require('../../domain/agent-generator.js')
18
-
19
- // Mock agent generator
20
- mockAgentGenerator = {
21
- generateDynamicAgent: vi.fn().mockResolvedValue({
22
- name: 'test-agent',
23
- type: 'frontend-specialist',
24
- domain: 'frontend',
25
- confidence: 0.9
26
- })
27
- }
28
-
29
- router = new MandatoryAgentRouter()
30
- router.agentGenerator = mockAgentGenerator
31
- })
32
-
33
- describe('Constructor', () => {
34
- it('should initialize with agent generator', () => {
35
- expect(router.agentGenerator).toBeDefined()
36
- expect(router.agentCache).toBeInstanceOf(Map)
37
- expect(Array.isArray(router.usageLog)).toBe(true)
38
- })
39
- })
40
-
41
- describe('executeTask()', () => {
42
- it('should assign agent to task', async () => {
43
- const task = { description: 'create login component', type: 'ui' }
44
- const context = { projectPath: '/test', files: [] }
45
- const projectPath = '/test'
46
-
47
- const result = await router.executeTask(task, context, projectPath)
48
-
49
- expect(result).toBeDefined()
50
- expect(result.agent).toBeDefined()
51
- expect(result.agent.name).toBe('test-agent')
52
- expect(result.context).toBeDefined()
53
- expect(result.taskAnalysis).toBeDefined()
54
- expect(result.routing).toBeDefined()
55
- })
56
-
57
- it('should throw error if no agent can be assigned', async () => {
58
- router.agentGenerator.generateDynamicAgent = vi.fn().mockResolvedValue(null)
59
-
60
- const task = { description: 'unknown task' }
61
- const context = { projectPath: '/test' }
62
- const projectPath = '/test'
63
-
64
- await expect(router.executeTask(task, context, projectPath)).rejects.toThrow('CRITICAL: No agent assigned')
65
- })
66
-
67
- it('should cache agents for reuse', async () => {
68
- const task = { description: 'create component', type: 'ui' }
69
- const context = { projectPath: '/test' }
70
- const projectPath = '/test'
71
-
72
- await router.executeTask(task, context, projectPath)
73
- await router.executeTask(task, context, projectPath)
74
-
75
- // Should only generate once, second call uses cache
76
- expect(mockAgentGenerator.generateDynamicAgent).toHaveBeenCalledTimes(1)
77
- })
78
-
79
- it('should log agent usage', async () => {
80
- const task = { description: 'test task' }
81
- const context = { projectPath: '/test', files: ['file1.js', 'file2.js'] }
82
- const projectPath = '/test'
83
-
84
- await router.executeTask(task, context, projectPath)
85
-
86
- expect(router.usageLog.length).toBeGreaterThan(0)
87
- expect(router.usageLog[0].task).toBe('test task')
88
- expect(router.usageLog[0].agent).toBe('test-agent')
89
- })
90
- })
91
-
92
- describe('analyzeTask()', () => {
93
- it('should detect frontend tasks', () => {
94
- const task = { description: 'create react component with styles' }
95
- const analysis = router.analyzeTask(task)
96
-
97
- expect(analysis.domain).toBe('frontend')
98
- expect(analysis.confidence).toBeGreaterThan(0)
99
- expect(analysis.matchedKeywords.length).toBeGreaterThan(0)
100
- })
101
-
102
- it('should detect backend tasks', () => {
103
- const task = { description: 'create API endpoint with authentication' }
104
- const analysis = router.analyzeTask(task)
105
-
106
- expect(analysis.domain).toBe('backend')
107
- expect(analysis.matchedKeywords).toContain('api')
108
- })
109
-
110
- it('should detect database tasks', () => {
111
- const task = { description: 'create migration schema for postgres' }
112
- const analysis = router.analyzeTask(task)
113
-
114
- expect(analysis.domain).toBe('database')
115
- expect(analysis.matchedKeywords).toContain('migration')
116
- })
117
-
118
- it('should detect devops tasks', () => {
119
- const task = { description: 'deploy to docker kubernetes' }
120
- const analysis = router.analyzeTask(task)
121
-
122
- expect(analysis.domain).toBe('devops')
123
- })
124
-
125
- it('should detect qa tasks', () => {
126
- const task = { description: 'write unit tests and fix bugs' }
127
- const analysis = router.analyzeTask(task)
128
-
129
- expect(analysis.domain).toBe('qa')
130
- })
131
-
132
- it('should detect architecture tasks', () => {
133
- const task = { description: 'design architecture pattern and refactor' }
134
- const analysis = router.analyzeTask(task)
135
-
136
- expect(analysis.domain).toBe('architecture')
137
- })
138
-
139
- it('should return generalist for unknown tasks', () => {
140
- const task = { description: 'random task description' }
141
- const analysis = router.analyzeTask(task)
142
-
143
- expect(analysis.domain).toBe('generalist')
144
- expect(analysis.confidence).toBeLessThan(1)
145
- })
146
-
147
- it('should include technology stack in analysis', () => {
148
- const task = { description: 'create react component with typescript' }
149
- const analysis = router.analyzeTask(task)
150
-
151
- expect(analysis.techStack).toBeDefined()
152
- expect(analysis.techStack.languages).toContain('typescript')
153
- expect(analysis.techStack.frameworks).toContain('react')
154
- })
155
- })
156
-
157
- describe('detectTechnology()', () => {
158
- it('should detect languages', () => {
159
- const task = { description: 'write python script' }
160
- const tech = router.detectTechnology(task, task.description)
161
-
162
- expect(tech.languages).toContain('python')
163
- })
164
-
165
- it('should detect frameworks', () => {
166
- const task = { description: 'build express api' }
167
- const tech = router.detectTechnology(task, task.description)
168
-
169
- expect(tech.frameworks).toContain('express')
170
- })
171
-
172
- it('should detect databases', () => {
173
- const task = { description: 'query mongodb database' }
174
- const tech = router.detectTechnology(task, task.description)
175
-
176
- expect(tech.databases).toContain('mongodb')
177
- })
178
-
179
- it('should return empty arrays for unknown tech', () => {
180
- const task = { description: 'random task' }
181
- const tech = router.detectTechnology(task, task.description)
182
-
183
- expect(tech.languages).toEqual([])
184
- expect(tech.frameworks).toEqual([])
185
- expect(tech.databases).toEqual([])
186
- })
187
- })
188
-
189
- describe('assignAgent()', () => {
190
- it('should generate agent for task', async () => {
191
- const taskAnalysis = {
192
- domain: 'frontend',
193
- techStack: { languages: ['typescript'], frameworks: ['react'] }
194
- }
195
- const context = { projectPath: '/test' }
196
-
197
- const agent = await router.assignAgent(taskAnalysis, context)
198
-
199
- expect(agent).toBeDefined()
200
- expect(mockAgentGenerator.generateDynamicAgent).toHaveBeenCalled()
201
- })
202
-
203
- it('should use cached agent if available', async () => {
204
- const taskAnalysis = {
205
- domain: 'frontend',
206
- techStack: { languages: ['typescript'] }
207
- }
208
- const context = { projectPath: '/test' }
209
-
210
- const agent1 = await router.assignAgent(taskAnalysis, context)
211
- const agent2 = await router.assignAgent(taskAnalysis, context)
212
-
213
- expect(agent1).toBe(agent2) // Same instance from cache
214
- expect(mockAgentGenerator.generateDynamicAgent).toHaveBeenCalledTimes(1)
215
- })
216
- })
217
-
218
- describe('filterContextForAgent()', () => {
219
- it('should filter context for frontend agent', async () => {
220
- const agent = { name: 'frontend-agent', domain: 'frontend' }
221
- const fullContext = {
222
- files: [
223
- 'src/components/Button.jsx',
224
- 'src/api/users.js',
225
- 'src/styles/main.css',
226
- 'migrations/001_users.sql'
227
- ]
228
- }
229
- const taskAnalysis = { domain: 'frontend' }
230
-
231
- const filtered = await router.filterContextForAgent(agent, fullContext, taskAnalysis)
232
-
233
- expect(filtered.files).toContain('src/components/Button.jsx')
234
- expect(filtered.files).toContain('src/styles/main.css')
235
- expect(filtered.files).not.toContain('migrations/001_users.sql')
236
- expect(filtered.relevantOnly).toBe(true)
237
- })
238
-
239
- it('should filter context for backend agent', async () => {
240
- const agent = { name: 'backend-agent', domain: 'backend' }
241
- const fullContext = {
242
- files: [
243
- 'src/components/Button.jsx',
244
- 'src/api/users.js',
245
- 'src/routes/auth.js',
246
- 'src/styles/main.css'
247
- ]
248
- }
249
- const taskAnalysis = { domain: 'backend' }
250
-
251
- const filtered = await router.filterContextForAgent(agent, fullContext, taskAnalysis)
252
-
253
- expect(filtered.files).toContain('src/api/users.js')
254
- expect(filtered.files).toContain('src/routes/auth.js')
255
- expect(filtered.files).not.toContain('src/styles/main.css')
256
- })
257
-
258
- it('should filter context for database agent', async () => {
259
- const agent = { name: 'database-agent', domain: 'database' }
260
- const fullContext = {
261
- files: [
262
- 'src/models/User.js',
263
- 'migrations/001_users.sql',
264
- 'src/components/Button.jsx'
265
- ]
266
- }
267
- const taskAnalysis = { domain: 'database' }
268
-
269
- const filtered = await router.filterContextForAgent(agent, fullContext, taskAnalysis)
270
-
271
- expect(filtered.files).toContain('src/models/User.js')
272
- expect(filtered.files).toContain('migrations/001_users.sql')
273
- expect(filtered.files).not.toContain('src/components/Button.jsx')
274
- })
275
- })
276
-
277
- describe('filterFiles()', () => {
278
- it('should exclude files matching exclude patterns', () => {
279
- const files = ['src/app.js', 'node_modules/lib.js', 'dist/bundle.js']
280
- const pattern = {
281
- include: [],
282
- exclude: ['node_modules', 'dist'],
283
- extensions: []
284
- }
285
-
286
- const filtered = router.filterFiles(files, pattern)
287
-
288
- expect(filtered).not.toContain('node_modules/lib.js')
289
- expect(filtered).not.toContain('dist/bundle.js')
290
- expect(filtered).toContain('src/app.js')
291
- })
292
-
293
- it('should include only files matching include patterns', () => {
294
- const files = ['src/components/Button.jsx', 'src/api/users.js', 'tests/test.js']
295
- const pattern = {
296
- include: ['components'],
297
- exclude: [],
298
- extensions: []
299
- }
300
-
301
- const filtered = router.filterFiles(files, pattern)
302
-
303
- expect(filtered).toContain('src/components/Button.jsx')
304
- expect(filtered).not.toContain('src/api/users.js')
305
- })
306
-
307
- it('should filter by extensions', () => {
308
- const files = ['src/app.js', 'src/app.ts', 'src/app.py']
309
- const pattern = {
310
- include: [],
311
- exclude: [],
312
- extensions: ['.js', '.ts']
313
- }
314
-
315
- const filtered = router.filterFiles(files, pattern)
316
-
317
- expect(filtered).toContain('src/app.js')
318
- expect(filtered).toContain('src/app.ts')
319
- expect(filtered).not.toContain('src/app.py')
320
- })
321
- })
322
-
323
- describe('getBestPractices()', () => {
324
- it('should return domain-specific practices', async () => {
325
- const practices = await router.getBestPractices('frontend', { languages: [], frameworks: [] })
326
-
327
- expect(practices.length).toBeGreaterThan(0)
328
- expect(practices).toContain('Component composition over inheritance')
329
- })
330
-
331
- it('should include tech-specific practices', async () => {
332
- const practices = await router.getBestPractices('frontend', {
333
- languages: ['typescript'],
334
- frameworks: ['react']
335
- })
336
-
337
- expect(practices.some(p => p.includes('Hooks') || p.includes('TypeScript'))).toBe(true)
338
- })
339
- })
340
-
341
- describe('getSimilarDomains()', () => {
342
- it('should return similar domains for frontend', () => {
343
- const similar = router.getSimilarDomains('frontend')
344
- expect(similar).toContain('fullstack')
345
- })
346
-
347
- it('should return similar domains for backend', () => {
348
- const similar = router.getSimilarDomains('backend')
349
- expect(similar).toContain('fullstack')
350
- })
351
-
352
- it('should return default for unknown domain', () => {
353
- const similar = router.getSimilarDomains('unknown')
354
- expect(similar).toEqual(['generalist'])
355
- })
356
- })
357
-
358
- describe('getUsageStats()', () => {
359
- it('should return usage statistics', async () => {
360
- const task = { description: 'test task' }
361
- const context = { projectPath: '/test' }
362
- const projectPath = '/test'
363
-
364
- await router.executeTask(task, context, projectPath)
365
- await router.executeTask(task, context, projectPath)
366
-
367
- const stats = router.getUsageStats()
368
-
369
- expect(stats.totalTasks).toBe(2)
370
- expect(stats.byAgent).toBeDefined()
371
- expect(stats.mostUsedAgent).toBe('test-agent')
372
- })
373
-
374
- it('should handle empty usage log', () => {
375
- const stats = router.getUsageStats()
376
-
377
- expect(stats.totalTasks).toBe(0)
378
- expect(stats.mostUsedAgent).toBeNull()
379
- })
380
- })
381
-
382
- describe('calculateContextReduction()', () => {
383
- it('should calculate reduction for filtered context', () => {
384
- const filteredContext = { relevantOnly: true }
385
- const reduction = router.calculateContextReduction(filteredContext)
386
-
387
- expect(reduction).toBe('70-90%')
388
- })
389
-
390
- it('should return 0% for unfiltered context', () => {
391
- const context = { relevantOnly: false }
392
- const reduction = router.calculateContextReduction(context)
393
-
394
- expect(reduction).toBe('0%')
395
- })
396
- })
397
- })
398
-