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.
- package/CHANGELOG.md +41 -0
- package/core/__tests__/agentic/memory-system.test.js +263 -0
- package/core/__tests__/agentic/plan-mode.test.js +336 -0
- package/core/agentic/chain-of-thought.js +578 -0
- package/core/agentic/command-executor.js +238 -4
- package/core/agentic/context-builder.js +208 -8
- package/core/agentic/ground-truth.js +591 -0
- package/core/agentic/loop-detector.js +406 -0
- package/core/agentic/memory-system.js +850 -0
- package/core/agentic/parallel-tools.js +366 -0
- package/core/agentic/plan-mode.js +572 -0
- package/core/agentic/prompt-builder.js +76 -1
- package/core/agentic/response-templates.js +290 -0
- package/core/agentic/semantic-compression.js +517 -0
- package/core/agentic/think-blocks.js +657 -0
- package/core/agentic/tool-registry.js +32 -0
- package/core/agentic/validation-rules.js +380 -0
- package/core/command-registry.js +48 -0
- package/core/commands.js +65 -1
- package/core/context-sync.js +183 -0
- package/package.json +7 -15
- package/templates/commands/done.md +7 -0
- package/templates/commands/feature.md +8 -0
- package/templates/commands/ship.md +8 -0
- package/templates/commands/spec.md +128 -0
- package/templates/global/CLAUDE.md +17 -0
- package/core/__tests__/agentic/agent-router.test.js +0 -398
- package/core/__tests__/agentic/command-executor.test.js +0 -223
- package/core/__tests__/agentic/context-builder.test.js +0 -160
- package/core/__tests__/agentic/context-filter.test.js +0 -494
- package/core/__tests__/agentic/prompt-builder.test.js +0 -204
- package/core/__tests__/agentic/template-loader.test.js +0 -164
- package/core/__tests__/agentic/tool-registry.test.js +0 -243
- package/core/__tests__/domain/agent-generator.test.js +0 -289
- package/core/__tests__/domain/agent-loader.test.js +0 -179
- package/core/__tests__/domain/analyzer.test.js +0 -324
- package/core/__tests__/infrastructure/author-detector.test.js +0 -103
- package/core/__tests__/infrastructure/config-manager.test.js +0 -454
- package/core/__tests__/infrastructure/path-manager.test.js +0 -412
- package/core/__tests__/setup.test.js +0 -15
- package/core/__tests__/utils/date-helper.test.js +0 -169
- package/core/__tests__/utils/file-helper.test.js +0 -258
- 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()
|