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
@@ -0,0 +1,591 @@
1
+ /**
2
+ * Ground Truth Verification
3
+ * Verifies actual state before critical operations
4
+ *
5
+ * OPTIMIZATION (P1.3): Anti-Hallucination Pattern
6
+ * - Reads actual files before assuming state
7
+ * - Compares expected vs actual state
8
+ * - Provides specific warnings for mismatches
9
+ * - Logs verification results for debugging
10
+ *
11
+ * Source: Devin, Cursor, Augment Code patterns
12
+ */
13
+
14
+ const fs = require('fs').promises
15
+ const path = require('path')
16
+ const contextBuilder = require('./context-builder')
17
+
18
+ /**
19
+ * Ground truth verification result
20
+ * @typedef {Object} VerificationResult
21
+ * @property {boolean} verified - Whether state matches expectations
22
+ * @property {Object} actual - Actual state from files
23
+ * @property {Object} expected - Expected state (if provided)
24
+ * @property {string[]} warnings - Warnings about state mismatches
25
+ * @property {string[]} recommendations - Suggested actions
26
+ */
27
+
28
+ /**
29
+ * Command-specific ground truth verifiers
30
+ */
31
+ const verifiers = {
32
+ /**
33
+ * /p:done - Verify task is actually complete-able
34
+ */
35
+ async done(context, state) {
36
+ const warnings = []
37
+ const recommendations = []
38
+ const actual = {}
39
+
40
+ // 1. Verify now.md exists and has real content
41
+ const nowPath = context.paths.now
42
+ try {
43
+ const nowContent = await fs.readFile(nowPath, 'utf-8')
44
+ actual.nowExists = true
45
+ actual.nowContent = nowContent.trim()
46
+ actual.nowLength = nowContent.length
47
+
48
+ // Check for placeholder content
49
+ if (nowContent.includes('No current task') ||
50
+ nowContent.match(/^#\s*NOW\s*$/m)) {
51
+ warnings.push('now.md appears to be empty or placeholder')
52
+ recommendations.push('Start a task first with /p:now "task"')
53
+ }
54
+
55
+ // Check for task metadata (started time)
56
+ const startedMatch = nowContent.match(/Started:\s*(.+)/i)
57
+ if (startedMatch) {
58
+ actual.startedAt = startedMatch[1]
59
+ // Calculate duration
60
+ const startTime = new Date(startedMatch[1])
61
+ if (!isNaN(startTime.getTime())) {
62
+ actual.durationMs = Date.now() - startTime.getTime()
63
+ actual.durationFormatted = formatDuration(actual.durationMs)
64
+ }
65
+ }
66
+ } catch (error) {
67
+ actual.nowExists = false
68
+ warnings.push('now.md does not exist')
69
+ recommendations.push('Create a task with /p:now "task"')
70
+ }
71
+
72
+ // 2. Verify next.md for auto-start
73
+ const nextPath = context.paths.next
74
+ try {
75
+ const nextContent = await fs.readFile(nextPath, 'utf-8')
76
+ actual.nextExists = true
77
+ const tasks = nextContent.match(/- \[ \]/g) || []
78
+ actual.pendingTasks = tasks.length
79
+ } catch {
80
+ actual.nextExists = false
81
+ actual.pendingTasks = 0
82
+ }
83
+
84
+ // 3. Verify metrics.md is writable
85
+ const metricsPath = context.paths.metrics
86
+ try {
87
+ await fs.access(path.dirname(metricsPath), fs.constants.W_OK)
88
+ actual.metricsWritable = true
89
+ } catch {
90
+ actual.metricsWritable = false
91
+ warnings.push('Cannot write to metrics directory')
92
+ }
93
+
94
+ return {
95
+ verified: warnings.length === 0,
96
+ actual,
97
+ warnings,
98
+ recommendations
99
+ }
100
+ },
101
+
102
+ /**
103
+ * /p:ship - Verify feature is ready to ship
104
+ */
105
+ async ship(context, state) {
106
+ const warnings = []
107
+ const recommendations = []
108
+ const actual = {}
109
+
110
+ // 1. Check for uncommitted changes
111
+ try {
112
+ const { execSync } = require('child_process')
113
+ const gitStatus = execSync('git status --porcelain', {
114
+ cwd: context.projectPath,
115
+ encoding: 'utf-8'
116
+ })
117
+ actual.hasUncommittedChanges = gitStatus.trim().length > 0
118
+ actual.uncommittedFiles = gitStatus.trim().split('\n').filter(Boolean).length
119
+
120
+ if (actual.hasUncommittedChanges) {
121
+ warnings.push(`${actual.uncommittedFiles} uncommitted file(s)`)
122
+ recommendations.push('Commit changes before shipping')
123
+ }
124
+ } catch {
125
+ actual.gitAvailable = false
126
+ // Not a git repo or git not available - not a blocker
127
+ }
128
+
129
+ // 2. Check for package.json version (if exists)
130
+ const pkgPath = path.join(context.projectPath, 'package.json')
131
+ try {
132
+ const pkgContent = await fs.readFile(pkgPath, 'utf-8')
133
+ const pkg = JSON.parse(pkgContent)
134
+ actual.currentVersion = pkg.version
135
+ actual.hasPackageJson = true
136
+ } catch {
137
+ actual.hasPackageJson = false
138
+ }
139
+
140
+ // 3. Check shipped.md for duplicate feature names
141
+ const shippedPath = context.paths.shipped
142
+ try {
143
+ const shippedContent = await fs.readFile(shippedPath, 'utf-8')
144
+ actual.shippedExists = true
145
+
146
+ // Check if feature name already shipped today
147
+ const featureName = context.params.feature || context.params.description
148
+ if (featureName) {
149
+ const today = new Date().toISOString().split('T')[0]
150
+ const todayPattern = new RegExp(`${today}.*${escapeRegex(featureName)}`, 'i')
151
+ if (todayPattern.test(shippedContent)) {
152
+ warnings.push(`Feature "${featureName}" already shipped today`)
153
+ recommendations.push('Use a different feature name or skip /p:ship')
154
+ }
155
+ }
156
+ } catch {
157
+ actual.shippedExists = false
158
+ }
159
+
160
+ // 4. Check for test failures (if test script exists)
161
+ if (actual.hasPackageJson) {
162
+ try {
163
+ const pkgContent = await fs.readFile(pkgPath, 'utf-8')
164
+ const pkg = JSON.parse(pkgContent)
165
+ actual.hasTestScript = !!pkg.scripts?.test
166
+ // Note: We don't run tests here, just check if they exist
167
+ // Running tests is the user's responsibility
168
+ } catch {
169
+ actual.hasTestScript = false
170
+ }
171
+ }
172
+
173
+ return {
174
+ verified: warnings.length === 0,
175
+ actual,
176
+ warnings,
177
+ recommendations
178
+ }
179
+ },
180
+
181
+ /**
182
+ * /p:feature - Verify feature can be added
183
+ */
184
+ async feature(context, state) {
185
+ const warnings = []
186
+ const recommendations = []
187
+ const actual = {}
188
+
189
+ // 1. Check next.md capacity
190
+ const nextPath = context.paths.next
191
+ try {
192
+ const nextContent = await fs.readFile(nextPath, 'utf-8')
193
+ actual.nextExists = true
194
+ const tasks = nextContent.match(/- \[[ x]\]/g) || []
195
+ actual.taskCount = tasks.length
196
+ actual.pendingTasks = (nextContent.match(/- \[ \]/g) || []).length
197
+
198
+ if (actual.taskCount >= 90) {
199
+ warnings.push(`Queue nearly full (${actual.taskCount}/100 tasks)`)
200
+ recommendations.push('Complete some tasks before adding more')
201
+ }
202
+ } catch {
203
+ actual.nextExists = false
204
+ actual.taskCount = 0
205
+ }
206
+
207
+ // 2. Check roadmap.md for duplicate features
208
+ const roadmapPath = context.paths.roadmap
209
+ try {
210
+ const roadmapContent = await fs.readFile(roadmapPath, 'utf-8')
211
+ actual.roadmapExists = true
212
+
213
+ const featureName = context.params.description || context.params.feature
214
+ if (featureName) {
215
+ const featurePattern = new RegExp(escapeRegex(featureName), 'i')
216
+ if (featurePattern.test(roadmapContent)) {
217
+ warnings.push(`Feature "${featureName}" may already exist in roadmap`)
218
+ recommendations.push('Check roadmap for duplicates with /p:roadmap')
219
+ }
220
+ }
221
+ } catch {
222
+ actual.roadmapExists = false
223
+ }
224
+
225
+ // 3. Check if there's an active task (should complete first?)
226
+ const nowPath = context.paths.now
227
+ try {
228
+ const nowContent = await fs.readFile(nowPath, 'utf-8')
229
+ actual.hasActiveTask = nowContent.trim().length > 0 &&
230
+ !nowContent.includes('No current task')
231
+
232
+ if (actual.hasActiveTask) {
233
+ recommendations.push('Consider completing current task first with /p:done')
234
+ }
235
+ } catch {
236
+ actual.hasActiveTask = false
237
+ }
238
+
239
+ return {
240
+ verified: warnings.length === 0,
241
+ actual,
242
+ warnings,
243
+ recommendations
244
+ }
245
+ },
246
+
247
+ /**
248
+ * /p:now - Verify task can be set (anti-hallucination: warn if replacing)
249
+ */
250
+ async now(context, state) {
251
+ const warnings = []
252
+ const recommendations = []
253
+ const actual = {}
254
+
255
+ // 1. Check if there's already an active task
256
+ const nowPath = context.paths.now
257
+ try {
258
+ const nowContent = await fs.readFile(nowPath, 'utf-8')
259
+ actual.nowExists = true
260
+ actual.nowContent = nowContent.trim()
261
+
262
+ const hasRealTask = nowContent.trim().length > 0 &&
263
+ !nowContent.includes('No current task') &&
264
+ !nowContent.match(/^#\s*NOW\s*$/m)
265
+
266
+ actual.hasActiveTask = hasRealTask
267
+
268
+ // ANTI-HALLUCINATION: Warn if replacing existing task
269
+ if (hasRealTask && context.params.task) {
270
+ const taskPreview = nowContent.substring(0, 50).replace(/\n/g, ' ')
271
+ warnings.push(`Replacing existing task: "${taskPreview}..."`)
272
+ recommendations.push('Use /p:done first to track completion')
273
+ }
274
+ } catch {
275
+ actual.nowExists = false
276
+ actual.hasActiveTask = false
277
+ }
278
+
279
+ // 2. Check next.md for available tasks
280
+ const nextPath = context.paths.next
281
+ try {
282
+ const nextContent = await fs.readFile(nextPath, 'utf-8')
283
+ const pendingTasks = (nextContent.match(/- \[ \]/g) || []).length
284
+ actual.pendingTasks = pendingTasks
285
+
286
+ if (!context.params.task && pendingTasks > 0) {
287
+ recommendations.push(`${pendingTasks} tasks available in queue`)
288
+ }
289
+ } catch {
290
+ actual.pendingTasks = 0
291
+ }
292
+
293
+ return {
294
+ verified: warnings.length === 0,
295
+ actual,
296
+ warnings,
297
+ recommendations
298
+ }
299
+ },
300
+
301
+ /**
302
+ * /p:init - Verify project can be initialized
303
+ */
304
+ async init(context, state) {
305
+ const warnings = []
306
+ const recommendations = []
307
+ const actual = {}
308
+
309
+ // 1. Check if already initialized
310
+ const configPath = path.join(context.projectPath, '.prjct/prjct.config.json')
311
+ try {
312
+ const configContent = await fs.readFile(configPath, 'utf-8')
313
+ actual.alreadyInitialized = true
314
+ actual.existingConfig = JSON.parse(configContent)
315
+ warnings.push('Project already initialized')
316
+ recommendations.push('Use /p:analyze to refresh analysis or delete .prjct/ to reinitialize')
317
+ } catch {
318
+ actual.alreadyInitialized = false
319
+ }
320
+
321
+ // 2. Check if global storage path is writable
322
+ const globalPath = path.join(require('os').homedir(), '.prjct-cli')
323
+ try {
324
+ await fs.access(globalPath, fs.constants.W_OK)
325
+ actual.globalPathWritable = true
326
+ } catch {
327
+ try {
328
+ // Try to create it
329
+ await fs.mkdir(globalPath, { recursive: true })
330
+ actual.globalPathWritable = true
331
+ actual.globalPathCreated = true
332
+ } catch {
333
+ actual.globalPathWritable = false
334
+ warnings.push('Cannot write to ~/.prjct-cli')
335
+ recommendations.push('Check directory permissions')
336
+ }
337
+ }
338
+
339
+ return {
340
+ verified: warnings.length === 0,
341
+ actual,
342
+ warnings,
343
+ recommendations
344
+ }
345
+ },
346
+
347
+ /**
348
+ * /p:sync - Verify sync can proceed
349
+ */
350
+ async sync(context, state) {
351
+ const warnings = []
352
+ const recommendations = []
353
+ const actual = {}
354
+
355
+ // 1. Check if project is initialized
356
+ const configPath = path.join(context.projectPath, '.prjct/prjct.config.json')
357
+ try {
358
+ const configContent = await fs.readFile(configPath, 'utf-8')
359
+ actual.hasConfig = true
360
+ actual.config = JSON.parse(configContent)
361
+ } catch {
362
+ actual.hasConfig = false
363
+ warnings.push('Project not initialized')
364
+ recommendations.push('Run /p:init first')
365
+ return { verified: false, actual, warnings, recommendations }
366
+ }
367
+
368
+ // 2. Check if global storage exists
369
+ const projectId = actual.config.projectId
370
+ const globalProjectPath = path.join(require('os').homedir(), '.prjct-cli/projects', projectId)
371
+ try {
372
+ await fs.access(globalProjectPath)
373
+ actual.globalStorageExists = true
374
+ } catch {
375
+ actual.globalStorageExists = false
376
+ warnings.push('Global storage missing')
377
+ recommendations.push('Run /p:init to recreate')
378
+ }
379
+
380
+ return {
381
+ verified: warnings.length === 0,
382
+ actual,
383
+ warnings,
384
+ recommendations
385
+ }
386
+ },
387
+
388
+ /**
389
+ * /p:analyze - Verify analysis can proceed
390
+ */
391
+ async analyze(context, state) {
392
+ const warnings = []
393
+ const recommendations = []
394
+ const actual = {}
395
+
396
+ // 1. Check if project has recognizable structure
397
+ const files = ['package.json', 'Cargo.toml', 'go.mod', 'requirements.txt', 'Gemfile', 'pom.xml']
398
+ actual.detectedFiles = []
399
+
400
+ for (const file of files) {
401
+ try {
402
+ await fs.access(path.join(context.projectPath, file))
403
+ actual.detectedFiles.push(file)
404
+ } catch {
405
+ // File doesn't exist
406
+ }
407
+ }
408
+
409
+ if (actual.detectedFiles.length === 0) {
410
+ warnings.push('No recognizable project files detected')
411
+ recommendations.push('Analysis may be limited without package.json or similar')
412
+ }
413
+
414
+ // 2. Check for source directories
415
+ const srcDirs = ['src', 'lib', 'app', 'core', 'components']
416
+ actual.detectedSrcDirs = []
417
+
418
+ for (const dir of srcDirs) {
419
+ try {
420
+ const stat = await fs.stat(path.join(context.projectPath, dir))
421
+ if (stat.isDirectory()) {
422
+ actual.detectedSrcDirs.push(dir)
423
+ }
424
+ } catch {
425
+ // Directory doesn't exist
426
+ }
427
+ }
428
+
429
+ return {
430
+ verified: true, // Analysis can always proceed, even with warnings
431
+ actual,
432
+ warnings,
433
+ recommendations
434
+ }
435
+ },
436
+
437
+ /**
438
+ * /p:spec - Verify spec can be created
439
+ */
440
+ async spec(context, state) {
441
+ const warnings = []
442
+ const recommendations = []
443
+ const actual = {}
444
+
445
+ // 1. Check specs directory exists
446
+ const specsPath = context.paths.specs
447
+ try {
448
+ await fs.access(specsPath)
449
+ actual.specsExists = true
450
+
451
+ // List existing specs
452
+ const files = await fs.readdir(specsPath)
453
+ actual.existingSpecs = files.filter(f => f.endsWith('.md'))
454
+ actual.specCount = actual.existingSpecs.length
455
+ } catch {
456
+ actual.specsExists = false
457
+ actual.specCount = 0
458
+ }
459
+
460
+ // 2. Check for duplicate spec name
461
+ const featureName = context.params.feature || context.params.name || context.params.description
462
+ if (featureName && actual.existingSpecs) {
463
+ const slug = featureName.toLowerCase().replace(/\s+/g, '-')
464
+ if (actual.existingSpecs.includes(`${slug}.md`)) {
465
+ warnings.push(`Spec "${featureName}" already exists`)
466
+ recommendations.push('Use a different name or edit existing spec')
467
+ }
468
+ }
469
+
470
+ return {
471
+ verified: warnings.length === 0,
472
+ actual,
473
+ warnings,
474
+ recommendations
475
+ }
476
+ }
477
+ }
478
+
479
+ /**
480
+ * Verify ground truth before command execution
481
+ *
482
+ * @param {string} commandName - Command to verify
483
+ * @param {Object} context - Built context from contextBuilder
484
+ * @param {Object} state - Current loaded state
485
+ * @returns {Promise<VerificationResult>}
486
+ */
487
+ async function verify(commandName, context, state) {
488
+ const verifier = verifiers[commandName]
489
+
490
+ if (!verifier) {
491
+ // No specific verification needed
492
+ return {
493
+ verified: true,
494
+ actual: {},
495
+ warnings: [],
496
+ recommendations: []
497
+ }
498
+ }
499
+
500
+ try {
501
+ return await verifier(context, state)
502
+ } catch (error) {
503
+ return {
504
+ verified: false,
505
+ actual: {},
506
+ warnings: [`Verification error: ${error.message}`],
507
+ recommendations: ['Check file permissions and project configuration']
508
+ }
509
+ }
510
+ }
511
+
512
+ /**
513
+ * Prepare command by verifying ground truth
514
+ * Returns enhanced context with verification results
515
+ *
516
+ * @param {string} commandName - Command name
517
+ * @param {Object} context - Built context
518
+ * @param {Object} state - Loaded state
519
+ * @returns {Promise<Object>} Enhanced context with groundTruth
520
+ */
521
+ async function prepareCommand(commandName, context, state) {
522
+ const verification = await verify(commandName, context, state)
523
+
524
+ return {
525
+ ...context,
526
+ groundTruth: {
527
+ ...verification,
528
+ verifiedAt: new Date().toISOString(),
529
+ command: commandName
530
+ }
531
+ }
532
+ }
533
+
534
+ /**
535
+ * Check if command requires ground truth verification
536
+ * @param {string} commandName - Command name
537
+ * @returns {boolean}
538
+ */
539
+ function requiresVerification(commandName) {
540
+ // ANTI-HALLUCINATION: Expanded verification for more commands
541
+ return ['done', 'ship', 'feature', 'spec', 'now', 'init', 'sync', 'analyze'].includes(commandName)
542
+ }
543
+
544
+ /**
545
+ * Format verification warnings for display
546
+ * @param {VerificationResult} result - Verification result
547
+ * @returns {string|null}
548
+ */
549
+ function formatWarnings(result) {
550
+ if (result.verified || result.warnings.length === 0) {
551
+ return null
552
+ }
553
+
554
+ let output = '⚠️ Ground Truth Warnings:\n'
555
+ result.warnings.forEach(w => {
556
+ output += ` • ${w}\n`
557
+ })
558
+
559
+ if (result.recommendations.length > 0) {
560
+ output += '\nRecommendations:\n'
561
+ result.recommendations.forEach(r => {
562
+ output += ` → ${r}\n`
563
+ })
564
+ }
565
+
566
+ return output
567
+ }
568
+
569
+ // Helpers
570
+
571
+ function formatDuration(ms) {
572
+ const hours = Math.floor(ms / (1000 * 60 * 60))
573
+ const minutes = Math.floor((ms % (1000 * 60 * 60)) / (1000 * 60))
574
+
575
+ if (hours > 0) {
576
+ return `${hours}h ${minutes}m`
577
+ }
578
+ return `${minutes}m`
579
+ }
580
+
581
+ function escapeRegex(string) {
582
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
583
+ }
584
+
585
+ module.exports = {
586
+ verify,
587
+ prepareCommand,
588
+ requiresVerification,
589
+ formatWarnings,
590
+ verifiers
591
+ }