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
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
+ })