dual-brain 3.8.1 → 3.9.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/README.md +1 -1
- package/hooks/control-panel.mjs +27 -3
- package/hooks/cost-logger.mjs +2 -3
- package/hooks/enforce-tier.mjs +3 -4
- package/hooks/failure-detector.mjs +15 -1
- package/hooks/profiles.mjs +35 -4
- package/hooks/test-orchestrator.mjs +64 -0
- package/install.mjs +31 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -63,7 +63,7 @@ npx -y dual-brain
|
|
|
63
63
|
| `hooks/gpt-work-dispatcher.mjs` | Dispatch execution tasks to GPT via Codex CLI |
|
|
64
64
|
| `hooks/session-report.mjs` | Session-end summary: activity, compliance, quality |
|
|
65
65
|
| `hooks/health-check.mjs` | Verify all hooks and dependencies are working |
|
|
66
|
-
| `hooks/test-orchestrator.mjs` | Self-test harness (
|
|
66
|
+
| `hooks/test-orchestrator.mjs` | Self-test harness (39 tests) |
|
|
67
67
|
| `hooks/setup-wizard.mjs` | Interactive config (optional — for custom plans) |
|
|
68
68
|
| `hooks/install-git-hooks.mjs` | Git pre-commit hook for quality gate |
|
|
69
69
|
|
package/hooks/control-panel.mjs
CHANGED
|
@@ -437,16 +437,40 @@ function showProfilePicker(rl) {
|
|
|
437
437
|
|
|
438
438
|
rl.question(' Choice: ', (answer) => {
|
|
439
439
|
const names = Object.keys(PROFILES);
|
|
440
|
-
const
|
|
440
|
+
const trimmed = answer.trim();
|
|
441
|
+
let selectedName = null;
|
|
442
|
+
|
|
443
|
+
// Try numeric selection first
|
|
444
|
+
const idx = parseInt(trimmed, 10) - 1;
|
|
441
445
|
if (idx >= 0 && idx < names.length) {
|
|
446
|
+
selectedName = names[idx];
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Try natural language alias resolution
|
|
450
|
+
if (!selectedName && trimmed && trimmed !== 'q') {
|
|
451
|
+
const PANEL_ALIASES = {
|
|
452
|
+
'auto': 'auto', 'adaptive': 'auto', 'smart': 'auto', 'default': 'auto', 'normal': 'auto',
|
|
453
|
+
'balanced': 'balanced', 'even': 'balanced', 'equal': 'balanced',
|
|
454
|
+
'cost-saver': 'cost-saver', 'cheap': 'cost-saver', 'save': 'cost-saver', 'conservative': 'cost-saver', 'frugal': 'cost-saver', 'budget': 'cost-saver',
|
|
455
|
+
'quality-first': 'quality-first', 'aggressive': 'quality-first', 'quality': 'quality-first', 'max': 'quality-first', 'full': 'quality-first', 'both': 'quality-first',
|
|
456
|
+
};
|
|
457
|
+
const cleaned = trimmed.toLowerCase()
|
|
458
|
+
.replace(/^(go|be|use|switch to|set|mode)\s+/i, '')
|
|
459
|
+
.replace(/\s+mode$/i, '');
|
|
460
|
+
selectedName = PANEL_ALIASES[cleaned] || null;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (selectedName) {
|
|
442
464
|
let customOverrides = null;
|
|
443
465
|
try {
|
|
444
466
|
const existing = JSON.parse(readFileSync(PROFILE_FILE, 'utf8'));
|
|
445
467
|
if (existing.custom_overrides?.budgets) customOverrides = { budgets: existing.custom_overrides.budgets };
|
|
446
468
|
} catch {}
|
|
447
|
-
saveProfile(
|
|
448
|
-
const pf = PROFILES[
|
|
469
|
+
saveProfile(selectedName, customOverrides);
|
|
470
|
+
const pf = PROFILES[selectedName];
|
|
449
471
|
console.log(` ✅ Switched to ${pf.emoji} ${pf.uiLabel}`);
|
|
472
|
+
} else if (trimmed && trimmed !== 'q') {
|
|
473
|
+
console.log(` Unknown profile: ${trimmed}. Try: cheap, aggressive, quality, balanced, auto`);
|
|
450
474
|
}
|
|
451
475
|
resolve();
|
|
452
476
|
});
|
package/hooks/cost-logger.mjs
CHANGED
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
* Output contract: must print "{}" to stdout and exit 0 within ~100 ms.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { createHash } from "crypto";
|
|
12
11
|
import { appendFileSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
13
12
|
import { dirname, join } from "path";
|
|
14
13
|
import { fileURLToPath } from "url";
|
|
@@ -265,8 +264,8 @@ async function main() {
|
|
|
265
264
|
// Record failures for adaptive routing (failure-loop detection)
|
|
266
265
|
if (status === 'error' && toolName === 'Agent') {
|
|
267
266
|
try {
|
|
268
|
-
const { recordFailure, pruneOldFailures } = await import('./failure-detector.mjs');
|
|
269
|
-
const promptHash =
|
|
267
|
+
const { computePromptHash, recordFailure, pruneOldFailures } = await import('./failure-detector.mjs');
|
|
268
|
+
const promptHash = computePromptHash(toolInput);
|
|
270
269
|
recordFailure(promptHash, tier, payload?.error || 'agent_error');
|
|
271
270
|
// Best-effort cleanup of stale failure entries (>24h old)
|
|
272
271
|
try { pruneOldFailures(); } catch {}
|
package/hooks/enforce-tier.mjs
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { readFileSync, writeFileSync, appendFileSync, renameSync } from 'fs';
|
|
3
|
-
import { createHash } from 'crypto';
|
|
4
3
|
import { dirname, resolve, join } from 'path';
|
|
5
4
|
import { fileURLToPath } from 'url';
|
|
6
5
|
import { classifyRisk, extractPaths } from './risk-classifier.mjs';
|
|
7
|
-
import { checkFailureLoop, recordFailure } from './failure-detector.mjs';
|
|
6
|
+
import { computePromptHash, checkFailureLoop, recordFailure } from './failure-detector.mjs';
|
|
8
7
|
|
|
9
8
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
9
|
const CONFIG_FILE = resolve(__dirname, '..', 'orchestrator.json');
|
|
@@ -214,7 +213,7 @@ try {
|
|
|
214
213
|
const currentModel = (ti.model || '').toLowerCase();
|
|
215
214
|
|
|
216
215
|
// Compute prompt hash early for duplicate detection and logging
|
|
217
|
-
const promptHash =
|
|
216
|
+
const promptHash = computePromptHash(ti);
|
|
218
217
|
|
|
219
218
|
// Burst detection — suppress noise during wave launches (3+ agents in 90s)
|
|
220
219
|
const burstMode = detectBurst();
|
|
@@ -316,7 +315,7 @@ try {
|
|
|
316
315
|
}
|
|
317
316
|
|
|
318
317
|
// Failure loop detection
|
|
319
|
-
const failureCheck = checkFailureLoop(promptHash
|
|
318
|
+
const failureCheck = checkFailureLoop(promptHash);
|
|
320
319
|
let failureMessage = null;
|
|
321
320
|
if (failureCheck.isLoop) {
|
|
322
321
|
if (failureCheck.suggestion === 'promote_tier' && tier === 'execute') {
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* pruneOldFailures() → { pruned, remaining }
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { createHash } from 'crypto';
|
|
11
12
|
import { readFileSync, appendFileSync, writeFileSync, renameSync, unlinkSync } from 'fs';
|
|
12
13
|
import { dirname, join } from 'path';
|
|
13
14
|
import { fileURLToPath } from 'url';
|
|
@@ -16,6 +17,19 @@ import { fileURLToPath } from 'url';
|
|
|
16
17
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
17
18
|
const LEDGER_FILE = join(__dirname, 'decision-ledger.jsonl');
|
|
18
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Canonical prompt hash used by all hooks for failure-loop correlation.
|
|
22
|
+
* Both enforce-tier (PreToolUse) and cost-logger (PostToolUse) must use this
|
|
23
|
+
* same function so that recorded failures can be matched during escalation.
|
|
24
|
+
*
|
|
25
|
+
* @param {object} toolInput — the raw tool_input from the hook payload
|
|
26
|
+
* @returns {string} 12-char hex hash
|
|
27
|
+
*/
|
|
28
|
+
function computePromptHash(toolInput) {
|
|
29
|
+
const text = (toolInput?.description || '') + (toolInput?.prompt || '');
|
|
30
|
+
return createHash('sha256').update(text).digest('hex').slice(0, 12);
|
|
31
|
+
}
|
|
32
|
+
|
|
19
33
|
/**
|
|
20
34
|
* Compute a decay weight based on failure age.
|
|
21
35
|
* 0-30 min → 1.0, 30-60 min → 0.5, 60-120 min → 0.25, >120 min → 0 (excluded by window)
|
|
@@ -121,4 +135,4 @@ function pruneOldFailures() {
|
|
|
121
135
|
return { pruned, remaining };
|
|
122
136
|
}
|
|
123
137
|
|
|
124
|
-
export { checkFailureLoop, recordFailure, pruneOldFailures };
|
|
138
|
+
export { computePromptHash, checkFailureLoop, recordFailure, pruneOldFailures };
|
package/hooks/profiles.mjs
CHANGED
|
@@ -20,6 +20,25 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
20
20
|
const PROFILE_FILE = join(__dirname, '..', 'dual-brain.profile.json');
|
|
21
21
|
const CONFIG_FILE = join(__dirname, '..', 'orchestrator.json');
|
|
22
22
|
|
|
23
|
+
const ALIASES = {
|
|
24
|
+
// auto
|
|
25
|
+
'auto': 'auto', 'adaptive': 'auto', 'smart': 'auto', 'default': 'auto', 'normal': 'auto',
|
|
26
|
+
// balanced
|
|
27
|
+
'balanced': 'balanced', 'even': 'balanced', 'equal': 'balanced',
|
|
28
|
+
// cost-saver
|
|
29
|
+
'cost-saver': 'cost-saver', 'cheap': 'cost-saver', 'save': 'cost-saver', 'conservative': 'cost-saver', 'frugal': 'cost-saver', 'budget': 'cost-saver',
|
|
30
|
+
// quality-first
|
|
31
|
+
'quality-first': 'quality-first', 'aggressive': 'quality-first', 'quality': 'quality-first', 'max': 'quality-first', 'full': 'quality-first', 'both': 'quality-first',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function resolveProfileName(input) {
|
|
35
|
+
if (!input) return null;
|
|
36
|
+
const cleaned = input.toLowerCase().trim()
|
|
37
|
+
.replace(/^(go|be|use|switch to|set|mode)\s+/i, '')
|
|
38
|
+
.replace(/\s+mode$/i, '');
|
|
39
|
+
return ALIASES[cleaned] || null;
|
|
40
|
+
}
|
|
41
|
+
|
|
23
42
|
const PROFILES = {
|
|
24
43
|
auto: {
|
|
25
44
|
description: 'Adapts routing based on task risk, provider health, and outcomes',
|
|
@@ -140,12 +159,22 @@ function getActiveProfile() {
|
|
|
140
159
|
}
|
|
141
160
|
|
|
142
161
|
function setActiveProfile(name, customOverrides = null) {
|
|
143
|
-
|
|
144
|
-
|
|
162
|
+
let resolved = name;
|
|
163
|
+
if (!PROFILES[resolved]) {
|
|
164
|
+
const alias = resolveProfileName(name);
|
|
165
|
+
if (alias) {
|
|
166
|
+
resolved = alias;
|
|
167
|
+
} else {
|
|
168
|
+
const aliasHint = Object.entries(ALIASES)
|
|
169
|
+
.filter(([k, v]) => k !== v)
|
|
170
|
+
.map(([k, v]) => `${k} → ${v}`)
|
|
171
|
+
.join(', ');
|
|
172
|
+
return { ok: false, error: `Unknown profile: ${name}. Available: ${Object.keys(PROFILES).join(', ')}. Aliases: ${aliasHint}` };
|
|
173
|
+
}
|
|
145
174
|
}
|
|
146
175
|
|
|
147
176
|
const data = {
|
|
148
|
-
active:
|
|
177
|
+
active: resolved,
|
|
149
178
|
switched_at: new Date().toISOString(),
|
|
150
179
|
};
|
|
151
180
|
if (customOverrides) data.custom_overrides = customOverrides;
|
|
@@ -154,7 +183,7 @@ function setActiveProfile(name, customOverrides = null) {
|
|
|
154
183
|
const tmp = PROFILE_FILE + '.tmp.' + process.pid;
|
|
155
184
|
writeFileSync(tmp, JSON.stringify(data, null, 2) + '\n');
|
|
156
185
|
renameSync(tmp, PROFILE_FILE);
|
|
157
|
-
return { ok: true, profile: PROFILES[
|
|
186
|
+
return { ok: true, profile: PROFILES[resolved], resolvedName: resolved };
|
|
158
187
|
} catch (err) {
|
|
159
188
|
return { ok: false, error: `Failed to write profile: ${err.message}` };
|
|
160
189
|
}
|
|
@@ -216,6 +245,8 @@ function getProfileOverrides(system) {
|
|
|
216
245
|
|
|
217
246
|
export {
|
|
218
247
|
PROFILES,
|
|
248
|
+
ALIASES,
|
|
249
|
+
resolveProfileName,
|
|
219
250
|
getActiveProfile,
|
|
220
251
|
setActiveProfile,
|
|
221
252
|
setBudgetOverrides,
|
|
@@ -1007,6 +1007,70 @@ test('failure decay: pruneOldFailures removes stale entries', () => {
|
|
|
1007
1007
|
}
|
|
1008
1008
|
});
|
|
1009
1009
|
|
|
1010
|
+
// ─── Test 40: adaptive loop end-to-end hash match ─────────────────────────
|
|
1011
|
+
test('adaptive loop: end-to-end hash match', () => {
|
|
1012
|
+
const LEDGER = resolve(HOOKS, 'decision-ledger.jsonl');
|
|
1013
|
+
const backup = existsSync(LEDGER) ? readFileSync(LEDGER, 'utf8') : null;
|
|
1014
|
+
|
|
1015
|
+
try {
|
|
1016
|
+
// Start with a clean ledger so prior failures don't interfere
|
|
1017
|
+
writeFileSync(LEDGER, '', 'utf8');
|
|
1018
|
+
|
|
1019
|
+
// Step 1: Define a specific Agent payload used consistently across all steps
|
|
1020
|
+
const toolInput = { prompt: 'fix the auth bug', description: 'patch auth module' };
|
|
1021
|
+
const agentPayload = JSON.stringify({ tool_name: 'Agent', tool_input: toolInput });
|
|
1022
|
+
|
|
1023
|
+
// Step 2: Run enforce-tier with this payload (computes and may log a promptHash)
|
|
1024
|
+
const firstRun = run(ENFORCE_TIER, agentPayload);
|
|
1025
|
+
if (firstRun.status !== 0) return `first enforce-tier run failed with status: ${firstRun.status}`;
|
|
1026
|
+
if (!firstRun.parsed) return `first enforce-tier run produced no valid JSON`;
|
|
1027
|
+
|
|
1028
|
+
// Step 3: Simulate 2 failures via cost-logger with the SAME tool_input
|
|
1029
|
+
const errorPayload = JSON.stringify({
|
|
1030
|
+
tool_name: 'Agent',
|
|
1031
|
+
tool_input: toolInput,
|
|
1032
|
+
error: 'test failure',
|
|
1033
|
+
});
|
|
1034
|
+
|
|
1035
|
+
const fail1 = runStream(COST_LOGGER, errorPayload);
|
|
1036
|
+
if (fail1.status !== 0) return `first cost-logger failure run failed with status: ${fail1.status}`;
|
|
1037
|
+
|
|
1038
|
+
const fail2 = runStream(COST_LOGGER, errorPayload);
|
|
1039
|
+
if (fail2.status !== 0) return `second cost-logger failure run failed with status: ${fail2.status}`;
|
|
1040
|
+
|
|
1041
|
+
// Verify cost-logger actually wrote failure entries to the ledger
|
|
1042
|
+
if (!existsSync(LEDGER)) return 'ledger file not created after cost-logger failures';
|
|
1043
|
+
const ledgerLines = readFileSync(LEDGER, 'utf8').split('\n').filter(Boolean);
|
|
1044
|
+
const failureEntries = ledgerLines
|
|
1045
|
+
.map(l => { try { return JSON.parse(l); } catch { return null; } })
|
|
1046
|
+
.filter(e => e && e.type === 'failure' && e.success === false);
|
|
1047
|
+
if (failureEntries.length < 2)
|
|
1048
|
+
return `expected >= 2 failure entries in ledger, got: ${failureEntries.length}`;
|
|
1049
|
+
|
|
1050
|
+
// Step 4: Run enforce-tier again with the same Agent payload
|
|
1051
|
+
const secondRun = run(ENFORCE_TIER, agentPayload);
|
|
1052
|
+
if (secondRun.status !== 0) return `second enforce-tier run failed with status: ${secondRun.status}`;
|
|
1053
|
+
if (!secondRun.parsed) return `second enforce-tier run produced no valid JSON`;
|
|
1054
|
+
|
|
1055
|
+
// Step 5: The second enforce-tier run should detect the failure loop
|
|
1056
|
+
// and mention escalation or failure loop in its systemMessage
|
|
1057
|
+
const msg = (secondRun.parsed.systemMessage || '').toLowerCase();
|
|
1058
|
+
if (!msg.includes('failure') && !msg.includes('escalat') && !msg.includes('loop') && !msg.includes('dual-brain'))
|
|
1059
|
+
return `expected failure loop / escalation in second enforce-tier systemMessage, got: "${secondRun.parsed.systemMessage || '(empty)'}"`;
|
|
1060
|
+
|
|
1061
|
+
// Bonus: verify the hashes match — the failure entries recorded by cost-logger
|
|
1062
|
+
// should have the same prompt_hash that enforce-tier uses for checkFailureLoop
|
|
1063
|
+
const failureHashes = [...new Set(failureEntries.map(e => e.prompt_hash))];
|
|
1064
|
+
if (failureHashes.length !== 1)
|
|
1065
|
+
return `expected all failure entries to share one hash, got ${failureHashes.length} distinct hashes: ${failureHashes.join(', ')}`;
|
|
1066
|
+
|
|
1067
|
+
return true;
|
|
1068
|
+
} finally {
|
|
1069
|
+
if (backup !== null) writeFileSync(LEDGER, backup, 'utf8');
|
|
1070
|
+
else try { writeFileSync(LEDGER, '', 'utf8'); } catch {}
|
|
1071
|
+
}
|
|
1072
|
+
});
|
|
1073
|
+
|
|
1010
1074
|
// ─── Summary ─────────────────────────────────────────────────────────────────
|
|
1011
1075
|
const total = passed + failed;
|
|
1012
1076
|
console.log(`\n${passed}/${total} tests passed`);
|
package/install.mjs
CHANGED
|
@@ -58,7 +58,8 @@ if (flag('--help') || flag('-h')) {
|
|
|
58
58
|
--help Show this help
|
|
59
59
|
|
|
60
60
|
🎛️ Routing modes:
|
|
61
|
-
|
|
61
|
+
🤖 Auto (default) Adapts routing based on risk, health, outcomes
|
|
62
|
+
⚖️ Balanced Auto-routes, uses both providers evenly
|
|
62
63
|
🛡️ Conservative Fewer GPT dispatches, sticks to Claude
|
|
63
64
|
🚀 Aggressive Maximizes both subscriptions, dual-brain for medium+
|
|
64
65
|
|
|
@@ -453,7 +454,7 @@ const PROFILES = {
|
|
|
453
454
|
function loadProfile(workspace) {
|
|
454
455
|
try {
|
|
455
456
|
const data = JSON.parse(readFileSync(profilePath(workspace), 'utf8'));
|
|
456
|
-
const name = data.active && PROFILES[data.active] ? data.active : '
|
|
457
|
+
const name = data.active && PROFILES[data.active] ? data.active : 'auto';
|
|
457
458
|
const profile = PROFILES[name];
|
|
458
459
|
const custom = data.custom_overrides || {};
|
|
459
460
|
return {
|
|
@@ -464,7 +465,7 @@ function loadProfile(workspace) {
|
|
|
464
465
|
switched_at: data.switched_at || null,
|
|
465
466
|
};
|
|
466
467
|
} catch {
|
|
467
|
-
return { name: '
|
|
468
|
+
return { name: 'auto', ...PROFILES.auto, switched_at: null };
|
|
468
469
|
}
|
|
469
470
|
}
|
|
470
471
|
|
|
@@ -497,8 +498,8 @@ function cmdMode() {
|
|
|
497
498
|
|
|
498
499
|
if (!modeArg || modeArg === 'list') {
|
|
499
500
|
const current = loadProfile(workspace);
|
|
500
|
-
const PEMOJIS = { balanced: '⚖️ ', 'cost-saver': '🛡️', 'quality-first': '🚀' };
|
|
501
|
-
const UI_NAMES = { balanced: '
|
|
501
|
+
const PEMOJIS = { auto: '🤖', balanced: '⚖️ ', 'cost-saver': '🛡️', 'quality-first': '🚀' };
|
|
502
|
+
const UI_NAMES = { auto: 'Auto (default)', balanced: 'Balanced', 'cost-saver': 'Conservative', 'quality-first': 'Aggressive' };
|
|
502
503
|
console.log('');
|
|
503
504
|
console.log(' 🎛️ Routing modes:');
|
|
504
505
|
console.log('');
|
|
@@ -513,13 +514,28 @@ function cmdMode() {
|
|
|
513
514
|
return;
|
|
514
515
|
}
|
|
515
516
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
517
|
+
let resolvedMode = modeArg;
|
|
518
|
+
if (!PROFILES[resolvedMode]) {
|
|
519
|
+
// Try natural language alias resolution
|
|
520
|
+
const cleaned = resolvedMode.toLowerCase().trim()
|
|
521
|
+
.replace(/^(go|be|use|switch to|set|mode)\s+/i, '')
|
|
522
|
+
.replace(/\s+mode$/i, '');
|
|
523
|
+
const MODE_ALIASES = {
|
|
524
|
+
'auto': 'auto', 'adaptive': 'auto', 'smart': 'auto', 'default': 'auto', 'normal': 'auto',
|
|
525
|
+
'balanced': 'balanced', 'even': 'balanced', 'equal': 'balanced',
|
|
526
|
+
'cost-saver': 'cost-saver', 'cheap': 'cost-saver', 'save': 'cost-saver', 'conservative': 'cost-saver', 'frugal': 'cost-saver', 'budget': 'cost-saver',
|
|
527
|
+
'quality-first': 'quality-first', 'aggressive': 'quality-first', 'quality': 'quality-first', 'max': 'quality-first', 'full': 'quality-first', 'both': 'quality-first',
|
|
528
|
+
};
|
|
529
|
+
resolvedMode = MODE_ALIASES[cleaned] || null;
|
|
530
|
+
if (!resolvedMode) {
|
|
531
|
+
console.error(` Unknown profile: ${modeArg}`);
|
|
532
|
+
console.error(` Available: ${Object.keys(PROFILES).join(', ')}`);
|
|
533
|
+
console.error(` Aliases: cheap, aggressive, quality, budget, frugal, smart, adaptive, ...`);
|
|
534
|
+
process.exit(1);
|
|
535
|
+
}
|
|
520
536
|
}
|
|
521
537
|
|
|
522
|
-
const profile = PROFILES[
|
|
538
|
+
const profile = PROFILES[resolvedMode];
|
|
523
539
|
|
|
524
540
|
let customOverrides = null;
|
|
525
541
|
try {
|
|
@@ -529,12 +545,12 @@ function cmdMode() {
|
|
|
529
545
|
}
|
|
530
546
|
} catch {}
|
|
531
547
|
|
|
532
|
-
saveProfile(workspace,
|
|
548
|
+
saveProfile(workspace, resolvedMode, customOverrides);
|
|
533
549
|
|
|
534
|
-
const PEMOJIS = { balanced: '⚖️ ', 'cost-saver': '🛡️', 'quality-first': '🚀' };
|
|
535
|
-
const UI_NAMES = { balanced: '
|
|
550
|
+
const PEMOJIS = { auto: '🤖', balanced: '⚖️ ', 'cost-saver': '🛡️', 'quality-first': '🚀' };
|
|
551
|
+
const UI_NAMES = { auto: 'Auto (default)', balanced: 'Balanced', 'cost-saver': 'Conservative', 'quality-first': 'Aggressive' };
|
|
536
552
|
console.log('');
|
|
537
|
-
console.log(` ✅ Mode switched: ${PEMOJIS[
|
|
553
|
+
console.log(` ✅ Mode switched: ${PEMOJIS[resolvedMode] || ''} ${UI_NAMES[resolvedMode] || resolvedMode}`);
|
|
538
554
|
console.log(` ${profile.description}`);
|
|
539
555
|
console.log('');
|
|
540
556
|
console.log(' 🧭 Routing changes:');
|
|
@@ -586,7 +602,7 @@ function cmdBudget() {
|
|
|
586
602
|
};
|
|
587
603
|
|
|
588
604
|
const data = {
|
|
589
|
-
active: existing.active || '
|
|
605
|
+
active: existing.active || 'auto',
|
|
590
606
|
switched_at: existing.switched_at || new Date().toISOString(),
|
|
591
607
|
custom_overrides: customOverrides,
|
|
592
608
|
};
|
package/package.json
CHANGED