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.
- package/CHANGELOG.md +142 -0
- package/core/__tests__/agentic/memory-system.test.js +263 -0
- package/core/__tests__/agentic/plan-mode.test.js +336 -0
- package/core/agentic/agent-router.js +253 -186
- package/core/agentic/chain-of-thought.js +578 -0
- package/core/agentic/command-executor.js +299 -17
- package/core/agentic/context-builder.js +208 -8
- package/core/agentic/context-filter.js +83 -83
- 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 +127 -2
- 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 +128 -60
- package/core/context-sync.js +183 -0
- package/core/domain/agent-generator.js +77 -46
- package/core/domain/agent-loader.js +183 -0
- package/core/domain/agent-matcher.js +217 -0
- package/core/domain/agent-validator.js +217 -0
- package/core/domain/context-estimator.js +175 -0
- package/core/domain/product-standards.js +92 -0
- package/core/domain/smart-cache.js +157 -0
- package/core/domain/task-analyzer.js +353 -0
- package/core/domain/tech-detector.js +365 -0
- package/package.json +8 -16
- 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 -212
- 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 -296
- 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
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
* Command Executor
|
|
3
3
|
* WITH MANDATORY AGENT ASSIGNMENT
|
|
4
4
|
* Every task MUST use a specialized agent
|
|
5
|
+
*
|
|
6
|
+
* OPTIMIZATION (P0.2): Explicit Validation
|
|
7
|
+
* - Pre-flight checks before execution
|
|
8
|
+
* - Specific error messages, never generic failures
|
|
9
|
+
* - Actionable suggestions in every error
|
|
10
|
+
*
|
|
11
|
+
* P3.4: Plan Mode + Approval Flow
|
|
12
|
+
* - Separates planning from execution
|
|
13
|
+
* - Requires approval for destructive commands
|
|
14
|
+
*
|
|
15
|
+
* Source: Claude Code, Devin, Augment Code patterns
|
|
5
16
|
*/
|
|
6
17
|
|
|
7
18
|
const templateLoader = require('./template-loader')
|
|
@@ -10,68 +21,242 @@ const promptBuilder = require('./prompt-builder')
|
|
|
10
21
|
const toolRegistry = require('./tool-registry')
|
|
11
22
|
const MandatoryAgentRouter = require('./agent-router')
|
|
12
23
|
const ContextFilter = require('./context-filter')
|
|
24
|
+
const ContextEstimator = require('../domain/context-estimator')
|
|
25
|
+
const { validate, formatError } = require('./validation-rules')
|
|
26
|
+
const loopDetector = require('./loop-detector')
|
|
27
|
+
const chainOfThought = require('./chain-of-thought')
|
|
28
|
+
const semanticCompression = require('./semantic-compression')
|
|
29
|
+
const responseTemplates = require('./response-templates')
|
|
30
|
+
const memorySystem = require('./memory-system')
|
|
31
|
+
const groundTruth = require('./ground-truth')
|
|
32
|
+
const thinkBlocks = require('./think-blocks')
|
|
33
|
+
const parallelTools = require('./parallel-tools')
|
|
34
|
+
const planMode = require('./plan-mode')
|
|
35
|
+
// P3.5, P3.6, P3.7: DELEGATED TO CLAUDE CODE
|
|
36
|
+
// - semantic-search → Claude Code has Grep/Glob with semantic understanding
|
|
37
|
+
// - code-intelligence → Claude Code has native LSP integration
|
|
38
|
+
// - browser-preview → Claude Code can use Bash directly
|
|
13
39
|
|
|
14
40
|
class CommandExecutor {
|
|
15
41
|
constructor() {
|
|
16
42
|
this.agentRouter = new MandatoryAgentRouter()
|
|
17
43
|
this.contextFilter = new ContextFilter()
|
|
44
|
+
this.contextEstimator = null
|
|
18
45
|
}
|
|
19
46
|
|
|
20
47
|
/**
|
|
21
48
|
* Execute command with MANDATORY agent assignment
|
|
22
49
|
*/
|
|
23
50
|
async execute(commandName, params, projectPath) {
|
|
51
|
+
// Context for loop detection
|
|
52
|
+
const loopContext = params.task || params.description || ''
|
|
53
|
+
|
|
54
|
+
// Check if we're in a loop BEFORE attempting
|
|
55
|
+
if (loopDetector.shouldEscalate(commandName, loopContext)) {
|
|
56
|
+
const escalation = loopDetector.getEscalationInfo(commandName, loopContext)
|
|
57
|
+
return {
|
|
58
|
+
success: false,
|
|
59
|
+
error: escalation.message,
|
|
60
|
+
escalation,
|
|
61
|
+
isLoopDetected: true,
|
|
62
|
+
suggestion: escalation.suggestion
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
24
66
|
try {
|
|
25
67
|
// 1. Load template
|
|
26
68
|
const template = await templateLoader.load(commandName)
|
|
27
69
|
|
|
28
|
-
// 2. Build
|
|
29
|
-
const
|
|
70
|
+
// 2. Build METADATA context only (lazy loading - no file reads yet)
|
|
71
|
+
const metadataContext = await contextBuilder.build(projectPath, params)
|
|
72
|
+
|
|
73
|
+
// 2.5. VALIDATE: Pre-flight checks with specific errors
|
|
74
|
+
const validation = await validate(commandName, metadataContext)
|
|
75
|
+
if (!validation.valid) {
|
|
76
|
+
return {
|
|
77
|
+
success: false,
|
|
78
|
+
error: formatError(validation),
|
|
79
|
+
validation,
|
|
80
|
+
isValidationError: true
|
|
81
|
+
}
|
|
82
|
+
}
|
|
30
83
|
|
|
31
|
-
//
|
|
32
|
-
const
|
|
33
|
-
|
|
84
|
+
// 2.55. P3.4 PLAN MODE: Check if command requires planning
|
|
85
|
+
const requiresPlanning = planMode.requiresPlanning(commandName)
|
|
86
|
+
const isDestructive = planMode.isDestructive(commandName)
|
|
87
|
+
const isInPlanningMode = planMode.isInPlanningMode(metadataContext.projectId)
|
|
88
|
+
|
|
89
|
+
// Start planning mode if required and not already in it
|
|
90
|
+
let activePlan = null
|
|
91
|
+
if (requiresPlanning && !isInPlanningMode && !params.skipPlanning) {
|
|
92
|
+
activePlan = planMode.startPlanning(metadataContext.projectId, commandName, params)
|
|
93
|
+
} else if (isInPlanningMode) {
|
|
94
|
+
activePlan = planMode.getActivePlan(metadataContext.projectId)
|
|
95
|
+
}
|
|
34
96
|
|
|
35
|
-
|
|
97
|
+
// 2.6. GROUND TRUTH: Verify actual state before critical operations
|
|
98
|
+
let groundTruthResult = null
|
|
99
|
+
if (groundTruth.requiresVerification(commandName)) {
|
|
100
|
+
const preState = await contextBuilder.loadStateForCommand(metadataContext, commandName)
|
|
101
|
+
groundTruthResult = await groundTruth.verify(commandName, metadataContext, preState)
|
|
102
|
+
|
|
103
|
+
// Log warnings but don't block (user can override)
|
|
104
|
+
if (!groundTruthResult.verified && groundTruthResult.warnings.length > 0) {
|
|
105
|
+
console.log(groundTruth.formatWarnings(groundTruthResult))
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 2.7. THINK BLOCKS (P3.1): Dynamic reasoning based on triggers
|
|
110
|
+
// ANTI-HALLUCINATION FIX: Load state BEFORE using it (was undefined)
|
|
111
|
+
let thinkBlock = null
|
|
112
|
+
const preThinkState = groundTruthResult?.actual || await contextBuilder.loadStateForCommand(metadataContext, commandName)
|
|
113
|
+
const thinkTrigger = thinkBlocks.detectTrigger(commandName, metadataContext, preThinkState)
|
|
114
|
+
if (thinkTrigger) {
|
|
115
|
+
thinkBlock = await thinkBlocks.generate(thinkTrigger, commandName, metadataContext, preThinkState)
|
|
116
|
+
|
|
117
|
+
// Log think block if in debug mode
|
|
118
|
+
if (process.env.PRJCT_DEBUG === 'true') {
|
|
119
|
+
console.log(thinkBlocks.format(thinkBlock, true))
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 2.8. CHAIN OF THOUGHT: Reasoning for critical commands
|
|
124
|
+
let reasoning = null
|
|
125
|
+
if (chainOfThought.requiresReasoning(commandName)) {
|
|
126
|
+
// Load state for reasoning
|
|
127
|
+
const reasoningState = await contextBuilder.loadStateForCommand(metadataContext, commandName)
|
|
128
|
+
reasoning = await chainOfThought.reason(commandName, metadataContext, reasoningState)
|
|
129
|
+
|
|
130
|
+
// If reasoning shows critical issues, warn but continue
|
|
131
|
+
if (reasoning.reasoning && !reasoning.reasoning.allPassed) {
|
|
132
|
+
console.log('⚠️ Chain of Thought detected issues:')
|
|
133
|
+
console.log(chainOfThought.formatPlan(reasoning))
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 3. CRITICAL: Force agent assignment for ALL task-related commands
|
|
138
|
+
const requiresAgent = template.metadata?.['required-agent'] !== false &&
|
|
139
|
+
(template.metadata?.['required-agent'] === true ||
|
|
140
|
+
this.isTaskCommand(commandName) ||
|
|
141
|
+
this.shouldUseAgent(commandName))
|
|
142
|
+
|
|
143
|
+
let context = metadataContext
|
|
36
144
|
let assignedAgent = null
|
|
37
145
|
|
|
146
|
+
// MANDATORY: Assign specialized agent for task commands
|
|
38
147
|
if (requiresAgent) {
|
|
39
|
-
// 4.
|
|
148
|
+
// 4. Create task object for analysis
|
|
40
149
|
const task = {
|
|
41
150
|
description: params.task || params.description || commandName,
|
|
42
151
|
type: commandName
|
|
43
152
|
}
|
|
44
153
|
|
|
154
|
+
// 5. LAZY CONTEXT: Analyze task FIRST, then estimate files needed
|
|
155
|
+
// This avoids reading all files before knowing what we need
|
|
45
156
|
const agentAssignment = await this.agentRouter.executeTask(
|
|
46
157
|
task,
|
|
47
|
-
|
|
158
|
+
metadataContext, // Only metadata, no files yet
|
|
48
159
|
projectPath
|
|
49
160
|
)
|
|
50
161
|
|
|
51
162
|
assignedAgent = agentAssignment.agent
|
|
163
|
+
const taskAnalysis = agentAssignment.taskAnalysis
|
|
164
|
+
|
|
165
|
+
// Validate agent was assigned
|
|
166
|
+
if (!assignedAgent || !assignedAgent.name) {
|
|
167
|
+
throw new Error(
|
|
168
|
+
`CRITICAL: Failed to assign agent for command "${commandName}". ` +
|
|
169
|
+
`System requires ALL task commands to use specialized agents.`
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 6. PRE-FILTER: Estimate which files are needed BEFORE reading
|
|
174
|
+
if (!this.contextEstimator) {
|
|
175
|
+
this.contextEstimator = new ContextEstimator()
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const estimatedFiles = await this.contextEstimator.estimateFiles(
|
|
179
|
+
taskAnalysis,
|
|
180
|
+
projectPath
|
|
181
|
+
)
|
|
52
182
|
|
|
53
|
-
//
|
|
183
|
+
// 7. Build context ONLY with estimated files (lazy loading)
|
|
54
184
|
const filtered = await this.contextFilter.filterForAgent(
|
|
55
185
|
assignedAgent,
|
|
56
186
|
task,
|
|
57
187
|
projectPath,
|
|
58
|
-
|
|
188
|
+
{
|
|
189
|
+
...metadataContext,
|
|
190
|
+
estimatedFiles, // Pre-filtered file list
|
|
191
|
+
fileCount: estimatedFiles.length
|
|
192
|
+
}
|
|
59
193
|
)
|
|
60
194
|
|
|
61
195
|
context = {
|
|
62
196
|
...filtered,
|
|
63
197
|
agent: assignedAgent,
|
|
64
|
-
originalSize:
|
|
198
|
+
originalSize: estimatedFiles.length, // Estimated, not actual full size
|
|
65
199
|
filteredSize: filtered.files?.length || 0,
|
|
66
|
-
reduction: filtered.metrics?.reductionPercent || 0
|
|
200
|
+
reduction: filtered.metrics?.reductionPercent || 0,
|
|
201
|
+
lazyLoaded: true // Flag indicating lazy loading was used
|
|
67
202
|
}
|
|
68
203
|
}
|
|
69
204
|
|
|
70
205
|
// 6. Load state with filtered context
|
|
71
|
-
const
|
|
206
|
+
const rawState = await contextBuilder.loadState(context)
|
|
207
|
+
|
|
208
|
+
// 6.5. SEMANTIC COMPRESSION: Compress state for reduced token usage
|
|
209
|
+
const compressedState = {}
|
|
210
|
+
for (const [key, content] of Object.entries(rawState)) {
|
|
211
|
+
if (content) {
|
|
212
|
+
const compressed = semanticCompression.compress(content, key)
|
|
213
|
+
compressedState[key] = {
|
|
214
|
+
raw: content,
|
|
215
|
+
summary: compressed.summary,
|
|
216
|
+
compressed
|
|
217
|
+
}
|
|
218
|
+
} else {
|
|
219
|
+
compressedState[key] = { raw: null, summary: 'Empty', compressed: null }
|
|
220
|
+
}
|
|
221
|
+
}
|
|
72
222
|
|
|
73
|
-
//
|
|
74
|
-
const
|
|
223
|
+
// Use compressed summaries for prompt, keep raw for tool execution
|
|
224
|
+
const state = {
|
|
225
|
+
...rawState,
|
|
226
|
+
_compressed: compressedState,
|
|
227
|
+
_compressionMetrics: semanticCompression.getMetrics()
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// 7. MEMORY: Load learned patterns AND relevant memories for this command
|
|
231
|
+
let learnedPatterns = null
|
|
232
|
+
let relevantMemories = null
|
|
233
|
+
if (context.projectId) {
|
|
234
|
+
learnedPatterns = {
|
|
235
|
+
commit_footer: await memorySystem.getSmartDecision(context.projectId, 'commit_footer'),
|
|
236
|
+
branch_naming: await memorySystem.getSmartDecision(context.projectId, 'branch_naming'),
|
|
237
|
+
test_before_ship: await memorySystem.getSmartDecision(context.projectId, 'test_before_ship'),
|
|
238
|
+
preferred_agent: await memorySystem.getSmartDecision(context.projectId, `preferred_agent_${commandName}`)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// P3.3: Get relevant memories for context
|
|
242
|
+
relevantMemories = await memorySystem.getRelevantMemories(
|
|
243
|
+
context.projectId,
|
|
244
|
+
{ commandName, params },
|
|
245
|
+
5 // Top 5 relevant memories
|
|
246
|
+
)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// 9. Build prompt with agent assignment, learned patterns, think blocks, memories, AND plan mode
|
|
250
|
+
const planInfo = {
|
|
251
|
+
isPlanning: requiresPlanning || isInPlanningMode,
|
|
252
|
+
requiresApproval: isDestructive && !params.approved,
|
|
253
|
+
active: activePlan,
|
|
254
|
+
allowedTools: planMode.getAllowedTools(
|
|
255
|
+
isInPlanningMode,
|
|
256
|
+
template.frontmatter['allowed-tools'] || []
|
|
257
|
+
)
|
|
258
|
+
}
|
|
259
|
+
const prompt = promptBuilder.build(template, context, state, assignedAgent, learnedPatterns, thinkBlock, relevantMemories, planInfo)
|
|
75
260
|
|
|
76
261
|
// 8. Log agent usage
|
|
77
262
|
if (assignedAgent) {
|
|
@@ -79,6 +264,9 @@ class CommandExecutor {
|
|
|
79
264
|
console.log(`📉 Context reduced by: ${context.reduction}%`)
|
|
80
265
|
}
|
|
81
266
|
|
|
267
|
+
// Record successful attempt
|
|
268
|
+
loopDetector.recordSuccess(commandName, loopContext)
|
|
269
|
+
|
|
82
270
|
return {
|
|
83
271
|
success: true,
|
|
84
272
|
template,
|
|
@@ -86,12 +274,89 @@ class CommandExecutor {
|
|
|
86
274
|
state,
|
|
87
275
|
prompt,
|
|
88
276
|
assignedAgent,
|
|
89
|
-
contextReduction: context.reduction
|
|
277
|
+
contextReduction: context.reduction,
|
|
278
|
+
reasoning, // Chain of thought results
|
|
279
|
+
thinkBlock, // Think blocks (P3.1)
|
|
280
|
+
groundTruth: groundTruthResult, // Ground truth verification (P1.3)
|
|
281
|
+
compressionMetrics: state._compressionMetrics,
|
|
282
|
+
learnedPatterns, // Memory system patterns
|
|
283
|
+
relevantMemories, // P3.3: Semantic memories
|
|
284
|
+
// Response formatter helper
|
|
285
|
+
formatResponse: (data) => responseTemplates.format(commandName, data),
|
|
286
|
+
// Think block formatter helper
|
|
287
|
+
formatThinkBlock: (verbose) => thinkBlocks.format(thinkBlock, verbose),
|
|
288
|
+
// P3.2: Parallel tools helper
|
|
289
|
+
parallel: {
|
|
290
|
+
execute: (toolCalls) => parallelTools.execute(toolCalls),
|
|
291
|
+
readAll: (paths) => parallelTools.readAll(paths),
|
|
292
|
+
canParallelize: (tools) => parallelTools.canParallelize(tools),
|
|
293
|
+
getMetrics: () => parallelTools.getMetrics()
|
|
294
|
+
},
|
|
295
|
+
// P3.3: Memory system helpers
|
|
296
|
+
memory: {
|
|
297
|
+
create: (memory) => memorySystem.createMemory(context.projectId, memory),
|
|
298
|
+
autoRemember: (type, value, ctx) => memorySystem.autoRemember(context.projectId, type, value, ctx),
|
|
299
|
+
search: (query) => memorySystem.searchMemories(context.projectId, query),
|
|
300
|
+
findByTags: (tags) => memorySystem.findByTags(context.projectId, tags),
|
|
301
|
+
getStats: () => memorySystem.getMemoryStats(context.projectId)
|
|
302
|
+
},
|
|
303
|
+
// P3.4: Plan Mode helpers
|
|
304
|
+
plan: {
|
|
305
|
+
active: activePlan,
|
|
306
|
+
isPlanning: requiresPlanning || isInPlanningMode,
|
|
307
|
+
isDestructive,
|
|
308
|
+
requiresApproval: isDestructive && !params.approved,
|
|
309
|
+
// Planning phase methods
|
|
310
|
+
recordInfo: (info) => planMode.recordGatheredInfo(context.projectId, info),
|
|
311
|
+
setAnalysis: (analysis) => planMode.setAnalysis(context.projectId, analysis),
|
|
312
|
+
propose: (plan) => planMode.proposePlan(context.projectId, plan),
|
|
313
|
+
// Approval methods
|
|
314
|
+
approve: (feedback) => planMode.approvePlan(context.projectId, feedback),
|
|
315
|
+
reject: (reason) => planMode.rejectPlan(context.projectId, reason),
|
|
316
|
+
getApprovalPrompt: () => planMode.generateApprovalPrompt(commandName, context),
|
|
317
|
+
// Execution methods
|
|
318
|
+
startExecution: () => planMode.startExecution(context.projectId),
|
|
319
|
+
getNextStep: () => planMode.getNextStep(context.projectId),
|
|
320
|
+
completeStep: (result) => planMode.completeStep(context.projectId, result),
|
|
321
|
+
failStep: (error) => planMode.failStep(context.projectId, error),
|
|
322
|
+
abort: (reason) => planMode.abortPlan(context.projectId, reason),
|
|
323
|
+
// Status
|
|
324
|
+
getStatus: () => planMode.formatStatus(context.projectId),
|
|
325
|
+
getAllowedTools: () => planMode.getAllowedTools(
|
|
326
|
+
isInPlanningMode,
|
|
327
|
+
template.frontmatter['allowed-tools'] || []
|
|
328
|
+
)
|
|
329
|
+
}
|
|
330
|
+
// P3.5, P3.6: DELEGATED TO CLAUDE CODE
|
|
331
|
+
// Use Claude Code's native tools instead:
|
|
332
|
+
// - Grep for semantic search
|
|
333
|
+
// - Glob for file patterns
|
|
334
|
+
// - Native LSP for code intelligence
|
|
90
335
|
}
|
|
91
336
|
} catch (error) {
|
|
337
|
+
// Record failed attempt for loop detection
|
|
338
|
+
const attemptInfo = loopDetector.recordAttempt(commandName, loopContext, {
|
|
339
|
+
success: false,
|
|
340
|
+
error: error.message
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
// Check if we should escalate after this failure
|
|
344
|
+
if (attemptInfo.shouldEscalate) {
|
|
345
|
+
const escalation = loopDetector.getEscalationInfo(commandName, loopContext)
|
|
346
|
+
return {
|
|
347
|
+
success: false,
|
|
348
|
+
error: escalation.message,
|
|
349
|
+
escalation,
|
|
350
|
+
isLoopDetected: true,
|
|
351
|
+
suggestion: escalation.suggestion
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
92
355
|
return {
|
|
93
356
|
success: false,
|
|
94
357
|
error: error.message,
|
|
358
|
+
attemptNumber: attemptInfo.attemptNumber,
|
|
359
|
+
isLooping: attemptInfo.isLooping
|
|
95
360
|
}
|
|
96
361
|
}
|
|
97
362
|
}
|
|
@@ -100,10 +365,27 @@ class CommandExecutor {
|
|
|
100
365
|
* Check if command is task-related
|
|
101
366
|
*/
|
|
102
367
|
isTaskCommand(commandName) {
|
|
103
|
-
const taskCommands = [
|
|
368
|
+
const taskCommands = [
|
|
369
|
+
'work', 'now', 'build', 'feature', 'bug', 'done',
|
|
370
|
+
'task', 'design', 'cleanup', 'fix', 'test'
|
|
371
|
+
]
|
|
104
372
|
return taskCommands.includes(commandName)
|
|
105
373
|
}
|
|
106
374
|
|
|
375
|
+
/**
|
|
376
|
+
* Determine if command should use an agent
|
|
377
|
+
* Expanded list of commands that benefit from agent specialization
|
|
378
|
+
*/
|
|
379
|
+
shouldUseAgent(commandName) {
|
|
380
|
+
// Commands that should ALWAYS use agents
|
|
381
|
+
const agentCommands = [
|
|
382
|
+
'work', 'now', 'build', 'feature', 'bug', 'done',
|
|
383
|
+
'task', 'design', 'cleanup', 'fix', 'test',
|
|
384
|
+
'sync', 'analyze' // These analyze/modify code, need specialization
|
|
385
|
+
]
|
|
386
|
+
return agentCommands.includes(commandName)
|
|
387
|
+
}
|
|
388
|
+
|
|
107
389
|
/**
|
|
108
390
|
* Execute tool with permission check
|
|
109
391
|
* @param {string} toolName - Tool name
|
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
* Context Builder
|
|
3
3
|
* Builds project context for Claude to make decisions
|
|
4
4
|
* NO if/else logic - just data collection
|
|
5
|
+
*
|
|
6
|
+
* OPTIMIZATION (P0.1): Smart Context Caching
|
|
7
|
+
* - Parallel file reads with Promise.all()
|
|
8
|
+
* - Session-based caching to avoid redundant reads
|
|
9
|
+
* - Selective loading based on command needs
|
|
10
|
+
*
|
|
11
|
+
* Source: Windsurf, Cursor patterns
|
|
5
12
|
*/
|
|
6
13
|
|
|
7
14
|
const fs = require('fs').promises
|
|
@@ -9,6 +16,28 @@ const pathManager = require('../infrastructure/path-manager')
|
|
|
9
16
|
const configManager = require('../infrastructure/config-manager')
|
|
10
17
|
|
|
11
18
|
class ContextBuilder {
|
|
19
|
+
constructor() {
|
|
20
|
+
// Session cache - cleared between commands or after timeout
|
|
21
|
+
this._cache = new Map()
|
|
22
|
+
// ANTI-HALLUCINATION: Reduced from 30s to 5s to prevent stale data
|
|
23
|
+
this._cacheTimeout = 5000 // 5 seconds (was 30s - caused stale context issues)
|
|
24
|
+
this._lastCacheTime = null
|
|
25
|
+
// Track file modification times for additional staleness detection
|
|
26
|
+
this._mtimes = new Map()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Clear cache if stale or force clear
|
|
31
|
+
* @param {boolean} force - Force clear regardless of timeout
|
|
32
|
+
*/
|
|
33
|
+
_clearCacheIfStale(force = false) {
|
|
34
|
+
if (force || !this._lastCacheTime ||
|
|
35
|
+
Date.now() - this._lastCacheTime > this._cacheTimeout) {
|
|
36
|
+
this._cache.clear()
|
|
37
|
+
this._lastCacheTime = Date.now()
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
12
41
|
/**
|
|
13
42
|
* Build full project context for Claude
|
|
14
43
|
* @param {string} projectPath - Local project path
|
|
@@ -34,7 +63,9 @@ class ContextBuilder {
|
|
|
34
63
|
metrics: pathManager.getFilePath(projectId, 'progress', 'metrics.md'),
|
|
35
64
|
ideas: pathManager.getFilePath(projectId, 'planning', 'ideas.md'),
|
|
36
65
|
roadmap: pathManager.getFilePath(projectId, 'planning', 'roadmap.md'),
|
|
66
|
+
specs: pathManager.getFilePath(projectId, 'planning', 'specs'),
|
|
37
67
|
memory: pathManager.getFilePath(projectId, 'memory', 'context.jsonl'),
|
|
68
|
+
patterns: pathManager.getFilePath(projectId, 'memory', 'patterns.json'),
|
|
38
69
|
analysis: pathManager.getFilePath(projectId, 'analysis', 'repo-summary.md'),
|
|
39
70
|
},
|
|
40
71
|
|
|
@@ -49,25 +80,182 @@ class ContextBuilder {
|
|
|
49
80
|
}
|
|
50
81
|
|
|
51
82
|
/**
|
|
52
|
-
* Load current project state
|
|
83
|
+
* Load current project state - PARALLEL VERSION
|
|
84
|
+
* Uses Promise.all() for 40-60% faster file I/O
|
|
85
|
+
*
|
|
53
86
|
* @param {Object} context - Context from build()
|
|
87
|
+
* @param {string[]} onlyKeys - Optional: only load specific keys (selective loading)
|
|
54
88
|
* @returns {Promise<Object>} Current state
|
|
55
89
|
*/
|
|
56
|
-
async loadState(context) {
|
|
90
|
+
async loadState(context, onlyKeys = null) {
|
|
91
|
+
this._clearCacheIfStale()
|
|
92
|
+
|
|
57
93
|
const state = {}
|
|
94
|
+
const entries = Object.entries(context.paths)
|
|
95
|
+
|
|
96
|
+
// Filter to only requested keys if specified
|
|
97
|
+
const filteredEntries = onlyKeys
|
|
98
|
+
? entries.filter(([key]) => onlyKeys.includes(key))
|
|
99
|
+
: entries
|
|
100
|
+
|
|
101
|
+
// ANTI-HALLUCINATION: Verify mtime before trusting cache
|
|
102
|
+
// Files can change between commands - stale cache causes hallucinations
|
|
103
|
+
for (const [, filePath] of filteredEntries) {
|
|
104
|
+
if (this._cache.has(filePath)) {
|
|
105
|
+
try {
|
|
106
|
+
const stat = await fs.stat(filePath)
|
|
107
|
+
const cachedMtime = this._mtimes.get(filePath)
|
|
108
|
+
if (!cachedMtime || stat.mtimeMs > cachedMtime) {
|
|
109
|
+
// File changed since cached - invalidate
|
|
110
|
+
this._cache.delete(filePath)
|
|
111
|
+
this._mtimes.delete(filePath)
|
|
112
|
+
}
|
|
113
|
+
} catch {
|
|
114
|
+
// File doesn't exist - invalidate cache
|
|
115
|
+
this._cache.delete(filePath)
|
|
116
|
+
this._mtimes.delete(filePath)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Separate cached vs uncached files
|
|
122
|
+
const uncachedEntries = []
|
|
123
|
+
for (const [key, filePath] of filteredEntries) {
|
|
124
|
+
if (this._cache.has(filePath)) {
|
|
125
|
+
state[key] = this._cache.get(filePath)
|
|
126
|
+
} else {
|
|
127
|
+
uncachedEntries.push([key, filePath])
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// PARALLEL READ: All uncached files at once
|
|
132
|
+
if (uncachedEntries.length > 0) {
|
|
133
|
+
const readPromises = uncachedEntries.map(async ([key, filePath]) => {
|
|
134
|
+
try {
|
|
135
|
+
const [content, stat] = await Promise.all([
|
|
136
|
+
fs.readFile(filePath, 'utf-8'),
|
|
137
|
+
fs.stat(filePath)
|
|
138
|
+
])
|
|
139
|
+
return { key, filePath, content, mtime: stat.mtimeMs }
|
|
140
|
+
} catch {
|
|
141
|
+
return { key, filePath, content: null, mtime: null }
|
|
142
|
+
}
|
|
143
|
+
})
|
|
58
144
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
145
|
+
const results = await Promise.all(readPromises)
|
|
146
|
+
|
|
147
|
+
// Populate state and cache (with mtime for anti-hallucination)
|
|
148
|
+
for (const { key, filePath, content, mtime } of results) {
|
|
149
|
+
state[key] = content
|
|
150
|
+
this._cache.set(filePath, content)
|
|
151
|
+
if (mtime) {
|
|
152
|
+
this._mtimes.set(filePath, mtime)
|
|
153
|
+
}
|
|
65
154
|
}
|
|
66
155
|
}
|
|
67
156
|
|
|
68
157
|
return state
|
|
69
158
|
}
|
|
70
159
|
|
|
160
|
+
/**
|
|
161
|
+
* Load state for specific command - optimized selective loading
|
|
162
|
+
* Each command only loads what it needs
|
|
163
|
+
*
|
|
164
|
+
* @param {Object} context - Context from build()
|
|
165
|
+
* @param {string} commandName - Command name for selective loading
|
|
166
|
+
* @returns {Promise<Object>} Current state (filtered)
|
|
167
|
+
*/
|
|
168
|
+
async loadStateForCommand(context, commandName) {
|
|
169
|
+
// Command-specific file requirements
|
|
170
|
+
// Minimizes context window usage
|
|
171
|
+
const commandFileMap = {
|
|
172
|
+
// Core workflow
|
|
173
|
+
'now': ['now', 'next'],
|
|
174
|
+
'done': ['now', 'next', 'metrics'],
|
|
175
|
+
'next': ['next'],
|
|
176
|
+
|
|
177
|
+
// Progress
|
|
178
|
+
'ship': ['now', 'shipped', 'metrics'],
|
|
179
|
+
'recap': ['shipped', 'metrics', 'now'],
|
|
180
|
+
'progress': ['shipped', 'metrics'],
|
|
181
|
+
|
|
182
|
+
// Planning
|
|
183
|
+
'idea': ['ideas', 'next'],
|
|
184
|
+
'feature': ['roadmap', 'next', 'ideas'],
|
|
185
|
+
'roadmap': ['roadmap'],
|
|
186
|
+
'spec': ['roadmap', 'next', 'specs'],
|
|
187
|
+
|
|
188
|
+
// Analysis
|
|
189
|
+
'analyze': ['analysis', 'context'],
|
|
190
|
+
'sync': ['analysis', 'context', 'now'],
|
|
191
|
+
|
|
192
|
+
// All files (fallback)
|
|
193
|
+
'default': null // null means load all
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const requiredFiles = commandFileMap[commandName] || commandFileMap.default
|
|
197
|
+
return this.loadState(context, requiredFiles)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Batch read multiple files in parallel
|
|
202
|
+
* Utility for custom file sets
|
|
203
|
+
*
|
|
204
|
+
* @param {string[]} filePaths - Array of file paths
|
|
205
|
+
* @returns {Promise<Map<string, string|null>>} Map of path -> content
|
|
206
|
+
*/
|
|
207
|
+
async batchRead(filePaths) {
|
|
208
|
+
this._clearCacheIfStale()
|
|
209
|
+
|
|
210
|
+
const results = new Map()
|
|
211
|
+
const uncachedPaths = []
|
|
212
|
+
|
|
213
|
+
// Check cache first
|
|
214
|
+
for (const filePath of filePaths) {
|
|
215
|
+
if (this._cache.has(filePath)) {
|
|
216
|
+
results.set(filePath, this._cache.get(filePath))
|
|
217
|
+
} else {
|
|
218
|
+
uncachedPaths.push(filePath)
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Parallel read uncached
|
|
223
|
+
if (uncachedPaths.length > 0) {
|
|
224
|
+
const readPromises = uncachedPaths.map(async (filePath) => {
|
|
225
|
+
try {
|
|
226
|
+
const content = await fs.readFile(filePath, 'utf-8')
|
|
227
|
+
return { filePath, content }
|
|
228
|
+
} catch {
|
|
229
|
+
return { filePath, content: null }
|
|
230
|
+
}
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
const readResults = await Promise.all(readPromises)
|
|
234
|
+
|
|
235
|
+
for (const { filePath, content } of readResults) {
|
|
236
|
+
results.set(filePath, content)
|
|
237
|
+
this._cache.set(filePath, content)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return results
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Invalidate cache for specific file (after write)
|
|
246
|
+
* @param {string} filePath - File that was written
|
|
247
|
+
*/
|
|
248
|
+
invalidateCache(filePath) {
|
|
249
|
+
this._cache.delete(filePath)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Force clear entire cache
|
|
254
|
+
*/
|
|
255
|
+
clearCache() {
|
|
256
|
+
this._clearCacheIfStale(true)
|
|
257
|
+
}
|
|
258
|
+
|
|
71
259
|
/**
|
|
72
260
|
* Check file existence
|
|
73
261
|
* @param {string} filePath - File path
|
|
@@ -81,6 +269,18 @@ class ContextBuilder {
|
|
|
81
269
|
return false
|
|
82
270
|
}
|
|
83
271
|
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Get cache stats (for debugging/metrics)
|
|
275
|
+
* @returns {Object} Cache statistics
|
|
276
|
+
*/
|
|
277
|
+
getCacheStats() {
|
|
278
|
+
return {
|
|
279
|
+
size: this._cache.size,
|
|
280
|
+
lastRefresh: this._lastCacheTime,
|
|
281
|
+
timeout: this._cacheTimeout
|
|
282
|
+
}
|
|
283
|
+
}
|
|
84
284
|
}
|
|
85
285
|
|
|
86
286
|
module.exports = new ContextBuilder()
|