@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,172 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import axios from 'axios';
|
|
5
|
+
import inquirer from 'inquirer';
|
|
6
|
+
import fs from 'fs/promises';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { getApiKey, getApiUrl, getProjectId } from '../utils/config.js';
|
|
9
|
+
|
|
10
|
+
export function createWorkCommand(): Command {
|
|
11
|
+
return new Command('work')
|
|
12
|
+
.alias('start')
|
|
13
|
+
.description('Select and execute a Roadmap Task (fetches IDE Prompt)')
|
|
14
|
+
.argument('[taskId]', 'Optional Task ID (e.g., T-1021) to start immediately')
|
|
15
|
+
.option('--project <id>', 'Project ID')
|
|
16
|
+
.action(async (taskId: string | undefined, options: { project?: string }) => {
|
|
17
|
+
const spinner = ora();
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const apiKey = getApiKey();
|
|
21
|
+
const apiUrl = getApiUrl();
|
|
22
|
+
const projectId = options.project || getProjectId();
|
|
23
|
+
|
|
24
|
+
if (!projectId) {
|
|
25
|
+
console.log(chalk.red('❌ Project ID is required. Run `rigstate link` or pass --project <id>'));
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!taskId) {
|
|
30
|
+
spinner.start('Fetching active roadmap tasks...');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 1. Fetch Roadmap
|
|
34
|
+
const response = await axios.get(
|
|
35
|
+
`${apiUrl}/api/v1/roadmap?project_id=${projectId}`,
|
|
36
|
+
{ headers: { 'Authorization': `Bearer ${apiKey}` }, timeout: 10000 }
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
if (!response.data.success) {
|
|
40
|
+
throw new Error(response.data.error || 'Failed to fetch roadmap');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const allTasks: any[] = response.data.data.roadmap || [];
|
|
44
|
+
|
|
45
|
+
// 2. Filter relevant tasks (ACTIVE or LOCKED or IN_PROGRESS if we had that status)
|
|
46
|
+
// We focus on ACTIVE (Started) and LOCKED (Next Up).
|
|
47
|
+
// Sort: ACTIVE first, then by step_number.
|
|
48
|
+
const actionableTasks = allTasks
|
|
49
|
+
.filter(t => ['ACTIVE', 'LOCKED'].includes(t.status))
|
|
50
|
+
.sort((a, b) => {
|
|
51
|
+
if (a.status === 'ACTIVE' && b.status !== 'ACTIVE') return -1;
|
|
52
|
+
if (b.status === 'ACTIVE' && a.status !== 'ACTIVE') return 1;
|
|
53
|
+
return a.step_number - b.step_number;
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
spinner.stop();
|
|
57
|
+
|
|
58
|
+
let selectedTask: any;
|
|
59
|
+
|
|
60
|
+
if (taskId) {
|
|
61
|
+
// Direct Selection via Arg
|
|
62
|
+
// taskId can be "T-1021" or just "1021" or the UUID
|
|
63
|
+
selectedTask = allTasks.find(t =>
|
|
64
|
+
t.id === taskId ||
|
|
65
|
+
`T-${t.step_number}` === taskId ||
|
|
66
|
+
t.step_number.toString() === taskId
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
if (!selectedTask) {
|
|
70
|
+
console.log(chalk.red(`❌ Task '${taskId}' not found in roadmap.`));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
// Interactive Selection
|
|
75
|
+
if (actionableTasks.length === 0) {
|
|
76
|
+
console.log(chalk.yellow('No active or locked tasks found. The Roadmap is clear! 🎉'));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const choices = actionableTasks.map(t => {
|
|
81
|
+
const id = `T-${t.step_number}`;
|
|
82
|
+
const statusIcon = t.status === 'ACTIVE' ? '▶️' : '🔒';
|
|
83
|
+
const priority = t.priority === 'MVP' ? chalk.magenta('[MVP]') : chalk.blue(`[${t.priority}]`);
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
name: `${statusIcon} ${chalk.bold(id)}: ${t.title} ${priority}`,
|
|
87
|
+
value: t,
|
|
88
|
+
short: `${id}: ${t.title}`
|
|
89
|
+
};
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const answer = await inquirer.prompt([{
|
|
93
|
+
type: 'list',
|
|
94
|
+
name: 'task',
|
|
95
|
+
message: 'Which task are you working on?',
|
|
96
|
+
choices,
|
|
97
|
+
pageSize: 15
|
|
98
|
+
}]);
|
|
99
|
+
|
|
100
|
+
selectedTask = answer.task;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 3. Display Task Context & Prompt
|
|
104
|
+
console.log('\n' + chalk.bold.underline(`🚀 WORK MODE: ${selectedTask.title}`));
|
|
105
|
+
console.log(chalk.dim(`ID: T-${selectedTask.step_number} | Status: ${selectedTask.status}`));
|
|
106
|
+
|
|
107
|
+
if (selectedTask.prompt_content) {
|
|
108
|
+
console.log(chalk.yellow.bold('\n📋 IDE EXECUTION SIGNAL (Prompt):'));
|
|
109
|
+
console.log(chalk.gray('--------------------------------------------------'));
|
|
110
|
+
console.log(selectedTask.prompt_content);
|
|
111
|
+
console.log(chalk.gray('--------------------------------------------------'));
|
|
112
|
+
|
|
113
|
+
// Copy to clipboard option?
|
|
114
|
+
// For now, let's offer to save it to a file which is more robust for Agents.
|
|
115
|
+
|
|
116
|
+
const { action } = await inquirer.prompt([{
|
|
117
|
+
type: 'list',
|
|
118
|
+
name: 'action',
|
|
119
|
+
message: 'What do you want to do?',
|
|
120
|
+
choices: [
|
|
121
|
+
{ name: 'Copy Prompt (Print clean)', value: 'print' },
|
|
122
|
+
{ name: 'Create .cursorrules (Agent Context)', value: 'cursorrules' },
|
|
123
|
+
{ name: 'Mark as ACTIVE (if LOCKED)', value: 'activate' },
|
|
124
|
+
{ name: 'Mark as COMPLETED', value: 'complete' },
|
|
125
|
+
{ name: 'Cancel', value: 'cancel' }
|
|
126
|
+
]
|
|
127
|
+
}]);
|
|
128
|
+
|
|
129
|
+
if (action === 'cursorrules') {
|
|
130
|
+
// Create a temporary .cursorrules or overwrite existing
|
|
131
|
+
// Warning: this ignores strict existing cursorrules, maybe append?
|
|
132
|
+
// For safety, let's create a .rigstate-context.md file
|
|
133
|
+
await fs.writeFile('.rigstate-prompt.md', selectedTask.prompt_content);
|
|
134
|
+
console.log(chalk.green(`✅ Prompt saved to ${chalk.bold('.rigstate-prompt.md')}`));
|
|
135
|
+
console.log(chalk.dim('You can now reference this file in your IDE chat (@.rigstate-prompt.md)'));
|
|
136
|
+
} else if (action === 'print') {
|
|
137
|
+
console.log('\n' + selectedTask.prompt_content + '\n');
|
|
138
|
+
} else if (action === 'activate' && selectedTask.status !== 'ACTIVE') {
|
|
139
|
+
try {
|
|
140
|
+
await axios.post(
|
|
141
|
+
`${apiUrl}/api/v1/roadmap/update-status`,
|
|
142
|
+
{ step_id: selectedTask.id, status: 'ACTIVE', project_id: projectId },
|
|
143
|
+
{ headers: { 'Authorization': `Bearer ${apiKey}` } }
|
|
144
|
+
);
|
|
145
|
+
console.log(chalk.green(`✅ Task marked as ACTIVE.`));
|
|
146
|
+
} catch (e: any) {
|
|
147
|
+
console.error(chalk.red(`Failed to update status: ${e.message}`));
|
|
148
|
+
}
|
|
149
|
+
} else if (action === 'complete') {
|
|
150
|
+
try {
|
|
151
|
+
await axios.post(
|
|
152
|
+
`${apiUrl}/api/v1/roadmap/update-status`,
|
|
153
|
+
{ step_id: selectedTask.id, status: 'COMPLETED', project_id: projectId },
|
|
154
|
+
{ headers: { 'Authorization': `Bearer ${apiKey}` } }
|
|
155
|
+
);
|
|
156
|
+
console.log(chalk.green(`✅ Task marked as COMPLETED. Great job!`));
|
|
157
|
+
} catch (e: any) {
|
|
158
|
+
console.error(chalk.red(`Failed to update status: ${e.message}`));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
} else {
|
|
163
|
+
console.log(chalk.yellow('\n⚠️ No specific IDE Prompt found for this task (Legacy Task?).'));
|
|
164
|
+
console.log(chalk.dim('Objective: ' + (selectedTask.summary || selectedTask.description || 'Check web UI for details.')));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
} catch (error: any) {
|
|
168
|
+
spinner.stop();
|
|
169
|
+
console.error(chalk.red(`\nCommand failed: ${error.message}`));
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bridge Listener - Connects to Agent Bridge for task polling
|
|
3
|
+
*
|
|
4
|
+
* Uses polling as Supabase Realtime requires direct SDK access
|
|
5
|
+
* which is complex in CLI context. Polling every 5 seconds
|
|
6
|
+
* provides good responsiveness for task-based workflows.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import axios from 'axios';
|
|
10
|
+
import { EventEmitter } from 'events';
|
|
11
|
+
|
|
12
|
+
export interface BridgeListenerEvents {
|
|
13
|
+
task: (task: BridgeTask) => void;
|
|
14
|
+
ping: () => void;
|
|
15
|
+
error: (error: Error) => void;
|
|
16
|
+
connected: () => void;
|
|
17
|
+
disconnected: () => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface BridgeTask {
|
|
21
|
+
id: string;
|
|
22
|
+
project_id: string;
|
|
23
|
+
task_id: string | null;
|
|
24
|
+
status: string;
|
|
25
|
+
proposal: string | null;
|
|
26
|
+
summary: string | null;
|
|
27
|
+
created_at: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface BridgeListener extends EventEmitter {
|
|
31
|
+
connect(): Promise<void>;
|
|
32
|
+
disconnect(): Promise<void>;
|
|
33
|
+
on<K extends keyof BridgeListenerEvents>(event: K, listener: BridgeListenerEvents[K]): this;
|
|
34
|
+
emit<K extends keyof BridgeListenerEvents>(event: K, ...args: Parameters<BridgeListenerEvents[K]>): boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const POLL_INTERVAL_MS = 5000; // 5 seconds
|
|
38
|
+
|
|
39
|
+
export function createBridgeListener(
|
|
40
|
+
projectId: string,
|
|
41
|
+
apiUrl: string,
|
|
42
|
+
apiKey: string
|
|
43
|
+
): BridgeListener {
|
|
44
|
+
const emitter = new EventEmitter() as BridgeListener;
|
|
45
|
+
let pollInterval: NodeJS.Timeout | null = null;
|
|
46
|
+
let isConnected = false;
|
|
47
|
+
let lastCheckedId: string | null = null;
|
|
48
|
+
|
|
49
|
+
const checkBridge = async () => {
|
|
50
|
+
try {
|
|
51
|
+
const response = await axios.get(`${apiUrl}/api/v1/agent/bridge`, {
|
|
52
|
+
params: {
|
|
53
|
+
project_id: projectId,
|
|
54
|
+
action: 'check'
|
|
55
|
+
},
|
|
56
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
57
|
+
timeout: 10000
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
if (response.data.success && response.data.data?.task) {
|
|
61
|
+
const task = response.data.data.task;
|
|
62
|
+
|
|
63
|
+
// Check if this is a new task
|
|
64
|
+
if (task.id !== lastCheckedId) {
|
|
65
|
+
lastCheckedId = task.id;
|
|
66
|
+
|
|
67
|
+
// Check for ping/heartbeat
|
|
68
|
+
if (task.proposal?.startsWith('ping')) {
|
|
69
|
+
emitter.emit('ping');
|
|
70
|
+
|
|
71
|
+
// Auto-acknowledge ping
|
|
72
|
+
await acknowledgePing(task.id);
|
|
73
|
+
} else {
|
|
74
|
+
// Emit task for processing
|
|
75
|
+
emitter.emit('task', task);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
} catch (error: any) {
|
|
80
|
+
// Don't emit error for network issues during polling
|
|
81
|
+
// as this is expected when offline
|
|
82
|
+
if (error.code !== 'ECONNREFUSED' && error.code !== 'ETIMEDOUT') {
|
|
83
|
+
emitter.emit('error', error);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const acknowledgePing = async (taskId: string) => {
|
|
89
|
+
try {
|
|
90
|
+
await axios.post(`${apiUrl}/api/v1/agent/bridge`, {
|
|
91
|
+
project_id: projectId,
|
|
92
|
+
action: 'update',
|
|
93
|
+
bridge_id: taskId,
|
|
94
|
+
status: 'COMPLETED',
|
|
95
|
+
summary: 'Pong! Guardian Daemon is active.'
|
|
96
|
+
}, {
|
|
97
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
98
|
+
timeout: 5000
|
|
99
|
+
});
|
|
100
|
+
} catch {
|
|
101
|
+
// Silently fail ping acknowledgment
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
emitter.connect = async () => {
|
|
106
|
+
if (isConnected) return;
|
|
107
|
+
|
|
108
|
+
// Initial check
|
|
109
|
+
await checkBridge();
|
|
110
|
+
|
|
111
|
+
// Start polling
|
|
112
|
+
pollInterval = setInterval(checkBridge, POLL_INTERVAL_MS);
|
|
113
|
+
isConnected = true;
|
|
114
|
+
emitter.emit('connected');
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
emitter.disconnect = async () => {
|
|
118
|
+
if (pollInterval) {
|
|
119
|
+
clearInterval(pollInterval);
|
|
120
|
+
pollInterval = null;
|
|
121
|
+
}
|
|
122
|
+
isConnected = false;
|
|
123
|
+
emitter.emit('disconnected');
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
return emitter;
|
|
127
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daemon Core - Orchestrates all background watchers
|
|
3
|
+
*
|
|
4
|
+
* Combines:
|
|
5
|
+
* 1. File Watcher (chokidar) - Monitors filesystem changes
|
|
6
|
+
* 2. Guardian Rules - Validates files against rules on change
|
|
7
|
+
* 3. Agent Bridge Listener - Receives tasks from Rigstate Cloud
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import chalk from 'chalk';
|
|
11
|
+
import ora from 'ora';
|
|
12
|
+
import * as fs from 'fs/promises';
|
|
13
|
+
import { EventEmitter } from 'events';
|
|
14
|
+
import { createFileWatcher, type FileWatcherEvents } from './file-watcher.js';
|
|
15
|
+
import { createHeuristicEngine } from './heuristic-engine.js';
|
|
16
|
+
import { createInterventionProtocol, type InterventionDecision } from './intervention-protocol.js';
|
|
17
|
+
import { createGuardianMonitor } from './guardian-monitor.js';
|
|
18
|
+
import { createBridgeListener, type BridgeListenerEvents } from './bridge-listener.js';
|
|
19
|
+
import { DaemonConfig, DaemonState } from './types.js';
|
|
20
|
+
import { trackSkillUsage } from './telemetry.js';
|
|
21
|
+
import { jitProvisionSkill } from '../utils/skills-provisioner.js';
|
|
22
|
+
|
|
23
|
+
export class GuardianDaemon extends EventEmitter {
|
|
24
|
+
private config: DaemonConfig;
|
|
25
|
+
private state: DaemonState;
|
|
26
|
+
private fileWatcher: ReturnType<typeof createFileWatcher> | null = null;
|
|
27
|
+
private guardianMonitor: ReturnType<typeof createGuardianMonitor> | null = null;
|
|
28
|
+
private heuristicEngine: ReturnType<typeof createHeuristicEngine> | null = null;
|
|
29
|
+
private interventionProtocol: ReturnType<typeof createInterventionProtocol> | null = null;
|
|
30
|
+
private bridgeListener: ReturnType<typeof createBridgeListener> | null = null;
|
|
31
|
+
|
|
32
|
+
constructor(config: DaemonConfig) {
|
|
33
|
+
super();
|
|
34
|
+
this.config = config;
|
|
35
|
+
this.state = {
|
|
36
|
+
isRunning: false,
|
|
37
|
+
startedAt: null,
|
|
38
|
+
filesChecked: 0,
|
|
39
|
+
violationsFound: 0,
|
|
40
|
+
tasksProcessed: 0,
|
|
41
|
+
lastActivity: null
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async start(): Promise<void> {
|
|
46
|
+
if (this.state.isRunning) {
|
|
47
|
+
console.log(chalk.yellow('Daemon is already running.'));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
this.printWelcome();
|
|
52
|
+
this.state.isRunning = true;
|
|
53
|
+
this.state.startedAt = new Date().toISOString();
|
|
54
|
+
|
|
55
|
+
// 1. Initialize Engines
|
|
56
|
+
this.heuristicEngine = createHeuristicEngine();
|
|
57
|
+
this.interventionProtocol = createInterventionProtocol();
|
|
58
|
+
this.guardianMonitor = createGuardianMonitor(this.config.projectId, this.config.apiUrl, this.config.apiKey);
|
|
59
|
+
|
|
60
|
+
// 2. Load and Sync Rules
|
|
61
|
+
await this.guardianMonitor.loadRules();
|
|
62
|
+
console.log(chalk.green(` ✓ Loaded ${this.guardianMonitor.getRuleCount()} rules`));
|
|
63
|
+
await this.syncHeuristics();
|
|
64
|
+
|
|
65
|
+
// 3. Setup File Watcher
|
|
66
|
+
if (this.config.checkOnChange) {
|
|
67
|
+
this.setupFileWatcher();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 4. Setup Bridge
|
|
71
|
+
if (this.config.bridgeEnabled) {
|
|
72
|
+
await this.setupBridge();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
this.printActive();
|
|
76
|
+
this.emit('started', this.state);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private printWelcome() {
|
|
80
|
+
console.log(chalk.bold.blue('\n🛡️ Guardian Daemon Starting...'));
|
|
81
|
+
console.log(chalk.dim(`Project: ${this.config.projectId}`));
|
|
82
|
+
console.log(chalk.dim(`Watch Path: ${this.config.watchPath}`));
|
|
83
|
+
console.log(chalk.dim('─'.repeat(50)));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private printActive() {
|
|
87
|
+
console.log(chalk.dim('─'.repeat(50)));
|
|
88
|
+
console.log(chalk.green.bold('✅ Guardian Daemon is now active'));
|
|
89
|
+
console.log(chalk.dim('Press Ctrl+C to stop\n'));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private async syncHeuristics() {
|
|
93
|
+
if (!this.heuristicEngine) return;
|
|
94
|
+
const synced = await this.heuristicEngine.refreshRules(this.config.projectId, this.config.apiUrl, this.config.apiKey);
|
|
95
|
+
if (synced) console.log(chalk.green(' ✓ Synced heuristic rules'));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private setupFileWatcher() {
|
|
99
|
+
console.log(chalk.dim('📂 Starting file watcher...'));
|
|
100
|
+
this.fileWatcher = createFileWatcher(this.config.watchPath);
|
|
101
|
+
this.fileWatcher.on('change', (path) => this.handleFileChange(path));
|
|
102
|
+
this.fileWatcher.start();
|
|
103
|
+
console.log(chalk.green(' ✓ File watcher active'));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private async handleFileChange(filePath: string) {
|
|
107
|
+
this.state.lastActivity = new Date().toISOString();
|
|
108
|
+
if (this.config.verbose) console.log(chalk.dim(` 📝 File changed: ${filePath}`));
|
|
109
|
+
|
|
110
|
+
// 1. Calculate metrics
|
|
111
|
+
let lineCount = 0;
|
|
112
|
+
try {
|
|
113
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
114
|
+
lineCount = content.split('\n').length;
|
|
115
|
+
} catch (e) { /* Deleted file or error */ }
|
|
116
|
+
|
|
117
|
+
// A. Prediction & JIT
|
|
118
|
+
if (this.heuristicEngine && this.interventionProtocol && this.guardianMonitor) {
|
|
119
|
+
const matches = await this.heuristicEngine.analyzeFile(filePath, {
|
|
120
|
+
lineCount,
|
|
121
|
+
rules: this.guardianMonitor.getRules()
|
|
122
|
+
});
|
|
123
|
+
for (const match of matches) {
|
|
124
|
+
console.log(chalk.magenta(` 💡 PREDICTIVE ACTIVATION: ${match.skillId}`));
|
|
125
|
+
console.log(chalk.dim(` Reason: ${match.reason}`));
|
|
126
|
+
|
|
127
|
+
const decision = this.interventionProtocol.evaluateTrigger(match.skillId, match.confidence);
|
|
128
|
+
this.interventionProtocol.enforce(decision);
|
|
129
|
+
|
|
130
|
+
await jitProvisionSkill(match.skillId, this.config.apiUrl, this.config.apiKey, this.config.projectId, process.cwd());
|
|
131
|
+
await trackSkillUsage(this.config.apiUrl, this.config.apiKey, this.config.projectId, match.skillId);
|
|
132
|
+
this.emit('skill:suggestion', match);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// B. Guardian Check
|
|
137
|
+
if (this.guardianMonitor) {
|
|
138
|
+
if (this.interventionProtocol) this.interventionProtocol.clear(filePath);
|
|
139
|
+
const result = await this.guardianMonitor.checkFile(filePath);
|
|
140
|
+
this.state.filesChecked++;
|
|
141
|
+
|
|
142
|
+
if (result.violations.length > 0) {
|
|
143
|
+
this.state.violationsFound += result.violations.length;
|
|
144
|
+
this.emit('violation', { file: filePath, violations: result.violations });
|
|
145
|
+
for (const v of result.violations) {
|
|
146
|
+
const color = v.severity === 'critical' ? chalk.red : v.severity === 'warning' ? chalk.yellow : chalk.blue;
|
|
147
|
+
console.log(color(` [${v.severity.toUpperCase()}] ${filePath}: ${v.message}`));
|
|
148
|
+
if (this.interventionProtocol) {
|
|
149
|
+
const decision = this.interventionProtocol.evaluateViolation(v.message, v.severity as any);
|
|
150
|
+
this.interventionProtocol.enforce(decision);
|
|
151
|
+
this.interventionProtocol.registerViolation(filePath, decision);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private async setupBridge() {
|
|
159
|
+
console.log(chalk.dim('🌉 Connecting to Agent Bridge...'));
|
|
160
|
+
this.bridgeListener = createBridgeListener(this.config.projectId, this.config.apiUrl, this.config.apiKey);
|
|
161
|
+
this.bridgeListener.on('task', (task) => {
|
|
162
|
+
this.state.lastActivity = new Date().toISOString();
|
|
163
|
+
this.state.tasksProcessed++;
|
|
164
|
+
console.log(chalk.cyan(`\n📥 New task received: ${task.id}`));
|
|
165
|
+
this.emit('task', task);
|
|
166
|
+
});
|
|
167
|
+
await this.bridgeListener.connect();
|
|
168
|
+
console.log(chalk.green(' ✓ Agent Bridge connected'));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async stop(): Promise<void> {
|
|
172
|
+
if (!this.state.isRunning) return;
|
|
173
|
+
console.log(chalk.dim('\n🛑 Stopping Guardian Daemon...'));
|
|
174
|
+
if (this.fileWatcher) await this.fileWatcher.stop();
|
|
175
|
+
if (this.bridgeListener) await this.bridgeListener.disconnect();
|
|
176
|
+
this.state.isRunning = false;
|
|
177
|
+
console.log(chalk.green('✓ Daemon stopped.'));
|
|
178
|
+
this.emit('stopped', this.state);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
getState(): DaemonState {
|
|
182
|
+
return { ...this.state };
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { GuardianDaemon } from './core.js';
|
|
2
|
+
import { getApiKey, getApiUrl, getProjectId } from '../utils/config.js';
|
|
3
|
+
import { loadManifest } from '../utils/manifest.js';
|
|
4
|
+
import { DaemonConfig } from './types.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Factory for creating a GuardianDaemon with resolved configuration.
|
|
8
|
+
*/
|
|
9
|
+
export async function createDaemon(options: {
|
|
10
|
+
project?: string;
|
|
11
|
+
path?: string;
|
|
12
|
+
noBridge?: boolean;
|
|
13
|
+
verbose?: boolean;
|
|
14
|
+
}): Promise<GuardianDaemon> {
|
|
15
|
+
const apiUrl = getApiUrl();
|
|
16
|
+
let projectId = options.project;
|
|
17
|
+
|
|
18
|
+
if (!projectId) {
|
|
19
|
+
const manifest = await loadManifest();
|
|
20
|
+
if (manifest) projectId = manifest.project_id;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (!projectId) projectId = getProjectId();
|
|
24
|
+
|
|
25
|
+
if (!projectId) {
|
|
26
|
+
throw new Error('No project ID found. Run "rigstate link" or use --project <id>.');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const apiKey = getApiKey();
|
|
30
|
+
if (!apiKey) {
|
|
31
|
+
throw new Error('Not authenticated. Run "rigstate login" first.');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const config: DaemonConfig = {
|
|
35
|
+
projectId,
|
|
36
|
+
apiUrl,
|
|
37
|
+
apiKey,
|
|
38
|
+
watchPath: options.path || process.cwd(),
|
|
39
|
+
checkOnChange: true,
|
|
40
|
+
bridgeEnabled: !options.noBridge,
|
|
41
|
+
verbose: !!options.verbose
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
return new GuardianDaemon(config);
|
|
45
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File Watcher - Monitors filesystem for changes using chokidar
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as chokidar from 'chokidar';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { EventEmitter } from 'events';
|
|
8
|
+
|
|
9
|
+
export interface FileWatcherEvents {
|
|
10
|
+
change: (filePath: string) => void;
|
|
11
|
+
add: (filePath: string) => void;
|
|
12
|
+
unlink: (filePath: string) => void;
|
|
13
|
+
error: (error: Error) => void;
|
|
14
|
+
ready: () => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface FileWatcher extends EventEmitter {
|
|
18
|
+
start(): void;
|
|
19
|
+
stop(): Promise<void>;
|
|
20
|
+
on<K extends keyof FileWatcherEvents>(event: K, listener: FileWatcherEvents[K]): this;
|
|
21
|
+
emit<K extends keyof FileWatcherEvents>(event: K, ...args: Parameters<FileWatcherEvents[K]>): boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const CODE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'];
|
|
25
|
+
|
|
26
|
+
function isCodeFile(filePath: string): boolean {
|
|
27
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
28
|
+
return CODE_EXTENSIONS.includes(ext);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function createFileWatcher(watchPath: string): FileWatcher {
|
|
32
|
+
const emitter = new EventEmitter() as FileWatcher;
|
|
33
|
+
let watcher: chokidar.FSWatcher | null = null;
|
|
34
|
+
|
|
35
|
+
emitter.start = () => {
|
|
36
|
+
const absolutePath = path.resolve(process.cwd(), watchPath);
|
|
37
|
+
|
|
38
|
+
watcher = chokidar.watch(absolutePath, {
|
|
39
|
+
ignored: (path) => {
|
|
40
|
+
// Always ignore node_modules
|
|
41
|
+
if (path.includes('node_modules')) return true;
|
|
42
|
+
if (path.includes('.git')) return true;
|
|
43
|
+
if (path.includes('.next')) return true;
|
|
44
|
+
if (path.includes('dist')) return true;
|
|
45
|
+
if (path.includes('build')) return true;
|
|
46
|
+
if (path.includes('.rigstate')) return true;
|
|
47
|
+
if (path.includes('coverage')) return true;
|
|
48
|
+
return false;
|
|
49
|
+
},
|
|
50
|
+
persistent: true,
|
|
51
|
+
ignoreInitial: true,
|
|
52
|
+
depth: 15,
|
|
53
|
+
awaitWriteFinish: {
|
|
54
|
+
stabilityThreshold: 200,
|
|
55
|
+
pollInterval: 100
|
|
56
|
+
},
|
|
57
|
+
usePolling: false, // Use native events when possible
|
|
58
|
+
interval: 300,
|
|
59
|
+
binaryInterval: 1000
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
watcher.on('change', (filePath: string) => {
|
|
63
|
+
if (isCodeFile(filePath)) {
|
|
64
|
+
emitter.emit('change', path.relative(process.cwd(), filePath));
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
watcher.on('add', (filePath: string) => {
|
|
69
|
+
if (isCodeFile(filePath)) {
|
|
70
|
+
emitter.emit('add', path.relative(process.cwd(), filePath));
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
watcher.on('unlink', (filePath: string) => {
|
|
75
|
+
if (isCodeFile(filePath)) {
|
|
76
|
+
emitter.emit('unlink', path.relative(process.cwd(), filePath));
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
watcher.on('error', (error: any) => {
|
|
81
|
+
emitter.emit('error', error);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
watcher.on('ready', () => {
|
|
85
|
+
emitter.emit('ready');
|
|
86
|
+
});
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
emitter.stop = async () => {
|
|
90
|
+
if (watcher) {
|
|
91
|
+
await watcher.close();
|
|
92
|
+
watcher = null;
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
return emitter;
|
|
97
|
+
}
|