@rigour-labs/cli 6.0.0 → 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 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')
@@ -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 { runHookChecker, scanInputForCredentials, formatDLPAlert, createDLPAuditEntry, writeDLPBlockManifest, allowLastDLPBlock } from '@rigour-labs/core';
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') {
@@ -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('-s, --semantic', 'Generate semantic embeddings for better matching (requires Transformers.js)', false)
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
+ });
@@ -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 content = await fs.readFile(eventsPath, 'utf8');
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
- const watcher = fs.watch(path.dirname(eventsPath), async (_eventType, filename) => {
137
- if (filename === 'events.jsonl') {
138
- try {
139
- const content = await fs.readFile(eventsPath, 'utf8');
140
- const lines = content.split('\n').filter((l) => l.trim());
141
- const lastLine = lines[lines.length - 1];
142
- if (lastLine)
143
- res.write(`data: ${lastLine}\n\n`);
144
- }
145
- catch {
146
- // ignore transient reads
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
- req.on('close', () => watcher.close());
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
- if (!absolutePath.startsWith(cwd)) {
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 content = await fs.readFile(absolutePath, 'utf8');
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,2 @@
1
+ import { Command } from 'commander';
2
+ export declare const teamCommand: Command;
@@ -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.0",
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": "6.0.0"
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",