@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 CHANGED
@@ -19,6 +19,8 @@ import { deepStatsCommand } from './commands/deep-stats.js';
19
19
  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
+ import { firewallTransactCommand, firewallAdversarialCommand, firewallAdmitCommand, firewallStatusCommand } from './commands/firewall.js';
23
+ import { teamCommand } from './commands/team.js';
22
24
  import { checkForUpdates } from './utils/version.js';
23
25
  import { getCliVersion } from './utils/cli-version.js';
24
26
  import chalk from 'chalk';
@@ -28,6 +30,7 @@ program.addCommand(indexCommand);
28
30
  program.addCommand(studioCommand);
29
31
  program.addCommand(brainCommand);
30
32
  program.addCommand(deepStatsCommand);
33
+ program.addCommand(teamCommand);
31
34
  program
32
35
  .name('rigour')
33
36
  .description('🛡️ Rigour: The Quality Gate Loop for AI-Assisted Engineering')
@@ -363,6 +366,28 @@ settingsCmd
363
366
  .command('path')
364
367
  .description('Show settings file path')
365
368
  .action(async () => { await settingsPathCommand(); });
369
+ const firewallCmd = program
370
+ .command('firewall')
371
+ .description('Agent Transaction Firewall — mediate, attest, and prove damage bounds');
372
+ firewallCmd
373
+ .command('status')
374
+ .description('Show transaction, attestation, and adversarial status')
375
+ .action(async () => { await firewallStatusCommand(process.cwd()); });
376
+ firewallCmd
377
+ .command('transact')
378
+ .description('Start a mediated transaction, verify gates, COMMIT or DISCARD')
379
+ .option('--agent <id>', 'Agent id for scope binding')
380
+ .option('--scope <globs>', 'Comma-separated allowed path globs', '**/*')
381
+ .option('--discard', 'Discard the current transaction worktree')
382
+ .action(async (options) => { await firewallTransactCommand(process.cwd(), options); });
383
+ firewallCmd
384
+ .command('adversarial')
385
+ .description('Replay deterministic adversarial corpus against the firewall kernel')
386
+ .action(async () => { await firewallAdversarialCommand(process.cwd()); });
387
+ firewallCmd
388
+ .command('admit')
389
+ .description('CI admission: require valid signed attestation with PASS gates')
390
+ .action(async () => { await firewallAdmitCommand(process.cwd()); });
366
391
  // Check for updates before parsing (non-blocking)
367
392
  (async () => {
368
393
  try {
@@ -0,0 +1,9 @@
1
+ export declare function firewallTransactCommand(cwd: string, options: {
2
+ agent?: string;
3
+ scope?: string;
4
+ discard?: boolean;
5
+ commit?: boolean;
6
+ }): Promise<void>;
7
+ export declare function firewallAdversarialCommand(cwd: string): Promise<void>;
8
+ export declare function firewallAdmitCommand(cwd: string): Promise<void>;
9
+ export declare function firewallStatusCommand(cwd: string): Promise<void>;
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Firewall CLI — transact, adversarial replay, CI admit.
3
+ */
4
+ import chalk from 'chalk';
5
+ import fs from 'fs-extra';
6
+ import path from 'path';
7
+ import yaml from 'yaml';
8
+ import { TransactionRunner, runAdversarialCorpus, persistAdversarialReport, createAttestation, admitForCi, loadLatestAttestation, verifyAttestation, loadCurrentTransaction, listTransactions, GateRunner, ConfigSchema, loadAgentScopesFromDisk, } from '@rigour-labs/core';
9
+ export async function firewallTransactCommand(cwd, options) {
10
+ if (options.discard) {
11
+ const current = await loadCurrentTransaction(cwd);
12
+ if (!current) {
13
+ console.error(chalk.red('No current transaction to discard'));
14
+ process.exit(1);
15
+ }
16
+ const discarded = await TransactionRunner.fromRecord(cwd, current).discard();
17
+ console.log(chalk.yellow(`DISCARD ${discarded.id}`));
18
+ return;
19
+ }
20
+ const registered = await loadAgentScopesFromDisk(cwd);
21
+ if (registered.length > 0 && !options.agent) {
22
+ console.error(chalk.red('Agent scopes are registered — pass --agent <id> to bind the writer (fail-closed)'));
23
+ process.exit(1);
24
+ }
25
+ const scope = (options.scope || '**/*').split(',').map(s => s.trim()).filter(Boolean);
26
+ const tx = new TransactionRunner(cwd, { agentId: options.agent, scope });
27
+ const started = await tx.start();
28
+ console.log(chalk.cyan(`START transaction ${started.id}`));
29
+ const gateCwd = started.worktreePath || cwd;
30
+ if (started.worktreePath) {
31
+ console.log(chalk.dim(`Worktree: ${started.worktreePath}`));
32
+ }
33
+ try {
34
+ await tx.syncWorktreeChanges();
35
+ }
36
+ catch (e) {
37
+ await tx.discard();
38
+ console.error(chalk.red(`DISCARD — scope/budget: ${e.message}`));
39
+ process.exit(1);
40
+ }
41
+ const configPath = path.join(cwd, 'rigour.yml');
42
+ let config = ConfigSchema.parse({ version: 1 });
43
+ if (await fs.pathExists(configPath)) {
44
+ config = ConfigSchema.parse(yaml.parse(await fs.readFile(configPath, 'utf-8')));
45
+ }
46
+ const runner = new GateRunner(config);
47
+ const { ok, gateResults } = await tx.verify(async () => {
48
+ const report = await runner.run(gateCwd);
49
+ const failedGates = [...new Set(report.failures.map(f => f.id))];
50
+ return { status: report.status, failedGates, score: report.stats.score };
51
+ });
52
+ if (!ok) {
53
+ const discarded = await tx.discard();
54
+ console.log(chalk.red(`DISCARD ${discarded.id} — gates ${gateResults.status}`));
55
+ process.exit(1);
56
+ }
57
+ const committed = await tx.commit();
58
+ const attestation = await createAttestation(cwd, {
59
+ transaction: committed,
60
+ gateResults: {
61
+ status: gateResults.status,
62
+ score: gateResults.score,
63
+ failedGates: gateResults.failedGates,
64
+ },
65
+ verifyRoot: gateCwd,
66
+ });
67
+ console.log(chalk.green(`COMMIT ${committed.id}`));
68
+ console.log(chalk.green(`Attestation ${attestation.signature.slice(0, 12)}… tree=${(attestation.treeDigest || '').slice(0, 12)} policy=${attestation.policyHash}`));
69
+ }
70
+ export async function firewallAdversarialCommand(cwd) {
71
+ const report = runAdversarialCorpus();
72
+ const out = await persistAdversarialReport(cwd, report);
73
+ console.log(chalk.bold(`Adversarial replay: ${report.passed} passed, ${report.failed} failed`));
74
+ for (const r of report.results) {
75
+ const mark = r.passed ? chalk.green('PASS') : chalk.red('FAIL');
76
+ console.log(` ${mark} ${r.caseId}: expected=${r.expected} actual=${r.actual} — ${r.reason}`);
77
+ if (r.suggestedRule) {
78
+ console.log(chalk.yellow(' suggested regression:\n') + chalk.dim(r.suggestedRule));
79
+ }
80
+ }
81
+ console.log(chalk.dim(`Report: ${out}`));
82
+ if (report.failed > 0)
83
+ process.exit(1);
84
+ }
85
+ export async function firewallAdmitCommand(cwd) {
86
+ const result = await admitForCi(cwd);
87
+ if (!result.admit) {
88
+ console.error(chalk.red(`ADMIT DENIED: ${result.reason}`));
89
+ process.exit(1);
90
+ }
91
+ const bundle = await loadLatestAttestation(cwd);
92
+ const valid = bundle ? await verifyAttestation(cwd, bundle) : false;
93
+ console.log(chalk.green(`ADMIT OK: ${result.reason} (signature ${valid ? 'valid' : 'n/a'})`));
94
+ }
95
+ export async function firewallStatusCommand(cwd) {
96
+ const current = await loadCurrentTransaction(cwd);
97
+ const txs = await listTransactions(cwd);
98
+ const attestation = await loadLatestAttestation(cwd);
99
+ const advPath = path.join(cwd, '.rigour', 'adversarial-report.json');
100
+ const adv = await fs.pathExists(advPath) ? await fs.readJson(advPath) : null;
101
+ console.log(chalk.bold('Firewall status'));
102
+ console.log(` Current TX: ${current ? `${current.id} (${current.status})` : 'none'}`);
103
+ console.log(` Transactions: ${txs.length}`);
104
+ console.log(` Attestation: ${attestation ? `${attestation.transactionId} gates=${attestation.gateResults.status}` : 'none'}`);
105
+ console.log(` Adversarial: ${adv ? `${adv.passed} pass / ${adv.failed} fail` : 'not run'}`);
106
+ }
@@ -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
+ });