@cpretzinger/boss-claude 1.0.0 → 1.0.2
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/README.md +304 -1
- package/bin/boss-claude.js +1138 -0
- package/bin/commands/mode.js +250 -0
- package/bin/onyx-guard.js +259 -0
- package/bin/onyx-guard.sh +251 -0
- package/bin/prompts.js +284 -0
- package/bin/rollback.js +85 -0
- package/bin/setup-wizard.js +492 -0
- package/config/.env.example +17 -0
- package/lib/README.md +83 -0
- package/lib/agent-logger.js +61 -0
- package/lib/agents/memory-engineers/github-memory-engineer.js +251 -0
- package/lib/agents/memory-engineers/postgres-memory-engineer.js +633 -0
- package/lib/agents/memory-engineers/qdrant-memory-engineer.js +358 -0
- package/lib/agents/memory-engineers/redis-memory-engineer.js +383 -0
- package/lib/agents/memory-supervisor.js +526 -0
- package/lib/agents/registry.js +135 -0
- package/lib/auto-monitor.js +131 -0
- package/lib/checkpoint-hook.js +112 -0
- package/lib/checkpoint.js +319 -0
- package/lib/commentator.js +213 -0
- package/lib/context-scribe.js +120 -0
- package/lib/delegation-strategies.js +326 -0
- package/lib/hierarchy-validator.js +643 -0
- package/lib/index.js +15 -0
- package/lib/init-with-mode.js +261 -0
- package/lib/init.js +44 -6
- package/lib/memory-result-aggregator.js +252 -0
- package/lib/memory.js +35 -7
- package/lib/mode-enforcer.js +473 -0
- package/lib/onyx-banner.js +169 -0
- package/lib/onyx-identity.js +214 -0
- package/lib/onyx-monitor.js +381 -0
- package/lib/onyx-reminder.js +188 -0
- package/lib/onyx-tool-interceptor.js +341 -0
- package/lib/onyx-wrapper.js +315 -0
- package/lib/orchestrator-gate.js +334 -0
- package/lib/output-formatter.js +296 -0
- package/lib/postgres.js +1 -1
- package/lib/prompt-injector.js +220 -0
- package/lib/prompts.js +532 -0
- package/lib/session.js +153 -6
- package/lib/setup/README.md +187 -0
- package/lib/setup/env-manager.js +785 -0
- package/lib/setup/error-recovery.js +630 -0
- package/lib/setup/explain-scopes.js +385 -0
- package/lib/setup/github-instructions.js +333 -0
- package/lib/setup/github-repo.js +254 -0
- package/lib/setup/import-credentials.js +498 -0
- package/lib/setup/index.js +62 -0
- package/lib/setup/init-postgres.js +785 -0
- package/lib/setup/init-redis.js +456 -0
- package/lib/setup/integration-test.js +652 -0
- package/lib/setup/progress.js +357 -0
- package/lib/setup/rollback.js +670 -0
- package/lib/setup/rollback.test.js +452 -0
- package/lib/setup/setup-with-rollback.example.js +351 -0
- package/lib/setup/summary.js +400 -0
- package/lib/setup/test-github-setup.js +10 -0
- package/lib/setup/test-postgres-init.js +98 -0
- package/lib/setup/verify-setup.js +102 -0
- package/lib/task-agent-worker.js +235 -0
- package/lib/token-monitor.js +466 -0
- package/lib/tool-wrapper-integration.js +369 -0
- package/lib/tool-wrapper.js +387 -0
- package/lib/validators/README.md +497 -0
- package/lib/validators/config.js +583 -0
- package/lib/validators/config.test.js +175 -0
- package/lib/validators/github.js +310 -0
- package/lib/validators/github.test.js +61 -0
- package/lib/validators/index.js +15 -0
- package/lib/validators/postgres.js +525 -0
- package/package.json +98 -13
- package/scripts/benchmark-memory.js +433 -0
- package/scripts/check-secrets.sh +12 -0
- package/scripts/fetch-todos.mjs +148 -0
- package/scripts/graceful-shutdown.sh +156 -0
- package/scripts/install-onyx-hooks.js +373 -0
- package/scripts/install.js +119 -18
- package/scripts/redis-monitor.js +284 -0
- package/scripts/redis-setup.js +412 -0
- package/scripts/test-memory-retrieval.js +201 -0
- package/scripts/validate-exports.js +68 -0
- package/scripts/validate-package.js +120 -0
- package/scripts/verify-onyx-deployment.js +309 -0
- package/scripts/verify-redis-deployment.js +354 -0
- package/scripts/verify-redis-init.js +219 -0
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REDIS-BASED MODE ENFORCEMENT SYSTEM
|
|
3
|
+
*
|
|
4
|
+
* Purpose: Store "mode=orchestrator" flag in Redis and check BEFORE every action
|
|
5
|
+
* Architecture: Fail-safe gate that prevents operations outside current mode
|
|
6
|
+
*
|
|
7
|
+
* Key Design Principles:
|
|
8
|
+
* 1. Single source of truth in Redis
|
|
9
|
+
* 2. Atomic check-and-execute pattern
|
|
10
|
+
* 3. TTL-based auto-expiration (safety fallback)
|
|
11
|
+
* 4. Zero-downtime mode switching
|
|
12
|
+
* 5. Audit trail of all mode changes
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import Redis from 'ioredis';
|
|
16
|
+
import { fileURLToPath } from 'url';
|
|
17
|
+
import { dirname, join } from 'path';
|
|
18
|
+
import { existsSync } from 'fs';
|
|
19
|
+
import os from 'os';
|
|
20
|
+
import dotenv from 'dotenv';
|
|
21
|
+
|
|
22
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
23
|
+
const __dirname = dirname(__filename);
|
|
24
|
+
|
|
25
|
+
// Load environment variables
|
|
26
|
+
const envPath = join(os.homedir(), '.boss-claude', '.env');
|
|
27
|
+
if (existsSync(envPath)) {
|
|
28
|
+
dotenv.config({ path: envPath });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ==========================================
|
|
32
|
+
// REDIS CONNECTION SINGLETON
|
|
33
|
+
// ==========================================
|
|
34
|
+
let redis = null;
|
|
35
|
+
|
|
36
|
+
function getRedis() {
|
|
37
|
+
if (!redis) {
|
|
38
|
+
if (!process.env.REDIS_URL) {
|
|
39
|
+
throw new Error('REDIS_URL not found. Run: boss-claude init');
|
|
40
|
+
}
|
|
41
|
+
redis = new Redis(process.env.REDIS_URL);
|
|
42
|
+
}
|
|
43
|
+
return redis;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ==========================================
|
|
47
|
+
// REDIS KEY ARCHITECTURE
|
|
48
|
+
// ==========================================
|
|
49
|
+
const KEYS = {
|
|
50
|
+
// Current active mode (orchestrator|specialist|worker)
|
|
51
|
+
MODE: 'boss:mode:current',
|
|
52
|
+
|
|
53
|
+
// Mode metadata (who set it, when, why)
|
|
54
|
+
MODE_META: 'boss:mode:metadata',
|
|
55
|
+
|
|
56
|
+
// Mode change history (sorted set by timestamp)
|
|
57
|
+
MODE_HISTORY: 'boss:mode:history',
|
|
58
|
+
|
|
59
|
+
// Action counter per mode (metrics)
|
|
60
|
+
MODE_ACTIONS: (mode) => `boss:mode:${mode}:actions`,
|
|
61
|
+
|
|
62
|
+
// Blocked action log (when mode check fails)
|
|
63
|
+
BLOCKED_ACTIONS: 'boss:mode:blocked_actions',
|
|
64
|
+
|
|
65
|
+
// Active session tracking
|
|
66
|
+
SESSION: 'boss:session:current',
|
|
67
|
+
|
|
68
|
+
// Agent identity (which agent is running)
|
|
69
|
+
AGENT_IDENTITY: 'boss:agent:identity'
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// ==========================================
|
|
73
|
+
// MODE DEFINITIONS
|
|
74
|
+
// ==========================================
|
|
75
|
+
const MODES = {
|
|
76
|
+
ORCHESTRATOR: 'orchestrator', // Meta-boss coordinating agents
|
|
77
|
+
SPECIALIST: 'specialist', // Domain expert (n8n, postgres, redis, etc)
|
|
78
|
+
WORKER: 'worker', // Individual task execution
|
|
79
|
+
REVIEW: 'review', // Code review mode
|
|
80
|
+
LEARNING: 'learning' // Training/improvement mode
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// Mode capabilities matrix
|
|
84
|
+
const MODE_CAPABILITIES = {
|
|
85
|
+
[MODES.ORCHESTRATOR]: {
|
|
86
|
+
canDelegate: true,
|
|
87
|
+
canReview: true,
|
|
88
|
+
canExecute: true,
|
|
89
|
+
canLearn: true,
|
|
90
|
+
canModifyGlobalConfig: true,
|
|
91
|
+
canSwitchMode: true,
|
|
92
|
+
maxTokenBudget: 100000,
|
|
93
|
+
requiresApproval: false
|
|
94
|
+
},
|
|
95
|
+
[MODES.SPECIALIST]: {
|
|
96
|
+
canDelegate: false,
|
|
97
|
+
canReview: false,
|
|
98
|
+
canExecute: true,
|
|
99
|
+
canLearn: true,
|
|
100
|
+
canModifyGlobalConfig: false,
|
|
101
|
+
canSwitchMode: false,
|
|
102
|
+
maxTokenBudget: 50000,
|
|
103
|
+
requiresApproval: true
|
|
104
|
+
},
|
|
105
|
+
[MODES.WORKER]: {
|
|
106
|
+
canDelegate: false,
|
|
107
|
+
canReview: false,
|
|
108
|
+
canExecute: true,
|
|
109
|
+
canLearn: false,
|
|
110
|
+
canModifyGlobalConfig: false,
|
|
111
|
+
canSwitchMode: false,
|
|
112
|
+
maxTokenBudget: 20000,
|
|
113
|
+
requiresApproval: true
|
|
114
|
+
},
|
|
115
|
+
[MODES.REVIEW]: {
|
|
116
|
+
canDelegate: false,
|
|
117
|
+
canReview: true,
|
|
118
|
+
canExecute: false,
|
|
119
|
+
canLearn: true,
|
|
120
|
+
canModifyGlobalConfig: false,
|
|
121
|
+
canSwitchMode: false,
|
|
122
|
+
maxTokenBudget: 30000,
|
|
123
|
+
requiresApproval: false
|
|
124
|
+
},
|
|
125
|
+
[MODES.LEARNING]: {
|
|
126
|
+
canDelegate: false,
|
|
127
|
+
canReview: false,
|
|
128
|
+
canExecute: false,
|
|
129
|
+
canLearn: true,
|
|
130
|
+
canModifyGlobalConfig: false,
|
|
131
|
+
canSwitchMode: false,
|
|
132
|
+
maxTokenBudget: 15000,
|
|
133
|
+
requiresApproval: false
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// ==========================================
|
|
138
|
+
// MODE ENFORCEMENT CLASS
|
|
139
|
+
// ==========================================
|
|
140
|
+
export class ModeEnforcer {
|
|
141
|
+
constructor() {
|
|
142
|
+
this.redis = getRedis();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* SET MODE - Atomic operation with metadata
|
|
147
|
+
*
|
|
148
|
+
* @param {string} mode - Mode to activate (orchestrator|specialist|worker|review|learning)
|
|
149
|
+
* @param {object} metadata - Who, when, why
|
|
150
|
+
* @param {number} ttl - TTL in seconds (default 86400 = 24h, prevents stuck modes)
|
|
151
|
+
*/
|
|
152
|
+
async setMode(mode, metadata = {}, ttl = 86400) {
|
|
153
|
+
if (!Object.values(MODES).includes(mode)) {
|
|
154
|
+
throw new Error(`Invalid mode: ${mode}. Valid modes: ${Object.values(MODES).join(', ')}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const client = this.redis;
|
|
158
|
+
const timestamp = Date.now();
|
|
159
|
+
|
|
160
|
+
const modeData = {
|
|
161
|
+
mode,
|
|
162
|
+
setAt: new Date().toISOString(),
|
|
163
|
+
setBy: metadata.agent || 'boss-claude',
|
|
164
|
+
reason: metadata.reason || 'mode-change',
|
|
165
|
+
sessionId: metadata.sessionId || null,
|
|
166
|
+
ttl
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
// ATOMIC MULTI OPERATION
|
|
170
|
+
const pipeline = client.multi();
|
|
171
|
+
|
|
172
|
+
// 1. Set current mode with TTL
|
|
173
|
+
pipeline.set(KEYS.MODE, mode, 'EX', ttl);
|
|
174
|
+
|
|
175
|
+
// 2. Store metadata
|
|
176
|
+
pipeline.set(KEYS.MODE_META, JSON.stringify(modeData));
|
|
177
|
+
|
|
178
|
+
// 3. Add to history (sorted set by timestamp)
|
|
179
|
+
pipeline.zadd(KEYS.MODE_HISTORY, timestamp, JSON.stringify(modeData));
|
|
180
|
+
|
|
181
|
+
// 4. Trim history to last 1000 entries
|
|
182
|
+
pipeline.zremrangebyrank(KEYS.MODE_HISTORY, 0, -1001);
|
|
183
|
+
|
|
184
|
+
await pipeline.exec();
|
|
185
|
+
|
|
186
|
+
console.log(`[MODE ENFORCER] Mode set to: ${mode}`);
|
|
187
|
+
console.log(`[MODE ENFORCER] Metadata:`, modeData);
|
|
188
|
+
|
|
189
|
+
return modeData;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* GET CURRENT MODE - Single source of truth
|
|
194
|
+
*/
|
|
195
|
+
async getCurrentMode() {
|
|
196
|
+
const client = this.redis;
|
|
197
|
+
const mode = await client.get(KEYS.MODE);
|
|
198
|
+
|
|
199
|
+
if (!mode) {
|
|
200
|
+
// Default to WORKER mode if not set (safe default)
|
|
201
|
+
console.warn('[MODE ENFORCER] No mode set. Defaulting to WORKER mode.');
|
|
202
|
+
await this.setMode(MODES.WORKER, { reason: 'default-initialization' });
|
|
203
|
+
return MODES.WORKER;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return mode;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* GET MODE METADATA - Who set it, when, why
|
|
211
|
+
*/
|
|
212
|
+
async getModeMetadata() {
|
|
213
|
+
const client = this.redis;
|
|
214
|
+
const metadata = await client.get(KEYS.MODE_META);
|
|
215
|
+
return metadata ? JSON.parse(metadata) : null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* CHECK MODE - Gate check before every action
|
|
220
|
+
*
|
|
221
|
+
* @param {string} requiredMode - Mode required for this action
|
|
222
|
+
* @param {string} action - Description of action being attempted
|
|
223
|
+
* @throws {Error} if current mode doesn't match required mode
|
|
224
|
+
*/
|
|
225
|
+
async checkMode(requiredMode, action = 'action') {
|
|
226
|
+
const currentMode = await this.getCurrentMode();
|
|
227
|
+
|
|
228
|
+
if (currentMode !== requiredMode) {
|
|
229
|
+
// Log blocked action
|
|
230
|
+
await this.logBlockedAction(currentMode, requiredMode, action);
|
|
231
|
+
|
|
232
|
+
throw new Error(
|
|
233
|
+
`[MODE ENFORCEMENT] Action blocked!\n` +
|
|
234
|
+
`Current mode: ${currentMode}\n` +
|
|
235
|
+
`Required mode: ${requiredMode}\n` +
|
|
236
|
+
`Action: ${action}\n` +
|
|
237
|
+
`Switch mode with: boss-claude mode ${requiredMode}`
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Increment action counter for this mode
|
|
242
|
+
await this.incrementActionCounter(currentMode);
|
|
243
|
+
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* CHECK CAPABILITY - Verify if current mode allows this capability
|
|
249
|
+
*
|
|
250
|
+
* @param {string} capability - Capability name (canDelegate, canReview, etc)
|
|
251
|
+
* @param {string} action - Description of action
|
|
252
|
+
* @throws {Error} if current mode lacks this capability
|
|
253
|
+
*/
|
|
254
|
+
async checkCapability(capability, action = 'action') {
|
|
255
|
+
const currentMode = await this.getCurrentMode();
|
|
256
|
+
const capabilities = MODE_CAPABILITIES[currentMode];
|
|
257
|
+
|
|
258
|
+
if (!capabilities) {
|
|
259
|
+
throw new Error(`Unknown mode: ${currentMode}`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (!capabilities[capability]) {
|
|
263
|
+
await this.logBlockedAction(currentMode, `capability:${capability}`, action);
|
|
264
|
+
|
|
265
|
+
throw new Error(
|
|
266
|
+
`[CAPABILITY ENFORCEMENT] Action blocked!\n` +
|
|
267
|
+
`Current mode: ${currentMode}\n` +
|
|
268
|
+
`Required capability: ${capability}\n` +
|
|
269
|
+
`Action: ${action}\n` +
|
|
270
|
+
`This mode does not have ${capability} capability.`
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return true;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* ENFORCE TOKEN BUDGET - Check if action fits within mode's token budget
|
|
279
|
+
*
|
|
280
|
+
* @param {number} estimatedTokens - Estimated token cost
|
|
281
|
+
* @param {string} action - Description of action
|
|
282
|
+
*/
|
|
283
|
+
async enforceTokenBudget(estimatedTokens, action = 'action') {
|
|
284
|
+
const currentMode = await this.getCurrentMode();
|
|
285
|
+
const capabilities = MODE_CAPABILITIES[currentMode];
|
|
286
|
+
|
|
287
|
+
if (estimatedTokens > capabilities.maxTokenBudget) {
|
|
288
|
+
await this.logBlockedAction(currentMode, 'token-budget-exceeded', action);
|
|
289
|
+
|
|
290
|
+
throw new Error(
|
|
291
|
+
`[TOKEN BUDGET ENFORCEMENT] Action blocked!\n` +
|
|
292
|
+
`Current mode: ${currentMode}\n` +
|
|
293
|
+
`Max budget: ${capabilities.maxTokenBudget} tokens\n` +
|
|
294
|
+
`Estimated cost: ${estimatedTokens} tokens\n` +
|
|
295
|
+
`Action: ${action}\n` +
|
|
296
|
+
`Consider delegating to a specialist or switching to orchestrator mode.`
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return true;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* LOG BLOCKED ACTION - Audit trail
|
|
305
|
+
*/
|
|
306
|
+
async logBlockedAction(currentMode, requiredMode, action) {
|
|
307
|
+
const client = this.redis;
|
|
308
|
+
const timestamp = Date.now();
|
|
309
|
+
|
|
310
|
+
const blockData = {
|
|
311
|
+
timestamp: new Date().toISOString(),
|
|
312
|
+
currentMode,
|
|
313
|
+
requiredMode,
|
|
314
|
+
action,
|
|
315
|
+
stackTrace: new Error().stack
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
await client.zadd(
|
|
319
|
+
KEYS.BLOCKED_ACTIONS,
|
|
320
|
+
timestamp,
|
|
321
|
+
JSON.stringify(blockData)
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
// Keep last 500 blocked actions
|
|
325
|
+
await client.zremrangebyrank(KEYS.BLOCKED_ACTIONS, 0, -501);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* INCREMENT ACTION COUNTER - Track actions per mode
|
|
330
|
+
*/
|
|
331
|
+
async incrementActionCounter(mode) {
|
|
332
|
+
const client = this.redis;
|
|
333
|
+
await client.incr(KEYS.MODE_ACTIONS(mode));
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* GET MODE HISTORY - Last N mode changes
|
|
338
|
+
*/
|
|
339
|
+
async getModeHistory(limit = 20) {
|
|
340
|
+
const client = this.redis;
|
|
341
|
+
const history = await client.zrevrange(KEYS.MODE_HISTORY, 0, limit - 1);
|
|
342
|
+
return history.map(entry => JSON.parse(entry));
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* GET BLOCKED ACTIONS - Last N blocked actions
|
|
347
|
+
*/
|
|
348
|
+
async getBlockedActions(limit = 20) {
|
|
349
|
+
const client = this.redis;
|
|
350
|
+
const blocked = await client.zrevrange(KEYS.BLOCKED_ACTIONS, 0, limit - 1);
|
|
351
|
+
return blocked.map(entry => JSON.parse(entry));
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* GET MODE STATS - Action counts per mode
|
|
356
|
+
*/
|
|
357
|
+
async getModeStats() {
|
|
358
|
+
const client = this.redis;
|
|
359
|
+
const stats = {};
|
|
360
|
+
|
|
361
|
+
for (const mode of Object.values(MODES)) {
|
|
362
|
+
const count = await client.get(KEYS.MODE_ACTIONS(mode));
|
|
363
|
+
stats[mode] = parseInt(count || 0);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return stats;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* RESET MODE - Emergency reset to safe default
|
|
371
|
+
*/
|
|
372
|
+
async resetMode() {
|
|
373
|
+
await this.setMode(MODES.WORKER, {
|
|
374
|
+
reason: 'emergency-reset',
|
|
375
|
+
agent: 'system'
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* SET AGENT IDENTITY - Track which agent is running
|
|
381
|
+
*/
|
|
382
|
+
async setAgentIdentity(agentName, domain = null) {
|
|
383
|
+
const client = this.redis;
|
|
384
|
+
const identity = {
|
|
385
|
+
agent: agentName,
|
|
386
|
+
domain,
|
|
387
|
+
setAt: new Date().toISOString()
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
await client.set(KEYS.AGENT_IDENTITY, JSON.stringify(identity));
|
|
391
|
+
return identity;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* GET AGENT IDENTITY - Who is running right now
|
|
396
|
+
*/
|
|
397
|
+
async getAgentIdentity() {
|
|
398
|
+
const client = this.redis;
|
|
399
|
+
const identity = await client.get(KEYS.AGENT_IDENTITY);
|
|
400
|
+
return identity ? JSON.parse(identity) : null;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// ==========================================
|
|
405
|
+
// CONVENIENCE EXPORTS
|
|
406
|
+
// ==========================================
|
|
407
|
+
export { MODES, MODE_CAPABILITIES };
|
|
408
|
+
|
|
409
|
+
// Singleton instance
|
|
410
|
+
let enforcerInstance = null;
|
|
411
|
+
|
|
412
|
+
export function getEnforcer() {
|
|
413
|
+
if (!enforcerInstance) {
|
|
414
|
+
enforcerInstance = new ModeEnforcer();
|
|
415
|
+
}
|
|
416
|
+
return enforcerInstance;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// ==========================================
|
|
420
|
+
// USAGE EXAMPLES
|
|
421
|
+
// ==========================================
|
|
422
|
+
|
|
423
|
+
/*
|
|
424
|
+
|
|
425
|
+
// Example 1: Set orchestrator mode
|
|
426
|
+
const enforcer = getEnforcer();
|
|
427
|
+
await enforcer.setMode(MODES.ORCHESTRATOR, {
|
|
428
|
+
agent: 'boss-claude',
|
|
429
|
+
reason: 'session-start',
|
|
430
|
+
sessionId: 'session-12345'
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
// Example 2: Gate check before action
|
|
434
|
+
try {
|
|
435
|
+
await enforcer.checkMode(MODES.ORCHESTRATOR, 'Delegating task to postgres-specialist');
|
|
436
|
+
// Action proceeds
|
|
437
|
+
console.log('✅ Action allowed');
|
|
438
|
+
} catch (error) {
|
|
439
|
+
// Action blocked
|
|
440
|
+
console.error('❌', error.message);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Example 3: Check capability
|
|
444
|
+
try {
|
|
445
|
+
await enforcer.checkCapability('canDelegate', 'Assigning task to n8n-workflow-architect');
|
|
446
|
+
// Delegation allowed
|
|
447
|
+
} catch (error) {
|
|
448
|
+
// Delegation blocked
|
|
449
|
+
console.error('❌', error.message);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Example 4: Token budget enforcement
|
|
453
|
+
try {
|
|
454
|
+
await enforcer.enforceTokenBudget(75000, 'Generating architecture document');
|
|
455
|
+
// Within budget
|
|
456
|
+
} catch (error) {
|
|
457
|
+
// Over budget - should delegate
|
|
458
|
+
console.error('❌', error.message);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Example 5: Get mode history
|
|
462
|
+
const history = await enforcer.getModeHistory(10);
|
|
463
|
+
console.log('Mode changes:', history);
|
|
464
|
+
|
|
465
|
+
// Example 6: Get blocked actions (security audit)
|
|
466
|
+
const blocked = await enforcer.getBlockedActions(10);
|
|
467
|
+
console.log('Recent blocked actions:', blocked);
|
|
468
|
+
|
|
469
|
+
// Example 7: Get mode stats
|
|
470
|
+
const stats = await enforcer.getModeStats();
|
|
471
|
+
console.log('Actions per mode:', stats);
|
|
472
|
+
|
|
473
|
+
*/
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ONYX MODE Banner System
|
|
3
|
+
* Displays "ONYX MODE: DELEGATE ONLY" banner at conversation start and every 10 messages
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* ASCII art banner for ONYX MODE
|
|
10
|
+
*/
|
|
11
|
+
const ONYX_BANNER = `
|
|
12
|
+
╔═══════════════════════════════════════════════════════════════════════════════╗
|
|
13
|
+
║ ║
|
|
14
|
+
║ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄ ▄ ▄ ▄ ▄▄ ▄▄ ║
|
|
15
|
+
║ ▐░░░░░░░░░░░▌▐░░▌ ▐░▌▐░▌ ▐░▌▐░░▌ ▐░░▌ ║
|
|
16
|
+
║ ▐░█▀▀▀▀▀▀▀█░▌▐░▌░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌░▌ ▐░▐░▌ ║
|
|
17
|
+
║ ▐░▌ ▐░▌▐░▌▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌▐░▌ ▐░▌▐░▌ ║
|
|
18
|
+
║ ▐░▌ ▐░▌▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▐░▌ ▐░▌ ║
|
|
19
|
+
║ ▐░▌ ▐░▌▐░▌ ▐░▌ ▐░▌ ▐░▐░▌ ▐░▌ ▐░▌ ▐░▌ ║
|
|
20
|
+
║ ▐░▌ ▐░▌▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▀ ▐░▌ ║
|
|
21
|
+
║ ▐░▌ ▐░▌▐░▌ ▐░▌▐░▌ ▐░▌░▌ ▐░▌ ▐░▌ ║
|
|
22
|
+
║ ▐░█▄▄▄▄▄▄▄█░▌▐░▌ ▐░▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ║
|
|
23
|
+
║ ▐░░░░░░░░░░░▌▐░▌ ▐░░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ║
|
|
24
|
+
║ ▀▀▀▀▀▀▀▀▀▀▀ ▀ ▀▀ ▀ ▀ ▀ ▀ ║
|
|
25
|
+
║ ║
|
|
26
|
+
║ ███╗ ███╗ ██████╗ ██████╗ ███████╗ ║
|
|
27
|
+
║ ████╗ ████║██╔═══██╗██╔══██╗██╔════╝ ║
|
|
28
|
+
║ ██╔████╔██║██║ ██║██║ ██║█████╗ ║
|
|
29
|
+
║ ██║╚██╔╝██║██║ ██║██║ ██║██╔══╝ ║
|
|
30
|
+
║ ██║ ╚═╝ ██║╚██████╔╝██████╔╝███████╗ ║
|
|
31
|
+
║ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝ ║
|
|
32
|
+
║ ║
|
|
33
|
+
║ DELEGATE ONLY - NO EXECUTION ║
|
|
34
|
+
║ ║
|
|
35
|
+
╚═══════════════════════════════════════════════════════════════════════════════╝
|
|
36
|
+
`;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Compact version of banner for periodic display
|
|
40
|
+
*/
|
|
41
|
+
const ONYX_BANNER_COMPACT = `
|
|
42
|
+
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
|
43
|
+
┃ ⬛ ONYX MODE: DELEGATE ONLY ⬛ No task execution | Agent delegation only ┃
|
|
44
|
+
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
45
|
+
`;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Message counter for periodic banner display
|
|
49
|
+
*/
|
|
50
|
+
let messageCount = 0;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Display the full ONYX MODE banner (for conversation start)
|
|
54
|
+
* @param {boolean} colorize - Whether to apply color styling
|
|
55
|
+
* @returns {string} Formatted banner
|
|
56
|
+
*/
|
|
57
|
+
export function displayOnyxBanner(colorize = true) {
|
|
58
|
+
if (colorize) {
|
|
59
|
+
return chalk.black.bgWhite(ONYX_BANNER);
|
|
60
|
+
}
|
|
61
|
+
return ONYX_BANNER;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Display the compact ONYX MODE banner (for periodic reminders)
|
|
66
|
+
* @param {boolean} colorize - Whether to apply color styling
|
|
67
|
+
* @returns {string} Formatted compact banner
|
|
68
|
+
*/
|
|
69
|
+
export function displayOnyxBannerCompact(colorize = true) {
|
|
70
|
+
if (colorize) {
|
|
71
|
+
return chalk.black.bgWhite(ONYX_BANNER_COMPACT);
|
|
72
|
+
}
|
|
73
|
+
return ONYX_BANNER_COMPACT;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Check if banner should be displayed based on message count
|
|
78
|
+
* Displays at start (count = 0) and every 10 messages
|
|
79
|
+
* @returns {boolean} True if banner should be shown
|
|
80
|
+
*/
|
|
81
|
+
export function shouldDisplayBanner() {
|
|
82
|
+
if (messageCount === 0 || messageCount % 10 === 0) {
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Increment message counter
|
|
90
|
+
* Call this after each user message
|
|
91
|
+
*/
|
|
92
|
+
export function incrementMessageCount() {
|
|
93
|
+
messageCount++;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Reset message counter
|
|
98
|
+
* Call this at conversation start
|
|
99
|
+
*/
|
|
100
|
+
export function resetMessageCount() {
|
|
101
|
+
messageCount = 0;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Get current message count
|
|
106
|
+
* @returns {number} Current message count
|
|
107
|
+
*/
|
|
108
|
+
export function getMessageCount() {
|
|
109
|
+
return messageCount;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Auto-display banner with message tracking
|
|
114
|
+
* Call this function after each message
|
|
115
|
+
* @param {boolean} isConversationStart - True if this is the first message
|
|
116
|
+
* @param {boolean} colorize - Whether to apply color styling
|
|
117
|
+
* @returns {string|null} Banner to display, or null if not needed
|
|
118
|
+
*/
|
|
119
|
+
export function autoDisplayBanner(isConversationStart = false, colorize = true) {
|
|
120
|
+
if (isConversationStart) {
|
|
121
|
+
resetMessageCount();
|
|
122
|
+
return displayOnyxBanner(colorize);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
incrementMessageCount();
|
|
126
|
+
|
|
127
|
+
if (shouldDisplayBanner()) {
|
|
128
|
+
return displayOnyxBannerCompact(colorize);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Format banner for injection into CLI output
|
|
136
|
+
* Returns formatted string that can be console.log'd
|
|
137
|
+
* @param {boolean} isConversationStart - True if this is the first message
|
|
138
|
+
* @returns {string} Formatted banner ready for output
|
|
139
|
+
*/
|
|
140
|
+
export function formatForCLI(isConversationStart = false) {
|
|
141
|
+
const banner = autoDisplayBanner(isConversationStart, true);
|
|
142
|
+
|
|
143
|
+
if (!banner) {
|
|
144
|
+
return '';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return `\n${banner}\n`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Get banner with statistics
|
|
152
|
+
* Displays banner plus message count info
|
|
153
|
+
* @param {boolean} isConversationStart - True if this is the first message
|
|
154
|
+
* @returns {string} Banner with stats
|
|
155
|
+
*/
|
|
156
|
+
export function getBannerWithStats(isConversationStart = false) {
|
|
157
|
+
const banner = autoDisplayBanner(isConversationStart, true);
|
|
158
|
+
|
|
159
|
+
if (!banner) {
|
|
160
|
+
return '';
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const stats = chalk.dim(`Message count: ${messageCount} | Next banner at: ${Math.ceil(messageCount / 10) * 10}`);
|
|
164
|
+
|
|
165
|
+
return `\n${banner}\n${stats}\n`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Export constants for direct use
|
|
169
|
+
export { ONYX_BANNER, ONYX_BANNER_COMPACT };
|