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,572 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan Mode System
|
|
3
|
+
*
|
|
4
|
+
* P3.4: Plan Mode + Approval Flow
|
|
5
|
+
* Separates planning from execution for better user confidence.
|
|
6
|
+
*
|
|
7
|
+
* Pattern from: Devin AI, Windsurf, Kiro
|
|
8
|
+
*
|
|
9
|
+
* ```
|
|
10
|
+
* // Planning Mode:
|
|
11
|
+
* // 1. Gather information (read-only tools)
|
|
12
|
+
* // 2. Analyze and understand codebase
|
|
13
|
+
* // 3. Ask clarifying questions
|
|
14
|
+
* // 4. Generate plan with <suggest_plan/>
|
|
15
|
+
* // 5. Wait for user approval
|
|
16
|
+
*
|
|
17
|
+
* // Execution Mode:
|
|
18
|
+
* // 1. Execute approved plan
|
|
19
|
+
* // 2. Show progress
|
|
20
|
+
* // 3. User can pause/abort
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Commands that require planning mode
|
|
26
|
+
*/
|
|
27
|
+
const PLAN_REQUIRED_COMMANDS = [
|
|
28
|
+
'feature', // New features need planning
|
|
29
|
+
'spec', // Specs are planning by definition
|
|
30
|
+
'design', // Architecture needs planning
|
|
31
|
+
'refactor', // Refactoring needs impact analysis
|
|
32
|
+
'migrate' // Migrations are high-risk
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Commands that are destructive and need approval
|
|
37
|
+
*/
|
|
38
|
+
const DESTRUCTIVE_COMMANDS = [
|
|
39
|
+
'ship', // Commits and pushes
|
|
40
|
+
'cleanup', // Deletes files/code
|
|
41
|
+
'git', // Git operations
|
|
42
|
+
'migrate' // Database/schema changes
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Read-only tools allowed in planning mode
|
|
47
|
+
*/
|
|
48
|
+
const PLANNING_TOOLS = [
|
|
49
|
+
'Read',
|
|
50
|
+
'Glob',
|
|
51
|
+
'Grep',
|
|
52
|
+
'GetTimestamp',
|
|
53
|
+
'GetDate',
|
|
54
|
+
'GetDateTime'
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Plan status enum
|
|
59
|
+
*/
|
|
60
|
+
const PLAN_STATUS = {
|
|
61
|
+
GATHERING: 'gathering', // Collecting information
|
|
62
|
+
ANALYZING: 'analyzing', // Understanding context
|
|
63
|
+
PROPOSING: 'proposing', // Generating plan
|
|
64
|
+
PENDING_APPROVAL: 'pending', // Waiting for user
|
|
65
|
+
APPROVED: 'approved', // User approved
|
|
66
|
+
REJECTED: 'rejected', // User rejected
|
|
67
|
+
EXECUTING: 'executing', // Running the plan
|
|
68
|
+
COMPLETED: 'completed', // Done
|
|
69
|
+
ABORTED: 'aborted' // User stopped mid-execution
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
class PlanMode {
|
|
73
|
+
constructor() {
|
|
74
|
+
this.activePlans = new Map() // projectId -> plan state
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Check if command requires planning mode
|
|
79
|
+
* @param {string} commandName
|
|
80
|
+
* @returns {boolean}
|
|
81
|
+
*/
|
|
82
|
+
requiresPlanning(commandName) {
|
|
83
|
+
return PLAN_REQUIRED_COMMANDS.includes(commandName)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Check if command is destructive and needs approval
|
|
88
|
+
* @param {string} commandName
|
|
89
|
+
* @returns {boolean}
|
|
90
|
+
*/
|
|
91
|
+
isDestructive(commandName) {
|
|
92
|
+
return DESTRUCTIVE_COMMANDS.includes(commandName)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Check if tool is allowed in planning mode
|
|
97
|
+
* @param {string} toolName
|
|
98
|
+
* @returns {boolean}
|
|
99
|
+
*/
|
|
100
|
+
isToolAllowedInPlanning(toolName) {
|
|
101
|
+
return PLANNING_TOOLS.includes(toolName)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Get allowed tools for current mode
|
|
106
|
+
* @param {boolean} inPlanningMode
|
|
107
|
+
* @param {string[]} templateTools - Tools from template
|
|
108
|
+
* @returns {string[]}
|
|
109
|
+
*/
|
|
110
|
+
getAllowedTools(inPlanningMode, templateTools) {
|
|
111
|
+
if (!inPlanningMode) {
|
|
112
|
+
return templateTools
|
|
113
|
+
}
|
|
114
|
+
// In planning mode, only allow read-only tools
|
|
115
|
+
return templateTools.filter(tool => PLANNING_TOOLS.includes(tool))
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Start planning mode for a command
|
|
120
|
+
* @param {string} projectId
|
|
121
|
+
* @param {string} commandName
|
|
122
|
+
* @param {Object} params
|
|
123
|
+
* @returns {Object} Plan state
|
|
124
|
+
*/
|
|
125
|
+
startPlanning(projectId, commandName, params) {
|
|
126
|
+
const plan = {
|
|
127
|
+
id: `plan_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
128
|
+
projectId,
|
|
129
|
+
command: commandName,
|
|
130
|
+
params,
|
|
131
|
+
status: PLAN_STATUS.GATHERING,
|
|
132
|
+
startedAt: new Date().toISOString(),
|
|
133
|
+
gatheredInfo: [],
|
|
134
|
+
analysis: null,
|
|
135
|
+
proposedPlan: null,
|
|
136
|
+
userFeedback: null,
|
|
137
|
+
approvedAt: null,
|
|
138
|
+
executionStartedAt: null,
|
|
139
|
+
completedAt: null,
|
|
140
|
+
steps: [],
|
|
141
|
+
currentStep: 0
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
this.activePlans.set(projectId, plan)
|
|
145
|
+
return plan
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Get active plan for project
|
|
150
|
+
* @param {string} projectId
|
|
151
|
+
* @returns {Object|null}
|
|
152
|
+
*/
|
|
153
|
+
getActivePlan(projectId) {
|
|
154
|
+
return this.activePlans.get(projectId) || null
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Check if project is in planning mode
|
|
159
|
+
* @param {string} projectId
|
|
160
|
+
* @returns {boolean}
|
|
161
|
+
*/
|
|
162
|
+
isInPlanningMode(projectId) {
|
|
163
|
+
const plan = this.getActivePlan(projectId)
|
|
164
|
+
if (!plan) return false
|
|
165
|
+
return [
|
|
166
|
+
PLAN_STATUS.GATHERING,
|
|
167
|
+
PLAN_STATUS.ANALYZING,
|
|
168
|
+
PLAN_STATUS.PROPOSING,
|
|
169
|
+
PLAN_STATUS.PENDING_APPROVAL
|
|
170
|
+
].includes(plan.status)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Record gathered information
|
|
175
|
+
* @param {string} projectId
|
|
176
|
+
* @param {Object} info - { type, source, data }
|
|
177
|
+
*/
|
|
178
|
+
recordGatheredInfo(projectId, info) {
|
|
179
|
+
const plan = this.getActivePlan(projectId)
|
|
180
|
+
if (!plan) return
|
|
181
|
+
|
|
182
|
+
plan.gatheredInfo.push({
|
|
183
|
+
...info,
|
|
184
|
+
gatheredAt: new Date().toISOString()
|
|
185
|
+
})
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Update plan status
|
|
190
|
+
* @param {string} projectId
|
|
191
|
+
* @param {string} status
|
|
192
|
+
*/
|
|
193
|
+
updateStatus(projectId, status) {
|
|
194
|
+
const plan = this.getActivePlan(projectId)
|
|
195
|
+
if (!plan) return
|
|
196
|
+
|
|
197
|
+
plan.status = status
|
|
198
|
+
|
|
199
|
+
// Track timestamps for key transitions
|
|
200
|
+
if (status === PLAN_STATUS.APPROVED) {
|
|
201
|
+
plan.approvedAt = new Date().toISOString()
|
|
202
|
+
} else if (status === PLAN_STATUS.EXECUTING) {
|
|
203
|
+
plan.executionStartedAt = new Date().toISOString()
|
|
204
|
+
} else if (status === PLAN_STATUS.COMPLETED || status === PLAN_STATUS.ABORTED) {
|
|
205
|
+
plan.completedAt = new Date().toISOString()
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Set analysis results
|
|
211
|
+
* @param {string} projectId
|
|
212
|
+
* @param {Object} analysis
|
|
213
|
+
*/
|
|
214
|
+
setAnalysis(projectId, analysis) {
|
|
215
|
+
const plan = this.getActivePlan(projectId)
|
|
216
|
+
if (!plan) return
|
|
217
|
+
|
|
218
|
+
plan.analysis = analysis
|
|
219
|
+
plan.status = PLAN_STATUS.ANALYZING
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Propose a plan for user approval
|
|
224
|
+
* @param {string} projectId
|
|
225
|
+
* @param {Object} proposedPlan
|
|
226
|
+
* @returns {Object} Formatted plan for display
|
|
227
|
+
*/
|
|
228
|
+
proposePlan(projectId, proposedPlan) {
|
|
229
|
+
const plan = this.getActivePlan(projectId)
|
|
230
|
+
if (!plan) return null
|
|
231
|
+
|
|
232
|
+
plan.proposedPlan = proposedPlan
|
|
233
|
+
plan.status = PLAN_STATUS.PENDING_APPROVAL
|
|
234
|
+
|
|
235
|
+
return this.formatPlanForApproval(plan)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Format plan for user approval display
|
|
240
|
+
* @param {Object} plan
|
|
241
|
+
* @returns {Object}
|
|
242
|
+
*/
|
|
243
|
+
formatPlanForApproval(plan) {
|
|
244
|
+
const proposed = plan.proposedPlan
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
summary: proposed.summary || `Plan for: ${plan.command}`,
|
|
248
|
+
approach: proposed.approach,
|
|
249
|
+
steps: proposed.steps || [],
|
|
250
|
+
risks: proposed.risks || [],
|
|
251
|
+
alternatives: proposed.alternatives || [],
|
|
252
|
+
estimatedTime: proposed.estimatedTime,
|
|
253
|
+
affectedFiles: proposed.affectedFiles || [],
|
|
254
|
+
requiresConfirmation: true,
|
|
255
|
+
planId: plan.id
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* User approves the plan
|
|
261
|
+
* @param {string} projectId
|
|
262
|
+
* @param {string} feedback - Optional user feedback
|
|
263
|
+
* @returns {Object} Approved plan ready for execution
|
|
264
|
+
*/
|
|
265
|
+
approvePlan(projectId, feedback = null) {
|
|
266
|
+
const plan = this.getActivePlan(projectId)
|
|
267
|
+
if (!plan || plan.status !== PLAN_STATUS.PENDING_APPROVAL) {
|
|
268
|
+
return null
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
plan.userFeedback = feedback
|
|
272
|
+
plan.status = PLAN_STATUS.APPROVED
|
|
273
|
+
plan.approvedAt = new Date().toISOString()
|
|
274
|
+
|
|
275
|
+
// Convert proposed plan to executable steps
|
|
276
|
+
plan.steps = (plan.proposedPlan.steps || []).map((step, index) => ({
|
|
277
|
+
index,
|
|
278
|
+
description: step.description || step,
|
|
279
|
+
status: 'pending',
|
|
280
|
+
tool: step.tool,
|
|
281
|
+
args: step.args
|
|
282
|
+
}))
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
approved: true,
|
|
286
|
+
planId: plan.id,
|
|
287
|
+
steps: plan.steps,
|
|
288
|
+
message: `Plan approved. ${plan.steps.length} steps to execute.`
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* User rejects the plan
|
|
294
|
+
* @param {string} projectId
|
|
295
|
+
* @param {string} reason
|
|
296
|
+
* @returns {Object}
|
|
297
|
+
*/
|
|
298
|
+
rejectPlan(projectId, reason = null) {
|
|
299
|
+
const plan = this.getActivePlan(projectId)
|
|
300
|
+
if (!plan) return null
|
|
301
|
+
|
|
302
|
+
plan.status = PLAN_STATUS.REJECTED
|
|
303
|
+
plan.userFeedback = reason
|
|
304
|
+
plan.completedAt = new Date().toISOString()
|
|
305
|
+
|
|
306
|
+
// Clear active plan
|
|
307
|
+
this.activePlans.delete(projectId)
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
rejected: true,
|
|
311
|
+
planId: plan.id,
|
|
312
|
+
reason,
|
|
313
|
+
message: 'Plan rejected. No changes made.'
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Start executing approved plan
|
|
319
|
+
* @param {string} projectId
|
|
320
|
+
* @returns {Object} First step to execute
|
|
321
|
+
*/
|
|
322
|
+
startExecution(projectId) {
|
|
323
|
+
const plan = this.getActivePlan(projectId)
|
|
324
|
+
if (!plan || plan.status !== PLAN_STATUS.APPROVED) {
|
|
325
|
+
return null
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
plan.status = PLAN_STATUS.EXECUTING
|
|
329
|
+
plan.executionStartedAt = new Date().toISOString()
|
|
330
|
+
plan.currentStep = 0
|
|
331
|
+
|
|
332
|
+
return this.getNextStep(projectId)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Get next step to execute
|
|
337
|
+
* @param {string} projectId
|
|
338
|
+
* @returns {Object|null}
|
|
339
|
+
*/
|
|
340
|
+
getNextStep(projectId) {
|
|
341
|
+
const plan = this.getActivePlan(projectId)
|
|
342
|
+
if (!plan || plan.status !== PLAN_STATUS.EXECUTING) {
|
|
343
|
+
return null
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const step = plan.steps[plan.currentStep]
|
|
347
|
+
if (!step) {
|
|
348
|
+
// All steps complete
|
|
349
|
+
this.completePlan(projectId)
|
|
350
|
+
return null
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return {
|
|
354
|
+
stepNumber: plan.currentStep + 1,
|
|
355
|
+
totalSteps: plan.steps.length,
|
|
356
|
+
step,
|
|
357
|
+
progress: Math.round((plan.currentStep / plan.steps.length) * 100)
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Mark current step as complete
|
|
363
|
+
* @param {string} projectId
|
|
364
|
+
* @param {Object} result - Step execution result
|
|
365
|
+
* @returns {Object} Next step or completion status
|
|
366
|
+
*/
|
|
367
|
+
completeStep(projectId, result = {}) {
|
|
368
|
+
const plan = this.getActivePlan(projectId)
|
|
369
|
+
if (!plan || plan.status !== PLAN_STATUS.EXECUTING) {
|
|
370
|
+
return null
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Update current step
|
|
374
|
+
plan.steps[plan.currentStep].status = 'completed'
|
|
375
|
+
plan.steps[plan.currentStep].result = result
|
|
376
|
+
plan.steps[plan.currentStep].completedAt = new Date().toISOString()
|
|
377
|
+
|
|
378
|
+
// Move to next step
|
|
379
|
+
plan.currentStep++
|
|
380
|
+
|
|
381
|
+
return this.getNextStep(projectId)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Mark step as failed
|
|
386
|
+
* @param {string} projectId
|
|
387
|
+
* @param {string} error
|
|
388
|
+
* @returns {Object}
|
|
389
|
+
*/
|
|
390
|
+
failStep(projectId, error) {
|
|
391
|
+
const plan = this.getActivePlan(projectId)
|
|
392
|
+
if (!plan) return null
|
|
393
|
+
|
|
394
|
+
plan.steps[plan.currentStep].status = 'failed'
|
|
395
|
+
plan.steps[plan.currentStep].error = error
|
|
396
|
+
|
|
397
|
+
return {
|
|
398
|
+
failed: true,
|
|
399
|
+
step: plan.currentStep + 1,
|
|
400
|
+
error,
|
|
401
|
+
options: ['retry', 'skip', 'abort']
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Complete the plan
|
|
407
|
+
* @param {string} projectId
|
|
408
|
+
* @returns {Object} Completion summary
|
|
409
|
+
*/
|
|
410
|
+
completePlan(projectId) {
|
|
411
|
+
const plan = this.getActivePlan(projectId)
|
|
412
|
+
if (!plan) return null
|
|
413
|
+
|
|
414
|
+
plan.status = PLAN_STATUS.COMPLETED
|
|
415
|
+
plan.completedAt = new Date().toISOString()
|
|
416
|
+
|
|
417
|
+
const summary = {
|
|
418
|
+
planId: plan.id,
|
|
419
|
+
command: plan.command,
|
|
420
|
+
totalSteps: plan.steps.length,
|
|
421
|
+
completedSteps: plan.steps.filter(s => s.status === 'completed').length,
|
|
422
|
+
failedSteps: plan.steps.filter(s => s.status === 'failed').length,
|
|
423
|
+
duration: this._calculateDuration(plan.executionStartedAt, plan.completedAt)
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Clear active plan
|
|
427
|
+
this.activePlans.delete(projectId)
|
|
428
|
+
|
|
429
|
+
return summary
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Abort plan execution
|
|
434
|
+
* @param {string} projectId
|
|
435
|
+
* @param {string} reason
|
|
436
|
+
* @returns {Object}
|
|
437
|
+
*/
|
|
438
|
+
abortPlan(projectId, reason = 'User requested') {
|
|
439
|
+
const plan = this.getActivePlan(projectId)
|
|
440
|
+
if (!plan) return null
|
|
441
|
+
|
|
442
|
+
plan.status = PLAN_STATUS.ABORTED
|
|
443
|
+
plan.completedAt = new Date().toISOString()
|
|
444
|
+
plan.abortReason = reason
|
|
445
|
+
|
|
446
|
+
const summary = {
|
|
447
|
+
aborted: true,
|
|
448
|
+
planId: plan.id,
|
|
449
|
+
reason,
|
|
450
|
+
completedSteps: plan.steps.filter(s => s.status === 'completed').length,
|
|
451
|
+
totalSteps: plan.steps.length
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Clear active plan
|
|
455
|
+
this.activePlans.delete(projectId)
|
|
456
|
+
|
|
457
|
+
return summary
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Generate approval prompt for destructive commands
|
|
462
|
+
* @param {string} commandName
|
|
463
|
+
* @param {Object} context
|
|
464
|
+
* @returns {Object}
|
|
465
|
+
*/
|
|
466
|
+
generateApprovalPrompt(commandName, context) {
|
|
467
|
+
const prompts = {
|
|
468
|
+
ship: {
|
|
469
|
+
title: 'Ship Confirmation',
|
|
470
|
+
message: 'Ready to commit and push changes?',
|
|
471
|
+
details: [
|
|
472
|
+
`Branch: ${context.branch || 'current'}`,
|
|
473
|
+
`Files: ${context.changedFiles?.length || 0} changed`,
|
|
474
|
+
`Commit: "${context.commitMessage || 'No message'}"`
|
|
475
|
+
],
|
|
476
|
+
options: [
|
|
477
|
+
{ key: 'y', label: 'Yes, ship it', action: 'approve' },
|
|
478
|
+
{ key: 'n', label: 'No, cancel', action: 'reject' },
|
|
479
|
+
{ key: 'e', label: 'Edit message', action: 'edit' }
|
|
480
|
+
]
|
|
481
|
+
},
|
|
482
|
+
cleanup: {
|
|
483
|
+
title: 'Cleanup Confirmation',
|
|
484
|
+
message: 'This will delete files/code. Continue?',
|
|
485
|
+
details: [
|
|
486
|
+
`Files to delete: ${context.filesToDelete?.length || 0}`,
|
|
487
|
+
`Code to remove: ${context.linesOfCode || 0} lines`
|
|
488
|
+
],
|
|
489
|
+
options: [
|
|
490
|
+
{ key: 'y', label: 'Yes, cleanup', action: 'approve' },
|
|
491
|
+
{ key: 'n', label: 'No, cancel', action: 'reject' },
|
|
492
|
+
{ key: 'l', label: 'List files first', action: 'list' }
|
|
493
|
+
]
|
|
494
|
+
},
|
|
495
|
+
git: {
|
|
496
|
+
title: 'Git Operation Confirmation',
|
|
497
|
+
message: `Execute: ${context.operation || 'git operation'}?`,
|
|
498
|
+
details: context.warnings || [],
|
|
499
|
+
options: [
|
|
500
|
+
{ key: 'y', label: 'Yes, execute', action: 'approve' },
|
|
501
|
+
{ key: 'n', label: 'No, cancel', action: 'reject' }
|
|
502
|
+
]
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
return prompts[commandName] || {
|
|
507
|
+
title: 'Confirmation Required',
|
|
508
|
+
message: `Execute ${commandName}?`,
|
|
509
|
+
options: [
|
|
510
|
+
{ key: 'y', label: 'Yes', action: 'approve' },
|
|
511
|
+
{ key: 'n', label: 'No', action: 'reject' }
|
|
512
|
+
]
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Format plan status for display
|
|
518
|
+
* @param {string} projectId
|
|
519
|
+
* @returns {string}
|
|
520
|
+
*/
|
|
521
|
+
formatStatus(projectId) {
|
|
522
|
+
const plan = this.getActivePlan(projectId)
|
|
523
|
+
if (!plan) return 'No active plan'
|
|
524
|
+
|
|
525
|
+
const statusEmoji = {
|
|
526
|
+
[PLAN_STATUS.GATHERING]: '🔍',
|
|
527
|
+
[PLAN_STATUS.ANALYZING]: '🧠',
|
|
528
|
+
[PLAN_STATUS.PROPOSING]: '📝',
|
|
529
|
+
[PLAN_STATUS.PENDING_APPROVAL]: '⏳',
|
|
530
|
+
[PLAN_STATUS.APPROVED]: '✅',
|
|
531
|
+
[PLAN_STATUS.EXECUTING]: '⚡',
|
|
532
|
+
[PLAN_STATUS.COMPLETED]: '🎉',
|
|
533
|
+
[PLAN_STATUS.REJECTED]: '❌',
|
|
534
|
+
[PLAN_STATUS.ABORTED]: '🛑'
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const lines = [
|
|
538
|
+
`${statusEmoji[plan.status] || '📋'} Plan: ${plan.command}`,
|
|
539
|
+
`Status: ${plan.status}`
|
|
540
|
+
]
|
|
541
|
+
|
|
542
|
+
if (plan.status === PLAN_STATUS.EXECUTING) {
|
|
543
|
+
const progress = Math.round((plan.currentStep / plan.steps.length) * 100)
|
|
544
|
+
lines.push(`Progress: ${plan.currentStep}/${plan.steps.length} (${progress}%)`)
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
return lines.join('\n')
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Calculate duration between two timestamps
|
|
552
|
+
* @private
|
|
553
|
+
*/
|
|
554
|
+
_calculateDuration(start, end) {
|
|
555
|
+
if (!start || !end) return null
|
|
556
|
+
const ms = new Date(end) - new Date(start)
|
|
557
|
+
const seconds = Math.floor(ms / 1000)
|
|
558
|
+
const minutes = Math.floor(seconds / 60)
|
|
559
|
+
const hours = Math.floor(minutes / 60)
|
|
560
|
+
|
|
561
|
+
if (hours > 0) return `${hours}h ${minutes % 60}m`
|
|
562
|
+
if (minutes > 0) return `${minutes}m ${seconds % 60}s`
|
|
563
|
+
return `${seconds}s`
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Export singleton and constants
|
|
568
|
+
module.exports = new PlanMode()
|
|
569
|
+
module.exports.PLAN_STATUS = PLAN_STATUS
|
|
570
|
+
module.exports.PLAN_REQUIRED_COMMANDS = PLAN_REQUIRED_COMMANDS
|
|
571
|
+
module.exports.DESTRUCTIVE_COMMANDS = DESTRUCTIVE_COMMANDS
|
|
572
|
+
module.exports.PLANNING_TOOLS = PLANNING_TOOLS
|