@rigour-labs/cli 6.0.0 ā 6.1.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +2 -0
- package/dist/commands/hooks.js +22 -1
- package/dist/commands/index.js +1 -1
- package/dist/commands/studio-contracts.d.ts +20 -0
- package/dist/commands/studio-contracts.js +78 -0
- package/dist/commands/studio-contracts.test.d.ts +1 -0
- package/dist/commands/studio-contracts.test.js +33 -0
- package/dist/commands/studio.js +194 -24
- package/dist/commands/team.d.ts +2 -0
- package/dist/commands/team.js +78 -0
- package/package.json +4 -3
- package/studio-dist/assets/KnowledgeGraph-CWGbolsD.js +430 -0
- package/studio-dist/assets/KnowledgeGraph-DEqMBSbC.css +1 -0
- package/studio-dist/assets/index-DR7ehi_i.js +398 -0
- package/studio-dist/assets/index-PA_eqVds.css +1 -0
- package/studio-dist/index.html +2 -2
- package/studio-dist/assets/index-DzNTOo-7.js +0 -372
- package/studio-dist/assets/index-jC3k12bf.css +0 -1
package/dist/cli.js
CHANGED
|
@@ -20,6 +20,7 @@ import { reviewCommand } from './commands/review.js';
|
|
|
20
20
|
import { checkPatternCommand } from './commands/check-pattern.js';
|
|
21
21
|
import { securityAuditCommand } from './commands/security-audit.js';
|
|
22
22
|
import { firewallTransactCommand, firewallAdversarialCommand, firewallAdmitCommand, firewallStatusCommand } from './commands/firewall.js';
|
|
23
|
+
import { teamCommand } from './commands/team.js';
|
|
23
24
|
import { checkForUpdates } from './utils/version.js';
|
|
24
25
|
import { getCliVersion } from './utils/cli-version.js';
|
|
25
26
|
import chalk from 'chalk';
|
|
@@ -29,6 +30,7 @@ program.addCommand(indexCommand);
|
|
|
29
30
|
program.addCommand(studioCommand);
|
|
30
31
|
program.addCommand(brainCommand);
|
|
31
32
|
program.addCommand(deepStatsCommand);
|
|
33
|
+
program.addCommand(teamCommand);
|
|
32
34
|
program
|
|
33
35
|
.name('rigour')
|
|
34
36
|
.description('š”ļø Rigour: The Quality Gate Loop for AI-Assisted Engineering')
|
package/dist/commands/hooks.js
CHANGED
|
@@ -18,7 +18,7 @@ import path from 'path';
|
|
|
18
18
|
import chalk from 'chalk';
|
|
19
19
|
import { randomUUID } from 'crypto';
|
|
20
20
|
import { fileURLToPath } from 'url';
|
|
21
|
-
import {
|
|
21
|
+
import { allowLastDLPBlock, createDLPAuditEntry, formatDLPAlert, recordInteractionEvidence, recordInteractionLesson, runHookChecker, scanInputForCredentials, updateAutomaticIndexForFiles, writeDLPBlockManifest, } from '@rigour-labs/core';
|
|
22
22
|
function getHookCliVersion() {
|
|
23
23
|
const thisDir = path.dirname(fileURLToPath(import.meta.url));
|
|
24
24
|
const packagePath = path.resolve(thisDir, '../../package.json');
|
|
@@ -648,7 +648,28 @@ export async function hooksCheckCommand(cwd, options = {}) {
|
|
|
648
648
|
cwd,
|
|
649
649
|
files,
|
|
650
650
|
timeout_ms: Number.isFinite(timeout) ? timeout : 5000,
|
|
651
|
+
agentId: options.agent || process.env.RIGOUR_AGENT_ID,
|
|
651
652
|
});
|
|
653
|
+
const requestId = randomUUID();
|
|
654
|
+
const outcome = result.status === 'pass' ? 'success' : result.status === 'fail' ? 'rejected' : 'error';
|
|
655
|
+
await Promise.allSettled([
|
|
656
|
+
updateAutomaticIndexForFiles(cwd, files),
|
|
657
|
+
recordInteractionEvidence(cwd, {
|
|
658
|
+
tool: 'rigour_hooks_check', requestId, phase: 'response', outcome,
|
|
659
|
+
deterministic: result.status === 'pass', agentId: options.agent || process.env.RIGOUR_AGENT_ID,
|
|
660
|
+
files, summary: `${result.failures.length} finding(s)`,
|
|
661
|
+
}),
|
|
662
|
+
recordInteractionLesson(cwd, {
|
|
663
|
+
tool: 'rigour_hooks_check', requestId, outcome,
|
|
664
|
+
deterministic: result.status === 'pass', agentId: options.agent || process.env.RIGOUR_AGENT_ID,
|
|
665
|
+
files, summary: `${result.failures.length} finding(s)`,
|
|
666
|
+
}),
|
|
667
|
+
logStudioEvent(cwd, {
|
|
668
|
+
type: 'hook_check', requestId, outcome, status: result.status,
|
|
669
|
+
agentId: options.agent || process.env.RIGOUR_AGENT_ID,
|
|
670
|
+
files, summary: `${files.length} file(s), ${result.failures.length} finding(s)`,
|
|
671
|
+
}),
|
|
672
|
+
]);
|
|
652
673
|
// Return Cursor-compatible format if detected as Cursor hook
|
|
653
674
|
if (cursorMode) {
|
|
654
675
|
if (result.status === 'fail') {
|
package/dist/commands/index.js
CHANGED
|
@@ -30,7 +30,7 @@ async function logStudioEvent(cwd, event) {
|
|
|
30
30
|
// native dependency issues from affecting the rest of the CLI.
|
|
31
31
|
export const indexCommand = new Command('index')
|
|
32
32
|
.description('Build or update the pattern index for the current project')
|
|
33
|
-
.option('-
|
|
33
|
+
.option('--no-semantic', 'Skip local semantic embeddings and build only the structural index')
|
|
34
34
|
.option('-f, --force', 'Force a full rebuild of the index', false)
|
|
35
35
|
.option('-o, --output <path>', 'Custom path for the index file')
|
|
36
36
|
.action(async (options) => {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type AgentStatus = 'active' | 'idle' | 'completed';
|
|
2
|
+
export type AgentSessionStatus = AgentStatus | 'aborted' | 'inactive';
|
|
3
|
+
export interface StudioAgent {
|
|
4
|
+
agentId: string;
|
|
5
|
+
taskScope: string[];
|
|
6
|
+
registeredAt: string;
|
|
7
|
+
lastCheckpoint?: string;
|
|
8
|
+
status: AgentStatus;
|
|
9
|
+
}
|
|
10
|
+
export interface StudioAgentSession {
|
|
11
|
+
schemaVersion: 1;
|
|
12
|
+
sessionId: string;
|
|
13
|
+
agents: StudioAgent[];
|
|
14
|
+
status: AgentSessionStatus;
|
|
15
|
+
createdAt: string;
|
|
16
|
+
derived: boolean;
|
|
17
|
+
dataQuality: 'valid' | 'degraded';
|
|
18
|
+
warnings: string[];
|
|
19
|
+
}
|
|
20
|
+
export declare function normalizeAgentSession(input: unknown, now?: string): StudioAgentSession;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
const AGENT_STATUSES = new Set(['active', 'idle', 'completed']);
|
|
2
|
+
const SESSION_STATUSES = new Set([
|
|
3
|
+
'active',
|
|
4
|
+
'idle',
|
|
5
|
+
'completed',
|
|
6
|
+
'aborted',
|
|
7
|
+
'inactive',
|
|
8
|
+
]);
|
|
9
|
+
function record(value) {
|
|
10
|
+
return value && typeof value === 'object' ? value : {};
|
|
11
|
+
}
|
|
12
|
+
function validDate(value, fallback) {
|
|
13
|
+
if (typeof value !== 'string' && typeof value !== 'number')
|
|
14
|
+
return fallback;
|
|
15
|
+
const date = new Date(value);
|
|
16
|
+
return Number.isNaN(date.getTime()) ? fallback : date.toISOString();
|
|
17
|
+
}
|
|
18
|
+
function inferSessionStatus(agents) {
|
|
19
|
+
if (agents.length === 0)
|
|
20
|
+
return 'inactive';
|
|
21
|
+
if (agents.some((agent) => agent.status === 'active'))
|
|
22
|
+
return 'active';
|
|
23
|
+
if (agents.some((agent) => agent.status === 'idle'))
|
|
24
|
+
return 'idle';
|
|
25
|
+
return 'completed';
|
|
26
|
+
}
|
|
27
|
+
export function normalizeAgentSession(input, now = new Date().toISOString()) {
|
|
28
|
+
const source = record(input);
|
|
29
|
+
const warnings = [];
|
|
30
|
+
const rawAgents = Array.isArray(source.agents) ? source.agents : [];
|
|
31
|
+
if (!Array.isArray(source.agents) && source.agents !== undefined) {
|
|
32
|
+
warnings.push('Ignored invalid agents collection.');
|
|
33
|
+
}
|
|
34
|
+
const agents = rawAgents.map((value, index) => {
|
|
35
|
+
const raw = record(value);
|
|
36
|
+
const agentId = typeof raw.agentId === 'string' && raw.agentId.trim()
|
|
37
|
+
? raw.agentId.trim()
|
|
38
|
+
: `unknown-agent-${index + 1}`;
|
|
39
|
+
if (agentId.startsWith('unknown-agent-'))
|
|
40
|
+
warnings.push(`Agent ${index + 1} had no identifier.`);
|
|
41
|
+
const taskScope = Array.isArray(raw.taskScope)
|
|
42
|
+
? raw.taskScope.filter((scope) => typeof scope === 'string' && scope.length > 0)
|
|
43
|
+
: [];
|
|
44
|
+
if (!Array.isArray(raw.taskScope) && raw.taskScope !== undefined) {
|
|
45
|
+
warnings.push(`Agent ${agentId} had an invalid scope.`);
|
|
46
|
+
}
|
|
47
|
+
const status = typeof raw.status === 'string' && AGENT_STATUSES.has(raw.status)
|
|
48
|
+
? raw.status
|
|
49
|
+
: 'idle';
|
|
50
|
+
if (status === 'idle' && raw.status !== 'idle')
|
|
51
|
+
warnings.push(`Agent ${agentId} had no valid status.`);
|
|
52
|
+
const registeredAt = validDate(raw.registeredAt, now);
|
|
53
|
+
const lastCheckpoint = raw.lastCheckpoint === undefined
|
|
54
|
+
? undefined
|
|
55
|
+
: validDate(raw.lastCheckpoint, registeredAt);
|
|
56
|
+
return { agentId, taskScope, registeredAt, lastCheckpoint, status };
|
|
57
|
+
});
|
|
58
|
+
const requestedStatus = typeof source.status === 'string'
|
|
59
|
+
? source.status
|
|
60
|
+
: undefined;
|
|
61
|
+
const status = requestedStatus && SESSION_STATUSES.has(requestedStatus)
|
|
62
|
+
? requestedStatus
|
|
63
|
+
: inferSessionStatus(agents);
|
|
64
|
+
if (requestedStatus !== status)
|
|
65
|
+
warnings.push('Session status was inferred from agent activity.');
|
|
66
|
+
return {
|
|
67
|
+
schemaVersion: 1,
|
|
68
|
+
sessionId: typeof source.sessionId === 'string' && source.sessionId.trim()
|
|
69
|
+
? source.sessionId
|
|
70
|
+
: agents.length > 0 ? 'legacy-session' : 'inactive',
|
|
71
|
+
agents,
|
|
72
|
+
status,
|
|
73
|
+
createdAt: validDate(source.createdAt, agents[0]?.registeredAt ?? now),
|
|
74
|
+
derived: Boolean(source.derived),
|
|
75
|
+
dataQuality: warnings.length > 0 ? 'degraded' : 'valid',
|
|
76
|
+
warnings,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { normalizeAgentSession } from './studio-contracts.js';
|
|
3
|
+
describe('normalizeAgentSession', () => {
|
|
4
|
+
it('repairs a legacy session without status', () => {
|
|
5
|
+
const session = normalizeAgentSession({
|
|
6
|
+
sessionId: 'legacy',
|
|
7
|
+
agents: [{ agentId: 'agent-1', taskScope: ['src/**'], registeredAt: '2026-09-10T00:00:00Z' }],
|
|
8
|
+
}, '2026-09-10T01:00:00.000Z');
|
|
9
|
+
expect(session.status).toBe('idle');
|
|
10
|
+
expect(session.agents[0].status).toBe('idle');
|
|
11
|
+
expect(session.dataQuality).toBe('degraded');
|
|
12
|
+
expect(session.warnings).toContain('Session status was inferred from agent activity.');
|
|
13
|
+
});
|
|
14
|
+
it('returns an inactive contract for malformed input', () => {
|
|
15
|
+
expect(normalizeAgentSession({ agents: 'broken' }).status).toBe('inactive');
|
|
16
|
+
expect(normalizeAgentSession(null).agents).toEqual([]);
|
|
17
|
+
});
|
|
18
|
+
it('preserves valid current sessions', () => {
|
|
19
|
+
const session = normalizeAgentSession({
|
|
20
|
+
sessionId: 'session-1',
|
|
21
|
+
status: 'active',
|
|
22
|
+
createdAt: '2026-09-10T00:00:00Z',
|
|
23
|
+
agents: [{
|
|
24
|
+
agentId: 'agent-1',
|
|
25
|
+
taskScope: ['src/**'],
|
|
26
|
+
registeredAt: '2026-09-10T00:00:00Z',
|
|
27
|
+
status: 'active',
|
|
28
|
+
}],
|
|
29
|
+
});
|
|
30
|
+
expect(session.dataQuality).toBe('valid');
|
|
31
|
+
expect(session.status).toBe('active');
|
|
32
|
+
});
|
|
33
|
+
});
|
package/dist/commands/studio.js
CHANGED
|
@@ -4,8 +4,11 @@ import os from 'os';
|
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
import { execa } from 'execa';
|
|
6
6
|
import fs from 'fs-extra';
|
|
7
|
+
import { createReadStream, promises as nativeFs } from 'fs';
|
|
8
|
+
import readline from 'readline';
|
|
7
9
|
import http from 'http';
|
|
8
10
|
import { randomUUID } from 'crypto';
|
|
11
|
+
import { normalizeAgentSession } from './studio-contracts.js';
|
|
9
12
|
function sendJson(res, status, body) {
|
|
10
13
|
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
11
14
|
res.end(JSON.stringify(body));
|
|
@@ -20,6 +23,44 @@ async function readJsonIfExists(filePath) {
|
|
|
20
23
|
return null;
|
|
21
24
|
}
|
|
22
25
|
}
|
|
26
|
+
async function readRecentLines(filePath, limit) {
|
|
27
|
+
const stat = await nativeFs.stat(filePath);
|
|
28
|
+
const bytes = Math.min(stat.size, 512 * 1024);
|
|
29
|
+
const handle = await nativeFs.open(filePath, 'r');
|
|
30
|
+
try {
|
|
31
|
+
const buffer = Buffer.alloc(bytes);
|
|
32
|
+
await handle.read(buffer, 0, bytes, stat.size - bytes);
|
|
33
|
+
return buffer.toString('utf8').split('\n').filter(line => line.trim()).slice(-limit);
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
await handle.close();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
async function readEventPage(filePath, limit, before) {
|
|
40
|
+
if (!(await fs.pathExists(filePath)))
|
|
41
|
+
return { events: [], hasMore: false };
|
|
42
|
+
const ring = [];
|
|
43
|
+
let matching = 0;
|
|
44
|
+
const lines = readline.createInterface({ input: createReadStream(filePath), crlfDelay: Infinity });
|
|
45
|
+
for await (const line of lines) {
|
|
46
|
+
if (!line.trim())
|
|
47
|
+
continue;
|
|
48
|
+
try {
|
|
49
|
+
const event = JSON.parse(line);
|
|
50
|
+
const timestamp = Date.parse(event.timestamp ?? event.createdAt ?? 0);
|
|
51
|
+
if (before && (!Number.isFinite(timestamp) || timestamp >= before))
|
|
52
|
+
continue;
|
|
53
|
+
matching++;
|
|
54
|
+
ring.push(event);
|
|
55
|
+
if (ring.length > limit)
|
|
56
|
+
ring.shift();
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// Malformed historical lines are ignored without hiding the remaining ledger.
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return { events: ring.reverse(), hasMore: matching > limit };
|
|
63
|
+
}
|
|
23
64
|
async function mergeMemoryStores(cwd) {
|
|
24
65
|
const sources = [];
|
|
25
66
|
const memories = {};
|
|
@@ -72,7 +113,7 @@ async function synthesizeAgents(cwd, checkpoints) {
|
|
|
72
113
|
const sessionPath = path.join(cwd, '.rigour/agent-session.json');
|
|
73
114
|
const session = await readJsonIfExists(sessionPath);
|
|
74
115
|
if (session?.agents?.length) {
|
|
75
|
-
return session;
|
|
116
|
+
return normalizeAgentSession(session);
|
|
76
117
|
}
|
|
77
118
|
const byAgent = new Map();
|
|
78
119
|
for (const cp of checkpoints) {
|
|
@@ -94,13 +135,13 @@ async function synthesizeAgents(cwd, checkpoints) {
|
|
|
94
135
|
}
|
|
95
136
|
}
|
|
96
137
|
const agents = [...byAgent.values()];
|
|
97
|
-
return {
|
|
138
|
+
return normalizeAgentSession({
|
|
98
139
|
sessionId: agents.length ? 'derived-from-checkpoints' : 'inactive',
|
|
99
140
|
agents,
|
|
100
141
|
status: agents.length ? 'completed' : 'inactive',
|
|
101
142
|
createdAt: agents[0]?.registeredAt || new Date().toISOString(),
|
|
102
143
|
derived: true,
|
|
103
|
-
};
|
|
144
|
+
});
|
|
104
145
|
}
|
|
105
146
|
async function handleApiRequest(req, res, url, ctx) {
|
|
106
147
|
if (!url.pathname.startsWith('/api'))
|
|
@@ -112,6 +153,7 @@ async function handleApiRequest(req, res, url, ctx) {
|
|
|
112
153
|
}
|
|
113
154
|
res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS, POST, DELETE');
|
|
114
155
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
156
|
+
res.setHeader('X-Rigour-Api-Version', '1');
|
|
115
157
|
if (req.method === 'OPTIONS') {
|
|
116
158
|
res.writeHead(204);
|
|
117
159
|
res.end();
|
|
@@ -124,30 +166,35 @@ async function handleApiRequest(req, res, url, ctx) {
|
|
|
124
166
|
'Cache-Control': 'no-cache',
|
|
125
167
|
Connection: 'keep-alive',
|
|
126
168
|
});
|
|
169
|
+
res.write(': connected\n\n');
|
|
127
170
|
if (await fs.pathExists(eventsPath)) {
|
|
128
|
-
const
|
|
129
|
-
const lines = content.split('\n').filter((l) => l.trim());
|
|
130
|
-
// Send recent history only ā full 77MB dumps freeze the UI
|
|
131
|
-
for (const line of lines.slice(-200)) {
|
|
171
|
+
for (const line of await readRecentLines(eventsPath, 200)) {
|
|
132
172
|
res.write(`data: ${line}\n\n`);
|
|
133
173
|
}
|
|
134
174
|
}
|
|
135
175
|
await fs.ensureDir(path.dirname(eventsPath));
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
176
|
+
let lastModified = (await fs.pathExists(eventsPath)) ? (await fs.stat(eventsPath)).mtimeMs : 0;
|
|
177
|
+
let ticks = 0;
|
|
178
|
+
const poller = setInterval(async () => {
|
|
179
|
+
try {
|
|
180
|
+
ticks++;
|
|
181
|
+
if (ticks % 15 === 0)
|
|
182
|
+
res.write(': heartbeat\n\n');
|
|
183
|
+
if (!(await fs.pathExists(eventsPath)))
|
|
184
|
+
return;
|
|
185
|
+
const stat = await fs.stat(eventsPath);
|
|
186
|
+
if (stat.mtimeMs <= lastModified)
|
|
187
|
+
return;
|
|
188
|
+
lastModified = stat.mtimeMs;
|
|
189
|
+
const lastLine = (await readRecentLines(eventsPath, 1)).at(-1);
|
|
190
|
+
if (lastLine)
|
|
191
|
+
res.write(`data: ${lastLine}\n\n`);
|
|
148
192
|
}
|
|
149
|
-
|
|
150
|
-
|
|
193
|
+
catch {
|
|
194
|
+
// A transient read failure must not terminate the event stream.
|
|
195
|
+
}
|
|
196
|
+
}, 1_000);
|
|
197
|
+
req.on('close', () => clearInterval(poller));
|
|
151
198
|
return true;
|
|
152
199
|
}
|
|
153
200
|
if (url.pathname === '/api/file') {
|
|
@@ -158,13 +205,21 @@ async function handleApiRequest(req, res, url, ctx) {
|
|
|
158
205
|
return true;
|
|
159
206
|
}
|
|
160
207
|
const absolutePath = path.resolve(cwd, filePath);
|
|
161
|
-
|
|
208
|
+
const relativePath = path.relative(path.resolve(cwd), absolutePath);
|
|
209
|
+
if (!relativePath || relativePath.startsWith(`..${path.sep}`) || path.isAbsolute(relativePath)) {
|
|
162
210
|
res.writeHead(403);
|
|
163
211
|
res.end('Forbidden');
|
|
164
212
|
return true;
|
|
165
213
|
}
|
|
166
214
|
try {
|
|
167
|
-
const
|
|
215
|
+
const [realRoot, realFile] = await Promise.all([fs.realpath(cwd), fs.realpath(absolutePath)]);
|
|
216
|
+
const realRelative = path.relative(realRoot, realFile);
|
|
217
|
+
if (!realRelative || realRelative.startsWith(`..${path.sep}`) || path.isAbsolute(realRelative)) {
|
|
218
|
+
res.writeHead(403);
|
|
219
|
+
res.end('Forbidden');
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
const content = await fs.readFile(realFile, 'utf8');
|
|
168
223
|
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
169
224
|
res.end(content);
|
|
170
225
|
}
|
|
@@ -267,6 +322,109 @@ async function handleApiRequest(req, res, url, ctx) {
|
|
|
267
322
|
}
|
|
268
323
|
return true;
|
|
269
324
|
}
|
|
325
|
+
if (url.pathname === '/api/health') {
|
|
326
|
+
try {
|
|
327
|
+
const { getSystemHealth } = await import('@rigour-labs/core');
|
|
328
|
+
sendJson(res, 200, await getSystemHealth(cwd));
|
|
329
|
+
}
|
|
330
|
+
catch (e) {
|
|
331
|
+
sendJson(res, 503, { schemaVersion: 1, generatedAt: new Date().toISOString(), error: e.message });
|
|
332
|
+
}
|
|
333
|
+
return true;
|
|
334
|
+
}
|
|
335
|
+
if (url.pathname === '/api/lessons' && req.method === 'GET') {
|
|
336
|
+
try {
|
|
337
|
+
const { listLessons, loadTeamConfiguration } = await import('@rigour-labs/core');
|
|
338
|
+
const [lessons, teamConfiguration] = await Promise.all([
|
|
339
|
+
listLessons(cwd),
|
|
340
|
+
loadTeamConfiguration(),
|
|
341
|
+
]);
|
|
342
|
+
sendJson(res, 200, { schemaVersion: 1, lessons, teamConfigured: Boolean(teamConfiguration) });
|
|
343
|
+
}
|
|
344
|
+
catch (e) {
|
|
345
|
+
sendJson(res, 500, { schemaVersion: 1, lessons: [], teamConfigured: false, error: e.message });
|
|
346
|
+
}
|
|
347
|
+
return true;
|
|
348
|
+
}
|
|
349
|
+
if (url.pathname === '/api/lessons' && req.method === 'POST') {
|
|
350
|
+
let body = '';
|
|
351
|
+
req.on('data', (chunk) => (body += chunk));
|
|
352
|
+
req.on('end', async () => {
|
|
353
|
+
try {
|
|
354
|
+
const payload = JSON.parse(body || '{}');
|
|
355
|
+
const allowedStates = new Set(['validated', 'promoted', 'rejected', 'superseded']);
|
|
356
|
+
if (typeof payload.id !== 'string' || !allowedStates.has(payload.state)) {
|
|
357
|
+
sendJson(res, 400, { error: 'A lesson id and valid target state are required.' });
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
const { transitionLesson } = await import('@rigour-labs/core');
|
|
361
|
+
const publishing = payload.state === 'promoted';
|
|
362
|
+
const changed = await transitionLesson(payload.id, payload.state, {
|
|
363
|
+
visibility: publishing ? 'team' : undefined,
|
|
364
|
+
queueSync: publishing,
|
|
365
|
+
});
|
|
366
|
+
sendJson(res, changed ? 200 : 404, { success: changed });
|
|
367
|
+
}
|
|
368
|
+
catch (e) {
|
|
369
|
+
sendJson(res, 400, { error: e.message });
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
return true;
|
|
373
|
+
}
|
|
374
|
+
if (url.pathname === '/api/agent-history') {
|
|
375
|
+
try {
|
|
376
|
+
const requested = Number(url.searchParams.get('limit') ?? 250);
|
|
377
|
+
const limit = Math.max(1, Math.min(500, Number.isFinite(requested) ? requested : 250));
|
|
378
|
+
const beforeValue = Number(url.searchParams.get('before'));
|
|
379
|
+
const before = Number.isFinite(beforeValue) && beforeValue > 0 ? beforeValue : undefined;
|
|
380
|
+
const page = await readEventPage(eventsPath, limit, before);
|
|
381
|
+
const { buildAgentRuns, normalizeAgentEvents } = await import('@rigour-labs/core');
|
|
382
|
+
const events = normalizeAgentEvents(page.events);
|
|
383
|
+
sendJson(res, 200, {
|
|
384
|
+
schemaVersion: 1,
|
|
385
|
+
events,
|
|
386
|
+
runs: buildAgentRuns(events),
|
|
387
|
+
hasMore: page.hasMore,
|
|
388
|
+
nextBefore: events.length ? Date.parse(events.at(-1).timestamp) : null,
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
catch (e) {
|
|
392
|
+
sendJson(res, 500, { schemaVersion: 1, events: [], runs: [], hasMore: false, error: e.message });
|
|
393
|
+
}
|
|
394
|
+
return true;
|
|
395
|
+
}
|
|
396
|
+
if (url.pathname === '/api/knowledge-graph') {
|
|
397
|
+
try {
|
|
398
|
+
const { buildAgentRuns, buildEngineeringKnowledgeGraph, getRepositoryId, listKnowledgeLessons, normalizeAgentEvents } = await import('@rigour-labs/core');
|
|
399
|
+
const [page, dependencyGraph, lessons, repositoryId, patternIndex, memory] = await Promise.all([
|
|
400
|
+
readEventPage(eventsPath, 2_000),
|
|
401
|
+
readJsonIfExists(path.join(cwd, '.rigour/dependency-graph.json')),
|
|
402
|
+
listKnowledgeLessons(cwd).catch(() => []),
|
|
403
|
+
getRepositoryId(cwd),
|
|
404
|
+
readJsonIfExists(path.join(cwd, '.rigour/patterns.json')),
|
|
405
|
+
mergeMemoryStores(cwd),
|
|
406
|
+
]);
|
|
407
|
+
const events = normalizeAgentEvents(page.events);
|
|
408
|
+
sendJson(res, 200, buildEngineeringKnowledgeGraph({
|
|
409
|
+
repository: { id: repositoryId, name: path.basename(cwd) },
|
|
410
|
+
dependencyGraph,
|
|
411
|
+
events,
|
|
412
|
+
runs: buildAgentRuns(events),
|
|
413
|
+
lessons,
|
|
414
|
+
patterns: Array.isArray(patternIndex?.patterns) ? patternIndex.patterns : [],
|
|
415
|
+
memories: Object.entries(memory.memories).map(([id, value]) => ({
|
|
416
|
+
id,
|
|
417
|
+
label: id,
|
|
418
|
+
source: value?.source,
|
|
419
|
+
detail: value?.type || value?.category || 'retained memory',
|
|
420
|
+
})),
|
|
421
|
+
}));
|
|
422
|
+
}
|
|
423
|
+
catch (e) {
|
|
424
|
+
sendJson(res, 500, { schemaVersion: 1, nodes: [], edges: [], counts: {}, truncated: false, error: e.message });
|
|
425
|
+
}
|
|
426
|
+
return true;
|
|
427
|
+
}
|
|
270
428
|
if (url.pathname === '/api/index-stats') {
|
|
271
429
|
try {
|
|
272
430
|
const indexPath = path.join(cwd, '.rigour/patterns.json');
|
|
@@ -405,7 +563,7 @@ async function handleApiRequest(req, res, url, ctx) {
|
|
|
405
563
|
if (url.pathname === '/api/drift') {
|
|
406
564
|
try {
|
|
407
565
|
const { generateTemporalDriftReport } = await import('@rigour-labs/core');
|
|
408
|
-
const report = generateTemporalDriftReport(cwd);
|
|
566
|
+
const report = await generateTemporalDriftReport(cwd);
|
|
409
567
|
sendJson(res, 200, report || { totalScans: 0 });
|
|
410
568
|
}
|
|
411
569
|
catch {
|
|
@@ -886,6 +1044,18 @@ export const studioCommand = new Command('studio')
|
|
|
886
1044
|
`http://127.0.0.1:${studioPort}`,
|
|
887
1045
|
]);
|
|
888
1046
|
const ctx = { cwd, eventsPath, allowedOrigins };
|
|
1047
|
+
const { ensureAutomaticIndex, loadTeamConfiguration, syncTeamOutbox } = await import('@rigour-labs/core');
|
|
1048
|
+
void ensureAutomaticIndex(cwd).catch((error) => {
|
|
1049
|
+
console.warn(chalk.yellow(`Structural index is degraded: ${error instanceof Error ? error.message : String(error)}`));
|
|
1050
|
+
});
|
|
1051
|
+
const syncTimer = setInterval(() => {
|
|
1052
|
+
void loadTeamConfiguration().then((config) => {
|
|
1053
|
+
if (config)
|
|
1054
|
+
return syncTeamOutbox().catch(() => undefined);
|
|
1055
|
+
return undefined;
|
|
1056
|
+
});
|
|
1057
|
+
}, 30_000);
|
|
1058
|
+
syncTimer.unref();
|
|
889
1059
|
console.log(chalk.bold.cyan('\nš”ļø Launching Rigour Studio...'));
|
|
890
1060
|
console.log(chalk.gray(`Project Root: ${cwd}`));
|
|
891
1061
|
const configPath = path.join(cwd, 'rigour.yml');
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { backfillTeamEmbeddings, doctorTeamConnection, initializeTeamSchema, saveTeamConfiguration, searchTeamKnowledge, syncTeamOutbox, } from '@rigour-labs/core';
|
|
4
|
+
export const teamCommand = new Command('team').description('Configure and diagnose PostgreSQL team learning');
|
|
5
|
+
teamCommand
|
|
6
|
+
.command('init-schema')
|
|
7
|
+
.description('Initialize the PostgreSQL team schema with an administrator connection')
|
|
8
|
+
.requiredOption('--database-url <url>', 'Administrator PostgreSQL URL; remote databases must require TLS')
|
|
9
|
+
.option('--pgvector', 'Enable the optional pgvector semantic knowledge index')
|
|
10
|
+
.action(async (options) => {
|
|
11
|
+
await initializeTeamSchema(options.databaseUrl, { pgvector: Boolean(options.pgvector) });
|
|
12
|
+
console.log(chalk.green(`Rigour team schema initialized${options.pgvector ? ' with pgvector' : ''}.`));
|
|
13
|
+
});
|
|
14
|
+
teamCommand
|
|
15
|
+
.command('configure')
|
|
16
|
+
.description('Configure a provisioned PostgreSQL team database')
|
|
17
|
+
.requiredOption('--database-url <url>', 'PostgreSQL URL; remote databases must require TLS')
|
|
18
|
+
.requiredOption('--organization <id>', 'Organization identifier')
|
|
19
|
+
.requiredOption('--team <id>', 'Team identifier')
|
|
20
|
+
.requiredOption('--actor <id>', 'Actor identifier provisioned for the database role')
|
|
21
|
+
.option('--initialize-schema', 'Create or update the Rigour schema (administrator only)')
|
|
22
|
+
.option('--pgvector', 'Use pgvector to rank team knowledge semantically')
|
|
23
|
+
.action(async (options) => {
|
|
24
|
+
if (options.initializeSchema)
|
|
25
|
+
await initializeTeamSchema(options.databaseUrl, { pgvector: Boolean(options.pgvector) });
|
|
26
|
+
const config = {
|
|
27
|
+
databaseUrl: options.databaseUrl,
|
|
28
|
+
organizationId: options.organization,
|
|
29
|
+
teamId: options.team,
|
|
30
|
+
actorId: options.actor,
|
|
31
|
+
semantic: options.pgvector ? {
|
|
32
|
+
provider: 'pgvector',
|
|
33
|
+
model: 'Xenova/all-MiniLM-L6-v2',
|
|
34
|
+
dimensions: 384,
|
|
35
|
+
} : undefined,
|
|
36
|
+
};
|
|
37
|
+
const result = await doctorTeamConnection(config);
|
|
38
|
+
await saveTeamConfiguration(config);
|
|
39
|
+
console.log(chalk.green(result.message));
|
|
40
|
+
});
|
|
41
|
+
teamCommand
|
|
42
|
+
.command('doctor')
|
|
43
|
+
.description('Verify team database TLS, schema, role, and membership')
|
|
44
|
+
.action(async () => {
|
|
45
|
+
const result = await doctorTeamConnection();
|
|
46
|
+
console.log(JSON.stringify(result, null, 2));
|
|
47
|
+
if (result.connectivity === 'offline')
|
|
48
|
+
process.exitCode = 1;
|
|
49
|
+
});
|
|
50
|
+
teamCommand
|
|
51
|
+
.command('semantic-backfill')
|
|
52
|
+
.description('Embed existing validated knowledge owned by the configured actor')
|
|
53
|
+
.option('--limit <count>', 'Maximum lessons to embed in this run', '200')
|
|
54
|
+
.action(async (options) => {
|
|
55
|
+
const limit = Number.parseInt(options.limit, 10);
|
|
56
|
+
if (!Number.isInteger(limit) || limit < 1)
|
|
57
|
+
throw new Error('--limit must be a positive integer.');
|
|
58
|
+
console.log(JSON.stringify(await backfillTeamEmbeddings(limit), null, 2));
|
|
59
|
+
});
|
|
60
|
+
teamCommand
|
|
61
|
+
.command('semantic-search')
|
|
62
|
+
.description('Diagnose advisory cross-repository knowledge recall')
|
|
63
|
+
.argument('<query>', 'Natural-language engineering question')
|
|
64
|
+
.option('--limit <count>', 'Maximum candidates to return', '8')
|
|
65
|
+
.action(async (query, options) => {
|
|
66
|
+
const limit = Number.parseInt(options.limit, 10);
|
|
67
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 25)
|
|
68
|
+
throw new Error('--limit must be between 1 and 25.');
|
|
69
|
+
console.log(JSON.stringify(await searchTeamKnowledge(query, limit), null, 2));
|
|
70
|
+
});
|
|
71
|
+
teamCommand
|
|
72
|
+
.command('sync')
|
|
73
|
+
.description('Synchronize approved team lessons from the offline outbox')
|
|
74
|
+
.option('--dry-run', 'Report pending records without sending them')
|
|
75
|
+
.action(async (options) => {
|
|
76
|
+
const result = await syncTeamOutbox({ dryRun: Boolean(options.dryRun) });
|
|
77
|
+
console.log(JSON.stringify(result, null, 2));
|
|
78
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rigour-labs/cli",
|
|
3
|
-
"version": "6.0.
|
|
3
|
+
"version": "6.1.0-beta.2",
|
|
4
4
|
"description": "AI-native quality gates with local LLM analysis. Forces AI agents (Claude, Cursor, Copilot, Cline, Windsurf) to meet engineering standards. Bayesian Brain learns your codebase. Zero config: npx rigour-scan.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://rigour.run",
|
|
@@ -53,12 +53,13 @@
|
|
|
53
53
|
"inquirer": "9.2.16",
|
|
54
54
|
"ora": "^8.0.1",
|
|
55
55
|
"yaml": "^2.8.2",
|
|
56
|
-
"@rigour-labs/core": "6.0.
|
|
56
|
+
"@rigour-labs/core": "6.1.0-beta.2"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"@types/fs-extra": "^11.0.4",
|
|
60
60
|
"@types/inquirer": "9.0.7",
|
|
61
|
-
"@types/node": "^25.0.3"
|
|
61
|
+
"@types/node": "^25.0.3",
|
|
62
|
+
"@rigour-labs/studio": "6.1.0-beta.2"
|
|
62
63
|
},
|
|
63
64
|
"scripts": {
|
|
64
65
|
"build": "tsc && pnpm bundle-studio",
|