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,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)) {
@@ -693,6 +703,27 @@ class PrjctCommands {
693
703
  timestamp: dateHelper.getTimestamp(),
694
704
  })
695
705
 
706
+ // P1.1: Learn patterns from this ship
707
+ const config = await configManager.getConfig(projectPath)
708
+ const projectId = config.projectId
709
+
710
+ // Record shipping workflow patterns
711
+ await memorySystem.learnDecision(projectId, 'commit_footer', 'prjct', 'ship')
712
+
713
+ // Track if tests were run (for quick_ship pattern learning)
714
+ if (testResult.success) {
715
+ await memorySystem.recordDecision(projectId, 'test_before_ship', 'true', 'ship')
716
+ }
717
+
718
+ // Record workflow if it's a quick ship (small changes)
719
+ const isQuickShip = !lintResult.success || !testResult.success
720
+ if (isQuickShip) {
721
+ await memorySystem.recordWorkflow(projectId, 'quick_ship', {
722
+ description: 'Ship without full checks',
723
+ feature_type: feature.toLowerCase().includes('doc') ? 'docs' : 'other'
724
+ })
725
+ }
726
+
696
727
  console.log('\nšŸŽ‰ Feature shipped successfully!\n')
697
728
  console.log('šŸ’” Recommendation: Compact conversation now')
698
729
  console.log(' (Keeps context clean for next feature)\n')
@@ -2316,8 +2347,14 @@ Agent: ${agent}
2316
2347
  gitCommits: analysisData.gitStats.totalCommits,
2317
2348
  })
2318
2349
 
2350
+ // Generate dynamic context for Claude
2351
+ const contextSync = require('./context-sync')
2352
+ const projectId = await configManager.getProjectId(projectPath)
2353
+ await contextSync.generateLocalContext(projectPath, projectId)
2354
+
2319
2355
  console.log('āœ… Analysis complete!\n')
2320
- console.log('šŸ“„ Full report: analysis/repo-summary.md\n')
2356
+ console.log('šŸ“„ Full report: analysis/repo-summary.md')
2357
+ console.log('šŸ“ Context: ~/.prjct-cli/projects/' + projectId + '/CLAUDE.md\n')
2321
2358
  console.log('Next steps:')
2322
2359
  console.log('• /p:sync → Generate agents based on stack')
2323
2360
  console.log('• /p:feature → Add a new feature')
@@ -2482,11 +2519,16 @@ Agent: ${agent}
2482
2519
  count: generatedAgents.length,
2483
2520
  })
2484
2521
 
2522
+ // Generate dynamic context for Claude
2523
+ const contextSync = require('./context-sync')
2524
+ await contextSync.generateLocalContext(projectPath, projectId)
2525
+
2485
2526
  console.log('\nāœ… Sync complete!\n')
2486
2527
  console.log(`šŸ¤– Agents Generated: ${generatedAgents.length}`)
2487
2528
  generatedAgents.forEach((agent) => {
2488
2529
  console.log(` • ${agent}`)
2489
2530
  })
2531
+ console.log('šŸ“ Context: ~/.prjct-cli/projects/' + projectId + '/CLAUDE.md')
2490
2532
  console.log('\nšŸ“‹ Based on: analysis/repo-summary.md')
2491
2533
  console.log('šŸ’” See templates/agents/AGENTS.md for reference\n')
2492
2534
  console.log('Next steps:')