@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,213 @@
|
|
|
1
|
+
import { watch } from 'fs';
|
|
2
|
+
import { readFileSync, existsSync, readdirSync, statSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import { logAgent } from './agent-logger.js';
|
|
6
|
+
import { getEfficiencyStats } from './session.js';
|
|
7
|
+
|
|
8
|
+
const TASK_DIR = '/private/tmp/claude/-Users-craigpretzinger-projects-BOSS-claude/tasks';
|
|
9
|
+
const WATCHED_FILES = new Map();
|
|
10
|
+
const MAX_CONTEXT = 200000; // Claude's context window size
|
|
11
|
+
|
|
12
|
+
function parseActivityLine(line) {
|
|
13
|
+
try {
|
|
14
|
+
const json = JSON.parse(line);
|
|
15
|
+
|
|
16
|
+
if (json.type === 'user' && json.message?.role === 'user') {
|
|
17
|
+
const content = json.message.content;
|
|
18
|
+
if (typeof content === 'string' && content.length > 0) {
|
|
19
|
+
// NO word wrap here - wrapping happens at display time in watch command
|
|
20
|
+
// Storing multi-line text in log breaks the log format
|
|
21
|
+
return `Task: ${content}`;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (json.type === 'assistant' && json.message?.role === 'assistant') {
|
|
26
|
+
const content = json.message.content;
|
|
27
|
+
if (Array.isArray(content)) {
|
|
28
|
+
for (const item of content) {
|
|
29
|
+
// Handle text responses (agent output)
|
|
30
|
+
if (item.type === 'text' && item.text) {
|
|
31
|
+
// NO word wrap here - wrapping happens at display time in watch command
|
|
32
|
+
// Storing multi-line text in log breaks the log format
|
|
33
|
+
return `Response: ${item.text}`;
|
|
34
|
+
}
|
|
35
|
+
if (item.type === 'tool_use') {
|
|
36
|
+
const toolName = item.name;
|
|
37
|
+
const input = item.input;
|
|
38
|
+
if (toolName === 'Read' && input?.file_path) {
|
|
39
|
+
return `Reading: ${input.file_path.split('/').pop()}`;
|
|
40
|
+
}
|
|
41
|
+
if (toolName === 'Bash' && input?.description) {
|
|
42
|
+
return `Exec: ${input.description}`;
|
|
43
|
+
}
|
|
44
|
+
if (toolName === 'Write' && input?.file_path) {
|
|
45
|
+
return `Writing: ${input.file_path.split('/').pop()}`;
|
|
46
|
+
}
|
|
47
|
+
if (toolName === 'Edit' && input?.file_path) {
|
|
48
|
+
return `Editing: ${input.file_path.split('/').pop()}`;
|
|
49
|
+
}
|
|
50
|
+
return `Using ${toolName}`;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
} catch (err) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function checkFileForActivity(filePath) {
|
|
62
|
+
try {
|
|
63
|
+
if (!existsSync(filePath)) return null;
|
|
64
|
+
|
|
65
|
+
const currentSize = readFileSync(filePath).length;
|
|
66
|
+
const lastSize = WATCHED_FILES.get(filePath) || 0;
|
|
67
|
+
|
|
68
|
+
if (currentSize <= lastSize) {
|
|
69
|
+
WATCHED_FILES.set(filePath, currentSize);
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const content = readFileSync(filePath, 'utf8');
|
|
74
|
+
const lines = content.split('\n').filter(l => l.trim());
|
|
75
|
+
|
|
76
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
77
|
+
const activity = parseActivityLine(lines[i]);
|
|
78
|
+
if (activity) {
|
|
79
|
+
WATCHED_FILES.set(filePath, currentSize);
|
|
80
|
+
return activity;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
WATCHED_FILES.set(filePath, currentSize);
|
|
85
|
+
return null;
|
|
86
|
+
} catch (err) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function getTokenContextStats() {
|
|
92
|
+
try {
|
|
93
|
+
const files = require('fs').readdirSync(TASK_DIR);
|
|
94
|
+
const outputFiles = files.filter(f => f.endsWith('.output'));
|
|
95
|
+
|
|
96
|
+
if (outputFiles.length === 0) return null;
|
|
97
|
+
|
|
98
|
+
// Get the most recent output file
|
|
99
|
+
const latestFile = outputFiles
|
|
100
|
+
.map(f => ({ name: f, path: join(TASK_DIR, f) }))
|
|
101
|
+
.sort((a, b) => {
|
|
102
|
+
try {
|
|
103
|
+
const statA = require('fs').statSync(a.path);
|
|
104
|
+
const statB = require('fs').statSync(b.path);
|
|
105
|
+
return statB.mtimeMs - statA.mtimeMs;
|
|
106
|
+
} catch {
|
|
107
|
+
return 0;
|
|
108
|
+
}
|
|
109
|
+
})[0];
|
|
110
|
+
|
|
111
|
+
if (!latestFile) return null;
|
|
112
|
+
|
|
113
|
+
const content = readFileSync(latestFile.path, 'utf8');
|
|
114
|
+
const lines = content.split('\n').filter(l => l.trim());
|
|
115
|
+
|
|
116
|
+
// Look for token budget in system warnings
|
|
117
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
118
|
+
try {
|
|
119
|
+
const json = JSON.parse(lines[i]);
|
|
120
|
+
if (json.type === 'system_warning' && json.message) {
|
|
121
|
+
const match = json.message.match(/Token usage: (\d+)\/(\d+)/);
|
|
122
|
+
if (match) {
|
|
123
|
+
const used = parseInt(match[1]);
|
|
124
|
+
const total = parseInt(match[2]);
|
|
125
|
+
const percent = ((used / total) * 100).toFixed(1);
|
|
126
|
+
return { used, total, percent };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
} catch {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return null;
|
|
135
|
+
} catch (err) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function startCommentator() {
|
|
141
|
+
console.log('🎙️ Commentator active - watching agents...\n');
|
|
142
|
+
|
|
143
|
+
if (!existsSync(TASK_DIR)) {
|
|
144
|
+
console.error(`Task directory not found: ${TASK_DIR}`);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
logAgent('Commentator', 'ACTIVE', 'Monitoring agent output files');
|
|
149
|
+
|
|
150
|
+
const watcher = watch(TASK_DIR, { recursive: false }, (eventType, filename) => {
|
|
151
|
+
if (filename && filename.endsWith('.output')) {
|
|
152
|
+
const filePath = join(TASK_DIR, filename);
|
|
153
|
+
const activity = checkFileForActivity(filePath);
|
|
154
|
+
if (activity) {
|
|
155
|
+
logAgent('Commentator', 'OBSERVE', activity);
|
|
156
|
+
console.log(`📢 ${activity}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// Poll efficiency stats every 30 seconds
|
|
162
|
+
const pollInterval = setInterval(async () => {
|
|
163
|
+
// Get context stats from task files
|
|
164
|
+
const contextStats = getTokenContextStats();
|
|
165
|
+
|
|
166
|
+
// Get efficiency stats from session
|
|
167
|
+
let effStats = null;
|
|
168
|
+
try {
|
|
169
|
+
effStats = await getEfficiencyStats();
|
|
170
|
+
} catch (e) {
|
|
171
|
+
// Redis might not be available
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
console.log('\n' + '─'.repeat(60));
|
|
175
|
+
console.log('📊 CONDUCTOR DASHBOARD');
|
|
176
|
+
console.log('─'.repeat(60));
|
|
177
|
+
|
|
178
|
+
if (contextStats) {
|
|
179
|
+
const pct = parseFloat(contextStats.percent);
|
|
180
|
+
const color = pct < 50 ? '\x1b[32m' : pct < 80 ? '\x1b[33m' : '\x1b[31m';
|
|
181
|
+
console.log(` Context: ${color}${contextStats.used.toLocaleString()}/${contextStats.total.toLocaleString()} (${contextStats.percent}%)\x1b[0m`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (effStats) {
|
|
185
|
+
const totalTokens = effStats.total_tokens || 0;
|
|
186
|
+
const onyxPct = totalTokens > 0 ? ((effStats.onyx_tokens / totalTokens) * 100).toFixed(1) : '0.0';
|
|
187
|
+
const agentPct = totalTokens > 0 ? ((effStats.agent_tokens / totalTokens) * 100).toFixed(1) : '0.0';
|
|
188
|
+
|
|
189
|
+
console.log(` 🎺 ONYX: ${effStats.onyx_tokens.toLocaleString()} tokens (${onyxPct}%)`);
|
|
190
|
+
console.log(` 🎻 Agents: ${effStats.agent_tokens.toLocaleString()} tokens (${agentPct}%)`);
|
|
191
|
+
console.log(` 📈 Efficiency: ${effStats.efficiency_ratio}`);
|
|
192
|
+
console.log(` 🎯 Delegations: ${effStats.delegations}`);
|
|
193
|
+
console.log(` 💎 Projected XP: +${effStats.projected_bonus_xp + effStats.delegation_bonus_xp}`);
|
|
194
|
+
} else {
|
|
195
|
+
console.log(' (Run boss-claude status to initialize tracking)');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
console.log('─'.repeat(60) + '\n');
|
|
199
|
+
|
|
200
|
+
// Log to agent log
|
|
201
|
+
if (effStats) {
|
|
202
|
+
logAgent('Commentator', 'STATS', `Efficiency: ${effStats.efficiency_ratio}, Delegations: ${effStats.delegations}`);
|
|
203
|
+
}
|
|
204
|
+
}, 30000); // 30 seconds
|
|
205
|
+
|
|
206
|
+
process.on('SIGINT', () => {
|
|
207
|
+
clearInterval(pollInterval);
|
|
208
|
+
watcher.close();
|
|
209
|
+
logAgent('Commentator', 'SHUTDOWN', 'Stopped');
|
|
210
|
+
console.log('\n👋 Commentator stopped');
|
|
211
|
+
process.exit(0);
|
|
212
|
+
});
|
|
213
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
|
|
5
|
+
const BOSS_DIR = join(os.homedir(), '.boss-claude');
|
|
6
|
+
const SCRIBE_FILE = join(BOSS_DIR, 'current-session.json');
|
|
7
|
+
|
|
8
|
+
// Ensure directory exists
|
|
9
|
+
try { mkdirSync(BOSS_DIR, { recursive: true }); } catch (e) {}
|
|
10
|
+
|
|
11
|
+
let scribe = null;
|
|
12
|
+
|
|
13
|
+
export function initScribe() {
|
|
14
|
+
scribe = {
|
|
15
|
+
startTime: Date.now(),
|
|
16
|
+
decisions: [],
|
|
17
|
+
filesChanged: [],
|
|
18
|
+
lessons: [],
|
|
19
|
+
issuesResolved: [],
|
|
20
|
+
bugsFound: []
|
|
21
|
+
};
|
|
22
|
+
saveScribe();
|
|
23
|
+
return scribe;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function getScribe() {
|
|
27
|
+
if (!scribe && existsSync(SCRIBE_FILE)) {
|
|
28
|
+
scribe = JSON.parse(readFileSync(SCRIBE_FILE, 'utf8'));
|
|
29
|
+
}
|
|
30
|
+
return scribe || initScribe();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function saveScribe() {
|
|
34
|
+
if (scribe) {
|
|
35
|
+
writeFileSync(SCRIBE_FILE, JSON.stringify(scribe, null, 2));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function logDecision(decision, reasoning = '') {
|
|
40
|
+
getScribe();
|
|
41
|
+
scribe.decisions.push({ decision, reasoning, timestamp: Date.now() });
|
|
42
|
+
saveScribe();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function logFileChange(filePath, changeType, description = '') {
|
|
46
|
+
getScribe();
|
|
47
|
+
scribe.filesChanged.push({ filePath, changeType, description, timestamp: Date.now() });
|
|
48
|
+
saveScribe();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function logLesson(lesson, category = 'general') {
|
|
52
|
+
getScribe();
|
|
53
|
+
scribe.lessons.push({ lesson, category, timestamp: Date.now() });
|
|
54
|
+
saveScribe();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function logIssueResolved(issue, solution) {
|
|
58
|
+
getScribe();
|
|
59
|
+
scribe.issuesResolved.push({ issue, solution, timestamp: Date.now() });
|
|
60
|
+
saveScribe();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function logBugFound(bug, location, impact = 'medium') {
|
|
64
|
+
getScribe();
|
|
65
|
+
scribe.bugsFound.push({ bug, location, impact, timestamp: Date.now() });
|
|
66
|
+
saveScribe();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function generateSessionSummary() {
|
|
70
|
+
const s = getScribe();
|
|
71
|
+
const duration = Math.round((Date.now() - s.startTime) / 60000);
|
|
72
|
+
|
|
73
|
+
let summary = `## Session Overview\nDuration: ${duration} minutes\n\n`;
|
|
74
|
+
|
|
75
|
+
if (s.decisions.length) {
|
|
76
|
+
summary += `### Decisions (${s.decisions.length})\n`;
|
|
77
|
+
s.decisions.forEach(d => summary += `- ${d.decision}${d.reasoning ? ` (${d.reasoning})` : ''}\n`);
|
|
78
|
+
summary += '\n';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (s.filesChanged.length) {
|
|
82
|
+
summary += `### Files Changed (${s.filesChanged.length})\n`;
|
|
83
|
+
s.filesChanged.forEach(f => summary += `- [${f.changeType}] ${f.filePath}${f.description ? `: ${f.description}` : ''}\n`);
|
|
84
|
+
summary += '\n';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (s.issuesResolved.length) {
|
|
88
|
+
summary += `### Issues Resolved (${s.issuesResolved.length})\n`;
|
|
89
|
+
s.issuesResolved.forEach(i => summary += `- ${i.issue} -> ${i.solution}\n`);
|
|
90
|
+
summary += '\n';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (s.lessons.length) {
|
|
94
|
+
summary += `### Lessons Learned (${s.lessons.length})\n`;
|
|
95
|
+
s.lessons.forEach(l => summary += `- [${l.category}] ${l.lesson}\n`);
|
|
96
|
+
summary += '\n';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
summary,
|
|
101
|
+
stats: {
|
|
102
|
+
duration,
|
|
103
|
+
decisions: s.decisions.length,
|
|
104
|
+
filesChanged: s.filesChanged.length,
|
|
105
|
+
issuesResolved: s.issuesResolved.length,
|
|
106
|
+
lessons: s.lessons.length
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function clearScribe() {
|
|
112
|
+
scribe = null;
|
|
113
|
+
if (existsSync(SCRIBE_FILE)) {
|
|
114
|
+
writeFileSync(SCRIBE_FILE, '{}');
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function exportScribe() {
|
|
119
|
+
return JSON.stringify(getScribe(), null, 2);
|
|
120
|
+
}
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* DELEGATION STRATEGIES
|
|
4
|
+
*
|
|
5
|
+
* Defines auto-delegation strategies for different tool types.
|
|
6
|
+
* Determines when to delegate vs. when to allow direct execution.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Tool categories for delegation
|
|
11
|
+
*/
|
|
12
|
+
export const TOOL_CATEGORIES = {
|
|
13
|
+
// Always delegate to Task agent
|
|
14
|
+
FILE_OPS: ['Read', 'Write', 'Edit', 'NotebookEdit'],
|
|
15
|
+
SEARCH_OPS: ['Grep', 'Glob'],
|
|
16
|
+
EXEC_OPS: ['Bash'],
|
|
17
|
+
|
|
18
|
+
// Sometimes delegate based on complexity
|
|
19
|
+
CONDITIONAL: ['WebFetch', 'WebSearch'],
|
|
20
|
+
|
|
21
|
+
// Never delegate - ONYX can use directly
|
|
22
|
+
ALLOWED: ['Task', 'TodoWrite', 'Skill']
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Complexity thresholds for conditional delegation
|
|
27
|
+
*/
|
|
28
|
+
const COMPLEXITY_THRESHOLDS = {
|
|
29
|
+
// File operations
|
|
30
|
+
file_read_lines: 500, // Auto-delegate reads >500 lines
|
|
31
|
+
file_write_lines: 100, // Auto-delegate writes >100 lines
|
|
32
|
+
|
|
33
|
+
// Search operations
|
|
34
|
+
grep_results: 50, // Auto-delegate if expected >50 results
|
|
35
|
+
glob_patterns: 10, // Auto-delegate complex glob patterns
|
|
36
|
+
|
|
37
|
+
// Bash operations
|
|
38
|
+
bash_command_length: 100, // Auto-delegate long commands
|
|
39
|
+
bash_piped_commands: 2 // Auto-delegate if >2 pipes
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Determine if tool should be delegated
|
|
44
|
+
*
|
|
45
|
+
* @param {string} toolName - Name of tool
|
|
46
|
+
* @param {Object} toolParams - Tool parameters
|
|
47
|
+
* @param {Object} context - Execution context
|
|
48
|
+
* @returns {Object} { shouldDelegate: boolean, reason: string, strategy: string }
|
|
49
|
+
*/
|
|
50
|
+
export function shouldDelegateTool(toolName, toolParams = {}, context = {}) {
|
|
51
|
+
const agentName = context.agentName || 'Unknown';
|
|
52
|
+
|
|
53
|
+
// ONYX agents ALWAYS delegate file/search/exec operations
|
|
54
|
+
const isOnyxAgent = agentName.toUpperCase().includes('ONYX') ||
|
|
55
|
+
agentName === 'BOSS' ||
|
|
56
|
+
agentName === 'META-BOSS';
|
|
57
|
+
|
|
58
|
+
// Check if tool is in always-delegate categories
|
|
59
|
+
const isFileOp = TOOL_CATEGORIES.FILE_OPS.includes(toolName);
|
|
60
|
+
const isSearchOp = TOOL_CATEGORIES.SEARCH_OPS.includes(toolName);
|
|
61
|
+
const isExecOp = TOOL_CATEGORIES.EXEC_OPS.includes(toolName);
|
|
62
|
+
const isAllowed = TOOL_CATEGORIES.ALLOWED.includes(toolName);
|
|
63
|
+
|
|
64
|
+
// Allowed tools - never delegate
|
|
65
|
+
if (isAllowed) {
|
|
66
|
+
return {
|
|
67
|
+
shouldDelegate: false,
|
|
68
|
+
reason: `${toolName} is allowed for ${agentName}`,
|
|
69
|
+
strategy: 'ALLOWED'
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ONYX agents - auto-delegate file/search/exec
|
|
74
|
+
if (isOnyxAgent) {
|
|
75
|
+
if (isFileOp || isSearchOp || isExecOp) {
|
|
76
|
+
return {
|
|
77
|
+
shouldDelegate: true,
|
|
78
|
+
reason: `${agentName} must delegate ${toolName} to Task agent`,
|
|
79
|
+
strategy: 'ONYX_AUTO_DELEGATE',
|
|
80
|
+
delegateTo: 'Task'
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Non-ONYX agents - check complexity thresholds
|
|
86
|
+
if (isFileOp) {
|
|
87
|
+
const complexity = analyzeFileOpComplexity(toolName, toolParams);
|
|
88
|
+
if (complexity.shouldDelegate) {
|
|
89
|
+
return {
|
|
90
|
+
shouldDelegate: true,
|
|
91
|
+
reason: `File operation too complex: ${complexity.reason}`,
|
|
92
|
+
strategy: 'COMPLEXITY_THRESHOLD',
|
|
93
|
+
delegateTo: 'Task',
|
|
94
|
+
complexity
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (isSearchOp) {
|
|
100
|
+
const complexity = analyzeSearchOpComplexity(toolName, toolParams);
|
|
101
|
+
if (complexity.shouldDelegate) {
|
|
102
|
+
return {
|
|
103
|
+
shouldDelegate: true,
|
|
104
|
+
reason: `Search operation too complex: ${complexity.reason}`,
|
|
105
|
+
strategy: 'COMPLEXITY_THRESHOLD',
|
|
106
|
+
delegateTo: 'Task',
|
|
107
|
+
complexity
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (isExecOp) {
|
|
113
|
+
const complexity = analyzeExecOpComplexity(toolName, toolParams);
|
|
114
|
+
if (complexity.shouldDelegate) {
|
|
115
|
+
return {
|
|
116
|
+
shouldDelegate: true,
|
|
117
|
+
reason: `Bash operation too complex: ${complexity.reason}`,
|
|
118
|
+
strategy: 'COMPLEXITY_THRESHOLD',
|
|
119
|
+
delegateTo: 'Task',
|
|
120
|
+
complexity
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Default: allow execution
|
|
126
|
+
return {
|
|
127
|
+
shouldDelegate: false,
|
|
128
|
+
reason: 'Below complexity threshold',
|
|
129
|
+
strategy: 'ALLOW_DIRECT'
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Analyze file operation complexity
|
|
135
|
+
*/
|
|
136
|
+
function analyzeFileOpComplexity(toolName, params) {
|
|
137
|
+
const result = {
|
|
138
|
+
shouldDelegate: false,
|
|
139
|
+
reason: '',
|
|
140
|
+
score: 0
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
if (toolName === 'Read') {
|
|
144
|
+
// Check if file is large (via limit param)
|
|
145
|
+
if (params.limit && params.limit > COMPLEXITY_THRESHOLDS.file_read_lines) {
|
|
146
|
+
result.shouldDelegate = true;
|
|
147
|
+
result.reason = `Reading ${params.limit} lines (threshold: ${COMPLEXITY_THRESHOLDS.file_read_lines})`;
|
|
148
|
+
result.score = 2;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (toolName === 'Write') {
|
|
153
|
+
// Check content length (both line count and character count)
|
|
154
|
+
const content = params.content || '';
|
|
155
|
+
const lineCount = content.split('\n').length;
|
|
156
|
+
const charCount = content.length;
|
|
157
|
+
|
|
158
|
+
if (lineCount > COMPLEXITY_THRESHOLDS.file_write_lines) {
|
|
159
|
+
result.shouldDelegate = true;
|
|
160
|
+
result.reason = `Writing ${lineCount} lines (threshold: ${COMPLEXITY_THRESHOLDS.file_write_lines})`;
|
|
161
|
+
result.score = 2;
|
|
162
|
+
} else if (charCount > 5000) {
|
|
163
|
+
// Large single-line content
|
|
164
|
+
result.shouldDelegate = true;
|
|
165
|
+
result.reason = `Writing large content (${charCount} chars)`;
|
|
166
|
+
result.score = 2;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (toolName === 'Edit') {
|
|
171
|
+
// Check if large replacement
|
|
172
|
+
const oldLength = (params.old_string || '').length;
|
|
173
|
+
const newLength = (params.new_string || '').length;
|
|
174
|
+
if (oldLength > 500 || newLength > 500) {
|
|
175
|
+
result.shouldDelegate = true;
|
|
176
|
+
result.reason = `Large edit operation (${Math.max(oldLength, newLength)} chars)`;
|
|
177
|
+
result.score = 2;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return result;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Analyze search operation complexity
|
|
186
|
+
*/
|
|
187
|
+
function analyzeSearchOpComplexity(toolName, params) {
|
|
188
|
+
const result = {
|
|
189
|
+
shouldDelegate: false,
|
|
190
|
+
reason: '',
|
|
191
|
+
score: 0
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
if (toolName === 'Grep') {
|
|
195
|
+
// Check if complex regex pattern
|
|
196
|
+
const pattern = params.pattern || '';
|
|
197
|
+
const hasAdvancedRegex = /[\[\]\{\}\(\)\|\\\^\$\*\+\?]/.test(pattern);
|
|
198
|
+
const isMultiline = params.multiline === true;
|
|
199
|
+
|
|
200
|
+
if (hasAdvancedRegex && isMultiline) {
|
|
201
|
+
result.shouldDelegate = true;
|
|
202
|
+
result.reason = 'Complex regex with multiline mode';
|
|
203
|
+
result.score = 2;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (toolName === 'Glob') {
|
|
208
|
+
// Check pattern complexity
|
|
209
|
+
const pattern = params.pattern || '';
|
|
210
|
+
const hasMultipleWildcards = (pattern.match(/\*\*/g) || []).length > 2;
|
|
211
|
+
const hasComplexBraces = /\{[^}]+,[^}]+\}/.test(pattern);
|
|
212
|
+
|
|
213
|
+
if (hasMultipleWildcards || hasComplexBraces) {
|
|
214
|
+
result.shouldDelegate = true;
|
|
215
|
+
result.reason = 'Complex glob pattern with multiple wildcards';
|
|
216
|
+
result.score = 2;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return result;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Analyze execution operation complexity
|
|
225
|
+
*/
|
|
226
|
+
function analyzeExecOpComplexity(toolName, params) {
|
|
227
|
+
const result = {
|
|
228
|
+
shouldDelegate: false,
|
|
229
|
+
reason: '',
|
|
230
|
+
score: 0
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
if (toolName === 'Bash') {
|
|
234
|
+
const command = params.command || '';
|
|
235
|
+
|
|
236
|
+
// Check command length
|
|
237
|
+
if (command.length > COMPLEXITY_THRESHOLDS.bash_command_length) {
|
|
238
|
+
result.shouldDelegate = true;
|
|
239
|
+
result.reason = `Long command (${command.length} chars)`;
|
|
240
|
+
result.score = 2;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Check for pipes
|
|
244
|
+
const pipeCount = (command.match(/\|/g) || []).length;
|
|
245
|
+
if (pipeCount > COMPLEXITY_THRESHOLDS.bash_piped_commands) {
|
|
246
|
+
result.shouldDelegate = true;
|
|
247
|
+
result.reason = `Complex piped command (${pipeCount} pipes)`;
|
|
248
|
+
result.score = 2;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Check for dangerous operations
|
|
252
|
+
const hasDangerousOps = /rm -rf|dd |mkfs|format|:(){:|fork/.test(command);
|
|
253
|
+
if (hasDangerousOps) {
|
|
254
|
+
result.shouldDelegate = true;
|
|
255
|
+
result.reason = 'Potentially dangerous operation detected';
|
|
256
|
+
result.score = 3;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return result;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Generate delegation message for Task tool
|
|
265
|
+
*/
|
|
266
|
+
export function generateDelegationMessage(toolName, toolParams, decision) {
|
|
267
|
+
const messages = {
|
|
268
|
+
Read: `Read file: ${toolParams.file_path || '[file_path]'}${toolParams.limit ? ` (${toolParams.limit} lines)` : ''}`,
|
|
269
|
+
Write: `Write to file: ${toolParams.file_path || '[file_path]'}`,
|
|
270
|
+
Edit: `Edit file: ${toolParams.file_path || '[file_path]'}`,
|
|
271
|
+
Bash: `Execute command: ${toolParams.command || ''}`,
|
|
272
|
+
Grep: `Search for pattern "${toolParams.pattern || '[pattern]'}" ${toolParams.path ? `in ${toolParams.path}` : 'in codebase'}`,
|
|
273
|
+
Glob: `Find files matching "${toolParams.pattern || '[pattern]'}"`
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const baseMessage = messages[toolName] || `Execute ${toolName} operation`;
|
|
277
|
+
const reason = decision.reason || 'Auto-delegation by Tool Wrapper';
|
|
278
|
+
|
|
279
|
+
return {
|
|
280
|
+
description: `AUTO-DELEGATED: ${baseMessage}`,
|
|
281
|
+
details: reason,
|
|
282
|
+
originalTool: toolName,
|
|
283
|
+
originalParams: toolParams,
|
|
284
|
+
strategy: decision.strategy
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Get delegation statistics
|
|
290
|
+
*/
|
|
291
|
+
export function getDelegationStats(delegationLog = []) {
|
|
292
|
+
const stats = {
|
|
293
|
+
total: delegationLog.length,
|
|
294
|
+
delegated: 0,
|
|
295
|
+
direct: 0,
|
|
296
|
+
byStrategy: {},
|
|
297
|
+
byTool: {},
|
|
298
|
+
delegationRate: 0
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
delegationLog.forEach(entry => {
|
|
302
|
+
if (entry.delegated) {
|
|
303
|
+
stats.delegated++;
|
|
304
|
+
const strategy = entry.strategy || 'UNKNOWN';
|
|
305
|
+
stats.byStrategy[strategy] = (stats.byStrategy[strategy] || 0) + 1;
|
|
306
|
+
} else {
|
|
307
|
+
stats.direct++;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const tool = entry.tool || 'UNKNOWN';
|
|
311
|
+
stats.byTool[tool] = (stats.byTool[tool] || 0) + 1;
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
stats.delegationRate = stats.total > 0
|
|
315
|
+
? (stats.delegated / stats.total * 100).toFixed(2)
|
|
316
|
+
: 0;
|
|
317
|
+
|
|
318
|
+
return stats;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export default {
|
|
322
|
+
TOOL_CATEGORIES,
|
|
323
|
+
shouldDelegateTool,
|
|
324
|
+
generateDelegationMessage,
|
|
325
|
+
getDelegationStats
|
|
326
|
+
};
|