@rigour-labs/cli 5.5.4 ā 6.1.0-beta.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/dist/cli.js +25 -0
- package/dist/commands/firewall.d.ts +9 -0
- package/dist/commands/firewall.js +106 -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 +279 -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-GVDvbvMe.js +0 -362
- package/studio-dist/assets/index-jC3k12bf.css +0 -1
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 {
|
|
@@ -719,12 +877,97 @@ async function handleApiRequest(req, res, url, ctx) {
|
|
|
719
877
|
});
|
|
720
878
|
return true;
|
|
721
879
|
}
|
|
880
|
+
if (url.pathname === '/api/firewall') {
|
|
881
|
+
try {
|
|
882
|
+
const { loadCurrentTransaction, listTransactions, loadLatestAttestation, verifyAttestation, } = await import('@rigour-labs/core');
|
|
883
|
+
const current = await loadCurrentTransaction(cwd);
|
|
884
|
+
const transactions = await listTransactions(cwd);
|
|
885
|
+
const attestation = await loadLatestAttestation(cwd);
|
|
886
|
+
const attestationValid = attestation ? await verifyAttestation(cwd, attestation) : false;
|
|
887
|
+
const advPath = path.join(cwd, '.rigour/adversarial-report.json');
|
|
888
|
+
const adversarial = await fs.pathExists(advPath) ? await fs.readJson(advPath) : null;
|
|
889
|
+
const decisionsPath = path.join(cwd, '.rigour/firewall-decisions.jsonl');
|
|
890
|
+
let decisions = [];
|
|
891
|
+
if (await fs.pathExists(decisionsPath)) {
|
|
892
|
+
const content = await fs.readFile(decisionsPath, 'utf8');
|
|
893
|
+
decisions = content
|
|
894
|
+
.split('\n')
|
|
895
|
+
.filter((l) => l.trim())
|
|
896
|
+
.slice(-100)
|
|
897
|
+
.map((l) => {
|
|
898
|
+
try {
|
|
899
|
+
return JSON.parse(l);
|
|
900
|
+
}
|
|
901
|
+
catch {
|
|
902
|
+
return null;
|
|
903
|
+
}
|
|
904
|
+
})
|
|
905
|
+
.filter(Boolean)
|
|
906
|
+
.reverse();
|
|
907
|
+
}
|
|
908
|
+
let recentDenies = [];
|
|
909
|
+
if (await fs.pathExists(eventsPath)) {
|
|
910
|
+
const content = await fs.readFile(eventsPath, 'utf8');
|
|
911
|
+
recentDenies = content
|
|
912
|
+
.split('\n')
|
|
913
|
+
.filter((l) => l.trim())
|
|
914
|
+
.map((l) => {
|
|
915
|
+
try {
|
|
916
|
+
return JSON.parse(l);
|
|
917
|
+
}
|
|
918
|
+
catch {
|
|
919
|
+
return null;
|
|
920
|
+
}
|
|
921
|
+
})
|
|
922
|
+
.filter((e) => e && (e.type === 'firewall_deny' || e.decision === 'timeout-deny' || e.decision === 'deny'))
|
|
923
|
+
.slice(-50)
|
|
924
|
+
.reverse();
|
|
925
|
+
}
|
|
926
|
+
const hooksPresent = (await fs.pathExists(path.join(cwd, '.cursor/hooks.json'))) ||
|
|
927
|
+
(await fs.pathExists(path.join(cwd, '.claude/settings.json'))) ||
|
|
928
|
+
(await fs.pathExists(path.join(cwd, '.clinerules'))) ||
|
|
929
|
+
(await fs.pathExists(path.join(cwd, '.windsurf/hooks.json')));
|
|
930
|
+
const agentScopesPath = path.join(cwd, '.rigour/agent-session.json');
|
|
931
|
+
const agentSession = await fs.pathExists(agentScopesPath) ? await fs.readJson(agentScopesPath) : null;
|
|
932
|
+
const scopeActive = Array.isArray(agentSession?.agents) && agentSession.agents.length > 0;
|
|
933
|
+
const typedSeen = recentDenies.some((e) => e.ruleId?.startsWith?.('shell.') || e.tool === 'rigour_run');
|
|
934
|
+
const gatewayWired = false; // McpGateway not yet the MCP proxy path
|
|
935
|
+
sendJson(res, 200, {
|
|
936
|
+
current,
|
|
937
|
+
transactions: transactions.slice(0, 20),
|
|
938
|
+
attestation,
|
|
939
|
+
attestationValid,
|
|
940
|
+
adversarial,
|
|
941
|
+
decisions,
|
|
942
|
+
recentDenies,
|
|
943
|
+
failClosed: true,
|
|
944
|
+
mediation: {
|
|
945
|
+
status: gatewayWired && hooksPresent ? 'full' : 'partial',
|
|
946
|
+
typedCommands: typedSeen || hooksPresent ? 'rigour_run_only' : 'not_observed',
|
|
947
|
+
scopeEnforcement: scopeActive ? 'requires_agent_id' : 'inactive',
|
|
948
|
+
arbitration: 'fail-closed',
|
|
949
|
+
hooksInstalled: hooksPresent,
|
|
950
|
+
mcpGateway: gatewayWired,
|
|
951
|
+
},
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
catch (e) {
|
|
955
|
+
sendJson(res, 500, { error: e.message });
|
|
956
|
+
}
|
|
957
|
+
return true;
|
|
958
|
+
}
|
|
722
959
|
if (url.pathname === '/api/arbitrate' && req.method === 'POST') {
|
|
723
960
|
let body = '';
|
|
724
961
|
req.on('data', (chunk) => (body += chunk));
|
|
725
962
|
req.on('end', async () => {
|
|
726
963
|
try {
|
|
727
964
|
const decision = JSON.parse(body);
|
|
965
|
+
const { consumeArbitrationToken } = await import('@rigour-labs/core');
|
|
966
|
+
const ok = await consumeArbitrationToken(cwd, decision.requestId, decision.token);
|
|
967
|
+
if (!ok) {
|
|
968
|
+
sendJson(res, 403, { error: 'Invalid or missing arbitration token (one-time, fail-closed)' });
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
728
971
|
const logEntry = JSON.stringify({
|
|
729
972
|
id: randomUUID(),
|
|
730
973
|
timestamp: new Date().toISOString(),
|
|
@@ -801,6 +1044,18 @@ export const studioCommand = new Command('studio')
|
|
|
801
1044
|
`http://127.0.0.1:${studioPort}`,
|
|
802
1045
|
]);
|
|
803
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();
|
|
804
1059
|
console.log(chalk.bold.cyan('\nš”ļø Launching Rigour Studio...'));
|
|
805
1060
|
console.log(chalk.gray(`Project Root: ${cwd}`));
|
|
806
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": "
|
|
3
|
+
"version": "6.1.0-beta.1",
|
|
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": "
|
|
56
|
+
"@rigour-labs/core": "6.1.0-beta.1"
|
|
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.1"
|
|
62
63
|
},
|
|
63
64
|
"scripts": {
|
|
64
65
|
"build": "tsc && pnpm bundle-studio",
|