smart-context-mcp 1.0.4 → 1.2.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.
package/src/metrics.js CHANGED
@@ -129,9 +129,10 @@ const appendLegacyMetricsFile = async (entry) => {
129
129
  };
130
130
 
131
131
  export const persistMetrics = async (entry) => {
132
+ let enrichedEntry = entry;
133
+
132
134
  try {
133
135
  const resolvedInput = resolveMetricsInput();
134
- let enrichedEntry = entry;
135
136
  const safety = getSqliteSafetyPolicy();
136
137
 
137
138
  if (!safety.shouldBlock) {
@@ -146,7 +147,11 @@ export const persistMetrics = async (entry) => {
146
147
  }
147
148
  });
148
149
  }
150
+ } catch {
151
+ // best-effort — never fail a tool call for metrics
152
+ }
149
153
 
154
+ try {
150
155
  await appendLegacyMetricsFile(enrichedEntry);
151
156
  } catch {
152
157
  // best-effort — never fail a tool call for metrics
package/src/server.js CHANGED
@@ -12,6 +12,21 @@ import { smartSummary } from './tools/smart-summary.js';
12
12
  import { smartMetrics } from './tools/smart-metrics.js';
13
13
  import { smartTurn } from './tools/smart-turn.js';
14
14
  import { projectRoot, projectRootSource } from './utils/paths.js';
15
+ import { setServerForStreaming } from './streaming.js';
16
+ import {
17
+ getSymbolBlame,
18
+ getFileAuthorshipStats,
19
+ findSymbolsByAuthor,
20
+ getRecentlyModifiedSymbols
21
+ } from './git-blame.js';
22
+ import {
23
+ discoverRelatedProjects,
24
+ searchAcrossProjects,
25
+ readAcrossProjects,
26
+ findSymbolAcrossProjects,
27
+ getCrossProjectDependencies,
28
+ getCrossProjectStats,
29
+ } from './cross-project.js';
15
30
 
16
31
  const require = createRequire(import.meta.url);
17
32
  const { version } = require('../package.json');
@@ -31,6 +46,9 @@ export const createDevctxServer = () => {
31
46
  version,
32
47
  });
33
48
 
49
+ // Enable streaming progress notifications
50
+ setServerForStreaming(server);
51
+
34
52
  server.tool(
35
53
  'smart_read',
36
54
  'Read a file with token-efficient modes. outline/signatures: compact structure (~90% savings). range: specific line range with line numbers. symbol: extract function/class/method by name (string or array for batch). full: file content capped at 12k chars. maxTokens: token budget — auto-selects the most detailed mode that fits (full -> outline -> signatures -> truncated). context=true (symbol mode only): includes callers, tests, and referenced types from the dependency graph; returns graphCoverage (imports/tests: full|partial|none) so the agent knows how reliable the cross-file context is. Responses are cached in memory per session and invalidated by file mtime; cached=true when served from cache. Every response includes a unified confidence block: { parser, truncated, cached, graphCoverage? }. Supports JS/TS, Python, Go, Rust, Java, C#, Kotlin, PHP, Swift, shell, Terraform, Dockerfile, SQL, JSON, TOML, YAML.',
@@ -78,7 +96,7 @@ export const createDevctxServer = () => {
78
96
 
79
97
  server.tool(
80
98
  'smart_context',
81
- 'Get curated context for a task in one call. Combines smart_search + smart_read + graph expansion. Returns relevant files, evidence for why each file was included, related tests, dependencies, symbol previews from the index, and symbol details — optimized for tokens. Includes a unified confidence block: { indexFreshness, graphCoverage } indicating index state and how complete the relational context is. Replaces the manual search → read → read cycle. Optional intent override, token budget, diff mode (pass diff=true for HEAD or diff="main" to scope context to changed files only), detail mode (minimal=index+signatures+snippets, balanced=default, deep=full content), and include array to control which fields are returned (["content","graph","hints","symbolDetail"]).',
99
+ 'Get curated context for a task in one call. Combines smart_search + smart_read + graph expansion. Returns relevant files, evidence for why each file was included, related tests, dependencies, symbol previews from the index, and symbol details — optimized for tokens. Includes a unified confidence block: { indexFreshness, graphCoverage } indicating index state and how complete the relational context is. Replaces the manual search → read → read cycle. Optional intent override, token budget, diff mode (pass diff=true for HEAD or diff="main" to scope context to changed files only), detail mode (minimal=index+signatures+snippets, balanced=default, deep=full content), include array to control which fields are returned (["content","graph","hints","symbolDetail"]), and prefetch=true to enable intelligent context prediction based on historical patterns (reduces round-trips by 40-60%).',
82
100
  {
83
101
  task: z.string(),
84
102
  intent: z.enum(['implementation', 'debug', 'tests', 'config', 'docs', 'explore']).optional(),
@@ -87,9 +105,10 @@ export const createDevctxServer = () => {
87
105
  diff: z.union([z.boolean(), z.string()]).optional(),
88
106
  detail: z.enum(['minimal', 'balanced', 'deep']).optional(),
89
107
  include: z.array(z.enum(['content', 'graph', 'hints', 'symbolDetail'])).optional(),
108
+ prefetch: z.boolean().optional(),
90
109
  },
91
- async ({ task, intent, maxTokens, entryFile, diff, detail, include }) =>
92
- asTextResult(await smartContext({ task, intent, maxTokens, entryFile, diff, detail, include })),
110
+ async ({ task, intent, maxTokens, entryFile, diff, detail, include, prefetch }) =>
111
+ asTextResult(await smartContext({ task, intent, maxTokens, entryFile, diff, detail, include, prefetch })),
93
112
  );
94
113
 
95
114
  server.tool(
@@ -103,23 +122,190 @@ export const createDevctxServer = () => {
103
122
 
104
123
  server.tool(
105
124
  'build_index',
106
- 'Build a lightweight symbol index for the project. Speeds up smart_search ranking and smart_read symbol lookups. Pass incremental=true to only reindex files with changed mtime (much faster for large repos). Without incremental, rebuilds from scratch.',
125
+ 'Build a lightweight symbol index for the project. Speeds up smart_search ranking and smart_read symbol lookups. Pass incremental=true to only reindex files with changed mtime (much faster for large repos). Pass warmCache=true to preload frequently accessed files after indexing. Without incremental, rebuilds from scratch. Sends progress notifications during indexing for large projects.',
107
126
  {
108
127
  incremental: z.boolean().optional(),
128
+ warmCache: z.boolean().optional(),
109
129
  },
110
- async ({ incremental }) => {
111
- if (incremental) {
112
- const { index, stats } = buildIndexIncremental(projectRoot);
130
+ async ({ incremental, warmCache }) => {
131
+ const { createProgressReporter } = await import('./streaming.js');
132
+ const progress = createProgressReporter('build_index');
133
+
134
+ try {
135
+ if (incremental) {
136
+ const { index, stats } = buildIndexIncremental(projectRoot, progress);
137
+ await persistIndex(index, projectRoot);
138
+ const symbolCount = Object.values(index.files).reduce((sum, f) => sum + f.symbols.length, 0);
139
+ const result = { status: 'ok', files: stats.total, symbols: symbolCount, ...stats };
140
+
141
+ if (warmCache) {
142
+ const { warmCache: warmCacheFn } = await import('./cache-warming.js');
143
+ const warmResult = await warmCacheFn(projectRoot, progress);
144
+ result.cacheWarming = warmResult;
145
+ }
146
+
147
+ progress.complete(result);
148
+ return asTextResult(result);
149
+ }
150
+
151
+ const index = buildIndex(projectRoot, progress);
113
152
  await persistIndex(index, projectRoot);
153
+ const fileCount = Object.keys(index.files).length;
114
154
  const symbolCount = Object.values(index.files).reduce((sum, f) => sum + f.symbols.length, 0);
115
- return asTextResult({ status: 'ok', files: stats.total, symbols: symbolCount, ...stats });
155
+ const result = { status: 'ok', files: fileCount, symbols: symbolCount };
156
+
157
+ if (warmCache) {
158
+ const { warmCache: warmCacheFn } = await import('./cache-warming.js');
159
+ const warmResult = await warmCacheFn(projectRoot, progress);
160
+ result.cacheWarming = warmResult;
161
+ }
162
+
163
+ progress.complete(result);
164
+ return asTextResult(result);
165
+ } catch (error) {
166
+ progress.error(error);
167
+ throw error;
168
+ }
169
+ },
170
+ );
171
+
172
+ server.tool(
173
+ 'warm_cache',
174
+ 'Preload frequently accessed files into OS cache to reduce cold-start latency. Analyzes last 30 days of access patterns and warms the top 50 most-used files (configurable via DEVCTX_WARM_FILES env). Skips files >1MB. Returns warmed/skipped counts. Use after build_index or before starting intensive work sessions.',
175
+ {},
176
+ async () => {
177
+ const { createProgressReporter } = await import('./streaming.js');
178
+ const { warmCache: warmCacheFn } = await import('./cache-warming.js');
179
+
180
+ const progress = createProgressReporter('warm_cache');
181
+
182
+ try {
183
+ const result = await warmCacheFn(projectRoot, progress);
184
+ progress.complete(result);
185
+ return asTextResult(result);
186
+ } catch (error) {
187
+ progress.error(error);
188
+ throw error;
116
189
  }
190
+ },
191
+ );
192
+
193
+ server.tool(
194
+ 'git_blame',
195
+ 'Get symbol-level git blame attribution. Modes: symbol (blame for specific file symbols), file (aggregated file stats), author (find symbols by author), recent (recently modified symbols). Returns author, email, date, commit, and authorship percentage for each symbol. Requires git repository and symbol index.',
196
+ {
197
+ mode: z.enum(['symbol', 'file', 'author', 'recent']),
198
+ filePath: z.string().optional(),
199
+ authorQuery: z.string().optional(),
200
+ limit: z.number().int().min(1).max(100).optional(),
201
+ daysBack: z.number().int().min(1).max(365).optional(),
202
+ },
203
+ async ({ mode, filePath, authorQuery, limit, daysBack }) => {
204
+ try {
205
+ if (mode === 'symbol') {
206
+ if (!filePath) {
207
+ throw new Error('filePath is required for symbol mode');
208
+ }
209
+ const result = await getSymbolBlame(filePath, projectRoot);
210
+ return asTextResult({ mode, filePath, symbols: result });
211
+ }
212
+
213
+ if (mode === 'file') {
214
+ if (!filePath) {
215
+ throw new Error('filePath is required for file mode');
216
+ }
217
+ const result = await getFileAuthorshipStats(filePath, projectRoot);
218
+ return asTextResult({ mode, filePath, ...result });
219
+ }
220
+
221
+ if (mode === 'author') {
222
+ if (!authorQuery) {
223
+ throw new Error('authorQuery is required for author mode');
224
+ }
225
+ const result = await findSymbolsByAuthor(authorQuery, projectRoot, limit || 50);
226
+ return asTextResult({ mode, authorQuery, matches: result.length, symbols: result });
227
+ }
117
228
 
118
- const index = buildIndex(projectRoot);
119
- await persistIndex(index, projectRoot);
120
- const fileCount = Object.keys(index.files).length;
121
- const symbolCount = Object.values(index.files).reduce((sum, f) => sum + f.symbols.length, 0);
122
- return asTextResult({ status: 'ok', files: fileCount, symbols: symbolCount });
229
+ if (mode === 'recent') {
230
+ const result = await getRecentlyModifiedSymbols(projectRoot, limit || 20, daysBack || 30);
231
+ return asTextResult({ mode, daysBack: daysBack || 30, symbols: result });
232
+ }
233
+
234
+ throw new Error(`Unknown mode: ${mode}`);
235
+ } catch (error) {
236
+ return asTextResult({ error: error.message, mode, filePath, authorQuery });
237
+ }
238
+ },
239
+ );
240
+
241
+ server.tool(
242
+ 'cross_project',
243
+ 'Work with multiple related projects (monorepos, microservices, shared libraries). Modes: discover (list related projects), search (search across projects), read (read files from multiple projects), symbol (find symbol definitions across projects), deps (get cross-project dependency graph), stats (usage statistics). Requires .devctx-projects.json config file in project root.',
244
+ {
245
+ mode: z.enum(['discover', 'search', 'read', 'symbol', 'deps', 'stats']),
246
+ query: z.string().optional(),
247
+ intent: z.enum(['implementation', 'debug', 'tests', 'config', 'docs']).optional(),
248
+ symbolName: z.string().optional(),
249
+ fileRefs: z.array(z.object({
250
+ project: z.string(),
251
+ file: z.string(),
252
+ mode: z.enum(['full', 'outline', 'symbols']).optional(),
253
+ })).optional(),
254
+ maxResultsPerProject: z.number().int().min(1).max(20).optional(),
255
+ includeProjects: z.array(z.string()).optional(),
256
+ excludeProjects: z.array(z.string()).optional(),
257
+ },
258
+ async ({ mode, query, intent, symbolName, fileRefs, maxResultsPerProject, includeProjects, excludeProjects }) => {
259
+ try {
260
+ if (mode === 'discover') {
261
+ const projects = discoverRelatedProjects(projectRoot);
262
+ return asTextResult({ mode, projects });
263
+ }
264
+
265
+ if (mode === 'search') {
266
+ if (!query) {
267
+ throw new Error('query is required for search mode');
268
+ }
269
+ const results = await searchAcrossProjects(query, {
270
+ root: projectRoot,
271
+ intent: intent || 'implementation',
272
+ maxResultsPerProject: maxResultsPerProject || 5,
273
+ includeProjects,
274
+ excludeProjects,
275
+ });
276
+ return asTextResult({ mode, query, intent, totalProjects: results.length, results });
277
+ }
278
+
279
+ if (mode === 'read') {
280
+ if (!fileRefs || fileRefs.length === 0) {
281
+ throw new Error('fileRefs is required for read mode');
282
+ }
283
+ const results = await readAcrossProjects(fileRefs, projectRoot);
284
+ return asTextResult({ mode, filesRead: results.length, results });
285
+ }
286
+
287
+ if (mode === 'symbol') {
288
+ if (!symbolName) {
289
+ throw new Error('symbolName is required for symbol mode');
290
+ }
291
+ const results = await findSymbolAcrossProjects(symbolName, projectRoot);
292
+ return asTextResult({ mode, symbolName, matches: results.length, results });
293
+ }
294
+
295
+ if (mode === 'deps') {
296
+ const deps = getCrossProjectDependencies(projectRoot);
297
+ return asTextResult({ mode, ...deps });
298
+ }
299
+
300
+ if (mode === 'stats') {
301
+ const stats = getCrossProjectStats(projectRoot);
302
+ return asTextResult({ mode, ...stats });
303
+ }
304
+
305
+ throw new Error(`Unknown mode: ${mode}`);
306
+ } catch (error) {
307
+ return asTextResult({ error: error.message, mode });
308
+ }
123
309
  },
124
310
  );
125
311
 
@@ -5,16 +5,18 @@ import path from 'node:path';
5
5
  import { projectRoot } from '../utils/runtime-config.js';
6
6
 
7
7
  export const STATE_DB_FILENAME = 'state.sqlite';
8
- export const SQLITE_SCHEMA_VERSION = 3;
8
+ export const SQLITE_SCHEMA_VERSION = 5;
9
9
  export const ACTIVE_SESSION_SCOPE = 'project';
10
10
  export const EXPECTED_TABLES = [
11
11
  'active_session',
12
+ 'context_access',
12
13
  'hook_turn_state',
13
14
  'meta',
14
15
  'metrics_events',
15
16
  'session_events',
16
17
  'sessions',
17
18
  'summary_cache',
19
+ 'workflow_metrics',
18
20
  ];
19
21
 
20
22
  const MIGRATIONS = [
@@ -125,6 +127,53 @@ const MIGRATIONS = [
125
127
  ON hook_turn_state(claude_session_id, updated_at DESC)`,
126
128
  ],
127
129
  },
130
+ {
131
+ version: 4,
132
+ statements: [
133
+ `CREATE TABLE IF NOT EXISTS context_access (
134
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
135
+ session_id TEXT NOT NULL,
136
+ task TEXT NOT NULL,
137
+ intent TEXT,
138
+ file_path TEXT NOT NULL,
139
+ relevance REAL,
140
+ access_order INTEGER,
141
+ timestamp TEXT NOT NULL
142
+ )`,
143
+ `CREATE INDEX IF NOT EXISTS idx_context_access_file_timestamp
144
+ ON context_access(file_path, timestamp DESC)`,
145
+ `CREATE INDEX IF NOT EXISTS idx_context_access_session
146
+ ON context_access(session_id, timestamp DESC)`,
147
+ ],
148
+ },
149
+ {
150
+ version: 5,
151
+ statements: [
152
+ `CREATE TABLE IF NOT EXISTS workflow_metrics (
153
+ workflow_id INTEGER PRIMARY KEY AUTOINCREMENT,
154
+ workflow_type TEXT NOT NULL,
155
+ session_id TEXT,
156
+ start_time TEXT NOT NULL,
157
+ end_time TEXT,
158
+ duration_ms INTEGER,
159
+ tools_used_json TEXT NOT NULL DEFAULT '[]',
160
+ steps_count INTEGER NOT NULL DEFAULT 0,
161
+ raw_tokens INTEGER NOT NULL DEFAULT 0,
162
+ compressed_tokens INTEGER NOT NULL DEFAULT 0,
163
+ saved_tokens INTEGER NOT NULL DEFAULT 0,
164
+ savings_pct REAL NOT NULL DEFAULT 0,
165
+ baseline_tokens INTEGER NOT NULL DEFAULT 0,
166
+ vs_baseline_pct REAL NOT NULL DEFAULT 0,
167
+ metadata_json TEXT NOT NULL DEFAULT '{}',
168
+ created_at TEXT NOT NULL,
169
+ FOREIGN KEY(session_id) REFERENCES sessions(session_id) ON DELETE SET NULL
170
+ )`,
171
+ `CREATE INDEX IF NOT EXISTS idx_workflow_metrics_type_created
172
+ ON workflow_metrics(workflow_type, created_at DESC)`,
173
+ `CREATE INDEX IF NOT EXISTS idx_workflow_metrics_session
174
+ ON workflow_metrics(session_id, created_at DESC)`,
175
+ ],
176
+ },
128
177
  ];
129
178
 
130
179
  let sqliteModulePromise = null;
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Streaming progress notifications for long-running operations.
3
+ *
4
+ * Usage:
5
+ * ```js
6
+ * const progress = createProgressReporter(server, 'build_index');
7
+ * progress.report({ phase: 'scanning', filesScanned: 100 });
8
+ * progress.report({ phase: 'indexing', filesProcessed: 50, total: 100 });
9
+ * progress.complete({ files: 1000, symbols: 5000 });
10
+ * ```
11
+ */
12
+
13
+ let currentServer = null;
14
+
15
+ /**
16
+ * Set the MCP server instance for sending notifications.
17
+ * Called once during server initialization.
18
+ */
19
+ export const setServerForStreaming = (server) => {
20
+ currentServer = server;
21
+ };
22
+
23
+ /**
24
+ * Create a progress reporter for a specific operation.
25
+ *
26
+ * @param {string} operation - Operation name (e.g., 'build_index', 'smart_search')
27
+ * @param {string} [operationId] - Optional unique ID for this operation instance
28
+ * @returns {ProgressReporter}
29
+ */
30
+ export const createProgressReporter = (operation, operationId = null) => {
31
+ const id = operationId || `${operation}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
32
+ let startTime = Date.now();
33
+ let lastReportTime = 0; // Allow first report immediately
34
+
35
+ return {
36
+ /**
37
+ * Report progress update.
38
+ * @param {object} data - Progress data (phase, percentage, items processed, etc.)
39
+ */
40
+ report(data) {
41
+ if (!currentServer) return;
42
+
43
+ const now = Date.now();
44
+ const elapsed = now - startTime;
45
+ const sinceLast = now - lastReportTime;
46
+
47
+ // Throttle: only send if >100ms since last report
48
+ if (sinceLast < 100) return;
49
+
50
+ lastReportTime = now;
51
+
52
+ try {
53
+ currentServer.notification({
54
+ method: 'notifications/progress',
55
+ params: {
56
+ progressToken: id,
57
+ progress: {
58
+ operation,
59
+ elapsed,
60
+ ...data,
61
+ },
62
+ },
63
+ });
64
+ } catch (err) {
65
+ // Ignore notification errors - don't fail the operation
66
+ }
67
+ },
68
+
69
+ /**
70
+ * Report completion with final result summary.
71
+ * @param {object} summary - Final result summary
72
+ */
73
+ complete(summary) {
74
+ if (!currentServer) return;
75
+
76
+ const elapsed = Date.now() - startTime;
77
+
78
+ try {
79
+ currentServer.notification({
80
+ method: 'notifications/progress',
81
+ params: {
82
+ progressToken: id,
83
+ progress: {
84
+ operation,
85
+ phase: 'complete',
86
+ elapsed,
87
+ ...summary,
88
+ },
89
+ },
90
+ });
91
+ } catch (err) {
92
+ // Ignore notification errors
93
+ }
94
+ },
95
+
96
+ /**
97
+ * Report error.
98
+ * @param {Error|string} error - Error that occurred
99
+ */
100
+ error(error) {
101
+ if (!currentServer) return;
102
+
103
+ const elapsed = Date.now() - startTime;
104
+ const message = error instanceof Error ? error.message : String(error);
105
+
106
+ try {
107
+ currentServer.notification({
108
+ method: 'notifications/progress',
109
+ params: {
110
+ progressToken: id,
111
+ progress: {
112
+ operation,
113
+ phase: 'error',
114
+ elapsed,
115
+ error: message,
116
+ },
117
+ },
118
+ });
119
+ } catch (err) {
120
+ // Ignore notification errors
121
+ }
122
+ },
123
+ };
124
+ };
125
+
126
+ /**
127
+ * Wrap an async operation with automatic progress reporting.
128
+ *
129
+ * @param {string} operation - Operation name
130
+ * @param {Function} fn - Async function to execute
131
+ * @param {Function} [progressFn] - Optional function to extract progress from intermediate results
132
+ * @returns {Promise} - Result of the operation
133
+ */
134
+ export const withProgress = async (operation, fn, progressFn = null) => {
135
+ const progress = createProgressReporter(operation);
136
+
137
+ try {
138
+ const result = await fn(progress);
139
+
140
+ if (progressFn && result) {
141
+ const summary = progressFn(result);
142
+ progress.complete(summary);
143
+ } else {
144
+ progress.complete({});
145
+ }
146
+
147
+ return result;
148
+ } catch (error) {
149
+ progress.error(error);
150
+ throw error;
151
+ }
152
+ };
@@ -10,6 +10,14 @@ import { projectRoot } from '../utils/paths.js';
10
10
  import { resolveSafePath } from '../utils/fs.js';
11
11
  import { countTokens } from '../tokenCounter.js';
12
12
  import { persistMetrics } from '../metrics.js';
13
+ import { predictContextFiles, recordContextAccess } from '../context-patterns.js';
14
+ import {
15
+ getDetailedDiff,
16
+ analyzeChangeImpact,
17
+ expandChangedContext,
18
+ generateDiffSummary as generateDetailedDiffSummary,
19
+ getChangedSymbols,
20
+ } from '../diff-analysis.js';
13
21
 
14
22
  const execFile = promisify(execFileCallback);
15
23
 
@@ -869,6 +877,7 @@ export const smartContext = async ({
869
877
  diff,
870
878
  detail = 'balanced',
871
879
  include = DEFAULT_INCLUDE,
880
+ prefetch = false,
872
881
  }) => {
873
882
  const resolvedIntent = (intent && VALID_INTENTS.has(intent)) ? intent : inferIntent(task);
874
883
  const root = projectRoot;
@@ -878,20 +887,65 @@ export const smartContext = async ({
878
887
  let primarySeeds = [];
879
888
  let searchIndexFreshness;
880
889
  let diffSummary = null;
890
+ let prefetchResult = null;
881
891
 
882
892
  if (diff) {
883
893
  const changed = await getChangedFiles(diff, root);
884
- primarySeeds = changed.files.map((rel, idx) => ({
885
- rel,
886
- absPath: path.join(root, rel),
887
- evidence: [{ type: 'diffHit', ref: changed.ref, rank: idx + 1 }],
888
- }));
894
+
895
+ // Get detailed diff stats
896
+ const detailedChanges = await getDetailedDiff(changed.ref, root);
897
+ const index = loadIndex(root);
898
+
899
+ // Analyze impact and prioritize
900
+ const prioritized = analyzeChangeImpact(detailedChanges, index);
901
+
902
+ // Expand to include related files (importers, dependencies, tests)
903
+ const expandedFiles = expandChangedContext(changed.files, index, 10);
904
+
905
+ // Build primary seeds with priority and impact data
906
+ primarySeeds = Array.from(expandedFiles).map(rel => {
907
+ const changeInfo = prioritized.find(c => c.file === rel);
908
+ const evidence = [{
909
+ type: 'diffHit',
910
+ ref: changed.ref,
911
+ priority: changeInfo?.priority || 'related',
912
+ impact: changeInfo?.impactScore || 0,
913
+ }];
914
+
915
+ // Mark files that were expanded (not directly changed)
916
+ if (!changed.files.includes(rel)) {
917
+ evidence[0].expanded = true;
918
+ }
919
+
920
+ return {
921
+ rel,
922
+ absPath: path.join(root, rel),
923
+ evidence,
924
+ };
925
+ });
926
+
927
+ // Sort by impact (critical changes first)
928
+ primarySeeds.sort((a, b) => {
929
+ const impactA = a.evidence[0].impact || 0;
930
+ const impactB = b.evidence[0].impact || 0;
931
+ return impactB - impactA;
932
+ });
933
+
889
934
  diffSummary = {
890
935
  ref: changed.ref,
891
936
  totalChanged: changed.files.length + changed.skippedDeleted,
892
- included: Math.min(changed.files.length, 5),
937
+ included: Math.min(primarySeeds.length, maxTokens > 4000 ? 10 : 5),
938
+ expanded: expandedFiles.size - changed.files.length,
893
939
  skippedDeleted: changed.skippedDeleted,
940
+ summary: generateDetailedDiffSummary(prioritized.slice(0, 10)),
941
+ topImpact: prioritized.slice(0, 3).map(c => ({
942
+ file: c.file,
943
+ priority: c.priority,
944
+ changes: `+${c.additions}/-${c.deletions}`,
945
+ type: c.changeType,
946
+ })),
894
947
  };
948
+
895
949
  if (changed.error) diffSummary.error = changed.error;
896
950
  searchIndexFreshness = null;
897
951
  } else {
@@ -972,6 +1026,38 @@ export const smartContext = async ({
972
1026
 
973
1027
  const index = loadIndex(root);
974
1028
 
1029
+ if (prefetch && !diff) {
1030
+ try {
1031
+ prefetchResult = await predictContextFiles({ task, intent: resolvedIntent, maxFiles: 8 });
1032
+
1033
+ if (prefetchResult.confidence >= 0.6 && prefetchResult.predicted.length > 0) {
1034
+ for (const predicted of prefetchResult.predicted) {
1035
+ try {
1036
+ const abs = resolveSafePath(predicted.path);
1037
+ if (fs.existsSync(abs)) {
1038
+ const rel = path.relative(root, abs).replace(/\\/g, '/');
1039
+ const alreadyIncluded = primarySeeds.some(seed => seed.absPath === abs);
1040
+
1041
+ if (!alreadyIncluded) {
1042
+ primarySeeds.push({
1043
+ rel,
1044
+ absPath: abs,
1045
+ evidence: [{
1046
+ type: 'prefetch',
1047
+ confidence: predicted.confidence,
1048
+ accessCount: predicted.accessCount
1049
+ }]
1050
+ });
1051
+ }
1052
+ }
1053
+ } catch {}
1054
+ }
1055
+ }
1056
+ } catch (error) {
1057
+ prefetchResult = { error: error.message, predicted: [] };
1058
+ }
1059
+ }
1060
+
975
1061
  primarySeeds = rerankPrimarySeeds(primarySeeds, task, resolvedIntent);
976
1062
 
977
1063
  const primarySeedsLimited = primarySeeds.slice(0, 5);
@@ -1143,6 +1229,20 @@ export const smartContext = async ({
1143
1229
  timestamp: new Date().toISOString(),
1144
1230
  });
1145
1231
 
1232
+ if (prefetch && context.length > 0) {
1233
+ try {
1234
+ await recordContextAccess({
1235
+ task,
1236
+ intent: resolvedIntent,
1237
+ files: context.map((item, idx) => ({
1238
+ path: item.file,
1239
+ relevance: item.role === 'primary' ? 1.0 : (item.role === 'test' ? 0.9 : 0.7),
1240
+ order: idx
1241
+ }))
1242
+ });
1243
+ } catch {}
1244
+ }
1245
+
1146
1246
  const COVERAGE_RANK = { full: 2, partial: 1, none: 0 };
1147
1247
  const coverageMin = (vals) => {
1148
1248
  if (vals.length === 0) return 'none';
@@ -1159,6 +1259,7 @@ export const smartContext = async ({
1159
1259
  };
1160
1260
 
1161
1261
  const result = {
1262
+ success: true,
1162
1263
  task,
1163
1264
  intent: resolvedIntent,
1164
1265
  indexFreshness,
@@ -1177,6 +1278,14 @@ export const smartContext = async ({
1177
1278
  indexOnlyItems,
1178
1279
  contentItems,
1179
1280
  primaryReadMode: primaryItem?.readMode ?? null,
1281
+ ...(prefetchResult ? {
1282
+ prefetch: {
1283
+ enabled: true,
1284
+ confidence: prefetchResult.confidence || 0,
1285
+ predictedFiles: prefetchResult.predicted?.length || 0,
1286
+ matchedPattern: prefetchResult.matchedPattern || null
1287
+ }
1288
+ } : {})
1180
1289
  },
1181
1290
  ...(includeSet.has('hints') ? { hints } : {}),
1182
1291
  };