@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.
Files changed (87) hide show
  1. package/README.md +304 -1
  2. package/bin/boss-claude.js +1138 -0
  3. package/bin/commands/mode.js +250 -0
  4. package/bin/onyx-guard.js +259 -0
  5. package/bin/onyx-guard.sh +251 -0
  6. package/bin/prompts.js +284 -0
  7. package/bin/rollback.js +85 -0
  8. package/bin/setup-wizard.js +492 -0
  9. package/config/.env.example +17 -0
  10. package/lib/README.md +83 -0
  11. package/lib/agent-logger.js +61 -0
  12. package/lib/agents/memory-engineers/github-memory-engineer.js +251 -0
  13. package/lib/agents/memory-engineers/postgres-memory-engineer.js +633 -0
  14. package/lib/agents/memory-engineers/qdrant-memory-engineer.js +358 -0
  15. package/lib/agents/memory-engineers/redis-memory-engineer.js +383 -0
  16. package/lib/agents/memory-supervisor.js +526 -0
  17. package/lib/agents/registry.js +135 -0
  18. package/lib/auto-monitor.js +131 -0
  19. package/lib/checkpoint-hook.js +112 -0
  20. package/lib/checkpoint.js +319 -0
  21. package/lib/commentator.js +213 -0
  22. package/lib/context-scribe.js +120 -0
  23. package/lib/delegation-strategies.js +326 -0
  24. package/lib/hierarchy-validator.js +643 -0
  25. package/lib/index.js +15 -0
  26. package/lib/init-with-mode.js +261 -0
  27. package/lib/init.js +44 -6
  28. package/lib/memory-result-aggregator.js +252 -0
  29. package/lib/memory.js +35 -7
  30. package/lib/mode-enforcer.js +473 -0
  31. package/lib/onyx-banner.js +169 -0
  32. package/lib/onyx-identity.js +214 -0
  33. package/lib/onyx-monitor.js +381 -0
  34. package/lib/onyx-reminder.js +188 -0
  35. package/lib/onyx-tool-interceptor.js +341 -0
  36. package/lib/onyx-wrapper.js +315 -0
  37. package/lib/orchestrator-gate.js +334 -0
  38. package/lib/output-formatter.js +296 -0
  39. package/lib/postgres.js +1 -1
  40. package/lib/prompt-injector.js +220 -0
  41. package/lib/prompts.js +532 -0
  42. package/lib/session.js +153 -6
  43. package/lib/setup/README.md +187 -0
  44. package/lib/setup/env-manager.js +785 -0
  45. package/lib/setup/error-recovery.js +630 -0
  46. package/lib/setup/explain-scopes.js +385 -0
  47. package/lib/setup/github-instructions.js +333 -0
  48. package/lib/setup/github-repo.js +254 -0
  49. package/lib/setup/import-credentials.js +498 -0
  50. package/lib/setup/index.js +62 -0
  51. package/lib/setup/init-postgres.js +785 -0
  52. package/lib/setup/init-redis.js +456 -0
  53. package/lib/setup/integration-test.js +652 -0
  54. package/lib/setup/progress.js +357 -0
  55. package/lib/setup/rollback.js +670 -0
  56. package/lib/setup/rollback.test.js +452 -0
  57. package/lib/setup/setup-with-rollback.example.js +351 -0
  58. package/lib/setup/summary.js +400 -0
  59. package/lib/setup/test-github-setup.js +10 -0
  60. package/lib/setup/test-postgres-init.js +98 -0
  61. package/lib/setup/verify-setup.js +102 -0
  62. package/lib/task-agent-worker.js +235 -0
  63. package/lib/token-monitor.js +466 -0
  64. package/lib/tool-wrapper-integration.js +369 -0
  65. package/lib/tool-wrapper.js +387 -0
  66. package/lib/validators/README.md +497 -0
  67. package/lib/validators/config.js +583 -0
  68. package/lib/validators/config.test.js +175 -0
  69. package/lib/validators/github.js +310 -0
  70. package/lib/validators/github.test.js +61 -0
  71. package/lib/validators/index.js +15 -0
  72. package/lib/validators/postgres.js +525 -0
  73. package/package.json +98 -13
  74. package/scripts/benchmark-memory.js +433 -0
  75. package/scripts/check-secrets.sh +12 -0
  76. package/scripts/fetch-todos.mjs +148 -0
  77. package/scripts/graceful-shutdown.sh +156 -0
  78. package/scripts/install-onyx-hooks.js +373 -0
  79. package/scripts/install.js +119 -18
  80. package/scripts/redis-monitor.js +284 -0
  81. package/scripts/redis-setup.js +412 -0
  82. package/scripts/test-memory-retrieval.js +201 -0
  83. package/scripts/validate-exports.js +68 -0
  84. package/scripts/validate-package.js +120 -0
  85. package/scripts/verify-onyx-deployment.js +309 -0
  86. package/scripts/verify-redis-deployment.js +354 -0
  87. package/scripts/verify-redis-init.js +219 -0
package/lib/prompts.js ADDED
@@ -0,0 +1,532 @@
1
+ /**
2
+ * BOSS CLAUDE INTERACTIVE CLI PROMPTS
3
+ * Beautiful, reusable prompt system with validation and colors
4
+ */
5
+
6
+ import inquirer from 'inquirer';
7
+ import chalk from 'chalk';
8
+ import ora from 'ora';
9
+
10
+ // ==================== COLOR THEME ====================
11
+ export const theme = {
12
+ primary: chalk.hex('#00D9FF'), // Cyan
13
+ success: chalk.hex('#00FF88'), // Green
14
+ warning: chalk.hex('#FFB800'), // Orange
15
+ error: chalk.hex('#FF3366'), // Red
16
+ info: chalk.hex('#A78BFA'), // Purple
17
+ muted: chalk.gray,
18
+ bold: chalk.bold,
19
+ dim: chalk.dim,
20
+
21
+ // Gradients (simulated with multiple colors)
22
+ gradient: (text) => {
23
+ const colors = ['#00D9FF', '#00FF88', '#FFB800'];
24
+ return text.split('').map((char, i) =>
25
+ chalk.hex(colors[i % colors.length])(char)
26
+ ).join('');
27
+ }
28
+ };
29
+
30
+ // ==================== VALIDATORS ====================
31
+ export const validators = {
32
+ required: (input) => {
33
+ return input.trim() !== '' || 'This field is required';
34
+ },
35
+
36
+ email: (input) => {
37
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
38
+ return emailRegex.test(input) || 'Please enter a valid email address';
39
+ },
40
+
41
+ url: (input) => {
42
+ try {
43
+ new URL(input);
44
+ return true;
45
+ } catch {
46
+ return 'Please enter a valid URL';
47
+ }
48
+ },
49
+
50
+ number: (input) => {
51
+ return !isNaN(parseFloat(input)) || 'Please enter a valid number';
52
+ },
53
+
54
+ integer: (input) => {
55
+ return Number.isInteger(Number(input)) || 'Please enter a whole number';
56
+ },
57
+
58
+ positive: (input) => {
59
+ return parseFloat(input) > 0 || 'Please enter a positive number';
60
+ },
61
+
62
+ minLength: (min) => (input) => {
63
+ return input.length >= min || `Must be at least ${min} characters`;
64
+ },
65
+
66
+ maxLength: (max) => (input) => {
67
+ return input.length <= max || `Must be no more than ${max} characters`;
68
+ },
69
+
70
+ pattern: (regex, message) => (input) => {
71
+ return regex.test(input) || message;
72
+ },
73
+
74
+ custom: (fn, message) => (input) => {
75
+ return fn(input) || message;
76
+ }
77
+ };
78
+
79
+ // ==================== PROMPT FUNCTIONS ====================
80
+
81
+ /**
82
+ * Text input prompt
83
+ */
84
+ export async function promptText({
85
+ message,
86
+ defaultValue = '',
87
+ validate = null,
88
+ transform = null,
89
+ placeholder = ''
90
+ }) {
91
+ const answers = await inquirer.prompt([{
92
+ type: 'input',
93
+ name: 'value',
94
+ message: theme.primary(message),
95
+ default: defaultValue,
96
+ validate: validate || (() => true),
97
+ transformer: transform,
98
+ prefix: theme.bold('?'),
99
+ suffix: placeholder ? theme.dim(` (${placeholder})`) : ''
100
+ }]);
101
+
102
+ return answers.value;
103
+ }
104
+
105
+ /**
106
+ * Password/secret input (hidden)
107
+ */
108
+ export async function promptPassword({
109
+ message,
110
+ validate = null,
111
+ mask = '*'
112
+ }) {
113
+ const answers = await inquirer.prompt([{
114
+ type: 'password',
115
+ name: 'value',
116
+ message: theme.primary(message),
117
+ mask,
118
+ validate: validate || validators.required,
119
+ prefix: theme.bold('?')
120
+ }]);
121
+
122
+ return answers.value;
123
+ }
124
+
125
+ /**
126
+ * Confirmation prompt (yes/no)
127
+ */
128
+ export async function promptConfirm({
129
+ message,
130
+ defaultValue = false
131
+ }) {
132
+ const answers = await inquirer.prompt([{
133
+ type: 'confirm',
134
+ name: 'value',
135
+ message: theme.primary(message),
136
+ default: defaultValue,
137
+ prefix: theme.bold('?')
138
+ }]);
139
+
140
+ return answers.value;
141
+ }
142
+
143
+ /**
144
+ * Select from list (single choice)
145
+ */
146
+ export async function promptSelect({
147
+ message,
148
+ choices,
149
+ defaultValue = null,
150
+ loop = true
151
+ }) {
152
+ const formattedChoices = choices.map(choice => {
153
+ if (typeof choice === 'string') {
154
+ return { name: choice, value: choice };
155
+ }
156
+ return choice;
157
+ });
158
+
159
+ const answers = await inquirer.prompt([{
160
+ type: 'list',
161
+ name: 'value',
162
+ message: theme.primary(message),
163
+ choices: formattedChoices,
164
+ default: defaultValue,
165
+ loop,
166
+ prefix: theme.bold('?')
167
+ }]);
168
+
169
+ return answers.value;
170
+ }
171
+
172
+ /**
173
+ * Multi-select from list (checkboxes)
174
+ */
175
+ export async function promptMultiSelect({
176
+ message,
177
+ choices,
178
+ defaultValues = [],
179
+ validate = null
180
+ }) {
181
+ const formattedChoices = choices.map(choice => {
182
+ if (typeof choice === 'string') {
183
+ return {
184
+ name: choice,
185
+ value: choice,
186
+ checked: defaultValues.includes(choice)
187
+ };
188
+ }
189
+ return {
190
+ ...choice,
191
+ checked: choice.checked || defaultValues.includes(choice.value)
192
+ };
193
+ });
194
+
195
+ const answers = await inquirer.prompt([{
196
+ type: 'checkbox',
197
+ name: 'value',
198
+ message: theme.primary(message),
199
+ choices: formattedChoices,
200
+ validate: validate || ((answer) => answer.length > 0 || 'Select at least one option'),
201
+ prefix: theme.bold('?')
202
+ }]);
203
+
204
+ return answers.value;
205
+ }
206
+
207
+ /**
208
+ * Number input with validation
209
+ */
210
+ export async function promptNumber({
211
+ message,
212
+ defaultValue = null,
213
+ min = null,
214
+ max = null,
215
+ integer = false
216
+ }) {
217
+ const validate = (input) => {
218
+ if (integer && !Number.isInteger(Number(input))) {
219
+ return 'Please enter a whole number';
220
+ }
221
+ const num = parseFloat(input);
222
+ if (isNaN(num)) return 'Please enter a valid number';
223
+ if (min !== null && num < min) return `Must be at least ${min}`;
224
+ if (max !== null && num > max) return `Must be no more than ${max}`;
225
+ return true;
226
+ };
227
+
228
+ const result = await promptText({
229
+ message,
230
+ defaultValue: defaultValue !== null ? String(defaultValue) : '',
231
+ validate
232
+ });
233
+
234
+ return integer ? parseInt(result, 10) : parseFloat(result);
235
+ }
236
+
237
+ /**
238
+ * Editor prompt (opens text editor)
239
+ */
240
+ export async function promptEditor({
241
+ message,
242
+ defaultValue = '',
243
+ validate = null
244
+ }) {
245
+ const answers = await inquirer.prompt([{
246
+ type: 'editor',
247
+ name: 'value',
248
+ message: theme.primary(message),
249
+ default: defaultValue,
250
+ validate: validate || (() => true),
251
+ prefix: theme.bold('?')
252
+ }]);
253
+
254
+ return answers.value;
255
+ }
256
+
257
+ /**
258
+ * Autocomplete search prompt
259
+ */
260
+ export async function promptSearch({
261
+ message,
262
+ choices,
263
+ defaultValue = ''
264
+ }) {
265
+ // Note: Requires inquirer-autocomplete-prompt plugin
266
+ // For now, fallback to regular select with search
267
+ return promptSelect({ message, choices, defaultValue });
268
+ }
269
+
270
+ // ==================== SPINNER UTILITIES ====================
271
+
272
+ /**
273
+ * Show loading spinner
274
+ */
275
+ export function spinner(text, type = 'dots') {
276
+ return ora({
277
+ text: theme.info(text),
278
+ spinner: type,
279
+ color: 'cyan'
280
+ });
281
+ }
282
+
283
+ /**
284
+ * Run async task with spinner
285
+ */
286
+ export async function withSpinner(text, task) {
287
+ const spin = spinner(text).start();
288
+ try {
289
+ const result = await task();
290
+ spin.succeed(theme.success(text + ' ✓'));
291
+ return result;
292
+ } catch (error) {
293
+ spin.fail(theme.error(text + ' ✗'));
294
+ throw error;
295
+ }
296
+ }
297
+
298
+ // ==================== DISPLAY UTILITIES ====================
299
+
300
+ /**
301
+ * Print styled header
302
+ */
303
+ export function header(text) {
304
+ console.log('\n' + theme.bold(theme.gradient(text)));
305
+ console.log(theme.dim('─'.repeat(text.length)) + '\n');
306
+ }
307
+
308
+ /**
309
+ * Print success message
310
+ */
311
+ export function success(text) {
312
+ console.log(theme.success('✓ ') + text);
313
+ }
314
+
315
+ /**
316
+ * Print error message
317
+ */
318
+ export function error(text) {
319
+ console.log(theme.error('✗ ') + text);
320
+ }
321
+
322
+ /**
323
+ * Print warning message
324
+ */
325
+ export function warning(text) {
326
+ console.log(theme.warning('⚠ ') + text);
327
+ }
328
+
329
+ /**
330
+ * Print info message
331
+ */
332
+ export function info(text) {
333
+ console.log(theme.info('ℹ ') + text);
334
+ }
335
+
336
+ /**
337
+ * Print muted/dim text
338
+ */
339
+ export function muted(text) {
340
+ console.log(theme.muted(text));
341
+ }
342
+
343
+ /**
344
+ * Print table
345
+ */
346
+ export function table(data) {
347
+ console.table(data);
348
+ }
349
+
350
+ /**
351
+ * Print key-value pairs
352
+ */
353
+ export function keyValue(pairs) {
354
+ const maxKeyLength = Math.max(...Object.keys(pairs).map(k => k.length));
355
+
356
+ Object.entries(pairs).forEach(([key, value]) => {
357
+ const paddedKey = key.padEnd(maxKeyLength);
358
+ console.log(theme.dim(paddedKey) + ' : ' + theme.primary(value));
359
+ });
360
+ }
361
+
362
+ /**
363
+ * Print divider
364
+ */
365
+ export function divider(char = '─', length = 50) {
366
+ console.log(theme.dim(char.repeat(length)));
367
+ }
368
+
369
+ /**
370
+ * Clear console
371
+ */
372
+ export function clear() {
373
+ console.clear();
374
+ }
375
+
376
+ /**
377
+ * Print blank line
378
+ */
379
+ export function blank(count = 1) {
380
+ console.log('\n'.repeat(count - 1));
381
+ }
382
+
383
+ // ==================== PRESET PROMPTS ====================
384
+
385
+ /**
386
+ * Prompt for Redis connection
387
+ */
388
+ export async function promptRedisConnection() {
389
+ header('Redis Connection');
390
+
391
+ const host = await promptText({
392
+ message: 'Redis host',
393
+ defaultValue: 'localhost',
394
+ validate: validators.required
395
+ });
396
+
397
+ const port = await promptNumber({
398
+ message: 'Redis port',
399
+ defaultValue: 6379,
400
+ min: 1,
401
+ max: 65535,
402
+ integer: true
403
+ });
404
+
405
+ const requiresAuth = await promptConfirm({
406
+ message: 'Requires authentication?',
407
+ defaultValue: false
408
+ });
409
+
410
+ let password = '';
411
+ if (requiresAuth) {
412
+ password = await promptPassword({
413
+ message: 'Redis password'
414
+ });
415
+ }
416
+
417
+ return { host, port, password };
418
+ }
419
+
420
+ /**
421
+ * Prompt for API key
422
+ */
423
+ export async function promptApiKey(serviceName) {
424
+ header(`${serviceName} API Key`);
425
+
426
+ const apiKey = await promptPassword({
427
+ message: `Enter ${serviceName} API key`,
428
+ validate: validators.required
429
+ });
430
+
431
+ return apiKey;
432
+ }
433
+
434
+ /**
435
+ * Prompt for project setup
436
+ */
437
+ export async function promptProjectSetup() {
438
+ header('Project Setup');
439
+
440
+ const name = await promptText({
441
+ message: 'Project name',
442
+ validate: validators.required,
443
+ placeholder: 'my-awesome-project'
444
+ });
445
+
446
+ const description = await promptText({
447
+ message: 'Description (optional)',
448
+ defaultValue: ''
449
+ });
450
+
451
+ const type = await promptSelect({
452
+ message: 'Project type',
453
+ choices: [
454
+ { name: 'Node.js Library', value: 'library' },
455
+ { name: 'CLI Tool', value: 'cli' },
456
+ { name: 'Web App', value: 'web' },
457
+ { name: 'API Service', value: 'api' }
458
+ ]
459
+ });
460
+
461
+ const features = await promptMultiSelect({
462
+ message: 'Select features',
463
+ choices: [
464
+ 'TypeScript',
465
+ 'Testing',
466
+ 'Linting',
467
+ 'CI/CD',
468
+ 'Documentation'
469
+ ]
470
+ });
471
+
472
+ return { name, description, type, features };
473
+ }
474
+
475
+ /**
476
+ * Prompt for confirmation with warning
477
+ */
478
+ export async function promptDangerousAction(action, details = '') {
479
+ warning(`You are about to ${action}`);
480
+ if (details) {
481
+ muted(details);
482
+ }
483
+ blank();
484
+
485
+ return promptConfirm({
486
+ message: 'Are you sure you want to continue?',
487
+ defaultValue: false
488
+ });
489
+ }
490
+
491
+ // ==================== EXPORTS ====================
492
+
493
+ export default {
494
+ // Core prompts
495
+ promptText,
496
+ promptPassword,
497
+ promptConfirm,
498
+ promptSelect,
499
+ promptMultiSelect,
500
+ promptNumber,
501
+ promptEditor,
502
+ promptSearch,
503
+
504
+ // Spinner utilities
505
+ spinner,
506
+ withSpinner,
507
+
508
+ // Display utilities
509
+ header,
510
+ success,
511
+ error,
512
+ warning,
513
+ info,
514
+ muted,
515
+ table,
516
+ keyValue,
517
+ divider,
518
+ clear,
519
+ blank,
520
+
521
+ // Preset prompts
522
+ promptRedisConnection,
523
+ promptApiKey,
524
+ promptProjectSetup,
525
+ promptDangerousAction,
526
+
527
+ // Validators
528
+ validators,
529
+
530
+ // Theme
531
+ theme
532
+ };
package/lib/session.js CHANGED
@@ -66,7 +66,11 @@ export async function loadSession() {
66
66
  repo,
67
67
  started_at: new Date().toISOString(),
68
68
  messages: [],
69
- tokens_used: 0
69
+ tokens_used: 0,
70
+ // Track conductor vs agent tokens for efficiency bonus
71
+ onyx_tokens: 0, // Tokens used by ONYX (orchestration overhead)
72
+ agent_tokens: 0, // Tokens used by spawned agents (actual work)
73
+ delegations: 0 // Number of Task tool delegations
70
74
  };
71
75
  }
72
76
 
@@ -90,7 +94,10 @@ export async function saveSession(summary, tags) {
90
94
  const session = sessionData ? JSON.parse(sessionData) : {
91
95
  started_at: new Date().toISOString(),
92
96
  messages: [],
93
- tokens_used: 0
97
+ tokens_used: 0,
98
+ onyx_tokens: 0,
99
+ agent_tokens: 0,
100
+ delegations: 0
94
101
  };
95
102
 
96
103
  // Save to GitHub Issues
@@ -101,8 +108,25 @@ export async function saveSession(summary, tags) {
101
108
  tags: tags ? tags.split(',').map(t => t.trim()) : []
102
109
  });
103
110
 
104
- // Calculate rewards
105
- const xpEarned = 50; // Base XP per session
111
+ // Calculate rewards with EFFICIENCY MULTIPLIER
112
+ // Formula: agent_tokens / onyx_tokens = efficiency ratio
113
+ // The more work agents do vs ONYX overhead, the higher the bonus
114
+ const baseXP = 50;
115
+ const onyxTokens = session.onyx_tokens || 1000; // Minimum 1000 to avoid division issues
116
+ const agentTokens = session.agent_tokens || 0;
117
+ const delegations = session.delegations || 0;
118
+
119
+ // Efficiency bonus: floor(agent_tokens / onyx_tokens), capped at 100
120
+ // Example: 600k agent / 20k onyx = 30x efficiency = 30 bonus XP
121
+ let efficiencyBonus = 0;
122
+ if (agentTokens > 0 && onyxTokens > 0) {
123
+ efficiencyBonus = Math.min(100, Math.floor(agentTokens / onyxTokens));
124
+ }
125
+
126
+ // Delegation bonus: +2 XP per successful delegation (up to 20 bonus)
127
+ const delegationBonus = Math.min(20, delegations * 2);
128
+
129
+ const xpEarned = baseXP + efficiencyBonus + delegationBonus;
106
130
  const tokensEarned = session.tokens_used || 0;
107
131
 
108
132
  await addXP(xpEarned);
@@ -131,7 +155,16 @@ export async function saveSession(summary, tags) {
131
155
  ...memory,
132
156
  repo_name: repo.name,
133
157
  xp_earned: xpEarned,
134
- tokens_earned: tokensEarned
158
+ tokens_earned: tokensEarned,
159
+ // Efficiency metrics
160
+ efficiency: {
161
+ onyx_tokens: onyxTokens,
162
+ agent_tokens: agentTokens,
163
+ ratio: agentTokens > 0 ? (agentTokens / onyxTokens).toFixed(1) + 'x' : '0x',
164
+ efficiency_bonus: efficiencyBonus,
165
+ delegation_bonus: delegationBonus,
166
+ delegations: delegations
167
+ }
135
168
  };
136
169
  }
137
170
 
@@ -149,10 +182,124 @@ export async function updateSessionTokens(tokens) {
149
182
  const session = data ? JSON.parse(data) : {
150
183
  started_at: new Date().toISOString(),
151
184
  messages: [],
152
- tokens_used: 0
185
+ tokens_used: 0,
186
+ onyx_tokens: 0,
187
+ agent_tokens: 0,
188
+ delegations: 0
189
+ };
190
+
191
+ session.tokens_used = (session.tokens_used || 0) + tokens;
192
+
193
+ await client.set(sessionKey, JSON.stringify(session));
194
+ }
195
+
196
+ /**
197
+ * Track tokens used by ONYX (orchestration overhead)
198
+ * Lower is better - ONYX should delegate, not do work
199
+ */
200
+ export async function trackOnyxTokens(tokens) {
201
+ const repo = await getCurrentRepo();
202
+ if (!repo) return;
203
+
204
+ const client = getRedis();
205
+ const sessionKey = `boss:session:${repo.name}:current`;
206
+
207
+ const data = await client.get(sessionKey);
208
+ const session = data ? JSON.parse(data) : {
209
+ started_at: new Date().toISOString(),
210
+ messages: [],
211
+ tokens_used: 0,
212
+ onyx_tokens: 0,
213
+ agent_tokens: 0,
214
+ delegations: 0
215
+ };
216
+
217
+ session.onyx_tokens = (session.onyx_tokens || 0) + tokens;
218
+ session.tokens_used = (session.tokens_used || 0) + tokens;
219
+
220
+ await client.set(sessionKey, JSON.stringify(session));
221
+ }
222
+
223
+ /**
224
+ * Track tokens used by spawned agents (actual work)
225
+ * Higher is better - agents should do the heavy lifting
226
+ */
227
+ export async function trackAgentTokens(tokens) {
228
+ const repo = await getCurrentRepo();
229
+ if (!repo) return;
230
+
231
+ const client = getRedis();
232
+ const sessionKey = `boss:session:${repo.name}:current`;
233
+
234
+ const data = await client.get(sessionKey);
235
+ const session = data ? JSON.parse(data) : {
236
+ started_at: new Date().toISOString(),
237
+ messages: [],
238
+ tokens_used: 0,
239
+ onyx_tokens: 0,
240
+ agent_tokens: 0,
241
+ delegations: 0
153
242
  };
154
243
 
244
+ session.agent_tokens = (session.agent_tokens || 0) + tokens;
155
245
  session.tokens_used = (session.tokens_used || 0) + tokens;
156
246
 
157
247
  await client.set(sessionKey, JSON.stringify(session));
158
248
  }
249
+
250
+ /**
251
+ * Record a delegation via Task tool
252
+ * More delegations = following the conductor protocol
253
+ */
254
+ export async function recordDelegation() {
255
+ const repo = await getCurrentRepo();
256
+ if (!repo) return;
257
+
258
+ const client = getRedis();
259
+ const sessionKey = `boss:session:${repo.name}:current`;
260
+
261
+ const data = await client.get(sessionKey);
262
+ const session = data ? JSON.parse(data) : {
263
+ started_at: new Date().toISOString(),
264
+ messages: [],
265
+ tokens_used: 0,
266
+ onyx_tokens: 0,
267
+ agent_tokens: 0,
268
+ delegations: 0
269
+ };
270
+
271
+ session.delegations = (session.delegations || 0) + 1;
272
+
273
+ await client.set(sessionKey, JSON.stringify(session));
274
+ }
275
+
276
+ /**
277
+ * Get current efficiency stats for display
278
+ */
279
+ export async function getEfficiencyStats() {
280
+ const repo = await getCurrentRepo();
281
+ if (!repo) return null;
282
+
283
+ const client = getRedis();
284
+ const sessionKey = `boss:session:${repo.name}:current`;
285
+
286
+ const data = await client.get(sessionKey);
287
+ if (!data) return null;
288
+
289
+ const session = JSON.parse(data);
290
+ const onyxTokens = session.onyx_tokens || 0;
291
+ const agentTokens = session.agent_tokens || 0;
292
+
293
+ const ratio = onyxTokens > 0 ? agentTokens / onyxTokens : 0;
294
+ const projectedBonus = Math.min(100, Math.floor(ratio));
295
+
296
+ return {
297
+ onyx_tokens: onyxTokens,
298
+ agent_tokens: agentTokens,
299
+ total_tokens: session.tokens_used || 0,
300
+ delegations: session.delegations || 0,
301
+ efficiency_ratio: ratio.toFixed(1) + 'x',
302
+ projected_bonus_xp: projectedBonus,
303
+ delegation_bonus_xp: Math.min(20, (session.delegations || 0) * 2)
304
+ };
305
+ }