smart-context-mcp 1.1.0 → 1.3.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.
@@ -0,0 +1,255 @@
1
+ /**
2
+ * Missed opportunities detector - identifies when devctx should have been used but wasn't
3
+ *
4
+ * Analyzes session metrics to detect patterns where devctx would have helped.
5
+ *
6
+ * Enable with environment variable: DEVCTX_DETECT_MISSED=true
7
+ *
8
+ * Detection heuristics (based on session metrics):
9
+ * - Low devctx adoption in complex sessions (many operations, few devctx calls)
10
+ * - Sessions with 0 devctx usage but high operation count
11
+ * - Inferred complexity vs actual devctx usage
12
+ *
13
+ * Note: We can't intercept native tool calls in real-time, so we analyze
14
+ * patterns from metrics after the fact.
15
+ */
16
+
17
+ const sessionActivity = {
18
+ devctxOperations: 0,
19
+ totalOperations: 0, // Estimated from devctx calls + time-based heuristic
20
+ lastDevctxCall: 0,
21
+ sessionStart: Date.now(),
22
+ enabled: false,
23
+ warnings: [],
24
+ };
25
+
26
+ const DEVCTX_TOOLS = new Set([
27
+ 'smart_read',
28
+ 'smart_search',
29
+ 'smart_context',
30
+ 'smart_shell',
31
+ 'smart_summary',
32
+ 'smart_turn',
33
+ 'smart_read_batch',
34
+ 'build_index',
35
+ ]);
36
+
37
+ /**
38
+ * Check if missed opportunity detection is enabled
39
+ */
40
+ export const isMissedDetectionEnabled = () => {
41
+ const envValue = process.env.DEVCTX_DETECT_MISSED?.toLowerCase();
42
+ const enabled = envValue === 'true' || envValue === '1' || envValue === 'yes';
43
+ sessionActivity.enabled = enabled;
44
+ return enabled;
45
+ };
46
+
47
+ /**
48
+ * Record devctx tool usage
49
+ */
50
+ export const recordDevctxOperation = () => {
51
+ if (!isMissedDetectionEnabled()) return;
52
+
53
+ sessionActivity.devctxOperations += 1;
54
+ sessionActivity.totalOperations += 1;
55
+ sessionActivity.lastDevctxCall = Date.now();
56
+ };
57
+
58
+ /**
59
+ * Estimate total operations based on time and activity
60
+ * Heuristic: If no devctx calls for >2 minutes, likely agent is using native tools
61
+ */
62
+ const estimateTotalOperations = () => {
63
+ const now = Date.now();
64
+ const sessionDuration = now - sessionActivity.sessionStart;
65
+ const timeSinceLastDevctx = now - sessionActivity.lastDevctxCall;
66
+
67
+ // If session is active (recent devctx calls), estimate conservatively
68
+ if (timeSinceLastDevctx < 2 * 60 * 1000) {
69
+ return sessionActivity.totalOperations;
70
+ }
71
+
72
+ // If long gap without devctx, estimate agent is using native tools
73
+ // Heuristic: ~1 operation per 10 seconds of activity
74
+ const estimatedNativeOps = Math.floor(timeSinceLastDevctx / 10000);
75
+ return sessionActivity.totalOperations + estimatedNativeOps;
76
+ };
77
+
78
+ /**
79
+ * Analyze session and detect missed opportunities
80
+ */
81
+ export const analyzeMissedOpportunities = () => {
82
+ if (!isMissedDetectionEnabled()) return null;
83
+
84
+ const now = Date.now();
85
+ const sessionDuration = now - sessionActivity.sessionStart;
86
+ const timeSinceLastDevctx = now - sessionActivity.lastDevctxCall;
87
+ const estimatedTotal = estimateTotalOperations();
88
+
89
+ const opportunities = [];
90
+
91
+ // Detection 1: Long session with no devctx usage
92
+ if (sessionDuration > 5 * 60 * 1000 && sessionActivity.devctxOperations === 0) {
93
+ opportunities.push({
94
+ type: 'no_devctx_usage',
95
+ severity: 'high',
96
+ reason: 'Session active for >5 minutes with 0 devctx calls. Agent may not be using devctx.',
97
+ suggestion: 'Use forcing prompt or check if MCP is active',
98
+ estimatedSavings: estimatedTotal * 10000, // Estimate ~10K tokens per operation
99
+ });
100
+ }
101
+
102
+ // Detection 2: Low devctx adoption in active session
103
+ const devctxRatio = estimatedTotal > 0 ? sessionActivity.devctxOperations / estimatedTotal : 0;
104
+ if (estimatedTotal >= 10 && devctxRatio < 0.3) {
105
+ opportunities.push({
106
+ type: 'low_devctx_adoption',
107
+ severity: 'medium',
108
+ reason: `Low devctx adoption: ${sessionActivity.devctxOperations}/${estimatedTotal} operations (${Math.round(devctxRatio * 100)}%). Target: >50%.`,
109
+ suggestion: 'Agent may be using native tools. Consider forcing prompt.',
110
+ estimatedSavings: (estimatedTotal - sessionActivity.devctxOperations) * 8000,
111
+ });
112
+ }
113
+
114
+ // Detection 3: Long gap without devctx (agent switched to native tools)
115
+ if (sessionActivity.devctxOperations > 0 && timeSinceLastDevctx > 3 * 60 * 1000) {
116
+ const minutesSince = Math.round(timeSinceLastDevctx / 60000);
117
+ opportunities.push({
118
+ type: 'devctx_usage_dropped',
119
+ severity: 'medium',
120
+ reason: `No devctx calls for ${minutesSince} minutes. Agent may have switched to native tools.`,
121
+ suggestion: 'Re-apply forcing prompt if task is still complex',
122
+ estimatedSavings: Math.floor(timeSinceLastDevctx / 10000) * 5000,
123
+ });
124
+ }
125
+
126
+ // Detection 4: Session too short to analyze
127
+ if (sessionDuration < 60 * 1000 && opportunities.length === 0) {
128
+ return {
129
+ opportunities: [],
130
+ message: 'Session too short to analyze (<1 minute)',
131
+ devctxOperations: sessionActivity.devctxOperations,
132
+ estimatedTotal,
133
+ };
134
+ }
135
+
136
+ return {
137
+ opportunities,
138
+ devctxOperations: sessionActivity.devctxOperations,
139
+ estimatedTotal,
140
+ devctxRatio: Math.round(devctxRatio * 100),
141
+ sessionDuration: Math.round(sessionDuration / 1000),
142
+ totalEstimatedSavings: opportunities.reduce((sum, opp) => sum + (opp.estimatedSavings || 0), 0),
143
+ };
144
+ };
145
+
146
+ /**
147
+ * Format missed opportunities as markdown
148
+ */
149
+ export const formatMissedOpportunities = () => {
150
+ if (!isMissedDetectionEnabled()) return '';
151
+
152
+ const analysis = analyzeMissedOpportunities();
153
+ if (!analysis) return '';
154
+
155
+ // Don't show if session is too short or no opportunities
156
+ if (analysis.message || analysis.opportunities.length === 0) {
157
+ return '';
158
+ }
159
+
160
+ const lines = [];
161
+ lines.push('');
162
+ lines.push('---');
163
+ lines.push('');
164
+ lines.push('⚠️ **Missed devctx opportunities detected:**');
165
+ lines.push('');
166
+
167
+ // Show session stats
168
+ lines.push(`**Session stats:**`);
169
+ lines.push(`- Duration: ${analysis.sessionDuration}s`);
170
+ lines.push(`- devctx operations: ${analysis.devctxOperations}`);
171
+ lines.push(`- Estimated total operations: ${analysis.estimatedTotal}`);
172
+ lines.push(`- devctx adoption: ${analysis.devctxRatio}%`);
173
+ lines.push('');
174
+
175
+ for (const opp of analysis.opportunities) {
176
+ const severityIcon = opp.severity === 'high' ? '🔴' : '🟡';
177
+ lines.push(`${severityIcon} **${opp.type.replace(/_/g, ' ')}**`);
178
+ lines.push(`- **Issue:** ${opp.reason}`);
179
+ lines.push(`- **Suggestion:** ${opp.suggestion}`);
180
+
181
+ if (opp.estimatedSavings) {
182
+ lines.push(`- **Potential savings:** ~${formatTokens(opp.estimatedSavings)}`);
183
+ }
184
+
185
+ lines.push('');
186
+ }
187
+
188
+ if (analysis.totalEstimatedSavings > 0) {
189
+ lines.push(`**Total potential savings:** ~${formatTokens(analysis.totalEstimatedSavings)}`);
190
+ lines.push('');
191
+ }
192
+
193
+ lines.push('**How to fix:**');
194
+ lines.push('1. Use forcing prompt: `Use devctx: smart_turn(start) → smart_context/smart_search → smart_read → smart_turn(end)`');
195
+ lines.push('2. Check if index is built: `ls .devctx/index.json`');
196
+ lines.push('3. Verify MCP is active in Cursor settings');
197
+ lines.push('');
198
+ lines.push('*To disable: `export DEVCTX_DETECT_MISSED=false`*');
199
+
200
+ return lines.join('\n');
201
+ };
202
+
203
+ /**
204
+ * Get session activity summary
205
+ */
206
+ export const getSessionActivity = () => {
207
+ return {
208
+ devctxOperations: sessionActivity.devctxOperations,
209
+ totalOperations: sessionActivity.totalOperations,
210
+ estimatedTotal: estimateTotalOperations(),
211
+ sessionDuration: Date.now() - sessionActivity.sessionStart,
212
+ timeSinceLastDevctx: Date.now() - sessionActivity.lastDevctxCall,
213
+ };
214
+ };
215
+
216
+ /**
217
+ * Reset session activity (for testing or manual reset)
218
+ */
219
+ export const resetSessionActivity = () => {
220
+ sessionActivity.devctxOperations = 0;
221
+ sessionActivity.totalOperations = 0;
222
+ sessionActivity.lastDevctxCall = 0;
223
+ sessionActivity.sessionStart = Date.now();
224
+ sessionActivity.warnings = [];
225
+ };
226
+
227
+ /**
228
+ * Testing helpers - expose internal state for test scenarios
229
+ */
230
+ export const __testing__ = {
231
+ setSessionStart: (timestamp) => {
232
+ sessionActivity.sessionStart = timestamp;
233
+ },
234
+ setLastDevctxCall: (timestamp) => {
235
+ sessionActivity.lastDevctxCall = timestamp;
236
+ },
237
+ setTotalOperations: (count) => {
238
+ sessionActivity.totalOperations = count;
239
+ },
240
+ getSessionActivity: () => sessionActivity,
241
+ };
242
+
243
+ /**
244
+ * Format token count for display
245
+ */
246
+ const formatTokens = (tokens) => {
247
+ if (tokens >= 1000000) {
248
+ return `${(tokens / 1000000).toFixed(1)}M tokens`;
249
+ }
250
+ if (tokens >= 1000) {
251
+ return `${(tokens / 1000).toFixed(1)}K tokens`;
252
+ }
253
+ return `${tokens} tokens`;
254
+ };
255
+
package/src/server.js CHANGED
@@ -49,6 +49,72 @@ export const createDevctxServer = () => {
49
49
  // Enable streaming progress notifications
50
50
  setServerForStreaming(server);
51
51
 
52
+ // Register prompts
53
+ server.prompt(
54
+ 'use-devctx',
55
+ 'Force the agent to use devctx tools for the current task. Use this prompt at the start of your message to ensure devctx is used instead of native tools.',
56
+ {},
57
+ async () => ({
58
+ messages: [
59
+ {
60
+ role: 'user',
61
+ content: {
62
+ type: 'text',
63
+ text: 'Use devctx: smart_turn(start) → smart_context/smart_search → smart_read → smart_turn(end)',
64
+ },
65
+ },
66
+ ],
67
+ })
68
+ );
69
+
70
+ server.prompt(
71
+ 'devctx-workflow',
72
+ 'Complete devctx workflow template with all recommended steps. Includes session start, context building, file reading, and session end.',
73
+ {},
74
+ async () => ({
75
+ messages: [
76
+ {
77
+ role: 'user',
78
+ content: {
79
+ type: 'text',
80
+ text: `Follow this devctx workflow:
81
+
82
+ 1. smart_turn(start) - Start session and recover previous context
83
+ 2. smart_context(task) - Build complete context for the task
84
+ 3. smart_search(query) - Search for specific patterns if needed
85
+ 4. smart_read(file) - Read files with appropriate mode (outline/signatures/symbol)
86
+ 5. smart_turn(end) - Save checkpoint for next session
87
+
88
+ Use devctx tools instead of native Read/Grep/Shell when possible.`,
89
+ },
90
+ },
91
+ ],
92
+ })
93
+ );
94
+
95
+ server.prompt(
96
+ 'devctx-preflight',
97
+ 'Preflight checklist before starting work. Ensures index is built and session is initialized.',
98
+ {},
99
+ async () => ({
100
+ messages: [
101
+ {
102
+ role: 'user',
103
+ content: {
104
+ type: 'text',
105
+ text: `Preflight checklist:
106
+
107
+ 1. build_index(incremental=true) - Build/update symbol index
108
+ 2. smart_turn(start) - Initialize session and recover context
109
+ 3. Proceed with your task using devctx tools
110
+
111
+ This ensures optimal performance and context recovery.`,
112
+ },
113
+ },
114
+ ],
115
+ })
116
+ );
117
+
52
118
  server.tool(
53
119
  'smart_read',
54
120
  '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.',
@@ -5,7 +5,7 @@ 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 = 4;
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',
@@ -16,6 +16,7 @@ export const EXPECTED_TABLES = [
16
16
  'session_events',
17
17
  'sessions',
18
18
  'summary_cache',
19
+ 'workflow_metrics',
19
20
  ];
20
21
 
21
22
  const MIGRATIONS = [
@@ -145,6 +146,34 @@ const MIGRATIONS = [
145
146
  ON context_access(session_id, timestamp DESC)`,
146
147
  ],
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
+ },
148
177
  ];
149
178
 
150
179
  let sqliteModulePromise = null;
@@ -11,6 +11,9 @@ import { resolveSafePath } from '../utils/fs.js';
11
11
  import { countTokens } from '../tokenCounter.js';
12
12
  import { persistMetrics } from '../metrics.js';
13
13
  import { predictContextFiles, recordContextAccess } from '../context-patterns.js';
14
+ import { recordToolUsage } from '../usage-feedback.js';
15
+ import { recordDecision, DECISION_REASONS, EXPECTED_BENEFITS } from '../decision-explainer.js';
16
+ import { recordDevctxOperation } from '../missed-opportunities.js';
14
17
  import {
15
18
  getDetailedDiff,
16
19
  analyzeChangeImpact,
@@ -1219,15 +1222,44 @@ export const smartContext = async ({
1219
1222
  const contentItems = context.filter((item) => typeof item.content === 'string' && item.content.length > 0).length;
1220
1223
  const primaryItem = context.find((item) => item.role === 'primary');
1221
1224
 
1225
+ const savedTokens = Math.max(0, totalRawTokens - totalCompressedTokens);
1226
+
1222
1227
  await persistMetrics({
1223
1228
  tool: 'smart_context',
1224
1229
  target: `${root} :: ${task}`,
1225
1230
  rawTokens: totalRawTokens,
1226
1231
  compressedTokens: totalCompressedTokens,
1227
- savedTokens: Math.max(0, totalRawTokens - totalCompressedTokens),
1232
+ savedTokens,
1228
1233
  savingsPct,
1229
1234
  timestamp: new Date().toISOString(),
1230
1235
  });
1236
+
1237
+ // Record usage for feedback
1238
+ recordToolUsage({
1239
+ tool: 'smart_context',
1240
+ savedTokens,
1241
+ target: task,
1242
+ });
1243
+
1244
+ // Record devctx operation for missed opportunity detection
1245
+ recordDevctxOperation();
1246
+
1247
+ // Record decision explanation
1248
+ let reason = DECISION_REASONS.TASK_CONTEXT;
1249
+ if (diff) {
1250
+ reason = DECISION_REASONS.DIFF_ANALYSIS;
1251
+ } else if (context.some(c => c.role === 'caller' || c.role === 'test')) {
1252
+ reason = DECISION_REASONS.RELATED_FILES;
1253
+ }
1254
+
1255
+ recordDecision({
1256
+ tool: 'smart_context',
1257
+ action: `build context for "${task}"`,
1258
+ reason,
1259
+ alternative: 'Multiple smart_read + smart_search calls',
1260
+ expectedBenefit: `${EXPECTED_BENEFITS.TOKEN_SAVINGS(savedTokens)}, ${EXPECTED_BENEFITS.COMPLETE_CONTEXT}`,
1261
+ context: `${context.length} files, ${totalCompressedTokens} tokens (${savingsPct}% compression)`,
1262
+ });
1231
1263
 
1232
1264
  if (prefetch && context.length > 0) {
1233
1265
  try {
@@ -14,6 +14,7 @@ import {
14
14
  readMetricsEntries,
15
15
  resolveMetricsInput,
16
16
  } from '../metrics.js';
17
+ import { analyzeAdoption } from '../analytics/adoption.js';
17
18
 
18
19
  const WINDOW_MS = {
19
20
  '24h': 24 * 60 * 60 * 1000,
@@ -197,6 +198,8 @@ export const smartMetrics = async ({
197
198
  .filter((entry) => (tool ? entry.tool === tool : true))
198
199
  .filter((entry) => (resolvedSessionId ? entry.sessionId === resolvedSessionId : true));
199
200
 
201
+ const adoption = analyzeAdoption(filteredEntries);
202
+
200
203
  return {
201
204
  filePath: resolved.storagePath,
202
205
  storagePath: resolved.storagePath,
@@ -212,6 +215,7 @@ export const smartMetrics = async ({
212
215
  },
213
216
  invalidLines,
214
217
  summary: aggregateMetrics(filteredEntries),
218
+ adoption,
215
219
  latestEntries: buildLatestEntries(filteredEntries, latest),
216
220
  };
217
221
  }
@@ -229,6 +233,8 @@ export const smartMetrics = async ({
229
233
  window,
230
234
  });
231
235
 
236
+ const adoption = analyzeAdoption(entries);
237
+
232
238
  return {
233
239
  filePath: resolved.storagePath,
234
240
  storagePath: resolved.storagePath,
@@ -244,6 +250,7 @@ export const smartMetrics = async ({
244
250
  },
245
251
  invalidLines,
246
252
  summary: aggregateMetrics(entries),
253
+ adoption,
247
254
  latestEntries: buildLatestEntries(entries, latest),
248
255
  };
249
256
  };
@@ -18,6 +18,15 @@ export const smartReadBatch = async ({ files, maxTokens }) => {
18
18
  maxTokens: item.maxTokens,
19
19
  });
20
20
 
21
+ if (readResult.error) {
22
+ results.push({
23
+ filePath: item.path,
24
+ mode: item.mode ?? 'outline',
25
+ error: readResult.error,
26
+ });
27
+ continue;
28
+ }
29
+
21
30
  const itemTokens = countTokens(readResult.content);
22
31
 
23
32
  if (maxTokens && totalTokens + itemTokens > maxTokens && results.length > 0) {
@@ -9,6 +9,9 @@ import { isDockerfile, readTextFile } from '../utils/fs.js';
9
9
  import { projectRoot } from '../utils/paths.js';
10
10
  import { truncate } from '../utils/text.js';
11
11
  import { countTokens } from '../tokenCounter.js';
12
+ import { recordToolUsage } from '../usage-feedback.js';
13
+ import { recordDecision, DECISION_REASONS, EXPECTED_BENEFITS } from '../decision-explainer.js';
14
+ import { recordDevctxOperation } from '../missed-opportunities.js';
12
15
 
13
16
  const execFile = promisify(execFileCb);
14
17
  import { summarizeGo, summarizeRust, summarizeJava, summarizeShell, summarizeTerraform, summarizeDockerfile, summarizeSql, extractGoSymbol, extractRustSymbol, extractJavaSymbol, summarizeCsharp, extractCsharpSymbol, summarizeKotlin, extractKotlinSymbol, summarizePhp, extractPhpSymbol, summarizeSwift, extractSwiftSymbol } from './smart-read/additional-languages.js';
@@ -363,7 +366,27 @@ const formatContextSections = (sections) => {
363
366
  };
364
367
 
365
368
  export const smartRead = async ({ filePath, mode = 'outline', startLine, endLine, symbol, maxTokens, context: includeContext }) => {
366
- const { fullPath, content } = readTextFile(filePath);
369
+ let fullPath, content;
370
+
371
+ try {
372
+ const result = readTextFile(filePath);
373
+ fullPath = result.fullPath;
374
+ content = result.content;
375
+ } catch (error) {
376
+ const errorMessage = error.message || String(error);
377
+ return {
378
+ error: errorMessage,
379
+ filePath,
380
+ mode,
381
+ metrics: buildMetrics({
382
+ tool: 'smart_read',
383
+ target: filePath,
384
+ rawText: '',
385
+ compressedText: errorMessage,
386
+ }),
387
+ };
388
+ }
389
+
367
390
  const extension = path.extname(fullPath).toLowerCase();
368
391
  const mtime = getFileMtime(fullPath);
369
392
 
@@ -433,6 +456,38 @@ export const smartRead = async ({ filePath, mode = 'outline', startLine, endLine
433
456
  });
434
457
 
435
458
  await persistMetrics(metrics);
459
+
460
+ // Record usage for feedback
461
+ recordToolUsage({
462
+ tool: 'smart_read',
463
+ savedTokens: metrics.savedTokens,
464
+ target: path.relative(projectRoot, fullPath),
465
+ });
466
+
467
+ // Record devctx operation for missed opportunity detection
468
+ recordDevctxOperation();
469
+
470
+ // Record decision explanation
471
+ const lineCount = content.split('\n').length;
472
+ let reason = DECISION_REASONS.LARGE_FILE;
473
+ let expectedBenefit = EXPECTED_BENEFITS.TOKEN_SAVINGS(metrics.savedTokens);
474
+
475
+ if (mode === 'symbol') {
476
+ reason = DECISION_REASONS.SYMBOL_EXTRACTION;
477
+ } else if (validBudget && effectiveMode !== mode) {
478
+ reason = DECISION_REASONS.TOKEN_BUDGET;
479
+ } else if (lineCount < 100) {
480
+ reason = `File is small (${lineCount} lines), but using ${effectiveMode} mode for consistency`;
481
+ }
482
+
483
+ recordDecision({
484
+ tool: 'smart_read',
485
+ action: `read ${path.relative(projectRoot, fullPath)} (${effectiveMode} mode)`,
486
+ reason,
487
+ alternative: 'Read (full file)',
488
+ expectedBenefit,
489
+ context: `${lineCount} lines, ${metrics.rawTokens} tokens → ${metrics.compressedTokens} tokens`,
490
+ });
436
491
 
437
492
  const confidence = { parser, truncated, cached: cacheHit && !contextResult };
438
493
  if (contextResult) confidence.graphCoverage = contextResult.graphCoverage;
@@ -8,6 +8,9 @@ import { loadIndex, queryIndex, queryRelated } from '../index.js';
8
8
  import { projectRoot } from '../utils/paths.js';
9
9
  import { isBinaryBuffer, isDockerfile, resolveSafePath } from '../utils/fs.js';
10
10
  import { truncate } from '../utils/text.js';
11
+ import { recordToolUsage } from '../usage-feedback.js';
12
+ import { recordDecision, DECISION_REASONS, EXPECTED_BENEFITS } from '../decision-explainer.js';
13
+ import { recordDevctxOperation } from '../missed-opportunities.js';
11
14
 
12
15
  const execFile = promisify(execFileCallback);
13
16
  const supportedGlobs = [
@@ -381,6 +384,34 @@ export const smartSearch = async ({ query, cwd = '.', intent, _testForceWalk = f
381
384
  });
382
385
 
383
386
  await persistMetrics(metrics);
387
+
388
+ // Record usage for feedback
389
+ recordToolUsage({
390
+ tool: 'smart_search',
391
+ savedTokens: metrics.savedTokens,
392
+ target: query,
393
+ });
394
+
395
+ // Record devctx operation for missed opportunity detection
396
+ recordDevctxOperation();
397
+
398
+ // Record decision explanation
399
+ let reason = DECISION_REASONS.MULTIPLE_FILES;
400
+ if (validIntent) {
401
+ reason = DECISION_REASONS.INTENT_AWARE;
402
+ }
403
+ if (indexHits && indexHits.size > 0) {
404
+ reason = DECISION_REASONS.INDEX_BOOST;
405
+ }
406
+
407
+ recordDecision({
408
+ tool: 'smart_search',
409
+ action: `search "${query}"${validIntent ? ` (intent: ${validIntent})` : ''}`,
410
+ reason,
411
+ alternative: 'Grep (unranked results)',
412
+ expectedBenefit: `${EXPECTED_BENEFITS.TOKEN_SAVINGS(metrics.savedTokens)}, ${EXPECTED_BENEFITS.BETTER_RANKING}`,
413
+ context: `${dedupedMatches.length} matches in ${groups.length} files, ranked by relevance`,
414
+ });
384
415
 
385
416
  let retrievalConfidence = 'high';
386
417
  if (provenance) {