@rigstate/cli 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/.env.example +5 -0
  2. package/IMPLEMENTATION.md +239 -0
  3. package/QUICK_START.md +220 -0
  4. package/README.md +150 -0
  5. package/dist/index.cjs +3987 -0
  6. package/dist/index.cjs.map +1 -0
  7. package/dist/index.d.cts +1 -0
  8. package/dist/index.d.ts +1 -0
  9. package/dist/index.js +3964 -0
  10. package/dist/index.js.map +1 -0
  11. package/install.sh +15 -0
  12. package/package.json +53 -0
  13. package/src/commands/check.ts +329 -0
  14. package/src/commands/config.ts +81 -0
  15. package/src/commands/daemon.ts +197 -0
  16. package/src/commands/env.ts +158 -0
  17. package/src/commands/fix.ts +140 -0
  18. package/src/commands/focus.ts +134 -0
  19. package/src/commands/hooks.ts +163 -0
  20. package/src/commands/init.ts +282 -0
  21. package/src/commands/link.ts +45 -0
  22. package/src/commands/login.ts +35 -0
  23. package/src/commands/mcp.ts +73 -0
  24. package/src/commands/nexus.ts +81 -0
  25. package/src/commands/override.ts +65 -0
  26. package/src/commands/scan.ts +242 -0
  27. package/src/commands/sync-rules.ts +191 -0
  28. package/src/commands/sync.ts +339 -0
  29. package/src/commands/watch.ts +283 -0
  30. package/src/commands/work.ts +172 -0
  31. package/src/daemon/bridge-listener.ts +127 -0
  32. package/src/daemon/core.ts +184 -0
  33. package/src/daemon/factory.ts +45 -0
  34. package/src/daemon/file-watcher.ts +97 -0
  35. package/src/daemon/guardian-monitor.ts +133 -0
  36. package/src/daemon/heuristic-engine.ts +203 -0
  37. package/src/daemon/intervention-protocol.ts +128 -0
  38. package/src/daemon/telemetry.ts +23 -0
  39. package/src/daemon/types.ts +18 -0
  40. package/src/hive/gateway.ts +74 -0
  41. package/src/hive/protocol.ts +29 -0
  42. package/src/hive/scrubber.ts +72 -0
  43. package/src/index.ts +85 -0
  44. package/src/nexus/council.ts +103 -0
  45. package/src/nexus/dispatcher.ts +133 -0
  46. package/src/utils/config.ts +83 -0
  47. package/src/utils/files.ts +95 -0
  48. package/src/utils/governance.ts +128 -0
  49. package/src/utils/logger.ts +66 -0
  50. package/src/utils/manifest.ts +18 -0
  51. package/src/utils/rule-engine.ts +292 -0
  52. package/src/utils/skills-provisioner.ts +153 -0
  53. package/src/utils/version.ts +1 -0
  54. package/src/utils/watchdog.ts +215 -0
  55. package/tsconfig.json +29 -0
  56. package/tsup.config.ts +11 -0
package/src/index.ts ADDED
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import chalk from 'chalk';
5
+ import { createLoginCommand } from './commands/login.js';
6
+ import { createLinkCommand } from './commands/link.js';
7
+ import { createScanCommand } from './commands/scan.js';
8
+ import { createFixCommand } from './commands/fix.js';
9
+ import { createSyncCommand } from './commands/sync.js';
10
+ import { createInitCommand } from './commands/init.js';
11
+ import { createCheckCommand } from './commands/check.js';
12
+ import { createHooksCommand } from './commands/hooks.js';
13
+ import { createDaemonCommand } from './commands/daemon.js';
14
+ import { createWorkCommand } from './commands/work.js';
15
+ import { createWatchCommand } from './commands/watch.js';
16
+ import { createFocusCommand } from './commands/focus.js';
17
+ import { createEnvPullCommand } from './commands/env.js';
18
+ import { createConfigCommand } from './commands/config.js';
19
+ import { createMcpCommand } from './commands/mcp.js';
20
+ import { createNexusCommand } from './commands/nexus.js';
21
+ import { createSyncRulesCommand } from './commands/sync-rules.js';
22
+ import { createOverrideCommand } from './commands/override.js';
23
+ import { checkVersion } from './utils/version.js';
24
+ import dotenv from 'dotenv';
25
+
26
+ // Load environment variables
27
+ dotenv.config();
28
+
29
+ const program = new Command();
30
+
31
+ program
32
+ .name('rigstate')
33
+ .description('CLI for Rigstate - The AI-Native Dev Studio')
34
+ .version('0.2.0');
35
+
36
+ // Register commands
37
+ program.addCommand(createLoginCommand());
38
+ program.addCommand(createLinkCommand());
39
+ program.addCommand(createScanCommand());
40
+ program.addCommand(createFixCommand());
41
+ program.addCommand(createSyncCommand());
42
+ program.addCommand(createInitCommand());
43
+ program.addCommand(createCheckCommand());
44
+ program.addCommand(createHooksCommand());
45
+ program.addCommand(createDaemonCommand());
46
+ program.addCommand(createWorkCommand());
47
+ program.addCommand(createWatchCommand());
48
+ program.addCommand(createFocusCommand());
49
+ program.addCommand(createEnvPullCommand());
50
+ program.addCommand(createConfigCommand());
51
+ program.addCommand(createMcpCommand());
52
+ program.addCommand(createNexusCommand());
53
+ program.addCommand(createSyncRulesCommand());
54
+ program.addCommand(createOverrideCommand());
55
+
56
+ program.hook('preAction', async () => {
57
+ await checkVersion();
58
+ });
59
+
60
+ // Add helpful examples
61
+ program.on('--help', () => {
62
+ console.log('');
63
+ console.log(chalk.bold('Examples:'));
64
+ console.log('');
65
+ console.log(chalk.cyan(' $ rigstate login sk_rigstate_your_api_key'));
66
+ console.log(chalk.dim(' Authenticate with your Rigstate API key'));
67
+ console.log('');
68
+ console.log(chalk.cyan(' $ rigstate scan'));
69
+ console.log(chalk.dim(' Scan the current directory'));
70
+ console.log('');
71
+ console.log(chalk.cyan(' $ rigstate scan ./src --project abc123'));
72
+ console.log(chalk.dim(' Scan a specific directory with project ID'));
73
+ console.log('');
74
+ console.log(chalk.cyan(' $ rigstate scan --json'));
75
+ console.log(chalk.dim(' Output results in JSON format (useful for IDE extensions)'));
76
+ console.log('');
77
+ });
78
+
79
+ // Parse arguments
80
+ program.parse(process.argv);
81
+
82
+ // Show help if no command provided
83
+ if (!process.argv.slice(2).length) {
84
+ program.outputHelp();
85
+ }
@@ -0,0 +1,103 @@
1
+
2
+ import { SwarmAgent, ServiceOrder } from '@rigstate/shared';
3
+
4
+ export type AgentWeight = number;
5
+
6
+ /**
7
+ * THE HIERARCHY OF RIGSTATE
8
+ * Defines the decision power of each agent in The Council.
9
+ */
10
+ export const AGENT_WEIGHTS: Record<SwarmAgent, AgentWeight> = {
11
+ 'FRANK': 100, // SUPREME COURT (Security/Architecture) - Veto Power
12
+ 'SVEN': 95, // SENTINEL (Compliance/Audit) - Highly Authoritative
13
+ 'SINDRE': 80, // VAULT KEEPER (Data Integrity) - High Authority on Data
14
+ 'MAJA': 70, // SCRIBE (Documentation) - Advisory
15
+ 'EITRI': 60 // BUILDER (Implementation) - Mutable, subservient to Architects
16
+ };
17
+
18
+ export interface Conflict {
19
+ id: string;
20
+ proposal: ServiceOrder;
21
+ objector: SwarmAgent;
22
+ reason: string;
23
+ severity: 'CRITICAL' | 'WARNING' | 'SUGGESTION';
24
+ alternativeProposal?: any;
25
+ }
26
+
27
+ export interface CouncilResolution {
28
+ outcome: 'UPHELD' | 'OVERRULED' | 'COMPROMISE';
29
+ winner: SwarmAgent;
30
+ action: 'BLOCK' | 'PROCEED' | 'MODIFY';
31
+ rationale: string;
32
+ }
33
+
34
+ /**
35
+ * THE COUNCIL v2
36
+ * Resolves disputes between agents using Weighted Democracy.
37
+ */
38
+ export class Council {
39
+
40
+ /**
41
+ * Judges a conflict based on Agent weights and Hierarchy.
42
+ */
43
+ public resolve(conflict: Conflict): CouncilResolution {
44
+ const proposer = conflict.proposal.targetAgent; // The agent doing the work (e.g. Eitri)
45
+ const objector = conflict.objector; // The agent complaining (e.g. Sven)
46
+
47
+ const proposerWeight = AGENT_WEIGHTS[proposer] || 50;
48
+ const objectorWeight = AGENT_WEIGHTS[objector] || 50;
49
+
50
+ console.log(`⚖️ COUNCIL IN SESSION: ${objector} (${objectorWeight}) objects to ${proposer} (${proposerWeight})`);
51
+
52
+ // 1. ABSOLUTE VETO (Frank/Sven on Critical Issues)
53
+ if ((objector === 'FRANK' || objector === 'SVEN') && conflict.severity === 'CRITICAL') {
54
+ return {
55
+ outcome: 'UPHELD',
56
+ winner: objector,
57
+ action: 'BLOCK',
58
+ rationale: `Absolute Veto by ${objector} on CRITICAL violation: ${conflict.reason}`
59
+ };
60
+ }
61
+
62
+ // 2. SUGGESTION OVERRIDE (Pragmatism)
63
+ // If it's just a suggestion, the Builder (Proposer) usually proceeds unless it's the Supreme Court (Frank).
64
+ if (conflict.severity === 'SUGGESTION' && objector !== 'FRANK') {
65
+ return {
66
+ outcome: 'OVERRULED',
67
+ winner: proposer,
68
+ action: 'PROCEED',
69
+ rationale: `Minor suggestion by ${objector} noted but overruled for velocity.`
70
+ };
71
+ }
72
+
73
+ // 3. WEIGHT COMPARISON
74
+ if (objectorWeight > proposerWeight) {
75
+ return {
76
+ outcome: 'UPHELD',
77
+ winner: objector,
78
+ action: 'BLOCK',
79
+ rationale: `${objector} outranks ${proposer}. Objection validated.`
80
+ };
81
+ }
82
+
83
+ // 4. TIE-BREAKER / LOWER AUTHORITY
84
+ // If weights are equal or objector is lower, we generally favor the builder (Proposer)
85
+ // unless it's a security suggestion.
86
+ if (conflict.severity === 'SUGGESTION' && proposer === 'EITRI') {
87
+ return {
88
+ outcome: 'OVERRULED',
89
+ winner: proposer,
90
+ action: 'PROCEED',
91
+ rationale: `${proposer} has implementation authority on non-critical suggestions.`
92
+ };
93
+ }
94
+
95
+ // Default: Proposer wins if they outrank objector
96
+ return {
97
+ outcome: 'OVERRULED',
98
+ winner: proposer,
99
+ action: 'PROCEED',
100
+ rationale: `Objection overruled. ${proposer} retains authority.`
101
+ };
102
+ }
103
+ }
@@ -0,0 +1,133 @@
1
+
2
+ import EventEmitter from 'events';
3
+ import { ServiceOrder, SwarmAgent, NexusContext } from '@rigstate/shared';
4
+ import { v4 as uuidv4 } from 'uuid';
5
+
6
+ import { HiveGateway } from '../hive/gateway';
7
+ import { Logger } from '../utils/logger';
8
+
9
+ /**
10
+ * THE NEXUS DISPATCHER
11
+ * "The Brain Stem" of Rigstate.
12
+ * Routes ServiceOrders between agents and enforces the Human Kill-Switch.
13
+ */
14
+ export class NexusDispatcher extends EventEmitter {
15
+ private context: NexusContext;
16
+ private orderQueue: ServiceOrder[] = [];
17
+ private orderHistory: ServiceOrder[] = [];
18
+ private gateway: HiveGateway;
19
+
20
+ constructor(context: NexusContext) {
21
+ super();
22
+ this.context = context;
23
+ this.gateway = new HiveGateway(
24
+ process.env.RIGSTATE_HIVE_URL || 'https://rigstate.com/api/hive',
25
+ process.env.RIGSTATE_HIVE_TOKEN
26
+ );
27
+ Logger.info(`🧠 NEXUS DISPATCHER ONLINE. Context: ${context.projectId} (DryRun: ${context.dryRun})`);
28
+ }
29
+
30
+ /**
31
+ * Creates a new Service Order and routes it.
32
+ */
33
+ public async dispatch(
34
+ source: SwarmAgent,
35
+ target: SwarmAgent,
36
+ intent: string,
37
+ action: string,
38
+ params: Record<string, any>,
39
+ constraints: string[] = []
40
+ ): Promise<ServiceOrder> {
41
+
42
+ const order: ServiceOrder = {
43
+ id: uuidv4(),
44
+ traceId: uuidv4(), // TODO: Inherit traceId if chained
45
+ sourceAgent: source,
46
+ targetAgent: target,
47
+ priority: 'NORMAL',
48
+ intent,
49
+ action,
50
+ parameters: params,
51
+ constraints,
52
+ status: 'PENDING',
53
+ createdAt: new Date().toISOString()
54
+ };
55
+
56
+ this.orderQueue.push(order);
57
+ this.emit('order:created', order);
58
+
59
+ // Security / Kill-Switch Check
60
+ // EITRI (The Smith) is the only one who can hammer the metal (write files)
61
+ if (target === 'EITRI' && order.action.startsWith('fs.write')) {
62
+ if (this.context.dryRun) {
63
+ Logger.info(`🛑 NEXUS KILL-SWITCH: Order ${order.id} blocked by Dry-Run protocol.`);
64
+ order.status = 'PENDING'; // Kept as PENDING but blocked
65
+ // Ideally status should represent AWAITING_APPROVAL explicitly
66
+ // But for now strict dry-run just prevents execution
67
+ this.emit('order:blocked', order);
68
+ return order;
69
+ }
70
+ }
71
+
72
+ // If automatic or dry-run disabled
73
+ return this.executeOrder(order);
74
+ }
75
+
76
+ /**
77
+ * Executes the order (simulated for now, essentially "Sending" it)
78
+ */
79
+ private async executeOrder(order: ServiceOrder): Promise<ServiceOrder> {
80
+ order.status = 'EXECUTING';
81
+ order.startedAt = new Date().toISOString();
82
+ this.emit('order:started', order);
83
+
84
+ try {
85
+ Logger.info(`🚀 NEXUS: Routing Order ${order.id} [${order.sourceAgent} -> ${order.targetAgent}]: ${order.intent}`);
86
+
87
+ // SPECIAL ROUTING: HIVE UPLINK
88
+ if (order.targetAgent === 'MAJA' && order.action === 'HIVE_TRANSMIT') {
89
+ const signal = order.parameters.signal;
90
+ if (signal) {
91
+ await this.gateway.transmit(signal);
92
+ order.status = 'COMPLETED';
93
+ return order;
94
+ }
95
+ }
96
+
97
+ // Here we would actually call the Agent's handler function
98
+ // For now, we just emit the specific event for listeners
99
+ this.emit(`agent:${order.targetAgent}`, order);
100
+
101
+ // Simulation of async completion would happen via callback/promise resolution elsewhere
102
+ return order;
103
+
104
+ } catch (error: unknown) {
105
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
106
+ Logger.error(`Dispatch failed for order ${order.id}`, error);
107
+
108
+ order.status = 'FAILED';
109
+ order.error = {
110
+ code: 'DISPATCH_ERROR',
111
+ message: errorMessage
112
+ };
113
+ this.emit('order:failed', order);
114
+ return order;
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Human Approval (The "Red Button")
120
+ */
121
+ public async approveOrder(orderId: string): Promise<void> {
122
+ const order = this.orderQueue.find(o => o.id === orderId);
123
+ if (!order) throw new Error(`Order ${orderId} not found`);
124
+
125
+ if (order.status !== 'AWAITING_APPROVAL') {
126
+ Logger.warn(`Order ${orderId} is not awaiting approval (Status: ${order.status})`);
127
+ return;
128
+ }
129
+
130
+ Logger.info(`✅ HUMAN APPROVED Order ${orderId}`);
131
+ await this.executeOrder(order);
132
+ }
133
+ }
@@ -0,0 +1,83 @@
1
+ import Conf from 'conf';
2
+
3
+ interface RigstateConfig {
4
+ apiKey?: string;
5
+ projectId?: string;
6
+ apiUrl?: string;
7
+ }
8
+
9
+ const config = new Conf<RigstateConfig>({
10
+ projectName: 'rigstate-cli',
11
+ defaults: {
12
+ apiUrl: 'http://localhost:3000',
13
+ },
14
+ });
15
+
16
+ /**
17
+ * Get the stored API key
18
+ * @throws {Error} If no API key is found (user not logged in)
19
+ */
20
+ export function getApiKey(): string {
21
+ const apiKey = config.get('apiKey');
22
+ if (!apiKey) {
23
+ throw new Error(
24
+ '❌ Not logged in. Please run "rigstate login <your-api-key>" first.'
25
+ );
26
+ }
27
+ return apiKey;
28
+ }
29
+
30
+ /**
31
+ * Set the API key
32
+ */
33
+ export function setApiKey(key: string): void {
34
+ config.set('apiKey', key);
35
+ }
36
+
37
+ /**
38
+ * Get the default project ID (if set)
39
+ */
40
+ export function getProjectId(): string | undefined {
41
+ return config.get('projectId');
42
+ }
43
+
44
+ /**
45
+ * Set the default project ID
46
+ */
47
+ export function setProjectId(projectId: string): void {
48
+ config.set('projectId', projectId);
49
+ }
50
+
51
+ /**
52
+ * Get the API URL
53
+ * Priority: Environment variable > Stored config > Production default
54
+ */
55
+ export function getApiUrl(): string {
56
+ // 1. Check environment variable first
57
+ if (process.env.RIGSTATE_API_URL) {
58
+ return process.env.RIGSTATE_API_URL;
59
+ }
60
+ // 2. Check stored config
61
+ const storedUrl = config.get('apiUrl');
62
+ if (storedUrl) {
63
+ return storedUrl;
64
+ }
65
+ // 3. Default to production
66
+ return 'https://app.rigstate.com';
67
+ }
68
+
69
+ /**
70
+ * Set the API URL
71
+ */
72
+ export function setApiUrl(url: string): void {
73
+ config.set('apiUrl', url);
74
+ }
75
+
76
+ /**
77
+ * Clear all config
78
+ */
79
+ export function clearConfig(): void {
80
+ config.clear();
81
+ }
82
+
83
+ export { config };
@@ -0,0 +1,95 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+
4
+ /**
5
+ * Read and parse .gitignore file
6
+ */
7
+ export async function readGitignore(dir: string): Promise<string[]> {
8
+ const gitignorePath = path.join(dir, '.gitignore');
9
+ try {
10
+ const content = await fs.readFile(gitignorePath, 'utf-8');
11
+ return content
12
+ .split('\n')
13
+ .map((line) => line.trim())
14
+ .filter((line) => line && !line.startsWith('#'));
15
+ } catch (error) {
16
+ // No .gitignore file found, return empty array
17
+ return [];
18
+ }
19
+ }
20
+
21
+ /**
22
+ * Check if a path should be ignored based on .gitignore patterns
23
+ */
24
+ export function shouldIgnore(filePath: string, patterns: string[]): boolean {
25
+ const relativePath = filePath.replace(/^\.\//, '');
26
+
27
+ // Default ignore patterns
28
+ const defaultIgnores = [
29
+ 'node_modules',
30
+ '.git',
31
+ 'dist',
32
+ 'build',
33
+ '.next',
34
+ '.turbo',
35
+ 'coverage',
36
+ '.env',
37
+ '.env.local',
38
+ ];
39
+
40
+ const allPatterns = [...defaultIgnores, ...patterns];
41
+
42
+ for (const pattern of allPatterns) {
43
+ if (pattern.endsWith('/')) {
44
+ // Directory pattern
45
+ const dir = pattern.slice(0, -1);
46
+ if (relativePath.includes(`${dir}/`) || relativePath === dir) {
47
+ return true;
48
+ }
49
+ } else if (pattern.includes('*')) {
50
+ // Glob pattern - simple implementation
51
+ const regex = new RegExp(
52
+ '^' + pattern.replace(/\*/g, '.*').replace(/\?/g, '.') + '$'
53
+ );
54
+ if (regex.test(relativePath)) {
55
+ return true;
56
+ }
57
+ } else {
58
+ // Exact match or contains
59
+ if (relativePath.includes(pattern)) {
60
+ return true;
61
+ }
62
+ }
63
+ }
64
+
65
+ return false;
66
+ }
67
+
68
+ /**
69
+ * Check if file is a code file based on extension
70
+ */
71
+ export function isCodeFile(filePath: string): boolean {
72
+ const codeExtensions = [
73
+ '.js',
74
+ '.jsx',
75
+ '.ts',
76
+ '.tsx',
77
+ '.py',
78
+ '.java',
79
+ '.go',
80
+ '.rb',
81
+ '.php',
82
+ '.c',
83
+ '.cpp',
84
+ '.h',
85
+ '.cs',
86
+ '.swift',
87
+ '.kt',
88
+ '.rs',
89
+ '.vue',
90
+ '.svelte',
91
+ ];
92
+
93
+ const ext = path.extname(filePath).toLowerCase();
94
+ return codeExtensions.includes(ext);
95
+ }
@@ -0,0 +1,128 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import chalk from 'chalk';
4
+
5
+ // --- Types ---
6
+
7
+ export enum InterventionLevel {
8
+ GHOST = 0, // Log only, silent
9
+ NUDGE = 1, // Warn on commit/complete
10
+ SENTINEL = 2 // Immediate SOFT_LOCK
11
+ }
12
+
13
+ export type LockStatus = 'OPEN' | 'SOFT_LOCK';
14
+
15
+ export interface GovernanceConfig {
16
+ governance: {
17
+ intervention_level: InterventionLevel;
18
+ allow_overrides: boolean;
19
+ };
20
+ }
21
+
22
+ export interface SessionState {
23
+ status: LockStatus;
24
+ active_violation?: string | null;
25
+ lock_reason?: string | null;
26
+ last_updated: string;
27
+ }
28
+
29
+ const DEFAULT_CONFIG: GovernanceConfig = {
30
+ governance: {
31
+ intervention_level: InterventionLevel.GHOST,
32
+ allow_overrides: true
33
+ }
34
+ };
35
+
36
+ const DEFAULT_SESSION: SessionState = {
37
+ status: 'OPEN',
38
+ active_violation: null,
39
+ lock_reason: null,
40
+ last_updated: new Date().toISOString()
41
+ };
42
+
43
+ // --- Config Management ---
44
+
45
+ export async function getGovernanceConfig(rootDir: string = process.cwd()): Promise<GovernanceConfig> {
46
+ try {
47
+ const configPath = path.join(rootDir, 'rigstate.config.json');
48
+ const content = await fs.readFile(configPath, 'utf-8');
49
+ const userConfig = JSON.parse(content);
50
+ return {
51
+ governance: {
52
+ ...DEFAULT_CONFIG.governance,
53
+ ...userConfig.governance
54
+ }
55
+ };
56
+ } catch (e) {
57
+ return DEFAULT_CONFIG;
58
+ }
59
+ }
60
+
61
+ // --- Session State Management ---
62
+
63
+ export async function getSessionState(rootDir: string = process.cwd()): Promise<SessionState> {
64
+ try {
65
+ const sessionPath = path.join(rootDir, '.rigstate', 'session.json');
66
+ const content = await fs.readFile(sessionPath, 'utf-8');
67
+ return JSON.parse(content);
68
+ } catch (e) {
69
+ return DEFAULT_SESSION;
70
+ }
71
+ }
72
+
73
+ export async function setSoftLock(
74
+ reason: string,
75
+ violationId: string,
76
+ rootDir: string = process.cwd()
77
+ ): Promise<void> {
78
+ const sessionPath = path.join(rootDir, '.rigstate', 'session.json');
79
+ const state: SessionState = {
80
+ status: 'SOFT_LOCK',
81
+ active_violation: violationId,
82
+ lock_reason: reason,
83
+ last_updated: new Date().toISOString()
84
+ };
85
+
86
+ await fs.mkdir(path.dirname(sessionPath), { recursive: true });
87
+ await fs.writeFile(sessionPath, JSON.stringify(state, null, 2), 'utf-8');
88
+ }
89
+
90
+ export async function clearSoftLock(
91
+ rootDir: string = process.cwd()
92
+ ): Promise<void> {
93
+ const sessionPath = path.join(rootDir, '.rigstate', 'session.json');
94
+ const state: SessionState = {
95
+ ...DEFAULT_SESSION,
96
+ last_updated: new Date().toISOString()
97
+ };
98
+
99
+ await fs.mkdir(path.dirname(sessionPath), { recursive: true });
100
+ await fs.writeFile(sessionPath, JSON.stringify(state, null, 2), 'utf-8');
101
+ }
102
+
103
+ /**
104
+ * Checks if the user is authorized to perform an override
105
+ * (For now, assumes CLI user is authorized, but logs it)
106
+ */
107
+ export async function performOverride(
108
+ violationId: string,
109
+ reason: string,
110
+ rootDir: string = process.cwd()
111
+ ): Promise<boolean> {
112
+ const config = await getGovernanceConfig(rootDir);
113
+
114
+ if (!config.governance.allow_overrides) {
115
+ console.log(chalk.red('❌ Overrides are disabled for this project.'));
116
+ return false;
117
+ }
118
+
119
+ // Security violations (SEC-*) cannot be overridden via CLI usually,
120
+ // but the implementation here depends on how we define "Security".
121
+ // For now, we allow overriding via this function, but the *Caller* should check violation type.
122
+
123
+ await clearSoftLock(rootDir);
124
+ // TODO: Add to Mission Report (Audit Log)
125
+ // We will handle logging in the command handler itself
126
+
127
+ return true;
128
+ }
@@ -0,0 +1,66 @@
1
+ import chalk from 'chalk';
2
+
3
+ export enum LogLevel {
4
+ INFO = 'INFO',
5
+ WARN = 'WARN',
6
+ ERROR = 'ERROR',
7
+ DEBUG = 'DEBUG'
8
+ }
9
+
10
+ export class Logger {
11
+ private static formatMessage(level: LogLevel, message: string, context?: unknown): string {
12
+ const timestamp = new Date().toISOString();
13
+ let prefix = '';
14
+
15
+ switch (level) {
16
+ case LogLevel.INFO:
17
+ prefix = chalk.blue(`[${LogLevel.INFO}]`);
18
+ break;
19
+ case LogLevel.WARN:
20
+ prefix = chalk.yellow(`[${LogLevel.WARN}]`);
21
+ break;
22
+ case LogLevel.ERROR:
23
+ prefix = chalk.red(`[${LogLevel.ERROR}]`);
24
+ break;
25
+ case LogLevel.DEBUG:
26
+ prefix = chalk.gray(`[${LogLevel.DEBUG}]`);
27
+ break;
28
+ }
29
+
30
+ let output = `${chalk.gray(timestamp)} ${prefix} ${message}`;
31
+
32
+ if (context) {
33
+ if (context instanceof Error) {
34
+ output += `\n${chalk.red(context.stack || context.message)}`;
35
+ } else if (typeof context === 'object') {
36
+ try {
37
+ output += `\n${chalk.gray(JSON.stringify(context, null, 2))}`;
38
+ } catch (e) {
39
+ output += `\n${chalk.gray('[Circular or invalid object]')}`;
40
+ }
41
+ } else {
42
+ output += ` ${String(context)}`;
43
+ }
44
+ }
45
+
46
+ return output;
47
+ }
48
+
49
+ static info(message: string, context?: unknown) {
50
+ console.log(this.formatMessage(LogLevel.INFO, message, context));
51
+ }
52
+
53
+ static warn(message: string, context?: unknown) {
54
+ console.warn(this.formatMessage(LogLevel.WARN, message, context));
55
+ }
56
+
57
+ static error(message: string, error?: unknown) {
58
+ console.error(this.formatMessage(LogLevel.ERROR, message, error));
59
+ }
60
+
61
+ static debug(message: string, context?: unknown) {
62
+ if (process.env.DEBUG || process.env.RIGSTATE_DEBUG) {
63
+ console.debug(this.formatMessage(LogLevel.DEBUG, message, context));
64
+ }
65
+ }
66
+ }