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.
Files changed (53) hide show
  1. package/CHANGELOG.md +142 -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/agent-router.js +253 -186
  5. package/core/agentic/chain-of-thought.js +578 -0
  6. package/core/agentic/command-executor.js +299 -17
  7. package/core/agentic/context-builder.js +208 -8
  8. package/core/agentic/context-filter.js +83 -83
  9. package/core/agentic/ground-truth.js +591 -0
  10. package/core/agentic/loop-detector.js +406 -0
  11. package/core/agentic/memory-system.js +850 -0
  12. package/core/agentic/parallel-tools.js +366 -0
  13. package/core/agentic/plan-mode.js +572 -0
  14. package/core/agentic/prompt-builder.js +127 -2
  15. package/core/agentic/response-templates.js +290 -0
  16. package/core/agentic/semantic-compression.js +517 -0
  17. package/core/agentic/think-blocks.js +657 -0
  18. package/core/agentic/tool-registry.js +32 -0
  19. package/core/agentic/validation-rules.js +380 -0
  20. package/core/command-registry.js +48 -0
  21. package/core/commands.js +128 -60
  22. package/core/context-sync.js +183 -0
  23. package/core/domain/agent-generator.js +77 -46
  24. package/core/domain/agent-loader.js +183 -0
  25. package/core/domain/agent-matcher.js +217 -0
  26. package/core/domain/agent-validator.js +217 -0
  27. package/core/domain/context-estimator.js +175 -0
  28. package/core/domain/product-standards.js +92 -0
  29. package/core/domain/smart-cache.js +157 -0
  30. package/core/domain/task-analyzer.js +353 -0
  31. package/core/domain/tech-detector.js +365 -0
  32. package/package.json +8 -16
  33. package/templates/commands/done.md +7 -0
  34. package/templates/commands/feature.md +8 -0
  35. package/templates/commands/ship.md +8 -0
  36. package/templates/commands/spec.md +128 -0
  37. package/templates/global/CLAUDE.md +17 -0
  38. package/core/__tests__/agentic/agent-router.test.js +0 -398
  39. package/core/__tests__/agentic/command-executor.test.js +0 -223
  40. package/core/__tests__/agentic/context-builder.test.js +0 -160
  41. package/core/__tests__/agentic/context-filter.test.js +0 -494
  42. package/core/__tests__/agentic/prompt-builder.test.js +0 -212
  43. package/core/__tests__/agentic/template-loader.test.js +0 -164
  44. package/core/__tests__/agentic/tool-registry.test.js +0 -243
  45. package/core/__tests__/domain/agent-generator.test.js +0 -296
  46. package/core/__tests__/domain/analyzer.test.js +0 -324
  47. package/core/__tests__/infrastructure/author-detector.test.js +0 -103
  48. package/core/__tests__/infrastructure/config-manager.test.js +0 -454
  49. package/core/__tests__/infrastructure/path-manager.test.js +0 -412
  50. package/core/__tests__/setup.test.js +0 -15
  51. package/core/__tests__/utils/date-helper.test.js +0 -169
  52. package/core/__tests__/utils/file-helper.test.js +0 -258
  53. 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',