@rigour-labs/mcp 6.5.1 → 6.5.3

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.
@@ -5,7 +5,7 @@
5
5
  */
6
6
  import path from 'path';
7
7
  import { PatternIndexer, savePatternIndex, loadPatternIndex, getDefaultIndexPath, } from '@rigour-labs/core/pattern-index';
8
- import { setStaticCache, setComponentCache, } from '@rigour-labs/core';
8
+ import { setStaticCache, setComponentCache, recordIndexChoice, } from '@rigour-labs/core';
9
9
  import { notifyProgress } from '../utils/notifications.js';
10
10
  import { buildTelemetryMeta, getWorkspaceCommitSha } from '../utils/context-telemetry.js';
11
11
  import { appendContextFooter } from '../utils/context-footer.js';
@@ -52,7 +52,8 @@ 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 ?? true });
55
+ const semantic = options.semantic ?? true;
56
+ const indexer = new PatternIndexer(cwd, { useEmbeddings: semantic });
56
57
  const existingIndex = await loadPatternIndex(indexPath);
57
58
  let index;
58
59
  if (existingIndex && !options.force) {
@@ -63,17 +64,17 @@ export async function handleIndex(cwd, options = {}) {
63
64
  }
64
65
  await savePatternIndex(index, indexPath);
65
66
  await syncIndexToCache(cwd, index);
67
+ await recordIndexChoice(cwd, index, semantic);
66
68
  notifyProgress('info', 'Pattern index complete');
67
69
  const byType = Object.entries(index.stats.byType)
68
70
  .map(([type, count]) => `${type}: ${count}`)
69
71
  .join(', ');
70
- let text = `✅ PATTERN INDEX ${options.force ? 'REBUILT' : 'UPDATED'}\n\n`;
72
+ let text = `✅ PATTERN INDEX ${existingIndex && !options.force ? 'UPDATED' : 'BUILT'}\n\n`;
71
73
  text += `- Total Patterns: ${index.stats.totalPatterns}\n`;
72
74
  text += `- Total Files: ${index.stats.totalFiles}\n`;
73
75
  text += `- Index Path: ${indexPath}\n`;
74
76
  text += `- Duration: ${index.stats.indexDurationMs}ms\n`;
75
- if (options.semantic)
76
- text += `- Semantic Search: Enabled\n`;
77
+ text += `- Search by meaning: ${semantic ? (index.patterns.some(p => p.embedding?.length) ? 'on' : 'unavailable (local model did not load; name matching still works)') : 'off'}\n`;
77
78
  text += `- Types: ${byType}\n\n`;
78
79
  text += `Index synced to context cache layers. Use rigour_context_scope before reading files.`;
79
80
  const telemetry = buildTelemetryMeta({
@@ -1,4 +1,27 @@
1
- import { type ToolResult } from '../utils/context-telemetry.js';
2
- export declare function handleRemember(cwd: string, key: string, value: string): Promise<ToolResult>;
3
- export declare function handleRecall(cwd: string, key?: string): Promise<ToolResult>;
4
- export declare function handleForget(cwd: string, key: string): Promise<ToolResult>;
1
+ /**
2
+ * Memory Persistence Tool Handlers
3
+ *
4
+ * Handlers for: rigour_remember, rigour_forget (rigour_recall is in memory-recall.ts)
5
+ *
6
+ * Three scopes: `repo` (.rigour/memory.json in this checkout), `user`
7
+ * (~/.rigour/memory.json, every repository) and `team` (stored for the repo and
8
+ * shared as a team lesson candidate; teammates' agents receive it once a
9
+ * person promotes it).
10
+ *
11
+ * DLP enforcement: rigour_remember scans BOTH key and value for
12
+ * credentials before persisting. Blocked if secrets detected.
13
+ *
14
+ * @since v2.17.0 — extracted from monolithic index.ts
15
+ * @since v4.2.0 — DLP gate on memory persistence
16
+ */
17
+ import { type LocalMemoryScope } from '../utils/config.js';
18
+ import { type GuidanceMeta, type ToolResult } from '../utils/context-telemetry.js';
19
+ /**
20
+ * Append a DLP audit event to .rigour/events.jsonl
21
+ */
22
+ export declare function appendDLPAudit(cwd: string, entry: Record<string, unknown>): Promise<void>;
23
+ export declare function getIndexHealthBlock(cwd: string): Promise<string>;
24
+ export declare function wrapRecallResult(text: string, candidateText: string, cacheStatus: 'exact-hit' | 'semantic-hit' | 'miss', deduplicatedTokens?: number, guidance?: GuidanceMeta): ToolResult;
25
+ export type MemoryScope = LocalMemoryScope | 'team';
26
+ export declare function handleRemember(cwd: string, key: string, value: string, scope?: MemoryScope): Promise<ToolResult>;
27
+ export declare function handleForget(cwd: string, key: string, scope?: LocalMemoryScope): Promise<ToolResult>;
@@ -1,7 +1,12 @@
1
1
  /**
2
2
  * Memory Persistence Tool Handlers
3
3
  *
4
- * Handlers for: rigour_remember, rigour_recall, rigour_forget
4
+ * Handlers for: rigour_remember, rigour_forget (rigour_recall is in memory-recall.ts)
5
+ *
6
+ * Three scopes: `repo` (.rigour/memory.json in this checkout), `user`
7
+ * (~/.rigour/memory.json, every repository) and `team` (stored for the repo and
8
+ * shared as a team lesson candidate; teammates' agents receive it once a
9
+ * person promotes it).
5
10
  *
6
11
  * DLP enforcement: rigour_remember scans BOTH key and value for
7
12
  * credentials before persisting. Blocked if secrets detected.
@@ -10,16 +15,16 @@
10
15
  * @since v4.2.0 — DLP gate on memory persistence
11
16
  */
12
17
  import { loadMemory, saveMemory } from '../utils/config.js';
13
- import { scanInputForCredentials, formatDLPAlert, createDLPAuditEntry, getSemanticQueryCache, setSemanticQueryCache, estimateTokenCount, } from '@rigour-labs/core';
18
+ import { scanInputForCredentials, formatDLPAlert, createDLPAuditEntry, loadTeamConfiguration, shareMemoryLesson, } from '@rigour-labs/core';
14
19
  import { loadPatternIndex, getDefaultIndexPath, } from '@rigour-labs/core/pattern-index';
15
- import { buildTelemetryMeta, getWorkspaceCommitSha } from '../utils/context-telemetry.js';
20
+ import { buildTelemetryMeta } from '../utils/context-telemetry.js';
16
21
  import { appendContextFooter } from '../utils/context-footer.js';
17
22
  import fs from 'fs-extra';
18
23
  import path from 'path';
19
24
  /**
20
25
  * Append a DLP audit event to .rigour/events.jsonl
21
26
  */
22
- async function appendDLPAudit(cwd, entry) {
27
+ export async function appendDLPAudit(cwd, entry) {
23
28
  try {
24
29
  const eventsPath = path.join(cwd, '.rigour', 'events.jsonl');
25
30
  await fs.ensureDir(path.dirname(eventsPath));
@@ -52,7 +57,7 @@ function extractStrings(obj, out) {
52
57
  extractStrings(v, out);
53
58
  }
54
59
  }
55
- async function getIndexHealthBlock(cwd) {
60
+ export async function getIndexHealthBlock(cwd) {
56
61
  const indexPath = getDefaultIndexPath(cwd);
57
62
  const index = await loadPatternIndex(indexPath);
58
63
  if (!index) {
@@ -60,7 +65,7 @@ async function getIndexHealthBlock(cwd) {
60
65
  }
61
66
  return `\n\n📊 Pattern Index: ${index.stats.totalPatterns} patterns across ${index.stats.totalFiles} files (updated ${index.lastUpdated}).`;
62
67
  }
63
- function wrapRecallResult(text, candidateText, cacheStatus, deduplicatedTokens = 0, guidance) {
68
+ export function wrapRecallResult(text, candidateText, cacheStatus, deduplicatedTokens = 0, guidance) {
64
69
  const telemetry = buildTelemetryMeta({
65
70
  candidateText,
66
71
  returnedText: text,
@@ -76,7 +81,7 @@ function wrapRecallResult(text, candidateText, cacheStatus, deduplicatedTokens =
76
81
  _guidance: guidance,
77
82
  };
78
83
  }
79
- export async function handleRemember(cwd, key, value) {
84
+ export async function handleRemember(cwd, key, value, scope = 'repo') {
80
85
  // Fallback: if key is missing but value exists, auto-generate a key
81
86
  if (!key && value) {
82
87
  key = value.slice(0, 40).toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_|_$/g, '') || 'convention';
@@ -108,142 +113,46 @@ export async function handleRemember(cwd, key, value) {
108
113
  }],
109
114
  };
110
115
  }
111
- // Clean — proceed with storage
112
- const store = await loadMemory(cwd);
116
+ // Clean — proceed with storage. A team memory is kept for this repository too.
117
+ const local = scope === 'user' ? 'user' : 'repo';
118
+ const store = await loadMemory(cwd, local);
113
119
  store.memories[key] = { value, timestamp: new Date().toISOString() };
114
- await saveMemory(cwd, store);
120
+ await saveMemory(cwd, store, local);
121
+ await appendDLPAudit(cwd, { type: 'memory_stored', key, scope, timestamp: new Date().toISOString() });
122
+ const sharing = scope === 'team' ? await shareWithTeam(cwd, key, value) : '';
115
123
  // If there were warnings (non-blocking), include them
116
124
  if (dlpResult.status === 'warning') {
117
125
  const alert = formatDLPAlert(dlpResult);
118
126
  return {
119
127
  content: [{
120
128
  type: "text",
121
- text: `MEMORY STORED: "${key}" has been saved (with warnings).\n\n${alert}\n\nStored value: ${value}`,
129
+ text: `MEMORY STORED: "${key}" has been saved (with warnings).${sharing}\n\n${alert}\n\nStored value: ${value}`,
122
130
  }],
123
131
  };
124
132
  }
125
133
  return {
126
134
  content: [{
127
135
  type: "text",
128
- text: `MEMORY STORED: "${key}" has been saved. This instruction will persist across sessions.\n\nStored value: ${value}`,
136
+ text: `MEMORY STORED: "${key}" has been saved ${scope === 'user' ? 'for all your repositories' : 'for this repository'}.${sharing}\n\nStored value: ${value}`,
129
137
  }],
130
138
  };
131
139
  }
132
- export async function handleRecall(cwd, key) {
133
- const commitSha = await getWorkspaceCommitSha(cwd);
134
- const cacheQuery = key ? `recall:${key}` : 'recall:all';
135
- const store = await loadMemory(cwd);
136
- const candidateText = JSON.stringify(store);
137
- const cached = await getSemanticQueryCache(cacheQuery, commitSha, cwd);
138
- if (cached?.evidence?.length) {
139
- const cachedBody = cached.evidence.join('\n');
140
- const indexHealth = await getIndexHealthBlock(cwd);
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
- });
147
- }
148
- if (key) {
149
- const memory = store.memories[key];
150
- if (!memory) {
151
- const text = `NO MEMORY FOUND for key "${key}". Use rigour_remember to store instructions.${await getIndexHealthBlock(cwd)}`;
152
- return wrapRecallResult(text, candidateText, 'miss');
153
- }
154
- // ── DLP Gate on recall: catch credentials stored before DLP existed ──
155
- const dlpResult = scanInputForCredentials(memory.value);
156
- if (dlpResult.status === 'blocked') {
157
- const alert = formatDLPAlert(dlpResult);
158
- await appendDLPAudit(cwd, {
159
- ...createDLPAuditEntry(dlpResult, { agent: 'rigour_recall' }),
160
- memory_key: key,
161
- action: 'recall_blocked',
162
- });
163
- return {
164
- content: [{
165
- type: "text",
166
- text: `🛑 RECALL BLOCKED — memory "${key}" contains ${dlpResult.detections.length} credential(s).\n\n${alert}\n\nThis memory was stored before DLP was active. Remove it with rigour_forget("${key}") and use environment variables instead.`,
167
- }],
168
- };
169
- }
170
- const body = `RECALLED MEMORY [${key}]:\n${memory.value}\n\n(Stored: ${memory.timestamp})`;
171
- const indexHealth = await getIndexHealthBlock(cwd);
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
- };
179
- await setSemanticQueryCache(cacheQuery, commitSha, {
180
- query: cacheQuery,
181
- resolvedOwner: 'memory',
182
- editScope: [],
183
- validationScope: [],
184
- evidence: [body],
185
- commitSha,
186
- confidence: 1,
187
- guidance,
188
- }, cwd);
189
- return wrapRecallResult(fullText, candidateText, 'miss', 0, guidance);
190
- }
191
- const keys = Object.keys(store.memories);
192
- if (keys.length === 0) {
193
- const text = `NO MEMORIES STORED. Use rigour_remember to persist important instructions.${await getIndexHealthBlock(cwd)}`;
194
- return wrapRecallResult(text, candidateText, 'miss');
195
- }
196
- // ── DLP scan all memories on bulk recall ──
197
- const cleanMemories = [];
198
- const taintedKeys = [];
199
- for (const k of keys) {
200
- const mem = store.memories[k];
201
- const dlpResult = scanInputForCredentials(mem.value);
202
- if (dlpResult.status === 'blocked') {
203
- taintedKeys.push(k);
204
- }
205
- else {
206
- cleanMemories.push(`## ${k}\n${mem.value}\n(Stored: ${mem.timestamp})`);
207
- }
208
- }
209
- let text = '';
210
- if (taintedKeys.length > 0) {
211
- text += `🛑 ${taintedKeys.length} memory(ies) BLOCKED — contain credentials: ${taintedKeys.join(', ')}\nUse rigour_forget to remove them.\n\n---\n\n`;
212
- }
213
- if (cleanMemories.length > 0) {
214
- text += `RECALLED ${cleanMemories.length} CLEAN MEMORIES:\n\n${cleanMemories.join('\n\n---\n\n')}\n\n---\nIMPORTANT: Follow these stored instructions throughout this session.`;
215
- }
216
- else if (taintedKeys.length > 0) {
217
- text += `No clean memories to recall. All stored memories contain credentials.`;
218
- }
219
- else {
220
- text = "NO MEMORIES STORED. Use rigour_remember to persist important instructions.";
221
- }
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
- };
229
- await setSemanticQueryCache(cacheQuery, commitSha, {
230
- query: cacheQuery,
231
- resolvedOwner: 'memory',
232
- editScope: [],
233
- validationScope: [],
234
- evidence: [text],
235
- commitSha,
236
- confidence: 1,
237
- guidance,
238
- }, cwd);
239
- return wrapRecallResult(text, candidateText, 'miss', Math.max(0, estimateTokenCount(candidateText) - estimateTokenCount(text)), guidance);
140
+ /** Share as a team lesson candidate and say what happens next. */
141
+ async function shareWithTeam(cwd, key, value) {
142
+ const lessonId = await shareMemoryLesson(cwd, key, value);
143
+ if (!lessonId)
144
+ return '\nNot shared: the local knowledge store is unavailable.';
145
+ if (!(await loadTeamConfiguration())) {
146
+ return '\nKept as a team candidate on this machine; it is shared once team mode is configured (rigour team configure).';
147
+ }
148
+ return '\nShared with your team as a candidate. Teammates\' agents receive it once someone promotes it in Studio (Knowledge › Lessons).';
240
149
  }
241
- export async function handleForget(cwd, key) {
242
- const store = await loadMemory(cwd);
150
+ export async function handleForget(cwd, key, scope = 'repo') {
151
+ const store = await loadMemory(cwd, scope);
243
152
  if (!store.memories[key]) {
244
- return { content: [{ type: "text", text: `NO MEMORY FOUND for key "${key}". Nothing to forget.` }] };
153
+ return { content: [{ type: "text", text: `NO MEMORY FOUND for key "${key}" in ${scope} memory. Nothing to forget.` }] };
245
154
  }
246
155
  delete store.memories[key];
247
- await saveMemory(cwd, store);
248
- return { content: [{ type: "text", text: `MEMORY DELETED: "${key}" has been removed.` }] };
156
+ await saveMemory(cwd, store, scope);
157
+ return { content: [{ type: "text", text: `MEMORY DELETED: "${key}" has been removed from ${scope} memory.` }] };
249
158
  }
@@ -0,0 +1,6 @@
1
+ import type { ToolResult } from '../utils/context-telemetry.js';
2
+ export interface RecallArgs {
3
+ key?: string;
4
+ query?: string;
5
+ }
6
+ export declare function handleRecall(cwd: string, args?: RecallArgs): Promise<ToolResult>;
@@ -0,0 +1,105 @@
1
+ /**
2
+ * rigour_recall: memories back to the agent.
3
+ *
4
+ * - `key`: that memory, from this repository, else from the user's own memory.
5
+ * - `query`: the few memories that match by meaning (local, see core
6
+ * memory/recall.ts); lessons this machine has validated or a person promoted,
7
+ * such as a kind of defect agents keep fixing; and team knowledge from
8
+ * pgvector when team mode is on, never a teammate's unreviewed candidate.
9
+ * - neither: every repository and user memory, as an agent loads at the start.
10
+ *
11
+ * Every returned memory passes the credential scan first; nothing is cached.
12
+ */
13
+ import { formatDLPAlert, createDLPAuditEntry, listKnowledgeLessons, rankByMeaning, rankMemories, scanInputForCredentials, searchTeamKnowledge } from '@rigour-labs/core';
14
+ import { loadMemory } from '../utils/config.js';
15
+ import { appendDLPAudit, getIndexHealthBlock, wrapRecallResult } from './memory-handlers.js';
16
+ export async function handleRecall(cwd, args = {}) {
17
+ const entries = await localEntries(cwd);
18
+ const candidateText = JSON.stringify(entries);
19
+ if (args.key)
20
+ return recallKey(cwd, args.key, entries, candidateText);
21
+ if (args.query?.trim())
22
+ return recallQuery(cwd, args.query.trim(), entries, candidateText);
23
+ return recallAll(cwd, entries, candidateText);
24
+ }
25
+ /** Repository memories first, then the user's own; a repository memory wins a shared key. */
26
+ async function localEntries(cwd) {
27
+ const repo = await loadMemory(cwd, 'repo');
28
+ const user = await loadMemory(cwd, 'user');
29
+ const toEntries = (scope, store) => Object.entries(store.memories).map(([key, m]) => ({ scope, key, value: m.value, timestamp: m.timestamp }));
30
+ const repoEntries = toEntries('repo', repo);
31
+ const repoKeys = new Set(repoEntries.map(e => e.key));
32
+ return [...repoEntries, ...toEntries('user', user).filter(e => !repoKeys.has(e.key))];
33
+ }
34
+ async function recallKey(cwd, key, entries, candidateText) {
35
+ const memory = entries.find(e => e.key === key);
36
+ if (!memory) {
37
+ const text = `NO MEMORY FOUND for key "${key}". Pass \`query\` to search by meaning, or use rigour_remember to store it.${await getIndexHealthBlock(cwd)}`;
38
+ return wrapRecallResult(text, candidateText, 'miss');
39
+ }
40
+ const blocked = await blockedReason(cwd, memory);
41
+ if (blocked)
42
+ return { content: [{ type: 'text', text: blocked }] };
43
+ const text = `RECALLED MEMORY [${key}] (${memory.scope}):\n${memory.value}\n\n(Stored: ${memory.timestamp})${await getIndexHealthBlock(cwd)}`;
44
+ return wrapRecallResult(text, candidateText, 'miss', 0, guidance(key, `Apply recalled memory "${key}" where relevant.`, [key]));
45
+ }
46
+ async function recallQuery(cwd, query, entries, candidateText) {
47
+ const clean = await cleanEntries(cwd, entries);
48
+ const [ranked, lessons, team] = await Promise.all([
49
+ rankMemories(query, clean),
50
+ rankByMeaning(query, await trustedLessons(cwd), lesson => lesson.subject),
51
+ searchTeamKnowledge(query, 3),
52
+ ]);
53
+ const teamOnly = team.status === 'ready' ? team.candidates.filter(c => !lessons.some(l => l.id === c.lessonId)) : [];
54
+ const sections = [];
55
+ if (ranked.length) {
56
+ sections.push(`MEMORIES MATCHING "${query}":\n\n` + ranked.map(m => `## ${m.key} (${m.scope}, ${m.match} ${m.score.toFixed(2)})\n${m.value}`).join('\n\n'));
57
+ }
58
+ if (lessons.length) {
59
+ sections.push('LESSONS (validated by repeated fixes, or promoted):\n\n' + lessons.map(l => `- ${l.subject} (${l.state}, ${l.match} ${l.score.toFixed(2)})`).join('\n'));
60
+ }
61
+ if (teamOnly.length) {
62
+ sections.push('TEAM KNOWLEDGE (promoted lessons):\n\n' + teamOnly.map(c => `- ${c.subject} (similarity ${c.similarity.toFixed(2)}, ${c.state})`).join('\n'));
63
+ }
64
+ const text = sections.length
65
+ ? `${sections.join('\n\n---\n\n')}\n\nApply these where they fit the task.`
66
+ : `NO MEMORY MATCHES "${query}". Recall with no arguments lists every memory.`;
67
+ const keys = ranked.map(m => m.key);
68
+ return wrapRecallResult(text, candidateText, 'miss', 0, guidance(query, `Apply ${keys.length + lessons.length + teamOnly.length} matching item(s).`, keys));
69
+ }
70
+ async function recallAll(cwd, entries, candidateText) {
71
+ if (entries.length === 0) {
72
+ return wrapRecallResult(`NO MEMORIES STORED. Use rigour_remember to persist important instructions.${await getIndexHealthBlock(cwd)}`, candidateText, 'miss');
73
+ }
74
+ const clean = await cleanEntries(cwd, entries);
75
+ const blocked = entries.filter(e => !clean.includes(e)).map(e => e.key);
76
+ let text = blocked.length ? `🛑 ${blocked.length} memory(ies) BLOCKED — contain credentials: ${blocked.join(', ')}\nUse rigour_forget to remove them.\n\n---\n\n` : '';
77
+ text += clean.length
78
+ ? `RECALLED ${clean.length} CLEAN MEMORIES:\n\n${clean.map(m => `## ${m.key} (${m.scope})\n${m.value}\n(Stored: ${m.timestamp})`).join('\n\n---\n\n')}\n\n---\nIMPORTANT: Follow these stored instructions throughout this session.`
79
+ : 'No clean memories to recall. All stored memories contain credentials.';
80
+ text += await getIndexHealthBlock(cwd);
81
+ return wrapRecallResult(text, candidateText, 'miss', 0, guidance('all memories', `Apply ${clean.length} recalled memory item(s) where relevant.`, clean.map(m => m.key)));
82
+ }
83
+ /** Lessons an agent may act on: validated by evidence, or promoted by a person. */
84
+ async function trustedLessons(cwd) {
85
+ return (await listKnowledgeLessons(cwd)).filter(lesson => lesson.state === 'validated' || lesson.state === 'promoted');
86
+ }
87
+ /** Memories with no credential in them; a blocked one is audited, and never returned. */
88
+ async function cleanEntries(cwd, entries) {
89
+ const clean = [];
90
+ for (const entry of entries) {
91
+ if (!(await blockedReason(cwd, entry)))
92
+ clean.push(entry);
93
+ }
94
+ return clean;
95
+ }
96
+ async function blockedReason(cwd, memory) {
97
+ const dlp = scanInputForCredentials(memory.value);
98
+ if (dlp.status !== 'blocked')
99
+ return undefined;
100
+ await appendDLPAudit(cwd, { ...createDLPAuditEntry(dlp, { agent: 'rigour_recall' }), memory_key: memory.key, action: 'recall_blocked' });
101
+ return `🛑 RECALL BLOCKED — memory "${memory.key}" contains ${dlp.detections.length} credential(s).\n\n${formatDLPAlert(dlp)}\n\nRemove it with rigour_forget("${memory.key}") and use environment variables instead.`;
102
+ }
103
+ function guidance(query, recommendation, keys) {
104
+ return { kind: 'memory', query, recommendation, memoryRefs: keys.map(id => ({ id, label: id })) };
105
+ }
@@ -1,3 +1,3 @@
1
1
  import { type ToolResult } from '../utils/context-telemetry.js';
2
- export declare function handleCheckPattern(cwd: string, patternName: string, type?: string, intent?: string, file?: string): Promise<ToolResult>;
2
+ export declare function handleCheckPattern(cwd: string, patternName: string, type?: string, intent?: string, file?: string, signature?: string, keywords?: string[]): Promise<ToolResult>;
3
3
  export declare function handleSecurityAudit(cwd: string): Promise<ToolResult>;
@@ -8,10 +8,10 @@
8
8
  import path from "path";
9
9
  import yaml from "yaml";
10
10
  import fs from "fs-extra";
11
- import { PatternMatcher, loadPatternIndex, getDefaultIndexPath, StalenessDetector, SecurityDetector, } from "@rigour-labs/core/pattern-index";
12
- import { ConfigSchema, getSemanticQueryCache, setSemanticQueryCache, estimateTokenCount } from "@rigour-labs/core";
11
+ import { assessPattern, loadPatternIndex, getDefaultIndexPath, SecurityDetector, } from "@rigour-labs/core/pattern-index";
12
+ import { ConfigSchema, estimateTokenCount } from "@rigour-labs/core";
13
13
  import { notifyProgress } from '../utils/notifications.js';
14
- import { buildTelemetryMeta, getWorkspaceCommitSha } from '../utils/context-telemetry.js';
14
+ import { buildTelemetryMeta } from '../utils/context-telemetry.js';
15
15
  import { appendContextFooter } from '../utils/context-footer.js';
16
16
  /**
17
17
  * Check if a file path is protected by safety.protected_paths in rigour.yml.
@@ -43,38 +43,14 @@ async function checkFileGuard(cwd, filePath) {
43
43
  return normalized.startsWith(clean + '/');
44
44
  }) ?? null;
45
45
  }
46
- function buildCacheQuery(patternName, type, intent, file) {
47
- return `check_pattern:${patternName}:${type ?? ''}:${intent ?? ''}:${file ?? ''}`;
48
- }
49
- export async function handleCheckPattern(cwd, patternName, type, intent, file) {
50
- const commitSha = await getWorkspaceCommitSha(cwd);
51
- const cacheQuery = buildCacheQuery(patternName, type, intent, file);
46
+ export async function handleCheckPattern(cwd, patternName, type, intent, file, signature, keywords) {
47
+ // No answer cache: a cache keyed on the commit kept naming functions renamed since, and the
48
+ // index lookup it saved is milliseconds.
52
49
  const indexPath = getDefaultIndexPath(cwd);
53
50
  const index = await loadPatternIndex(indexPath);
54
51
  const indexScanEstimate = index
55
52
  ? `Pattern index scan (${index.stats.totalPatterns} patterns) for ${patternName}`
56
53
  : `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
- _guidance: cached.guidance ?? {
72
- kind: 'pattern',
73
- query: patternName,
74
- recommendation: `Apply the cached pattern guidance for "${patternName}".`,
75
- },
76
- };
77
- }
78
54
  let resultText = "";
79
55
  let matchedPattern;
80
56
  // 0. File Guard — BLOCK writes to protected paths
@@ -103,30 +79,28 @@ export async function handleCheckPattern(cwd, patternName, type, intent, file) {
103
79
  };
104
80
  }
105
81
  }
106
- // 1. Check for Reinvention
82
+ // 1. Reuse: does it exist already, and is what would be reused out of date?
83
+ let action = 'ALLOW';
107
84
  if (index) {
108
- const matcher = new PatternMatcher(index);
109
- const matchResult = await matcher.match({ name: patternName, type, intent });
110
- if (matchResult.status === "FOUND_SIMILAR") {
111
- matchedPattern = matchResult.matches[0].pattern;
112
- resultText += `🚨 PATTERN REINVENTION DETECTED\n`;
113
- resultText += `Similar pattern already exists: "${matchResult.matches[0].pattern.name}" in ${matchResult.matches[0].pattern.file}\n`;
114
- resultText += `SUGGESTION: ${matchResult.suggestion}\n\n`;
85
+ const assessment = await assessPattern(cwd, index, { name: patternName, type, intent, signature, keywords });
86
+ action = assessment.action;
87
+ if (assessment.match) {
88
+ const best = assessment.match;
89
+ matchedPattern = best.pattern;
90
+ resultText += action === 'BLOCK' ? `🚨 PATTERN REINVENTION DETECTED\n` : `💡 SIMILAR PATTERN EXISTS\n`;
91
+ resultText += `"${best.pattern.name}" in ${best.pattern.file}:${best.pattern.line} (${best.matchType}, ${best.confidence}%)\n`;
92
+ resultText += `SUGGESTION: ${assessment.suggestion}\n\n`;
93
+ }
94
+ if (assessment.deprecations.length) {
95
+ resultText += `⚠️ THE EXISTING "${matchedPattern?.name}" USES SOMETHING DEPRECATED\n`;
96
+ for (const issue of assessment.deprecations)
97
+ resultText += `- ${issue.reason}\n REPLACEMENT: ${issue.replacement}\n`;
98
+ resultText += `\n`;
115
99
  }
116
100
  }
117
101
  else {
118
102
  resultText += `⚠️ Pattern index not found. Run rigour_index to enable reinvention detection.\n\n`;
119
103
  }
120
- // 2. Check for Staleness/Best Practices
121
- const detector = new StalenessDetector(cwd);
122
- const staleness = await detector.checkStaleness(`${type || 'function'} ${patternName} {}`);
123
- if (staleness.status !== "FRESH") {
124
- resultText += `⚠️ STALENESS/ANTI-PATTERN WARNING\n`;
125
- for (const issue of staleness.issues) {
126
- resultText += `- ${issue.reason}\n REPLACEMENT: ${issue.replacement}\n`;
127
- }
128
- resultText += `\n`;
129
- }
130
104
  // 3. Check Security for this library (if it's an import)
131
105
  if (intent && intent.includes('import')) {
132
106
  const security = new SecurityDetector(cwd);
@@ -147,9 +121,12 @@ export async function handleCheckPattern(cwd, patternName, type, intent, file) {
147
121
  }
148
122
  else {
149
123
  recommendation = "Proceed with caution, addressing the warnings above.";
150
- if (resultText.includes("🚨 PATTERN REINVENTION")) {
124
+ if (action === 'BLOCK') {
151
125
  recommendation = "STOP and REUSE the existing pattern mentioned above. Do not create a duplicate.";
152
126
  }
127
+ else if (action === 'WARN') {
128
+ recommendation = "Look at the similar pattern above and reuse it if it does what you need; otherwise proceed.";
129
+ }
153
130
  else if (resultText.includes("🛡️ SECURITY/CVE WARNING")) {
154
131
  recommendation = "STOP and update your dependencies or find an alternative library. Do not proceed with vulnerable code.";
155
132
  }
@@ -165,16 +142,6 @@ export async function handleCheckPattern(cwd, patternName, type, intent, file) {
165
142
  patternRefs: matchedPattern ? [{ id: matchedPattern.id, label: matchedPattern.name, file: matchedPattern.file }] : [],
166
143
  selectedFiles: file ? [file] : matchedPattern?.file ? [matchedPattern.file] : [],
167
144
  };
168
- await setSemanticQueryCache(cacheQuery, commitSha, {
169
- query: cacheQuery,
170
- resolvedOwner: file ? path.dirname(file) : 'patterns',
171
- editScope: file ? [file] : [],
172
- validationScope: [],
173
- evidence: [resultText],
174
- commitSha,
175
- confidence: resultText.includes('✅') ? 0.9 : 0.7,
176
- guidance,
177
- }, cwd);
178
145
  const telemetry = buildTelemetryMeta({
179
146
  candidateText: indexScanEstimate,
180
147
  returnedText: resultText,
@@ -5,7 +5,7 @@
5
5
  * With no diff, the change is taken from git: uncommitted work (what the
6
6
  * agent just wrote, new files included), or the branch against `base`.
7
7
  */
8
- import { acknowledgeReview, buildReviewTask, diffFromGit, recordReviewOutcome, reviewChange, toReviewFinding } from "@rigour-labs/core";
8
+ import { acknowledgeReview, buildReviewTask, diffFromGit, recordFixLessons, recordReviewOutcome, reviewChange, toReviewFinding } from "@rigour-labs/core";
9
9
  import { notifyProgress } from '../utils/notifications.js';
10
10
  export async function handleReview(config, cwd, args) {
11
11
  notifyProgress("info", args.diff ? "Reviewing the provided diff..." : `Reviewing ${args.base ? `this branch against ${args.base}` : "uncommitted changes"}...`);
@@ -13,7 +13,8 @@ export async function handleReview(config, cwd, args) {
13
13
  const diff = args.diff ?? diffFromGit(cwd, args.base ? { mode: 'base', base: args.base } : { mode: 'working' });
14
14
  const result = await reviewChange({ cwd, config, diff, files: args.files });
15
15
  const task = args.mode === 'agent' ? buildReviewTask(cwd, diff, config.gates.deep?.router, config.gates.deep?.review_lessons, config.gates.deep?.repo_rules) : undefined;
16
- recordReviewOutcome(cwd, result.findings, Object.keys(result.changedLines));
16
+ const capture = recordReviewOutcome(cwd, result.findings, Object.keys(result.changedLines));
17
+ await recordFixLessons(cwd, capture.fixes).catch(() => undefined); // learning never fails a review
17
18
  const stats = result.report?.stats;
18
19
  return text({
19
20
  status: result.status,
@@ -25,8 +26,10 @@ export async function handleReview(config, cwd, args) {
25
26
  file_findings: result.fileFindings.map(toReviewFinding),
26
27
  context_findings: result.contextFindings.map(toReviewFinding),
27
28
  advisory_count: result.advisory.length,
29
+ muted_count: result.muted,
28
30
  excluded_outside_changed_lines: result.excludedOutsideChangedLines,
29
31
  unlocated_failures: result.unlocated,
32
+ gate_errors: result.gateErrors,
30
33
  ...(task ? { review_task: { items: task.items, already_reviewed: task.alreadyReviewed, team_lessons: task.lessons, repo_rules: task.rules, instructions: task.instructions } } : {}),
31
34
  next_step: nextStep(result.findings.length, task?.items.length ?? 0),
32
35
  });
@@ -226,6 +226,18 @@ export declare function loadConfig(cwd: string): Promise<{
226
226
  custom_patterns: string[];
227
227
  audit_log: boolean;
228
228
  };
229
+ unindexed_reads: {
230
+ enabled: boolean;
231
+ migrations: string[];
232
+ };
233
+ deprecated_dependencies: {
234
+ enabled: boolean;
235
+ registry?: string | undefined;
236
+ };
237
+ migration_order: {
238
+ enabled: boolean;
239
+ dirs: string[];
240
+ };
229
241
  semantic_bugs: {
230
242
  enabled: boolean;
231
243
  rules?: string[] | undefined;
@@ -281,9 +293,13 @@ export interface MemoryStore {
281
293
  timestamp: string;
282
294
  }>;
283
295
  }
284
- export declare function getMemoryPath(cwd: string): Promise<string>;
285
- export declare function loadMemory(cwd: string): Promise<MemoryStore>;
286
- export declare function saveMemory(cwd: string, store: MemoryStore): Promise<void>;
296
+ /** The home whose .rigour/ holds user-scope memory: RIGOUR_HOME when set (tests, sandboxes), else the OS home. */
297
+ export declare function userHome(): string;
298
+ /** Where memories live: `repo` in this checkout's .rigour/, `user` in ~/.rigour/ for every repository. */
299
+ export type LocalMemoryScope = 'repo' | 'user';
300
+ export declare function getMemoryPath(cwd: string, scope?: LocalMemoryScope): Promise<string>;
301
+ export declare function loadMemory(cwd: string, scope?: LocalMemoryScope): Promise<MemoryStore>;
302
+ export declare function saveMemory(cwd: string, store: MemoryStore, scope?: LocalMemoryScope): Promise<void>;
287
303
  export interface McpSettings {
288
304
  deep_default_mode: 'off' | 'quick' | 'full';
289
305
  }