@rigour-labs/mcp 5.2.9 → 5.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.
@@ -316,6 +316,14 @@ export declare const TOOL_DEFINITIONS: ({
316
316
  type: string;
317
317
  description: string;
318
318
  };
319
+ agentId: {
320
+ type: string;
321
+ description: string;
322
+ };
323
+ taskId: {
324
+ type: string;
325
+ description: string;
326
+ };
319
327
  cwd: {
320
328
  type: "string";
321
329
  description: string;
@@ -603,4 +611,116 @@ export declare const TOOL_DEFINITIONS: ({
603
611
  idempotentHint: boolean;
604
612
  openWorldHint: boolean;
605
613
  };
614
+ } | {
615
+ name: string;
616
+ description: string;
617
+ inputSchema: {
618
+ type: string;
619
+ properties: {
620
+ taskId: {
621
+ type: string;
622
+ description: string;
623
+ };
624
+ cwd: {
625
+ type: "string";
626
+ description: string;
627
+ };
628
+ };
629
+ required: string[];
630
+ };
631
+ annotations: {
632
+ title: string;
633
+ readOnlyHint: boolean;
634
+ destructiveHint: boolean;
635
+ idempotentHint: boolean;
636
+ openWorldHint: boolean;
637
+ };
638
+ } | {
639
+ name: string;
640
+ description: string;
641
+ inputSchema: {
642
+ type: string;
643
+ properties: {
644
+ target: {
645
+ type: string;
646
+ description: string;
647
+ };
648
+ taskId: {
649
+ type: string;
650
+ description: string;
651
+ };
652
+ cwd: {
653
+ type: "string";
654
+ description: string;
655
+ };
656
+ };
657
+ required: string[];
658
+ };
659
+ annotations: {
660
+ title: string;
661
+ readOnlyHint: boolean;
662
+ destructiveHint: boolean;
663
+ idempotentHint: boolean;
664
+ openWorldHint: boolean;
665
+ };
666
+ } | {
667
+ name: string;
668
+ description: string;
669
+ inputSchema: {
670
+ type: string;
671
+ properties: {
672
+ semantic: {
673
+ type: string;
674
+ description: string;
675
+ };
676
+ force: {
677
+ type: string;
678
+ description: string;
679
+ };
680
+ output: {
681
+ type: string;
682
+ description: string;
683
+ };
684
+ cwd: {
685
+ type: "string";
686
+ description: string;
687
+ };
688
+ };
689
+ required: string[];
690
+ };
691
+ annotations: {
692
+ title: string;
693
+ readOnlyHint: boolean;
694
+ destructiveHint: boolean;
695
+ idempotentHint: boolean;
696
+ openWorldHint: boolean;
697
+ };
698
+ } | {
699
+ name: string;
700
+ description: string;
701
+ inputSchema: {
702
+ type: string;
703
+ properties: {
704
+ query: {
705
+ type: string;
706
+ description: string;
707
+ };
708
+ limit: {
709
+ type: string;
710
+ description: string;
711
+ };
712
+ cwd: {
713
+ type: "string";
714
+ description: string;
715
+ };
716
+ };
717
+ required: string[];
718
+ };
719
+ annotations: {
720
+ title: string;
721
+ readOnlyHint: boolean;
722
+ destructiveHint: boolean;
723
+ idempotentHint: boolean;
724
+ openWorldHint: boolean;
725
+ };
606
726
  })[];
@@ -192,7 +192,7 @@ export const TOOL_DEFINITIONS = [
192
192
  },
193
193
  {
194
194
  name: "rigour_recall",
195
- description: "Load project memory and stored conventions. CALL THIS at the start of every coding task to restore team decisions, naming conventions, and architectural preferences stored from previous sessions.",
195
+ description: "Load project memory and stored conventions. CALL THIS at the START of every coding task (before reading files) to restore team decisions, naming conventions, and architectural preferences. Returns index health status and uses semantic cache on repeat calls — second recall with the same key is served from cache.",
196
196
  inputSchema: {
197
197
  type: "object",
198
198
  properties: {
@@ -332,15 +332,17 @@ export const TOOL_DEFINITIONS = [
332
332
  },
333
333
  {
334
334
  name: "rigour_checkpoint",
335
- description: "Record a quality checkpoint during long-running agent execution. Use periodically (every 15-30 min) to enable drift detection and quality monitoring. Essential for GPT-5.3 coworking mode.",
335
+ description: "Record a quality checkpoint during long-running agent execution. Use periodically (every 15-30 min) to enable drift detection, quality monitoring, and compact subagent handoffs. Triggers incremental pattern index refresh when filesChanged is provided. Essential for GPT-5.3 coworking mode — call BEFORE rigour_handoff to compress context under 2K tokens.",
336
336
  inputSchema: {
337
337
  type: "object",
338
338
  properties: {
339
339
  ...cwdParam(),
340
340
  progressPct: { type: "number", description: "Estimated progress percentage (0-100)." },
341
- filesChanged: { type: "array", items: { type: "string" }, description: "List of files modified since last checkpoint." },
341
+ filesChanged: { type: "array", items: { type: "string" }, description: "List of files modified since last checkpoint. Triggers incremental index refresh." },
342
342
  summary: { type: "string", description: "Brief description of work done since last checkpoint." },
343
343
  qualityScore: { type: "number", description: "Self-assessed quality score (0-100). Be honest - artificially high scores trigger drift detection." },
344
+ agentId: { type: "string", description: "Optional agent ID for checkpoint packet binding." },
345
+ taskId: { type: "string", description: "Optional task ID for checkpoint metrics." },
344
346
  },
345
347
  required: ["cwd", "progressPct", "summary", "qualityScore"],
346
348
  },
@@ -525,4 +527,123 @@ export const TOOL_DEFINITIONS = [
525
527
  openWorldHint: false,
526
528
  },
527
529
  },
530
+ // ─── Context Telemetry & Cost Efficiency ───────────────
531
+ {
532
+ name: "rigour_context_stats",
533
+ description: "Returns context retrieval efficiency, candidate tokens vs returned tokens, potential avoided tokens, cache hit rate, and repeated reads prevented.",
534
+ inputSchema: {
535
+ type: "object",
536
+ properties: {
537
+ ...cwdParam(),
538
+ taskId: { type: "string", description: "Optional Task ID (e.g. 'CTP-142') to filter statistics." },
539
+ },
540
+ required: ["cwd"],
541
+ },
542
+ annotations: {
543
+ title: "Context Stats",
544
+ readOnlyHint: true,
545
+ destructiveHint: false,
546
+ idempotentHint: true,
547
+ openWorldHint: false,
548
+ },
549
+ },
550
+ {
551
+ name: "rigour_task_cost",
552
+ description: "Returns both verified actual model usage/cost (from Cursor Admin API or imported CSV) and Rigour estimated avoided context/cost USD.",
553
+ inputSchema: {
554
+ type: "object",
555
+ properties: {
556
+ ...cwdParam(),
557
+ taskId: { type: "string", description: "Optional Task ID (e.g. 'CTP-142') to filter cost stats." },
558
+ },
559
+ required: ["cwd"],
560
+ },
561
+ annotations: {
562
+ title: "Task Cost Breakdown",
563
+ readOnlyHint: true,
564
+ destructiveHint: false,
565
+ idempotentHint: true,
566
+ openWorldHint: false,
567
+ },
568
+ },
569
+ {
570
+ name: "rigour_cache_stats",
571
+ description: "Returns detailed performance stats across all 4 cache layers (exact hits, semantic hits, partial hits, misses, hit rate, tokens served from cache).",
572
+ inputSchema: {
573
+ type: "object",
574
+ properties: {
575
+ ...cwdParam(),
576
+ },
577
+ required: ["cwd"],
578
+ },
579
+ annotations: {
580
+ title: "Cache Efficiency Stats",
581
+ readOnlyHint: true,
582
+ destructiveHint: false,
583
+ idempotentHint: true,
584
+ openWorldHint: false,
585
+ },
586
+ },
587
+ {
588
+ name: "rigour_context_explain",
589
+ description: "Audits why specific files/services were included or excluded, cache hit/miss status, invalidation reasons, and prior agent requests.",
590
+ inputSchema: {
591
+ type: "object",
592
+ properties: {
593
+ ...cwdParam(),
594
+ target: { type: "string", description: "File path, service name, or query term to explain." },
595
+ taskId: { type: "string", description: "Optional Task ID to restrict audit trace." },
596
+ },
597
+ required: ["cwd", "target"],
598
+ },
599
+ annotations: {
600
+ title: "Context Explainability Audit",
601
+ readOnlyHint: true,
602
+ destructiveHint: false,
603
+ idempotentHint: true,
604
+ openWorldHint: false,
605
+ },
606
+ },
607
+ // ─── Pattern Index & Scoped Context ──────────────────
608
+ {
609
+ name: "rigour_index",
610
+ description: "Build or update the Rigour pattern index (.rigour/patterns.json). CALL THIS when the index is missing or stale — before rigour_context_scope or rigour_check_pattern. One AST pass extracts functions, classes, routes, and signatures for reuse. Use semantic=true for embedding-based search.",
611
+ inputSchema: {
612
+ type: "object",
613
+ properties: {
614
+ ...cwdParam(),
615
+ semantic: { type: "boolean", description: "Generate semantic embeddings for better matching (requires Transformers.js). Default: false." },
616
+ force: { type: "boolean", description: "Force a full rebuild instead of incremental update. Default: false." },
617
+ output: { type: "string", description: "Custom path for the index file." },
618
+ },
619
+ required: ["cwd"],
620
+ },
621
+ annotations: {
622
+ title: "Build Pattern Index",
623
+ readOnlyHint: false,
624
+ destructiveHint: false,
625
+ idempotentHint: true,
626
+ openWorldHint: false,
627
+ },
628
+ },
629
+ {
630
+ name: "rigour_context_scope",
631
+ description: "CALL THIS BEFORE reading source files. Returns a minimal editScope (3-10 files) with signatures from the pattern index instead of full file bodies. Uses semantic search when embeddings are available. If index is missing, instructs to call rigour_index first. Highest-impact token saver in the Rigour protocol.",
632
+ inputSchema: {
633
+ type: "object",
634
+ properties: {
635
+ ...cwdParam(),
636
+ query: { type: "string", description: "Natural-language description of what you need to work on (e.g. 'add priority field to task service')." },
637
+ limit: { type: "number", description: "Maximum number of pattern matches to return (default: 10)." },
638
+ },
639
+ required: ["cwd", "query"],
640
+ },
641
+ annotations: {
642
+ title: "Scoped Context Retrieval",
643
+ readOnlyHint: true,
644
+ destructiveHint: false,
645
+ idempotentHint: true,
646
+ openWorldHint: false,
647
+ },
648
+ },
528
649
  ];
@@ -0,0 +1,6 @@
1
+ import { type ToolResult } from '../utils/context-telemetry.js';
2
+ export declare function handleIndex(cwd: string, options?: {
3
+ semantic?: boolean;
4
+ force?: boolean;
5
+ output?: string;
6
+ }): Promise<ToolResult>;
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Pattern Index MCP Tool Handler
3
+ *
4
+ * Wraps PatternIndexer for agent-accessible index build/update.
5
+ */
6
+ import path from 'path';
7
+ import { PatternIndexer, savePatternIndex, loadPatternIndex, getDefaultIndexPath, } from '@rigour-labs/core/pattern-index';
8
+ import { setStaticCache, setComponentCache, } from '@rigour-labs/core';
9
+ import { notifyProgress } from '../utils/notifications.js';
10
+ import { buildTelemetryMeta, getWorkspaceCommitSha } from '../utils/context-telemetry.js';
11
+ import { appendContextFooter } from '../utils/context-footer.js';
12
+ import fs from 'fs-extra';
13
+ async function syncIndexToCache(cwd, index) {
14
+ const commitSha = await getWorkspaceCommitSha(cwd);
15
+ const repo = path.basename(cwd);
16
+ const byFile = new Map();
17
+ for (const pattern of index.patterns) {
18
+ const list = byFile.get(pattern.file) ?? [];
19
+ list.push(pattern);
20
+ byFile.set(pattern.file, list);
21
+ }
22
+ for (const [filePath, patterns] of byFile) {
23
+ const absPath = path.join(cwd, filePath);
24
+ let content = '';
25
+ try {
26
+ if (await fs.pathExists(absPath)) {
27
+ content = await fs.readFile(absPath, 'utf-8');
28
+ }
29
+ }
30
+ catch {
31
+ content = patterns.map(p => p.signature ?? p.name).join('\n');
32
+ }
33
+ await setStaticCache(repo, 'main', filePath, content || filePath, {
34
+ exports: patterns.map(p => p.name),
35
+ rigourPatterns: patterns.map(p => `${p.type}:${p.name}`),
36
+ ownership: path.dirname(filePath),
37
+ }, cwd);
38
+ const componentName = path.dirname(filePath) || filePath;
39
+ const dossier = {
40
+ component: componentName,
41
+ responsibility: `Indexed patterns in ${filePath}`,
42
+ canonicalFiles: [filePath],
43
+ contracts: patterns.filter(p => p.type === 'interface' || p.type === 'type').map(p => p.name),
44
+ directConsumers: [],
45
+ validationCommands: [],
46
+ };
47
+ await setComponentCache(componentName, commitSha, dossier, `index-${index.lastUpdated}`, '3', cwd);
48
+ }
49
+ }
50
+ export async function handleIndex(cwd, options = {}) {
51
+ const indexPath = options.output || getDefaultIndexPath(cwd);
52
+ const candidateEstimate = 'Full codebase AST scan for pattern extraction';
53
+ try {
54
+ notifyProgress('info', 'Building pattern index...');
55
+ const indexer = new PatternIndexer(cwd, { useEmbeddings: options.semantic ?? false });
56
+ const existingIndex = await loadPatternIndex(indexPath);
57
+ let index;
58
+ if (existingIndex && !options.force) {
59
+ index = await indexer.updateIndex(existingIndex);
60
+ }
61
+ else {
62
+ index = await indexer.buildIndex();
63
+ }
64
+ await savePatternIndex(index, indexPath);
65
+ await syncIndexToCache(cwd, index);
66
+ notifyProgress('info', 'Pattern index complete');
67
+ const byType = Object.entries(index.stats.byType)
68
+ .map(([type, count]) => `${type}: ${count}`)
69
+ .join(', ');
70
+ let text = `✅ PATTERN INDEX ${options.force ? 'REBUILT' : 'UPDATED'}\n\n`;
71
+ text += `- Total Patterns: ${index.stats.totalPatterns}\n`;
72
+ text += `- Total Files: ${index.stats.totalFiles}\n`;
73
+ text += `- Index Path: ${indexPath}\n`;
74
+ text += `- Duration: ${index.stats.indexDurationMs}ms\n`;
75
+ if (options.semantic)
76
+ text += `- Semantic Search: Enabled\n`;
77
+ text += `- Types: ${byType}\n\n`;
78
+ text += `Index synced to context cache layers. Use rigour_context_scope before reading files.`;
79
+ const telemetry = buildTelemetryMeta({
80
+ candidateText: candidateEstimate,
81
+ returnedText: text,
82
+ cacheStatus: 'miss',
83
+ });
84
+ return {
85
+ content: [{ type: 'text', text: appendContextFooter(text, telemetry, 'rigour_context_scope("your task")') }],
86
+ _telemetry: telemetry,
87
+ };
88
+ }
89
+ catch (error) {
90
+ return {
91
+ content: [{ type: 'text', text: `RIGOUR ERROR: Failed to build pattern index: ${error.message}` }],
92
+ isError: true,
93
+ };
94
+ }
95
+ }
@@ -1,10 +1,4 @@
1
- type ToolResult = {
2
- content: {
3
- type: string;
4
- text: string;
5
- }[];
6
- };
1
+ import { type ToolResult } from '../utils/context-telemetry.js';
7
2
  export declare function handleRemember(cwd: string, key: string, value: string): Promise<ToolResult>;
8
3
  export declare function handleRecall(cwd: string, key?: string): Promise<ToolResult>;
9
4
  export declare function handleForget(cwd: string, key: string): Promise<ToolResult>;
10
- export {};
@@ -10,7 +10,10 @@
10
10
  * @since v4.2.0 — DLP gate on memory persistence
11
11
  */
12
12
  import { loadMemory, saveMemory } from '../utils/config.js';
13
- import { scanInputForCredentials, formatDLPAlert, createDLPAuditEntry } from '@rigour-labs/core';
13
+ import { scanInputForCredentials, formatDLPAlert, createDLPAuditEntry, getSemanticQueryCache, setSemanticQueryCache, estimateTokenCount, } from '@rigour-labs/core';
14
+ import { loadPatternIndex, getDefaultIndexPath, } from '@rigour-labs/core/pattern-index';
15
+ import { buildTelemetryMeta, getWorkspaceCommitSha } from '../utils/context-telemetry.js';
16
+ import { appendContextFooter } from '../utils/context-footer.js';
14
17
  import fs from 'fs-extra';
15
18
  import path from 'path';
16
19
  /**
@@ -49,6 +52,29 @@ function extractStrings(obj, out) {
49
52
  extractStrings(v, out);
50
53
  }
51
54
  }
55
+ async function getIndexHealthBlock(cwd) {
56
+ const indexPath = getDefaultIndexPath(cwd);
57
+ const index = await loadPatternIndex(indexPath);
58
+ if (!index) {
59
+ return '\n\n📊 Pattern Index: NOT FOUND — call rigour_index to enable scope optimization and reinvention detection.';
60
+ }
61
+ return `\n\n📊 Pattern Index: ${index.stats.totalPatterns} patterns across ${index.stats.totalFiles} files (updated ${index.lastUpdated}).`;
62
+ }
63
+ function wrapRecallResult(text, candidateText, cacheStatus, deduplicatedTokens = 0) {
64
+ const telemetry = buildTelemetryMeta({
65
+ candidateText,
66
+ returnedText: text,
67
+ cacheStatus,
68
+ deduplicatedTokens,
69
+ });
70
+ return {
71
+ content: [{
72
+ type: 'text',
73
+ text: appendContextFooter(text, telemetry, 'rigour_context_scope("your task")'),
74
+ }],
75
+ _telemetry: telemetry,
76
+ };
77
+ }
52
78
  export async function handleRemember(cwd, key, value) {
53
79
  // Fallback: if key is missing but value exists, auto-generate a key
54
80
  if (!key && value) {
@@ -103,11 +129,21 @@ export async function handleRemember(cwd, key, value) {
103
129
  };
104
130
  }
105
131
  export async function handleRecall(cwd, key) {
132
+ const commitSha = await getWorkspaceCommitSha(cwd);
133
+ const cacheQuery = key ? `recall:${key}` : 'recall:all';
106
134
  const store = await loadMemory(cwd);
135
+ const candidateText = JSON.stringify(store);
136
+ const cached = await getSemanticQueryCache(cacheQuery, commitSha, cwd);
137
+ if (cached?.evidence?.length) {
138
+ const cachedBody = cached.evidence.join('\n');
139
+ const indexHealth = await getIndexHealthBlock(cwd);
140
+ return wrapRecallResult(`${cachedBody}${indexHealth}`, candidateText, 'semantic-hit', Math.max(0, estimateTokenCount(candidateText) - estimateTokenCount(cachedBody)));
141
+ }
107
142
  if (key) {
108
143
  const memory = store.memories[key];
109
144
  if (!memory) {
110
- return { content: [{ type: "text", text: `NO MEMORY FOUND for key "${key}". Use rigour_remember to store instructions.` }] };
145
+ const text = `NO MEMORY FOUND for key "${key}". Use rigour_remember to store instructions.${await getIndexHealthBlock(cwd)}`;
146
+ return wrapRecallResult(text, candidateText, 'miss');
111
147
  }
112
148
  // ── DLP Gate on recall: catch credentials stored before DLP existed ──
113
149
  const dlpResult = scanInputForCredentials(memory.value);
@@ -125,11 +161,24 @@ export async function handleRecall(cwd, key) {
125
161
  }],
126
162
  };
127
163
  }
128
- return { content: [{ type: "text", text: `RECALLED MEMORY [${key}]:\n${memory.value}\n\n(Stored: ${memory.timestamp})` }] };
164
+ const body = `RECALLED MEMORY [${key}]:\n${memory.value}\n\n(Stored: ${memory.timestamp})`;
165
+ const indexHealth = await getIndexHealthBlock(cwd);
166
+ const fullText = `${body}${indexHealth}`;
167
+ await setSemanticQueryCache(cacheQuery, commitSha, {
168
+ query: cacheQuery,
169
+ resolvedOwner: 'memory',
170
+ editScope: [],
171
+ validationScope: [],
172
+ evidence: [body],
173
+ commitSha,
174
+ confidence: 1,
175
+ }, cwd);
176
+ return wrapRecallResult(fullText, candidateText, 'miss');
129
177
  }
130
178
  const keys = Object.keys(store.memories);
131
179
  if (keys.length === 0) {
132
- return { content: [{ type: "text", text: "NO MEMORIES STORED. Use rigour_remember to persist important instructions." }] };
180
+ const text = `NO MEMORIES STORED. Use rigour_remember to persist important instructions.${await getIndexHealthBlock(cwd)}`;
181
+ return wrapRecallResult(text, candidateText, 'miss');
133
182
  }
134
183
  // ── DLP scan all memories on bulk recall ──
135
184
  const cleanMemories = [];
@@ -157,7 +206,17 @@ export async function handleRecall(cwd, key) {
157
206
  else {
158
207
  text = "NO MEMORIES STORED. Use rigour_remember to persist important instructions.";
159
208
  }
160
- return { content: [{ type: "text", text }] };
209
+ text += await getIndexHealthBlock(cwd);
210
+ await setSemanticQueryCache(cacheQuery, commitSha, {
211
+ query: cacheQuery,
212
+ resolvedOwner: 'memory',
213
+ editScope: [],
214
+ validationScope: [],
215
+ evidence: [text],
216
+ commitSha,
217
+ confidence: 1,
218
+ }, cwd);
219
+ return wrapRecallResult(text, candidateText, 'miss', Math.max(0, estimateTokenCount(candidateText) - estimateTokenCount(text)));
161
220
  }
162
221
  export async function handleForget(cwd, key) {
163
222
  const store = await loadMemory(cwd);
@@ -1,9 +1,3 @@
1
- type ToolResult = {
2
- content: {
3
- type: string;
4
- text: string;
5
- }[];
6
- };
1
+ import { type ToolResult } from '../utils/context-telemetry.js';
7
2
  export declare function handleCheckPattern(cwd: string, patternName: string, type?: string, intent?: string, file?: string): Promise<ToolResult>;
8
3
  export declare function handleSecurityAudit(cwd: string): Promise<ToolResult>;
9
- export {};
@@ -9,8 +9,10 @@ import path from "path";
9
9
  import yaml from "yaml";
10
10
  import fs from "fs-extra";
11
11
  import { PatternMatcher, loadPatternIndex, getDefaultIndexPath, StalenessDetector, SecurityDetector, } from "@rigour-labs/core/pattern-index";
12
- import { ConfigSchema } from "@rigour-labs/core";
12
+ import { ConfigSchema, getSemanticQueryCache, setSemanticQueryCache, estimateTokenCount } from "@rigour-labs/core";
13
13
  import { notifyProgress } from '../utils/notifications.js';
14
+ import { buildTelemetryMeta, getWorkspaceCommitSha } from '../utils/context-telemetry.js';
15
+ import { appendContextFooter } from '../utils/context-footer.js';
14
16
  /**
15
17
  * Check if a file path is protected by safety.protected_paths in rigour.yml.
16
18
  * Returns the matched pattern or null.
@@ -41,7 +43,33 @@ async function checkFileGuard(cwd, filePath) {
41
43
  return normalized.startsWith(clean + '/');
42
44
  }) ?? null;
43
45
  }
46
+ function buildCacheQuery(patternName, type, intent, file) {
47
+ return `check_pattern:${patternName}:${type ?? ''}:${intent ?? ''}:${file ?? ''}`;
48
+ }
44
49
  export async function handleCheckPattern(cwd, patternName, type, intent, file) {
50
+ const commitSha = await getWorkspaceCommitSha(cwd);
51
+ const cacheQuery = buildCacheQuery(patternName, type, intent, file);
52
+ const indexPath = getDefaultIndexPath(cwd);
53
+ const index = await loadPatternIndex(indexPath);
54
+ const indexScanEstimate = index
55
+ ? `Pattern index scan (${index.stats.totalPatterns} patterns) for ${patternName}`
56
+ : `Full pattern discovery for ${patternName}`;
57
+ const cached = await getSemanticQueryCache(cacheQuery, commitSha, cwd);
58
+ if (cached?.evidence?.length) {
59
+ const cachedText = cached.evidence.join('\n');
60
+ const telemetry = buildTelemetryMeta({
61
+ candidateText: indexScanEstimate,
62
+ returnedText: cachedText,
63
+ cacheStatus: 'exact-hit',
64
+ });
65
+ return {
66
+ content: [{
67
+ type: 'text',
68
+ text: appendContextFooter(cachedText, telemetry, 'proceed with implementation or rigour_check when done'),
69
+ }],
70
+ _telemetry: telemetry,
71
+ };
72
+ }
45
73
  let resultText = "";
46
74
  // 0. File Guard — BLOCK writes to protected paths
47
75
  if (file) {
@@ -52,12 +80,18 @@ export async function handleCheckPattern(cwd, patternName, type, intent, file) {
52
80
  resultText += `This path matches protected pattern "${matched}" in rigour.yml.\n`;
53
81
  resultText += `CI/CD pipelines, governance configs, and protected docs require human review.\n\n`;
54
82
  resultText += `RECOMMENDED ACTION: STOP. Do not create or modify this file. Ask the human to make this change manually.`;
55
- return { content: [{ type: "text", text: resultText }] };
83
+ const telemetry = buildTelemetryMeta({
84
+ candidateText: indexScanEstimate,
85
+ returnedText: resultText,
86
+ cacheStatus: 'miss',
87
+ });
88
+ return {
89
+ content: [{ type: "text", text: appendContextFooter(resultText, telemetry) }],
90
+ _telemetry: telemetry,
91
+ };
56
92
  }
57
93
  }
58
94
  // 1. Check for Reinvention
59
- const indexPath = getDefaultIndexPath(cwd);
60
- const index = await loadPatternIndex(indexPath);
61
95
  if (index) {
62
96
  const matcher = new PatternMatcher(index);
63
97
  const matchResult = await matcher.match({ name: patternName, type, intent });
@@ -68,7 +102,7 @@ export async function handleCheckPattern(cwd, patternName, type, intent, file) {
68
102
  }
69
103
  }
70
104
  else {
71
- resultText += `⚠️ Pattern index not found. Run 'rigour index' to enable reinvention detection.\n\n`;
105
+ resultText += `⚠️ Pattern index not found. Run rigour_index to enable reinvention detection.\n\n`;
72
106
  }
73
107
  // 2. Check for Staleness/Best Practices
74
108
  const detector = new StalenessDetector(cwd);
@@ -110,12 +144,42 @@ export async function handleCheckPattern(cwd, patternName, type, intent, file) {
110
144
  }
111
145
  resultText += `\nRECOMMENDED ACTION: ${recommendation}`;
112
146
  }
113
- return { content: [{ type: "text", text: resultText }] };
147
+ await setSemanticQueryCache(cacheQuery, commitSha, {
148
+ query: cacheQuery,
149
+ resolvedOwner: file ? path.dirname(file) : 'patterns',
150
+ editScope: file ? [file] : [],
151
+ validationScope: [],
152
+ evidence: [resultText],
153
+ commitSha,
154
+ confidence: resultText.includes('✅') ? 0.9 : 0.7,
155
+ }, cwd);
156
+ const telemetry = buildTelemetryMeta({
157
+ candidateText: indexScanEstimate,
158
+ returnedText: resultText,
159
+ cacheStatus: 'miss',
160
+ deduplicatedTokens: index
161
+ ? Math.max(0, estimateTokenCount(indexScanEstimate) - estimateTokenCount(resultText))
162
+ : 0,
163
+ });
164
+ return {
165
+ content: [{
166
+ type: "text",
167
+ text: appendContextFooter(resultText, telemetry, 'rigour_check before declaring done'),
168
+ }],
169
+ _telemetry: telemetry,
170
+ };
114
171
  }
115
172
  export async function handleSecurityAudit(cwd) {
116
173
  notifyProgress("info", "Running CVE security audit...");
117
174
  const security = new SecurityDetector(cwd);
118
175
  const summary = await security.getSecuritySummary();
119
176
  notifyProgress("info", "Security audit complete");
120
- return { content: [{ type: "text", text: summary }] };
177
+ return {
178
+ content: [{ type: "text", text: summary }],
179
+ _telemetry: buildTelemetryMeta({
180
+ candidateText: summary,
181
+ returnedText: summary,
182
+ cacheStatus: 'none',
183
+ }),
184
+ };
121
185
  }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Closed-loop context efficiency footers appended to MCP tool responses.
3
+ */
4
+ import type { TelemetryMeta } from './context-telemetry.js';
5
+ export declare function formatContextFooter(meta: TelemetryMeta, nextStep?: string): string;
6
+ export declare function appendContextFooter(text: string, meta: TelemetryMeta | undefined, nextStep?: string): string;
@@ -0,0 +1,19 @@
1
+ export function formatContextFooter(meta, nextStep) {
2
+ const saved = Math.max(0, meta.candidateTokens - meta.returnedTokens);
3
+ let footer = `\n\n[Rigour Context] cache=${meta.cacheStatus}`;
4
+ if (saved > 0) {
5
+ footer += ` | saved~${saved.toLocaleString()} tokens`;
6
+ }
7
+ if (meta.deduplicatedTokens && meta.deduplicatedTokens > 0) {
8
+ footer += ` | dedup~${meta.deduplicatedTokens.toLocaleString()} tokens`;
9
+ }
10
+ if (nextStep) {
11
+ footer += ` | Next: ${nextStep}`;
12
+ }
13
+ return footer;
14
+ }
15
+ export function appendContextFooter(text, meta, nextStep) {
16
+ if (!meta)
17
+ return text;
18
+ return text + formatContextFooter(meta, nextStep);
19
+ }