@rigour-labs/cli 5.1.1 → 5.1.2

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/cli.js CHANGED
@@ -15,6 +15,7 @@ import { hooksInitCommand, hooksCheckCommand } from './commands/hooks.js';
15
15
  import { settingsShowCommand, settingsSetKeyCommand, settingsRemoveKeyCommand, settingsSetCommand, settingsGetCommand, settingsResetCommand, settingsPathCommand } from './commands/settings.js';
16
16
  import { doctorCommand } from './commands/doctor.js';
17
17
  import { brainCommand } from './commands/brain.js';
18
+ import { deepStatsCommand } from './commands/deep-stats.js';
18
19
  import { checkForUpdates } from './utils/version.js';
19
20
  import { getCliVersion } from './utils/cli-version.js';
20
21
  import chalk from 'chalk';
@@ -23,6 +24,7 @@ const program = new Command();
23
24
  program.addCommand(indexCommand);
24
25
  program.addCommand(studioCommand);
25
26
  program.addCommand(brainCommand);
27
+ program.addCommand(deepStatsCommand);
26
28
  program
27
29
  .name('rigour')
28
30
  .description('šŸ›”ļø Rigour: The Quality Gate Loop for AI-Assisted Engineering')
@@ -71,6 +73,7 @@ program
71
73
  .option('--api-base-url <url>', 'Custom API base URL (for self-hosted or proxy endpoints)')
72
74
  .option('--model-name <name>', 'Override cloud model name')
73
75
  .option('--agents <count>', 'Number of parallel agents for deep scan (cloud-only, default: 1)', '1')
76
+ .option('--no-cache', 'Force full scan even if no files changed')
74
77
  .addHelpText('after', `
75
78
  Examples:
76
79
  $ rigour check # AST only. Instant. Free.
@@ -227,11 +230,14 @@ hooksCmd
227
230
  .option('--stdin', 'Read hook payload from stdin (Cursor/Windsurf/Cline format)')
228
231
  .option('--block', 'Exit code 2 on failures (for blocking hooks)')
229
232
  .option('--timeout <ms>', 'Timeout in milliseconds (default: 5000)')
233
+ .option('--mode <mode>', 'Check mode: "check" (default) or "dlp" (credential scanning)')
234
+ .option('--agent <name>', 'Agent name for DLP audit trail (e.g., cursor, claude)')
230
235
  .addHelpText('after', `
231
236
  Examples:
232
237
  $ rigour hooks check --files src/app.ts
233
238
  $ rigour hooks check --files src/a.ts,src/b.ts --block
234
239
  $ echo '{"file_path":"src/app.ts"}' | rigour hooks check --stdin
240
+ $ echo 'AWS_SECRET=AKIA...' | rigour hooks check --mode dlp --stdin
235
241
  `)
236
242
  .action(async (options) => {
237
243
  await hooksCheckCommand(process.cwd(), options);
@@ -278,11 +284,15 @@ settingsCmd
278
284
  (async () => {
279
285
  try {
280
286
  const updateInfo = await checkForUpdates(CLI_VERSION);
281
- // Suppress update message in JSON/CI mode to keep stdout clean
282
- const isSilent = process.argv.includes('--json') || process.argv.includes('--ci');
283
- if (updateInfo?.hasUpdate && !isSilent) {
284
- console.log(chalk.yellow(`\n⚔ Update available: ${updateInfo.currentVersion} → ${updateInfo.latestVersion}`));
285
- console.log(chalk.dim(` Run: npx @rigour-labs/cli@latest init --force\n`));
287
+ // Suppress update message when stdout must be clean JSON:
288
+ // --json, --ci flags, or hooks subcommand (Cursor/Claude parse stdout as JSON)
289
+ const isSilent = process.argv.includes('--json') || process.argv.includes('--ci') || process.argv.includes('hooks');
290
+ // Skip for local dev builds where package.json version hasn't been bumped
291
+ const isDevBuild = CLI_VERSION === '1.0.0' || CLI_VERSION === '0.0.0';
292
+ if (updateInfo?.hasUpdate && !isSilent && !isDevBuild) {
293
+ // Use stderr so stdout stays clean for programmatic consumers
294
+ console.error(chalk.yellow(`\n⚔ Update available: ${updateInfo.currentVersion} → ${updateInfo.latestVersion}`));
295
+ console.error(chalk.dim(` Run: npx @rigour-labs/cli@latest init --force\n`));
286
296
  }
287
297
  }
288
298
  catch {
@@ -42,8 +42,25 @@ async function handleStatus(core) {
42
42
  const cwd = process.cwd();
43
43
  const stats = await core.getProjectStats(cwd);
44
44
  if (!stats) {
45
- console.log(chalk.yellow(' SQLite not available (sqlite3 not installed).'));
46
- console.log(chalk.dim(' Run: npm install sqlite3'));
45
+ const os = await import('os');
46
+ const path = await import('path');
47
+ const fs = await import('fs-extra');
48
+ const rigourDir = path.default.join(os.default.homedir(), '.rigour');
49
+ console.log(chalk.yellow(' SQLite not available — installing sqlite3...'));
50
+ try {
51
+ const { execSync } = await import('child_process');
52
+ // Install into ~/.rigour/ so it's always findable regardless of cwd
53
+ await fs.default.ensureDir(rigourDir);
54
+ if (!fs.default.existsSync(path.default.join(rigourDir, 'package.json'))) {
55
+ fs.default.writeJsonSync(path.default.join(rigourDir, 'package.json'), { name: 'rigour-deps', private: true });
56
+ }
57
+ execSync('npm install sqlite3 --no-save', { cwd: rigourDir, stdio: 'pipe', timeout: 120000 });
58
+ console.log(chalk.green(' āœ” sqlite3 installed to ~/.rigour/. Re-run `rigour brain` to see stats.'));
59
+ }
60
+ catch (err) {
61
+ console.log(chalk.red(` Auto-install failed: ${err?.message?.slice(0, 100) || 'unknown error'}`));
62
+ console.log(chalk.dim(' Manual fix: cd ~/.rigour && npm install sqlite3'));
63
+ }
47
64
  return;
48
65
  }
49
66
  if (stats.totalScans === 0) {
@@ -3,6 +3,8 @@ export interface CheckOptions {
3
3
  json?: boolean;
4
4
  interactive?: boolean;
5
5
  config?: string;
6
+ noCache?: boolean;
7
+ cache?: boolean;
6
8
  deep?: boolean;
7
9
  pro?: boolean;
8
10
  apiKey?: string;
@@ -2,7 +2,7 @@ import fs from 'fs-extra';
2
2
  import path from 'path';
3
3
  import chalk from 'chalk';
4
4
  import yaml from 'yaml';
5
- import { GateRunner, ConfigSchema, recordScore, getScoreTrend, resolveDeepOptions, getProvenanceTrends, getQualityTrend } from '@rigour-labs/core';
5
+ import { GateRunner, ConfigSchema, recordScore, getScoreTrend, resolveDeepOptions, getProvenanceTrends, getQualityTrend, IncrementalCache } from '@rigour-labs/core';
6
6
  import inquirer from 'inquirer';
7
7
  import { randomUUID } from 'crypto';
8
8
  // Exit codes per spec
@@ -44,6 +44,51 @@ export async function checkCommand(cwd, files = [], options = {}) {
44
44
  const config = ConfigSchema.parse(rawConfig);
45
45
  const isDeep = !!options.deep || !!options.pro || !!options.apiKey;
46
46
  const isSilent = !!options.ci || !!options.json;
47
+ const useCache = options.cache !== false && !options.noCache && !isDeep; // Cache only for non-deep runs
48
+ // ── Incremental Cache: skip scan if no files changed ──
49
+ if (useCache && files.length === 0) {
50
+ try {
51
+ const cache = new IncrementalCache(cwd);
52
+ const { FileScanner } = await import('@rigour-labs/core');
53
+ const allFiles = await FileScanner.findFiles({ cwd, ignore: config.ignore });
54
+ const result = await cache.check(allFiles, configContent);
55
+ if (result.hit && result.report) {
56
+ if (!isSilent) {
57
+ console.log(chalk.green(`⚔ No files changed — returning cached result (${result.checkMs}ms)\n`));
58
+ }
59
+ // Still write the report and record score
60
+ const reportPath = path.join(cwd, config.output.report_path);
61
+ await fs.writeJson(reportPath, result.report, { spaces: 2 });
62
+ recordScore(cwd, result.report);
63
+ if (options.json) {
64
+ process.stdout.write(JSON.stringify(result.report, null, 2) + '\n', () => {
65
+ process.exit(result.report.status === 'PASS' ? EXIT_PASS : EXIT_FAIL);
66
+ });
67
+ return;
68
+ }
69
+ if (options.ci) {
70
+ const scoreStr = result.report.stats.score !== undefined ? ` (${result.report.stats.score}/100)` : '';
71
+ console.log(`${result.report.status}${scoreStr} (cached)`);
72
+ process.exit(result.report.status === 'PASS' ? EXIT_PASS : EXIT_FAIL);
73
+ }
74
+ renderStandardOutput(result.report, config);
75
+ const trend = getScoreTrend(cwd);
76
+ if (trend && trend.recentScores.length >= 3) {
77
+ const arrow = trend.direction === 'improving' ? chalk.green('↑') :
78
+ trend.direction === 'degrading' ? chalk.red('↓') : chalk.dim('→');
79
+ const trendColor = trend.direction === 'improving' ? chalk.green :
80
+ trend.direction === 'degrading' ? chalk.red : chalk.dim;
81
+ const scoresStr = trend.recentScores.map(s => String(s)).join(' → ');
82
+ console.log(trendColor(`\nScore Trend: ${scoresStr} (${trend.direction} ${arrow})`));
83
+ }
84
+ console.log(chalk.dim(`\nFinished in ${result.checkMs}ms (cached) | Score: ${result.report.stats.score ?? '?'}/100`));
85
+ process.exit(result.report.status === 'PASS' ? EXIT_PASS : EXIT_FAIL);
86
+ }
87
+ }
88
+ catch {
89
+ // Cache check failed — proceed with full scan silently
90
+ }
91
+ }
47
92
  if (!isSilent) {
48
93
  if (isDeep) {
49
94
  console.log(chalk.blue.bold('Running Rigour checks + deep analysis...\n'));
@@ -110,6 +155,18 @@ export async function checkCommand(cwd, files = [], options = {}) {
110
155
  await fs.writeJson(reportPath, report, { spaces: 2 });
111
156
  // Record score for trend tracking
112
157
  recordScore(cwd, report);
158
+ // Save incremental cache for next run (non-deep only)
159
+ if (useCache && files.length === 0) {
160
+ try {
161
+ const cache = new IncrementalCache(cwd);
162
+ const { FileScanner } = await import('@rigour-labs/core');
163
+ const allFiles = await FileScanner.findFiles({ cwd, ignore: config.ignore });
164
+ await cache.save(allFiles, configContent, report);
165
+ }
166
+ catch {
167
+ // Cache save is best-effort
168
+ }
169
+ }
113
170
  // Persist to SQLite if deep analysis was used
114
171
  if (isDeep) {
115
172
  try {
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare const deepStatsCommand: Command;
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Deep Stats Command — show score trends and deep analysis history.
3
+ * Uses the JSONL score history (no sqlite required).
4
+ */
5
+ import chalk from 'chalk';
6
+ import { Command } from 'commander';
7
+ import { getScoreHistory, getScoreTrend } from '@rigour-labs/core';
8
+ export const deepStatsCommand = new Command('deep-stats')
9
+ .description('Show score trends and deep analysis statistics')
10
+ .option('--limit <n>', 'Number of recent scans to show', '10')
11
+ .action(async (options) => {
12
+ const cwd = process.cwd();
13
+ const limit = parseInt(options.limit, 10) || 10;
14
+ const history = getScoreHistory(cwd, limit);
15
+ const trend = getScoreTrend(cwd);
16
+ console.log(chalk.bold.cyan('\nšŸ“Š Rigour Deep Stats\n'));
17
+ if (history.length === 0) {
18
+ console.log(chalk.dim(' No scan history found.'));
19
+ console.log(chalk.dim(' Run `rigour check` to start recording scores.\n'));
20
+ return;
21
+ }
22
+ // Score trend
23
+ if (trend && trend.recentScores.length >= 3) {
24
+ const arrow = trend.direction === 'improving' ? chalk.green('↑') :
25
+ trend.direction === 'degrading' ? chalk.red('↓') : chalk.dim('→');
26
+ const trendColor = trend.direction === 'improving' ? chalk.green :
27
+ trend.direction === 'degrading' ? chalk.red : chalk.dim;
28
+ const scoresStr = trend.recentScores.map(s => String(s)).join(' → ');
29
+ console.log(` Trend: ${trendColor(`${scoresStr} (${trend.direction} ${arrow})`)}`);
30
+ console.log(` Average: ${chalk.bold(String(trend.recentAvg))} (recent) vs ${String(trend.previousAvg)} (previous)`);
31
+ console.log('');
32
+ }
33
+ // Recent scans table
34
+ console.log(chalk.bold(' Recent Scans:'));
35
+ console.log(chalk.dim(' ─────────────────────────────────────────────'));
36
+ for (const entry of history.slice(-limit)) {
37
+ const date = new Date(entry.timestamp).toLocaleDateString('en-US', {
38
+ month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit'
39
+ });
40
+ const scoreColor = entry.score >= 80 ? chalk.green :
41
+ entry.score >= 50 ? chalk.yellow : chalk.red;
42
+ const statusIcon = entry.status === 'PASS' ? chalk.green('āœ”') : chalk.red('✘');
43
+ const scoreBar = 'ā–ˆ'.repeat(Math.round(entry.score / 10)) + 'ā–‘'.repeat(10 - Math.round(entry.score / 10));
44
+ console.log(` ${statusIcon} ${chalk.dim(date)} ${scoreColor(scoreBar)} ${scoreColor(String(entry.score).padStart(3))}/100 ${chalk.dim(`${entry.failureCount} findings`)}`);
45
+ }
46
+ // Severity breakdown from last scan
47
+ const latest = history[history.length - 1];
48
+ if (latest.severity_breakdown && Object.keys(latest.severity_breakdown).length > 0) {
49
+ console.log(chalk.bold('\n Latest Severity Breakdown:'));
50
+ const sev = latest.severity_breakdown;
51
+ if (sev.critical)
52
+ console.log(` ${chalk.red('ā—')} Critical: ${chalk.red(String(sev.critical))}`);
53
+ if (sev.high)
54
+ console.log(` ${chalk.yellow('ā—')} High: ${chalk.yellow(String(sev.high))}`);
55
+ if (sev.medium)
56
+ console.log(` ${chalk.blue('ā—')} Medium: ${chalk.blue(String(sev.medium))}`);
57
+ if (sev.low)
58
+ console.log(` ${chalk.dim('ā—')} Low: ${chalk.dim(String(sev.low))}`);
59
+ }
60
+ // Provenance breakdown
61
+ if (latest.provenance_breakdown && Object.keys(latest.provenance_breakdown).length > 0) {
62
+ console.log(chalk.bold('\n Latest Provenance:'));
63
+ const prov = latest.provenance_breakdown;
64
+ for (const [key, count] of Object.entries(prov)) {
65
+ if (count > 0) {
66
+ const color = key === 'ai-drift' ? chalk.magenta :
67
+ key === 'security' ? chalk.red :
68
+ key === 'deep-analysis' ? chalk.blue : chalk.dim;
69
+ console.log(` ${color('ā—')} ${key}: ${color(String(count))}`);
70
+ }
71
+ }
72
+ }
73
+ console.log('');
74
+ });
@@ -75,11 +75,19 @@ function detectTools(cwd) {
75
75
  return detected;
76
76
  }
77
77
  function resolveCheckerCommand(cwd) {
78
+ // 1. Try project-local node_modules (installed as dependency)
78
79
  const localPath = path.join(cwd, 'node_modules', '@rigour-labs', 'core', 'dist', 'hooks', 'standalone-checker.js');
79
80
  if (fs.existsSync(localPath)) {
80
81
  return { command: 'node', args: [localPath] };
81
82
  }
82
- return { command: 'rigour', args: ['hooks', 'check'] };
83
+ // 2. Try dev checkout: ESM has no __dirname, derive from import.meta.url
84
+ const thisDir = path.dirname(new URL(import.meta.url).pathname);
85
+ const localCli = path.resolve(thisDir, '../cli.js');
86
+ if (fs.existsSync(localCli)) {
87
+ return { command: 'node', args: [localCli, 'hooks', 'check'] };
88
+ }
89
+ // 3. Fallback: assume globally installed or aliased
90
+ return { command: 'npx', args: ['@rigour-labs/cli', 'hooks', 'check'] };
83
91
  }
84
92
  function shellEscape(arg) {
85
93
  if (/^[A-Za-z0-9_/@%+=:,.-]+$/.test(arg)) {
@@ -154,14 +162,14 @@ function generateCursorHooks(checker, block, dlp = true) {
154
162
  afterFileEdit: [{ command: `${checkerCommand} --stdin${blockFlag}` }],
155
163
  };
156
164
  if (dlp) {
157
- hookEntries.beforeFileEdit = [{ command: `${checkerCommand} --mode dlp --stdin` }];
165
+ hookEntries.beforeSubmitPrompt = [{ command: `${checkerCommand} --mode dlp --stdin` }];
158
166
  }
159
167
  const hooks = { version: 1, hooks: hookEntries };
160
168
  return [{
161
169
  path: '.cursor/hooks.json',
162
170
  content: JSON.stringify(hooks, null, 4),
163
171
  description: dlp
164
- ? 'Cursor hooks — afterFileEdit quality checks + beforeFileEdit DLP credential interception'
172
+ ? 'Cursor hooks — afterFileEdit quality checks + beforeSubmitPrompt DLP credential interception'
165
173
  : 'Cursor afterFileEdit hook config',
166
174
  }];
167
175
  }
@@ -429,36 +437,116 @@ function parseStdinFiles(input) {
429
437
  if (Array.isArray(payload.files)) {
430
438
  return payload.files;
431
439
  }
440
+ // Direct file_path (Cursor afterFileEdit, Claude Code)
432
441
  if (payload.file_path) {
433
442
  return [payload.file_path];
434
443
  }
444
+ // Claude Code camelCase format
435
445
  if (payload.toolInput?.path) {
436
446
  return [payload.toolInput.path];
437
447
  }
438
448
  if (payload.toolInput?.file_path) {
439
449
  return [payload.toolInput.file_path];
440
450
  }
451
+ // Cursor postToolUse: snake_case tool_input (may be object or string)
452
+ if (payload.tool_input) {
453
+ const ti = payload.tool_input;
454
+ if (typeof ti === 'object' && ti !== null) {
455
+ if (ti.file_path)
456
+ return [ti.file_path];
457
+ if (ti.path)
458
+ return [ti.path];
459
+ }
460
+ }
461
+ // Cursor postToolUse: file_path inside tool_output (JSON string)
462
+ if (typeof payload.tool_output === 'string') {
463
+ try {
464
+ const toolOut = JSON.parse(payload.tool_output);
465
+ if (toolOut.file_path)
466
+ return [toolOut.file_path];
467
+ }
468
+ catch {
469
+ // tool_output wasn't JSON
470
+ }
471
+ }
441
472
  return [];
442
473
  }
443
474
  catch {
444
475
  return input.split('\n').map(l => l.trim()).filter(Boolean);
445
476
  }
446
477
  }
478
+ /**
479
+ * Detect if stdin payload is from a Cursor hook (has hook_event_name or prompt field).
480
+ * Cursor hooks send structured JSON with specific fields and expect
481
+ * { continue: boolean, user_message?: string } back.
482
+ */
483
+ function isCursorHookPayload(payload) {
484
+ return payload && (typeof payload.hook_event_name === 'string' ||
485
+ typeof payload.prompt === 'string' ||
486
+ typeof payload.conversation_id === 'string');
487
+ }
488
+ /**
489
+ * Extract text to scan from a Cursor beforeSubmitPrompt payload.
490
+ * The prompt field contains the user's input text.
491
+ */
492
+ function extractCursorPromptText(payload) {
493
+ if (typeof payload.prompt === 'string')
494
+ return payload.prompt;
495
+ if (typeof payload.content === 'string')
496
+ return payload.content;
497
+ if (typeof payload.new_content === 'string')
498
+ return payload.new_content;
499
+ return '';
500
+ }
447
501
  export async function hooksCheckCommand(cwd, options = {}) {
448
502
  // ── DLP Mode: Scan text for credentials ──────────────────
449
503
  if (options.mode === 'dlp') {
450
- const input = options.stdin
504
+ let rawInput = options.stdin
451
505
  ? await readStdin()
452
506
  : (options.files ?? ''); // Reuse files param as text in DLP mode
453
- if (!input) {
454
- process.stdout.write(JSON.stringify({ status: 'clean', detections: [], duration_ms: 0, scanned_length: 0 }));
507
+ if (!rawInput) {
508
+ process.stdout.write(JSON.stringify({ continue: true }));
509
+ return;
510
+ }
511
+ // Parse Cursor's structured payload to extract prompt text
512
+ let textToScan = rawInput;
513
+ let cursorMode = false;
514
+ try {
515
+ const payload = JSON.parse(rawInput);
516
+ if (isCursorHookPayload(payload)) {
517
+ cursorMode = true;
518
+ textToScan = extractCursorPromptText(payload);
519
+ }
520
+ }
521
+ catch {
522
+ // Not JSON — scan raw text as-is
523
+ }
524
+ if (!textToScan) {
525
+ process.stdout.write(JSON.stringify(cursorMode ? { continue: true } : { status: 'clean', detections: [], duration_ms: 0, scanned_length: 0 }));
455
526
  return;
456
527
  }
457
- const result = scanInputForCredentials(input, {
528
+ const result = scanInputForCredentials(textToScan, {
458
529
  enabled: true,
459
530
  block_on_detection: options.block ?? true,
460
531
  });
461
- process.stdout.write(JSON.stringify(result));
532
+ // Return Cursor-compatible format if detected as Cursor hook
533
+ if (cursorMode) {
534
+ if (result.status === 'blocked') {
535
+ const messages = result.detections
536
+ .map((d) => `[${d.type}] ${d.description} → ${d.recommendation}`)
537
+ .join('\n');
538
+ process.stdout.write(JSON.stringify({
539
+ continue: false,
540
+ user_message: `šŸ›‘ Rigour DLP: ${result.detections.length} credential(s) detected in your prompt:\n${messages}\n\nReplace with environment variable references before submitting.`,
541
+ }));
542
+ }
543
+ else {
544
+ process.stdout.write(JSON.stringify({ continue: true }));
545
+ }
546
+ }
547
+ else {
548
+ process.stdout.write(JSON.stringify(result));
549
+ }
462
550
  if (result.status !== 'clean') {
463
551
  process.stderr.write('\n' + formatDLPAlert(result) + '\n');
464
552
  // Audit trail
@@ -479,11 +567,27 @@ export async function hooksCheckCommand(cwd, options = {}) {
479
567
  }
480
568
  // ── Standard Mode: Check files ───────────────────────────
481
569
  const timeout = options.timeout ? Number(options.timeout) : 5000;
570
+ let rawStdin = '';
571
+ let cursorMode = false;
572
+ if (options.stdin) {
573
+ rawStdin = await readStdin();
574
+ // Detect Cursor/IDE hook payload format
575
+ try {
576
+ const payload = JSON.parse(rawStdin);
577
+ if (isCursorHookPayload(payload) || typeof payload.new_content === 'string') {
578
+ cursorMode = true;
579
+ }
580
+ }
581
+ catch (parseErr) {
582
+ // Not valid JSON — log for debugging (stderr only, stdout must stay clean)
583
+ process.stderr.write(`[rigour-hook-debug] stdin JSON parse failed: ${parseErr?.message?.slice(0, 100)}\n`);
584
+ }
585
+ }
482
586
  const files = options.stdin
483
- ? parseStdinFiles(await readStdin())
587
+ ? parseStdinFiles(rawStdin)
484
588
  : (options.files ?? '').split(',').map(f => f.trim()).filter(Boolean);
485
589
  if (files.length === 0) {
486
- process.stdout.write(JSON.stringify({ status: 'pass', failures: [], duration_ms: 0 }));
590
+ process.stdout.write(JSON.stringify(cursorMode ? { continue: true } : { status: 'pass', failures: [], duration_ms: 0 }));
487
591
  return;
488
592
  }
489
593
  const result = await runHookChecker({
@@ -491,7 +595,27 @@ export async function hooksCheckCommand(cwd, options = {}) {
491
595
  files,
492
596
  timeout_ms: Number.isFinite(timeout) ? timeout : 5000,
493
597
  });
494
- process.stdout.write(JSON.stringify(result));
598
+ // Return Cursor-compatible format if detected as Cursor hook
599
+ if (cursorMode) {
600
+ if (result.status === 'fail') {
601
+ const messages = result.failures
602
+ .map(f => {
603
+ const loc = f.line ? `:${f.line}` : '';
604
+ return `[${f.gate}] ${f.file}${loc}: ${f.message}`;
605
+ })
606
+ .join('\n');
607
+ process.stdout.write(JSON.stringify({
608
+ continue: !options.block, // block mode = stop, otherwise warn
609
+ user_message: `āš ļø Rigour: ${result.failures.length} issue(s) found:\n${messages}`,
610
+ }));
611
+ }
612
+ else {
613
+ process.stdout.write(JSON.stringify({ continue: true }));
614
+ }
615
+ }
616
+ else {
617
+ process.stdout.write(JSON.stringify(result));
618
+ }
495
619
  if (result.status === 'fail') {
496
620
  for (const failure of result.failures) {
497
621
  const loc = failure.line ? `:${failure.line}` : '';
@@ -30,7 +30,7 @@ describe('hooksInitCommand', () => {
30
30
  const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
31
31
  expect(settings.hooks).toBeDefined();
32
32
  expect(settings.hooks.PostToolUse).toBeDefined();
33
- expect(settings.hooks.PostToolUse[0].hooks[0].command).toContain('rigour hooks check');
33
+ expect(settings.hooks.PostToolUse[0].hooks[0].command).toContain('hooks check');
34
34
  });
35
35
  it('should generate Cursor hooks', async () => {
36
36
  await hooksInitCommand(testDir, { tool: 'cursor' });
@@ -115,7 +115,7 @@ describe('hooksInitCommand — DLP integration', () => {
115
115
  const hooksPath = path.join(testDir, '.cursor', 'hooks.json');
116
116
  const hooks = JSON.parse(fs.readFileSync(hooksPath, 'utf-8'));
117
117
  expect(hooks.hooks.afterFileEdit).toBeDefined();
118
- expect(hooks.hooks.beforeFileEdit).toBeDefined();
118
+ expect(hooks.hooks.beforeSubmitPrompt).toBeDefined();
119
119
  });
120
120
  it('should generate Windsurf hooks with DLP by default', async () => {
121
121
  await hooksInitCommand(testDir, { tool: 'windsurf', force: true });
@@ -23,57 +23,61 @@ async function logStudioEvent(cwd, event) {
23
23
  // Silent fail
24
24
  }
25
25
  }
26
- function detectIDE(cwd) {
27
- // Check for Claude Code markers
26
+ /**
27
+ * Detect ALL IDEs/agents present in the project (not just the first match).
28
+ * A project using Cursor often also has CLAUDE.md, .clinerules, etc.
29
+ * We need hooks for every tool that has markers.
30
+ */
31
+ function detectAllIDEs(cwd) {
32
+ const detected = [];
28
33
  if (fs.existsSync(path.join(cwd, 'CLAUDE.md')) || fs.existsSync(path.join(cwd, '.claude'))) {
29
- return 'claude';
34
+ detected.push('claude');
30
35
  }
31
- // Check for Gemini Code Assist markers
32
- if (fs.existsSync(path.join(cwd, '.gemini'))) {
33
- return 'gemini';
36
+ if (fs.existsSync(path.join(cwd, '.cursor'))) {
37
+ detected.push('cursor');
34
38
  }
35
- // Check for Codex/Aider AGENTS.md (universal standard)
36
- if (fs.existsSync(path.join(cwd, 'AGENTS.md'))) {
37
- return 'codex';
39
+ if (fs.existsSync(path.join(cwd, '.clinerules'))) {
40
+ detected.push('cline');
38
41
  }
39
- // Check for Windsurf markers
40
42
  if (fs.existsSync(path.join(cwd, '.windsurfrules')) || fs.existsSync(path.join(cwd, '.windsurf'))) {
41
- return 'windsurf';
43
+ detected.push('windsurf');
42
44
  }
43
- // Check for Cline-specific markers
44
- if (fs.existsSync(path.join(cwd, '.clinerules'))) {
45
- return 'cline';
45
+ if (fs.existsSync(path.join(cwd, '.gemini'))) {
46
+ detected.push('gemini');
46
47
  }
47
- // Check for Cursor-specific markers
48
- if (fs.existsSync(path.join(cwd, '.cursor'))) {
49
- return 'cursor';
48
+ if (fs.existsSync(path.join(cwd, 'AGENTS.md'))) {
49
+ detected.push('codex');
50
50
  }
51
- // Check for VS Code markers
52
51
  if (fs.existsSync(path.join(cwd, '.vscode'))) {
53
- return 'vscode';
54
- }
55
- // Check environment variables that IDEs/Agents set
56
- const termProgram = process.env.TERM_PROGRAM || '';
57
- const terminal = process.env.TERMINAL_EMULATOR || '';
58
- const appName = process.env.APP_NAME || '';
59
- if (termProgram.toLowerCase().includes('cursor') || terminal.toLowerCase().includes('cursor')) {
60
- return 'cursor';
61
- }
62
- if (termProgram.toLowerCase().includes('cline') || appName.toLowerCase().includes('cline')) {
63
- return 'cline';
64
- }
65
- if (termProgram.toLowerCase().includes('vscode') || process.env.VSCODE_INJECTION) {
66
- return 'vscode';
67
- }
68
- // Check for Claude Code environment
69
- if (process.env.CLAUDE_CODE || process.env.ANTHROPIC_API_KEY) {
70
- return 'claude';
71
- }
72
- // Check for Gemini environment
73
- if (process.env.GEMINI_API_KEY || process.env.GOOGLE_CLOUD_PROJECT) {
74
- return 'gemini';
52
+ detected.push('vscode');
53
+ }
54
+ // Fallback: check environment variables if no file markers found
55
+ if (detected.length === 0) {
56
+ const termProgram = process.env.TERM_PROGRAM || '';
57
+ const terminal = process.env.TERMINAL_EMULATOR || '';
58
+ const appName = process.env.APP_NAME || '';
59
+ if (termProgram.toLowerCase().includes('cursor') || terminal.toLowerCase().includes('cursor')) {
60
+ detected.push('cursor');
61
+ }
62
+ else if (termProgram.toLowerCase().includes('cline') || appName.toLowerCase().includes('cline')) {
63
+ detected.push('cline');
64
+ }
65
+ else if (termProgram.toLowerCase().includes('vscode') || process.env.VSCODE_INJECTION) {
66
+ detected.push('vscode');
67
+ }
68
+ else if (process.env.CLAUDE_CODE || process.env.ANTHROPIC_API_KEY) {
69
+ detected.push('claude');
70
+ }
71
+ else if (process.env.GEMINI_API_KEY || process.env.GOOGLE_CLOUD_PROJECT) {
72
+ detected.push('gemini');
73
+ }
75
74
  }
76
- return 'unknown';
75
+ return detected.length > 0 ? detected : ['unknown'];
76
+ }
77
+ /** Legacy single-IDE detection for backward compatibility (returns primary IDE). */
78
+ function detectIDE(cwd) {
79
+ const all = detectAllIDEs(cwd);
80
+ return all[0] || 'unknown';
77
81
  }
78
82
  export async function initCommand(cwd, options = {}) {
79
83
  const discovery = new DiscoveryService();
@@ -154,6 +158,12 @@ export async function initCommand(cwd, options = {}) {
154
158
  console.log(chalk.cyan(` Paradigm: `) + chalk.bold(recommendedConfig.paradigm.toUpperCase()));
155
159
  }
156
160
  console.log('');
161
+ const ALL_HOOK_TOOLS = ['claude', 'cursor', 'cline', 'windsurf'];
162
+ recommendedConfig.hooks = {
163
+ ...recommendedConfig.hooks,
164
+ enabled: true,
165
+ tools: ALL_HOOK_TOOLS,
166
+ };
157
167
  const yamlHeader = `# āš ļø TEAM STANDARD - DO NOT MODIFY WITHOUT TEAM APPROVAL
158
168
  # AI Assistants: Adjust YOUR code to meet these standards, not the other way around.
159
169
  # Modifying thresholds or adding ignores to pass checks defeats the purpose of Rigour.
@@ -178,21 +188,47 @@ export async function initCommand(cwd, options = {}) {
178
188
  const instructionsPath = path.join(rigourDocsDir, 'AGENT_INSTRUCTIONS.md');
179
189
  const ruleContent = `# šŸ›”ļø Rigour: Mandatory Engineering Governance Protocol
180
190
 
181
- **You are an AI Engineering Agent governed by Rigour.**
191
+ **You are an AI Engineering Agent governed by Rigour.** You MUST use the Rigour MCP tools at specific points in your workflow. This is non-negotiable.
192
+
193
+ ## 🚦 BEFORE Writing Any Code
182
194
 
183
- Your access to this codebase is conditional upon your strict adherence to the **Rigour Governance Protocol**. This is an automated requirement for every engineering task.
195
+ 1. **Load Context**: Call the \`rigour_recall\` MCP tool to load project memory and conventions.
196
+ 2. **Check Patterns**: Call the \`rigour_check_pattern\` MCP tool with the name and type of what you're about to create (e.g., name: "capitalize", type: "function"). This prevents reinventing existing utilities and catches security issues (CVEs).
184
197
 
185
- ## 🚦 The Mandatory Governance Loop
186
- You MUST execute this loop for every task. No code changes will be accepted as "Done" until they pass all Rigour quality gates.
198
+ ## āœļø AFTER Writing Code (Automatic)
187
199
 
188
- 1. **Context Loading**: Call \`rigour_recall\` (MCP) to align with project-specific engineering memory.
189
- 2. **Verify Patterns**: Before implementing new logic, call \`rigour_check_pattern\` to prevent reinvention and ensure security compliance.
190
- 3. **Audit Enforcement**: Before finishing, you MUST call \`npx @rigour-labs/cli check\`.
191
- 4. **Failure Remediation**: If the check returns **FAIL**, you MUST implement all required engineering refinements.
192
- 5. **PASS or REJECT**: You may only declare a task complete when the check returns **PASS**.
200
+ Real-time hooks run automatically after every file edit. If a hook returns a violation, you MUST fix the issue before continuing. Do NOT ignore hook warnings or errors.
201
+
202
+ ## āœ… BEFORE Declaring "Done"
203
+
204
+ 1. **Run Quality Gates**: Call the \`rigour_check\` MCP tool to verify all quality gates pass.
205
+ 2. **If FAIL**: Read the failure details, fix the code, and call \`rigour_check\` again.
206
+ 3. **Only declare a task complete when \`rigour_check\` returns PASS.**
207
+
208
+ ## šŸ“‹ Available Rigour MCP Tools
209
+
210
+ You MUST use these tools. They are registered via MCP and available in your tool list:
211
+
212
+ - \`rigour_recall\` — Load project memory and conventions (call at the START of every task)
213
+ - \`rigour_check_pattern\` — Check if a function/component/hook already exists BEFORE creating a new one
214
+ - \`rigour_check\` — Run all quality gates (call BEFORE declaring done)
215
+ - \`rigour_explain\` — Get detailed, actionable fix instructions for gate failures
216
+ - \`rigour_get_fix_packet\` — Get machine-readable fix data for automated remediation
217
+ - \`rigour_remember\` — Save important decisions, conventions, or context for future tasks
218
+ - \`rigour_security_audit\` — Audit project dependencies for known CVEs
219
+ - \`rigour_review\` — Review a code diff against quality gates
220
+ - \`rigour_hooks_check\` — Run the fast hook checker on specific files manually
193
221
 
194
222
  ## āš–ļø Transparency & Accountability
195
- **Every action, tool call, and logic shift you perform is automatically logged to the project's local audit trail.** This data is visible in real-time in the human-monitored **Rigour Studio**.
223
+
224
+ Every action, tool call, and code change is automatically logged to the project's local audit trail. This data is visible in real-time in **Rigour Studio** (the human-monitored governance dashboard).
225
+
226
+ ## āš ļø CRITICAL RULES
227
+
228
+ - NEVER skip the governance loop (recall → check_pattern → write → rigour_check)
229
+ - NEVER modify \`rigour.yml\` thresholds or ignore lists to make checks pass
230
+ - NEVER claim "done" without a passing \`rigour_check\` result
231
+ - If you're unsure whether to proceed, call \`rigour_check\` — it's fast and free
196
232
 
197
233
  ${AGNOSTIC_AI_INSTRUCTIONS}
198
234
  ${CODE_QUALITY_RULES}
@@ -200,75 +236,105 @@ ${DEBUGGING_RULES}
200
236
  ${COLLABORATION_RULES}
201
237
  `;
202
238
  // 1. Create Universal Instructions
203
- if (!(await fs.pathExists(instructionsPath))) {
239
+ if (!(await fs.pathExists(instructionsPath)) || options.force) {
204
240
  await fs.writeFile(instructionsPath, ruleContent);
205
241
  console.log(chalk.green('āœ” Initialized Universal Agent Handshake (docs/AGENT_INSTRUCTIONS.md)'));
206
242
  }
207
- // 2. Create IDE-Specific Rules based on detection or user preference
208
- const detectedIDE = detectIDE(cwd);
209
- const targetIDE = options.ide || (detectedIDE !== 'unknown' ? detectedIDE : 'all');
210
- if (detectedIDE !== 'unknown' && !options.ide) {
211
- console.log(chalk.dim(` (Auto-detected IDE: ${detectedIDE})`));
212
- }
213
- if (targetIDE === 'cursor' || targetIDE === 'all') {
243
+ // 2. Create IDE-Specific Rules for ALL supported tools.
244
+ // Detection is unreliable (Cursor reports as vscode, doesn't create .cursor/),
245
+ // so we always set up everything. The files are tiny and inert if unused.
246
+ const shouldSetup = (_ide) => true;
247
+ if (shouldSetup('cursor')) {
214
248
  const cursorRulesDir = path.join(cwd, '.cursor', 'rules');
215
249
  await fs.ensureDir(cursorRulesDir);
216
250
  const mdcPath = path.join(cursorRulesDir, 'rigour.mdc');
251
+ // Cursor .mdc must be SHORT and forceful — long rules get ignored.
252
+ // Keep ONLY the mandatory MCP tool workflow, no generic coding advice.
217
253
  const mdcContent = `---
218
- description: Enforcement of Rigour quality gates and best practices.
254
+ description: MANDATORY - Rigour governance. You MUST call Rigour MCP tools before and after every code task.
219
255
  globs: **/*
256
+ alwaysApply: true
220
257
  ---
221
258
 
222
- ${ruleContent}`;
223
- if (!(await fs.pathExists(mdcPath))) {
259
+ # Rigour Governance — MANDATORY for every task
260
+
261
+ You MUST follow this exact sequence for EVERY coding task. No exceptions.
262
+
263
+ ## Step 1: BEFORE writing any code
264
+ \`\`\`
265
+ Call rigour_recall → loads project memory and conventions
266
+ Call rigour_check_pattern → checks if the thing you're creating already exists
267
+ \`\`\`
268
+ Both calls are REQUIRED before you write a single line of code.
269
+
270
+ ## Step 2: Write your code
271
+ Hooks will automatically check your code after each file edit. If a hook blocks you, fix the issue immediately.
272
+
273
+ ## Step 3: BEFORE saying "done"
274
+ \`\`\`
275
+ Call rigour_check → runs all quality gates
276
+ \`\`\`
277
+ If it returns FAIL, fix the issues and call rigour_check again. You may ONLY declare done when it returns PASS.
278
+
279
+ ## When the user asks you to remember something
280
+ \`\`\`
281
+ Call rigour_remember with key="short_snake_case_id" and value="the full text to remember"
282
+ \`\`\`
283
+
284
+ ## NEVER do these
285
+ - Never skip rigour_recall at the start of a task
286
+ - Never skip rigour_check before declaring done
287
+ - Never modify rigour.yml to make checks pass
288
+ `;
289
+ if (!(await fs.pathExists(mdcPath)) || options.force) {
224
290
  await fs.writeFile(mdcPath, mdcContent);
225
291
  console.log(chalk.green('āœ” Initialized Cursor Handshake (.cursor/rules/rigour.mdc)'));
226
292
  }
227
293
  }
228
- if (targetIDE === 'vscode' || targetIDE === 'all') {
294
+ if (shouldSetup('vscode')) {
229
295
  // VS Code users use the universal AGENT_INSTRUCTIONS.md (already created above)
230
296
  // We could also add .vscode/settings.json or snippets here if needed
231
297
  console.log(chalk.green('āœ” VS Code mode - using Universal Handshake (docs/AGENT_INSTRUCTIONS.md)'));
232
298
  }
233
- if (targetIDE === 'cline' || targetIDE === 'all') {
299
+ if (shouldSetup('cline')) {
234
300
  const clineRulesPath = path.join(cwd, '.clinerules');
235
- if (!(await fs.pathExists(clineRulesPath))) {
301
+ if (!(await fs.pathExists(clineRulesPath)) || options.force) {
236
302
  await fs.writeFile(clineRulesPath, ruleContent);
237
303
  console.log(chalk.green('āœ” Initialized Cline Handshake (.clinerules)'));
238
304
  }
239
305
  }
240
306
  // Claude Code (CLAUDE.md)
241
- if (targetIDE === 'claude' || targetIDE === 'all') {
307
+ if (shouldSetup('claude')) {
242
308
  const claudePath = path.join(cwd, 'CLAUDE.md');
243
309
  const claudeContent = `# CLAUDE.md - Project Instructions for Claude Code
244
310
 
245
311
  This file provides Claude Code with context about this project.
246
312
 
247
- ## Project Overview
248
-
249
- This project uses Rigour for quality gates. Always run \`npx @rigour-labs/cli check\` before marking tasks complete.
250
-
251
- ## Commands
313
+ ## Quick Commands
252
314
 
253
315
  \`\`\`bash
254
- # Verify quality gates
316
+ # Run quality gates (CLI alternative to MCP)
255
317
  npx @rigour-labs/cli check
256
318
 
257
- # Get fix packet for failures
319
+ # Get fix instructions
258
320
  npx @rigour-labs/cli explain
259
321
 
260
322
  # Self-healing agent loop
261
323
  npx @rigour-labs/cli run -- claude "<task>"
262
324
  \`\`\`
263
325
 
326
+ ## Rigour MCP Tools (PREFERRED over CLI)
327
+
328
+ Use the Rigour MCP tools instead of CLI commands when available. They are faster and integrated into your workflow.
329
+
264
330
  ${ruleContent}`;
265
- if (!(await fs.pathExists(claudePath))) {
331
+ if (!(await fs.pathExists(claudePath)) || options.force) {
266
332
  await fs.writeFile(claudePath, claudeContent);
267
333
  console.log(chalk.green('āœ” Initialized Claude Code Handshake (CLAUDE.md)'));
268
334
  }
269
335
  }
270
336
  // Gemini Code Assist (.gemini/styleguide.md)
271
- if (targetIDE === 'gemini' || targetIDE === 'all') {
337
+ if (shouldSetup('gemini')) {
272
338
  const geminiDir = path.join(cwd, '.gemini');
273
339
  await fs.ensureDir(geminiDir);
274
340
  const geminiStylePath = path.join(geminiDir, 'styleguide.md');
@@ -281,13 +347,13 @@ This project uses Rigour for quality gates.
281
347
  Always run \`npx @rigour-labs/cli check\` before marking any task complete.
282
348
 
283
349
  ${ruleContent}`;
284
- if (!(await fs.pathExists(geminiStylePath))) {
350
+ if (!(await fs.pathExists(geminiStylePath)) || options.force) {
285
351
  await fs.writeFile(geminiStylePath, geminiContent);
286
352
  console.log(chalk.green('āœ” Initialized Gemini Handshake (.gemini/styleguide.md)'));
287
353
  }
288
354
  }
289
355
  // OpenAI Codex / Aider (AGENTS.md - Universal Standard)
290
- if (targetIDE === 'codex' || targetIDE === 'all') {
356
+ if (shouldSetup('codex')) {
291
357
  const agentsPath = path.join(cwd, 'AGENTS.md');
292
358
  const agentsContent = `# AGENTS.md - Universal AI Agent Instructions
293
359
 
@@ -310,22 +376,25 @@ npx @rigour-labs/cli check
310
376
  \`\`\`
311
377
 
312
378
  ${ruleContent}`;
313
- if (!(await fs.pathExists(agentsPath))) {
379
+ if (!(await fs.pathExists(agentsPath)) || options.force) {
314
380
  await fs.writeFile(agentsPath, agentsContent);
315
381
  console.log(chalk.green('āœ” Initialized Universal Agent Handshake (AGENTS.md)'));
316
382
  }
317
383
  }
318
384
  // Windsurf (.windsurfrules)
319
- if (targetIDE === 'windsurf' || targetIDE === 'all') {
385
+ if (shouldSetup('windsurf')) {
320
386
  const windsurfPath = path.join(cwd, '.windsurfrules');
321
- if (!(await fs.pathExists(windsurfPath))) {
387
+ if (!(await fs.pathExists(windsurfPath)) || options.force) {
322
388
  await fs.writeFile(windsurfPath, ruleContent);
323
389
  console.log(chalk.green('āœ” Initialized Windsurf Handshake (.windsurfrules)'));
324
390
  }
325
391
  }
326
- // 3. Auto-initialize hooks for detected AI coding tools
327
- await initHooksForDetectedTools(cwd, detectedIDE);
328
- // 4. Update .gitignore
392
+ // 3. Auto-initialize hooks for ALL supported AI coding tools
393
+ const allSupportedIDEs = ['claude', 'cursor', 'cline', 'windsurf'];
394
+ await initHooksForAllDetectedTools(cwd, allSupportedIDEs);
395
+ // 4. Auto-register MCP server for all supported tools
396
+ await initMCPForDetectedTools(cwd, allSupportedIDEs, options.force);
397
+ // 5. Update .gitignore
329
398
  const gitignorePath = path.join(cwd, '.gitignore');
330
399
  const ignorePatterns = ['rigour-report.json', 'rigour-fix-packet.json', '.rigour/'];
331
400
  try {
@@ -344,8 +413,9 @@ ${ruleContent}`;
344
413
  catch (e) {
345
414
  // Failing to update .gitignore isn't fatal
346
415
  }
416
+ // 6. Auto-build pattern index (with semantic embeddings)
417
+ await buildPatternIndex(cwd, options.force);
347
418
  console.log(chalk.blue('\nRigour is ready. Run `npx @rigour-labs/cli check` to verify your project.'));
348
- console.log(chalk.cyan('Next Step: ') + chalk.bold('rigour index') + chalk.dim(' (Populate the Pattern Index)'));
349
419
  // Bootstrap initial memory for the Studio
350
420
  const rigourDir = path.join(cwd, ".rigour");
351
421
  await fs.ensureDir(rigourDir);
@@ -428,28 +498,34 @@ async function checkPrerequisites() {
428
498
  const isReady = hasApiKey || (hasSidecar && (hasDeepModel || hasLiteModel));
429
499
  if (isReady) {
430
500
  console.log(chalk.green('\n āœ“ Deep analysis is ready!'));
501
+ if (hasSidecar && hasDeepModel) {
502
+ console.log(chalk.dim(' Run: rigour check --deep (Rigour local engine)'));
503
+ console.log(chalk.dim(' Run: rigour check --deep --pro (full model, code-specialized)'));
504
+ }
505
+ else if (hasSidecar && hasLiteModel) {
506
+ console.log(chalk.dim(' Run: rigour check --deep (Rigour local engine — lite)'));
507
+ }
431
508
  if (hasApiKey) {
432
509
  const defaultProvider = settings.deep?.defaultProvider || configuredKeys[0]?.[0] || 'unknown';
433
- console.log(chalk.dim(` Run: rigour check --deep --provider ${defaultProvider}`));
434
- }
435
- if (hasSidecar && hasDeepModel) {
436
- console.log(chalk.dim(' Run: rigour check --deep (100% local, free)'));
510
+ console.log(chalk.dim(` Run: rigour check --deep --provider ${defaultProvider} (cloud BYOK)`));
437
511
  }
438
512
  }
439
513
  else {
440
514
  console.log(chalk.bold.yellow('\n ⚔ Set up deep analysis (optional):'));
441
515
  console.log('');
442
- console.log(chalk.bold(' Option A: Cloud API (Recommended — instant, high quality)'));
516
+ console.log(chalk.bold(' Option A: Rigour Local Engine (Recommended — private, no API key needed)'));
517
+ console.log(chalk.dim(' rigour check --deep # Lite model (500MB, any CPU)'));
518
+ console.log(chalk.dim(' rigour check --deep --pro # Full model (900MB, code-specialized)'));
519
+ console.log(chalk.dim(' Fine-tuned on real code quality findings via RLAIF pipeline.'));
520
+ console.log(chalk.dim(' 100% local — your code never leaves your machine.'));
521
+ console.log('');
522
+ console.log(chalk.bold(' Option B: Cloud BYOK (bring your own API key)'));
443
523
  console.log(chalk.dim(' rigour settings set-key anthropic sk-ant-xxx # Claude'));
444
524
  console.log(chalk.dim(' rigour settings set-key openai sk-xxx # OpenAI'));
445
- console.log(chalk.dim(' rigour settings set-key groq gsk_xxx # Groq (fast + free tier)'));
446
- console.log(chalk.dim(' Then: rigour check --deep'));
447
- console.log('');
448
- console.log(chalk.bold(' Option B: 100% Local (Free, private, 350MB download)'));
449
- console.log(chalk.dim(' rigour check --deep # Auto-downloads model'));
450
- console.log(chalk.dim(' rigour check --deep --pro # Larger model (900MB)'));
525
+ console.log(chalk.dim(' rigour settings set-key groq gsk_xxx # Groq'));
526
+ console.log(chalk.dim(' Then: rigour check --deep --provider <name>'));
451
527
  console.log('');
452
- console.log(chalk.dim(' Without deep analysis, Rigour still runs 15+ AST-based quality gates.'));
528
+ console.log(chalk.dim(' Without deep analysis, Rigour still runs 27+ deterministic quality gates.'));
453
529
  }
454
530
  console.log('');
455
531
  }
@@ -460,18 +536,139 @@ const IDE_TO_HOOK_TOOL = {
460
536
  cline: 'cline',
461
537
  windsurf: 'windsurf',
462
538
  };
463
- async function initHooksForDetectedTools(cwd, detectedIDE) {
464
- const hookTool = IDE_TO_HOOK_TOOL[detectedIDE];
465
- if (!hookTool) {
466
- return; // Unknown IDE or no hook support (vscode, gemini, codex)
467
- }
539
+ /**
540
+ * Build the pattern index so rigour_check_pattern can detect duplicates.
541
+ * Uses semantic embeddings by default for fuzzy matching.
542
+ * Non-fatal — if indexing fails, init still succeeds.
543
+ */
544
+ async function buildPatternIndex(cwd, force) {
468
545
  try {
469
- console.log(chalk.dim(`\n Setting up real-time hooks for ${detectedIDE}...`));
470
- await hooksInitCommand(cwd, { tool: hookTool, dlp: true });
471
- console.log(chalk.dim(` šŸ›‘ DLP protection active — credentials intercepted before reaching agents`));
546
+ console.log(chalk.dim('\n Building pattern index (this enables duplicate detection)...'));
547
+ const { PatternIndexer, savePatternIndex, loadPatternIndex, getDefaultIndexPath } = await import('@rigour-labs/core/pattern-index');
548
+ const indexPath = getDefaultIndexPath(cwd);
549
+ const existingIndex = await loadPatternIndex(indexPath);
550
+ const indexer = new PatternIndexer(cwd, { useEmbeddings: false });
551
+ let index;
552
+ if (existingIndex && !force) {
553
+ index = await indexer.updateIndex(existingIndex);
554
+ }
555
+ else {
556
+ index = await indexer.buildIndex();
557
+ }
558
+ await savePatternIndex(index, indexPath);
559
+ console.log(chalk.green(`āœ” Pattern index built: ${index.stats.totalPatterns} patterns across ${index.stats.totalFiles} files`));
472
560
  }
473
- catch {
474
- // Non-fatal — hooks are a bonus, not a requirement
475
- console.log(chalk.dim(` (Hooks setup skipped — run 'rigour hooks init' manually)`));
561
+ catch (err) {
562
+ console.log(chalk.dim(` (Pattern index build skipped: ${err?.message || err})`));
563
+ }
564
+ }
565
+ /**
566
+ * Initialize hooks for ALL detected IDEs/agents.
567
+ * Returns the list of hook tool names that were successfully initialized.
568
+ */
569
+ async function initHooksForAllDetectedTools(cwd, detectedIDEs) {
570
+ const enabledTools = [];
571
+ for (const ide of detectedIDEs) {
572
+ const hookTool = IDE_TO_HOOK_TOOL[ide];
573
+ if (!hookTool)
574
+ continue; // No hook support (vscode, gemini, codex)
575
+ try {
576
+ console.log(chalk.dim(`\n Setting up real-time hooks for ${ide}...`));
577
+ await hooksInitCommand(cwd, { tool: hookTool, dlp: true, force: true, block: true });
578
+ enabledTools.push(hookTool);
579
+ }
580
+ catch (err) {
581
+ console.log(chalk.dim(` (Hooks setup for ${ide} failed: ${err?.message || err})`));
582
+ }
583
+ }
584
+ if (enabledTools.length > 0) {
585
+ console.log(chalk.dim(` šŸ›‘ DLP protection active for: ${enabledTools.join(', ')}`));
586
+ }
587
+ return enabledTools;
588
+ }
589
+ /**
590
+ * Auto-register the Rigour MCP server for detected AI coding tools.
591
+ *
592
+ * Cursor: .cursor/mcp.json → { mcpServers: { rigour: { command, args } } }
593
+ * Claude: .claude/settings.json → merge mcpServers into existing settings
594
+ */
595
+ /**
596
+ * Resolve the MCP server config. If the CLI is running from a local dev
597
+ * checkout (not npx/global), point MCP at the sibling rigour-mcp dist
598
+ * so it works without publishing. Otherwise use npx.
599
+ */
600
+ function resolveMCPServerConfig() {
601
+ // ESM has no __dirname — derive from import.meta.url
602
+ const thisDir = path.dirname(new URL(import.meta.url).pathname);
603
+ // thisDir is packages/rigour-cli/dist/commands/
604
+ // Sibling MCP package is at packages/rigour-mcp/dist/index.js
605
+ const localMcpEntry = path.resolve(thisDir, '../../../rigour-mcp/dist/index.js');
606
+ if (fs.existsSync(localMcpEntry)) {
607
+ // Running from local dev checkout — use local path
608
+ return { command: 'node', args: [localMcpEntry] };
609
+ }
610
+ // Fallback: published npm package
611
+ return { command: 'npx', args: ['-y', '@rigour-labs/mcp'] };
612
+ }
613
+ async function initMCPForDetectedTools(cwd, detectedIDEs, force) {
614
+ const mcpServerConfig = resolveMCPServerConfig();
615
+ for (const ide of detectedIDEs) {
616
+ try {
617
+ if (ide === 'cursor') {
618
+ await setupCursorMCP(cwd, mcpServerConfig, force);
619
+ }
620
+ else if (ide === 'claude') {
621
+ await setupClaudeMCP(cwd, mcpServerConfig, force);
622
+ }
623
+ // Other IDEs: MCP not yet supported or handled differently
624
+ }
625
+ catch {
626
+ // Non-fatal
627
+ }
628
+ }
629
+ }
630
+ async function setupCursorMCP(cwd, serverConfig, force) {
631
+ const mcpPath = path.join(cwd, '.cursor', 'mcp.json');
632
+ await fs.ensureDir(path.dirname(mcpPath));
633
+ let existing = {};
634
+ if (await fs.pathExists(mcpPath)) {
635
+ try {
636
+ existing = await fs.readJson(mcpPath);
637
+ }
638
+ catch {
639
+ existing = {};
640
+ }
641
+ // Don't overwrite if rigour already registered (unless --force)
642
+ if (existing?.mcpServers?.rigour && !force) {
643
+ return;
644
+ }
645
+ }
646
+ if (!existing.mcpServers)
647
+ existing.mcpServers = {};
648
+ existing.mcpServers.rigour = serverConfig;
649
+ await fs.writeJson(mcpPath, existing, { spaces: 4 });
650
+ console.log(chalk.green('āœ” Registered Rigour MCP server (.cursor/mcp.json)'));
651
+ }
652
+ async function setupClaudeMCP(cwd, serverConfig, force) {
653
+ const settingsPath = path.join(cwd, '.claude', 'settings.json');
654
+ await fs.ensureDir(path.dirname(settingsPath));
655
+ // Always read existing settings (hooks may have written this file already)
656
+ let existing = {};
657
+ if (await fs.pathExists(settingsPath)) {
658
+ try {
659
+ existing = await fs.readJson(settingsPath);
660
+ }
661
+ catch {
662
+ existing = {};
663
+ }
664
+ if (existing?.mcpServers?.rigour && !force) {
665
+ return;
666
+ }
476
667
  }
668
+ // Merge — preserve existing hooks config, just add/update mcpServers.rigour
669
+ if (!existing.mcpServers)
670
+ existing.mcpServers = {};
671
+ existing.mcpServers.rigour = serverConfig;
672
+ await fs.writeJson(settingsPath, existing, { spaces: 4 });
673
+ console.log(chalk.green('āœ” Registered Rigour MCP server (.claude/settings.json)'));
477
674
  }
@@ -54,7 +54,7 @@ export async function runLoop(cwd, agentArgs, options) {
54
54
  console.log(chalk.cyan(`\nšŸš€ DEPLOYING AGENT:`));
55
55
  console.log(chalk.dim(` Command: ${currentArgs.join(' ')}`));
56
56
  try {
57
- await execa(currentArgs[0], currentArgs.slice(1), { shell: true, stdio: 'inherit', cwd });
57
+ await execa(currentArgs[0], currentArgs.slice(1), { stdio: 'inherit', cwd });
58
58
  }
59
59
  catch (error) {
60
60
  console.warn(chalk.yellow(`\nāš ļø Agent command finished with non-zero exit code. Rigour will now verify state...`));
@@ -14,8 +14,15 @@ export const studioCommand = new Command('studio')
14
14
  const apiPort = parseInt(options.port) + 1;
15
15
  const eventsPath = path.join(cwd, '.rigour/events.jsonl');
16
16
  // Calculate the local dist path (where the pre-built Studio UI lives)
17
+ // When running from source: cli/dist/commands/studio.js → cli/studio-dist/
18
+ // When running via npx: node_modules/@rigour-labs/cli/dist/commands/studio.js → cli/dist/studio-dist/
17
19
  const __dirname = path.dirname(new URL(import.meta.url).pathname);
18
- const localStudioDist = path.join(__dirname, '../studio-dist');
20
+ const candidates = [
21
+ path.join(__dirname, '../studio-dist'), // npm publish: cli/dist/studio-dist/
22
+ path.join(__dirname, '../../studio-dist'), // monorepo: cli/studio-dist/
23
+ path.join(__dirname, '../../../studio-dist'), // npx: @rigour-labs/cli/studio-dist/
24
+ ];
25
+ const localStudioDist = candidates.find(p => fs.pathExistsSync(p)) ?? candidates[0];
19
26
  const workspaceRoot = path.join(__dirname, '../../../../');
20
27
  console.log(chalk.bold.cyan('\nšŸ›”ļø Launching Rigour Studio...'));
21
28
  console.log(chalk.gray(`Project Root: ${cwd}`));
@@ -35,7 +42,6 @@ export const studioCommand = new Command('studio')
35
42
  // Start the Studio dev server in the workspace root
36
43
  const studioProcess = execa('pnpm', ['--filter', '@rigour-labs/studio', 'dev', '--port', options.port], {
37
44
  stdio: 'inherit',
38
- shell: true,
39
45
  cwd: workspaceRoot
40
46
  });
41
47
  await setupApiAndLaunch(apiPort, options.port, eventsPath, cwd, studioProcess);
@@ -30,8 +30,8 @@ describe('Init Command Rules Verification', () => {
30
30
  // Check for key sections in universal instructions
31
31
  expect(instructionsContent).toContain('# šŸ›”ļø Rigour: Mandatory Engineering Governance Protocol');
32
32
  expect(instructionsContent).toContain('# Code Quality Standards');
33
- // Check that MDC includes agnostic rules
34
- expect(mdcContent).toContain('# šŸ¤– CRITICAL INSTRUCTION FOR AI');
33
+ // Check that MDC includes governance rules
34
+ expect(mdcContent).toContain('# Rigour Governance');
35
35
  });
36
36
  it('should create .clinerules when ide is cline or all', async () => {
37
37
  const initCommand = await getInitCommand();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rigour-labs/cli",
3
- "version": "5.1.1",
3
+ "version": "5.1.2",
4
4
  "description": "CLI quality gates for AI-generated code. Forces AI agents (Claude, Cursor, Copilot) to meet strict engineering standards with PASS/FAIL enforcement.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://rigour.run",
@@ -44,7 +44,7 @@
44
44
  "inquirer": "9.2.16",
45
45
  "ora": "^8.0.1",
46
46
  "yaml": "^2.8.2",
47
- "@rigour-labs/core": "5.1.1"
47
+ "@rigour-labs/core": "5.1.2"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@types/fs-extra": "^11.0.4",