@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.
- package/.env.example +5 -0
- package/IMPLEMENTATION.md +239 -0
- package/QUICK_START.md +220 -0
- package/README.md +150 -0
- package/dist/index.cjs +3987 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3964 -0
- package/dist/index.js.map +1 -0
- package/install.sh +15 -0
- package/package.json +53 -0
- package/src/commands/check.ts +329 -0
- package/src/commands/config.ts +81 -0
- package/src/commands/daemon.ts +197 -0
- package/src/commands/env.ts +158 -0
- package/src/commands/fix.ts +140 -0
- package/src/commands/focus.ts +134 -0
- package/src/commands/hooks.ts +163 -0
- package/src/commands/init.ts +282 -0
- package/src/commands/link.ts +45 -0
- package/src/commands/login.ts +35 -0
- package/src/commands/mcp.ts +73 -0
- package/src/commands/nexus.ts +81 -0
- package/src/commands/override.ts +65 -0
- package/src/commands/scan.ts +242 -0
- package/src/commands/sync-rules.ts +191 -0
- package/src/commands/sync.ts +339 -0
- package/src/commands/watch.ts +283 -0
- package/src/commands/work.ts +172 -0
- package/src/daemon/bridge-listener.ts +127 -0
- package/src/daemon/core.ts +184 -0
- package/src/daemon/factory.ts +45 -0
- package/src/daemon/file-watcher.ts +97 -0
- package/src/daemon/guardian-monitor.ts +133 -0
- package/src/daemon/heuristic-engine.ts +203 -0
- package/src/daemon/intervention-protocol.ts +128 -0
- package/src/daemon/telemetry.ts +23 -0
- package/src/daemon/types.ts +18 -0
- package/src/hive/gateway.ts +74 -0
- package/src/hive/protocol.ts +29 -0
- package/src/hive/scrubber.ts +72 -0
- package/src/index.ts +85 -0
- package/src/nexus/council.ts +103 -0
- package/src/nexus/dispatcher.ts +133 -0
- package/src/utils/config.ts +83 -0
- package/src/utils/files.ts +95 -0
- package/src/utils/governance.ts +128 -0
- package/src/utils/logger.ts +66 -0
- package/src/utils/manifest.ts +18 -0
- package/src/utils/rule-engine.ts +292 -0
- package/src/utils/skills-provisioner.ts +153 -0
- package/src/utils/version.ts +1 -0
- package/src/utils/watchdog.ts +215 -0
- package/tsconfig.json +29 -0
- 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
|
+
}
|