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,366 @@
1
+ /**
2
+ * Parallel Tools Executor
3
+ *
4
+ * P3.2: Multi-Tool Parallel Execution
5
+ * Executes independent tool calls in parallel for 2-3x faster execution.
6
+ *
7
+ * Pattern from: Cursor, Windsurf, Claude Code
8
+ *
9
+ * ```
10
+ * // Instead of:
11
+ * const file1 = await read(path1)
12
+ * const file2 = await read(path2)
13
+ * const search = await grep(pattern)
14
+ *
15
+ * // Do:
16
+ * const [file1, file2, search] = await parallel([
17
+ * { tool: 'Read', args: [path1] },
18
+ * { tool: 'Read', args: [path2] },
19
+ * { tool: 'Grep', args: [pattern] }
20
+ * ])
21
+ * ```
22
+ */
23
+
24
+ const toolRegistry = require('./tool-registry')
25
+
26
+ /**
27
+ * Tool parallelization rules
28
+ * - parallelizable: Can run alongside other tools
29
+ * - sequential: Must run one at a time
30
+ * - dependencies: Other tools this depends on
31
+ */
32
+ const TOOL_RULES = {
33
+ // Read-only tools - fully parallelizable
34
+ Read: {
35
+ parallelizable: true,
36
+ category: 'read',
37
+ dependencies: []
38
+ },
39
+ Glob: {
40
+ parallelizable: true,
41
+ category: 'read',
42
+ dependencies: []
43
+ },
44
+ Grep: {
45
+ parallelizable: true,
46
+ category: 'read',
47
+ dependencies: []
48
+ },
49
+ GetTimestamp: {
50
+ parallelizable: true,
51
+ category: 'read',
52
+ dependencies: []
53
+ },
54
+ GetDate: {
55
+ parallelizable: true,
56
+ category: 'read',
57
+ dependencies: []
58
+ },
59
+ GetDateTime: {
60
+ parallelizable: true,
61
+ category: 'read',
62
+ dependencies: []
63
+ },
64
+
65
+ // Write tools - sequential within same file
66
+ Write: {
67
+ parallelizable: false, // Sequential for same file
68
+ category: 'write',
69
+ dependencies: [],
70
+ conflictsWith: (other) => other.tool === 'Write' && other.args[0] === this.args?.[0]
71
+ },
72
+ Edit: {
73
+ parallelizable: false,
74
+ category: 'write',
75
+ dependencies: ['Read'] // Should read first
76
+ },
77
+
78
+ // Bash - sequential (can have side effects)
79
+ Bash: {
80
+ parallelizable: false,
81
+ category: 'execute',
82
+ dependencies: []
83
+ },
84
+ Exec: {
85
+ parallelizable: false,
86
+ category: 'execute',
87
+ dependencies: []
88
+ }
89
+ }
90
+
91
+ class ParallelTools {
92
+ constructor() {
93
+ this.rules = TOOL_RULES
94
+ this.metrics = {
95
+ totalCalls: 0,
96
+ parallelBatches: 0,
97
+ sequentialCalls: 0,
98
+ timeSaved: 0
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Execute multiple tool calls, parallelizing where possible
104
+ *
105
+ * @param {Array<{tool: string, args: any[]}>} toolCalls - Array of tool calls
106
+ * @returns {Promise<Array>} Results in same order as input
107
+ */
108
+ async execute(toolCalls) {
109
+ if (!toolCalls || toolCalls.length === 0) {
110
+ return []
111
+ }
112
+
113
+ const startTime = Date.now()
114
+ this.metrics.totalCalls += toolCalls.length
115
+
116
+ // Group into parallelizable batches
117
+ const batches = this._groupIntoBatches(toolCalls)
118
+
119
+ const results = new Array(toolCalls.length)
120
+
121
+ // Execute each batch
122
+ for (const batch of batches) {
123
+ if (batch.parallel) {
124
+ // Execute all in parallel
125
+ this.metrics.parallelBatches++
126
+ const batchResults = await Promise.all(
127
+ batch.calls.map(call => this._executeSingle(call))
128
+ )
129
+
130
+ // Map results back to original indices
131
+ batch.calls.forEach((call, i) => {
132
+ results[call.originalIndex] = batchResults[i]
133
+ })
134
+ } else {
135
+ // Execute sequentially
136
+ for (const call of batch.calls) {
137
+ this.metrics.sequentialCalls++
138
+ results[call.originalIndex] = await this._executeSingle(call)
139
+ }
140
+ }
141
+ }
142
+
143
+ // Calculate time saved (estimate)
144
+ const totalTime = Date.now() - startTime
145
+ const estimatedSequential = toolCalls.length * 50 // ~50ms per call estimate
146
+ this.metrics.timeSaved += Math.max(0, estimatedSequential - totalTime)
147
+
148
+ return results
149
+ }
150
+
151
+ /**
152
+ * Execute a single tool call
153
+ * @private
154
+ */
155
+ async _executeSingle(call) {
156
+ const tool = toolRegistry.get(call.tool)
157
+ if (!tool) {
158
+ return { error: `Unknown tool: ${call.tool}` }
159
+ }
160
+
161
+ try {
162
+ const result = await tool(...(call.args || []))
163
+ return { success: true, result }
164
+ } catch (error) {
165
+ return { success: false, error: error.message }
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Group tool calls into batches based on parallelization rules
171
+ * @private
172
+ */
173
+ _groupIntoBatches(toolCalls) {
174
+ const batches = []
175
+ let currentBatch = { parallel: true, calls: [] }
176
+
177
+ // Add original indices for result mapping
178
+ const indexedCalls = toolCalls.map((call, index) => ({
179
+ ...call,
180
+ originalIndex: index
181
+ }))
182
+
183
+ for (const call of indexedCalls) {
184
+ const rule = this.rules[call.tool] || { parallelizable: false }
185
+
186
+ if (rule.parallelizable) {
187
+ // Can add to current parallel batch
188
+ if (currentBatch.parallel) {
189
+ currentBatch.calls.push(call)
190
+ } else {
191
+ // Start new parallel batch
192
+ if (currentBatch.calls.length > 0) {
193
+ batches.push(currentBatch)
194
+ }
195
+ currentBatch = { parallel: true, calls: [call] }
196
+ }
197
+ } else {
198
+ // Must be sequential
199
+ // Flush current batch if exists
200
+ if (currentBatch.calls.length > 0) {
201
+ batches.push(currentBatch)
202
+ }
203
+ // Create sequential batch for this call
204
+ batches.push({ parallel: false, calls: [call] })
205
+ currentBatch = { parallel: true, calls: [] }
206
+ }
207
+ }
208
+
209
+ // Don't forget last batch
210
+ if (currentBatch.calls.length > 0) {
211
+ batches.push(currentBatch)
212
+ }
213
+
214
+ return batches
215
+ }
216
+
217
+ /**
218
+ * Check if a set of tool calls can be parallelized
219
+ *
220
+ * @param {Array<{tool: string, args: any[]}>} toolCalls
221
+ * @returns {boolean}
222
+ */
223
+ canParallelize(toolCalls) {
224
+ return toolCalls.every(call => {
225
+ const rule = this.rules[call.tool]
226
+ return rule && rule.parallelizable
227
+ })
228
+ }
229
+
230
+ /**
231
+ * Analyze tool calls and return optimization suggestions
232
+ *
233
+ * @param {Array<{tool: string, args: any[]}>} toolCalls
234
+ * @returns {Object} Analysis with suggestions
235
+ */
236
+ analyze(toolCalls) {
237
+ const readCalls = []
238
+ const writeCalls = []
239
+ const execCalls = []
240
+
241
+ for (const call of toolCalls) {
242
+ const rule = this.rules[call.tool] || {}
243
+ switch (rule.category) {
244
+ case 'read':
245
+ readCalls.push(call)
246
+ break
247
+ case 'write':
248
+ writeCalls.push(call)
249
+ break
250
+ case 'execute':
251
+ execCalls.push(call)
252
+ break
253
+ }
254
+ }
255
+
256
+ const canParallelizeReads = readCalls.length > 1
257
+ const hasSequentialWrites = writeCalls.length > 1
258
+ const hasExecCalls = execCalls.length > 0
259
+
260
+ return {
261
+ total: toolCalls.length,
262
+ reads: readCalls.length,
263
+ writes: writeCalls.length,
264
+ execs: execCalls.length,
265
+ canParallelizeReads,
266
+ hasSequentialWrites,
267
+ hasExecCalls,
268
+ suggestions: this._generateSuggestions({
269
+ canParallelizeReads,
270
+ hasSequentialWrites,
271
+ hasExecCalls,
272
+ readCalls,
273
+ writeCalls
274
+ })
275
+ }
276
+ }
277
+
278
+ /**
279
+ * Generate optimization suggestions
280
+ * @private
281
+ */
282
+ _generateSuggestions({ canParallelizeReads, hasSequentialWrites, readCalls, writeCalls }) {
283
+ const suggestions = []
284
+
285
+ if (canParallelizeReads) {
286
+ suggestions.push(`Parallelize ${readCalls.length} read operations`)
287
+ }
288
+
289
+ if (hasSequentialWrites) {
290
+ // Check for same-file writes
291
+ const files = writeCalls.map(c => c.args?.[0])
292
+ const uniqueFiles = new Set(files)
293
+ if (uniqueFiles.size < files.length) {
294
+ suggestions.push('Multiple writes to same file - must be sequential')
295
+ } else {
296
+ suggestions.push(`Can parallelize ${writeCalls.length} writes to different files`)
297
+ }
298
+ }
299
+
300
+ return suggestions
301
+ }
302
+
303
+ /**
304
+ * Get execution metrics
305
+ * @returns {Object}
306
+ */
307
+ getMetrics() {
308
+ return {
309
+ ...this.metrics,
310
+ efficiency: this.metrics.totalCalls > 0
311
+ ? Math.round((this.metrics.parallelBatches / this.metrics.totalCalls) * 100)
312
+ : 0
313
+ }
314
+ }
315
+
316
+ /**
317
+ * Reset metrics
318
+ */
319
+ resetMetrics() {
320
+ this.metrics = {
321
+ totalCalls: 0,
322
+ parallelBatches: 0,
323
+ sequentialCalls: 0,
324
+ timeSaved: 0
325
+ }
326
+ }
327
+
328
+ /**
329
+ * Convenience method: Parallel read multiple files
330
+ *
331
+ * @param {string[]} filePaths - Array of file paths
332
+ * @returns {Promise<Map<string, string|null>>}
333
+ */
334
+ async readAll(filePaths) {
335
+ const toolCalls = filePaths.map(path => ({
336
+ tool: 'Read',
337
+ args: [path]
338
+ }))
339
+
340
+ const results = await this.execute(toolCalls)
341
+
342
+ const map = new Map()
343
+ filePaths.forEach((path, i) => {
344
+ map.set(path, results[i]?.result || null)
345
+ })
346
+
347
+ return map
348
+ }
349
+
350
+ /**
351
+ * Convenience method: Parallel grep search
352
+ *
353
+ * @param {Array<{pattern: string, path: string}>} searches
354
+ * @returns {Promise<Array>}
355
+ */
356
+ async searchAll(searches) {
357
+ const toolCalls = searches.map(s => ({
358
+ tool: 'Grep',
359
+ args: [s.pattern, s.path]
360
+ }))
361
+
362
+ return this.execute(toolCalls)
363
+ }
364
+ }
365
+
366
+ module.exports = new ParallelTools()