prjct-cli 0.10.0 → 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 (43) hide show
  1. package/CHANGELOG.md +31 -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/chain-of-thought.js +578 -0
  5. package/core/agentic/command-executor.js +238 -4
  6. package/core/agentic/context-builder.js +208 -8
  7. package/core/agentic/ground-truth.js +591 -0
  8. package/core/agentic/loop-detector.js +406 -0
  9. package/core/agentic/memory-system.js +850 -0
  10. package/core/agentic/parallel-tools.js +366 -0
  11. package/core/agentic/plan-mode.js +572 -0
  12. package/core/agentic/prompt-builder.js +76 -1
  13. package/core/agentic/response-templates.js +290 -0
  14. package/core/agentic/semantic-compression.js +517 -0
  15. package/core/agentic/think-blocks.js +657 -0
  16. package/core/agentic/tool-registry.js +32 -0
  17. package/core/agentic/validation-rules.js +380 -0
  18. package/core/command-registry.js +48 -0
  19. package/core/commands.js +43 -1
  20. package/core/context-sync.js +183 -0
  21. package/package.json +7 -15
  22. package/templates/commands/done.md +7 -0
  23. package/templates/commands/feature.md +8 -0
  24. package/templates/commands/ship.md +8 -0
  25. package/templates/commands/spec.md +128 -0
  26. package/templates/global/CLAUDE.md +17 -0
  27. package/core/__tests__/agentic/agent-router.test.js +0 -398
  28. package/core/__tests__/agentic/command-executor.test.js +0 -223
  29. package/core/__tests__/agentic/context-builder.test.js +0 -160
  30. package/core/__tests__/agentic/context-filter.test.js +0 -494
  31. package/core/__tests__/agentic/prompt-builder.test.js +0 -204
  32. package/core/__tests__/agentic/template-loader.test.js +0 -164
  33. package/core/__tests__/agentic/tool-registry.test.js +0 -243
  34. package/core/__tests__/domain/agent-generator.test.js +0 -289
  35. package/core/__tests__/domain/agent-loader.test.js +0 -179
  36. package/core/__tests__/domain/analyzer.test.js +0 -324
  37. package/core/__tests__/infrastructure/author-detector.test.js +0 -103
  38. package/core/__tests__/infrastructure/config-manager.test.js +0 -454
  39. package/core/__tests__/infrastructure/path-manager.test.js +0 -412
  40. package/core/__tests__/setup.test.js +0 -15
  41. package/core/__tests__/utils/date-helper.test.js +0 -169
  42. package/core/__tests__/utils/file-helper.test.js +0 -258
  43. 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')
@@ -11,6 +22,20 @@ const toolRegistry = require('./tool-registry')
11
22
  const MandatoryAgentRouter = require('./agent-router')
12
23
  const ContextFilter = require('./context-filter')
13
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
14
39
 
15
40
  class CommandExecutor {
16
41
  constructor() {
@@ -23,6 +48,21 @@ class CommandExecutor {
23
48
  * Execute command with MANDATORY agent assignment
24
49
  */
25
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
+
26
66
  try {
27
67
  // 1. Load template
28
68
  const template = await templateLoader.load(commandName)
@@ -30,6 +70,70 @@ class CommandExecutor {
30
70
  // 2. Build METADATA context only (lazy loading - no file reads yet)
31
71
  const metadataContext = await contextBuilder.build(projectPath, params)
32
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
+ }
83
+
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
+ }
96
+
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
+
33
137
  // 3. CRITICAL: Force agent assignment for ALL task-related commands
34
138
  const requiresAgent = template.metadata?.['required-agent'] !== false &&
35
139
  (template.metadata?.['required-agent'] === true ||
@@ -99,10 +203,60 @@ class CommandExecutor {
99
203
  }
100
204
 
101
205
  // 6. Load state with filtered context
102
- const state = await contextBuilder.loadState(context)
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
+ }
222
+
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
+ }
103
248
 
104
- // 7. Build prompt with agent assignment
105
- const prompt = promptBuilder.build(template, context, state, assignedAgent)
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)
106
260
 
107
261
  // 8. Log agent usage
108
262
  if (assignedAgent) {
@@ -110,6 +264,9 @@ class CommandExecutor {
110
264
  console.log(`📉 Context reduced by: ${context.reduction}%`)
111
265
  }
112
266
 
267
+ // Record successful attempt
268
+ loopDetector.recordSuccess(commandName, loopContext)
269
+
113
270
  return {
114
271
  success: true,
115
272
  template,
@@ -117,12 +274,89 @@ class CommandExecutor {
117
274
  state,
118
275
  prompt,
119
276
  assignedAgent,
120
- 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
121
335
  }
122
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
+
123
355
  return {
124
356
  success: false,
125
357
  error: error.message,
358
+ attemptNumber: attemptInfo.attemptNumber,
359
+ isLooping: attemptInfo.isLooping
126
360
  }
127
361
  }
128
362
  }
@@ -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
- // Read all core files
60
- for (const [key, filePath] of Object.entries(context.paths)) {
61
- try {
62
- state[key] = await fs.readFile(filePath, 'utf-8')
63
- } catch {
64
- state[key] = null
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()