@rigour-labs/mcp 5.5.4 → 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 ?? {});
@@ -136,7 +165,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
136
165
  break;
137
166
  case "rigour_run_supervised": {
138
167
  const { command, maxRetries = 3, dryRun = false } = args;
139
- result = await handleRunSupervised(runner, cwd, command, maxRetries, dryRun, requestId, config);
168
+ result = await handleRunSupervised({
169
+ runner,
170
+ cwd,
171
+ command,
172
+ maxRetries,
173
+ dryRun,
174
+ requestId,
175
+ config,
176
+ });
140
177
  break;
141
178
  }
142
179
  // Multi-agent governance
@@ -225,12 +262,41 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
225
262
  candidateTokens: telemetry?.candidateTokens ?? returnedTokens,
226
263
  returnedTokens,
227
264
  deduplicatedTokens: telemetry?.deduplicatedTokens ?? 0,
265
+ candidateFiles: telemetry?.candidateFiles,
266
+ returnedFiles: telemetry?.returnedFiles,
228
267
  }, cwd);
229
268
  }
230
269
  }
231
270
  catch {
232
271
  // non-blocking context telemetry logging
233
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
+ }
234
300
  // ── Prepend image DLP warning if image content was detected ──
235
301
  if (hasImageContent && Array.isArray(result.content)) {
236
302
  result.content.unshift({
@@ -238,9 +304,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
238
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.",
239
305
  });
240
306
  }
307
+ const studioImpact = buildStudioImpact(result._guidance, result._telemetry);
241
308
  await logStudioEvent(cwd, {
242
- type: "tool_response", requestId, tool: name, status: "success",
243
- 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,
244
313
  });
245
314
  // ─── Dashboard State + MCP App UI Hint ──────────────
246
315
  const report = result._rigour_report;
@@ -253,7 +322,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
253
322
  }
254
323
  // Attach UI hint for MCP-App-capable clients (Claude Desktop, VS Code, ChatGPT, Goose)
255
324
  if (!result.isError) {
256
- result._meta = { ui: { resourceUri: DASHBOARD_URI } };
325
+ result._meta = buildMcpResultMeta(result._meta, requestId, DASHBOARD_URI, result._guidance, result._telemetry);
257
326
  }
258
327
  // Notify clients that the dashboard resource has updated
259
328
  try {
@@ -272,6 +341,19 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
272
341
  content: [{ type: "text", text: `RIGOUR ERROR: ${error.message}` }],
273
342
  isError: true,
274
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
+ }
275
357
  await logStudioEvent(cwd, {
276
358
  type: "tool_response", requestId, tool: name,
277
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}`;
@@ -82,6 +75,46 @@ async function validateTaskScopeAgainstIndex(cwd, taskScope) {
82
75
  }
83
76
  // ─── Agent Register ───────────────────────────────────────────────
84
77
  export async function handleAgentRegister(cwd, agentId, taskScope, requestId) {
78
+ const { validateAgentClaimedScope, loadOperatorScopes, isScopeSubset, } = await import('@rigour-labs/core');
79
+ const authority = process.env.RIGOUR_ALLOW_AGENT_SCOPE_AUTHORITY === '1';
80
+ if (!authority) {
81
+ const claimCheck = validateAgentClaimedScope(taskScope);
82
+ if (!claimCheck.ok) {
83
+ await logStudioEvent(cwd, {
84
+ type: 'firewall_deny',
85
+ requestId,
86
+ tool: 'rigour_agent_register',
87
+ decision: 'deny',
88
+ reason: claimCheck.reason,
89
+ ruleId: 'scope.register-denied',
90
+ });
91
+ return {
92
+ content: [{ type: 'text', text: `❌ SCOPE REGISTER DENIED: ${claimCheck.reason}` }],
93
+ isError: true,
94
+ };
95
+ }
96
+ }
97
+ const operatorScopes = await loadOperatorScopes(cwd);
98
+ if (operatorScopes) {
99
+ const allowed = operatorScopes[agentId];
100
+ if (!allowed || !isScopeSubset(taskScope, allowed)) {
101
+ const msg = allowed
102
+ ? `Claimed scope is not a subset of operator scopes for "${agentId}": ${allowed.join(', ')}`
103
+ : `No operator scopes defined for agent "${agentId}" in .rigour/operator-scopes.json`;
104
+ await logStudioEvent(cwd, {
105
+ type: 'firewall_deny',
106
+ requestId,
107
+ tool: 'rigour_agent_register',
108
+ decision: 'deny',
109
+ reason: msg,
110
+ ruleId: 'scope.operator-required',
111
+ });
112
+ return {
113
+ content: [{ type: 'text', text: `❌ SCOPE REGISTER DENIED: ${msg}` }],
114
+ isError: true,
115
+ };
116
+ }
117
+ }
85
118
  const sessionPath = path.join(cwd, '.rigour', 'agent-session.json');
86
119
  let session = { agents: [], startedAt: new Date().toISOString() };
87
120
  if (await fs.pathExists(sessionPath)) {
@@ -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
  }
@@ -8,5 +8,17 @@ type ToolResult = {
8
8
  isError?: boolean;
9
9
  };
10
10
  export declare function handleRun(cwd: string, command: string, requestId: string): Promise<ToolResult>;
11
- export declare function handleRunSupervised(runner: GateRunner, cwd: string, command: string, maxRetries: number, dryRun: boolean, requestId: string, config?: Config): Promise<ToolResult>;
11
+ export declare function handleRunSupervised(opts: {
12
+ runner: GateRunner;
13
+ cwd: string;
14
+ command: string;
15
+ maxRetries: number;
16
+ dryRun: boolean;
17
+ requestId: string;
18
+ config?: Config;
19
+ }): Promise<ToolResult>;
20
+ /**
21
+ * Fail-closed: timeout with no human decision → timeout-deny (not approve).
22
+ */
23
+ export declare function pollArbitration(cwd: string, rid: string, timeout: number): Promise<string | null>;
12
24
  export {};
@@ -2,35 +2,76 @@
2
2
  * Execution & Supervision Tool Handlers
3
3
  *
4
4
  * Handlers for: rigour_run, rigour_run_supervised
5
- *
6
- * @since v2.17.0 — extracted from monolithic index.ts
5
+ * Agent Transaction Firewall: fail-closed arbitration + typed commands.
7
6
  */
8
7
  import fs from "fs-extra";
9
8
  import path from "path";
10
- import { FixPacketService } from "@rigour-labs/core";
9
+ import { FixPacketService, runTypedCommand, evaluateTypedCommand, issueArbitrationToken, } from "@rigour-labs/core";
11
10
  import { logStudioEvent } from '../utils/config.js';
12
11
  import { notifyProgress } from '../utils/notifications.js';
13
- export async function handleRun(cwd, command, requestId) {
14
- // 1. Log Interceptable Event
12
+ async function requireHumanApproval(cwd, command, requestId, tool) {
13
+ const arbitrationToken = await issueArbitrationToken(cwd, requestId, 60_000);
15
14
  await logStudioEvent(cwd, {
16
15
  type: "interception_requested",
17
16
  requestId,
18
- tool: "rigour_run",
17
+ tool,
19
18
  command,
19
+ arbitrationToken,
20
20
  });
21
- // 2. Poll for Human Arbitration (Max 60s wait)
22
21
  console.error(`[RIGOUR] Waiting for human arbitration for command: ${command}`);
23
22
  const decision = await pollArbitration(cwd, requestId, 60000);
24
- if (decision === 'reject') {
23
+ if (decision === 'approve')
24
+ return null;
25
+ await logStudioEvent(cwd, {
26
+ type: "firewall_deny",
27
+ requestId,
28
+ tool: "human_arbitration",
29
+ decision: decision ?? 'timeout-deny',
30
+ reason: decision === 'reject' ? 'Human rejected' : 'Arbitration timed out (fail-closed)',
31
+ });
32
+ return {
33
+ content: [{
34
+ type: "text",
35
+ text: decision === 'reject'
36
+ ? `❌ COMMAND REJECTED BY GOVERNOR: The execution of "${command}" was blocked by a human operator in the Governance Studio.`
37
+ : `❌ COMMAND DENIED (fail-closed): No human arbitration within 60s for "${command}".`,
38
+ }],
39
+ isError: true,
40
+ };
41
+ }
42
+ export async function handleRun(cwd, command, requestId) {
43
+ const typed = evaluateTypedCommand(command);
44
+ await logStudioEvent(cwd, {
45
+ type: "firewall_precheck",
46
+ requestId,
47
+ tool: "rigour_run",
48
+ command,
49
+ firewallDecision: typed.decision,
50
+ firewallReason: typed.reason,
51
+ ruleId: typed.ruleId,
52
+ });
53
+ if (typed.decision !== 'allow') {
54
+ await logStudioEvent(cwd, {
55
+ type: "firewall_deny",
56
+ requestId,
57
+ tool: "rigour_run",
58
+ decision: typed.decision,
59
+ reason: typed.reason,
60
+ ruleId: typed.ruleId,
61
+ });
25
62
  return {
26
- content: [{ type: "text", text: `❌ COMMAND REJECTED BY GOVERNOR: The execution of "${command}" was blocked by a human operator in the Governance Studio.` }],
63
+ content: [{
64
+ type: "text",
65
+ text: `❌ FIREWALL DENY (${typed.ruleId}): ${typed.reason}\nCommand: ${command}`,
66
+ }],
27
67
  isError: true,
28
68
  };
29
69
  }
30
- // Execute
31
- const { execa } = await import("execa");
70
+ const denied = await requireHumanApproval(cwd, command, requestId, "rigour_run");
71
+ if (denied)
72
+ return denied;
32
73
  try {
33
- const { stdout, stderr } = await execa(command, { shell: true, cwd });
74
+ const { stdout, stderr } = await runTypedCommand(command, cwd);
34
75
  return {
35
76
  content: [{ type: "text", text: `✅ COMMAND EXECUTED (Approved by Governor):\n\nSTDOUT:\n${stdout}\n\nSTDERR:\n${stderr}` }],
36
77
  };
@@ -42,8 +83,29 @@ export async function handleRun(cwd, command, requestId) {
42
83
  };
43
84
  }
44
85
  }
45
- export async function handleRunSupervised(runner, cwd, command, maxRetries, dryRun, requestId, config) {
46
- const { execa } = await import("execa");
86
+ export async function handleRunSupervised(opts) {
87
+ const { runner, cwd, command, maxRetries, dryRun, requestId, config } = opts;
88
+ const typed = evaluateTypedCommand(command);
89
+ if (typed.decision !== 'allow') {
90
+ await logStudioEvent(cwd, {
91
+ type: "firewall_deny",
92
+ requestId,
93
+ tool: "rigour_run_supervised",
94
+ decision: typed.decision,
95
+ reason: typed.reason,
96
+ ruleId: typed.ruleId,
97
+ });
98
+ return {
99
+ content: [{
100
+ type: "text",
101
+ text: `❌ FIREWALL DENY (${typed.ruleId}): ${typed.reason}\nCommand: ${command}`,
102
+ }],
103
+ isError: true,
104
+ };
105
+ }
106
+ const denied = await requireHumanApproval(cwd, command, requestId, "rigour_run_supervised");
107
+ if (denied)
108
+ return denied;
47
109
  let iteration = 0;
48
110
  let lastReport = null;
49
111
  let result = null;
@@ -61,7 +123,7 @@ export async function handleRunSupervised(runner, cwd, command, maxRetries, dryR
61
123
  notifyProgress("info", `Supervisor iteration ${iteration}/${maxRetries}...`);
62
124
  if (!dryRun) {
63
125
  try {
64
- await execa(command, { shell: true, cwd });
126
+ await runTypedCommand(command, cwd);
65
127
  }
66
128
  catch (e) {
67
129
  console.error(`[RIGOUR] Iteration ${iteration} command error: ${e.message}`);
@@ -99,7 +161,6 @@ export async function handleRunSupervised(runner, cwd, command, maxRetries, dryR
99
161
  }
100
162
  if (iteration >= maxRetries) {
101
163
  notifyProgress("error", `Supervisor FAILED after ${iteration} iterations \u2014 ${lastReport.failures.length} violations remain`);
102
- // Use FixPacketService for structured output instead of ad-hoc formatting
103
164
  let fixPacketText;
104
165
  if (config) {
105
166
  const fixPacketService = new FixPacketService();
@@ -107,7 +168,6 @@ export async function handleRunSupervised(runner, cwd, command, maxRetries, dryR
107
168
  fixPacketText = formatFixPacketForSupervisor(fixPacket);
108
169
  }
109
170
  else {
110
- // Fallback if config not available
111
171
  fixPacketText = lastReport.failures.map((f, i) => {
112
172
  const sevTag = `[${(f.severity || 'medium').toUpperCase()}]`;
113
173
  return `${i + 1}. ${sevTag} [${f.id}] ${f.title}: ${f.details}${f.files?.length ? ` (${f.files.join(', ')})` : ''}`;
@@ -168,28 +228,44 @@ function formatFixPacketForSupervisor(fixPacket) {
168
228
  }
169
229
  return lines.join('\n');
170
230
  }
171
- // ─── Private Helpers ──────────────────────────────────────────────
172
- async function pollArbitration(cwd, rid, timeout) {
173
- const start = Date.now();
231
+ /**
232
+ * Fail-closed: timeout with no human decision → timeout-deny (not approve).
233
+ */
234
+ export async function pollArbitration(cwd, rid, timeout) {
174
235
  const eventsPath = path.join(cwd, '.rigour/events.jsonl');
175
- while (Date.now() - start < timeout) {
176
- if (await fs.pathExists(eventsPath)) {
177
- const content = await fs.readFile(eventsPath, 'utf-8');
178
- const lines = content.split('\n').filter(l => l.trim());
179
- for (const line of lines.reverse()) {
180
- let event;
181
- try {
182
- event = JSON.parse(line);
183
- }
184
- catch {
185
- continue;
186
- }
187
- if (event.tool === 'human_arbitration' && event.requestId === rid) {
188
- return event.decision;
189
- }
190
- }
236
+ const maxIterations = Math.max(1, Math.ceil(timeout / 1000));
237
+ for (let i = 0; i < maxIterations; i++) {
238
+ const found = await readArbitrationDecision(eventsPath, rid);
239
+ if (found)
240
+ return found;
241
+ await sleepMs(1000);
242
+ }
243
+ return "timeout-deny";
244
+ }
245
+ async function readArbitrationDecision(eventsPath, rid) {
246
+ if (!(await fs.pathExists(eventsPath)))
247
+ return null;
248
+ const content = await fs.readFile(eventsPath, 'utf-8');
249
+ const lines = content.split('\n').filter(l => l.trim());
250
+ for (const line of lines.reverse()) {
251
+ let event;
252
+ try {
253
+ event = JSON.parse(line);
254
+ }
255
+ catch {
256
+ continue;
257
+ }
258
+ if (event.tool === 'human_arbitration' && event.requestId === rid) {
259
+ return event.decision;
191
260
  }
192
- await new Promise(r => setTimeout(r, 1000));
193
261
  }
194
- return "approve"; // Default auto-approve if no human response
262
+ return null;
263
+ }
264
+ function sleepMs(ms) {
265
+ return new Promise(resolve => {
266
+ const timerId = setTimeout(() => {
267
+ clearTimeout(timerId);
268
+ resolve();
269
+ }, ms);
270
+ });
195
271
  }
@@ -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";
@@ -73,7 +73,12 @@ export async function handleHooksCheck(cwd, files, timeout, text, agent) {
73
73
  const input = { cwd, files };
74
74
  if (timeout)
75
75
  input.timeout_ms = timeout;
76
+ if (agent)
77
+ input.agentId = agent;
76
78
  const result = await runHookChecker(input);
79
+ if (files.length > 0) {
80
+ await updateAutomaticIndexForFiles(cwd, files).catch(() => undefined);
81
+ }
77
82
  if (result.status === 'pass') {
78
83
  return {
79
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": "5.5.4",
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": "5.5.4"
65
+ "@rigour-labs/core": "6.1.0-beta.1"
66
66
  },
67
67
  "devDependencies": {
68
68
  "@types/node": "^25.0.3",