@rigour-labs/cli 1.0.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/dist/cli.js +25 -6
- package/dist/commands/check.d.ts +5 -1
- package/dist/commands/check.js +78 -31
- package/dist/commands/explain.d.ts +1 -0
- package/dist/commands/explain.js +67 -0
- package/dist/commands/init.d.ts +7 -1
- package/dist/commands/init.js +99 -25
- package/dist/commands/run.d.ts +1 -0
- package/dist/commands/run.js +63 -19
- package/package.json +2 -2
- package/src/cli.ts +27 -7
- package/src/commands/check.ts +84 -31
- package/src/commands/explain.ts +68 -0
- package/src/commands/init.ts +108 -26
- package/src/commands/run.ts +71 -19
package/LICENSE
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c) 2026 Rigour
|
|
3
|
+
Copyright (c) 2026 Ashutosh (https://github.com/erashu212), Rigour Labs.
|
|
4
4
|
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
package/dist/cli.js
CHANGED
|
@@ -4,6 +4,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
4
4
|
const commander_1 = require("commander");
|
|
5
5
|
const init_js_1 = require("./commands/init.js");
|
|
6
6
|
const check_js_1 = require("./commands/check.js");
|
|
7
|
+
const explain_js_1 = require("./commands/explain.js");
|
|
7
8
|
const run_js_1 = require("./commands/run.js");
|
|
8
9
|
const program = new commander_1.Command();
|
|
9
10
|
program
|
|
@@ -12,22 +13,40 @@ program
|
|
|
12
13
|
.version('1.0.0');
|
|
13
14
|
program
|
|
14
15
|
.command('init')
|
|
15
|
-
.description('Initialize
|
|
16
|
-
.
|
|
17
|
-
|
|
16
|
+
.description('Initialize Rigour in the current directory')
|
|
17
|
+
.option('-p, --preset <name>', 'Project preset (ui, api, infra, data)')
|
|
18
|
+
.option('--paradigm <name>', 'Coding paradigm (oop, functional, minimal)')
|
|
19
|
+
.option('--dry-run', 'Show detected configuration without writing files')
|
|
20
|
+
.option('--explain', 'Show detection markers for roles and paradigms')
|
|
21
|
+
.action(async (options) => {
|
|
22
|
+
await (0, init_js_1.initCommand)(process.cwd(), options);
|
|
18
23
|
});
|
|
19
24
|
program
|
|
20
25
|
.command('check')
|
|
21
26
|
.description('Run quality gate checks')
|
|
27
|
+
.option('--ci', 'CI mode (minimal output, non-zero exit on fail)')
|
|
28
|
+
.option('--json', 'Output report in JSON format')
|
|
29
|
+
.action(async (options) => {
|
|
30
|
+
await (0, check_js_1.checkCommand)(process.cwd(), options);
|
|
31
|
+
});
|
|
32
|
+
program
|
|
33
|
+
.command('explain')
|
|
34
|
+
.description('Explain the last quality gate report with actionable bullets')
|
|
22
35
|
.action(async () => {
|
|
23
|
-
await (0,
|
|
36
|
+
await (0, explain_js_1.explainCommand)(process.cwd());
|
|
24
37
|
});
|
|
25
38
|
program
|
|
26
39
|
.command('run')
|
|
27
40
|
.description('Execute an agent command in a loop until quality gates pass')
|
|
28
41
|
.argument('[command...]', 'The agent command to run (e.g., cursor-agent ...)')
|
|
29
|
-
.option('-i, --iterations <number>', 'Maximum number of loop iterations', '3')
|
|
42
|
+
.option('-i, --iterations <number>', 'Maximum number of loop iterations (deprecated, use --max-cycles)', '3')
|
|
43
|
+
.option('-c, --max-cycles <number>', 'Maximum number of loop iterations', '3')
|
|
44
|
+
.option('--fail-fast', 'Abort loop immediately on first gate failure')
|
|
30
45
|
.action(async (args, options) => {
|
|
31
|
-
|
|
46
|
+
const maxCycles = parseInt(options.maxCycles || options.iterations);
|
|
47
|
+
await (0, run_js_1.runLoop)(process.cwd(), args, {
|
|
48
|
+
iterations: maxCycles,
|
|
49
|
+
failFast: !!options.failFast
|
|
50
|
+
});
|
|
32
51
|
});
|
|
33
52
|
program.parse();
|
package/dist/commands/check.d.ts
CHANGED
package/dist/commands/check.js
CHANGED
|
@@ -9,43 +9,90 @@ const path_1 = __importDefault(require("path"));
|
|
|
9
9
|
const chalk_1 = __importDefault(require("chalk"));
|
|
10
10
|
const yaml_1 = __importDefault(require("yaml"));
|
|
11
11
|
const core_1 = require("@rigour-labs/core");
|
|
12
|
-
|
|
12
|
+
// Exit codes per spec
|
|
13
|
+
const EXIT_PASS = 0;
|
|
14
|
+
const EXIT_FAIL = 1;
|
|
15
|
+
const EXIT_CONFIG_ERROR = 2;
|
|
16
|
+
const EXIT_INTERNAL_ERROR = 3;
|
|
17
|
+
async function checkCommand(cwd, options = {}) {
|
|
13
18
|
const configPath = path_1.default.join(cwd, 'rigour.yml');
|
|
14
19
|
if (!(await fs_extra_1.default.pathExists(configPath))) {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const runner = new core_1.GateRunner(config);
|
|
23
|
-
const report = await runner.run(cwd);
|
|
24
|
-
// Write machine report
|
|
25
|
-
const reportPath = path_1.default.join(cwd, config.output.report_path);
|
|
26
|
-
await fs_extra_1.default.writeJson(reportPath, report, { spaces: 2 });
|
|
27
|
-
// Print human summary
|
|
28
|
-
if (report.status === 'PASS') {
|
|
29
|
-
console.log(chalk_1.default.green.bold('✔ PASS - All quality gates satisfied.'));
|
|
20
|
+
if (options.json) {
|
|
21
|
+
console.log(JSON.stringify({ error: 'CONFIG_ERROR', message: 'rigour.yml not found' }));
|
|
22
|
+
}
|
|
23
|
+
else if (!options.ci) {
|
|
24
|
+
console.error(chalk_1.default.red('Error: rigour.yml not found. Run `rigour init` first.'));
|
|
25
|
+
}
|
|
26
|
+
process.exit(EXIT_CONFIG_ERROR);
|
|
30
27
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
28
|
+
try {
|
|
29
|
+
const configContent = await fs_extra_1.default.readFile(configPath, 'utf-8');
|
|
30
|
+
const rawConfig = yaml_1.default.parse(configContent);
|
|
31
|
+
const config = core_1.ConfigSchema.parse(rawConfig);
|
|
32
|
+
if (!options.ci && !options.json) {
|
|
33
|
+
console.log(chalk_1.default.blue('Running Rigour checks...\n'));
|
|
34
|
+
}
|
|
35
|
+
const runner = new core_1.GateRunner(config);
|
|
36
|
+
const report = await runner.run(cwd);
|
|
37
|
+
// Write machine report
|
|
38
|
+
const reportPath = path_1.default.join(cwd, config.output.report_path);
|
|
39
|
+
await fs_extra_1.default.writeJson(reportPath, report, { spaces: 2 });
|
|
40
|
+
// Generate Fix Packet v2 on failure
|
|
41
|
+
if (report.status === 'FAIL') {
|
|
42
|
+
const { FixPacketService } = await import('@rigour-labs/core');
|
|
43
|
+
const fixPacketService = new FixPacketService();
|
|
44
|
+
const fixPacket = fixPacketService.generate(report, config);
|
|
45
|
+
const fixPacketPath = path_1.default.join(cwd, 'rigour-fix-packet.json');
|
|
46
|
+
await fs_extra_1.default.writeJson(fixPacketPath, fixPacket, { spaces: 2 });
|
|
47
|
+
}
|
|
48
|
+
// JSON output mode
|
|
49
|
+
if (options.json) {
|
|
50
|
+
console.log(JSON.stringify(report, null, 2));
|
|
51
|
+
process.exit(report.status === 'PASS' ? EXIT_PASS : EXIT_FAIL);
|
|
52
|
+
}
|
|
53
|
+
// CI mode: minimal output
|
|
54
|
+
if (options.ci) {
|
|
55
|
+
if (report.status === 'PASS') {
|
|
56
|
+
console.log('PASS');
|
|
39
57
|
}
|
|
40
|
-
|
|
41
|
-
console.log(
|
|
58
|
+
else {
|
|
59
|
+
console.log(`FAIL: ${report.failures.length} violation(s)`);
|
|
60
|
+
report.failures.forEach((f) => {
|
|
61
|
+
console.log(` - [${f.id}] ${f.title}`);
|
|
62
|
+
});
|
|
42
63
|
}
|
|
43
|
-
|
|
64
|
+
process.exit(report.status === 'PASS' ? EXIT_PASS : EXIT_FAIL);
|
|
44
65
|
}
|
|
45
|
-
|
|
66
|
+
// Normal human-readable output
|
|
67
|
+
if (report.status === 'PASS') {
|
|
68
|
+
console.log(chalk_1.default.green.bold('✔ PASS - All quality gates satisfied.'));
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
console.log(chalk_1.default.red.bold('✘ FAIL - Quality gate violations found.\n'));
|
|
72
|
+
for (const failure of report.failures) {
|
|
73
|
+
console.log(chalk_1.default.red(`[${failure.id}] ${failure.title}`));
|
|
74
|
+
console.log(chalk_1.default.dim(` Details: ${failure.details}`));
|
|
75
|
+
if (failure.files && failure.files.length > 0) {
|
|
76
|
+
console.log(chalk_1.default.dim(' Files:'));
|
|
77
|
+
failure.files.forEach((f) => console.log(chalk_1.default.dim(` - ${f}`)));
|
|
78
|
+
}
|
|
79
|
+
if (failure.hint) {
|
|
80
|
+
console.log(chalk_1.default.cyan(` Hint: ${failure.hint}`));
|
|
81
|
+
}
|
|
82
|
+
console.log('');
|
|
83
|
+
}
|
|
84
|
+
console.log(chalk_1.default.yellow(`See ${config.output.report_path} for full details.`));
|
|
85
|
+
}
|
|
86
|
+
console.log(chalk_1.default.dim(`\nFinished in ${report.stats.duration_ms}ms`));
|
|
87
|
+
process.exit(report.status === 'PASS' ? EXIT_PASS : EXIT_FAIL);
|
|
46
88
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
89
|
+
catch (error) {
|
|
90
|
+
if (options.json) {
|
|
91
|
+
console.log(JSON.stringify({ error: 'INTERNAL_ERROR', message: error.message }));
|
|
92
|
+
}
|
|
93
|
+
else if (!options.ci) {
|
|
94
|
+
console.error(chalk_1.default.red(`Internal error: ${error.message}`));
|
|
95
|
+
}
|
|
96
|
+
process.exit(EXIT_INTERNAL_ERROR);
|
|
50
97
|
}
|
|
51
98
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function explainCommand(cwd: string): Promise<void>;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.explainCommand = explainCommand;
|
|
7
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
10
|
+
async function explainCommand(cwd) {
|
|
11
|
+
const configPath = path_1.default.join(cwd, 'rigour.yml');
|
|
12
|
+
let reportPath = path_1.default.join(cwd, 'rigour-report.json');
|
|
13
|
+
// Try to read custom path from config
|
|
14
|
+
if (await fs_extra_1.default.pathExists(configPath)) {
|
|
15
|
+
try {
|
|
16
|
+
const yaml = await import('yaml');
|
|
17
|
+
const configContent = await fs_extra_1.default.readFile(configPath, 'utf-8');
|
|
18
|
+
const config = yaml.parse(configContent);
|
|
19
|
+
if (config?.output?.report_path) {
|
|
20
|
+
reportPath = path_1.default.join(cwd, config.output.report_path);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
catch (e) { }
|
|
24
|
+
}
|
|
25
|
+
if (!(await fs_extra_1.default.pathExists(reportPath))) {
|
|
26
|
+
console.error(chalk_1.default.red(`Error: No report found at ${reportPath}`));
|
|
27
|
+
console.error(chalk_1.default.dim('Run `rigour check` first to generate a report.'));
|
|
28
|
+
process.exit(2);
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
const reportContent = await fs_extra_1.default.readFile(reportPath, 'utf-8');
|
|
32
|
+
const report = JSON.parse(reportContent);
|
|
33
|
+
console.log(chalk_1.default.bold('\n📋 Rigour Report Explanation\n'));
|
|
34
|
+
console.log(chalk_1.default.bold('Status: ') + (report.status === 'PASS'
|
|
35
|
+
? chalk_1.default.green.bold('✅ PASS')
|
|
36
|
+
: chalk_1.default.red.bold('🛑 FAIL')));
|
|
37
|
+
console.log(chalk_1.default.bold('\nGate Summary:'));
|
|
38
|
+
for (const [gate, status] of Object.entries(report.summary || {})) {
|
|
39
|
+
const icon = status === 'PASS' ? '✅' : status === 'FAIL' ? '❌' : '⏭️';
|
|
40
|
+
console.log(` ${icon} ${gate}: ${status}`);
|
|
41
|
+
}
|
|
42
|
+
if (report.failures && report.failures.length > 0) {
|
|
43
|
+
console.log(chalk_1.default.bold.red(`\n🔧 ${report.failures.length} Violation(s) to Fix:\n`));
|
|
44
|
+
report.failures.forEach((failure, index) => {
|
|
45
|
+
console.log(chalk_1.default.white(`${index + 1}. `) + chalk_1.default.bold.yellow(`[${failure.id.toUpperCase()}]`) + chalk_1.default.white(` ${failure.title}`));
|
|
46
|
+
console.log(chalk_1.default.dim(` └─ ${failure.details}`));
|
|
47
|
+
if (failure.files && failure.files.length > 0) {
|
|
48
|
+
console.log(chalk_1.default.cyan(` 📁 Files: ${failure.files.join(', ')}`));
|
|
49
|
+
}
|
|
50
|
+
if (failure.hint) {
|
|
51
|
+
console.log(chalk_1.default.green(` 💡 Hint: ${failure.hint}`));
|
|
52
|
+
}
|
|
53
|
+
console.log('');
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
else if (report.status === 'PASS') {
|
|
57
|
+
console.log(chalk_1.default.green('\n✨ All quality gates passed! No violations found.\n'));
|
|
58
|
+
}
|
|
59
|
+
if (report.stats) {
|
|
60
|
+
console.log(chalk_1.default.dim(`Duration: ${report.stats.duration_ms}ms`));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
console.error(chalk_1.default.red(`Error reading report: ${error.message}`));
|
|
65
|
+
process.exit(3);
|
|
66
|
+
}
|
|
67
|
+
}
|
package/dist/commands/init.d.ts
CHANGED
|
@@ -1 +1,7 @@
|
|
|
1
|
-
export
|
|
1
|
+
export interface InitOptions {
|
|
2
|
+
preset?: string;
|
|
3
|
+
paradigm?: string;
|
|
4
|
+
dryRun?: boolean;
|
|
5
|
+
explain?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare function initCommand(cwd: string, options?: InitOptions): Promise<void>;
|
package/dist/commands/init.js
CHANGED
|
@@ -9,14 +9,70 @@ const path_1 = __importDefault(require("path"));
|
|
|
9
9
|
const chalk_1 = __importDefault(require("chalk"));
|
|
10
10
|
const yaml_1 = __importDefault(require("yaml"));
|
|
11
11
|
const core_1 = require("@rigour-labs/core");
|
|
12
|
-
async function initCommand(cwd) {
|
|
12
|
+
async function initCommand(cwd, options = {}) {
|
|
13
13
|
const discovery = new core_1.DiscoveryService();
|
|
14
|
-
const
|
|
14
|
+
const result = await discovery.discover(cwd);
|
|
15
|
+
let recommendedConfig = result.config;
|
|
16
|
+
// Override with user options if provided and re-apply template logic if necessary
|
|
17
|
+
if (options.preset || options.paradigm) {
|
|
18
|
+
const core = await import('@rigour-labs/core');
|
|
19
|
+
let customBase = { ...core.UNIVERSAL_CONFIG };
|
|
20
|
+
if (options.preset) {
|
|
21
|
+
const t = core.TEMPLATES.find((t) => t.name === options.preset);
|
|
22
|
+
if (t)
|
|
23
|
+
customBase = discovery.mergeConfig(customBase, t.config);
|
|
24
|
+
}
|
|
25
|
+
else if (recommendedConfig.preset) {
|
|
26
|
+
const t = core.TEMPLATES.find((t) => t.name === recommendedConfig.preset);
|
|
27
|
+
if (t)
|
|
28
|
+
customBase = discovery.mergeConfig(customBase, t.config);
|
|
29
|
+
}
|
|
30
|
+
if (options.paradigm) {
|
|
31
|
+
const t = core.PARADIGM_TEMPLATES.find((t) => t.name === options.paradigm);
|
|
32
|
+
if (t)
|
|
33
|
+
customBase = discovery.mergeConfig(customBase, t.config);
|
|
34
|
+
}
|
|
35
|
+
else if (recommendedConfig.paradigm) {
|
|
36
|
+
const t = core.PARADIGM_TEMPLATES.find((t) => t.name === recommendedConfig.paradigm);
|
|
37
|
+
if (t)
|
|
38
|
+
customBase = discovery.mergeConfig(customBase, t.config);
|
|
39
|
+
}
|
|
40
|
+
recommendedConfig = customBase;
|
|
41
|
+
if (options.preset)
|
|
42
|
+
recommendedConfig.preset = options.preset;
|
|
43
|
+
if (options.paradigm)
|
|
44
|
+
recommendedConfig.paradigm = options.paradigm;
|
|
45
|
+
}
|
|
46
|
+
if (options.dryRun || options.explain) {
|
|
47
|
+
console.log(chalk_1.default.bold.blue('\n🔍 Rigour Auto-Discovery (Dry Run):'));
|
|
48
|
+
if (recommendedConfig.preset) {
|
|
49
|
+
console.log(chalk_1.default.cyan(` Role: `) + chalk_1.default.bold(recommendedConfig.preset.toUpperCase()));
|
|
50
|
+
if (options.explain && result.matches.preset) {
|
|
51
|
+
console.log(chalk_1.default.dim(` (Marker found: ${result.matches.preset.marker})`));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (recommendedConfig.paradigm) {
|
|
55
|
+
console.log(chalk_1.default.cyan(` Paradigm: `) + chalk_1.default.bold(recommendedConfig.paradigm.toUpperCase()));
|
|
56
|
+
if (options.explain && result.matches.paradigm) {
|
|
57
|
+
console.log(chalk_1.default.dim(` (Marker found: ${result.matches.paradigm.marker})`));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
console.log(chalk_1.default.yellow('\n[DRY RUN] No files will be written.'));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
15
63
|
const configPath = path_1.default.join(cwd, 'rigour.yml');
|
|
16
64
|
if (await fs_extra_1.default.pathExists(configPath)) {
|
|
17
65
|
console.log(chalk_1.default.yellow('rigour.yml already exists. Skipping initialization.'));
|
|
18
66
|
return;
|
|
19
67
|
}
|
|
68
|
+
console.log(chalk_1.default.bold.blue('\n🔍 Rigour Auto-Discovery:'));
|
|
69
|
+
if (recommendedConfig.preset) {
|
|
70
|
+
console.log(chalk_1.default.cyan(` Role: `) + chalk_1.default.bold(recommendedConfig.preset.toUpperCase()));
|
|
71
|
+
}
|
|
72
|
+
if (recommendedConfig.paradigm) {
|
|
73
|
+
console.log(chalk_1.default.cyan(` Paradigm: `) + chalk_1.default.bold(recommendedConfig.paradigm.toUpperCase()));
|
|
74
|
+
}
|
|
75
|
+
console.log('');
|
|
20
76
|
await fs_extra_1.default.writeFile(configPath, yaml_1.default.stringify(recommendedConfig));
|
|
21
77
|
console.log(chalk_1.default.green('✔ Created rigour.yml'));
|
|
22
78
|
// Create required directories and files
|
|
@@ -28,33 +84,51 @@ async function initCommand(cwd) {
|
|
|
28
84
|
console.log(chalk_1.default.dim(` - Created ${file}`));
|
|
29
85
|
}
|
|
30
86
|
}
|
|
31
|
-
// Agent Handshake (
|
|
87
|
+
// Agent Handshake (Universal / AntiGravity / Cursor)
|
|
88
|
+
const rigourDocsDir = path_1.default.join(cwd, 'docs');
|
|
89
|
+
await fs_extra_1.default.ensureDir(rigourDocsDir);
|
|
90
|
+
const instructionsPath = path_1.default.join(rigourDocsDir, 'AGENT_INSTRUCTIONS.md');
|
|
91
|
+
const ruleContent = `# 🛡️ Rigour: Engineering Excellence Protocol
|
|
92
|
+
|
|
93
|
+
You are an Elite Software Engineer. You do not just write code that "works"; you write code that is **modular, maintainable, and rigorously verified.**
|
|
94
|
+
|
|
95
|
+
## 🚦 The Rigour Loop (Mandatory)
|
|
96
|
+
Before claiming "Done" for any task, you MUST follow this loop:
|
|
97
|
+
|
|
98
|
+
1. **Check**: Run \`npx @rigour-labs/cli check\` to verify compliance.
|
|
99
|
+
2. **Analyze**: If it fails, read \`rigour-fix-packet.json\` (V2 High-Fidelity) for exact failure points and constraints.
|
|
100
|
+
3. **Refactor**: Apply **SOLID** and **DRY** principles to resolve the violations according to constraints.
|
|
101
|
+
4. **Repeat**: Continue until \`npx @rigour-labs/cli check\` returns **PASS**.
|
|
102
|
+
|
|
103
|
+
## 🧩 Engineering Standards
|
|
104
|
+
- **Single Responsibility**: Keep files small and focused (max 500 lines).
|
|
105
|
+
- **DRY (Don't Repeat Yourself)**: Extract common logic into utilities.
|
|
106
|
+
- **Done is Done**: No \`TODO\` or \`FIXME\` comments allowed in the final state.
|
|
107
|
+
- **Memory Preservation**: Always update docs/SPEC.md, docs/ARCH.md, docs/DECISIONS.md.
|
|
108
|
+
|
|
109
|
+
## 🛠️ Commands
|
|
110
|
+
\`\`\`bash
|
|
111
|
+
# Verify current state
|
|
112
|
+
npx @rigour-labs/cli check
|
|
113
|
+
|
|
114
|
+
# Self-healing agent loop
|
|
115
|
+
npx @rigour-labs/cli run -- <agent-command>
|
|
116
|
+
\`\`\`
|
|
117
|
+
`;
|
|
118
|
+
// 1. Create Universal Instructions
|
|
119
|
+
await fs_extra_1.default.writeFile(instructionsPath, ruleContent);
|
|
120
|
+
console.log(chalk_1.default.green('✔ Initialized Universal Agent Handshake (docs/AGENT_INSTRUCTIONS.md)'));
|
|
121
|
+
// 2. Create Cursor Specific Rules (.mdc)
|
|
32
122
|
const cursorRulesDir = path_1.default.join(cwd, '.cursor', 'rules');
|
|
33
123
|
await fs_extra_1.default.ensureDir(cursorRulesDir);
|
|
34
|
-
const
|
|
35
|
-
const
|
|
124
|
+
const mdcPath = path_1.default.join(cursorRulesDir, 'rigour.mdc');
|
|
125
|
+
const mdcContent = `---
|
|
36
126
|
description: Enforcement of Rigour quality gates and best practices.
|
|
37
127
|
globs: **/*
|
|
38
128
|
---
|
|
39
129
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
## Core Rules
|
|
45
|
-
- **Never claim done** until you run \`rigour check\` and it returns PASS.
|
|
46
|
-
- If checks FAIL, fix **only** the listed failures. Do not add new features or refactor unrelated code.
|
|
47
|
-
- Maintain project memory in \`docs/SPEC.md\`, \`docs/ARCH.md\`, and \`docs/DECISIONS.md\`.
|
|
48
|
-
- Keep files modular. If a file exceeds 500 lines, you MUST break it into smaller components.
|
|
49
|
-
- No \`TODO\` or \`FIXME\` comments allowed in the final submission.
|
|
50
|
-
|
|
51
|
-
## Workflow
|
|
52
|
-
1. Write/Modify code.
|
|
53
|
-
2. Run \`rigour check\`.
|
|
54
|
-
3. If FAIL: Read \`rigour-report.json\` for exact failure points and fix them.
|
|
55
|
-
4. If PASS: You may claim task completion.
|
|
56
|
-
`;
|
|
57
|
-
await fs_extra_1.default.writeFile(rulePath, ruleContent);
|
|
58
|
-
console.log(chalk_1.default.green('✔ Initialized Agent Handshake (.cursor/rules/rigour.mdc)'));
|
|
59
|
-
console.log(chalk_1.default.blue('\nRigour is ready. Run `rigour check` to verify your project.'));
|
|
130
|
+
${ruleContent}`;
|
|
131
|
+
await fs_extra_1.default.writeFile(mdcPath, mdcContent);
|
|
132
|
+
console.log(chalk_1.default.green('✔ Initialized Cursor Handshake (.cursor/rules/rigour.mdc)'));
|
|
133
|
+
console.log(chalk_1.default.blue('\nRigour is ready. Run `npx @rigour-labs/cli check` to verify your project.'));
|
|
60
134
|
}
|
package/dist/commands/run.d.ts
CHANGED
package/dist/commands/run.js
CHANGED
|
@@ -10,11 +10,16 @@ const chalk_1 = __importDefault(require("chalk"));
|
|
|
10
10
|
const yaml_1 = __importDefault(require("yaml"));
|
|
11
11
|
const execa_1 = require("execa");
|
|
12
12
|
const core_1 = require("@rigour-labs/core");
|
|
13
|
+
// Exit codes per spec
|
|
14
|
+
const EXIT_PASS = 0;
|
|
15
|
+
const EXIT_FAIL = 1;
|
|
16
|
+
const EXIT_CONFIG_ERROR = 2;
|
|
17
|
+
const EXIT_INTERNAL_ERROR = 3;
|
|
13
18
|
async function runLoop(cwd, agentArgs, options) {
|
|
14
19
|
const configPath = path_1.default.join(cwd, 'rigour.yml');
|
|
15
20
|
if (!(await fs_extra_1.default.pathExists(configPath))) {
|
|
16
21
|
console.error(chalk_1.default.red('Error: rigour.yml not found. Run `rigour init` first.'));
|
|
17
|
-
process.exit(
|
|
22
|
+
process.exit(EXIT_CONFIG_ERROR);
|
|
18
23
|
}
|
|
19
24
|
try {
|
|
20
25
|
const configContent = await fs_extra_1.default.readFile(configPath, 'utf-8');
|
|
@@ -28,42 +33,81 @@ async function runLoop(cwd, agentArgs, options) {
|
|
|
28
33
|
console.log(chalk_1.default.bold.blue(`\n══════════════════════════════════════════════════════════════════`));
|
|
29
34
|
console.log(chalk_1.default.bold.blue(` RIGOUR LOOP: Iteration ${iteration}/${maxIterations}`));
|
|
30
35
|
console.log(chalk_1.default.bold.blue(`══════════════════════════════════════════════════════════════════`));
|
|
31
|
-
// 1.
|
|
32
|
-
|
|
36
|
+
// 1. Prepare Command
|
|
37
|
+
let currentArgs = [...agentArgs];
|
|
38
|
+
if (iteration > 1 && agentArgs.length > 0) {
|
|
39
|
+
// Iteration contract: In later cycles, we focus strictly on the fix packet
|
|
40
|
+
console.log(chalk_1.default.yellow(`\n🔄 REFINEMENT CYCLE - Instructing agent to fix specific violations...`));
|
|
41
|
+
// We keep the first part of the command (the agent) but can append or wrap
|
|
42
|
+
// For simplicity, we assume the agent can read the JSON file we generate
|
|
43
|
+
}
|
|
44
|
+
// Snapshot changed files before agent runs
|
|
45
|
+
let beforeFiles = [];
|
|
46
|
+
try {
|
|
47
|
+
const { stdout } = await (0, execa_1.execa)('git', ['status', '--porcelain'], { cwd });
|
|
48
|
+
beforeFiles = stdout.split('\n').filter(l => l.trim()).map(l => l.slice(3).trim());
|
|
49
|
+
}
|
|
50
|
+
catch (e) { }
|
|
51
|
+
// 2. Run the agent command
|
|
52
|
+
if (currentArgs.length > 0) {
|
|
33
53
|
console.log(chalk_1.default.cyan(`\n🚀 DEPLOYING AGENT:`));
|
|
34
|
-
console.log(chalk_1.default.dim(` Command: ${
|
|
54
|
+
console.log(chalk_1.default.dim(` Command: ${currentArgs.join(' ')}`));
|
|
35
55
|
try {
|
|
36
|
-
await (0, execa_1.execa)(
|
|
56
|
+
await (0, execa_1.execa)(currentArgs[0], currentArgs.slice(1), { shell: true, stdio: 'inherit', cwd });
|
|
37
57
|
}
|
|
38
58
|
catch (error) {
|
|
39
59
|
console.warn(chalk_1.default.yellow(`\n⚠️ Agent command finished with non-zero exit code. Rigour will now verify state...`));
|
|
40
60
|
}
|
|
41
61
|
}
|
|
42
|
-
//
|
|
62
|
+
// Snapshot changed files after agent runs
|
|
63
|
+
let afterFiles = [];
|
|
64
|
+
try {
|
|
65
|
+
const { stdout } = await (0, execa_1.execa)('git', ['status', '--porcelain'], { cwd });
|
|
66
|
+
afterFiles = stdout.split('\n').filter(l => l.trim()).map(l => l.slice(3).trim());
|
|
67
|
+
}
|
|
68
|
+
catch (e) { }
|
|
69
|
+
const changedThisCycle = afterFiles.filter(f => !beforeFiles.includes(f));
|
|
70
|
+
const maxFiles = config.gates.safety?.max_files_changed_per_cycle || 10;
|
|
71
|
+
if (changedThisCycle.length > maxFiles) {
|
|
72
|
+
console.log(chalk_1.default.red.bold(`\n🛑 SAFETY RAIL ABORT: Agent changed ${changedThisCycle.length} files (max: ${maxFiles}).`));
|
|
73
|
+
console.log(chalk_1.default.red(` This looks like explosive behavior. Check your agent's instructions.`));
|
|
74
|
+
process.exit(EXIT_FAIL);
|
|
75
|
+
}
|
|
76
|
+
// 3. Run Rigour Check
|
|
43
77
|
console.log(chalk_1.default.magenta('\n🔍 AUDITING QUALITY GATES...'));
|
|
44
78
|
const report = await runner.run(cwd);
|
|
79
|
+
// Write report
|
|
80
|
+
const reportPath = path_1.default.join(cwd, config.output.report_path);
|
|
81
|
+
await fs_extra_1.default.writeJson(reportPath, report, { spaces: 2 });
|
|
45
82
|
if (report.status === 'PASS') {
|
|
46
83
|
console.log(chalk_1.default.green.bold('\n✨ PASS - All quality gates satisfied.'));
|
|
47
84
|
console.log(chalk_1.default.green(` Your solution meets the required Engineering Rigour criteria.\n`));
|
|
48
85
|
return;
|
|
49
86
|
}
|
|
50
|
-
//
|
|
87
|
+
// 4. Generate Fix Packet v2
|
|
88
|
+
const { FixPacketService } = await import('@rigour-labs/core');
|
|
89
|
+
const fixPacketService = new FixPacketService();
|
|
90
|
+
const fixPacket = fixPacketService.generate(report, config);
|
|
91
|
+
const fixPacketPath = path_1.default.join(cwd, 'rigour-fix-packet.json');
|
|
92
|
+
await fs_extra_1.default.writeJson(fixPacketPath, fixPacket, { spaces: 2 });
|
|
51
93
|
console.log(chalk_1.default.red.bold(`\n🛑 FAIL - Found ${report.failures.length} engineering violations.`));
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
console.log(chalk_1.default.
|
|
94
|
+
console.log(chalk_1.default.dim(` Fix Packet generated: rigour-fix-packet.json`));
|
|
95
|
+
if (options.failFast) {
|
|
96
|
+
console.log(chalk_1.default.red.bold(`\n🛑 FAIL-FAST: Aborting loop as requested.`));
|
|
97
|
+
process.exit(EXIT_FAIL);
|
|
98
|
+
}
|
|
99
|
+
// Print summary
|
|
100
|
+
const summary = report.failures.map((f, i) => {
|
|
101
|
+
return chalk_1.default.white(`${i + 1}. `) + chalk_1.default.bold.red(`[${f.id.toUpperCase()}] `) + chalk_1.default.white(f.title);
|
|
102
|
+
}).join('\n');
|
|
103
|
+
console.log(chalk_1.default.bold.white('\n📋 VIOLATIONS SUMMARY:'));
|
|
104
|
+
console.log(summary);
|
|
62
105
|
if (iteration === maxIterations) {
|
|
63
106
|
console.log(chalk_1.default.red.bold(`\n❌ CRITICAL: Reached maximum iterations (${maxIterations}).`));
|
|
64
107
|
console.log(chalk_1.default.red(` Quality gates remain unfulfilled. Refactor manually or check agent logs.`));
|
|
65
|
-
process.exit(
|
|
108
|
+
process.exit(EXIT_FAIL);
|
|
66
109
|
}
|
|
110
|
+
console.log(chalk_1.default.dim('\nReturning control to agent for the next refinement cycle...'));
|
|
67
111
|
}
|
|
68
112
|
}
|
|
69
113
|
catch (error) {
|
|
@@ -71,6 +115,6 @@ async function runLoop(cwd, agentArgs, options) {
|
|
|
71
115
|
if (error.issues) {
|
|
72
116
|
console.error(chalk_1.default.dim(JSON.stringify(error.issues, null, 2)));
|
|
73
117
|
}
|
|
74
|
-
process.exit(
|
|
118
|
+
process.exit(EXIT_INTERNAL_ERROR);
|
|
75
119
|
}
|
|
76
120
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rigour-labs/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"bin": {
|
|
5
5
|
"rigour": "dist/cli.js"
|
|
6
6
|
},
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"fs-extra": "^11.2.0",
|
|
21
21
|
"globby": "^14.0.1",
|
|
22
22
|
"yaml": "^2.8.2",
|
|
23
|
-
"@rigour-labs/core": "1.
|
|
23
|
+
"@rigour-labs/core": "1.2.1"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"@types/fs-extra": "^11.0.4",
|
package/src/cli.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { Command } from 'commander';
|
|
3
3
|
import { initCommand } from './commands/init.js';
|
|
4
4
|
import { checkCommand } from './commands/check.js';
|
|
5
|
+
import { explainCommand } from './commands/explain.js';
|
|
5
6
|
import { runLoop } from './commands/run.js';
|
|
6
7
|
|
|
7
8
|
const program = new Command();
|
|
@@ -13,25 +14,44 @@ program
|
|
|
13
14
|
|
|
14
15
|
program
|
|
15
16
|
.command('init')
|
|
16
|
-
.description('Initialize
|
|
17
|
-
.
|
|
18
|
-
|
|
17
|
+
.description('Initialize Rigour in the current directory')
|
|
18
|
+
.option('-p, --preset <name>', 'Project preset (ui, api, infra, data)')
|
|
19
|
+
.option('--paradigm <name>', 'Coding paradigm (oop, functional, minimal)')
|
|
20
|
+
.option('--dry-run', 'Show detected configuration without writing files')
|
|
21
|
+
.option('--explain', 'Show detection markers for roles and paradigms')
|
|
22
|
+
.action(async (options: any) => {
|
|
23
|
+
await initCommand(process.cwd(), options);
|
|
19
24
|
});
|
|
20
25
|
|
|
21
26
|
program
|
|
22
27
|
.command('check')
|
|
23
28
|
.description('Run quality gate checks')
|
|
29
|
+
.option('--ci', 'CI mode (minimal output, non-zero exit on fail)')
|
|
30
|
+
.option('--json', 'Output report in JSON format')
|
|
31
|
+
.action(async (options: any) => {
|
|
32
|
+
await checkCommand(process.cwd(), options);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
program
|
|
36
|
+
.command('explain')
|
|
37
|
+
.description('Explain the last quality gate report with actionable bullets')
|
|
24
38
|
.action(async () => {
|
|
25
|
-
await
|
|
39
|
+
await explainCommand(process.cwd());
|
|
26
40
|
});
|
|
27
41
|
|
|
28
42
|
program
|
|
29
43
|
.command('run')
|
|
30
44
|
.description('Execute an agent command in a loop until quality gates pass')
|
|
31
45
|
.argument('[command...]', 'The agent command to run (e.g., cursor-agent ...)')
|
|
32
|
-
.option('-i, --iterations <number>', 'Maximum number of loop iterations', '3')
|
|
33
|
-
.
|
|
34
|
-
|
|
46
|
+
.option('-i, --iterations <number>', 'Maximum number of loop iterations (deprecated, use --max-cycles)', '3')
|
|
47
|
+
.option('-c, --max-cycles <number>', 'Maximum number of loop iterations', '3')
|
|
48
|
+
.option('--fail-fast', 'Abort loop immediately on first gate failure')
|
|
49
|
+
.action(async (args: string[], options: any) => {
|
|
50
|
+
const maxCycles = parseInt(options.maxCycles || options.iterations);
|
|
51
|
+
await runLoop(process.cwd(), args, {
|
|
52
|
+
iterations: maxCycles,
|
|
53
|
+
failFast: !!options.failFast
|
|
54
|
+
});
|
|
35
55
|
});
|
|
36
56
|
|
|
37
57
|
program.parse();
|
package/src/commands/check.ts
CHANGED
|
@@ -4,52 +4,105 @@ import chalk from 'chalk';
|
|
|
4
4
|
import yaml from 'yaml';
|
|
5
5
|
import { GateRunner, ConfigSchema, Failure } from '@rigour-labs/core';
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
// Exit codes per spec
|
|
8
|
+
const EXIT_PASS = 0;
|
|
9
|
+
const EXIT_FAIL = 1;
|
|
10
|
+
const EXIT_CONFIG_ERROR = 2;
|
|
11
|
+
const EXIT_INTERNAL_ERROR = 3;
|
|
12
|
+
|
|
13
|
+
export interface CheckOptions {
|
|
14
|
+
ci?: boolean;
|
|
15
|
+
json?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function checkCommand(cwd: string, options: CheckOptions = {}) {
|
|
8
19
|
const configPath = path.join(cwd, 'rigour.yml');
|
|
9
20
|
|
|
10
21
|
if (!(await fs.pathExists(configPath))) {
|
|
11
|
-
|
|
12
|
-
|
|
22
|
+
if (options.json) {
|
|
23
|
+
console.log(JSON.stringify({ error: 'CONFIG_ERROR', message: 'rigour.yml not found' }));
|
|
24
|
+
} else if (!options.ci) {
|
|
25
|
+
console.error(chalk.red('Error: rigour.yml not found. Run `rigour init` first.'));
|
|
26
|
+
}
|
|
27
|
+
process.exit(EXIT_CONFIG_ERROR);
|
|
13
28
|
}
|
|
14
29
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
30
|
+
try {
|
|
31
|
+
const configContent = await fs.readFile(configPath, 'utf-8');
|
|
32
|
+
const rawConfig = yaml.parse(configContent);
|
|
33
|
+
const config = ConfigSchema.parse(rawConfig);
|
|
18
34
|
|
|
19
|
-
|
|
35
|
+
if (!options.ci && !options.json) {
|
|
36
|
+
console.log(chalk.blue('Running Rigour checks...\n'));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const runner = new GateRunner(config);
|
|
40
|
+
const report = await runner.run(cwd);
|
|
20
41
|
|
|
21
|
-
|
|
22
|
-
|
|
42
|
+
// Write machine report
|
|
43
|
+
const reportPath = path.join(cwd, config.output.report_path);
|
|
44
|
+
await fs.writeJson(reportPath, report, { spaces: 2 });
|
|
23
45
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
46
|
+
// Generate Fix Packet v2 on failure
|
|
47
|
+
if (report.status === 'FAIL') {
|
|
48
|
+
const { FixPacketService } = await import('@rigour-labs/core');
|
|
49
|
+
const fixPacketService = new FixPacketService();
|
|
50
|
+
const fixPacket = fixPacketService.generate(report, config);
|
|
51
|
+
const fixPacketPath = path.join(cwd, 'rigour-fix-packet.json');
|
|
52
|
+
await fs.writeJson(fixPacketPath, fixPacket, { spaces: 2 });
|
|
53
|
+
}
|
|
27
54
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
55
|
+
// JSON output mode
|
|
56
|
+
if (options.json) {
|
|
57
|
+
console.log(JSON.stringify(report, null, 2));
|
|
58
|
+
process.exit(report.status === 'PASS' ? EXIT_PASS : EXIT_FAIL);
|
|
59
|
+
}
|
|
33
60
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
61
|
+
// CI mode: minimal output
|
|
62
|
+
if (options.ci) {
|
|
63
|
+
if (report.status === 'PASS') {
|
|
64
|
+
console.log('PASS');
|
|
65
|
+
} else {
|
|
66
|
+
console.log(`FAIL: ${report.failures.length} violation(s)`);
|
|
67
|
+
report.failures.forEach((f: Failure) => {
|
|
68
|
+
console.log(` - [${f.id}] ${f.title}`);
|
|
69
|
+
});
|
|
40
70
|
}
|
|
41
|
-
|
|
42
|
-
|
|
71
|
+
process.exit(report.status === 'PASS' ? EXIT_PASS : EXIT_FAIL);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Normal human-readable output
|
|
75
|
+
if (report.status === 'PASS') {
|
|
76
|
+
console.log(chalk.green.bold('✔ PASS - All quality gates satisfied.'));
|
|
77
|
+
} else {
|
|
78
|
+
console.log(chalk.red.bold('✘ FAIL - Quality gate violations found.\n'));
|
|
79
|
+
|
|
80
|
+
for (const failure of report.failures as Failure[]) {
|
|
81
|
+
console.log(chalk.red(`[${failure.id}] ${failure.title}`));
|
|
82
|
+
console.log(chalk.dim(` Details: ${failure.details}`));
|
|
83
|
+
if (failure.files && failure.files.length > 0) {
|
|
84
|
+
console.log(chalk.dim(' Files:'));
|
|
85
|
+
failure.files.forEach((f: string) => console.log(chalk.dim(` - ${f}`)));
|
|
86
|
+
}
|
|
87
|
+
if (failure.hint) {
|
|
88
|
+
console.log(chalk.cyan(` Hint: ${failure.hint}`));
|
|
89
|
+
}
|
|
90
|
+
console.log('');
|
|
43
91
|
}
|
|
44
|
-
|
|
92
|
+
|
|
93
|
+
console.log(chalk.yellow(`See ${config.output.report_path} for full details.`));
|
|
45
94
|
}
|
|
46
95
|
|
|
47
|
-
console.log(chalk.
|
|
48
|
-
}
|
|
96
|
+
console.log(chalk.dim(`\nFinished in ${report.stats.duration_ms}ms`));
|
|
49
97
|
|
|
50
|
-
|
|
98
|
+
process.exit(report.status === 'PASS' ? EXIT_PASS : EXIT_FAIL);
|
|
51
99
|
|
|
52
|
-
|
|
53
|
-
|
|
100
|
+
} catch (error: any) {
|
|
101
|
+
if (options.json) {
|
|
102
|
+
console.log(JSON.stringify({ error: 'INTERNAL_ERROR', message: error.message }));
|
|
103
|
+
} else if (!options.ci) {
|
|
104
|
+
console.error(chalk.red(`Internal error: ${error.message}`));
|
|
105
|
+
}
|
|
106
|
+
process.exit(EXIT_INTERNAL_ERROR);
|
|
54
107
|
}
|
|
55
108
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
|
|
5
|
+
export async function explainCommand(cwd: string) {
|
|
6
|
+
const configPath = path.join(cwd, 'rigour.yml');
|
|
7
|
+
let reportPath = path.join(cwd, 'rigour-report.json');
|
|
8
|
+
|
|
9
|
+
// Try to read custom path from config
|
|
10
|
+
if (await fs.pathExists(configPath)) {
|
|
11
|
+
try {
|
|
12
|
+
const yaml = await import('yaml');
|
|
13
|
+
const configContent = await fs.readFile(configPath, 'utf-8');
|
|
14
|
+
const config = yaml.parse(configContent);
|
|
15
|
+
if (config?.output?.report_path) {
|
|
16
|
+
reportPath = path.join(cwd, config.output.report_path);
|
|
17
|
+
}
|
|
18
|
+
} catch (e) { }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (!(await fs.pathExists(reportPath))) {
|
|
22
|
+
console.error(chalk.red(`Error: No report found at ${reportPath}`));
|
|
23
|
+
console.error(chalk.dim('Run `rigour check` first to generate a report.'));
|
|
24
|
+
process.exit(2);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const reportContent = await fs.readFile(reportPath, 'utf-8');
|
|
29
|
+
const report = JSON.parse(reportContent);
|
|
30
|
+
|
|
31
|
+
console.log(chalk.bold('\n📋 Rigour Report Explanation\n'));
|
|
32
|
+
console.log(chalk.bold('Status: ') + (report.status === 'PASS'
|
|
33
|
+
? chalk.green.bold('✅ PASS')
|
|
34
|
+
: chalk.red.bold('🛑 FAIL')));
|
|
35
|
+
|
|
36
|
+
console.log(chalk.bold('\nGate Summary:'));
|
|
37
|
+
for (const [gate, status] of Object.entries(report.summary || {})) {
|
|
38
|
+
const icon = status === 'PASS' ? '✅' : status === 'FAIL' ? '❌' : '⏭️';
|
|
39
|
+
console.log(` ${icon} ${gate}: ${status}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (report.failures && report.failures.length > 0) {
|
|
43
|
+
console.log(chalk.bold.red(`\n🔧 ${report.failures.length} Violation(s) to Fix:\n`));
|
|
44
|
+
|
|
45
|
+
report.failures.forEach((failure: any, index: number) => {
|
|
46
|
+
console.log(chalk.white(`${index + 1}. `) + chalk.bold.yellow(`[${failure.id.toUpperCase()}]`) + chalk.white(` ${failure.title}`));
|
|
47
|
+
console.log(chalk.dim(` └─ ${failure.details}`));
|
|
48
|
+
if (failure.files && failure.files.length > 0) {
|
|
49
|
+
console.log(chalk.cyan(` 📁 Files: ${failure.files.join(', ')}`));
|
|
50
|
+
}
|
|
51
|
+
if (failure.hint) {
|
|
52
|
+
console.log(chalk.green(` 💡 Hint: ${failure.hint}`));
|
|
53
|
+
}
|
|
54
|
+
console.log('');
|
|
55
|
+
});
|
|
56
|
+
} else if (report.status === 'PASS') {
|
|
57
|
+
console.log(chalk.green('\n✨ All quality gates passed! No violations found.\n'));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (report.stats) {
|
|
61
|
+
console.log(chalk.dim(`Duration: ${report.stats.duration_ms}ms`));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
} catch (error: any) {
|
|
65
|
+
console.error(chalk.red(`Error reading report: ${error.message}`));
|
|
66
|
+
process.exit(3);
|
|
67
|
+
}
|
|
68
|
+
}
|
package/src/commands/init.ts
CHANGED
|
@@ -4,9 +4,62 @@ import chalk from 'chalk';
|
|
|
4
4
|
import yaml from 'yaml';
|
|
5
5
|
import { DiscoveryService } from '@rigour-labs/core';
|
|
6
6
|
|
|
7
|
-
export
|
|
7
|
+
export interface InitOptions {
|
|
8
|
+
preset?: string;
|
|
9
|
+
paradigm?: string;
|
|
10
|
+
dryRun?: boolean;
|
|
11
|
+
explain?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function initCommand(cwd: string, options: InitOptions = {}) {
|
|
8
15
|
const discovery = new DiscoveryService();
|
|
9
|
-
const
|
|
16
|
+
const result = await discovery.discover(cwd);
|
|
17
|
+
let recommendedConfig = result.config;
|
|
18
|
+
|
|
19
|
+
// Override with user options if provided and re-apply template logic if necessary
|
|
20
|
+
if (options.preset || options.paradigm) {
|
|
21
|
+
const core = await import('@rigour-labs/core');
|
|
22
|
+
|
|
23
|
+
let customBase = { ...core.UNIVERSAL_CONFIG };
|
|
24
|
+
|
|
25
|
+
if (options.preset) {
|
|
26
|
+
const t = core.TEMPLATES.find((t: any) => t.name === options.preset);
|
|
27
|
+
if (t) customBase = (discovery as any).mergeConfig(customBase, t.config);
|
|
28
|
+
} else if (recommendedConfig.preset) {
|
|
29
|
+
const t = core.TEMPLATES.find((t: any) => t.name === recommendedConfig.preset);
|
|
30
|
+
if (t) customBase = (discovery as any).mergeConfig(customBase, t.config);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (options.paradigm) {
|
|
34
|
+
const t = core.PARADIGM_TEMPLATES.find((t: any) => t.name === options.paradigm);
|
|
35
|
+
if (t) customBase = (discovery as any).mergeConfig(customBase, t.config);
|
|
36
|
+
} else if (recommendedConfig.paradigm) {
|
|
37
|
+
const t = core.PARADIGM_TEMPLATES.find((t: any) => t.name === recommendedConfig.paradigm);
|
|
38
|
+
if (t) customBase = (discovery as any).mergeConfig(customBase, t.config);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
recommendedConfig = customBase;
|
|
42
|
+
if (options.preset) recommendedConfig.preset = options.preset;
|
|
43
|
+
if (options.paradigm) recommendedConfig.paradigm = options.paradigm;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (options.dryRun || options.explain) {
|
|
47
|
+
console.log(chalk.bold.blue('\n🔍 Rigour Auto-Discovery (Dry Run):'));
|
|
48
|
+
if (recommendedConfig.preset) {
|
|
49
|
+
console.log(chalk.cyan(` Role: `) + chalk.bold(recommendedConfig.preset.toUpperCase()));
|
|
50
|
+
if (options.explain && result.matches.preset) {
|
|
51
|
+
console.log(chalk.dim(` (Marker found: ${result.matches.preset.marker})`));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (recommendedConfig.paradigm) {
|
|
55
|
+
console.log(chalk.cyan(` Paradigm: `) + chalk.bold(recommendedConfig.paradigm.toUpperCase()));
|
|
56
|
+
if (options.explain && result.matches.paradigm) {
|
|
57
|
+
console.log(chalk.dim(` (Marker found: ${result.matches.paradigm.marker})`));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
console.log(chalk.yellow('\n[DRY RUN] No files will be written.'));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
10
63
|
|
|
11
64
|
const configPath = path.join(cwd, 'rigour.yml');
|
|
12
65
|
|
|
@@ -15,6 +68,15 @@ export async function initCommand(cwd: string) {
|
|
|
15
68
|
return;
|
|
16
69
|
}
|
|
17
70
|
|
|
71
|
+
console.log(chalk.bold.blue('\n🔍 Rigour Auto-Discovery:'));
|
|
72
|
+
if (recommendedConfig.preset) {
|
|
73
|
+
console.log(chalk.cyan(` Role: `) + chalk.bold(recommendedConfig.preset.toUpperCase()));
|
|
74
|
+
}
|
|
75
|
+
if (recommendedConfig.paradigm) {
|
|
76
|
+
console.log(chalk.cyan(` Paradigm: `) + chalk.bold(recommendedConfig.paradigm.toUpperCase()));
|
|
77
|
+
}
|
|
78
|
+
console.log('');
|
|
79
|
+
|
|
18
80
|
await fs.writeFile(configPath, yaml.stringify(recommendedConfig));
|
|
19
81
|
console.log(chalk.green('✔ Created rigour.yml'));
|
|
20
82
|
|
|
@@ -28,36 +90,56 @@ export async function initCommand(cwd: string) {
|
|
|
28
90
|
}
|
|
29
91
|
}
|
|
30
92
|
|
|
31
|
-
// Agent Handshake (
|
|
32
|
-
const
|
|
33
|
-
await fs.ensureDir(
|
|
34
|
-
const
|
|
93
|
+
// Agent Handshake (Universal / AntiGravity / Cursor)
|
|
94
|
+
const rigourDocsDir = path.join(cwd, 'docs');
|
|
95
|
+
await fs.ensureDir(rigourDocsDir);
|
|
96
|
+
const instructionsPath = path.join(rigourDocsDir, 'AGENT_INSTRUCTIONS.md');
|
|
35
97
|
|
|
36
|
-
const ruleContent =
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
98
|
+
const ruleContent = `# 🛡️ Rigour: Engineering Excellence Protocol
|
|
99
|
+
|
|
100
|
+
You are an Elite Software Engineer. You do not just write code that "works"; you write code that is **modular, maintainable, and rigorously verified.**
|
|
101
|
+
|
|
102
|
+
## 🚦 The Rigour Loop (Mandatory)
|
|
103
|
+
Before claiming "Done" for any task, you MUST follow this loop:
|
|
40
104
|
|
|
41
|
-
|
|
105
|
+
1. **Check**: Run \`npx @rigour-labs/cli check\` to verify compliance.
|
|
106
|
+
2. **Analyze**: If it fails, read \`rigour-fix-packet.json\` (V2 High-Fidelity) for exact failure points and constraints.
|
|
107
|
+
3. **Refactor**: Apply **SOLID** and **DRY** principles to resolve the violations according to constraints.
|
|
108
|
+
4. **Repeat**: Continue until \`npx @rigour-labs/cli check\` returns **PASS**.
|
|
42
109
|
|
|
43
|
-
|
|
110
|
+
## 🧩 Engineering Standards
|
|
111
|
+
- **Single Responsibility**: Keep files small and focused (max 500 lines).
|
|
112
|
+
- **DRY (Don't Repeat Yourself)**: Extract common logic into utilities.
|
|
113
|
+
- **Done is Done**: No \`TODO\` or \`FIXME\` comments allowed in the final state.
|
|
114
|
+
- **Memory Preservation**: Always update docs/SPEC.md, docs/ARCH.md, docs/DECISIONS.md.
|
|
44
115
|
|
|
45
|
-
##
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
-
|
|
49
|
-
- Keep files modular. If a file exceeds 500 lines, you MUST break it into smaller components.
|
|
50
|
-
- No \`TODO\` or \`FIXME\` comments allowed in the final submission.
|
|
116
|
+
## 🛠️ Commands
|
|
117
|
+
\`\`\`bash
|
|
118
|
+
# Verify current state
|
|
119
|
+
npx @rigour-labs/cli check
|
|
51
120
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
3. If FAIL: Read \`rigour-report.json\` for exact failure points and fix them.
|
|
56
|
-
4. If PASS: You may claim task completion.
|
|
121
|
+
# Self-healing agent loop
|
|
122
|
+
npx @rigour-labs/cli run -- <agent-command>
|
|
123
|
+
\`\`\`
|
|
57
124
|
`;
|
|
58
125
|
|
|
59
|
-
|
|
60
|
-
|
|
126
|
+
// 1. Create Universal Instructions
|
|
127
|
+
await fs.writeFile(instructionsPath, ruleContent);
|
|
128
|
+
console.log(chalk.green('✔ Initialized Universal Agent Handshake (docs/AGENT_INSTRUCTIONS.md)'));
|
|
129
|
+
|
|
130
|
+
// 2. Create Cursor Specific Rules (.mdc)
|
|
131
|
+
const cursorRulesDir = path.join(cwd, '.cursor', 'rules');
|
|
132
|
+
await fs.ensureDir(cursorRulesDir);
|
|
133
|
+
const mdcPath = path.join(cursorRulesDir, 'rigour.mdc');
|
|
134
|
+
const mdcContent = `---
|
|
135
|
+
description: Enforcement of Rigour quality gates and best practices.
|
|
136
|
+
globs: **/*
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
${ruleContent}`;
|
|
140
|
+
|
|
141
|
+
await fs.writeFile(mdcPath, mdcContent);
|
|
142
|
+
console.log(chalk.green('✔ Initialized Cursor Handshake (.cursor/rules/rigour.mdc)'));
|
|
61
143
|
|
|
62
|
-
console.log(chalk.blue('\nRigour is ready. Run `rigour check` to verify your project.'));
|
|
144
|
+
console.log(chalk.blue('\nRigour is ready. Run `npx @rigour-labs/cli check` to verify your project.'));
|
|
63
145
|
}
|
package/src/commands/run.ts
CHANGED
|
@@ -5,12 +5,18 @@ import yaml from 'yaml';
|
|
|
5
5
|
import { execa } from 'execa';
|
|
6
6
|
import { GateRunner, ConfigSchema } from '@rigour-labs/core';
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
// Exit codes per spec
|
|
9
|
+
const EXIT_PASS = 0;
|
|
10
|
+
const EXIT_FAIL = 1;
|
|
11
|
+
const EXIT_CONFIG_ERROR = 2;
|
|
12
|
+
const EXIT_INTERNAL_ERROR = 3;
|
|
13
|
+
|
|
14
|
+
export async function runLoop(cwd: string, agentArgs: string[], options: { iterations: number, failFast?: boolean }) {
|
|
9
15
|
const configPath = path.join(cwd, 'rigour.yml');
|
|
10
16
|
|
|
11
17
|
if (!(await fs.pathExists(configPath))) {
|
|
12
18
|
console.error(chalk.red('Error: rigour.yml not found. Run `rigour init` first.'));
|
|
13
|
-
process.exit(
|
|
19
|
+
process.exit(EXIT_CONFIG_ERROR);
|
|
14
20
|
}
|
|
15
21
|
|
|
16
22
|
try {
|
|
@@ -28,52 +34,98 @@ export async function runLoop(cwd: string, agentArgs: string[], options: { itera
|
|
|
28
34
|
console.log(chalk.bold.blue(` RIGOUR LOOP: Iteration ${iteration}/${maxIterations}`));
|
|
29
35
|
console.log(chalk.bold.blue(`══════════════════════════════════════════════════════════════════`));
|
|
30
36
|
|
|
31
|
-
// 1.
|
|
32
|
-
|
|
37
|
+
// 1. Prepare Command
|
|
38
|
+
let currentArgs = [...agentArgs];
|
|
39
|
+
if (iteration > 1 && agentArgs.length > 0) {
|
|
40
|
+
// Iteration contract: In later cycles, we focus strictly on the fix packet
|
|
41
|
+
console.log(chalk.yellow(`\n🔄 REFINEMENT CYCLE - Instructing agent to fix specific violations...`));
|
|
42
|
+
// We keep the first part of the command (the agent) but can append or wrap
|
|
43
|
+
// For simplicity, we assume the agent can read the JSON file we generate
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Snapshot changed files before agent runs
|
|
47
|
+
let beforeFiles: string[] = [];
|
|
48
|
+
try {
|
|
49
|
+
const { stdout } = await execa('git', ['status', '--porcelain'], { cwd });
|
|
50
|
+
beforeFiles = stdout.split('\n').filter(l => l.trim()).map(l => l.slice(3).trim());
|
|
51
|
+
} catch (e) { }
|
|
52
|
+
|
|
53
|
+
// 2. Run the agent command
|
|
54
|
+
if (currentArgs.length > 0) {
|
|
33
55
|
console.log(chalk.cyan(`\n🚀 DEPLOYING AGENT:`));
|
|
34
|
-
console.log(chalk.dim(` Command: ${
|
|
56
|
+
console.log(chalk.dim(` Command: ${currentArgs.join(' ')}`));
|
|
35
57
|
try {
|
|
36
|
-
await execa(
|
|
58
|
+
await execa(currentArgs[0], currentArgs.slice(1), { shell: true, stdio: 'inherit', cwd });
|
|
37
59
|
} catch (error: any) {
|
|
38
60
|
console.warn(chalk.yellow(`\n⚠️ Agent command finished with non-zero exit code. Rigour will now verify state...`));
|
|
39
61
|
}
|
|
40
62
|
}
|
|
41
63
|
|
|
42
|
-
//
|
|
64
|
+
// Snapshot changed files after agent runs
|
|
65
|
+
let afterFiles: string[] = [];
|
|
66
|
+
try {
|
|
67
|
+
const { stdout } = await execa('git', ['status', '--porcelain'], { cwd });
|
|
68
|
+
afterFiles = stdout.split('\n').filter(l => l.trim()).map(l => l.slice(3).trim());
|
|
69
|
+
} catch (e) { }
|
|
70
|
+
|
|
71
|
+
const changedThisCycle = afterFiles.filter(f => !beforeFiles.includes(f));
|
|
72
|
+
const maxFiles = config.gates.safety?.max_files_changed_per_cycle || 10;
|
|
73
|
+
|
|
74
|
+
if (changedThisCycle.length > maxFiles) {
|
|
75
|
+
console.log(chalk.red.bold(`\n🛑 SAFETY RAIL ABORT: Agent changed ${changedThisCycle.length} files (max: ${maxFiles}).`));
|
|
76
|
+
console.log(chalk.red(` This looks like explosive behavior. Check your agent's instructions.`));
|
|
77
|
+
process.exit(EXIT_FAIL);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 3. Run Rigour Check
|
|
43
81
|
console.log(chalk.magenta('\n🔍 AUDITING QUALITY GATES...'));
|
|
44
82
|
const report = await runner.run(cwd);
|
|
45
83
|
|
|
84
|
+
// Write report
|
|
85
|
+
const reportPath = path.join(cwd, config.output.report_path);
|
|
86
|
+
await fs.writeJson(reportPath, report, { spaces: 2 });
|
|
87
|
+
|
|
46
88
|
if (report.status === 'PASS') {
|
|
47
89
|
console.log(chalk.green.bold('\n✨ PASS - All quality gates satisfied.'));
|
|
48
90
|
console.log(chalk.green(` Your solution meets the required Engineering Rigour criteria.\n`));
|
|
49
91
|
return;
|
|
50
92
|
}
|
|
51
93
|
|
|
52
|
-
//
|
|
94
|
+
// 4. Generate Fix Packet v2
|
|
95
|
+
const { FixPacketService } = await import('@rigour-labs/core');
|
|
96
|
+
const fixPacketService = new FixPacketService();
|
|
97
|
+
const fixPacket = fixPacketService.generate(report, config);
|
|
98
|
+
const fixPacketPath = path.join(cwd, 'rigour-fix-packet.json');
|
|
99
|
+
await fs.writeJson(fixPacketPath, fixPacket, { spaces: 2 });
|
|
100
|
+
|
|
53
101
|
console.log(chalk.red.bold(`\n🛑 FAIL - Found ${report.failures.length} engineering violations.`));
|
|
102
|
+
console.log(chalk.dim(` Fix Packet generated: rigour-fix-packet.json`));
|
|
54
103
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
return msg;
|
|
60
|
-
}).join('\n\n');
|
|
104
|
+
if (options.failFast) {
|
|
105
|
+
console.log(chalk.red.bold(`\n🛑 FAIL-FAST: Aborting loop as requested.`));
|
|
106
|
+
process.exit(EXIT_FAIL);
|
|
107
|
+
}
|
|
61
108
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
109
|
+
// Print summary
|
|
110
|
+
const summary = report.failures.map((f, i) => {
|
|
111
|
+
return chalk.white(`${i + 1}. `) + chalk.bold.red(`[${f.id.toUpperCase()}] `) + chalk.white(f.title);
|
|
112
|
+
}).join('\n');
|
|
113
|
+
console.log(chalk.bold.white('\n📋 VIOLATIONS SUMMARY:'));
|
|
114
|
+
console.log(summary);
|
|
65
115
|
|
|
66
116
|
if (iteration === maxIterations) {
|
|
67
117
|
console.log(chalk.red.bold(`\n❌ CRITICAL: Reached maximum iterations (${maxIterations}).`));
|
|
68
118
|
console.log(chalk.red(` Quality gates remain unfulfilled. Refactor manually or check agent logs.`));
|
|
69
|
-
process.exit(
|
|
119
|
+
process.exit(EXIT_FAIL);
|
|
70
120
|
}
|
|
121
|
+
|
|
122
|
+
console.log(chalk.dim('\nReturning control to agent for the next refinement cycle...'));
|
|
71
123
|
}
|
|
72
124
|
} catch (error: any) {
|
|
73
125
|
console.error(chalk.red(`\n❌ FATAL ERROR: ${error.message}`));
|
|
74
126
|
if (error.issues) {
|
|
75
127
|
console.error(chalk.dim(JSON.stringify(error.issues, null, 2)));
|
|
76
128
|
}
|
|
77
|
-
process.exit(
|
|
129
|
+
process.exit(EXIT_INTERNAL_ERROR);
|
|
78
130
|
}
|
|
79
131
|
}
|