@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
@@ -0,0 +1,18 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+
4
+ export interface RigstateManifest {
5
+ project_id: string;
6
+ api_url?: string;
7
+ linked_at?: string;
8
+ }
9
+
10
+ export async function loadManifest(): Promise<RigstateManifest | null> {
11
+ try {
12
+ const manifestPath = path.join(process.cwd(), '.rigstate');
13
+ const content = await fs.readFile(manifestPath, 'utf-8');
14
+ return JSON.parse(content);
15
+ } catch {
16
+ return null;
17
+ }
18
+ }
@@ -0,0 +1,292 @@
1
+ /**
2
+ * Rule Engine - Evaluates Guardian rules against local files
3
+ */
4
+
5
+ import fs from 'fs/promises';
6
+ import path from 'path';
7
+ import chalk from 'chalk';
8
+
9
+ export interface EffectiveRule {
10
+ id: string;
11
+ rule_name: string;
12
+ rule_type: string;
13
+ value: Record<string, unknown>;
14
+ severity: 'critical' | 'warning' | 'info';
15
+ description: string;
16
+ source: 'global' | 'project_override';
17
+ is_enabled: boolean;
18
+ }
19
+
20
+ export interface Violation {
21
+ file: string;
22
+ rule: string;
23
+ ruleType: string;
24
+ severity: 'critical' | 'warning' | 'info';
25
+ message: string;
26
+ details?: string;
27
+ line?: number;
28
+ }
29
+
30
+ export interface CheckResult {
31
+ file: string;
32
+ violations: Violation[];
33
+ passed: boolean;
34
+ }
35
+
36
+ /**
37
+ * Check a single file against all rules
38
+ */
39
+ export async function checkFile(
40
+ filePath: string,
41
+ rules: EffectiveRule[],
42
+ rootPath: string
43
+ ): Promise<CheckResult> {
44
+ const violations: Violation[] = [];
45
+ const relativePath = path.relative(rootPath, filePath);
46
+
47
+ try {
48
+ const content = await fs.readFile(filePath, 'utf-8');
49
+ const lines = content.split('\n');
50
+
51
+ for (const rule of rules) {
52
+ const ruleViolations = await evaluateRule(rule, content, lines, relativePath);
53
+ violations.push(...ruleViolations);
54
+ }
55
+
56
+ } catch (error: any) {
57
+ violations.push({
58
+ file: relativePath,
59
+ rule: 'FILE_READ_ERROR',
60
+ ruleType: 'SYSTEM',
61
+ severity: 'warning',
62
+ message: `Could not read file: ${error.message}`
63
+ });
64
+ }
65
+
66
+ return {
67
+ file: relativePath,
68
+ violations,
69
+ passed: violations.length === 0
70
+ };
71
+ }
72
+
73
+ /**
74
+ * Evaluate a single rule against file content
75
+ */
76
+ async function evaluateRule(
77
+ rule: EffectiveRule,
78
+ content: string,
79
+ lines: string[],
80
+ filePath: string
81
+ ): Promise<Violation[]> {
82
+ const violations: Violation[] = [];
83
+
84
+ switch (rule.rule_type) {
85
+ case 'MAX_FILE_LINES': {
86
+ const value = rule.value as { limit: number; warning_threshold?: number };
87
+ const lineCount = lines.length;
88
+
89
+ if (lineCount > value.limit) {
90
+ violations.push({
91
+ file: filePath,
92
+ rule: rule.rule_name,
93
+ ruleType: rule.rule_type,
94
+ severity: 'critical',
95
+ message: `File exceeds ${value.limit} lines`,
96
+ details: `Current: ${lineCount} lines (limit: ${value.limit})`
97
+ });
98
+ } else if (value.warning_threshold && lineCount > value.warning_threshold) {
99
+ violations.push({
100
+ file: filePath,
101
+ rule: rule.rule_name,
102
+ ruleType: rule.rule_type,
103
+ severity: 'warning',
104
+ message: `File approaching line limit`,
105
+ details: `Current: ${lineCount} lines (warning at: ${value.warning_threshold}, limit: ${value.limit})`
106
+ });
107
+ }
108
+ break;
109
+ }
110
+
111
+ case 'MAX_FUNCTION_LINES': {
112
+ const value = rule.value as { limit: number };
113
+ const functionViolations = checkFunctionLines(content, lines, filePath, rule, value.limit);
114
+ violations.push(...functionViolations);
115
+ break;
116
+ }
117
+
118
+ case 'PATTERN_FORBIDDEN': {
119
+ const value = rule.value as { pattern: string; message?: string };
120
+ const pattern = new RegExp(value.pattern, 'g');
121
+
122
+ let match;
123
+ while ((match = pattern.exec(content)) !== null) {
124
+ const lineNumber = content.substring(0, match.index).split('\n').length;
125
+ violations.push({
126
+ file: filePath,
127
+ rule: rule.rule_name,
128
+ ruleType: rule.rule_type,
129
+ severity: rule.severity,
130
+ message: value.message || `Forbidden pattern found: ${value.pattern}`,
131
+ line: lineNumber,
132
+ details: `Found: "${match[0].substring(0, 50)}${match[0].length > 50 ? '...' : ''}"`
133
+ });
134
+ }
135
+ break;
136
+ }
137
+
138
+ case 'PATTERN_REQUIRED': {
139
+ const value = rule.value as { pattern: string; context?: string };
140
+ const pattern = new RegExp(value.pattern);
141
+
142
+ // Only check if context matches (e.g., only in certain file types)
143
+ const shouldCheck = !value.context || filePath.includes(value.context);
144
+
145
+ if (shouldCheck && !pattern.test(content)) {
146
+ violations.push({
147
+ file: filePath,
148
+ rule: rule.rule_name,
149
+ ruleType: rule.rule_type,
150
+ severity: rule.severity,
151
+ message: `Required pattern not found: ${value.pattern}`,
152
+ details: value.context ? `Expected in files matching: ${value.context}` : undefined
153
+ });
154
+ }
155
+ break;
156
+ }
157
+
158
+ case 'NAMING_CONVENTION': {
159
+ const value = rule.value as { pattern: string; context: string };
160
+ const pattern = new RegExp(value.pattern);
161
+ const fileName = path.basename(filePath);
162
+
163
+ // Check if context matches (e.g., "components" for component files)
164
+ if (filePath.includes(value.context) && !pattern.test(fileName)) {
165
+ violations.push({
166
+ file: filePath,
167
+ rule: rule.rule_name,
168
+ ruleType: rule.rule_type,
169
+ severity: rule.severity,
170
+ message: `File name does not match naming convention`,
171
+ details: `Expected pattern: ${value.pattern}`
172
+ });
173
+ }
174
+ break;
175
+ }
176
+
177
+ default:
178
+ // Unknown rule type, skip
179
+ break;
180
+ }
181
+
182
+ return violations;
183
+ }
184
+
185
+ /**
186
+ * Check function line counts using simple heuristics (not full AST)
187
+ * Works for TypeScript/JavaScript
188
+ */
189
+ function checkFunctionLines(
190
+ content: string,
191
+ lines: string[],
192
+ filePath: string,
193
+ rule: EffectiveRule,
194
+ limit: number
195
+ ): Violation[] {
196
+ const violations: Violation[] = [];
197
+
198
+ // Simple regex-based detection for functions
199
+ // Matches: function name(), async function name(), const name = () =>, const name = function()
200
+ const functionPatterns = [
201
+ /(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\([^)]*\)\s*\{/g,
202
+ /(?:export\s+)?const\s+(\w+)\s*=\s*(?:async\s+)?\([^)]*\)\s*=>\s*\{/g,
203
+ /(?:export\s+)?const\s+(\w+)\s*=\s*(?:async\s+)?function\s*\([^)]*\)\s*\{/g,
204
+ /(\w+)\s*\([^)]*\)\s*\{/g // Method in class/object
205
+ ];
206
+
207
+ for (const pattern of functionPatterns) {
208
+ let match;
209
+ while ((match = pattern.exec(content)) !== null) {
210
+ const functionName = match[1];
211
+ const startIndex = match.index + match[0].length - 1; // Position of opening brace
212
+
213
+ // Find matching closing brace
214
+ let braceCount = 1;
215
+ let endIndex = startIndex + 1;
216
+
217
+ while (braceCount > 0 && endIndex < content.length) {
218
+ if (content[endIndex] === '{') braceCount++;
219
+ else if (content[endIndex] === '}') braceCount--;
220
+ endIndex++;
221
+ }
222
+
223
+ // Count lines in function
224
+ const functionContent = content.substring(startIndex, endIndex);
225
+ const functionLines = functionContent.split('\n').length;
226
+
227
+ if (functionLines > limit) {
228
+ const lineNumber = content.substring(0, match.index).split('\n').length;
229
+ violations.push({
230
+ file: filePath,
231
+ rule: rule.rule_name,
232
+ ruleType: rule.rule_type,
233
+ severity: rule.severity,
234
+ message: `Function "${functionName}" exceeds ${limit} lines`,
235
+ line: lineNumber,
236
+ details: `Current: ${functionLines} lines (limit: ${limit})`
237
+ });
238
+ }
239
+ }
240
+ }
241
+
242
+ return violations;
243
+ }
244
+
245
+ /**
246
+ * Format violations for console output
247
+ */
248
+ export function formatViolations(violations: Violation[]): void {
249
+ for (const v of violations) {
250
+ const severityColor = v.severity === 'critical' ? chalk.red :
251
+ v.severity === 'warning' ? chalk.yellow : chalk.blue;
252
+
253
+ const lineInfo = v.line ? chalk.dim(`:${v.line}`) : '';
254
+
255
+ console.log(` ${severityColor(`[${v.severity.toUpperCase()}]`)} ${v.file}${lineInfo}`);
256
+ console.log(` ${v.message}`);
257
+ if (v.details) {
258
+ console.log(` ${chalk.dim(v.details)}`);
259
+ }
260
+ }
261
+ }
262
+
263
+ /**
264
+ * Summarize results
265
+ */
266
+ export function summarizeResults(results: CheckResult[]): {
267
+ totalFiles: number;
268
+ totalViolations: number;
269
+ criticalCount: number;
270
+ warningCount: number;
271
+ infoCount: number;
272
+ } {
273
+ let criticalCount = 0;
274
+ let warningCount = 0;
275
+ let infoCount = 0;
276
+
277
+ for (const result of results) {
278
+ for (const v of result.violations) {
279
+ if (v.severity === 'critical') criticalCount++;
280
+ else if (v.severity === 'warning') warningCount++;
281
+ else infoCount++;
282
+ }
283
+ }
284
+
285
+ return {
286
+ totalFiles: results.length,
287
+ totalViolations: criticalCount + warningCount + infoCount,
288
+ criticalCount,
289
+ warningCount,
290
+ infoCount
291
+ };
292
+ }
@@ -0,0 +1,153 @@
1
+ import { AgentSkill } from '@rigstate/rules-engine';
2
+ import axios from 'axios';
3
+ import fs from 'fs/promises';
4
+ import path from 'path';
5
+ import chalk from 'chalk';
6
+
7
+ /**
8
+ * Provisions Agent Skills to the local project.
9
+ *
10
+ * Flow:
11
+ * 1. Fetch skills from API (database)
12
+ * 2. Merge with core skills from rules-engine
13
+ * 3. Write to .agent/skills/<name>/SKILL.md
14
+ * 4. Return list of provisioned skills for .cursorrules injection
15
+ */
16
+ export async function provisionSkills(
17
+ apiUrl: string,
18
+ apiKey: string,
19
+ projectId: string,
20
+ rootDir: string
21
+ ): Promise<AgentSkill[]> {
22
+ const skills: AgentSkill[] = [];
23
+
24
+ // 1. Fetch skills from database (user + global + core)
25
+ try {
26
+ const response = await axios.get(`${apiUrl}/api/v1/skills`, {
27
+ params: { project_id: projectId },
28
+ headers: { Authorization: `Bearer ${apiKey}` }
29
+ });
30
+
31
+ if (response.data.success && response.data.data) {
32
+ for (const dbSkill of response.data.data) {
33
+ skills.push({
34
+ name: dbSkill.name,
35
+ description: dbSkill.description,
36
+ specialist: dbSkill.specialist || 'General',
37
+ version: dbSkill.version || '1.0.0',
38
+ governance: dbSkill.governance || 'OPEN',
39
+ content: dbSkill.content
40
+ });
41
+ }
42
+ }
43
+ } catch (e: any) {
44
+ // API might not have skills endpoint yet - fall through to core skills
45
+ const msg = e.response?.data?.error || e.message;
46
+ console.log(chalk.dim(` (Skills API not available: ${msg}, using core library)`));
47
+ }
48
+
49
+ // 2. If no skills from DB, use core library from rules-engine
50
+ if (skills.length === 0) {
51
+ const { getRigstateStandardSkills } = await import('@rigstate/rules-engine');
52
+ const coreSkills = getRigstateStandardSkills();
53
+ skills.push(...coreSkills);
54
+ }
55
+
56
+ // 3. Write skills to .agent/skills/
57
+ const skillsDir = path.join(rootDir, '.agent', 'skills');
58
+ await fs.mkdir(skillsDir, { recursive: true });
59
+
60
+ for (const skill of skills) {
61
+ const skillDir = path.join(skillsDir, skill.name);
62
+ await fs.mkdir(skillDir, { recursive: true });
63
+
64
+ const skillContent = `---
65
+ name: ${skill.name}
66
+ description: ${skill.description}
67
+ version: "${skill.version}"
68
+ specialist: ${skill.specialist}
69
+ governance: ${skill.governance}
70
+ ---
71
+
72
+ ${skill.content}
73
+
74
+ ---
75
+ *Provisioned by Rigstate CLI. Do not modify manually.*`;
76
+
77
+ const skillPath = path.join(skillDir, 'SKILL.md');
78
+ await fs.writeFile(skillPath, skillContent, 'utf-8');
79
+ }
80
+
81
+ console.log(chalk.green(` āœ… Provisioned ${skills.length} skill(s) to .agent/skills/`));
82
+
83
+ return skills;
84
+ }
85
+
86
+ /**
87
+ * Generate the <available_skills> XML block for .cursorrules
88
+ */
89
+ export function generateSkillsDiscoveryBlock(skills: AgentSkill[]): string {
90
+ if (skills.length === 0) return '';
91
+
92
+ const skillBlocks = skills.map(skill => ` <skill>
93
+ <name>${skill.name}</name>
94
+ <description>${skill.description}</description>
95
+ <location>.agent/skills/${skill.name}/SKILL.md</location>
96
+ </skill>`).join('\n');
97
+
98
+ return `<available_skills>
99
+ ${skillBlocks}
100
+ </available_skills>`;
101
+ }
102
+ /**
103
+ * Just-In-Time provisioning of a specific skill.
104
+ * Checks if the skill is already in .cursorrules and injects it if not.
105
+ */
106
+ export async function jitProvisionSkill(
107
+ skillId: string,
108
+ apiUrl: string,
109
+ apiKey: string,
110
+ projectId: string,
111
+ rootDir: string
112
+ ): Promise<boolean> {
113
+ const rulesPath = path.join(rootDir, '.cursorrules');
114
+ let rulesContent = '';
115
+
116
+ try {
117
+ rulesContent = await fs.readFile(rulesPath, 'utf-8');
118
+ } catch (e) {
119
+ return false;
120
+ }
121
+
122
+ const isProvisioned = rulesContent.includes(`<name>${skillId}</name>`) ||
123
+ rulesContent.includes(`.agent/skills/${skillId}`);
124
+
125
+ if (isProvisioned) return false;
126
+
127
+ console.log(chalk.yellow(` ⚔ JIT PROVISIONING: Injecting ${skillId}...`));
128
+
129
+ try {
130
+ const skills = await provisionSkills(apiUrl, apiKey, projectId, rootDir);
131
+ const skillsBlock = generateSkillsDiscoveryBlock(skills);
132
+
133
+ if (rulesContent.includes('<available_skills>')) {
134
+ rulesContent = rulesContent.replace(
135
+ /<available_skills>[\s\S]*?<\/available_skills>/,
136
+ skillsBlock
137
+ );
138
+ } else if (rulesContent.includes('## 🧠 PROJECT CONTEXT')) {
139
+ const insertPoint = rulesContent.indexOf('---', rulesContent.indexOf('## 🧠 PROJECT CONTEXT'));
140
+ if (insertPoint !== -1) {
141
+ rulesContent = rulesContent.slice(0, insertPoint + 3) +
142
+ '\n\n' + skillsBlock + '\n' +
143
+ rulesContent.slice(insertPoint + 3);
144
+ }
145
+ }
146
+
147
+ await fs.writeFile(rulesPath, rulesContent, 'utf-8');
148
+ return true;
149
+ } catch (e: any) {
150
+ console.log(chalk.red(` Failed to provision skill: ${e.message}`));
151
+ return false;
152
+ }
153
+ }
@@ -0,0 +1 @@
1
+ export async function checkVersion() {}
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Guardian Watchdog - Scans files against governance rules
3
+ * Now fetches rules from API with fallback to cache/defaults
4
+ */
5
+
6
+ import fs from 'fs/promises';
7
+ import path from 'path';
8
+ import chalk from 'chalk';
9
+ import axios from 'axios';
10
+ import { getApiUrl, getApiKey } from './config.js';
11
+
12
+ interface EffectiveRule {
13
+ id: string;
14
+ rule_name: string;
15
+ rule_type: string;
16
+ value: Record<string, unknown>;
17
+ severity: 'critical' | 'warning' | 'info';
18
+ description: string;
19
+ source: 'global' | 'project_override';
20
+ is_enabled: boolean;
21
+ }
22
+
23
+ interface ScanResult {
24
+ file: string;
25
+ lines: number;
26
+ status: 'OK' | 'WARNING' | 'VIOLATION';
27
+ }
28
+
29
+ const DEFAULT_LMAX = 400;
30
+ const DEFAULT_LMAX_WARNING = 350;
31
+ const CACHE_FILE = '.rigstate/rules-cache.json';
32
+
33
+ async function countLines(filePath: string): Promise<number> {
34
+ try {
35
+ const content = await fs.readFile(filePath, 'utf-8');
36
+ return content.split('\n').length;
37
+ } catch (e) {
38
+ return 0;
39
+ }
40
+ }
41
+
42
+ async function getFiles(dir: string, extension: string[]): Promise<string[]> {
43
+ const entries = await fs.readdir(dir, { withFileTypes: true });
44
+ const files = await Promise.all(entries.map(async (entry) => {
45
+ const res = path.resolve(dir, entry.name);
46
+ if (entry.isDirectory()) {
47
+ if (entry.name === 'node_modules' || entry.name === '.git' || entry.name === '.next' || entry.name === 'dist') return [];
48
+ return getFiles(res, extension);
49
+ } else {
50
+ return extension.some(ext => entry.name.endsWith(ext)) ? res : [];
51
+ }
52
+ }));
53
+ return files.flat();
54
+ }
55
+
56
+ /**
57
+ * Fetch rules from API with fallback
58
+ */
59
+ async function fetchRulesFromApi(projectId: string): Promise<{
60
+ lmax: number;
61
+ lmaxWarning: number;
62
+ source: string;
63
+ }> {
64
+ try {
65
+ const apiUrl = getApiUrl();
66
+ const apiKey = getApiKey();
67
+
68
+ const response = await axios.get(`${apiUrl}/api/v1/guardian/rules`, {
69
+ params: { project_id: projectId },
70
+ headers: { Authorization: `Bearer ${apiKey}` },
71
+ timeout: 10000
72
+ });
73
+
74
+ if (response.data.success && response.data.data.settings) {
75
+ return {
76
+ lmax: response.data.data.settings.lmax || DEFAULT_LMAX,
77
+ lmaxWarning: response.data.data.settings.lmax_warning || DEFAULT_LMAX_WARNING,
78
+ source: 'API (Dynamic)'
79
+ };
80
+ }
81
+ } catch (error) {
82
+ // Try to load from cache
83
+ try {
84
+ const cachePath = path.join(process.cwd(), CACHE_FILE);
85
+ const content = await fs.readFile(cachePath, 'utf-8');
86
+ const cached = JSON.parse(content);
87
+ if (cached.settings) {
88
+ return {
89
+ lmax: cached.settings.lmax || DEFAULT_LMAX,
90
+ lmaxWarning: cached.settings.lmax_warning || DEFAULT_LMAX_WARNING,
91
+ source: 'Cache (Fallback)'
92
+ };
93
+ }
94
+ } catch {
95
+ // Cache read failed
96
+ }
97
+ }
98
+
99
+ // Default fallback
100
+ return {
101
+ lmax: DEFAULT_LMAX,
102
+ lmaxWarning: DEFAULT_LMAX_WARNING,
103
+ source: 'Default (Hardcoded)'
104
+ };
105
+ }
106
+
107
+ export async function runGuardianWatchdog(
108
+ rootPath: string,
109
+ settings: Record<string, any> = {},
110
+ projectId?: string
111
+ ): Promise<void> {
112
+ console.log(chalk.bold('\nšŸ›”ļø Active Guardian Watchdog Initiated...'));
113
+
114
+ // Try to get rules from API if projectId is provided
115
+ let lmax = settings.lmax || DEFAULT_LMAX;
116
+ let lmaxWarning = settings.lmax_warning || DEFAULT_LMAX_WARNING;
117
+ let ruleSource = settings.lmax ? 'Settings (Passed)' : 'Default';
118
+
119
+ if (projectId) {
120
+ const apiRules = await fetchRulesFromApi(projectId);
121
+ lmax = apiRules.lmax;
122
+ lmaxWarning = apiRules.lmaxWarning;
123
+ ruleSource = apiRules.source;
124
+ }
125
+
126
+ console.log(chalk.dim(`Governance Rules: L_max=${lmax}, L_max_warning=${lmaxWarning}, Source: ${ruleSource}`));
127
+
128
+ const targetExtensions = ['.ts', '.tsx'];
129
+ let scanTarget = rootPath;
130
+ const webSrc = path.join(rootPath, 'apps', 'web', 'src');
131
+
132
+ try {
133
+ await fs.access(webSrc);
134
+ scanTarget = webSrc;
135
+ } catch {
136
+ // apps/web/src not found, scanning root or provided path
137
+ }
138
+
139
+ console.log(chalk.dim(`Scanning target: ${path.relative(process.cwd(), scanTarget)}`));
140
+
141
+ const files = await getFiles(scanTarget, targetExtensions);
142
+
143
+ let violations = 0;
144
+ let warnings = 0;
145
+
146
+ const results: ScanResult[] = [];
147
+
148
+ for (const file of files) {
149
+ const lines = await countLines(file);
150
+ const relPath = path.relative(rootPath, file);
151
+
152
+ if (lines > lmax) {
153
+ results.push({ file: relPath, lines, status: 'VIOLATION' });
154
+ violations++;
155
+ console.log(chalk.red(`[VIOLATION] ${relPath}: ${lines} lines (Limit: ${lmax})`));
156
+ } else if (lines > lmaxWarning) {
157
+ results.push({ file: relPath, lines, status: 'WARNING' });
158
+ warnings++;
159
+ console.log(chalk.yellow(`[WARNING] ${relPath}: ${lines} lines (Threshold: ${lmaxWarning})`));
160
+ }
161
+ }
162
+
163
+ if (violations === 0 && warnings === 0) {
164
+ console.log(chalk.green(`āœ” All ${files.length} files are within governance limits.`));
165
+ } else {
166
+ console.log('\n' + chalk.bold('Summary:'));
167
+ console.log(chalk.red(`Violations: ${violations}`));
168
+ console.log(chalk.yellow(`Warnings: ${warnings}`));
169
+
170
+ // --- GOVERNANCE INTERVENTION LOGIC ---
171
+ const { getGovernanceConfig, setSoftLock, InterventionLevel } = await import('./governance.js');
172
+ const { governance } = await getGovernanceConfig(rootPath);
173
+ console.log(chalk.dim(`Intervention Level: ${InterventionLevel[governance.intervention_level] || 'UNKNOWN'} (${governance.intervention_level})`));
174
+
175
+ if (violations > 0) {
176
+ console.log(chalk.red.bold('\nCRITICAL: Governance violations detected. Immediate refactoring required.'));
177
+
178
+ // Check for SENTINEL MODE (Level 2)
179
+ if (governance.intervention_level >= InterventionLevel.SENTINEL) {
180
+ console.log(chalk.red.bold('šŸ›‘ SENTINEL MODE: Session SOFT_LOCKED until resolved.'));
181
+ console.log(chalk.red(' Run "rigstate override <id> --reason \\"...\\"" if this is an emergency.'));
182
+ await setSoftLock('Sentinel Mode: Governance Violations Detected', 'ARC-VIOLATION', rootPath);
183
+ }
184
+ }
185
+ }
186
+
187
+ // Sync to Cloud via API
188
+ if (projectId) {
189
+ try {
190
+ const apiUrl = getApiUrl();
191
+ const apiKey = getApiKey();
192
+
193
+ const payloadViolations = results.filter(r => r.status === 'VIOLATION').map(v => ({
194
+ uid: 'V-' + Buffer.from(v.file).toString('base64').replace(/=/g, ''),
195
+ filePath: v.file,
196
+ lineCount: v.lines,
197
+ limitValue: lmax,
198
+ severity: 'CRITICAL'
199
+ }));
200
+
201
+ await axios.post(`${apiUrl}/api/v1/guardian/sync`, {
202
+ projectId,
203
+ violations: payloadViolations,
204
+ warnings: warnings
205
+ }, {
206
+ headers: { Authorization: `Bearer ${apiKey}` }
207
+ });
208
+
209
+ console.log(chalk.dim('āœ” Violations synced to Rigstate Cloud.'));
210
+
211
+ } catch (e: any) {
212
+ console.log(chalk.dim('⚠ Cloud sync skipped: ' + (e.message || 'Unknown')));
213
+ }
214
+ }
215
+ }