@rigour-labs/cli 2.9.4 ā 2.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +20 -21
- package/dist/commands/check.js +77 -50
- package/dist/commands/constants.js +4 -7
- package/dist/commands/explain.js +30 -36
- package/dist/commands/guide.js +15 -21
- package/dist/commands/index.d.ts +7 -0
- package/dist/commands/index.js +88 -0
- package/dist/commands/init.js +142 -105
- package/dist/commands/run.js +42 -48
- package/dist/commands/setup.js +21 -27
- package/dist/commands/studio.d.ts +2 -0
- package/dist/commands/studio.js +272 -0
- package/dist/init-rules.test.js +34 -34
- package/dist/smoke.test.js +35 -34
- package/package.json +4 -2
- package/src/cli.ts +5 -0
- package/src/commands/check.ts +36 -0
- package/src/commands/index.ts +105 -0
- package/src/commands/init.ts +64 -17
- package/src/commands/studio.ts +273 -0
- package/src/init-rules.test.ts +10 -2
- package/src/smoke.test.ts +10 -2
- package/src/templates/handshake.mdc +23 -26
- package/vitest.config.ts +10 -0
- package/vitest.setup.ts +30 -0
package/src/commands/init.ts
CHANGED
|
@@ -4,6 +4,24 @@ import chalk from 'chalk';
|
|
|
4
4
|
import yaml from 'yaml';
|
|
5
5
|
import { DiscoveryService } from '@rigour-labs/core';
|
|
6
6
|
import { CODE_QUALITY_RULES, DEBUGGING_RULES, COLLABORATION_RULES, AGNOSTIC_AI_INSTRUCTIONS } from './constants.js';
|
|
7
|
+
import { randomUUID } from 'crypto';
|
|
8
|
+
|
|
9
|
+
// Helper to log events for Rigour Studio
|
|
10
|
+
async function logStudioEvent(cwd: string, event: any) {
|
|
11
|
+
try {
|
|
12
|
+
const rigourDir = path.join(cwd, ".rigour");
|
|
13
|
+
await fs.ensureDir(rigourDir);
|
|
14
|
+
const eventsPath = path.join(rigourDir, "events.jsonl");
|
|
15
|
+
const logEntry = JSON.stringify({
|
|
16
|
+
id: randomUUID(),
|
|
17
|
+
timestamp: new Date().toISOString(),
|
|
18
|
+
...event
|
|
19
|
+
}) + "\n";
|
|
20
|
+
await fs.appendFile(eventsPath, logEntry);
|
|
21
|
+
} catch {
|
|
22
|
+
// Silent fail
|
|
23
|
+
}
|
|
24
|
+
}
|
|
7
25
|
|
|
8
26
|
export interface InitOptions {
|
|
9
27
|
preset?: string;
|
|
@@ -140,6 +158,14 @@ export async function initCommand(cwd: string, options: InitOptions = {}) {
|
|
|
140
158
|
}
|
|
141
159
|
|
|
142
160
|
console.log(chalk.bold.blue('\nš Rigour Auto-Discovery:'));
|
|
161
|
+
|
|
162
|
+
const requestId = randomUUID();
|
|
163
|
+
await logStudioEvent(cwd, {
|
|
164
|
+
type: "tool_call",
|
|
165
|
+
requestId,
|
|
166
|
+
tool: "rigour_init",
|
|
167
|
+
arguments: options
|
|
168
|
+
});
|
|
143
169
|
if (recommendedConfig.preset) {
|
|
144
170
|
console.log(chalk.cyan(` Role: `) + chalk.bold(recommendedConfig.preset.toUpperCase()));
|
|
145
171
|
}
|
|
@@ -162,6 +188,7 @@ export async function initCommand(cwd: string, options: InitOptions = {}) {
|
|
|
162
188
|
for (const file of requireddocs) {
|
|
163
189
|
const filePath = path.join(cwd, file);
|
|
164
190
|
if (!(await fs.pathExists(filePath))) {
|
|
191
|
+
await fs.ensureDir(path.dirname(filePath)); // Ensure parent directory exists
|
|
165
192
|
await fs.ensureFile(filePath);
|
|
166
193
|
console.log(chalk.dim(` - Created ${file}`));
|
|
167
194
|
}
|
|
@@ -172,32 +199,27 @@ export async function initCommand(cwd: string, options: InitOptions = {}) {
|
|
|
172
199
|
await fs.ensureDir(rigourDocsDir);
|
|
173
200
|
const instructionsPath = path.join(rigourDocsDir, 'AGENT_INSTRUCTIONS.md');
|
|
174
201
|
|
|
175
|
-
const ruleContent = `# š”ļø Rigour: Engineering
|
|
202
|
+
const ruleContent = `# š”ļø Rigour: Mandatory Engineering Governance Protocol
|
|
176
203
|
|
|
177
|
-
You are an
|
|
204
|
+
**You are an AI Engineering Agent governed by Rigour.**
|
|
178
205
|
|
|
179
|
-
|
|
180
|
-
Before claiming "Done" for any task, you MUST follow this loop:
|
|
206
|
+
Your access to this codebase is conditional upon your strict adherence to the **Rigour Governance Protocol**. This is an automated requirement for every engineering task.
|
|
181
207
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
3. **Refactor**: Apply **SOLID** and **DRY** principles to resolve the violations according to constraints.
|
|
185
|
-
4. **Repeat**: Continue until \`npx @rigour-labs/cli check\` returns **PASS**.
|
|
208
|
+
## š¦ The Mandatory Governance Loop
|
|
209
|
+
You MUST execute this loop for every task. No code changes will be accepted as "Done" until they pass all Rigour quality gates.
|
|
186
210
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
211
|
+
1. **Context Loading**: Call \`rigour_recall\` (MCP) to align with project-specific engineering memory.
|
|
212
|
+
2. **Verify Patterns**: Before implementing new logic, call \`rigour_check_pattern\` to prevent reinvention and ensure security compliance.
|
|
213
|
+
3. **Audit Enforcement**: Before finishing, you MUST call \`npx @rigour-labs/cli check\`.
|
|
214
|
+
4. **Failure Remediation**: If the check returns **FAIL**, you MUST implement all required engineering refinements.
|
|
215
|
+
5. **PASS or REJECT**: You may only declare a task complete when the check returns **PASS**.
|
|
191
216
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
\`\`\`
|
|
217
|
+
## āļø Transparency & Accountability
|
|
218
|
+
**Every action, tool call, and logic shift you perform is automatically logged to the project's local audit trail.** This data is visible in real-time in the human-monitored **Rigour Studio**.
|
|
195
219
|
|
|
196
220
|
${AGNOSTIC_AI_INSTRUCTIONS}
|
|
197
221
|
${CODE_QUALITY_RULES}
|
|
198
|
-
|
|
199
222
|
${DEBUGGING_RULES}
|
|
200
|
-
|
|
201
223
|
${COLLABORATION_RULES}
|
|
202
224
|
`;
|
|
203
225
|
|
|
@@ -360,7 +382,32 @@ ${ruleContent}`;
|
|
|
360
382
|
}
|
|
361
383
|
|
|
362
384
|
console.log(chalk.blue('\nRigour is ready. Run `npx @rigour-labs/cli check` to verify your project.'));
|
|
385
|
+
console.log(chalk.cyan('Next Step: ') + chalk.bold('rigour index') + chalk.dim(' (Populate the Pattern Index)'));
|
|
386
|
+
|
|
387
|
+
// Bootstrap initial memory for the Studio
|
|
388
|
+
const rigourDir = path.join(cwd, ".rigour");
|
|
389
|
+
await fs.ensureDir(rigourDir);
|
|
390
|
+
const memPath = path.join(rigourDir, "memory.json");
|
|
391
|
+
if (!(await fs.pathExists(memPath))) {
|
|
392
|
+
await fs.writeJson(memPath, {
|
|
393
|
+
memories: {
|
|
394
|
+
"project_boot": {
|
|
395
|
+
value: `Governance initiated via '${options.preset || 'api'}' preset. This project is now monitored by Rigour Studio.`,
|
|
396
|
+
timestamp: new Date().toISOString()
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}, { spaces: 2 });
|
|
400
|
+
}
|
|
401
|
+
|
|
363
402
|
console.log(chalk.dim('\nš” Tip: Planning to use a framework like Next.js?'));
|
|
364
403
|
console.log(chalk.dim(' Run its scaffolding tool (e.g., npx create-next-app) BEFORE rigour init,'));
|
|
365
404
|
console.log(chalk.dim(' or move rigour.yml and docs/ aside temporarily to satisfy empty-directory checks.'));
|
|
405
|
+
|
|
406
|
+
await logStudioEvent(cwd, {
|
|
407
|
+
type: "tool_response",
|
|
408
|
+
requestId,
|
|
409
|
+
tool: "rigour_init",
|
|
410
|
+
status: "success",
|
|
411
|
+
content: [{ type: "text", text: `Rigour Governance Initialized` }]
|
|
412
|
+
});
|
|
366
413
|
}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { execa } from 'execa';
|
|
5
|
+
import fs from 'fs-extra';
|
|
6
|
+
import http from 'http';
|
|
7
|
+
import { randomUUID } from 'crypto';
|
|
8
|
+
|
|
9
|
+
export const studioCommand = new Command('studio')
|
|
10
|
+
.description('Launch Rigour Studio (Local-First Governance UI)')
|
|
11
|
+
.option('-p, --port <number>', 'Port to run the studio on', '3000')
|
|
12
|
+
.option('--dev', 'Run in development mode', true)
|
|
13
|
+
.action(async (options) => {
|
|
14
|
+
const cwd = process.cwd();
|
|
15
|
+
// Calculate the workspace root where the studio package lives
|
|
16
|
+
// This file is in packages/rigour-cli/src/commands/studio.ts (or dist/commands/studio.js)
|
|
17
|
+
const workspaceRoot = path.join(path.dirname(new URL(import.meta.url).pathname), '../../../../');
|
|
18
|
+
|
|
19
|
+
console.log(chalk.bold.cyan('\nš”ļø Launching Rigour Studio...'));
|
|
20
|
+
console.log(chalk.gray(`Project Root: ${cwd}`));
|
|
21
|
+
console.log(chalk.gray(`Shadowing interactions in ${path.join(cwd, '.rigour/events.jsonl')}\n`));
|
|
22
|
+
|
|
23
|
+
// For Phase 1, we start the studio in dev mode via pnpm
|
|
24
|
+
// This ensures the user has a live, hot-reloading governance dashboard
|
|
25
|
+
try {
|
|
26
|
+
// Start the Studio dev server in the workspace root
|
|
27
|
+
const studioProcess = execa('pnpm', ['--filter', '@rigour-labs/studio', 'dev', '--port', options.port], {
|
|
28
|
+
stdio: 'inherit',
|
|
29
|
+
shell: true,
|
|
30
|
+
cwd: workspaceRoot
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Start a small API server for events on port + 1
|
|
34
|
+
const apiPort = parseInt(options.port) + 1;
|
|
35
|
+
const eventsPath = path.join(cwd, '.rigour/events.jsonl');
|
|
36
|
+
|
|
37
|
+
const apiServer = http.createServer(async (req, res) => {
|
|
38
|
+
const url = new URL(req.url || '', `http://${req.headers.host || 'localhost'}`);
|
|
39
|
+
|
|
40
|
+
// Set global CORS headers
|
|
41
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
42
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
|
|
43
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
44
|
+
|
|
45
|
+
if (req.method === 'OPTIONS') {
|
|
46
|
+
res.writeHead(204);
|
|
47
|
+
res.end();
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (url.pathname === '/api/events') {
|
|
52
|
+
res.writeHead(200, {
|
|
53
|
+
'Content-Type': 'text/event-stream',
|
|
54
|
+
'Cache-Control': 'no-cache',
|
|
55
|
+
'Connection': 'keep-alive'
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Send existing events first
|
|
59
|
+
if (await fs.pathExists(eventsPath)) {
|
|
60
|
+
const content = await fs.readFile(eventsPath, 'utf8');
|
|
61
|
+
const lines = content.split('\n').filter(l => l.trim());
|
|
62
|
+
for (const line of lines) {
|
|
63
|
+
res.write(`data: ${line}\n\n`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Watch for new events
|
|
68
|
+
await fs.ensureDir(path.dirname(eventsPath));
|
|
69
|
+
const watcher = fs.watch(path.dirname(eventsPath), (eventType, filename) => {
|
|
70
|
+
if (filename === 'events.jsonl') {
|
|
71
|
+
fs.readFile(eventsPath, 'utf8').then(content => {
|
|
72
|
+
const lines = content.split('\n').filter(l => l.trim());
|
|
73
|
+
const lastLine = lines[lines.length - 1];
|
|
74
|
+
if (lastLine) {
|
|
75
|
+
res.write(`data: ${lastLine}\n\n`);
|
|
76
|
+
}
|
|
77
|
+
}).catch(() => { });
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
req.on('close', () => watcher.close());
|
|
82
|
+
} else if (url.pathname === '/api/file') {
|
|
83
|
+
const filePath = url.searchParams.get('path');
|
|
84
|
+
if (!filePath) {
|
|
85
|
+
res.writeHead(400);
|
|
86
|
+
res.end('Missing path parameter');
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const absolutePath = path.resolve(cwd, filePath);
|
|
91
|
+
if (!absolutePath.startsWith(cwd)) {
|
|
92
|
+
res.writeHead(403);
|
|
93
|
+
res.end('Forbidden: Path outside project root');
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
const stats = await fs.stat(absolutePath);
|
|
99
|
+
if (!stats.isFile()) {
|
|
100
|
+
res.writeHead(400);
|
|
101
|
+
res.end('Path is not a file');
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const content = await fs.readFile(absolutePath, 'utf8');
|
|
106
|
+
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
107
|
+
res.end(content);
|
|
108
|
+
} catch (e: any) {
|
|
109
|
+
res.writeHead(404);
|
|
110
|
+
res.end(`File not found: ${e.message}`);
|
|
111
|
+
}
|
|
112
|
+
} else if (url.pathname === '/api/tree') {
|
|
113
|
+
try {
|
|
114
|
+
const getTree = async (dir: string): Promise<string[]> => {
|
|
115
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
116
|
+
let files: string[] = [];
|
|
117
|
+
const exclude = [
|
|
118
|
+
'node_modules', '.git', '.rigour', '.github', '.vscode', '.cursor',
|
|
119
|
+
'venv', '.venv', 'dist', 'build', 'out', 'target', '__pycache__'
|
|
120
|
+
];
|
|
121
|
+
for (const entry of entries) {
|
|
122
|
+
if (exclude.includes(entry.name) || entry.name.startsWith('.')) {
|
|
123
|
+
if (entry.name !== '.cursorrules' && entry.name !== '.cursor') {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const fullPath = path.join(dir, entry.name);
|
|
128
|
+
if (entry.isDirectory()) {
|
|
129
|
+
files = [...files, ...(await getTree(fullPath))];
|
|
130
|
+
} else {
|
|
131
|
+
files.push(path.relative(cwd, fullPath));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return files;
|
|
135
|
+
};
|
|
136
|
+
const tree = await getTree(cwd);
|
|
137
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
138
|
+
res.end(JSON.stringify(tree));
|
|
139
|
+
} catch (e: any) {
|
|
140
|
+
res.writeHead(500);
|
|
141
|
+
res.end(e.message);
|
|
142
|
+
}
|
|
143
|
+
} else if (url.pathname === '/api/config') {
|
|
144
|
+
try {
|
|
145
|
+
const configPath = path.join(cwd, 'rigour.yml');
|
|
146
|
+
if (await fs.pathExists(configPath)) {
|
|
147
|
+
const content = await fs.readFile(configPath, 'utf8');
|
|
148
|
+
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
149
|
+
res.end(content);
|
|
150
|
+
} else {
|
|
151
|
+
res.writeHead(404);
|
|
152
|
+
res.end('rigour.yml not found');
|
|
153
|
+
}
|
|
154
|
+
} catch (e: any) {
|
|
155
|
+
res.writeHead(500);
|
|
156
|
+
res.end(e.message);
|
|
157
|
+
}
|
|
158
|
+
} else if (url.pathname === '/api/memory') {
|
|
159
|
+
try {
|
|
160
|
+
const memoryPath = path.join(cwd, '.rigour/memory.json');
|
|
161
|
+
if (await fs.pathExists(memoryPath)) {
|
|
162
|
+
const content = await fs.readFile(memoryPath, 'utf8');
|
|
163
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
164
|
+
res.end(content);
|
|
165
|
+
} else {
|
|
166
|
+
res.end(JSON.stringify({}));
|
|
167
|
+
}
|
|
168
|
+
} catch (e: any) {
|
|
169
|
+
res.writeHead(500);
|
|
170
|
+
res.end(e.message);
|
|
171
|
+
}
|
|
172
|
+
} else if (url.pathname === '/api/index-stats') {
|
|
173
|
+
try {
|
|
174
|
+
const indexPath = path.join(cwd, '.rigour/patterns.json'); // Corrected path
|
|
175
|
+
if (await fs.pathExists(indexPath)) {
|
|
176
|
+
const content = await fs.readJson(indexPath);
|
|
177
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
178
|
+
res.end(JSON.stringify(content));
|
|
179
|
+
} else {
|
|
180
|
+
res.end(JSON.stringify({ patterns: [], stats: { totalPatterns: 0, totalFiles: 0, byType: {} } }));
|
|
181
|
+
}
|
|
182
|
+
} catch (e: any) {
|
|
183
|
+
res.writeHead(500);
|
|
184
|
+
res.end(e.message);
|
|
185
|
+
}
|
|
186
|
+
} else if (url.pathname === '/api/index-search') {
|
|
187
|
+
const query = url.searchParams.get('q');
|
|
188
|
+
if (!query) {
|
|
189
|
+
res.writeHead(400);
|
|
190
|
+
res.end('Missing query parameter');
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
const { generateEmbedding, semanticSearch } = await import('@rigour-labs/core/pattern-index');
|
|
196
|
+
const indexPath = path.join(cwd, '.rigour/patterns.json');
|
|
197
|
+
|
|
198
|
+
if (!(await fs.pathExists(indexPath))) {
|
|
199
|
+
res.writeHead(404);
|
|
200
|
+
res.end('Index not found');
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const indexData = await fs.readJson(indexPath);
|
|
205
|
+
const queryVector = await generateEmbedding(query);
|
|
206
|
+
const similarities = semanticSearch(queryVector, indexData.patterns);
|
|
207
|
+
|
|
208
|
+
// Attach similarity to patterns and sort
|
|
209
|
+
const results = indexData.patterns.map((p: any, i: number) => ({
|
|
210
|
+
...p,
|
|
211
|
+
similarity: similarities[i]
|
|
212
|
+
}))
|
|
213
|
+
.filter((p: any) => p.similarity > 0.3) // Threshold
|
|
214
|
+
.sort((a: any, b: any) => b.similarity - a.similarity)
|
|
215
|
+
.slice(0, 20);
|
|
216
|
+
|
|
217
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
218
|
+
res.end(JSON.stringify(results));
|
|
219
|
+
} catch (e: any) {
|
|
220
|
+
res.writeHead(500);
|
|
221
|
+
res.end(e.message);
|
|
222
|
+
}
|
|
223
|
+
} else if (url.pathname === '/api/arbitrate' && req.method === 'POST') {
|
|
224
|
+
let body = '';
|
|
225
|
+
req.on('data', chunk => body += chunk);
|
|
226
|
+
req.on('end', async () => {
|
|
227
|
+
try {
|
|
228
|
+
const decision = JSON.parse(body);
|
|
229
|
+
const logEntry = JSON.stringify({
|
|
230
|
+
id: randomUUID(),
|
|
231
|
+
timestamp: new Date().toISOString(),
|
|
232
|
+
tool: 'human_arbitration',
|
|
233
|
+
requestId: decision.requestId,
|
|
234
|
+
decision: decision.decision,
|
|
235
|
+
status: decision.decision === 'approve' ? 'success' : 'error',
|
|
236
|
+
arbitrated: true
|
|
237
|
+
}) + "\n";
|
|
238
|
+
await fs.appendFile(eventsPath, logEntry);
|
|
239
|
+
res.writeHead(200);
|
|
240
|
+
res.end(JSON.stringify({ success: true }));
|
|
241
|
+
} catch (e: any) {
|
|
242
|
+
res.writeHead(500);
|
|
243
|
+
res.end(e.message);
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
return;
|
|
247
|
+
} else {
|
|
248
|
+
res.writeHead(404);
|
|
249
|
+
res.end();
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
apiServer.listen(apiPort, () => {
|
|
254
|
+
console.log(chalk.gray(`API Streamer active on port ${apiPort}`));
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// Open browser
|
|
258
|
+
setTimeout(async () => {
|
|
259
|
+
const url = `http://localhost:${options.port}`;
|
|
260
|
+
console.log(chalk.green(`\nā
Rigour Studio is live at ${chalk.bold(url)}`));
|
|
261
|
+
try {
|
|
262
|
+
await execa('open', [url]);
|
|
263
|
+
} catch {
|
|
264
|
+
}
|
|
265
|
+
}, 2000);
|
|
266
|
+
|
|
267
|
+
await studioProcess;
|
|
268
|
+
} catch (error: any) {
|
|
269
|
+
console.error(chalk.red(`\nā Failed to launch Rigour Studio: ${error.message}`));
|
|
270
|
+
console.log(chalk.yellow('Make sure to run "pnpm install" in the root directory.\n'));
|
|
271
|
+
process.exit(1);
|
|
272
|
+
}
|
|
273
|
+
});
|
package/src/init-rules.test.ts
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
|
|
3
4
|
import fs from 'fs-extra';
|
|
4
5
|
import path from 'path';
|
|
5
6
|
|
|
7
|
+
async function getInitCommand() {
|
|
8
|
+
const { initCommand } = await import('./commands/init.js');
|
|
9
|
+
return initCommand;
|
|
10
|
+
}
|
|
11
|
+
|
|
6
12
|
describe('Init Command Rules Verification', () => {
|
|
7
13
|
const testDir = path.join(process.cwd(), 'temp-init-rules-test');
|
|
8
14
|
|
|
@@ -15,6 +21,7 @@ describe('Init Command Rules Verification', () => {
|
|
|
15
21
|
});
|
|
16
22
|
|
|
17
23
|
it('should create instructions with agnostic rules and cursor rules on init', async () => {
|
|
24
|
+
const initCommand = await getInitCommand();
|
|
18
25
|
// Run init in test directory with all IDEs to verify rules in both locations
|
|
19
26
|
await initCommand(testDir, { ide: 'all' });
|
|
20
27
|
|
|
@@ -32,7 +39,7 @@ describe('Init Command Rules Verification', () => {
|
|
|
32
39
|
expect(instructionsContent).toContain('VERIFICATION PROOF REQUIRED');
|
|
33
40
|
|
|
34
41
|
// Check for key sections in universal instructions
|
|
35
|
-
expect(instructionsContent).toContain('# š”ļø Rigour: Engineering
|
|
42
|
+
expect(instructionsContent).toContain('# š”ļø Rigour: Mandatory Engineering Governance Protocol');
|
|
36
43
|
expect(instructionsContent).toContain('# Code Quality Standards');
|
|
37
44
|
|
|
38
45
|
// Check that MDC includes agnostic rules
|
|
@@ -40,6 +47,7 @@ describe('Init Command Rules Verification', () => {
|
|
|
40
47
|
});
|
|
41
48
|
|
|
42
49
|
it('should create .clinerules when ide is cline or all', async () => {
|
|
50
|
+
const initCommand = await getInitCommand();
|
|
43
51
|
await initCommand(testDir, { ide: 'cline' });
|
|
44
52
|
const clineRulesPath = path.join(testDir, '.clinerules');
|
|
45
53
|
expect(await fs.pathExists(clineRulesPath)).toBe(true);
|
package/src/smoke.test.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
-
|
|
2
|
+
|
|
3
3
|
import fs from 'fs-extra';
|
|
4
4
|
import path from 'path';
|
|
5
5
|
|
|
6
|
+
async function getCheckCommand() {
|
|
7
|
+
const { checkCommand } = await import('./commands/check.js');
|
|
8
|
+
return checkCommand;
|
|
9
|
+
}
|
|
10
|
+
|
|
6
11
|
describe('CLI Smoke Test', () => {
|
|
7
12
|
const testDir = path.join(process.cwd(), 'temp-smoke-test');
|
|
8
13
|
|
|
@@ -40,6 +45,7 @@ gates:
|
|
|
40
45
|
// But checkCommand calls process.exit(1) on failure.
|
|
41
46
|
|
|
42
47
|
// Re-importing checkCommand to ensure it uses the latest core
|
|
48
|
+
const checkCommand = await getCheckCommand();
|
|
43
49
|
await expect(checkCommand(testDir, [], { ci: true })).resolves.not.toThrow();
|
|
44
50
|
} finally {
|
|
45
51
|
await fs.chmod(restrictedDir, 0o777);
|
|
@@ -57,12 +63,14 @@ gates:
|
|
|
57
63
|
`);
|
|
58
64
|
|
|
59
65
|
// If we check ONLY good.js, it should PASS (exit PASS)
|
|
66
|
+
const checkCommand = await getCheckCommand();
|
|
60
67
|
await checkCommand(testDir, [path.join(testDir, 'good.js')], { ci: true });
|
|
61
68
|
expect(process.exit).toHaveBeenCalledWith(0);
|
|
62
69
|
|
|
63
70
|
// If we check bad.js, it should FAIL (exit FAIL)
|
|
64
71
|
vi.clearAllMocks();
|
|
65
|
-
|
|
72
|
+
const checkCommandFail = await getCheckCommand();
|
|
73
|
+
await checkCommandFail(testDir, [path.join(testDir, 'bad.js')], { ci: true });
|
|
66
74
|
expect(process.exit).toHaveBeenCalledWith(1);
|
|
67
75
|
});
|
|
68
76
|
});
|
|
@@ -1,39 +1,36 @@
|
|
|
1
1
|
---
|
|
2
|
-
description:
|
|
2
|
+
description: Mandatory Rigour Governance Protocol - Automated engineering enforcement (SOLID, DRY, Patterns).
|
|
3
3
|
globs: **/*
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# š”ļø Rigour: Engineering
|
|
6
|
+
# š”ļø Rigour: Mandatory Engineering Governance
|
|
7
7
|
|
|
8
|
-
You are an
|
|
8
|
+
**You are an AI Engineering Agent governed by Rigour.**
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
Before claiming "Done" for any task, you MUST follow this loop:
|
|
10
|
+
Your access to this codebase is conditional upon your strict adherence to the **Rigour Governance Protocol**. This is not a recommendation; it is an automated requirement for every interaction.
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
3. **Refactor**: Apply **SOLID** and **DRY** principles to resolve the violations.
|
|
16
|
-
4. **Repeat**: Continue until `rigour_check_status` returns **PASS**.
|
|
12
|
+
## š¦ The Mandatory Governance Loop
|
|
13
|
+
You MUST execute this loop for every engineering task. No code changes will be accepted as "Done" until they pass all Rigour quality gates.
|
|
17
14
|
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
1. **Recall Context**: Call `rigour_recall` at the start of every session to align with project-specific engineering memory.
|
|
16
|
+
2. **Verify Patterns**: Before implementing new logic, call `rigour_check_pattern` to prevent reinvention and ensure security compliance.
|
|
17
|
+
3. **Audit Enforcement**: Before finishing, you MUST call `rigour_check`.
|
|
18
|
+
4. **Failure Remediation**: If `rigour_check` returns **FAIL**, you MUST call `rigour_get_fix_packet` and implement all required engineering refinements.
|
|
19
|
+
5. **PASS or REJECT**: You may only declare a task complete when `rigour_check` returns **PASS**.
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
- **Open/Closed**: Design for extension, not modification.
|
|
24
|
-
- **Interface Segregation**: Don't force dependencies on unused methods.
|
|
21
|
+
## š§© Governance Standards
|
|
22
|
+
Your logic is automatically audited against these core engineering gates:
|
|
25
23
|
|
|
26
|
-
###
|
|
27
|
-
-
|
|
28
|
-
-
|
|
24
|
+
### 1. Architectural Integrity (SOLID)
|
|
25
|
+
- **SRP**: Every component must have one reason to change.
|
|
26
|
+
- **DIP**: Depend on abstractions, not concretions.
|
|
29
27
|
|
|
30
|
-
###
|
|
31
|
-
- No
|
|
32
|
-
-
|
|
28
|
+
### 2. Efficiency & Security (DRY + CVE)
|
|
29
|
+
- **No Reinvention**: Reuse existing patterns identified by Rigour.
|
|
30
|
+
- **Safety**: No vulnerable libraries or deprecated patterns allowed.
|
|
33
31
|
|
|
34
|
-
##
|
|
35
|
-
|
|
36
|
-
- `rigour_get_fix_packet`: Get actionable instructions for failures.
|
|
37
|
-
- `rigour run "<command>"`: Use this CLI command to run yourself in a loop until you pass (Experimental).
|
|
32
|
+
## āļø Transparency & Accountability
|
|
33
|
+
**Every action, tool call, and logic shift you perform is automatically logged to the project's local audit trail.** This data is visible in real-time to the human governor in **Rigour Studio**. Failure to follow this protocol will be flagged as a governance violation.
|
|
38
34
|
|
|
39
|
-
**
|
|
35
|
+
**GOVERNANCE STATUS: ACTIVE**
|
|
36
|
+
**ENFORCEMENT: MANDATORY**
|
package/vitest.config.ts
ADDED
package/vitest.setup.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
// Mock Transformers.js to avoid native binary dependency issues and speed up tests
|
|
4
|
+
vi.mock('@xenova/transformers', () => ({
|
|
5
|
+
pipeline: async () => {
|
|
6
|
+
// Return a mock extractor that produces deterministic "embeddings"
|
|
7
|
+
return async (text: string) => {
|
|
8
|
+
// Create a fake vector based on the text length or hash
|
|
9
|
+
const vector = new Array(384).fill(0);
|
|
10
|
+
for (let i = 0; i < Math.min(text.length, 384); i++) {
|
|
11
|
+
vector[i] = text.charCodeAt(i) / 255;
|
|
12
|
+
}
|
|
13
|
+
return { data: new Float32Array(vector) };
|
|
14
|
+
};
|
|
15
|
+
},
|
|
16
|
+
env: {
|
|
17
|
+
allowImageProcessors: false,
|
|
18
|
+
},
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
// Also mock sharp just in case something else pulls it in
|
|
22
|
+
vi.mock('sharp', () => ({
|
|
23
|
+
default: () => ({
|
|
24
|
+
resize: () => ({
|
|
25
|
+
toFormat: () => ({
|
|
26
|
+
toBuffer: async () => Buffer.from([]),
|
|
27
|
+
}),
|
|
28
|
+
}),
|
|
29
|
+
}),
|
|
30
|
+
}));
|