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.
- package/CHANGELOG.md +142 -0
- package/core/__tests__/agentic/memory-system.test.js +263 -0
- package/core/__tests__/agentic/plan-mode.test.js +336 -0
- package/core/agentic/agent-router.js +253 -186
- package/core/agentic/chain-of-thought.js +578 -0
- package/core/agentic/command-executor.js +299 -17
- package/core/agentic/context-builder.js +208 -8
- package/core/agentic/context-filter.js +83 -83
- 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 +127 -2
- 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 +128 -60
- package/core/context-sync.js +183 -0
- 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 +8 -16
- 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 -212
- 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 -296
- 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
|
@@ -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
|
+
})
|