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,380 @@
1
+ /**
2
+ * Validation Rules
3
+ * Explicit pre-flight checks for each command
4
+ * Returns SPECIFIC error messages, never generic failures
5
+ *
6
+ * OPTIMIZATION (P0.2): Anti-Hallucination Pattern
7
+ * - Ground truth verification before actions
8
+ * - Specific error messages for each failure mode
9
+ * - Actionable suggestions in every error
10
+ *
11
+ * Source: Claude Code, Devin, Augment Code patterns
12
+ */
13
+
14
+ const contextBuilder = require('./context-builder')
15
+
16
+ /**
17
+ * Validation result structure
18
+ * @typedef {Object} ValidationResult
19
+ * @property {boolean} valid - Whether validation passed
20
+ * @property {string|null} error - Specific error message if invalid
21
+ * @property {string|null} suggestion - Actionable next step
22
+ * @property {Object} state - Pre-loaded state for command execution
23
+ */
24
+
25
+ /**
26
+ * Command-specific validation rules
27
+ * Each rule returns { valid, error, suggestion, state }
28
+ */
29
+ const validationRules = {
30
+ /**
31
+ * /p:done - Complete current task
32
+ */
33
+ async done(context) {
34
+ const state = await contextBuilder.loadStateForCommand(context, 'done')
35
+
36
+ // Check 1: now.md exists and has content
37
+ if (!state.now || state.now.trim() === '') {
38
+ return {
39
+ valid: false,
40
+ error: 'No active task to complete',
41
+ suggestion: 'Start a task first with /p:now "task description"',
42
+ state
43
+ }
44
+ }
45
+
46
+ // Check 2: Task is not a placeholder/comment
47
+ const content = state.now.trim()
48
+ if (content.startsWith('#') && content.split('\n').length === 1) {
49
+ return {
50
+ valid: false,
51
+ error: 'now.md contains only a header, no task description',
52
+ suggestion: 'Add task details or start fresh with /p:now "task"',
53
+ state
54
+ }
55
+ }
56
+
57
+ // Check 3: Task is not blocked
58
+ if (content.toLowerCase().includes('blocked') ||
59
+ content.toLowerCase().includes('waiting for')) {
60
+ return {
61
+ valid: false,
62
+ error: 'Task appears to be blocked',
63
+ suggestion: 'Resolve the blocker first or use /p:pause to save progress',
64
+ state
65
+ }
66
+ }
67
+
68
+ return { valid: true, error: null, suggestion: null, state }
69
+ },
70
+
71
+ /**
72
+ * /p:ship - Ship a feature
73
+ */
74
+ async ship(context) {
75
+ const state = await contextBuilder.loadStateForCommand(context, 'ship')
76
+
77
+ // Check 1: Has something to ship (now.md or recent shipped items)
78
+ const hasCurrentTask = state.now && state.now.trim() !== ''
79
+ const hasRecentShips = state.shipped && state.shipped.trim() !== ''
80
+
81
+ if (!hasCurrentTask && !hasRecentShips) {
82
+ return {
83
+ valid: false,
84
+ error: 'Nothing to ship yet',
85
+ suggestion: 'Build something first with /p:now "feature name"',
86
+ state
87
+ }
88
+ }
89
+
90
+ // Check 2: Feature name provided (from params)
91
+ if (!context.params.feature && !context.params.description) {
92
+ // Try to extract from now.md
93
+ if (hasCurrentTask) {
94
+ // Auto-extract feature name - this is OK
95
+ return { valid: true, error: null, suggestion: null, state }
96
+ }
97
+ return {
98
+ valid: false,
99
+ error: 'No feature name specified',
100
+ suggestion: 'Specify what to ship: /p:ship "feature name"',
101
+ state
102
+ }
103
+ }
104
+
105
+ return { valid: true, error: null, suggestion: null, state }
106
+ },
107
+
108
+ /**
109
+ * /p:now - Set or show current task
110
+ */
111
+ async now(context) {
112
+ const state = await contextBuilder.loadStateForCommand(context, 'now')
113
+
114
+ // If no task param, this is a "show" request - always valid
115
+ if (!context.params.task && !context.params.description) {
116
+ return { valid: true, error: null, suggestion: null, state }
117
+ }
118
+
119
+ // Check: If setting new task, warn if one exists
120
+ if (state.now && state.now.trim() !== '') {
121
+ return {
122
+ valid: true, // Still valid, but with warning
123
+ error: null,
124
+ suggestion: `Note: Replacing current task. Use /p:done first to track completion.`,
125
+ state
126
+ }
127
+ }
128
+
129
+ return { valid: true, error: null, suggestion: null, state }
130
+ },
131
+
132
+ /**
133
+ * /p:next - Show priority queue
134
+ */
135
+ async next(context) {
136
+ const state = await contextBuilder.loadStateForCommand(context, 'next')
137
+
138
+ if (!state.next || state.next.trim() === '' ||
139
+ !state.next.includes('- [')) {
140
+ return {
141
+ valid: true, // Valid but empty
142
+ error: null,
143
+ suggestion: 'Queue is empty. Add tasks with /p:feature or /p:idea',
144
+ state
145
+ }
146
+ }
147
+
148
+ return { valid: true, error: null, suggestion: null, state }
149
+ },
150
+
151
+ /**
152
+ * /p:idea - Capture an idea
153
+ */
154
+ async idea(context) {
155
+ const state = await contextBuilder.loadStateForCommand(context, 'idea')
156
+
157
+ // Check: Idea text provided
158
+ if (!context.params.text && !context.params.description) {
159
+ return {
160
+ valid: false,
161
+ error: 'No idea text provided',
162
+ suggestion: 'Provide your idea: /p:idea "your idea here"',
163
+ state
164
+ }
165
+ }
166
+
167
+ return { valid: true, error: null, suggestion: null, state }
168
+ },
169
+
170
+ /**
171
+ * /p:feature - Add a new feature
172
+ */
173
+ async feature(context) {
174
+ const state = await contextBuilder.loadStateForCommand(context, 'feature')
175
+
176
+ // If no description, show interactive template - valid
177
+ if (!context.params.description && !context.params.feature) {
178
+ return { valid: true, error: null, suggestion: null, state }
179
+ }
180
+
181
+ return { valid: true, error: null, suggestion: null, state }
182
+ },
183
+
184
+ /**
185
+ * /p:pause - Pause current task
186
+ */
187
+ async pause(context) {
188
+ const state = await contextBuilder.loadStateForCommand(context, 'now')
189
+
190
+ if (!state.now || state.now.trim() === '') {
191
+ return {
192
+ valid: false,
193
+ error: 'No active task to pause',
194
+ suggestion: 'Start a task first with /p:now "task"',
195
+ state
196
+ }
197
+ }
198
+
199
+ return { valid: true, error: null, suggestion: null, state }
200
+ },
201
+
202
+ /**
203
+ * /p:resume - Resume paused task
204
+ */
205
+ async resume(context) {
206
+ const state = await contextBuilder.loadState(context, ['now'])
207
+
208
+ // Check if there's a paused state to resume
209
+ // This would need to check a paused.md or similar
210
+ return { valid: true, error: null, suggestion: null, state }
211
+ },
212
+
213
+ /**
214
+ * /p:recap - Show project overview
215
+ */
216
+ async recap(context) {
217
+ const state = await contextBuilder.loadStateForCommand(context, 'recap')
218
+ return { valid: true, error: null, suggestion: null, state }
219
+ },
220
+
221
+ /**
222
+ * /p:progress - Show progress metrics
223
+ */
224
+ async progress(context) {
225
+ const state = await contextBuilder.loadStateForCommand(context, 'progress')
226
+ return { valid: true, error: null, suggestion: null, state }
227
+ },
228
+
229
+ /**
230
+ * /p:analyze - Analyze repository
231
+ */
232
+ async analyze(context) {
233
+ const state = await contextBuilder.loadStateForCommand(context, 'analyze')
234
+ return { valid: true, error: null, suggestion: null, state }
235
+ },
236
+
237
+ /**
238
+ * /p:sync - Sync project state
239
+ */
240
+ async sync(context) {
241
+ const state = await contextBuilder.loadStateForCommand(context, 'sync')
242
+ return { valid: true, error: null, suggestion: null, state }
243
+ },
244
+
245
+ /**
246
+ * /p:bug - Report a bug
247
+ */
248
+ async bug(context) {
249
+ const state = await contextBuilder.loadState(context, ['next'])
250
+
251
+ if (!context.params.description && !context.params.bug) {
252
+ return {
253
+ valid: false,
254
+ error: 'No bug description provided',
255
+ suggestion: 'Describe the bug: /p:bug "description of the issue"',
256
+ state
257
+ }
258
+ }
259
+
260
+ return { valid: true, error: null, suggestion: null, state }
261
+ },
262
+
263
+ /**
264
+ * /p:help - Show help
265
+ */
266
+ async help(context) {
267
+ const state = await contextBuilder.loadStateForCommand(context, 'now')
268
+ return { valid: true, error: null, suggestion: null, state }
269
+ },
270
+
271
+ /**
272
+ * /p:ask - Intent translator
273
+ */
274
+ async ask(context) {
275
+ if (!context.params.query && !context.params.question) {
276
+ return {
277
+ valid: false,
278
+ error: 'No question provided',
279
+ suggestion: 'Ask what you want to do: /p:ask "how do I..."',
280
+ state: {}
281
+ }
282
+ }
283
+
284
+ return { valid: true, error: null, suggestion: null, state: {} }
285
+ },
286
+
287
+ /**
288
+ * /p:suggest - Smart suggestions
289
+ */
290
+ async suggest(context) {
291
+ const state = await contextBuilder.loadState(context, ['now', 'next', 'shipped', 'metrics'])
292
+ return { valid: true, error: null, suggestion: null, state }
293
+ },
294
+
295
+ /**
296
+ * /p:spec - Spec-driven development
297
+ */
298
+ async spec(context) {
299
+ const state = await contextBuilder.loadState(context, ['roadmap', 'next'])
300
+
301
+ // If no feature name, this is a "show template" request - always valid
302
+ if (!context.params.feature && !context.params.name && !context.params.description) {
303
+ return {
304
+ valid: true,
305
+ error: null,
306
+ suggestion: 'Provide a feature name to create a spec',
307
+ state
308
+ }
309
+ }
310
+
311
+ // Check queue capacity for new tasks
312
+ const queueContent = state.next || ''
313
+ const taskCount = (queueContent.match(/- \[[ x]\]/g) || []).length
314
+ if (taskCount >= 95) {
315
+ return {
316
+ valid: false,
317
+ error: 'Queue almost full (95+ tasks)',
318
+ suggestion: 'Complete some tasks before creating a new spec. Use /p:done',
319
+ state
320
+ }
321
+ }
322
+
323
+ return { valid: true, error: null, suggestion: null, state }
324
+ }
325
+ }
326
+
327
+ /**
328
+ * Validate command before execution
329
+ *
330
+ * @param {string} commandName - Command to validate
331
+ * @param {Object} context - Built context from contextBuilder
332
+ * @returns {Promise<ValidationResult>}
333
+ */
334
+ async function validate(commandName, context) {
335
+ const validator = validationRules[commandName]
336
+
337
+ if (!validator) {
338
+ // No specific validation - default to valid
339
+ return {
340
+ valid: true,
341
+ error: null,
342
+ suggestion: null,
343
+ state: await contextBuilder.loadState(context)
344
+ }
345
+ }
346
+
347
+ try {
348
+ return await validator(context)
349
+ } catch (error) {
350
+ return {
351
+ valid: false,
352
+ error: `Validation error: ${error.message}`,
353
+ suggestion: 'Check file permissions and project configuration',
354
+ state: {}
355
+ }
356
+ }
357
+ }
358
+
359
+ /**
360
+ * Format validation error for display
361
+ * Minimal, actionable output
362
+ *
363
+ * @param {ValidationResult} result - Validation result
364
+ * @returns {string} Formatted error message
365
+ */
366
+ function formatError(result) {
367
+ if (result.valid) return null
368
+
369
+ let output = `āŒ ${result.error}`
370
+ if (result.suggestion) {
371
+ output += `\n→ ${result.suggestion}`
372
+ }
373
+ return output
374
+ }
375
+
376
+ module.exports = {
377
+ validate,
378
+ formatError,
379
+ validationRules
380
+ }
@@ -88,6 +88,31 @@ const COMMANDS = [
88
88
  ],
89
89
  },
90
90
 
91
+ // 3.5. Spec-Driven Development (P1.2)
92
+ {
93
+ name: 'spec',
94
+ category: 'core',
95
+ description: 'Create detailed specifications for complex features',
96
+ usage: {
97
+ claude: '/p:spec "Dark Mode"',
98
+ terminal: 'prjct spec "Dark Mode"',
99
+ },
100
+ params: '[feature]',
101
+ implemented: true,
102
+ hasTemplate: true,
103
+ icon: 'FileText',
104
+ requiresInit: true,
105
+ blockingRules: null,
106
+ features: [
107
+ 'Requirements documentation',
108
+ 'Design decisions tracking',
109
+ 'Tasks broken into 20-30min chunks',
110
+ 'User approval workflow',
111
+ 'Auto-add tasks to queue on approve',
112
+ 'Integrates with /p:feature',
113
+ ],
114
+ },
115
+
91
116
  // 4. Work - Unified task management (replaces now + build)
92
117
  {
93
118
  name: 'work',
@@ -346,6 +371,29 @@ const COMMANDS = [
346
371
  blockingRules: null,
347
372
  isOptional: true,
348
373
  },
374
+ {
375
+ name: 'spec',
376
+ category: 'optional',
377
+ description: 'Spec-driven development for complex features',
378
+ usage: {
379
+ claude: '/p:spec "Dark Mode"',
380
+ terminal: 'prjct spec "Dark Mode"',
381
+ },
382
+ params: '[feature_name]',
383
+ implemented: true,
384
+ hasTemplate: true,
385
+ icon: 'FileText',
386
+ requiresInit: true,
387
+ blockingRules: null,
388
+ isOptional: true,
389
+ features: [
390
+ 'Clear requirements before coding',
391
+ 'Design decisions documented',
392
+ 'Tasks broken into 20-30 min chunks',
393
+ 'User approval before starting',
394
+ 'Auto-adds tasks to queue on approval',
395
+ ],
396
+ },
349
397
  {
350
398
  name: 'cleanup',
351
399
  category: 'optional',
package/core/commands.js CHANGED
@@ -21,6 +21,7 @@ const path = require('path')
21
21
  const commandExecutor = require('./agentic/command-executor')
22
22
  const contextBuilder = require('./agentic/context-builder')
23
23
  const toolRegistry = require('./agentic/tool-registry')
24
+ const memorySystem = require('./agentic/memory-system')
24
25
  const pathManager = require('./infrastructure/path-manager')
25
26
  const configManager = require('./infrastructure/config-manager')
26
27
  const authorDetector = require('./infrastructure/author-detector')
@@ -306,6 +307,7 @@ class PrjctCommands {
306
307
  const globalPath = pathManager.getGlobalProjectPath(projectId)
307
308
 
308
309
  // Create base files
310
+ // P1.1: Added patterns.json for Layered Memory System
309
311
  const baseFiles = {
310
312
  'core/now.md': '# NOW\n\nNo current task. Use `/p:now` to set focus.\n',
311
313
  'core/next.md': '# NEXT\n\n## Priority Queue\n\n',
@@ -314,7 +316,15 @@ class PrjctCommands {
314
316
  'progress/metrics.md': '# METRICS\n\n',
315
317
  'planning/ideas.md': '# IDEAS šŸ’”\n\n## Brain Dump\n\n',
316
318
  'planning/roadmap.md': '# ROADMAP\n\n',
319
+ 'planning/specs/.gitkeep': '# Specs directory - created by /p:spec\n',
317
320
  'memory/context.jsonl': '',
321
+ 'memory/patterns.json': JSON.stringify({
322
+ version: 1,
323
+ decisions: {},
324
+ preferences: {},
325
+ workflows: {},
326
+ counters: {}
327
+ }, null, 2),
318
328
  }
319
329
 
320
330
  for (const [filePath, content] of Object.entries(baseFiles)) {
@@ -379,9 +389,17 @@ class PrjctCommands {
379
389
  console.log(' Custom: Describe your preferred stack\n')
380
390
  console.log('Which option do you prefer? (Respond to continue setup)')
381
391
 
392
+ // Update global CLAUDE.md with latest instructions
393
+ const commandInstaller = require('./infrastructure/command-installer')
394
+ await commandInstaller.installGlobalConfig()
395
+
382
396
  return { success: true, mode: 'architect', projectId, idea }
383
397
  }
384
398
 
399
+ // Update global CLAUDE.md with latest instructions (fallback for any case)
400
+ const commandInstaller = require('./infrastructure/command-installer')
401
+ await commandInstaller.installGlobalConfig()
402
+
385
403
  return { success: true, projectId }
386
404
  } catch (error) {
387
405
  console.error('āŒ Error:', error.message)
@@ -693,6 +711,27 @@ class PrjctCommands {
693
711
  timestamp: dateHelper.getTimestamp(),
694
712
  })
695
713
 
714
+ // P1.1: Learn patterns from this ship
715
+ const config = await configManager.getConfig(projectPath)
716
+ const projectId = config.projectId
717
+
718
+ // Record shipping workflow patterns
719
+ await memorySystem.learnDecision(projectId, 'commit_footer', 'prjct', 'ship')
720
+
721
+ // Track if tests were run (for quick_ship pattern learning)
722
+ if (testResult.success) {
723
+ await memorySystem.recordDecision(projectId, 'test_before_ship', 'true', 'ship')
724
+ }
725
+
726
+ // Record workflow if it's a quick ship (small changes)
727
+ const isQuickShip = !lintResult.success || !testResult.success
728
+ if (isQuickShip) {
729
+ await memorySystem.recordWorkflow(projectId, 'quick_ship', {
730
+ description: 'Ship without full checks',
731
+ feature_type: feature.toLowerCase().includes('doc') ? 'docs' : 'other'
732
+ })
733
+ }
734
+
696
735
  console.log('\nšŸŽ‰ Feature shipped successfully!\n')
697
736
  console.log('šŸ’” Recommendation: Compact conversation now')
698
737
  console.log(' (Keeps context clean for next feature)\n')
@@ -2316,8 +2355,21 @@ Agent: ${agent}
2316
2355
  gitCommits: analysisData.gitStats.totalCommits,
2317
2356
  })
2318
2357
 
2358
+ // Generate dynamic context for Claude
2359
+ const contextSync = require('./context-sync')
2360
+ const projectId = await configManager.getProjectId(projectPath)
2361
+ await contextSync.generateLocalContext(projectPath, projectId)
2362
+
2363
+ // Update global CLAUDE.md with latest instructions
2364
+ const commandInstaller = require('./infrastructure/command-installer')
2365
+ const globalConfigResult = await commandInstaller.installGlobalConfig()
2366
+ if (globalConfigResult.success) {
2367
+ console.log('šŸ“ Updated ~/.claude/CLAUDE.md')
2368
+ }
2369
+
2319
2370
  console.log('āœ… Analysis complete!\n')
2320
- console.log('šŸ“„ Full report: analysis/repo-summary.md\n')
2371
+ console.log('šŸ“„ Full report: analysis/repo-summary.md')
2372
+ console.log('šŸ“ Context: ~/.prjct-cli/projects/' + projectId + '/CLAUDE.md\n')
2321
2373
  console.log('Next steps:')
2322
2374
  console.log('• /p:sync → Generate agents based on stack')
2323
2375
  console.log('• /p:feature → Add a new feature')
@@ -2482,11 +2534,23 @@ Agent: ${agent}
2482
2534
  count: generatedAgents.length,
2483
2535
  })
2484
2536
 
2537
+ // Generate dynamic context for Claude
2538
+ const contextSync = require('./context-sync')
2539
+ await contextSync.generateLocalContext(projectPath, projectId)
2540
+
2541
+ // Update global CLAUDE.md with latest instructions
2542
+ const commandInstaller = require('./infrastructure/command-installer')
2543
+ const globalConfigResult = await commandInstaller.installGlobalConfig()
2544
+ if (globalConfigResult.success) {
2545
+ console.log('šŸ“ Updated ~/.claude/CLAUDE.md')
2546
+ }
2547
+
2485
2548
  console.log('\nāœ… Sync complete!\n')
2486
2549
  console.log(`šŸ¤– Agents Generated: ${generatedAgents.length}`)
2487
2550
  generatedAgents.forEach((agent) => {
2488
2551
  console.log(` • ${agent}`)
2489
2552
  })
2553
+ console.log('šŸ“ Context: ~/.prjct-cli/projects/' + projectId + '/CLAUDE.md')
2490
2554
  console.log('\nšŸ“‹ Based on: analysis/repo-summary.md')
2491
2555
  console.log('šŸ’” See templates/agents/AGENTS.md for reference\n')
2492
2556
  console.log('Next steps:')