@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 Rigour Contributors
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 VibeGuard in the current directory')
16
- .action(async () => {
17
- await (0, init_js_1.initCommand)(process.cwd());
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, check_js_1.checkCommand)(process.cwd());
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
- await (0, run_js_1.runLoop)(process.cwd(), args, { iterations: parseInt(options.iterations) });
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();
@@ -1 +1,5 @@
1
- export declare function checkCommand(cwd: string): Promise<void>;
1
+ export interface CheckOptions {
2
+ ci?: boolean;
3
+ json?: boolean;
4
+ }
5
+ export declare function checkCommand(cwd: string, options?: CheckOptions): Promise<void>;
@@ -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
- async function checkCommand(cwd) {
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
- console.error(chalk_1.default.red('Error: rigour.yml not found. Run `rigour init` first.'));
16
- process.exit(1);
17
- }
18
- const configContent = await fs_extra_1.default.readFile(configPath, 'utf-8');
19
- const rawConfig = yaml_1.default.parse(configContent);
20
- const config = core_1.ConfigSchema.parse(rawConfig);
21
- console.log(chalk_1.default.blue('Running Rigour checks...\n'));
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
- else {
32
- console.log(chalk_1.default.red.bold('✘ FAIL - Quality gate violations found.\n'));
33
- for (const failure of report.failures) {
34
- console.log(chalk_1.default.red(`[${failure.id}] ${failure.title}`));
35
- console.log(chalk_1.default.dim(` Details: ${failure.details}`));
36
- if (failure.files && failure.files.length > 0) {
37
- console.log(chalk_1.default.dim(' Files:'));
38
- failure.files.forEach((f) => console.log(chalk_1.default.dim(` - ${f}`)));
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
- if (failure.hint) {
41
- console.log(chalk_1.default.cyan(` Hint: ${failure.hint}`));
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
- console.log('');
64
+ process.exit(report.status === 'PASS' ? EXIT_PASS : EXIT_FAIL);
44
65
  }
45
- console.log(chalk_1.default.yellow(`See ${config.output.report_path} for full details.`));
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
- console.log(chalk_1.default.dim(`\nFinished in ${report.stats.duration_ms}ms`));
48
- if (report.status !== 'PASS') {
49
- process.exit(1);
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
+ }
@@ -1 +1,7 @@
1
- export declare function initCommand(cwd: string): Promise<void>;
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>;
@@ -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 recommendedConfig = await discovery.discover(cwd);
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 (Cursor/AntiGravity)
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 rulePath = path_1.default.join(cursorRulesDir, 'rigour.mdc');
35
- const ruleContent = `---
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
- # Rigour Enforcement
41
-
42
- You are operating under Rigour engineering discipline.
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
  }
@@ -1,3 +1,4 @@
1
1
  export declare function runLoop(cwd: string, agentArgs: string[], options: {
2
2
  iterations: number;
3
+ failFast?: boolean;
3
4
  }): Promise<void>;
@@ -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(1);
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. Run the agent command
32
- if (agentArgs.length > 0) {
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: ${agentArgs.join(' ')}`));
54
+ console.log(chalk_1.default.dim(` Command: ${currentArgs.join(' ')}`));
35
55
  try {
36
- await (0, execa_1.execa)(agentArgs[0], agentArgs.slice(1), { shell: true, stdio: 'inherit', cwd });
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
- // 2. Run Rigour Check
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
- // 3. Generate and print Fix Packet for next iteration
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
- const fixPacket = report.failures.map((f, i) => {
53
- let msg = chalk_1.default.white(`${i + 1}. `) + chalk_1.default.bold.red(`[${f.id.toUpperCase()}] `) + chalk_1.default.white(f.title);
54
- msg += `\n ├─ ` + chalk_1.default.dim(`Details: ${f.details}`);
55
- if (f.hint)
56
- msg += `\n └─ ` + chalk_1.default.yellow(`FIX: ${f.hint}`);
57
- return msg;
58
- }).join('\n\n');
59
- console.log(chalk_1.default.bold.white('\n📋 ACTIONABLE FIX PACKET:'));
60
- console.log(fixPacket);
61
- console.log(chalk_1.default.dim('\nReturning control to agent for the next refinement cycle...'));
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(1);
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(1);
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.0.0",
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.0.0"
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 VibeGuard in the current directory')
17
- .action(async () => {
18
- await initCommand(process.cwd());
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 checkCommand(process.cwd());
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
- .action(async (args, options) => {
34
- await runLoop(process.cwd(), args, { iterations: parseInt(options.iterations) });
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();
@@ -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
- export async function checkCommand(cwd: string) {
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
- console.error(chalk.red('Error: rigour.yml not found. Run `rigour init` first.'));
12
- process.exit(1);
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
- const configContent = await fs.readFile(configPath, 'utf-8');
16
- const rawConfig = yaml.parse(configContent);
17
- const config = ConfigSchema.parse(rawConfig);
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
- console.log(chalk.blue('Running Rigour checks...\n'));
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
- const runner = new GateRunner(config);
22
- const report = await runner.run(cwd);
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
- // Write machine report
25
- const reportPath = path.join(cwd, config.output.report_path);
26
- await fs.writeJson(reportPath, report, { spaces: 2 });
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
- // Print human summary
29
- if (report.status === 'PASS') {
30
- console.log(chalk.green.bold('✔ PASS - All quality gates satisfied.'));
31
- } else {
32
- console.log(chalk.red.bold('✘ FAIL - Quality gate violations found.\n'));
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
- for (const failure of report.failures as Failure[]) {
35
- console.log(chalk.red(`[${failure.id}] ${failure.title}`));
36
- console.log(chalk.dim(` Details: ${failure.details}`));
37
- if (failure.files && failure.files.length > 0) {
38
- console.log(chalk.dim(' Files:'));
39
- failure.files.forEach((f: string) => console.log(chalk.dim(` - ${f}`)));
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
- if (failure.hint) {
42
- console.log(chalk.cyan(` Hint: ${failure.hint}`));
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
- console.log('');
92
+
93
+ console.log(chalk.yellow(`See ${config.output.report_path} for full details.`));
45
94
  }
46
95
 
47
- console.log(chalk.yellow(`See ${config.output.report_path} for full details.`));
48
- }
96
+ console.log(chalk.dim(`\nFinished in ${report.stats.duration_ms}ms`));
49
97
 
50
- console.log(chalk.dim(`\nFinished in ${report.stats.duration_ms}ms`));
98
+ process.exit(report.status === 'PASS' ? EXIT_PASS : EXIT_FAIL);
51
99
 
52
- if (report.status !== 'PASS') {
53
- process.exit(1);
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
+ }
@@ -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 async function initCommand(cwd: string) {
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 recommendedConfig = await discovery.discover(cwd);
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 (Cursor/AntiGravity)
32
- const cursorRulesDir = path.join(cwd, '.cursor', 'rules');
33
- await fs.ensureDir(cursorRulesDir);
34
- const rulePath = path.join(cursorRulesDir, 'rigour.mdc');
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
- description: Enforcement of Rigour quality gates and best practices.
38
- globs: **/*
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
- # Rigour Enforcement
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
- You are operating under Rigour engineering discipline.
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
- ## Core Rules
46
- - **Never claim done** until you run \`rigour check\` and it returns PASS.
47
- - If checks FAIL, fix **only** the listed failures. Do not add new features or refactor unrelated code.
48
- - Maintain project memory in \`docs/SPEC.md\`, \`docs/ARCH.md\`, and \`docs/DECISIONS.md\`.
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
- ## Workflow
53
- 1. Write/Modify code.
54
- 2. Run \`rigour check\`.
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
- await fs.writeFile(rulePath, ruleContent);
60
- console.log(chalk.green('✔ Initialized Agent Handshake (.cursor/rules/rigour.mdc)'));
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
  }
@@ -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
- export async function runLoop(cwd: string, agentArgs: string[], options: { iterations: number }) {
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(1);
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. Run the agent command
32
- if (agentArgs.length > 0) {
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: ${agentArgs.join(' ')}`));
56
+ console.log(chalk.dim(` Command: ${currentArgs.join(' ')}`));
35
57
  try {
36
- await execa(agentArgs[0], agentArgs.slice(1), { shell: true, stdio: 'inherit', cwd });
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
- // 2. Run Rigour Check
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
- // 3. Generate and print Fix Packet for next iteration
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
- const fixPacket = report.failures.map((f, i) => {
56
- let msg = chalk.white(`${i + 1}. `) + chalk.bold.red(`[${f.id.toUpperCase()}] `) + chalk.white(f.title);
57
- msg += `\n ├─ ` + chalk.dim(`Details: ${f.details}`);
58
- if (f.hint) msg += `\n └─ ` + chalk.yellow(`FIX: ${f.hint}`);
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
- console.log(chalk.bold.white('\n📋 ACTIONABLE FIX PACKET:'));
63
- console.log(fixPacket);
64
- console.log(chalk.dim('\nReturning control to agent for the next refinement cycle...'));
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(1);
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(1);
129
+ process.exit(EXIT_INTERNAL_ERROR);
78
130
  }
79
131
  }