prjct-cli 0.10.13 → 0.11.0

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 (44) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/CLAUDE.md +47 -2
  3. package/bin/dev.js +217 -0
  4. package/bin/prjct +10 -0
  5. package/bin/serve.js +78 -0
  6. package/core/agentic/command-executor.js +38 -112
  7. package/core/agentic/prompt-builder.js +72 -0
  8. package/core/bus/index.js +322 -0
  9. package/core/command-registry.js +65 -0
  10. package/core/domain/snapshot-manager.js +375 -0
  11. package/core/plugin/hooks.js +313 -0
  12. package/core/plugin/index.js +52 -0
  13. package/core/plugin/loader.js +331 -0
  14. package/core/plugin/registry.js +325 -0
  15. package/core/plugins/webhook.js +143 -0
  16. package/core/session/index.js +449 -0
  17. package/core/session/metrics.js +293 -0
  18. package/package.json +18 -4
  19. package/templates/agentic/agent-routing.md +42 -9
  20. package/templates/agentic/checklist-routing.md +98 -0
  21. package/templates/checklists/accessibility.md +33 -0
  22. package/templates/checklists/architecture.md +28 -0
  23. package/templates/checklists/code-quality.md +28 -0
  24. package/templates/checklists/data.md +33 -0
  25. package/templates/checklists/documentation.md +33 -0
  26. package/templates/checklists/infrastructure.md +33 -0
  27. package/templates/checklists/performance.md +33 -0
  28. package/templates/checklists/security.md +33 -0
  29. package/templates/checklists/testing.md +33 -0
  30. package/templates/checklists/ux-ui.md +37 -0
  31. package/templates/commands/bug.md +27 -1
  32. package/templates/commands/done.md +176 -54
  33. package/templates/commands/feature.md +38 -1
  34. package/templates/commands/history.md +176 -0
  35. package/templates/commands/init.md +28 -1
  36. package/templates/commands/now.md +191 -9
  37. package/templates/commands/pause.md +176 -12
  38. package/templates/commands/redo.md +142 -0
  39. package/templates/commands/resume.md +166 -62
  40. package/templates/commands/serve.md +121 -0
  41. package/templates/commands/ship.md +45 -1
  42. package/templates/commands/sync.md +34 -1
  43. package/templates/commands/task.md +27 -1
  44. package/templates/commands/undo.md +152 -0
@@ -7,9 +7,65 @@
7
7
  * P3.1: Includes think blocks for anti-hallucination
8
8
  * P3.3: Includes relevant memories from semantic database
9
9
  * P3.4: Includes plan mode instructions
10
+ * P4.1: Includes quality checklists (Claude decides which to apply)
10
11
  */
11
12
 
13
+ const fs = require('fs')
14
+ const path = require('path')
15
+
12
16
  class PromptBuilder {
17
+ constructor() {
18
+ this._checklistsCache = null
19
+ this._checklistRoutingCache = null
20
+ }
21
+
22
+ /**
23
+ * Load quality checklists from templates/checklists/
24
+ * Returns checklist content - Claude decides which to apply
25
+ * NO if/else logic here - just load and provide
26
+ */
27
+ loadChecklists() {
28
+ if (this._checklistsCache) return this._checklistsCache
29
+
30
+ const checklistsDir = path.join(__dirname, '..', '..', 'templates', 'checklists')
31
+ const checklists = {}
32
+
33
+ try {
34
+ if (fs.existsSync(checklistsDir)) {
35
+ const files = fs.readdirSync(checklistsDir).filter(f => f.endsWith('.md'))
36
+ for (const file of files) {
37
+ const name = file.replace('.md', '')
38
+ const content = fs.readFileSync(path.join(checklistsDir, file), 'utf-8')
39
+ checklists[name] = content
40
+ }
41
+ }
42
+ } catch (err) {
43
+ // Silent fail - checklists are optional enhancement
44
+ }
45
+
46
+ this._checklistsCache = checklists
47
+ return checklists
48
+ }
49
+
50
+ /**
51
+ * Load checklist routing template
52
+ * Claude reads this to decide which checklists to apply
53
+ */
54
+ loadChecklistRouting() {
55
+ if (this._checklistRoutingCache) return this._checklistRoutingCache
56
+
57
+ const routingPath = path.join(__dirname, '..', '..', 'templates', 'agentic', 'checklist-routing.md')
58
+
59
+ try {
60
+ if (fs.existsSync(routingPath)) {
61
+ this._checklistRoutingCache = fs.readFileSync(routingPath, 'utf-8')
62
+ }
63
+ } catch (err) {
64
+ // Silent fail
65
+ }
66
+
67
+ return this._checklistRoutingCache || null
68
+ }
13
69
  /**
14
70
  * Build concise prompt - only essentials
15
71
  * CRITICAL: Includes full agent content if agent is provided
@@ -154,6 +210,22 @@ class PromptBuilder {
154
210
  parts.push(`\n## APPROVAL REQUIRED\nShow changes, list affected files, ask for confirmation.\n`)
155
211
  }
156
212
 
213
+ // P4.1: Quality Checklists (Claude decides which to apply)
214
+ // Only for code-modifying commands that benefit from quality gates
215
+ const checklistCommands = ['now', 'build', 'feature', 'design', 'fix', 'bug', 'cleanup', 'spec', 'work']
216
+ if (checklistCommands.includes(commandName)) {
217
+ const routing = this.loadChecklistRouting()
218
+ const checklists = this.loadChecklists()
219
+
220
+ if (routing && Object.keys(checklists).length > 0) {
221
+ parts.push('\n## QUALITY CHECKLISTS\n')
222
+ parts.push('Apply relevant checklists based on task. Read checklist-routing.md for guidance.\n')
223
+ parts.push(`Available: ${Object.keys(checklists).join(', ')}\n`)
224
+ parts.push('Path: templates/checklists/{name}.md\n')
225
+ parts.push('Use Read tool to load checklists you determine are relevant.\n')
226
+ }
227
+ }
228
+
157
229
  // Simple execution directive
158
230
  parts.push('\nEXECUTE: Follow flow. Use tools. Decide.\n')
159
231
 
@@ -0,0 +1,322 @@
1
+ /**
2
+ * EventBus - Lightweight Pub/Sub System for prjct-cli
3
+ *
4
+ * Simple event bus for decoupled communication between components.
5
+ * Supports sync/async listeners, wildcards, and one-time subscriptions.
6
+ *
7
+ * @version 1.0.0
8
+ */
9
+
10
+ const fs = require('fs').promises
11
+ const path = require('path')
12
+ const pathManager = require('../infrastructure/path-manager')
13
+
14
+ /**
15
+ * Event Types - All events that can be emitted
16
+ */
17
+ const EventTypes = {
18
+ // Session events
19
+ SESSION_STARTED: 'session.started',
20
+ SESSION_PAUSED: 'session.paused',
21
+ SESSION_RESUMED: 'session.resumed',
22
+ SESSION_COMPLETED: 'session.completed',
23
+
24
+ // Task events
25
+ TASK_CREATED: 'task.created',
26
+ TASK_COMPLETED: 'task.completed',
27
+ TASK_UPDATED: 'task.updated',
28
+
29
+ // Feature events
30
+ FEATURE_ADDED: 'feature.added',
31
+ FEATURE_SHIPPED: 'feature.shipped',
32
+ FEATURE_UPDATED: 'feature.updated',
33
+
34
+ // Idea events
35
+ IDEA_CAPTURED: 'idea.captured',
36
+ IDEA_PROMOTED: 'idea.promoted',
37
+
38
+ // Snapshot events
39
+ SNAPSHOT_CREATED: 'snapshot.created',
40
+ SNAPSHOT_RESTORED: 'snapshot.restored',
41
+
42
+ // Git events
43
+ COMMIT_CREATED: 'git.commit',
44
+ PUSH_COMPLETED: 'git.push',
45
+
46
+ // System events
47
+ PROJECT_INITIALIZED: 'project.init',
48
+ PROJECT_SYNCED: 'project.sync',
49
+ ANALYSIS_COMPLETED: 'analysis.completed',
50
+
51
+ // Wildcard
52
+ ALL: '*'
53
+ }
54
+
55
+ class EventBus {
56
+ constructor() {
57
+ this.listeners = new Map()
58
+ this.onceListeners = new Map()
59
+ this.history = []
60
+ this.historyLimit = 100
61
+ this.projectId = null
62
+ }
63
+
64
+ /**
65
+ * Initialize event bus for a project
66
+ * @param {string} projectId
67
+ */
68
+ async initialize(projectId) {
69
+ this.projectId = projectId
70
+ }
71
+
72
+ /**
73
+ * Subscribe to an event
74
+ * @param {string} event - Event type or wildcard pattern
75
+ * @param {Function} callback - Handler function
76
+ * @returns {Function} Unsubscribe function
77
+ */
78
+ on(event, callback) {
79
+ if (!this.listeners.has(event)) {
80
+ this.listeners.set(event, new Set())
81
+ }
82
+ this.listeners.get(event).add(callback)
83
+
84
+ // Return unsubscribe function
85
+ return () => this.off(event, callback)
86
+ }
87
+
88
+ /**
89
+ * Subscribe to an event once
90
+ * @param {string} event
91
+ * @param {Function} callback
92
+ * @returns {Function} Unsubscribe function
93
+ */
94
+ once(event, callback) {
95
+ if (!this.onceListeners.has(event)) {
96
+ this.onceListeners.set(event, new Set())
97
+ }
98
+ this.onceListeners.get(event).add(callback)
99
+
100
+ return () => {
101
+ const listeners = this.onceListeners.get(event)
102
+ if (listeners) {
103
+ listeners.delete(callback)
104
+ }
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Unsubscribe from an event
110
+ * @param {string} event
111
+ * @param {Function} callback
112
+ */
113
+ off(event, callback) {
114
+ const listeners = this.listeners.get(event)
115
+ if (listeners) {
116
+ listeners.delete(callback)
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Emit an event
122
+ * @param {string} event - Event type
123
+ * @param {Object} data - Event payload
124
+ * @returns {Promise<void>}
125
+ */
126
+ async emit(event, data = {}) {
127
+ const timestamp = new Date().toISOString()
128
+ const eventData = {
129
+ type: event,
130
+ timestamp,
131
+ projectId: this.projectId,
132
+ ...data
133
+ }
134
+
135
+ // Store in history
136
+ this.history.push(eventData)
137
+ if (this.history.length > this.historyLimit) {
138
+ this.history.shift()
139
+ }
140
+
141
+ // Log event if project initialized
142
+ if (this.projectId) {
143
+ await this.logEvent(eventData)
144
+ }
145
+
146
+ // Get all matching listeners
147
+ const callbacks = this.getMatchingListeners(event)
148
+
149
+ // Execute all callbacks
150
+ const results = await Promise.allSettled(
151
+ callbacks.map(cb => this.executeCallback(cb, eventData))
152
+ )
153
+
154
+ // Log any errors
155
+ results.forEach((result, index) => {
156
+ if (result.status === 'rejected') {
157
+ console.error(`Event listener error for ${event}:`, result.reason)
158
+ }
159
+ })
160
+
161
+ // Handle once listeners
162
+ const onceCallbacks = this.onceListeners.get(event)
163
+ if (onceCallbacks) {
164
+ for (const cb of onceCallbacks) {
165
+ await this.executeCallback(cb, eventData)
166
+ }
167
+ this.onceListeners.delete(event)
168
+ }
169
+
170
+ // Also trigger wildcard once listeners
171
+ const wildcardOnce = this.onceListeners.get(EventTypes.ALL)
172
+ if (wildcardOnce) {
173
+ for (const cb of wildcardOnce) {
174
+ await this.executeCallback(cb, eventData)
175
+ }
176
+ // Don't delete wildcard once - only for specific events
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Get all listeners that match an event (including wildcards)
182
+ * @param {string} event
183
+ * @returns {Function[]}
184
+ */
185
+ getMatchingListeners(event) {
186
+ const callbacks = []
187
+
188
+ // Exact match
189
+ const exact = this.listeners.get(event)
190
+ if (exact) {
191
+ callbacks.push(...exact)
192
+ }
193
+
194
+ // Wildcard match (*)
195
+ const wildcard = this.listeners.get(EventTypes.ALL)
196
+ if (wildcard) {
197
+ callbacks.push(...wildcard)
198
+ }
199
+
200
+ // Namespace wildcard (e.g., 'session.*' matches 'session.started')
201
+ const namespace = event.split('.')[0]
202
+ const namespaceWildcard = this.listeners.get(`${namespace}.*`)
203
+ if (namespaceWildcard) {
204
+ callbacks.push(...namespaceWildcard)
205
+ }
206
+
207
+ return callbacks
208
+ }
209
+
210
+ /**
211
+ * Execute a callback safely (handles sync and async)
212
+ * @param {Function} callback
213
+ * @param {Object} data
214
+ */
215
+ async executeCallback(callback, data) {
216
+ try {
217
+ const result = callback(data)
218
+ if (result instanceof Promise) {
219
+ await result
220
+ }
221
+ } catch (error) {
222
+ console.error('Event callback error:', error)
223
+ throw error
224
+ }
225
+ }
226
+
227
+ /**
228
+ * Log event to persistent storage
229
+ * @param {Object} eventData
230
+ */
231
+ async logEvent(eventData) {
232
+ try {
233
+ const globalPath = pathManager.getGlobalProjectPath(this.projectId)
234
+ const eventsPath = path.join(globalPath, 'memory', 'events.jsonl')
235
+
236
+ // Ensure directory exists
237
+ await fs.mkdir(path.dirname(eventsPath), { recursive: true })
238
+
239
+ // Append event
240
+ const line = JSON.stringify(eventData) + '\n'
241
+ await fs.appendFile(eventsPath, line)
242
+ } catch {
243
+ // Silently fail - logging should not break functionality
244
+ }
245
+ }
246
+
247
+ /**
248
+ * Get recent events from history
249
+ * @param {number} limit
250
+ * @param {string} [type] - Optional filter by event type
251
+ * @returns {Object[]}
252
+ */
253
+ getHistory(limit = 10, type = null) {
254
+ let events = this.history
255
+ if (type) {
256
+ events = events.filter(e => e.type === type || e.type.startsWith(type))
257
+ }
258
+ return events.slice(-limit)
259
+ }
260
+
261
+ /**
262
+ * Clear all listeners
263
+ */
264
+ clear() {
265
+ this.listeners.clear()
266
+ this.onceListeners.clear()
267
+ }
268
+
269
+ /**
270
+ * Get count of listeners for an event
271
+ * @param {string} event
272
+ * @returns {number}
273
+ */
274
+ listenerCount(event) {
275
+ const listeners = this.listeners.get(event)
276
+ return listeners ? listeners.size : 0
277
+ }
278
+
279
+ /**
280
+ * Get all registered event types
281
+ * @returns {string[]}
282
+ */
283
+ getRegisteredEvents() {
284
+ return Array.from(this.listeners.keys())
285
+ }
286
+ }
287
+
288
+ // Singleton instance
289
+ const eventBus = new EventBus()
290
+
291
+ // Convenience methods for common events
292
+ const emit = {
293
+ sessionStarted: (data) => eventBus.emit(EventTypes.SESSION_STARTED, data),
294
+ sessionPaused: (data) => eventBus.emit(EventTypes.SESSION_PAUSED, data),
295
+ sessionResumed: (data) => eventBus.emit(EventTypes.SESSION_RESUMED, data),
296
+ sessionCompleted: (data) => eventBus.emit(EventTypes.SESSION_COMPLETED, data),
297
+
298
+ taskCreated: (data) => eventBus.emit(EventTypes.TASK_CREATED, data),
299
+ taskCompleted: (data) => eventBus.emit(EventTypes.TASK_COMPLETED, data),
300
+
301
+ featureAdded: (data) => eventBus.emit(EventTypes.FEATURE_ADDED, data),
302
+ featureShipped: (data) => eventBus.emit(EventTypes.FEATURE_SHIPPED, data),
303
+
304
+ ideaCaptured: (data) => eventBus.emit(EventTypes.IDEA_CAPTURED, data),
305
+
306
+ snapshotCreated: (data) => eventBus.emit(EventTypes.SNAPSHOT_CREATED, data),
307
+ snapshotRestored: (data) => eventBus.emit(EventTypes.SNAPSHOT_RESTORED, data),
308
+
309
+ commitCreated: (data) => eventBus.emit(EventTypes.COMMIT_CREATED, data),
310
+ pushCompleted: (data) => eventBus.emit(EventTypes.PUSH_COMPLETED, data),
311
+
312
+ projectInitialized: (data) => eventBus.emit(EventTypes.PROJECT_INITIALIZED, data),
313
+ projectSynced: (data) => eventBus.emit(EventTypes.PROJECT_SYNCED, data),
314
+ analysisCompleted: (data) => eventBus.emit(EventTypes.ANALYSIS_COMPLETED, data)
315
+ }
316
+
317
+ module.exports = {
318
+ EventBus,
319
+ EventTypes,
320
+ eventBus,
321
+ emit
322
+ }
@@ -427,6 +427,71 @@ const COMMANDS = [
427
427
  isOptional: true,
428
428
  },
429
429
 
430
+ // ===== SNAPSHOT COMMANDS (Undo/Redo Support) =====
431
+ {
432
+ name: 'undo',
433
+ category: 'optional',
434
+ description: 'Revert to previous snapshot',
435
+ usage: {
436
+ claude: '/p:undo',
437
+ terminal: 'prjct undo',
438
+ },
439
+ params: null,
440
+ implemented: true,
441
+ hasTemplate: true,
442
+ icon: 'RotateCcw',
443
+ requiresInit: true,
444
+ blockingRules: null,
445
+ isOptional: true,
446
+ features: [
447
+ 'Git-based snapshots',
448
+ 'Preserves redo history',
449
+ 'Non-destructive',
450
+ ],
451
+ },
452
+ {
453
+ name: 'redo',
454
+ category: 'optional',
455
+ description: 'Redo previously undone changes',
456
+ usage: {
457
+ claude: '/p:redo',
458
+ terminal: 'prjct redo',
459
+ },
460
+ params: null,
461
+ implemented: true,
462
+ hasTemplate: true,
463
+ icon: 'RotateCw',
464
+ requiresInit: true,
465
+ blockingRules: null,
466
+ isOptional: true,
467
+ features: [
468
+ 'Only available after undo',
469
+ 'Restores undone state',
470
+ 'Clears on new snapshot',
471
+ ],
472
+ },
473
+ {
474
+ name: 'history',
475
+ category: 'optional',
476
+ description: 'View snapshot history',
477
+ usage: {
478
+ claude: '/p:history',
479
+ terminal: 'prjct history',
480
+ },
481
+ params: null,
482
+ implemented: true,
483
+ hasTemplate: true,
484
+ icon: 'Clock',
485
+ requiresInit: true,
486
+ blockingRules: null,
487
+ isOptional: true,
488
+ features: [
489
+ 'Shows all snapshots',
490
+ 'Current position indicator',
491
+ 'Redo availability count',
492
+ ],
493
+ },
494
+
430
495
  // ===== SETUP COMMANDS (Not part of daily workflow) =====
431
496
  {
432
497
  name: 'start',