@rigour-labs/mcp 6.0.0 → 6.1.0-beta.1

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/dist/index.js CHANGED
@@ -16,6 +16,7 @@ import { GateRunner } from "@rigour-labs/core";
16
16
  import { loadConfig, loadMcpSettings, logStudioEvent } from './utils/config.js';
17
17
  import { bindServer } from './utils/notifications.js';
18
18
  import { getMcpVersion } from './utils/package-version.js';
19
+ import { buildMcpResultMeta, buildStudioImpact } from './utils/impact-receipt.js';
19
20
  // Dashboard (MCP App)
20
21
  import { DASHBOARD_URI, getDashboardHtml, pushTimelineEntry, updateScore } from './dashboard/index.js';
21
22
  // Tool definitions & advertised registry
@@ -32,9 +33,25 @@ import { handleCheckDeep, handleDeepStats } from './tools/deep-handlers.js';
32
33
  import { handleMcpGetSettings, handleMcpSetSettings } from './tools/mcp-settings-handler.js';
33
34
  import { handleContextStats, handleTaskCost, handleCacheStats, handleContextExplain, handleContextScope } from './tools/context-handlers.js';
34
35
  import { handleIndex } from './tools/index-handlers.js';
35
- import { recordContextEvent, estimateTokenCount } from '@rigour-labs/core';
36
+ import { recordContextEvent, recordInteractionEvidence, recordInteractionLesson, syncTeamOutbox, estimateTokenCount } from '@rigour-labs/core';
36
37
  // ─── Server Setup ─────────────────────────────────────────────────
37
38
  const server = new Server({ name: "rigour-mcp", version: getMcpVersion('5.5.0') }, { capabilities: { tools: {}, prompts: {}, logging: {}, resources: {} } });
39
+ function interactionMetadata(args) {
40
+ const input = (args && typeof args === 'object' ? args : {});
41
+ const rawFiles = Array.isArray(input.files)
42
+ ? input.files
43
+ : Array.isArray(input.filesChanged)
44
+ ? input.filesChanged
45
+ : Array.isArray(input.filesInScope)
46
+ ? input.filesInScope
47
+ : input.file ? [input.file] : [];
48
+ return {
49
+ taskId: String(input.taskId || input.task_id || '') || undefined,
50
+ agentId: String(input.agentId || input.agent_id || input.agent || '') || undefined,
51
+ sessionId: String(input.sessionId || input.session_id || '') || undefined,
52
+ files: rawFiles.filter((file) => typeof file === 'string').slice(0, 100),
53
+ };
54
+ }
38
55
  // Bind server for logging notifications from handlers
39
56
  bindServer(server);
40
57
  // ─── Tool Listing ─────────────────────────────────────────────────
@@ -69,6 +86,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
69
86
  const requestId = randomUUID();
70
87
  try {
71
88
  await logStudioEvent(cwd, { type: "tool_call", requestId, tool: name, arguments: args });
89
+ try {
90
+ await recordInteractionEvidence(cwd, {
91
+ tool: name,
92
+ outcome: 'success',
93
+ phase: 'request',
94
+ requestId,
95
+ ...interactionMetadata(args),
96
+ });
97
+ }
98
+ catch {
99
+ // Evidence capture must never block the requested governance operation.
100
+ }
72
101
  // ── Image DLP warning ──────────────────────────────
73
102
  // MCP args may contain base64 image data. Text DLP cannot scan images.
74
103
  const argsStr = JSON.stringify(args ?? {});
@@ -233,12 +262,41 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
233
262
  candidateTokens: telemetry?.candidateTokens ?? returnedTokens,
234
263
  returnedTokens,
235
264
  deduplicatedTokens: telemetry?.deduplicatedTokens ?? 0,
265
+ candidateFiles: telemetry?.candidateFiles,
266
+ returnedFiles: telemetry?.returnedFiles,
236
267
  }, cwd);
237
268
  }
238
269
  }
239
270
  catch {
240
271
  // non-blocking context telemetry logging
241
272
  }
273
+ try {
274
+ const reportStatus = result?._rigour_report?.status;
275
+ const deterministic = Boolean(reportStatus === 'PASS');
276
+ const outcome = result?.isError ? 'error' : 'success';
277
+ await recordInteractionEvidence(cwd, {
278
+ tool: name,
279
+ outcome,
280
+ phase: 'response',
281
+ requestId,
282
+ deterministic,
283
+ summary: result?._guidance?.recommendation,
284
+ ...interactionMetadata(args),
285
+ });
286
+ await recordInteractionLesson(cwd, {
287
+ tool: name,
288
+ outcome,
289
+ requestId,
290
+ deterministic,
291
+ verifiedOutcome: deterministic,
292
+ summary: result?._guidance?.recommendation,
293
+ ...interactionMetadata(args),
294
+ });
295
+ void syncTeamOutbox().catch(() => undefined);
296
+ }
297
+ catch {
298
+ // Learning is evidence collection and must not block governance tools.
299
+ }
242
300
  // ── Prepend image DLP warning if image content was detected ──
243
301
  if (hasImageContent && Array.isArray(result.content)) {
244
302
  result.content.unshift({
@@ -246,9 +304,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
246
304
  text: "⚠ DLP Notice: Image content detected in this request. Text-based credential scanning cannot analyze images. Avoid sharing screenshots containing API keys, tokens, or passwords.",
247
305
  });
248
306
  }
307
+ const studioImpact = buildStudioImpact(result._guidance, result._telemetry);
249
308
  await logStudioEvent(cwd, {
250
- type: "tool_response", requestId, tool: name, status: "success",
251
- content: result.content, _rigour_report: result._rigour_report,
309
+ type: "tool_response", requestId, tool: name, status: result.isError ? "error" : "success",
310
+ content: result.content,
311
+ _rigour_report: result._rigour_report,
312
+ ...studioImpact,
252
313
  });
253
314
  // ─── Dashboard State + MCP App UI Hint ──────────────
254
315
  const report = result._rigour_report;
@@ -261,7 +322,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
261
322
  }
262
323
  // Attach UI hint for MCP-App-capable clients (Claude Desktop, VS Code, ChatGPT, Goose)
263
324
  if (!result.isError) {
264
- result._meta = { ui: { resourceUri: DASHBOARD_URI } };
325
+ result._meta = buildMcpResultMeta(result._meta, requestId, DASHBOARD_URI, result._guidance, result._telemetry);
265
326
  }
266
327
  // Notify clients that the dashboard resource has updated
267
328
  try {
@@ -280,6 +341,19 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
280
341
  content: [{ type: "text", text: `RIGOUR ERROR: ${error.message}` }],
281
342
  isError: true,
282
343
  };
344
+ try {
345
+ await recordInteractionEvidence(cwd, {
346
+ tool: name,
347
+ outcome: 'error',
348
+ phase: 'response',
349
+ requestId,
350
+ summary: String(error?.message || 'Unknown error').slice(0, 500),
351
+ ...interactionMetadata(args),
352
+ });
353
+ }
354
+ catch {
355
+ // Failure evidence capture is intentionally non-blocking.
356
+ }
283
357
  await logStudioEvent(cwd, {
284
358
  type: "tool_response", requestId, tool: name,
285
359
  status: "error", error: error.message, content: errorResponse.content,
@@ -9,8 +9,8 @@
9
9
  import fs from "fs-extra";
10
10
  import path from "path";
11
11
  import { logStudioEvent } from '../utils/config.js';
12
- import { setTaskCheckpointCache, getTaskCheckpointCache, estimateTokenCount, } from '@rigour-labs/core';
13
- import { PatternIndexer, savePatternIndex, loadPatternIndex, getDefaultIndexPath, } from '@rigour-labs/core/pattern-index';
12
+ import { setTaskCheckpointCache, getTaskCheckpointCache, estimateTokenCount, updateAutomaticIndexForFiles, } from '@rigour-labs/core';
13
+ import { loadPatternIndex, getDefaultIndexPath, } from '@rigour-labs/core/pattern-index';
14
14
  import { buildTelemetryMeta, getWorkspaceCommitSha } from '../utils/context-telemetry.js';
15
15
  import { appendContextFooter } from '../utils/context-footer.js';
16
16
  const HANDOFF_CONTEXT_TOKEN_LIMIT = 2000;
@@ -38,16 +38,9 @@ function globMatchesFile(filePath, glob) {
38
38
  async function refreshIndexForFiles(cwd, filesChanged) {
39
39
  if (filesChanged.length === 0)
40
40
  return null;
41
- const indexPath = getDefaultIndexPath(cwd);
42
- const existingIndex = await loadPatternIndex(indexPath);
43
- if (!existingIndex) {
44
- return 'Index not found — run rigour_index to enable incremental updates.';
45
- }
46
41
  try {
47
- const indexer = new PatternIndexer(cwd, { useEmbeddings: existingIndex.patterns.some(p => p.embedding?.length) });
48
- const updated = await indexer.updateIndex(existingIndex);
49
- await savePatternIndex(updated, indexPath);
50
- return `Index refreshed for ${filesChanged.length} changed file(s) — ${updated.stats.totalPatterns} patterns total.`;
42
+ const { index, affectedFiles } = await updateAutomaticIndexForFiles(cwd, filesChanged);
43
+ return `Index refreshed ${filesChanged.length} changed and ${affectedFiles.length} affected file(s) — ${index.stats.totalPatterns} patterns total.`;
51
44
  }
52
45
  catch (error) {
53
46
  return `Index refresh failed: ${error.message}`;
@@ -11,7 +11,7 @@
11
11
  import fs from 'fs-extra';
12
12
  import path from 'path';
13
13
  import { createHash } from 'crypto';
14
- import { getTaskContextStats, getTaskCostStats, getCacheStats, explainContext, getSemanticQueryCache, setSemanticQueryCache, findRelatedSemanticQueryCache, filterExistingEditScope, estimateTokenCount, } from '@rigour-labs/core';
14
+ import { getTaskContextStats, getTaskCostStats, getCacheStats, explainContext, getSemanticQueryCache, setSemanticQueryCache, findRelatedSemanticQueryCache, filterExistingEditScope, estimateTokenCount, ensureAutomaticIndex, getApplicableLessons, searchTeamKnowledge, } from '@rigour-labs/core';
15
15
  import { loadPatternIndex, getDefaultIndexPath, generateEmbedding, semanticSearch, } from '@rigour-labs/core/pattern-index';
16
16
  import { buildTelemetryMeta, getWorkspaceCommitSha } from '../utils/context-telemetry.js';
17
17
  import { appendContextFooter } from '../utils/context-footer.js';
@@ -107,6 +107,10 @@ function scopeTelemetryMeta(opts) {
107
107
  taskId: opts.taskId,
108
108
  agentId: opts.agentId,
109
109
  queryHash: hashScopeQuery(opts.query),
110
+ candidateTokens: opts.candidateTokens,
111
+ returnedTokens: opts.returnedTokens,
112
+ candidateFiles: opts.candidateFiles,
113
+ returnedFiles: opts.returnedFiles,
110
114
  });
111
115
  }
112
116
  function textMatchPatterns(query, patterns, limit) {
@@ -155,6 +159,7 @@ async function serveCachedScope(cached, opts) {
155
159
  cacheNote: opts.note,
156
160
  };
157
161
  const cachedText = JSON.stringify(payload, null, 2);
162
+ const returnedTokens = estimateTokenCount(cachedText);
158
163
  const telemetry = scopeTelemetryMeta({
159
164
  candidateText: opts.fullIndexCandidate,
160
165
  returnedText: cachedText,
@@ -162,6 +167,11 @@ async function serveCachedScope(cached, opts) {
162
167
  query: opts.query,
163
168
  taskId: opts.taskId,
164
169
  agentId: opts.agentId,
170
+ candidateTokens: cached.sourceTokens,
171
+ returnedTokens,
172
+ candidateFiles: cached.candidateFiles,
173
+ returnedFiles: valid.length,
174
+ deduplicatedTokens: Math.max(0, (cached.sourceTokens ?? returnedTokens) - returnedTokens),
165
175
  });
166
176
  const label = opts.cacheStatus === 'partial-hit'
167
177
  ? 'CONTEXT SCOPE (partial cache)'
@@ -170,6 +180,15 @@ async function serveCachedScope(cached, opts) {
170
180
  return {
171
181
  content: [{ type: 'text', text: appendContextFooter(text, telemetry, 'rigour_check_pattern before creating code') }],
172
182
  _telemetry: telemetry,
183
+ _guidance: {
184
+ ...(cached.guidance ?? {
185
+ kind: 'context-scope',
186
+ recommendation: `Reuse the cached scope of ${valid.length} file(s).`,
187
+ }),
188
+ query: opts.query,
189
+ selectedFiles: valid,
190
+ excludedFileCount: Math.max(0, (cached.candidateFiles ?? valid.length) - valid.length),
191
+ },
173
192
  };
174
193
  }
175
194
  export async function handleContextScope(cwd, query, limit = 10, taskId, agentId) {
@@ -205,22 +224,9 @@ export async function handleContextScope(cwd, query, limit = 10, taskId, agentId
205
224
  if (served)
206
225
  return served;
207
226
  }
208
- const index = await loadPatternIndex(indexPath);
209
- if (!index) {
210
- const text = '⚠️ Pattern index not found. Call rigour_index first to enable scoped context retrieval.\n\n'
211
- + 'Without an index, agents tend to read entire directories — wasting tokens.';
212
- return {
213
- content: [{ type: 'text', text }],
214
- _telemetry: scopeTelemetryMeta({
215
- candidateText: fullIndexCandidate,
216
- returnedText: text,
217
- cacheStatus: 'miss',
218
- query,
219
- taskId,
220
- agentId,
221
- }),
222
- };
223
- }
227
+ const index = await loadPatternIndex(indexPath) ?? await ensureAutomaticIndex(cwd, {
228
+ backgroundSemantic: process.env.NODE_ENV !== 'test',
229
+ });
224
230
  let matched = [];
225
231
  const hasEmbeddings = index.patterns.some(p => p.embedding && p.embedding.length > 0);
226
232
  if (hasEmbeddings) {
@@ -252,6 +258,10 @@ export async function handleContextScope(cwd, query, limit = 10, taskId, agentId
252
258
  .slice(0, Math.max(0, 20 - editScope.length))
253
259
  .map(f => ({ file: f, reason: 'Outside semantic match for query' }));
254
260
  const signatures = matched.map(p => `- ${p.file}:${p.line} ${p.type} ${p.name} — ${p.signature || p.description || '(no signature)'}`);
261
+ const [applicableLearning, semanticKnowledge] = await Promise.all([
262
+ getApplicableLessons(cwd, agentId, process.env.RIGOUR_TEAM_ID),
263
+ searchTeamKnowledge(query, Math.min(limit, 8)),
264
+ ]);
255
265
  let fullFileTokens = 0;
256
266
  for (const file of editScope) {
257
267
  try {
@@ -262,22 +272,51 @@ export async function handleContextScope(cwd, query, limit = 10, taskId, agentId
262
272
  fullFileTokens += 500;
263
273
  }
264
274
  }
275
+ const guidance = {
276
+ kind: 'context-scope',
277
+ query,
278
+ recommendation: editScope.length > 0
279
+ ? `Read only the ${editScope.length} evidence-backed file(s) in this scope.`
280
+ : 'Refine the task query before reading broadly.',
281
+ patternRefs: matched.map(pattern => ({ id: pattern.id, label: pattern.name, file: pattern.file })),
282
+ lessonRefs: applicableLearning.lessons.map(lesson => ({ id: lesson.id, label: lesson.subject, visibility: lesson.visibility })),
283
+ selectedFiles: editScope,
284
+ excludedFileCount: Math.max(0, index.stats.totalFiles - editScope.length),
285
+ conflicts: applicableLearning.conflicts.length,
286
+ };
265
287
  const scopePayload = {
266
288
  query,
267
289
  editScope,
268
290
  skipScope,
269
291
  signatures,
292
+ learningGuidance: applicableLearning.lessons.map(lesson => ({
293
+ subject: lesson.subject,
294
+ scope: lesson.repositoryId,
295
+ visibility: lesson.visibility,
296
+ confidence: lesson.confidence,
297
+ provenance: lesson.provenance,
298
+ })),
299
+ learningConflicts: applicableLearning.conflicts,
300
+ semanticKnowledge: {
301
+ ...semanticKnowledge,
302
+ purpose: 'Advisory cross-repository recall only; similarity never promotes or enforces a lesson.',
303
+ },
270
304
  indexHealth: {
271
305
  totalPatterns: index.stats.totalPatterns,
272
306
  totalFiles: index.stats.totalFiles,
273
307
  lastUpdated: index.lastUpdated,
274
308
  semanticEnabled: hasEmbeddings,
275
309
  },
276
- estimatedTokensSaved: Math.max(0, fullFileTokens - estimateTokenCount(signatures.join('\n'))),
310
+ estimatedTokensSaved: 0,
277
311
  recommendation: editScope.length > 0
278
312
  ? `Read ONLY these ${editScope.length} file(s). Do NOT scan the whole repo.`
279
313
  : 'No strong matches — refine query or run rigour_index --semantic',
280
314
  };
315
+ // Compare compact guidance with the raw contents of the selected files only.
316
+ // Repository-wide files excluded by structural matching are reported separately.
317
+ let resultText = JSON.stringify(scopePayload, null, 2);
318
+ scopePayload.estimatedTokensSaved = Math.max(0, fullFileTokens - estimateTokenCount(resultText));
319
+ resultText = JSON.stringify(scopePayload, null, 2);
281
320
  await setSemanticQueryCache(query, commitSha, {
282
321
  query,
283
322
  resolvedOwner: editScope[0] ? path.dirname(editScope[0]) : 'unknown',
@@ -286,8 +325,11 @@ export async function handleContextScope(cwd, query, limit = 10, taskId, agentId
286
325
  evidence: signatures.slice(0, 5),
287
326
  commitSha,
288
327
  confidence: matched.length > 0 ? 0.85 : 0.3,
328
+ sourceTokens: fullFileTokens,
329
+ candidateFiles: index.stats.totalFiles,
330
+ returnedFiles: editScope.length,
331
+ guidance,
289
332
  }, cwd);
290
- const resultText = JSON.stringify(scopePayload, null, 2);
291
333
  const telemetry = scopeTelemetryMeta({
292
334
  candidateText: fullIndexCandidate,
293
335
  returnedText: resultText,
@@ -296,6 +338,10 @@ export async function handleContextScope(cwd, query, limit = 10, taskId, agentId
296
338
  query,
297
339
  taskId,
298
340
  agentId,
341
+ candidateTokens: fullFileTokens,
342
+ returnedTokens: estimateTokenCount(resultText),
343
+ candidateFiles: index.stats.totalFiles,
344
+ returnedFiles: editScope.length,
299
345
  });
300
346
  return {
301
347
  content: [{
@@ -303,5 +349,6 @@ export async function handleContextScope(cwd, query, limit = 10, taskId, agentId
303
349
  text: appendContextFooter(`CONTEXT SCOPE\n\n${resultText}`, telemetry, 'rigour_check_pattern before creating code'),
304
350
  }],
305
351
  _telemetry: telemetry,
352
+ _guidance: guidance,
306
353
  };
307
354
  }
@@ -7,7 +7,7 @@
7
7
  * @since v3.0.0 — real-time hooks for AI coding tools
8
8
  * @since v4.2.0 — AI Agent DLP warning layer
9
9
  */
10
- import { runHookChecker, generateHookFiles } from "@rigour-labs/core";
10
+ import { runHookChecker, generateHookFiles, updateAutomaticIndexForFiles } from "@rigour-labs/core";
11
11
  import { scanInputForCredentials, formatDLPAlert, createDLPAuditEntry, generateDLPHookFiles } from "@rigour-labs/core";
12
12
  import fs from "fs-extra";
13
13
  import path from "path";
@@ -76,6 +76,9 @@ export async function handleHooksCheck(cwd, files, timeout, text, agent) {
76
76
  if (agent)
77
77
  input.agentId = agent;
78
78
  const result = await runHookChecker(input);
79
+ if (files.length > 0) {
80
+ await updateAutomaticIndexForFiles(cwd, files).catch(() => undefined);
81
+ }
79
82
  if (result.status === 'pass') {
80
83
  return {
81
84
  content: [{
@@ -52,7 +52,7 @@ export async function handleIndex(cwd, options = {}) {
52
52
  const candidateEstimate = 'Full codebase AST scan for pattern extraction';
53
53
  try {
54
54
  notifyProgress('info', 'Building pattern index...');
55
- const indexer = new PatternIndexer(cwd, { useEmbeddings: options.semantic ?? false });
55
+ const indexer = new PatternIndexer(cwd, { useEmbeddings: options.semantic ?? true });
56
56
  const existingIndex = await loadPatternIndex(indexPath);
57
57
  let index;
58
58
  if (existingIndex && !options.force) {
@@ -60,7 +60,7 @@ async function getIndexHealthBlock(cwd) {
60
60
  }
61
61
  return `\n\n📊 Pattern Index: ${index.stats.totalPatterns} patterns across ${index.stats.totalFiles} files (updated ${index.lastUpdated}).`;
62
62
  }
63
- function wrapRecallResult(text, candidateText, cacheStatus, deduplicatedTokens = 0) {
63
+ function wrapRecallResult(text, candidateText, cacheStatus, deduplicatedTokens = 0, guidance) {
64
64
  const telemetry = buildTelemetryMeta({
65
65
  candidateText,
66
66
  returnedText: text,
@@ -73,6 +73,7 @@ function wrapRecallResult(text, candidateText, cacheStatus, deduplicatedTokens =
73
73
  text: appendContextFooter(text, telemetry, 'rigour_context_scope("your task")'),
74
74
  }],
75
75
  _telemetry: telemetry,
76
+ _guidance: guidance,
76
77
  };
77
78
  }
78
79
  export async function handleRemember(cwd, key, value) {
@@ -137,7 +138,12 @@ export async function handleRecall(cwd, key) {
137
138
  if (cached?.evidence?.length) {
138
139
  const cachedBody = cached.evidence.join('\n');
139
140
  const indexHealth = await getIndexHealthBlock(cwd);
140
- return wrapRecallResult(`${cachedBody}${indexHealth}`, candidateText, 'semantic-hit', Math.max(0, estimateTokenCount(candidateText) - estimateTokenCount(cachedBody)));
141
+ return wrapRecallResult(`${cachedBody}${indexHealth}`, candidateText, 'semantic-hit', Math.max(0, estimateTokenCount(candidateText) - estimateTokenCount(cachedBody)), cached.guidance ?? {
142
+ kind: 'memory',
143
+ query: key ?? 'all memories',
144
+ recommendation: key ? `Apply recalled memory "${key}" where relevant.` : 'Apply the recalled project memories where relevant.',
145
+ memoryRefs: key ? [{ id: key, label: key }] : [],
146
+ });
141
147
  }
142
148
  if (key) {
143
149
  const memory = store.memories[key];
@@ -164,6 +170,12 @@ export async function handleRecall(cwd, key) {
164
170
  const body = `RECALLED MEMORY [${key}]:\n${memory.value}\n\n(Stored: ${memory.timestamp})`;
165
171
  const indexHealth = await getIndexHealthBlock(cwd);
166
172
  const fullText = `${body}${indexHealth}`;
173
+ const guidance = {
174
+ kind: 'memory',
175
+ query: key,
176
+ recommendation: `Apply recalled memory "${key}" where relevant.`,
177
+ memoryRefs: [{ id: key, label: key }],
178
+ };
167
179
  await setSemanticQueryCache(cacheQuery, commitSha, {
168
180
  query: cacheQuery,
169
181
  resolvedOwner: 'memory',
@@ -172,8 +184,9 @@ export async function handleRecall(cwd, key) {
172
184
  evidence: [body],
173
185
  commitSha,
174
186
  confidence: 1,
187
+ guidance,
175
188
  }, cwd);
176
- return wrapRecallResult(fullText, candidateText, 'miss');
189
+ return wrapRecallResult(fullText, candidateText, 'miss', 0, guidance);
177
190
  }
178
191
  const keys = Object.keys(store.memories);
179
192
  if (keys.length === 0) {
@@ -207,6 +220,12 @@ export async function handleRecall(cwd, key) {
207
220
  text = "NO MEMORIES STORED. Use rigour_remember to persist important instructions.";
208
221
  }
209
222
  text += await getIndexHealthBlock(cwd);
223
+ const guidance = {
224
+ kind: 'memory',
225
+ query: 'all memories',
226
+ recommendation: `Apply ${cleanMemories.length} recalled project memory item(s) where relevant.`,
227
+ memoryRefs: keys.filter(k => !taintedKeys.includes(k)).map(k => ({ id: k, label: k })),
228
+ };
210
229
  await setSemanticQueryCache(cacheQuery, commitSha, {
211
230
  query: cacheQuery,
212
231
  resolvedOwner: 'memory',
@@ -215,8 +234,9 @@ export async function handleRecall(cwd, key) {
215
234
  evidence: [text],
216
235
  commitSha,
217
236
  confidence: 1,
237
+ guidance,
218
238
  }, cwd);
219
- return wrapRecallResult(text, candidateText, 'miss', Math.max(0, estimateTokenCount(candidateText) - estimateTokenCount(text)));
239
+ return wrapRecallResult(text, candidateText, 'miss', Math.max(0, estimateTokenCount(candidateText) - estimateTokenCount(text)), guidance);
220
240
  }
221
241
  export async function handleForget(cwd, key) {
222
242
  const store = await loadMemory(cwd);
@@ -68,9 +68,15 @@ export async function handleCheckPattern(cwd, patternName, type, intent, file) {
68
68
  text: appendContextFooter(cachedText, telemetry, 'proceed with implementation or rigour_check when done'),
69
69
  }],
70
70
  _telemetry: telemetry,
71
+ _guidance: cached.guidance ?? {
72
+ kind: 'pattern',
73
+ query: patternName,
74
+ recommendation: `Apply the cached pattern guidance for "${patternName}".`,
75
+ },
71
76
  };
72
77
  }
73
78
  let resultText = "";
79
+ let matchedPattern;
74
80
  // 0. File Guard — BLOCK writes to protected paths
75
81
  if (file) {
76
82
  const matched = await checkFileGuard(cwd, file);
@@ -88,6 +94,12 @@ export async function handleCheckPattern(cwd, patternName, type, intent, file) {
88
94
  return {
89
95
  content: [{ type: "text", text: appendContextFooter(resultText, telemetry) }],
90
96
  _telemetry: telemetry,
97
+ _guidance: {
98
+ kind: 'pattern',
99
+ query: patternName,
100
+ recommendation: `STOP. Do not modify protected path "${file}" without human review.`,
101
+ selectedFiles: [file],
102
+ },
91
103
  };
92
104
  }
93
105
  }
@@ -96,6 +108,7 @@ export async function handleCheckPattern(cwd, patternName, type, intent, file) {
96
108
  const matcher = new PatternMatcher(index);
97
109
  const matchResult = await matcher.match({ name: patternName, type, intent });
98
110
  if (matchResult.status === "FOUND_SIMILAR") {
111
+ matchedPattern = matchResult.matches[0].pattern;
99
112
  resultText += `🚨 PATTERN REINVENTION DETECTED\n`;
100
113
  resultText += `Similar pattern already exists: "${matchResult.matches[0].pattern.name}" in ${matchResult.matches[0].pattern.file}\n`;
101
114
  resultText += `SUGGESTION: ${matchResult.suggestion}\n\n`;
@@ -128,11 +141,12 @@ export async function handleCheckPattern(cwd, patternName, type, intent, file) {
128
141
  resultText += `\n`;
129
142
  }
130
143
  }
144
+ let recommendation = 'Proceed with implementation; no conflicting codebase pattern was found.';
131
145
  if (!resultText) {
132
146
  resultText = `✅ Pattern "${patternName}" is fresh, secure, and unique to the codebase.\n\nRECOMMENDED ACTION: Proceed with implementation.`;
133
147
  }
134
148
  else {
135
- let recommendation = "Proceed with caution, addressing the warnings above.";
149
+ recommendation = "Proceed with caution, addressing the warnings above.";
136
150
  if (resultText.includes("🚨 PATTERN REINVENTION")) {
137
151
  recommendation = "STOP and REUSE the existing pattern mentioned above. Do not create a duplicate.";
138
152
  }
@@ -144,6 +158,13 @@ export async function handleCheckPattern(cwd, patternName, type, intent, file) {
144
158
  }
145
159
  resultText += `\nRECOMMENDED ACTION: ${recommendation}`;
146
160
  }
161
+ const guidance = {
162
+ kind: 'pattern',
163
+ query: patternName,
164
+ recommendation,
165
+ patternRefs: matchedPattern ? [{ id: matchedPattern.id, label: matchedPattern.name, file: matchedPattern.file }] : [],
166
+ selectedFiles: file ? [file] : matchedPattern?.file ? [matchedPattern.file] : [],
167
+ };
147
168
  await setSemanticQueryCache(cacheQuery, commitSha, {
148
169
  query: cacheQuery,
149
170
  resolvedOwner: file ? path.dirname(file) : 'patterns',
@@ -152,6 +173,7 @@ export async function handleCheckPattern(cwd, patternName, type, intent, file) {
152
173
  evidence: [resultText],
153
174
  commitSha,
154
175
  confidence: resultText.includes('✅') ? 0.9 : 0.7,
176
+ guidance,
155
177
  }, cwd);
156
178
  const telemetry = buildTelemetryMeta({
157
179
  candidateText: indexScanEstimate,
@@ -167,6 +189,7 @@ export async function handleCheckPattern(cwd, patternName, type, intent, file) {
167
189
  text: appendContextFooter(resultText, telemetry, 'rigour_check before declaring done'),
168
190
  }],
169
191
  _telemetry: telemetry,
192
+ _guidance: guidance,
170
193
  };
171
194
  }
172
195
  export async function handleSecurityAudit(cwd) {
@@ -8,8 +8,32 @@ export type TelemetryMeta = {
8
8
  taskId?: string;
9
9
  agentId?: string;
10
10
  queryHash?: string;
11
+ candidateFiles?: number;
12
+ returnedFiles?: number;
11
13
  skipRecording?: boolean;
12
14
  };
15
+ export type GuidanceMeta = {
16
+ kind: 'context-scope' | 'pattern' | 'memory';
17
+ query?: string;
18
+ recommendation: string;
19
+ patternRefs?: Array<{
20
+ id: string;
21
+ label: string;
22
+ file?: string;
23
+ }>;
24
+ lessonRefs?: Array<{
25
+ id: string;
26
+ label: string;
27
+ visibility?: string;
28
+ }>;
29
+ memoryRefs?: Array<{
30
+ id: string;
31
+ label: string;
32
+ }>;
33
+ selectedFiles?: string[];
34
+ excludedFileCount?: number;
35
+ conflicts?: number;
36
+ };
13
37
  export type ToolResult = {
14
38
  content: {
15
39
  type: string;
@@ -20,6 +44,7 @@ export type ToolResult = {
20
44
  _rigour_report?: unknown;
21
45
  _meta?: unknown;
22
46
  _telemetry?: TelemetryMeta;
47
+ _guidance?: GuidanceMeta;
23
48
  };
24
49
  export declare function buildTelemetryMeta(opts: {
25
50
  candidateText: string;
@@ -29,6 +54,10 @@ export declare function buildTelemetryMeta(opts: {
29
54
  taskId?: string;
30
55
  agentId?: string;
31
56
  queryHash?: string;
57
+ candidateTokens?: number;
58
+ returnedTokens?: number;
59
+ candidateFiles?: number;
60
+ returnedFiles?: number;
32
61
  skipRecording?: boolean;
33
62
  }): TelemetryMeta;
34
63
  /** Resolve a short commit/ref fingerprint for cache keys. */
@@ -6,14 +6,16 @@ import path from 'path';
6
6
  import { estimateTokenCount } from '@rigour-labs/core';
7
7
  export function buildTelemetryMeta(opts) {
8
8
  return {
9
- candidateTokens: estimateTokenCount(opts.candidateText),
10
- returnedTokens: estimateTokenCount(opts.returnedText),
9
+ candidateTokens: opts.candidateTokens ?? estimateTokenCount(opts.candidateText),
10
+ returnedTokens: opts.returnedTokens ?? estimateTokenCount(opts.returnedText),
11
11
  cacheStatus: opts.cacheStatus,
12
12
  deduplicatedTokens: opts.deduplicatedTokens ?? 0,
13
13
  isEstimated: true,
14
14
  taskId: opts.taskId,
15
15
  agentId: opts.agentId,
16
16
  queryHash: opts.queryHash,
17
+ candidateFiles: opts.candidateFiles,
18
+ returnedFiles: opts.returnedFiles,
17
19
  skipRecording: opts.skipRecording,
18
20
  };
19
21
  }
@@ -0,0 +1,13 @@
1
+ import type { GuidanceMeta, TelemetryMeta } from './context-telemetry.js';
2
+ export declare function buildStudioImpact(guidance?: GuidanceMeta, telemetry?: TelemetryMeta): {
3
+ guidance: GuidanceMeta | undefined;
4
+ telemetry: {
5
+ candidateTokens: number;
6
+ returnedTokens: number;
7
+ candidateFiles: number | undefined;
8
+ returnedFiles: number | undefined;
9
+ cacheStatus: import("./context-telemetry.js").CacheStatus;
10
+ classification: "observed" | "measured estimate";
11
+ } | undefined;
12
+ };
13
+ export declare function buildMcpResultMeta(existingMeta: unknown, requestId: string, resourceUri: string, guidance?: GuidanceMeta, telemetry?: TelemetryMeta): Record<string, unknown>;
@@ -0,0 +1,43 @@
1
+ function classification(telemetry) {
2
+ return telemetry?.isEstimated ? 'measured estimate' : 'observed';
3
+ }
4
+ export function buildStudioImpact(guidance, telemetry) {
5
+ return {
6
+ guidance,
7
+ telemetry: telemetry ? {
8
+ candidateTokens: telemetry.candidateTokens,
9
+ returnedTokens: telemetry.returnedTokens,
10
+ candidateFiles: telemetry.candidateFiles,
11
+ returnedFiles: telemetry.returnedFiles,
12
+ cacheStatus: telemetry.cacheStatus,
13
+ classification: classification(telemetry),
14
+ } : undefined,
15
+ };
16
+ }
17
+ export function buildMcpResultMeta(existingMeta, requestId, resourceUri, guidance, telemetry) {
18
+ const base = existingMeta && typeof existingMeta === 'object' && !Array.isArray(existingMeta)
19
+ ? existingMeta
20
+ : {};
21
+ if (!guidance)
22
+ return { ...base, ui: { resourceUri } };
23
+ return {
24
+ ...base,
25
+ ui: { resourceUri },
26
+ rigourImpact: {
27
+ requestId,
28
+ classification: classification(telemetry),
29
+ guidance,
30
+ context: telemetry ? {
31
+ candidateTokens: telemetry.candidateTokens,
32
+ returnedTokens: telemetry.returnedTokens,
33
+ avoidedTokens: Math.max(0, telemetry.candidateTokens - telemetry.returnedTokens),
34
+ candidateFiles: telemetry.candidateFiles,
35
+ returnedFiles: telemetry.returnedFiles,
36
+ excludedFiles: typeof telemetry.candidateFiles === 'number' && typeof telemetry.returnedFiles === 'number'
37
+ ? Math.max(0, telemetry.candidateFiles - telemetry.returnedFiles)
38
+ : undefined,
39
+ cacheStatus: telemetry.cacheStatus,
40
+ } : undefined,
41
+ },
42
+ };
43
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rigour-labs/mcp",
3
- "version": "6.0.0",
3
+ "version": "6.1.0-beta.1",
4
4
  "description": "MCP server + live dashboard for AI code governance — OWASP LLM Top 10 (10/10), real-time MCP App UI, 25+ security patterns, Bayesian learning Brain, hallucinated import detection, multi-agent governance. Works with Claude, Cursor, VS Code, ChatGPT, Goose, Windsurf. Industry presets for HIPAA, SOC2, FedRAMP.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://rigour.run",
@@ -62,7 +62,7 @@
62
62
  "execa": "^8.0.1",
63
63
  "fs-extra": "^11.4.0",
64
64
  "yaml": "^2.8.2",
65
- "@rigour-labs/core": "6.0.0"
65
+ "@rigour-labs/core": "6.1.0-beta.1"
66
66
  },
67
67
  "devDependencies": {
68
68
  "@types/node": "^25.0.3",