prjct-cli 0.10.0 → 0.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/CHANGELOG.md +31 -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 +43 -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
@@ -0,0 +1,578 @@
1
+ /**
2
+ * Chain of Thought Layer
3
+ *
4
+ * Adds internal reasoning for critical commands before execution.
5
+ * Inspired by Devin's <think> blocks pattern.
6
+ *
7
+ * OPTIMIZATION (P2.2): Chain of Thought
8
+ * - Internal reasoning for critical commands
9
+ * - Visible reasoning in debug mode
10
+ * - Ground truth verification before action
11
+ *
12
+ * Source: Devin pattern
13
+ */
14
+
15
+ const memorySystem = require('./memory-system')
16
+
17
+ class ChainOfThought {
18
+ constructor() {
19
+ // Commands that require reasoning
20
+ this.criticalCommands = ['ship', 'feature', 'analyze', 'sync', 'cleanup', 'init', 'spec']
21
+
22
+ // Debug mode for visible reasoning
23
+ this.debugMode = process.env.PRJCT_DEBUG === 'true'
24
+ }
25
+
26
+ /**
27
+ * Check if command requires chain of thought
28
+ * @param {string} commandName
29
+ * @returns {boolean}
30
+ */
31
+ requiresReasoning(commandName) {
32
+ return this.criticalCommands.includes(commandName)
33
+ }
34
+
35
+ /**
36
+ * Generate reasoning chain for a command
37
+ * @param {string} commandName
38
+ * @param {Object} context
39
+ * @param {Object} state
40
+ * @returns {Promise<Object>} Reasoning result
41
+ */
42
+ async reason(commandName, context, state) {
43
+ if (!this.requiresReasoning(commandName)) {
44
+ return { skip: true, reason: 'Non-critical command' }
45
+ }
46
+
47
+ const reasoner = this._getReasonerForCommand(commandName)
48
+ if (!reasoner) {
49
+ return { skip: true, reason: 'No reasoner defined' }
50
+ }
51
+
52
+ const startTime = Date.now()
53
+ const reasoning = await reasoner.call(this, context, state)
54
+
55
+ return {
56
+ command: commandName,
57
+ reasoning,
58
+ duration: Date.now() - startTime,
59
+ timestamp: new Date().toISOString()
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Get the appropriate reasoner for a command
65
+ * @private
66
+ */
67
+ _getReasonerForCommand(commandName) {
68
+ const reasoners = {
69
+ ship: this._reasonShip,
70
+ feature: this._reasonFeature,
71
+ analyze: this._reasonAnalyze,
72
+ sync: this._reasonSync,
73
+ cleanup: this._reasonCleanup,
74
+ init: this._reasonInit,
75
+ spec: this._reasonSpec
76
+ }
77
+ return reasoners[commandName]
78
+ }
79
+
80
+ /**
81
+ * Reasoning for /p:ship
82
+ * @private
83
+ */
84
+ async _reasonShip(context, state) {
85
+ const steps = []
86
+
87
+ // Step 1: Verify current task
88
+ steps.push({
89
+ step: 'verify_task',
90
+ question: 'Is there an active task to ship?',
91
+ check: state.now && state.now.trim() !== '',
92
+ result: state.now ? `Active: "${this._extractTaskName(state.now)}"` : 'No active task',
93
+ pass: !!state.now && state.now.trim() !== ''
94
+ })
95
+
96
+ // Step 2: Check shipped history format
97
+ steps.push({
98
+ step: 'check_format',
99
+ question: 'What format does shipped.md use?',
100
+ check: true,
101
+ result: state.shipped ? 'Found existing format' : 'New file, will use default',
102
+ pass: true
103
+ })
104
+
105
+ // Step 3: Check version
106
+ steps.push({
107
+ step: 'check_version',
108
+ question: 'What version should we use?',
109
+ check: true,
110
+ result: context.version || 'Will read from package.json',
111
+ pass: true
112
+ })
113
+
114
+ // Step 4: Check for memory patterns
115
+ const commitPattern = await memorySystem.getSmartDecision(
116
+ context.projectId,
117
+ 'commit_footer'
118
+ )
119
+ steps.push({
120
+ step: 'check_memory',
121
+ question: 'Is there a learned commit pattern?',
122
+ check: !!commitPattern,
123
+ result: commitPattern ? `Pattern: "${commitPattern}"` : 'Will use default prjct footer',
124
+ pass: true
125
+ })
126
+
127
+ // Step 5: Propose plan
128
+ const plan = this._generateShipPlan(steps, context)
129
+
130
+ return {
131
+ steps,
132
+ allPassed: steps.every(s => s.pass),
133
+ plan,
134
+ confidence: this._calculateConfidence(steps)
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Reasoning for /p:feature
140
+ * @private
141
+ */
142
+ async _reasonFeature(context, state) {
143
+ const steps = []
144
+
145
+ // Step 1: Check if feature description provided
146
+ const featureDesc = context.params?.description || context.params?.feature
147
+ steps.push({
148
+ step: 'check_description',
149
+ question: 'Is feature description provided?',
150
+ check: !!featureDesc,
151
+ result: featureDesc ? `Feature: "${featureDesc}"` : 'No description - will show template',
152
+ pass: true // Both modes are valid
153
+ })
154
+
155
+ // Step 2: Check roadmap state
156
+ steps.push({
157
+ step: 'check_roadmap',
158
+ question: 'What is current roadmap state?',
159
+ check: true,
160
+ result: state.roadmap ? 'Roadmap exists' : 'No roadmap yet',
161
+ pass: true
162
+ })
163
+
164
+ // Step 3: Check queue capacity
165
+ const queueSize = this._countQueueItems(state.next)
166
+ steps.push({
167
+ step: 'check_queue',
168
+ question: 'Is there room in the queue?',
169
+ check: queueSize < 100,
170
+ result: `Queue: ${queueSize}/100 tasks`,
171
+ pass: queueSize < 100
172
+ })
173
+
174
+ // Step 4: Analyze impact/effort if description given
175
+ if (featureDesc) {
176
+ const analysis = this._analyzeFeature(featureDesc)
177
+ steps.push({
178
+ step: 'analyze_feature',
179
+ question: 'What is estimated impact/effort?',
180
+ check: true,
181
+ result: `Impact: ${analysis.impact}, Effort: ${analysis.effort}`,
182
+ pass: true
183
+ })
184
+ }
185
+
186
+ return {
187
+ steps,
188
+ allPassed: steps.every(s => s.pass),
189
+ plan: this._generateFeaturePlan(steps, featureDesc),
190
+ confidence: this._calculateConfidence(steps)
191
+ }
192
+ }
193
+
194
+ /**
195
+ * Reasoning for /p:analyze
196
+ * @private
197
+ */
198
+ async _reasonAnalyze(context, state) {
199
+ const steps = []
200
+
201
+ // Step 1: Check if analysis exists
202
+ steps.push({
203
+ step: 'check_existing',
204
+ question: 'Does analysis already exist?',
205
+ check: true,
206
+ result: state.analysis ? 'Existing analysis found' : 'No previous analysis',
207
+ pass: true
208
+ })
209
+
210
+ // Step 2: Check project structure
211
+ steps.push({
212
+ step: 'check_structure',
213
+ question: 'Is project structure valid?',
214
+ check: true,
215
+ result: context.projectPath ? 'Valid project path' : 'No project path',
216
+ pass: !!context.projectPath
217
+ })
218
+
219
+ // Step 3: Check agents directory
220
+ steps.push({
221
+ step: 'check_agents',
222
+ question: 'Are agents generated?',
223
+ check: true,
224
+ result: 'Will regenerate based on analysis',
225
+ pass: true
226
+ })
227
+
228
+ return {
229
+ steps,
230
+ allPassed: steps.every(s => s.pass),
231
+ plan: ['Analyze codebase', 'Detect technologies', 'Generate specialized agents', 'Update context'],
232
+ confidence: this._calculateConfidence(steps)
233
+ }
234
+ }
235
+
236
+ /**
237
+ * Reasoning for /p:sync
238
+ * @private
239
+ */
240
+ async _reasonSync(_context, state) {
241
+ const steps = []
242
+
243
+ // Step 1: Check current state
244
+ steps.push({
245
+ step: 'check_state',
246
+ question: 'What needs syncing?',
247
+ check: true,
248
+ result: 'Will sync agents, context, and metrics',
249
+ pass: true
250
+ })
251
+
252
+ // Step 2: Check for stale data
253
+ const isStale = !state.analysis || this._isDataStale(state)
254
+ steps.push({
255
+ step: 'check_stale',
256
+ question: 'Is project data stale?',
257
+ check: isStale,
258
+ result: isStale ? 'Data is stale, will refresh' : 'Data is current',
259
+ pass: true
260
+ })
261
+
262
+ return {
263
+ steps,
264
+ allPassed: steps.every(s => s.pass),
265
+ plan: ['Refresh context', 'Update agents', 'Sync metrics'],
266
+ confidence: this._calculateConfidence(steps)
267
+ }
268
+ }
269
+
270
+ /**
271
+ * Reasoning for /p:cleanup
272
+ * @private
273
+ */
274
+ async _reasonCleanup(context, _state) {
275
+ const steps = []
276
+
277
+ // Step 1: Identify cleanup targets
278
+ const cleanupType = context.params?.type || 'all'
279
+ steps.push({
280
+ step: 'identify_targets',
281
+ question: 'What should be cleaned up?',
282
+ check: true,
283
+ result: `Cleanup type: ${cleanupType}`,
284
+ pass: true
285
+ })
286
+
287
+ // Step 2: Check for safe cleanup
288
+ steps.push({
289
+ step: 'safety_check',
290
+ question: 'Is cleanup safe to proceed?',
291
+ check: true,
292
+ result: 'Will only remove temp files and stale entries',
293
+ pass: true
294
+ })
295
+
296
+ return {
297
+ steps,
298
+ allPassed: steps.every(s => s.pass),
299
+ plan: ['Identify stale files', 'Archive old entries', 'Clean temp data'],
300
+ confidence: this._calculateConfidence(steps)
301
+ }
302
+ }
303
+
304
+ /**
305
+ * Reasoning for /p:init
306
+ * @private
307
+ */
308
+ async _reasonInit(context, _state) {
309
+ const steps = []
310
+
311
+ // Step 1: Check if already initialized
312
+ steps.push({
313
+ step: 'check_existing',
314
+ question: 'Is project already initialized?',
315
+ check: true,
316
+ result: context.projectId ? 'Already initialized' : 'Not initialized',
317
+ pass: true
318
+ })
319
+
320
+ // Step 2: Check for idea/description
321
+ const idea = context.params?.idea || context.params?.description
322
+ steps.push({
323
+ step: 'check_idea',
324
+ question: 'Is there a project idea?',
325
+ check: !!idea,
326
+ result: idea ? `Idea: "${idea}"` : 'No idea - will ask or analyze existing code',
327
+ pass: true
328
+ })
329
+
330
+ // Step 3: Determine mode
331
+ const mode = idea ? 'architect' : 'standard'
332
+ steps.push({
333
+ step: 'determine_mode',
334
+ question: 'Which initialization mode?',
335
+ check: true,
336
+ result: `Mode: ${mode}`,
337
+ pass: true
338
+ })
339
+
340
+ return {
341
+ steps,
342
+ allPassed: steps.every(s => s.pass),
343
+ plan: mode === 'architect'
344
+ ? ['Enter architect mode', 'Ask discovery questions', 'Generate plan', 'Create structure']
345
+ : ['Create config', 'Initialize storage', 'Analyze existing code'],
346
+ confidence: this._calculateConfidence(steps)
347
+ }
348
+ }
349
+
350
+ /**
351
+ * Reasoning for /p:spec
352
+ * @private
353
+ */
354
+ async _reasonSpec(context, state) {
355
+ const steps = []
356
+
357
+ // Step 1: Check if feature name provided
358
+ const featureName = context.params?.feature || context.params?.name || context.params?.description
359
+ steps.push({
360
+ step: 'check_feature',
361
+ question: 'Is feature name provided?',
362
+ check: !!featureName,
363
+ result: featureName ? `Feature: "${featureName}"` : 'No feature - will show template',
364
+ pass: true // Both modes valid
365
+ })
366
+
367
+ // Step 2: Check for existing spec
368
+ if (featureName) {
369
+ const slug = featureName.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '')
370
+ const specExists = state.specs && state.specs.includes(slug)
371
+ steps.push({
372
+ step: 'check_existing',
373
+ question: 'Does spec already exist?',
374
+ check: !specExists,
375
+ result: specExists ? `Spec "${slug}.md" exists - will update` : 'New spec',
376
+ pass: true
377
+ })
378
+ }
379
+
380
+ // Step 3: Check queue capacity
381
+ const queueSize = this._countQueueItems(state.next)
382
+ steps.push({
383
+ step: 'check_queue',
384
+ question: 'Is there room for new tasks?',
385
+ check: queueSize < 90,
386
+ result: `Queue: ${queueSize}/100`,
387
+ pass: queueSize < 90
388
+ })
389
+
390
+ // Step 4: Check complexity
391
+ if (featureName) {
392
+ const isComplex = this._isComplexFeature(featureName)
393
+ steps.push({
394
+ step: 'assess_complexity',
395
+ question: 'Is this a complex feature?',
396
+ check: true,
397
+ result: isComplex ? 'Complex - spec recommended' : 'Simple - consider /p:feature',
398
+ pass: true
399
+ })
400
+ }
401
+
402
+ return {
403
+ steps,
404
+ allPassed: steps.every(s => s.pass),
405
+ plan: featureName
406
+ ? ['Analyze requirements', 'Propose design', 'Break into tasks', 'Request approval', 'Add to queue']
407
+ : ['Show spec template', 'Guide through requirements'],
408
+ confidence: this._calculateConfidence(steps)
409
+ }
410
+ }
411
+
412
+ /**
413
+ * Check if feature sounds complex
414
+ * @private
415
+ */
416
+ _isComplexFeature(name) {
417
+ const complexKeywords = [
418
+ 'authentication', 'auth', 'payment', 'integration', 'migration',
419
+ 'refactor', 'architecture', 'database', 'api', 'system',
420
+ 'security', 'performance', 'scale', 'redesign'
421
+ ]
422
+ const nameLower = name.toLowerCase()
423
+ return complexKeywords.some(kw => nameLower.includes(kw))
424
+ }
425
+
426
+ /**
427
+ * Format reasoning for output
428
+ * @param {Object} reasoning
429
+ * @returns {string}
430
+ */
431
+ formatReasoning(reasoning) {
432
+ if (reasoning.skip) {
433
+ return ''
434
+ }
435
+
436
+ const lines = ['<think>']
437
+
438
+ reasoning.reasoning.steps.forEach(step => {
439
+ const icon = step.pass ? '✓' : '✗'
440
+ lines.push(`${icon} ${step.question}`)
441
+ lines.push(` → ${step.result}`)
442
+ })
443
+
444
+ if (reasoning.reasoning.plan) {
445
+ lines.push('')
446
+ lines.push('PLAN:')
447
+ reasoning.reasoning.plan.forEach((step, i) => {
448
+ lines.push(`${i + 1}. ${step}`)
449
+ })
450
+ }
451
+
452
+ lines.push(`</think>`)
453
+ lines.push(`Confidence: ${Math.round(reasoning.reasoning.confidence * 100)}%`)
454
+
455
+ return lines.join('\n')
456
+ }
457
+
458
+ /**
459
+ * Format for user-visible output (non-debug)
460
+ * @param {Object} reasoning
461
+ * @returns {string}
462
+ */
463
+ formatPlan(reasoning) {
464
+ if (reasoning.skip || !reasoning.reasoning?.plan) {
465
+ return ''
466
+ }
467
+
468
+ const lines = ['PLAN:']
469
+ reasoning.reasoning.plan.forEach((step, i) => {
470
+ lines.push(`${i + 1}. ${step}`)
471
+ })
472
+
473
+ if (!reasoning.reasoning.allPassed) {
474
+ const failed = reasoning.reasoning.steps.filter(s => !s.pass)
475
+ lines.push('')
476
+ lines.push('⚠️ Issues:')
477
+ failed.forEach(s => {
478
+ lines.push(` - ${s.result}`)
479
+ })
480
+ }
481
+
482
+ return lines.join('\n')
483
+ }
484
+
485
+ // Helper methods
486
+
487
+ _extractTaskName(nowContent) {
488
+ if (!nowContent) return ''
489
+ const lines = nowContent.split('\n')
490
+ for (const line of lines) {
491
+ if (line.startsWith('**') && line.includes('**')) {
492
+ return line.replace(/\*\*/g, '').trim()
493
+ }
494
+ if (line.startsWith('# ')) {
495
+ return line.replace('# ', '').trim()
496
+ }
497
+ }
498
+ return nowContent.substring(0, 50).trim()
499
+ }
500
+
501
+ _countQueueItems(nextContent) {
502
+ if (!nextContent) return 0
503
+ const matches = nextContent.match(/- \[[ x]\]/g)
504
+ return matches ? matches.length : 0
505
+ }
506
+
507
+ _analyzeFeature(description) {
508
+ const desc = description.toLowerCase()
509
+
510
+ // Simple heuristics for impact/effort
511
+ let impact = 'medium'
512
+ let effort = 'medium'
513
+
514
+ if (desc.includes('critical') || desc.includes('urgent') || desc.includes('security')) {
515
+ impact = 'high'
516
+ } else if (desc.includes('minor') || desc.includes('small') || desc.includes('typo')) {
517
+ impact = 'low'
518
+ }
519
+
520
+ if (desc.includes('refactor') || desc.includes('rewrite') || desc.includes('migrate')) {
521
+ effort = 'high'
522
+ } else if (desc.includes('fix') || desc.includes('update') || desc.includes('add')) {
523
+ effort = 'low'
524
+ }
525
+
526
+ return { impact, effort }
527
+ }
528
+
529
+ _isDataStale(state) {
530
+ // Consider data stale if analysis is more than 24 hours old
531
+ // This is a placeholder - actual implementation would check timestamps
532
+ return !state.analysis
533
+ }
534
+
535
+ _generateShipPlan(steps, context) {
536
+ const plan = []
537
+
538
+ if (steps.find(s => s.step === 'verify_task')?.pass) {
539
+ plan.push('Mark task complete')
540
+ }
541
+
542
+ plan.push('Update shipped.md')
543
+ plan.push('Update metrics')
544
+
545
+ if (context.params?.commit !== false) {
546
+ plan.push('Create commit')
547
+ }
548
+
549
+ if (context.params?.push !== false) {
550
+ plan.push('Push to remote')
551
+ }
552
+
553
+ return plan
554
+ }
555
+
556
+ _generateFeaturePlan(_steps, featureDesc) {
557
+ if (!featureDesc) {
558
+ return ['Show feature template', 'Wait for user input']
559
+ }
560
+
561
+ return [
562
+ 'Analyze feature value',
563
+ 'Estimate effort',
564
+ 'Break into tasks',
565
+ 'Add to roadmap',
566
+ 'Start first task'
567
+ ]
568
+ }
569
+
570
+ _calculateConfidence(steps) {
571
+ if (steps.length === 0) return 1.0
572
+
573
+ const passed = steps.filter(s => s.pass).length
574
+ return passed / steps.length
575
+ }
576
+ }
577
+
578
+ module.exports = new ChainOfThought()