@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 +15 -5
- package/dist/commands/brain.js +19 -2
- package/dist/commands/check.d.ts +2 -0
- package/dist/commands/check.js +58 -1
- package/dist/commands/deep-stats.d.ts +2 -0
- package/dist/commands/deep-stats.js +74 -0
- package/dist/commands/hooks.js +135 -11
- package/dist/commands/hooks.test.js +2 -2
- package/dist/commands/init.js +304 -107
- package/dist/commands/run.js +1 -1
- package/dist/commands/studio.js +8 -2
- package/dist/init-rules.test.js +2 -2
- package/package.json +2 -2
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
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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 {
|
package/dist/commands/brain.js
CHANGED
|
@@ -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
|
-
|
|
46
|
-
|
|
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) {
|
package/dist/commands/check.d.ts
CHANGED
package/dist/commands/check.js
CHANGED
|
@@ -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,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
|
+
});
|
package/dist/commands/hooks.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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 +
|
|
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
|
-
|
|
504
|
+
let rawInput = options.stdin
|
|
451
505
|
? await readStdin()
|
|
452
506
|
: (options.files ?? ''); // Reuse files param as text in DLP mode
|
|
453
|
-
if (!
|
|
454
|
-
process.stdout.write(JSON.stringify({
|
|
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(
|
|
528
|
+
const result = scanInputForCredentials(textToScan, {
|
|
458
529
|
enabled: true,
|
|
459
530
|
block_on_detection: options.block ?? true,
|
|
460
531
|
});
|
|
461
|
-
|
|
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(
|
|
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
|
-
|
|
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('
|
|
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.
|
|
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 });
|
package/dist/commands/init.js
CHANGED
|
@@ -23,57 +23,61 @@ async function logStudioEvent(cwd, event) {
|
|
|
23
23
|
// Silent fail
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
34
|
+
detected.push('claude');
|
|
30
35
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
return 'gemini';
|
|
36
|
+
if (fs.existsSync(path.join(cwd, '.cursor'))) {
|
|
37
|
+
detected.push('cursor');
|
|
34
38
|
}
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
43
|
+
detected.push('windsurf');
|
|
42
44
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
return 'cline';
|
|
45
|
+
if (fs.existsSync(path.join(cwd, '.gemini'))) {
|
|
46
|
+
detected.push('gemini');
|
|
46
47
|
}
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
54
|
-
}
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
|
|
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
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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:
|
|
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
|
-
|
|
223
|
-
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
-
##
|
|
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
|
-
#
|
|
316
|
+
# Run quality gates (CLI alternative to MCP)
|
|
255
317
|
npx @rigour-labs/cli check
|
|
256
318
|
|
|
257
|
-
# Get fix
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
|
327
|
-
|
|
328
|
-
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
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(
|
|
470
|
-
|
|
471
|
-
|
|
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
|
-
|
|
475
|
-
|
|
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
|
}
|
package/dist/commands/run.js
CHANGED
|
@@ -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), {
|
|
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...`));
|
package/dist/commands/studio.js
CHANGED
|
@@ -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
|
|
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);
|
package/dist/init-rules.test.js
CHANGED
|
@@ -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
|
|
34
|
-
expect(mdcContent).toContain('#
|
|
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.
|
|
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.
|
|
47
|
+
"@rigour-labs/core": "5.1.2"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@types/fs-extra": "^11.0.4",
|