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.
- package/CHANGELOG.md +41 -0
- package/core/__tests__/agentic/memory-system.test.js +263 -0
- package/core/__tests__/agentic/plan-mode.test.js +336 -0
- package/core/agentic/chain-of-thought.js +578 -0
- package/core/agentic/command-executor.js +238 -4
- package/core/agentic/context-builder.js +208 -8
- 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 +76 -1
- 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 +65 -1
- package/core/context-sync.js +183 -0
- package/package.json +7 -15
- 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 -204
- 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 -289
- package/core/__tests__/domain/agent-loader.test.js +0 -179
- 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
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,46 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.10.3] - 2025-11-27
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **Global CLAUDE.md Auto-Update** - Now updates `~/.claude/CLAUDE.md` on every sync/analyze/init
|
|
8
|
+
- `/p:sync`: Updates global config after generating agents
|
|
9
|
+
- `/p:analyze`: Updates global config after analysis
|
|
10
|
+
- `/p:init`: Updates global config for all init modes
|
|
11
|
+
- Ensures Claude always has latest prjct instructions
|
|
12
|
+
|
|
13
|
+
## [0.10.2] - 2025-11-27
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- **Dynamic Project Context for Claude** - Claude now actually uses generated agents and project context
|
|
18
|
+
- New `core/context-sync.js` module generates project-specific CLAUDE.md
|
|
19
|
+
- Context file stored in global storage: `~/.prjct-cli/projects/{projectId}/CLAUDE.md`
|
|
20
|
+
- Auto-generates on `/p:sync`, `/p:analyze`, and `/p:init`
|
|
21
|
+
- Reads ALL agents dynamically (agents vary per project)
|
|
22
|
+
- Extracts: stack, current task, priority queue, active features
|
|
23
|
+
- Instructions added to global CLAUDE.md template for Claude to read project context
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- **`/p:sync` Command** - Now generates dynamic project context after syncing agents
|
|
28
|
+
- **`/p:analyze` Command** - Now generates dynamic project context after analysis
|
|
29
|
+
- **Global CLAUDE.md Template** - Added instructions for Claude to read project-specific context
|
|
30
|
+
- New "🤖 Project Context (OBLIGATORIO)" section
|
|
31
|
+
- Instructs Claude to read `~/.prjct-cli/projects/{projectId}/CLAUDE.md` before working
|
|
32
|
+
|
|
33
|
+
### Technical Details
|
|
34
|
+
|
|
35
|
+
- **New Files**:
|
|
36
|
+
- `core/context-sync.js` (~140 lines) - Context generation with dynamic agent reading
|
|
37
|
+
|
|
38
|
+
- **Modified Files**:
|
|
39
|
+
- `core/commands.js` - Added context sync calls to sync() and analyze()
|
|
40
|
+
- `templates/global/CLAUDE.md` - Added project context reading instructions
|
|
41
|
+
|
|
42
|
+
- **Architecture Decision**: Context lives in global storage (NOT in repo) to avoid being overwritten by commits
|
|
43
|
+
|
|
3
44
|
## [0.10.1] - 2025-11-24
|
|
4
45
|
|
|
5
46
|
### Added
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory System Tests
|
|
3
|
+
* P3.3: Semantic Memory Database
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const memorySystem = require('../../agentic/memory-system')
|
|
7
|
+
const fs = require('fs').promises
|
|
8
|
+
const path = require('path')
|
|
9
|
+
const os = require('os')
|
|
10
|
+
|
|
11
|
+
// Generate unique project ID for each test run
|
|
12
|
+
let testCounter = 0
|
|
13
|
+
const getTestProjectId = () => `test-memory-${Date.now()}-${++testCounter}`
|
|
14
|
+
|
|
15
|
+
describe('MemorySystem P3.3', () => {
|
|
16
|
+
let TEST_PROJECT_ID
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
// Use unique project ID for each test to avoid data leakage
|
|
20
|
+
TEST_PROJECT_ID = getTestProjectId()
|
|
21
|
+
// Reset internal state
|
|
22
|
+
memorySystem._memories = null
|
|
23
|
+
memorySystem._memoriesLoaded = false
|
|
24
|
+
memorySystem._patterns = null
|
|
25
|
+
memorySystem._patternsLoaded = false
|
|
26
|
+
memorySystem._sessionMemory.clear()
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
describe('createMemory', () => {
|
|
30
|
+
it('should create a memory with tags', async () => {
|
|
31
|
+
const memoryId = await memorySystem.createMemory(TEST_PROJECT_ID, {
|
|
32
|
+
title: 'Test Memory',
|
|
33
|
+
content: 'This is test content',
|
|
34
|
+
tags: ['code_style', 'naming_convention'],
|
|
35
|
+
userTriggered: true
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
expect(memoryId).toMatch(/^mem_/)
|
|
39
|
+
|
|
40
|
+
const memories = await memorySystem.getAllMemories(TEST_PROJECT_ID)
|
|
41
|
+
expect(memories.length).toBe(1)
|
|
42
|
+
expect(memories[0].title).toBe('Test Memory')
|
|
43
|
+
expect(memories[0].tags).toContain('code_style')
|
|
44
|
+
expect(memories[0].userTriggered).toBe(true)
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
describe('updateMemory', () => {
|
|
49
|
+
it('should update memory content and tags', async () => {
|
|
50
|
+
const memoryId = await memorySystem.createMemory(TEST_PROJECT_ID, {
|
|
51
|
+
title: 'Original Title',
|
|
52
|
+
content: 'Original content',
|
|
53
|
+
tags: ['code_style']
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const updated = await memorySystem.updateMemory(TEST_PROJECT_ID, memoryId, {
|
|
57
|
+
title: 'Updated Title',
|
|
58
|
+
content: 'Updated content',
|
|
59
|
+
tags: ['naming_convention', 'architecture']
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
expect(updated).toBe(true)
|
|
63
|
+
|
|
64
|
+
const memories = await memorySystem.getAllMemories(TEST_PROJECT_ID)
|
|
65
|
+
const memory = memories.find(m => m.id === memoryId)
|
|
66
|
+
|
|
67
|
+
expect(memory.title).toBe('Updated Title')
|
|
68
|
+
expect(memory.content).toBe('Updated content')
|
|
69
|
+
expect(memory.tags).toContain('architecture')
|
|
70
|
+
expect(memory.tags).not.toContain('code_style')
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('should return false for non-existent memory', async () => {
|
|
74
|
+
const result = await memorySystem.updateMemory(TEST_PROJECT_ID, 'non_existent_id', {
|
|
75
|
+
title: 'New Title'
|
|
76
|
+
})
|
|
77
|
+
expect(result).toBe(false)
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
describe('deleteMemory', () => {
|
|
82
|
+
it('should delete a memory', async () => {
|
|
83
|
+
const memoryId = await memorySystem.createMemory(TEST_PROJECT_ID, {
|
|
84
|
+
title: 'To Delete',
|
|
85
|
+
content: 'Will be deleted',
|
|
86
|
+
tags: ['test']
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
const deleted = await memorySystem.deleteMemory(TEST_PROJECT_ID, memoryId)
|
|
90
|
+
expect(deleted).toBe(true)
|
|
91
|
+
|
|
92
|
+
const memories = await memorySystem.getAllMemories(TEST_PROJECT_ID)
|
|
93
|
+
expect(memories.find(m => m.id === memoryId)).toBeUndefined()
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
describe('findByTags', () => {
|
|
98
|
+
beforeEach(async () => {
|
|
99
|
+
// Create test memories
|
|
100
|
+
await memorySystem.createMemory(TEST_PROJECT_ID, {
|
|
101
|
+
title: 'Memory 1',
|
|
102
|
+
content: 'Content 1',
|
|
103
|
+
tags: ['code_style', 'naming_convention']
|
|
104
|
+
})
|
|
105
|
+
await memorySystem.createMemory(TEST_PROJECT_ID, {
|
|
106
|
+
title: 'Memory 2',
|
|
107
|
+
content: 'Content 2',
|
|
108
|
+
tags: ['architecture', 'naming_convention']
|
|
109
|
+
})
|
|
110
|
+
await memorySystem.createMemory(TEST_PROJECT_ID, {
|
|
111
|
+
title: 'Memory 3',
|
|
112
|
+
content: 'Content 3',
|
|
113
|
+
tags: ['commit_style']
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('should find memories with ANY tag (OR)', async () => {
|
|
118
|
+
const results = await memorySystem.findByTags(TEST_PROJECT_ID, ['code_style', 'architecture'], false)
|
|
119
|
+
expect(results.length).toBe(2)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('should find memories with ALL tags (AND)', async () => {
|
|
123
|
+
const results = await memorySystem.findByTags(TEST_PROJECT_ID, ['naming_convention', 'architecture'], true)
|
|
124
|
+
expect(results.length).toBe(1)
|
|
125
|
+
expect(results[0].title).toBe('Memory 2')
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
describe('searchMemories', () => {
|
|
130
|
+
beforeEach(async () => {
|
|
131
|
+
await memorySystem.createMemory(TEST_PROJECT_ID, {
|
|
132
|
+
title: 'React Hooks Pattern',
|
|
133
|
+
content: 'Use custom hooks for reusable logic',
|
|
134
|
+
tags: ['code_style']
|
|
135
|
+
})
|
|
136
|
+
await memorySystem.createMemory(TEST_PROJECT_ID, {
|
|
137
|
+
title: 'API Design',
|
|
138
|
+
content: 'REST endpoints follow /api/v1 pattern',
|
|
139
|
+
tags: ['architecture']
|
|
140
|
+
})
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('should search by title', async () => {
|
|
144
|
+
const results = await memorySystem.searchMemories(TEST_PROJECT_ID, 'React')
|
|
145
|
+
expect(results.length).toBe(1)
|
|
146
|
+
expect(results[0].title).toContain('React')
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('should search by content', async () => {
|
|
150
|
+
const results = await memorySystem.searchMemories(TEST_PROJECT_ID, 'endpoints')
|
|
151
|
+
expect(results.length).toBe(1)
|
|
152
|
+
expect(results[0].content).toContain('endpoints')
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('should be case insensitive', async () => {
|
|
156
|
+
const results = await memorySystem.searchMemories(TEST_PROJECT_ID, 'HOOKS')
|
|
157
|
+
expect(results.length).toBe(1)
|
|
158
|
+
})
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
describe('getRelevantMemories', () => {
|
|
162
|
+
beforeEach(async () => {
|
|
163
|
+
// Create memories with different relevance
|
|
164
|
+
await memorySystem.createMemory(TEST_PROJECT_ID, {
|
|
165
|
+
title: 'Commit Style',
|
|
166
|
+
content: 'Use conventional commits',
|
|
167
|
+
tags: ['commit_style', 'ship_workflow'],
|
|
168
|
+
userTriggered: true
|
|
169
|
+
})
|
|
170
|
+
await memorySystem.createMemory(TEST_PROJECT_ID, {
|
|
171
|
+
title: 'Test Behavior',
|
|
172
|
+
content: 'Run tests before shipping',
|
|
173
|
+
tags: ['test_behavior', 'ship_workflow']
|
|
174
|
+
})
|
|
175
|
+
await memorySystem.createMemory(TEST_PROJECT_ID, {
|
|
176
|
+
title: 'Code Style',
|
|
177
|
+
content: 'Use TypeScript strict mode',
|
|
178
|
+
tags: ['code_style']
|
|
179
|
+
})
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it('should return memories relevant to ship command', async () => {
|
|
183
|
+
const context = { commandName: 'ship', params: {} }
|
|
184
|
+
const results = await memorySystem.getRelevantMemories(TEST_PROJECT_ID, context, 5)
|
|
185
|
+
|
|
186
|
+
expect(results.length).toBeGreaterThan(0)
|
|
187
|
+
// Ship command should prioritize commit_style and ship_workflow tags
|
|
188
|
+
const hasRelevantTags = results.some(m =>
|
|
189
|
+
m.tags.includes('commit_style') || m.tags.includes('ship_workflow')
|
|
190
|
+
)
|
|
191
|
+
expect(hasRelevantTags).toBe(true)
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it('should prioritize user triggered memories', async () => {
|
|
195
|
+
const context = { commandName: 'ship', params: {} }
|
|
196
|
+
const results = await memorySystem.getRelevantMemories(TEST_PROJECT_ID, context, 5)
|
|
197
|
+
|
|
198
|
+
// User triggered should be ranked higher
|
|
199
|
+
const userTriggeredIndex = results.findIndex(m => m.userTriggered)
|
|
200
|
+
expect(userTriggeredIndex).toBeLessThanOrEqual(1) // Should be in top 2
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
it('should limit results', async () => {
|
|
204
|
+
const context = { commandName: 'ship', params: {} }
|
|
205
|
+
const results = await memorySystem.getRelevantMemories(TEST_PROJECT_ID, context, 1)
|
|
206
|
+
expect(results.length).toBeLessThanOrEqual(1)
|
|
207
|
+
})
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
describe('autoRemember', () => {
|
|
211
|
+
it('should create memory from user decision', async () => {
|
|
212
|
+
await memorySystem.autoRemember(TEST_PROJECT_ID, 'commit_footer', 'prjct', 'User chose prjct footer')
|
|
213
|
+
|
|
214
|
+
const memories = await memorySystem.getAllMemories(TEST_PROJECT_ID)
|
|
215
|
+
expect(memories.length).toBe(1)
|
|
216
|
+
expect(memories[0].content).toContain('commit_footer: prjct')
|
|
217
|
+
expect(memories[0].tags).toContain('commit_style')
|
|
218
|
+
expect(memories[0].userTriggered).toBe(true)
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it('should update existing memory instead of creating duplicate', async () => {
|
|
222
|
+
await memorySystem.autoRemember(TEST_PROJECT_ID, 'commit_footer', 'prjct', 'First choice')
|
|
223
|
+
await memorySystem.autoRemember(TEST_PROJECT_ID, 'commit_footer', 'claude', 'Changed mind')
|
|
224
|
+
|
|
225
|
+
const memories = await memorySystem.getAllMemories(TEST_PROJECT_ID)
|
|
226
|
+
expect(memories.length).toBe(1)
|
|
227
|
+
expect(memories[0].content).toContain('commit_footer: claude')
|
|
228
|
+
})
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
describe('getMemoryStats', () => {
|
|
232
|
+
it('should return memory statistics', async () => {
|
|
233
|
+
await memorySystem.createMemory(TEST_PROJECT_ID, {
|
|
234
|
+
title: 'Memory 1',
|
|
235
|
+
content: 'Content 1',
|
|
236
|
+
tags: ['code_style'],
|
|
237
|
+
userTriggered: true
|
|
238
|
+
})
|
|
239
|
+
await memorySystem.createMemory(TEST_PROJECT_ID, {
|
|
240
|
+
title: 'Memory 2',
|
|
241
|
+
content: 'Content 2',
|
|
242
|
+
tags: ['code_style', 'architecture']
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
const stats = await memorySystem.getMemoryStats(TEST_PROJECT_ID)
|
|
246
|
+
|
|
247
|
+
expect(stats.totalMemories).toBe(2)
|
|
248
|
+
expect(stats.userTriggered).toBe(1)
|
|
249
|
+
expect(stats.tagCounts.code_style).toBe(2)
|
|
250
|
+
expect(stats.tagCounts.architecture).toBe(1)
|
|
251
|
+
})
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
// Cleanup test directories after each test
|
|
255
|
+
afterEach(async () => {
|
|
256
|
+
try {
|
|
257
|
+
const testPath = path.join(os.homedir(), '.prjct-cli', 'projects', TEST_PROJECT_ID)
|
|
258
|
+
await fs.rm(testPath, { recursive: true, force: true })
|
|
259
|
+
} catch {
|
|
260
|
+
// Ignore cleanup errors
|
|
261
|
+
}
|
|
262
|
+
})
|
|
263
|
+
})
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan Mode Tests
|
|
3
|
+
* P3.4: Plan Mode + Approval Flow
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const planMode = require('../../agentic/plan-mode')
|
|
7
|
+
const { PLAN_STATUS, PLAN_REQUIRED_COMMANDS, DESTRUCTIVE_COMMANDS, PLANNING_TOOLS } = require('../../agentic/plan-mode')
|
|
8
|
+
|
|
9
|
+
describe('PlanMode P3.4', () => {
|
|
10
|
+
const TEST_PROJECT_ID = 'test-plan-mode'
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
// Clear any active plans
|
|
14
|
+
planMode.activePlans.clear()
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
describe('requiresPlanning', () => {
|
|
18
|
+
it('should return true for feature command', () => {
|
|
19
|
+
expect(planMode.requiresPlanning('feature')).toBe(true)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('should return true for spec command', () => {
|
|
23
|
+
expect(planMode.requiresPlanning('spec')).toBe(true)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('should return false for now command', () => {
|
|
27
|
+
expect(planMode.requiresPlanning('now')).toBe(false)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('should return false for done command', () => {
|
|
31
|
+
expect(planMode.requiresPlanning('done')).toBe(false)
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
describe('isDestructive', () => {
|
|
36
|
+
it('should return true for ship command', () => {
|
|
37
|
+
expect(planMode.isDestructive('ship')).toBe(true)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('should return true for cleanup command', () => {
|
|
41
|
+
expect(planMode.isDestructive('cleanup')).toBe(true)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('should return false for feature command', () => {
|
|
45
|
+
expect(planMode.isDestructive('feature')).toBe(false)
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
describe('isToolAllowedInPlanning', () => {
|
|
50
|
+
it('should allow Read in planning mode', () => {
|
|
51
|
+
expect(planMode.isToolAllowedInPlanning('Read')).toBe(true)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('should allow Glob in planning mode', () => {
|
|
55
|
+
expect(planMode.isToolAllowedInPlanning('Glob')).toBe(true)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('should not allow Write in planning mode', () => {
|
|
59
|
+
expect(planMode.isToolAllowedInPlanning('Write')).toBe(false)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('should not allow Bash in planning mode', () => {
|
|
63
|
+
expect(planMode.isToolAllowedInPlanning('Bash')).toBe(false)
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
describe('getAllowedTools', () => {
|
|
68
|
+
it('should filter to read-only tools in planning mode', () => {
|
|
69
|
+
const templateTools = ['Read', 'Write', 'Glob', 'Bash', 'Grep']
|
|
70
|
+
const allowed = planMode.getAllowedTools(true, templateTools)
|
|
71
|
+
|
|
72
|
+
expect(allowed).toContain('Read')
|
|
73
|
+
expect(allowed).toContain('Glob')
|
|
74
|
+
expect(allowed).toContain('Grep')
|
|
75
|
+
expect(allowed).not.toContain('Write')
|
|
76
|
+
expect(allowed).not.toContain('Bash')
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('should return all template tools when not in planning mode', () => {
|
|
80
|
+
const templateTools = ['Read', 'Write', 'Glob', 'Bash']
|
|
81
|
+
const allowed = planMode.getAllowedTools(false, templateTools)
|
|
82
|
+
|
|
83
|
+
expect(allowed).toEqual(templateTools)
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
describe('startPlanning', () => {
|
|
88
|
+
it('should create a new plan with correct initial state', () => {
|
|
89
|
+
const plan = planMode.startPlanning(TEST_PROJECT_ID, 'feature', { description: 'Add dark mode' })
|
|
90
|
+
|
|
91
|
+
expect(plan.id).toMatch(/^plan_/)
|
|
92
|
+
expect(plan.projectId).toBe(TEST_PROJECT_ID)
|
|
93
|
+
expect(plan.command).toBe('feature')
|
|
94
|
+
expect(plan.status).toBe(PLAN_STATUS.GATHERING)
|
|
95
|
+
expect(plan.gatheredInfo).toEqual([])
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('should store plan in activePlans', () => {
|
|
99
|
+
planMode.startPlanning(TEST_PROJECT_ID, 'feature', {})
|
|
100
|
+
|
|
101
|
+
expect(planMode.getActivePlan(TEST_PROJECT_ID)).not.toBeNull()
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
describe('isInPlanningMode', () => {
|
|
106
|
+
it('should return true when gathering info', () => {
|
|
107
|
+
planMode.startPlanning(TEST_PROJECT_ID, 'feature', {})
|
|
108
|
+
|
|
109
|
+
expect(planMode.isInPlanningMode(TEST_PROJECT_ID)).toBe(true)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('should return true when pending approval', () => {
|
|
113
|
+
planMode.startPlanning(TEST_PROJECT_ID, 'feature', {})
|
|
114
|
+
planMode.proposePlan(TEST_PROJECT_ID, { summary: 'Test', steps: [] })
|
|
115
|
+
|
|
116
|
+
expect(planMode.isInPlanningMode(TEST_PROJECT_ID)).toBe(true)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('should return false when no active plan', () => {
|
|
120
|
+
expect(planMode.isInPlanningMode('non-existent-project')).toBe(false)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('should return false when plan is executing', () => {
|
|
124
|
+
planMode.startPlanning(TEST_PROJECT_ID, 'feature', {})
|
|
125
|
+
planMode.proposePlan(TEST_PROJECT_ID, { summary: 'Test', steps: [{ description: 'Step 1' }] })
|
|
126
|
+
planMode.approvePlan(TEST_PROJECT_ID)
|
|
127
|
+
planMode.startExecution(TEST_PROJECT_ID)
|
|
128
|
+
|
|
129
|
+
expect(planMode.isInPlanningMode(TEST_PROJECT_ID)).toBe(false)
|
|
130
|
+
})
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
describe('recordGatheredInfo', () => {
|
|
134
|
+
it('should add info to gatheredInfo array', () => {
|
|
135
|
+
planMode.startPlanning(TEST_PROJECT_ID, 'feature', {})
|
|
136
|
+
planMode.recordGatheredInfo(TEST_PROJECT_ID, { type: 'file', source: 'src/index.js', data: 'content' })
|
|
137
|
+
|
|
138
|
+
const plan = planMode.getActivePlan(TEST_PROJECT_ID)
|
|
139
|
+
expect(plan.gatheredInfo.length).toBe(1)
|
|
140
|
+
expect(plan.gatheredInfo[0].type).toBe('file')
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
describe('proposePlan', () => {
|
|
145
|
+
it('should set status to pending approval', () => {
|
|
146
|
+
planMode.startPlanning(TEST_PROJECT_ID, 'feature', {})
|
|
147
|
+
planMode.proposePlan(TEST_PROJECT_ID, {
|
|
148
|
+
summary: 'Add dark mode feature',
|
|
149
|
+
approach: 'CSS variables with theme context',
|
|
150
|
+
steps: [{ description: 'Create theme context' }, { description: 'Add toggle' }]
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
const plan = planMode.getActivePlan(TEST_PROJECT_ID)
|
|
154
|
+
expect(plan.status).toBe(PLAN_STATUS.PENDING_APPROVAL)
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('should return formatted plan for display', () => {
|
|
158
|
+
planMode.startPlanning(TEST_PROJECT_ID, 'feature', {})
|
|
159
|
+
const formatted = planMode.proposePlan(TEST_PROJECT_ID, {
|
|
160
|
+
summary: 'Test plan',
|
|
161
|
+
approach: 'Test approach',
|
|
162
|
+
steps: [{ description: 'Step 1' }]
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
expect(formatted.summary).toBe('Test plan')
|
|
166
|
+
expect(formatted.approach).toBe('Test approach')
|
|
167
|
+
expect(formatted.requiresConfirmation).toBe(true)
|
|
168
|
+
})
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
describe('approvePlan', () => {
|
|
172
|
+
it('should change status to approved', () => {
|
|
173
|
+
planMode.startPlanning(TEST_PROJECT_ID, 'feature', {})
|
|
174
|
+
planMode.proposePlan(TEST_PROJECT_ID, { steps: [{ description: 'Step 1' }] })
|
|
175
|
+
const result = planMode.approvePlan(TEST_PROJECT_ID)
|
|
176
|
+
|
|
177
|
+
expect(result.approved).toBe(true)
|
|
178
|
+
const plan = planMode.getActivePlan(TEST_PROJECT_ID)
|
|
179
|
+
expect(plan.status).toBe(PLAN_STATUS.APPROVED)
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it('should convert proposed steps to executable steps', () => {
|
|
183
|
+
planMode.startPlanning(TEST_PROJECT_ID, 'feature', {})
|
|
184
|
+
planMode.proposePlan(TEST_PROJECT_ID, {
|
|
185
|
+
steps: [{ description: 'Step 1' }, { description: 'Step 2' }]
|
|
186
|
+
})
|
|
187
|
+
const result = planMode.approvePlan(TEST_PROJECT_ID)
|
|
188
|
+
|
|
189
|
+
expect(result.steps.length).toBe(2)
|
|
190
|
+
expect(result.steps[0].status).toBe('pending')
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
it('should return null if not pending approval', () => {
|
|
194
|
+
planMode.startPlanning(TEST_PROJECT_ID, 'feature', {})
|
|
195
|
+
// Not proposed yet, so should return null
|
|
196
|
+
const result = planMode.approvePlan(TEST_PROJECT_ID)
|
|
197
|
+
|
|
198
|
+
expect(result).toBeNull()
|
|
199
|
+
})
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
describe('rejectPlan', () => {
|
|
203
|
+
it('should mark plan as rejected', () => {
|
|
204
|
+
planMode.startPlanning(TEST_PROJECT_ID, 'feature', {})
|
|
205
|
+
planMode.proposePlan(TEST_PROJECT_ID, { steps: [] })
|
|
206
|
+
const result = planMode.rejectPlan(TEST_PROJECT_ID, 'Not the right approach')
|
|
207
|
+
|
|
208
|
+
expect(result.rejected).toBe(true)
|
|
209
|
+
expect(result.reason).toBe('Not the right approach')
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
it('should clear active plan after rejection', () => {
|
|
213
|
+
planMode.startPlanning(TEST_PROJECT_ID, 'feature', {})
|
|
214
|
+
planMode.proposePlan(TEST_PROJECT_ID, { steps: [] })
|
|
215
|
+
planMode.rejectPlan(TEST_PROJECT_ID)
|
|
216
|
+
|
|
217
|
+
expect(planMode.getActivePlan(TEST_PROJECT_ID)).toBeNull()
|
|
218
|
+
})
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
describe('execution flow', () => {
|
|
222
|
+
beforeEach(() => {
|
|
223
|
+
planMode.startPlanning(TEST_PROJECT_ID, 'feature', {})
|
|
224
|
+
planMode.proposePlan(TEST_PROJECT_ID, {
|
|
225
|
+
steps: [
|
|
226
|
+
{ description: 'Step 1', tool: 'Write' },
|
|
227
|
+
{ description: 'Step 2', tool: 'Bash' }
|
|
228
|
+
]
|
|
229
|
+
})
|
|
230
|
+
planMode.approvePlan(TEST_PROJECT_ID)
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
it('should start execution and return first step', () => {
|
|
234
|
+
const step = planMode.startExecution(TEST_PROJECT_ID)
|
|
235
|
+
|
|
236
|
+
expect(step.stepNumber).toBe(1)
|
|
237
|
+
expect(step.totalSteps).toBe(2)
|
|
238
|
+
expect(step.progress).toBe(0)
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
it('should advance to next step on completion', () => {
|
|
242
|
+
planMode.startExecution(TEST_PROJECT_ID)
|
|
243
|
+
const nextStep = planMode.completeStep(TEST_PROJECT_ID, { success: true })
|
|
244
|
+
|
|
245
|
+
expect(nextStep.stepNumber).toBe(2)
|
|
246
|
+
expect(nextStep.progress).toBe(50)
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
it('should complete plan when all steps done', () => {
|
|
250
|
+
planMode.startExecution(TEST_PROJECT_ID)
|
|
251
|
+
planMode.completeStep(TEST_PROJECT_ID)
|
|
252
|
+
const result = planMode.completeStep(TEST_PROJECT_ID)
|
|
253
|
+
|
|
254
|
+
// When all steps complete, getNextStep returns null and completePlan is called
|
|
255
|
+
expect(result).toBeNull()
|
|
256
|
+
expect(planMode.getActivePlan(TEST_PROJECT_ID)).toBeNull()
|
|
257
|
+
})
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
describe('abortPlan', () => {
|
|
261
|
+
it('should abort and clear active plan', () => {
|
|
262
|
+
planMode.startPlanning(TEST_PROJECT_ID, 'feature', {})
|
|
263
|
+
planMode.proposePlan(TEST_PROJECT_ID, { steps: [{ description: 'Step 1' }] })
|
|
264
|
+
planMode.approvePlan(TEST_PROJECT_ID)
|
|
265
|
+
planMode.startExecution(TEST_PROJECT_ID)
|
|
266
|
+
|
|
267
|
+
const result = planMode.abortPlan(TEST_PROJECT_ID, 'User cancelled')
|
|
268
|
+
|
|
269
|
+
expect(result.aborted).toBe(true)
|
|
270
|
+
expect(result.reason).toBe('User cancelled')
|
|
271
|
+
expect(planMode.getActivePlan(TEST_PROJECT_ID)).toBeNull()
|
|
272
|
+
})
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
describe('generateApprovalPrompt', () => {
|
|
276
|
+
it('should generate ship approval prompt', () => {
|
|
277
|
+
const prompt = planMode.generateApprovalPrompt('ship', {
|
|
278
|
+
branch: 'feature/dark-mode',
|
|
279
|
+
changedFiles: ['a.js', 'b.js'],
|
|
280
|
+
commitMessage: 'Add dark mode'
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
expect(prompt.title).toBe('Ship Confirmation')
|
|
284
|
+
expect(prompt.details).toContain('Branch: feature/dark-mode')
|
|
285
|
+
expect(prompt.options.length).toBe(3)
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
it('should generate cleanup approval prompt', () => {
|
|
289
|
+
const prompt = planMode.generateApprovalPrompt('cleanup', {
|
|
290
|
+
filesToDelete: ['temp.js'],
|
|
291
|
+
linesOfCode: 50
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
expect(prompt.title).toBe('Cleanup Confirmation')
|
|
295
|
+
expect(prompt.message).toContain('delete')
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
it('should generate default prompt for unknown commands', () => {
|
|
299
|
+
const prompt = planMode.generateApprovalPrompt('unknown', {})
|
|
300
|
+
|
|
301
|
+
expect(prompt.title).toBe('Confirmation Required')
|
|
302
|
+
expect(prompt.options.length).toBe(2)
|
|
303
|
+
})
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
describe('formatStatus', () => {
|
|
307
|
+
it('should format gathering status', () => {
|
|
308
|
+
planMode.startPlanning(TEST_PROJECT_ID, 'feature', {})
|
|
309
|
+
const status = planMode.formatStatus(TEST_PROJECT_ID)
|
|
310
|
+
|
|
311
|
+
expect(status).toContain('🔍')
|
|
312
|
+
expect(status).toContain('feature')
|
|
313
|
+
expect(status).toContain('gathering')
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
it('should show progress during execution', () => {
|
|
317
|
+
planMode.startPlanning(TEST_PROJECT_ID, 'feature', {})
|
|
318
|
+
planMode.proposePlan(TEST_PROJECT_ID, {
|
|
319
|
+
steps: [{ description: 'Step 1' }, { description: 'Step 2' }]
|
|
320
|
+
})
|
|
321
|
+
planMode.approvePlan(TEST_PROJECT_ID)
|
|
322
|
+
planMode.startExecution(TEST_PROJECT_ID)
|
|
323
|
+
|
|
324
|
+
const status = planMode.formatStatus(TEST_PROJECT_ID)
|
|
325
|
+
|
|
326
|
+
expect(status).toContain('⚡')
|
|
327
|
+
expect(status).toContain('Progress')
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
it('should return message for no active plan', () => {
|
|
331
|
+
const status = planMode.formatStatus('non-existent')
|
|
332
|
+
|
|
333
|
+
expect(status).toBe('No active plan')
|
|
334
|
+
})
|
|
335
|
+
})
|
|
336
|
+
})
|