dual-brain 7.1.3 → 7.1.5
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/CLAUDE.md +5 -5
- package/README.md +3 -3
- package/bin/dual-brain.mjs +66 -18
- package/hooks/head-guard.mjs +43 -7
- package/hooks/test-orchestrator.mjs +6 -6
- package/mcp-server/index.mjs +2 -2
- package/package.json +44 -4
- package/plugin.json +1 -1
- package/src/decide.mjs +68 -6
- package/src/dispatch.mjs +4 -3
- package/src/index.mjs +2 -2
- package/src/profile.mjs +6 -103
- package/src/session.mjs +24 -7
- package/src/tui.mjs +10 -1
- package/hooks/agent-fleet.mjs +0 -659
- package/hooks/context-guard.mjs +0 -468
- package/hooks/dag-scheduler.mjs +0 -1249
- package/hooks/head-guard.sh +0 -41
- package/hooks/hook-dispatch.mjs +0 -254
- package/hooks/ledger-analysis.mjs +0 -337
- package/hooks/parallelism-scaler.mjs +0 -572
- package/hooks/quality-tiers.mjs +0 -642
- package/src/test.mjs +0 -1374
package/CLAUDE.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
This project uses dual-provider orchestration. Config: `.claude/orchestrator.json`.
|
|
4
4
|
|
|
5
|
-
## Core Architecture (
|
|
5
|
+
## Core Architecture (v7)
|
|
6
6
|
|
|
7
7
|
Four modules in `src/` form the decision pipeline:
|
|
8
8
|
|
|
@@ -71,21 +71,21 @@ Dual-brain is a multi-round conversation between Claude and GPT — not a single
|
|
|
71
71
|
## Quality Gate
|
|
72
72
|
|
|
73
73
|
Before ending a session with code changes:
|
|
74
|
-
1.
|
|
75
|
-
2.
|
|
74
|
+
1. `node .claude/hooks/session-report.mjs` (allowed by head-guard for hook scripts)
|
|
75
|
+
2. `node .claude/hooks/quality-gate.mjs`
|
|
76
76
|
|
|
77
77
|
Gate statuses: `pass` (safe to end), `issues_found` (fix first), `needs_human_review` (GPT unavailable).
|
|
78
78
|
|
|
79
79
|
## Profiles
|
|
80
80
|
|
|
81
|
-
Profile persists to `.
|
|
81
|
+
Profile persists to `.dualbrain/profile.json` (project-scoped, gitignored).
|
|
82
82
|
|
|
83
83
|
- **auto** (default): Adapts routing based on task risk, provider health, and outcomes
|
|
84
84
|
- **balanced**: Best model per tier, normal budgets, reviews at medium+ risk
|
|
85
85
|
- **cost-saver**: Prefer cheaper models, lower budgets, skip GPT for non-critical
|
|
86
86
|
- **quality-first**: Dual-brain for medium+ risk, higher budgets, stricter reviews
|
|
87
87
|
|
|
88
|
-
Switch
|
|
88
|
+
Switch via the interactive Profile screen in `dual-brain`, or set `bias` in `.dualbrain/profile.json`.
|
|
89
89
|
|
|
90
90
|
## Adaptive Routing (Auto Mode)
|
|
91
91
|
|
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ dual-brain detects the intent and risk of your task, picks the best model based
|
|
|
24
24
|
|
|
25
25
|
### `dual-brain init`
|
|
26
26
|
|
|
27
|
-
First-time setup. Three questions: which providers you have, subscription tiers, and optimization preference.
|
|
27
|
+
First-time setup. Three questions: which providers you have, subscription tiers, and optimization preference. The actual flow auto-detects existing auth and adapts — you may see fewer prompts if credentials are already configured.
|
|
28
28
|
|
|
29
29
|
```
|
|
30
30
|
Dual-Brain Orchestrator — First-time setup
|
|
@@ -121,7 +121,7 @@ Preferences are stored in `.dualbrain/profile.json` and applied on every `go` in
|
|
|
121
121
|
import { orchestrate } from 'dual-brain';
|
|
122
122
|
|
|
123
123
|
const result = await orchestrate({ prompt: "fix the bug", cwd: "." });
|
|
124
|
-
console.log(result.summary);
|
|
124
|
+
console.log(result.result?.summary);
|
|
125
125
|
```
|
|
126
126
|
|
|
127
127
|
Individual modules are also exported:
|
|
@@ -174,7 +174,7 @@ For Claude Code users, a hooks layer provides deeper integration. Hooks fire on
|
|
|
174
174
|
|
|
175
175
|
```bash
|
|
176
176
|
# Install hooks into .claude/settings.json
|
|
177
|
-
npx
|
|
177
|
+
npx dual-brain install
|
|
178
178
|
```
|
|
179
179
|
|
|
180
180
|
The installer auto-detects your environment (Claude CLI, Codex CLI, Replit), registers `enforce-tier.mjs` and `cost-logger.mjs` hooks, and writes `orchestrator.json` with your subscription config. Re-run anytime — it's idempotent.
|
package/bin/dual-brain.mjs
CHANGED
|
@@ -66,9 +66,7 @@ Commands:
|
|
|
66
66
|
|
|
67
67
|
Interactive mode (entered with no args on a TTY):
|
|
68
68
|
Shows dashboard screen with menu-driven navigation.
|
|
69
|
-
[
|
|
70
|
-
[s] Status, [p] Profile, [a] Auth, [d] Diagnostics
|
|
71
|
-
[c] Command mode (REPL), [q] Exit
|
|
69
|
+
[s] Status, [p] Profile, [a] Auth, [d] Diagnostics, [q] Exit
|
|
72
70
|
|
|
73
71
|
Options:
|
|
74
72
|
--version Print version
|
|
@@ -82,8 +80,9 @@ Options:
|
|
|
82
80
|
/**
|
|
83
81
|
* Print a compact auth status table to stdout.
|
|
84
82
|
* @param {{ claude: object, openai: object }} auth Result from detectAuth()
|
|
83
|
+
* @param {object} [profile] Optional loaded profile to cross-check enabled state
|
|
85
84
|
*/
|
|
86
|
-
function printAuthTable(auth) {
|
|
85
|
+
function printAuthTable(auth, profile) {
|
|
87
86
|
const W = 55; // inner width (wide enough for source labels)
|
|
88
87
|
const hbar = '═'.repeat(W);
|
|
89
88
|
const pad = (s) => {
|
|
@@ -91,15 +90,21 @@ function printAuthTable(auth) {
|
|
|
91
90
|
return s + ' '.repeat(Math.max(0, W - visible.length));
|
|
92
91
|
};
|
|
93
92
|
|
|
93
|
+
const claudeDisabled = profile?.providers?.claude?.enabled === false;
|
|
94
|
+
const openaiDisabled = profile?.providers?.openai?.enabled === false;
|
|
95
|
+
|
|
96
|
+
const claudeDisabledNote = claudeDisabled ? ' (auth ok, but disabled in profile)' : '';
|
|
97
|
+
const openaiDisabledNote = openaiDisabled ? ' (auth ok, but disabled in profile)' : '';
|
|
98
|
+
|
|
94
99
|
const claudeLine1 = auth.claude.found
|
|
95
|
-
? ` Claude: ✓ found via ${auth.claude.source}`
|
|
100
|
+
? ` Claude: ✓ found via ${auth.claude.source}${claudeDisabledNote}`
|
|
96
101
|
: ` Claude: ✗ not found`;
|
|
97
102
|
const claudeLine2 = auth.claude.found
|
|
98
103
|
? ` ${auth.claude.masked}`
|
|
99
104
|
: ` run: dual-brain auth setup`;
|
|
100
105
|
|
|
101
106
|
const openaiLine1 = auth.openai.found
|
|
102
|
-
? ` OpenAI: ✓ found via ${auth.openai.source}`
|
|
107
|
+
? ` OpenAI: ✓ found via ${auth.openai.source}${openaiDisabledNote}`
|
|
103
108
|
: ` OpenAI: ✗ not found`;
|
|
104
109
|
const openaiLine2 = auth.openai.found
|
|
105
110
|
? ` ${auth.openai.masked}`
|
|
@@ -122,7 +127,7 @@ async function cmdInit(rl) {
|
|
|
122
127
|
|
|
123
128
|
// --- Step 1: Auth preflight ---
|
|
124
129
|
const auth = await detectAuth();
|
|
125
|
-
printAuthTable(auth);
|
|
130
|
+
printAuthTable(auth, loadProfile(cwd));
|
|
126
131
|
|
|
127
132
|
const noneFound = !auth.claude.found && !auth.openai.found;
|
|
128
133
|
if (noneFound) {
|
|
@@ -148,6 +153,9 @@ async function cmdInit(rl) {
|
|
|
148
153
|
const profile = await runOnboarding({ interactive: true, detectedAuth: auth, rl });
|
|
149
154
|
saveProfile(profile, { cwd });
|
|
150
155
|
|
|
156
|
+
// --- Step 2b: Install hooks so enforcement is active from first run ---
|
|
157
|
+
await cmdInstall(cwd);
|
|
158
|
+
|
|
151
159
|
// --- Step 3: Show dashboard ---
|
|
152
160
|
console.log('');
|
|
153
161
|
const repo = loadRepoCache(cwd);
|
|
@@ -166,7 +174,8 @@ async function cmdAuth(subArgs = [], rl) {
|
|
|
166
174
|
}
|
|
167
175
|
|
|
168
176
|
const auth = await detectAuth();
|
|
169
|
-
|
|
177
|
+
const profile = loadProfile(process.cwd());
|
|
178
|
+
printAuthTable(auth, profile);
|
|
170
179
|
|
|
171
180
|
// If anything is missing, point to setup command
|
|
172
181
|
if (!auth.claude.found || !auth.openai.found) {
|
|
@@ -272,6 +281,27 @@ async function cmdGo(args) {
|
|
|
272
281
|
}, cwd);
|
|
273
282
|
} else {
|
|
274
283
|
result = await dispatch({ decision, prompt, files, cwd });
|
|
284
|
+
if (result.status === 'completed' && result.type === 'native-agent') {
|
|
285
|
+
const nd = result.nativeDispatch || {};
|
|
286
|
+
const promptPreview = (nd.prompt || prompt).slice(0, 100);
|
|
287
|
+
const promptSuffix = (nd.prompt || prompt).length > 100 ? '...' : '';
|
|
288
|
+
console.log(`\nRouted: ${decision.provider}/${nd.model || decision.model} (${decision.tier})`);
|
|
289
|
+
console.log('To dispatch, use the Agent tool with:');
|
|
290
|
+
console.log(` model: ${nd.model || decision.model}`);
|
|
291
|
+
console.log(` prompt: ${promptPreview}${promptSuffix}`);
|
|
292
|
+
if (nd.isolation) console.log(` isolation: ${nd.isolation}`);
|
|
293
|
+
if (nd.maxTurns) console.log(` maxTurns: ${nd.maxTurns}`);
|
|
294
|
+
saveSession({
|
|
295
|
+
objective: prompt,
|
|
296
|
+
branch: null,
|
|
297
|
+
filesChanged: files,
|
|
298
|
+
commandsRun: [`dual-brain go "${prompt}"`],
|
|
299
|
+
lastResult: { status: 'success', summary: `native-agent routed to ${nd.model || decision.model}` },
|
|
300
|
+
provider: decision.provider,
|
|
301
|
+
nextAction: null,
|
|
302
|
+
}, cwd);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
275
305
|
const statusLine = result.status === 'completed' ? 'Done' : `Failed (exit ${result.exitCode})`;
|
|
276
306
|
console.log(`\n${statusLine} in ${(result.durationMs / 1000).toFixed(1)}s`);
|
|
277
307
|
if (result.summary) console.log(result.summary);
|
|
@@ -339,10 +369,20 @@ async function cmdStatus(args = []) {
|
|
|
339
369
|
const totalTokens = Object.values(sessionStats).reduce((s, v) => s + v.tokens, 0);
|
|
340
370
|
console.log(`\nSession: ${totalCalls} dispatch${totalCalls !== 1 ? 'es' : ''}, ${totalTokens} tokens observed`);
|
|
341
371
|
|
|
342
|
-
// Models
|
|
372
|
+
// Models — only list enabled providers
|
|
343
373
|
console.log('\nAvailable models:');
|
|
344
|
-
|
|
345
|
-
|
|
374
|
+
const claudeEnabled = profile?.providers?.claude?.enabled !== false;
|
|
375
|
+
const openaiEnabled = profile?.providers?.openai?.enabled !== false;
|
|
376
|
+
if (claudeEnabled && available.claude.length) {
|
|
377
|
+
console.log(` Claude : ${available.claude.join(', ')}`);
|
|
378
|
+
} else if (!claudeEnabled) {
|
|
379
|
+
console.log(` Claude : (disabled — run "dual-brain init" to enable)`);
|
|
380
|
+
}
|
|
381
|
+
if (openaiEnabled && available.openai.length) {
|
|
382
|
+
console.log(` OpenAI : ${available.openai.join(', ')}`);
|
|
383
|
+
} else if (!openaiEnabled) {
|
|
384
|
+
console.log(` OpenAI : (disabled — run "dual-brain init" to enable)`);
|
|
385
|
+
}
|
|
346
386
|
|
|
347
387
|
// Head model
|
|
348
388
|
console.log(`\nHead model : ${getHeadModel(profile)}`);
|
|
@@ -425,7 +465,7 @@ async function cmdStatus(args = []) {
|
|
|
425
465
|
|
|
426
466
|
const PROVIDER_MODEL_CLASSES = {
|
|
427
467
|
claude: ['haiku', 'sonnet', 'opus'],
|
|
428
|
-
openai: ['
|
|
468
|
+
openai: ['gpt-4.1-mini', 'gpt-4.1', 'gpt-5.2', 'gpt-5.4-mini', 'gpt-5.3-codex', 'gpt-5.4', 'gpt-5.5'],
|
|
429
469
|
};
|
|
430
470
|
|
|
431
471
|
function cmdHot(providerArg) {
|
|
@@ -448,8 +488,8 @@ function cmdCool(providerArg) {
|
|
|
448
488
|
console.log(`Cleared hot state for all ${provider} model classes.`);
|
|
449
489
|
}
|
|
450
490
|
|
|
451
|
-
async function cmdInstall() {
|
|
452
|
-
|
|
491
|
+
async function cmdInstall(cwd) {
|
|
492
|
+
if (!cwd) cwd = process.cwd();
|
|
453
493
|
|
|
454
494
|
// Run the main install.mjs (orchestrator config, all hooks, CLAUDE.md, etc.)
|
|
455
495
|
const { spawnSync } = await import('child_process');
|
|
@@ -468,8 +508,6 @@ async function cmdInstall() {
|
|
|
468
508
|
console.log(`Enforcement hooks already present (${skipped.length}):`);
|
|
469
509
|
for (const item of skipped) console.log(` = ${item}`);
|
|
470
510
|
}
|
|
471
|
-
|
|
472
|
-
process.exit(0);
|
|
473
511
|
}
|
|
474
512
|
|
|
475
513
|
function cmdRemember(text) {
|
|
@@ -541,6 +579,7 @@ async function welcomeScreen(rl, ask) {
|
|
|
541
579
|
} else {
|
|
542
580
|
// Enter or anything else → save and go to dashboard
|
|
543
581
|
saveProfile(setup.profile, { cwd });
|
|
582
|
+
await cmdInstall(cwd);
|
|
544
583
|
return { next: 'dashboard' };
|
|
545
584
|
}
|
|
546
585
|
} else {
|
|
@@ -667,6 +706,8 @@ async function welcomeScreen(rl, ask) {
|
|
|
667
706
|
console.log(box('Setup Complete', summaryLines));
|
|
668
707
|
console.log('');
|
|
669
708
|
|
|
709
|
+
await cmdInstall(cwd);
|
|
710
|
+
|
|
670
711
|
return { next: 'dashboard' };
|
|
671
712
|
}
|
|
672
713
|
|
|
@@ -680,8 +721,15 @@ async function dashboardScreen(rl, ask) {
|
|
|
680
721
|
const env = detectEnvironment();
|
|
681
722
|
|
|
682
723
|
// Build status lines for box
|
|
683
|
-
|
|
684
|
-
const
|
|
724
|
+
// If auth is found but provider is disabled in profile, show warning instead of green
|
|
725
|
+
const claudeProviderEnabled = profile?.providers?.claude?.enabled !== false;
|
|
726
|
+
const openaiProviderEnabled = profile?.providers?.openai?.enabled !== false;
|
|
727
|
+
const claudeStatus = auth.claude.found
|
|
728
|
+
? (claudeProviderEnabled ? `🟢 Claude ${badge('connected')}` : `⚠️ Claude ${badge('warning')} disabled`)
|
|
729
|
+
: `🔴 Claude ${badge('missing')}`;
|
|
730
|
+
const openaiStatus = auth.openai.found
|
|
731
|
+
? (openaiProviderEnabled ? `🟢 OpenAI ${badge('connected')}` : `⚠️ OpenAI ${badge('warning')} disabled`)
|
|
732
|
+
: `🔴 OpenAI ${badge('missing')}`;
|
|
685
733
|
const envLabel = env.hasReplitTools ? 'Replit + replit-tools' : env.isReplit ? 'Replit' : 'local';
|
|
686
734
|
|
|
687
735
|
// Enforcement check
|
package/hooks/head-guard.mjs
CHANGED
|
@@ -15,7 +15,15 @@
|
|
|
15
15
|
|
|
16
16
|
import { readFileSync } from 'fs';
|
|
17
17
|
|
|
18
|
-
const BLOCKED_TOOLS = new Set(['Edit', 'Write', 'NotebookEdit'
|
|
18
|
+
const BLOCKED_TOOLS = new Set(['Edit', 'Write', 'NotebookEdit']);
|
|
19
|
+
|
|
20
|
+
// Patterns that indicate a Bash command is writing/mutating the filesystem.
|
|
21
|
+
// Anchored to avoid false positives on grep/find output containing these words.
|
|
22
|
+
const WRITE_BASH_RE = /\brm\b|\bmv\b|\bcp\b|\bmkdir\b|\btouch\b|\bchmod\b|\bchown\b|\bdd\b|\binstall\b|\btruncate\b|\btee\b|\bsed\s+-i\b|\bawk\s+-i\b|>>|(?<![><])>(?![>=])/;
|
|
23
|
+
|
|
24
|
+
function isBashWriteIntent(command) {
|
|
25
|
+
return WRITE_BASH_RE.test(command);
|
|
26
|
+
}
|
|
19
27
|
|
|
20
28
|
// Read stdin JSON payload
|
|
21
29
|
let input;
|
|
@@ -23,9 +31,16 @@ try {
|
|
|
23
31
|
const raw = readFileSync('/dev/stdin', 'utf8');
|
|
24
32
|
input = JSON.parse(raw);
|
|
25
33
|
} catch {
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
|
|
34
|
+
// Can't parse input — fail closed to avoid guard bypass.
|
|
35
|
+
const output = {
|
|
36
|
+
hookSpecificOutput: {
|
|
37
|
+
hookEventName: 'PreToolUse',
|
|
38
|
+
permissionDecision: 'deny',
|
|
39
|
+
permissionDecisionReason: '[dual-brain] head-guard could not parse hook input — blocking as a safety measure.',
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
process.stdout.write(JSON.stringify(output));
|
|
43
|
+
process.exit(2);
|
|
29
44
|
}
|
|
30
45
|
|
|
31
46
|
const toolName = input.tool_name || '';
|
|
@@ -50,9 +65,30 @@ if (BLOCKED_TOOLS.has(toolName)) {
|
|
|
50
65
|
process.exit(2);
|
|
51
66
|
}
|
|
52
67
|
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
if (toolName
|
|
68
|
+
// Bash: allow read-only commands; block write-intent ones.
|
|
69
|
+
// Always allow node .claude/hooks/ and node hooks/ — CLAUDE.md instructs HEAD to run these.
|
|
70
|
+
if (toolName === 'Bash') {
|
|
71
|
+
const command = (input.tool_input && input.tool_input.command) || '';
|
|
72
|
+
if (/^node\s+\.?(?:\.claude\/)?hooks\//.test(command.trimStart())) {
|
|
73
|
+
process.exit(0);
|
|
74
|
+
}
|
|
75
|
+
if (isBashWriteIntent(command)) {
|
|
76
|
+
const output = {
|
|
77
|
+
hookSpecificOutput: {
|
|
78
|
+
hookEventName: 'PreToolUse',
|
|
79
|
+
permissionDecision: 'deny',
|
|
80
|
+
permissionDecisionReason:
|
|
81
|
+
'[dual-brain] HEAD cannot run write-intent Bash commands. Dispatch via: dual-brain go "task description"',
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
process.stdout.write(JSON.stringify(output));
|
|
85
|
+
process.exit(2);
|
|
86
|
+
}
|
|
87
|
+
process.exit(0);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Block MCP filesystem write tools by name.
|
|
91
|
+
if (toolName.startsWith('mcp__') && /write|create|delete|remove|move|rename|append|patch|truncate|copy|commit|push|stage|merge|update|overwrite/i.test(toolName)) {
|
|
56
92
|
const output = {
|
|
57
93
|
hookSpecificOutput: {
|
|
58
94
|
hookEventName: 'PreToolUse',
|
|
@@ -445,7 +445,7 @@ test('enforce-tier: cost-saver demotes think', () => {
|
|
|
445
445
|
// cost-saver's demote_think=true demotes think→execute when text lacks think words
|
|
446
446
|
const payload = JSON.stringify({
|
|
447
447
|
tool_name: 'Agent',
|
|
448
|
-
tool_input: { prompt: 'edit the README file', model: 'opus' },
|
|
448
|
+
tool_input: { prompt: '<!-- dual-brain-dispatch: test23 -->edit the README file', model: 'opus' },
|
|
449
449
|
});
|
|
450
450
|
const { parsed, status } = run(ENFORCE_TIER, payload);
|
|
451
451
|
if (status !== 0) return `non-zero exit: ${status}`;
|
|
@@ -494,7 +494,7 @@ test('enforce-tier: auto profile with high-risk file', () => {
|
|
|
494
494
|
// Description with auth/credentials path → risk classifier detects critical risk → promote to think
|
|
495
495
|
const payload = JSON.stringify({
|
|
496
496
|
tool_name: 'Agent',
|
|
497
|
-
tool_input: { description: 'update src/auth/credentials.mjs', prompt: 'change the token logic', model: 'sonnet' },
|
|
497
|
+
tool_input: { description: 'update src/auth/credentials.mjs', prompt: '<!-- dual-brain-dispatch: test25 -->change the token logic', model: 'sonnet' },
|
|
498
498
|
});
|
|
499
499
|
const { parsed, status } = run(ENFORCE_TIER, payload);
|
|
500
500
|
if (status !== 0) return `non-zero exit: ${status}`;
|
|
@@ -740,9 +740,9 @@ test('install: preserves existing hooks', () => {
|
|
|
740
740
|
if (!installSrc.includes('.filter'))
|
|
741
741
|
return 'install.mjs missing .filter() call — may clobber non-dual-brain hooks';
|
|
742
742
|
|
|
743
|
-
// The merge logic should
|
|
744
|
-
if (!installSrc.includes('existingEntries'))
|
|
745
|
-
return 'install.mjs missing
|
|
743
|
+
// The merge logic should filter existing hooks before merging dual-brain hooks
|
|
744
|
+
if (!installSrc.includes('existingPre') && !installSrc.includes('existingEntries'))
|
|
745
|
+
return 'install.mjs missing existing hook preservation — may not preserve other hooks';
|
|
746
746
|
|
|
747
747
|
// Verify it reads existing settings before overwriting
|
|
748
748
|
if (!installSrc.includes('existing') || !installSrc.includes('settings.json'))
|
|
@@ -1017,7 +1017,7 @@ test('adaptive loop: end-to-end hash match', () => {
|
|
|
1017
1017
|
writeFileSync(LEDGER, '', 'utf8');
|
|
1018
1018
|
|
|
1019
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' };
|
|
1020
|
+
const toolInput = { prompt: '<!-- dual-brain-dispatch: test40 -->fix the auth bug', description: 'patch auth module' };
|
|
1021
1021
|
const agentPayload = JSON.stringify({ tool_name: 'Agent', tool_input: toolInput });
|
|
1022
1022
|
|
|
1023
1023
|
// Step 2: Run enforce-tier with this payload (computes and may log a promptHash)
|
package/mcp-server/index.mjs
CHANGED
|
@@ -253,7 +253,7 @@ async function handleRequest(msg) {
|
|
|
253
253
|
return respond(id, {
|
|
254
254
|
protocolVersion: '2024-11-05',
|
|
255
255
|
capabilities: { tools: {} },
|
|
256
|
-
serverInfo: { name: 'dual-brain', version: '7.1.
|
|
256
|
+
serverInfo: { name: 'dual-brain', version: '7.1.4' },
|
|
257
257
|
});
|
|
258
258
|
|
|
259
259
|
case 'initialized':
|
|
@@ -283,7 +283,7 @@ async function handleRequest(msg) {
|
|
|
283
283
|
} catch (err) {
|
|
284
284
|
const code = err.code ?? -32000;
|
|
285
285
|
const message = err.message ?? 'Internal error';
|
|
286
|
-
return errorResponse(id, code, message
|
|
286
|
+
return errorResponse(id, code, message);
|
|
287
287
|
}
|
|
288
288
|
}
|
|
289
289
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dual-brain",
|
|
3
|
-
"version": "7.1.
|
|
3
|
+
"version": "7.1.5",
|
|
4
4
|
"description": "AI orchestration across Claude + OpenAI subscriptions — smart routing, budget awareness, and dual-brain collaboration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -46,10 +46,50 @@
|
|
|
46
46
|
"node": ">=20.0.0"
|
|
47
47
|
},
|
|
48
48
|
"files": [
|
|
49
|
-
"src
|
|
49
|
+
"src/profile.mjs",
|
|
50
|
+
"src/detect.mjs",
|
|
51
|
+
"src/decide.mjs",
|
|
52
|
+
"src/dispatch.mjs",
|
|
53
|
+
"src/playbook.mjs",
|
|
54
|
+
"src/health.mjs",
|
|
55
|
+
"src/repo.mjs",
|
|
56
|
+
"src/session.mjs",
|
|
57
|
+
"src/decompose.mjs",
|
|
58
|
+
"src/brief.mjs",
|
|
59
|
+
"src/redact.mjs",
|
|
60
|
+
"src/index.mjs",
|
|
61
|
+
"src/tui.mjs",
|
|
62
|
+
"src/install-hooks.mjs",
|
|
63
|
+
"src/update-check.mjs",
|
|
50
64
|
"bin/*.mjs",
|
|
51
|
-
"hooks
|
|
52
|
-
"hooks
|
|
65
|
+
"hooks/enforce-tier.mjs",
|
|
66
|
+
"hooks/cost-logger.mjs",
|
|
67
|
+
"hooks/cost-report.mjs",
|
|
68
|
+
"hooks/dual-brain-review.mjs",
|
|
69
|
+
"hooks/dual-brain-think.mjs",
|
|
70
|
+
"hooks/quality-gate.mjs",
|
|
71
|
+
"hooks/test-orchestrator.mjs",
|
|
72
|
+
"hooks/setup-wizard.mjs",
|
|
73
|
+
"hooks/health-check.mjs",
|
|
74
|
+
"hooks/install-git-hooks.mjs",
|
|
75
|
+
"hooks/session-report.mjs",
|
|
76
|
+
"hooks/budget-balancer.mjs",
|
|
77
|
+
"hooks/gpt-work-dispatcher.mjs",
|
|
78
|
+
"hooks/profiles.mjs",
|
|
79
|
+
"hooks/summary-checkpoint.mjs",
|
|
80
|
+
"hooks/decision-ledger.mjs",
|
|
81
|
+
"hooks/control-panel.mjs",
|
|
82
|
+
"hooks/risk-classifier.mjs",
|
|
83
|
+
"hooks/failure-detector.mjs",
|
|
84
|
+
"hooks/vibe-router.mjs",
|
|
85
|
+
"hooks/plan-generator.mjs",
|
|
86
|
+
"hooks/vibe-memory.mjs",
|
|
87
|
+
"hooks/wave-orchestrator.mjs",
|
|
88
|
+
"hooks/task-classifier.mjs",
|
|
89
|
+
"hooks/model-registry.mjs",
|
|
90
|
+
"hooks/auto-update-wrapper.mjs",
|
|
91
|
+
"hooks/head-guard.mjs",
|
|
92
|
+
"hooks/auto-update.sh",
|
|
53
93
|
"mcp-server/*.mjs",
|
|
54
94
|
"mcp-server/README.md",
|
|
55
95
|
"install.mjs",
|
package/plugin.json
CHANGED
package/src/decide.mjs
CHANGED
|
@@ -71,10 +71,37 @@ const MODEL_CAPABILITIES = {
|
|
|
71
71
|
effortLevels: ['low', 'medium', 'high'],
|
|
72
72
|
costTier: 'medium',
|
|
73
73
|
},
|
|
74
|
+
'gpt-5.2': {
|
|
75
|
+
provider: 'openai',
|
|
76
|
+
tierFit: ['search', 'execute'],
|
|
77
|
+
contextWindow: 200_000,
|
|
78
|
+
costTier: 'medium',
|
|
79
|
+
strengths: ['code-generation', 'analysis'],
|
|
80
|
+
weaknesses: [],
|
|
81
|
+
effortLevels: null,
|
|
82
|
+
},
|
|
83
|
+
'gpt-5.4-mini': {
|
|
84
|
+
provider: 'openai',
|
|
85
|
+
tierFit: ['search'],
|
|
86
|
+
contextWindow: 200_000,
|
|
87
|
+
costTier: 'low',
|
|
88
|
+
strengths: ['quick-tasks', 'search'],
|
|
89
|
+
weaknesses: ['complex-edits', 'architecture'],
|
|
90
|
+
effortLevels: null,
|
|
91
|
+
},
|
|
92
|
+
'gpt-5.3-codex': {
|
|
93
|
+
provider: 'openai',
|
|
94
|
+
tierFit: ['execute'],
|
|
95
|
+
contextWindow: 200_000,
|
|
96
|
+
costTier: 'medium',
|
|
97
|
+
strengths: ['code-generation', 'refactoring'],
|
|
98
|
+
weaknesses: ['architecture', 'security'],
|
|
99
|
+
effortLevels: null,
|
|
100
|
+
},
|
|
74
101
|
'gpt-5.4': {
|
|
75
102
|
provider: 'openai',
|
|
76
103
|
tierFit: ['execute', 'think'],
|
|
77
|
-
contextWindow:
|
|
104
|
+
contextWindow: 1_050_000,
|
|
78
105
|
strengths: ['refactor', 'debug', 'code-generation', 'test'],
|
|
79
106
|
weaknesses: ['cost'],
|
|
80
107
|
effortLevels: ['low', 'medium', 'high', 'xhigh'],
|
|
@@ -83,7 +110,7 @@ const MODEL_CAPABILITIES = {
|
|
|
83
110
|
'gpt-5.5': {
|
|
84
111
|
provider: 'openai',
|
|
85
112
|
tierFit: ['think'],
|
|
86
|
-
contextWindow:
|
|
113
|
+
contextWindow: 1_000_000,
|
|
87
114
|
strengths: ['architecture', 'security', 'review', 'planning', 'complex-debug'],
|
|
88
115
|
weaknesses: ['cost', 'latency'],
|
|
89
116
|
effortLevels: ['low', 'medium', 'high', 'xhigh'],
|
|
@@ -264,16 +291,19 @@ function applyHealthDowngrade(model, score, provider, available, isHighStakes) {
|
|
|
264
291
|
}
|
|
265
292
|
}
|
|
266
293
|
|
|
267
|
-
function applyProfileBias(model, profile, provider, available) {
|
|
294
|
+
function applyProfileBias(model, profile, provider, available, tier) {
|
|
268
295
|
const mode = profile?.mode || profile?.profile || 'auto';
|
|
269
296
|
if (mode === 'cost-saver') {
|
|
270
|
-
// Prefer cheapest available
|
|
297
|
+
// Prefer cheapest available that also fits the required tier
|
|
271
298
|
const ranks = {
|
|
272
299
|
claude: ['haiku', 'sonnet', 'opus'],
|
|
273
300
|
openai: ['gpt-4.1-mini', 'gpt-4.1', 'gpt-5.2', 'gpt-5.4-mini', 'gpt-5.3-codex', 'gpt-5.4', 'gpt-5.5'],
|
|
274
301
|
};
|
|
275
302
|
for (const m of ranks[provider]) {
|
|
276
|
-
if (available.includes(m))
|
|
303
|
+
if (!available.includes(m)) continue;
|
|
304
|
+
const caps = MODEL_CAPABILITIES[m];
|
|
305
|
+
if (tier && caps && !caps.tierFit.includes(tier)) continue;
|
|
306
|
+
return m;
|
|
277
307
|
}
|
|
278
308
|
}
|
|
279
309
|
if (mode === 'quality-first') {
|
|
@@ -449,6 +479,35 @@ export function parsePreferences(preferences) {
|
|
|
449
479
|
return signals;
|
|
450
480
|
}
|
|
451
481
|
|
|
482
|
+
// ─── Internal: safety floor for critical-risk tasks ───────────────────────────
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Ensure critical-risk tasks are never handled by the cheapest (haiku/gpt-4.1-mini) model.
|
|
486
|
+
* Cost-saver mode is the main culprit; escalate silently but emit a stderr warning.
|
|
487
|
+
* @param {string} model
|
|
488
|
+
* @param {string} provider
|
|
489
|
+
* @param {string[]} available
|
|
490
|
+
* @param {'low'|'medium'|'high'|'critical'} risk
|
|
491
|
+
* @returns {string}
|
|
492
|
+
*/
|
|
493
|
+
function applyCriticalRiskFloor(model, provider, available, risk) {
|
|
494
|
+
if (risk !== 'critical') return model;
|
|
495
|
+
|
|
496
|
+
const cheapModels = { claude: 'haiku', openai: 'gpt-4.1-mini' };
|
|
497
|
+
const floorModels = { claude: 'sonnet', openai: 'gpt-4.1' };
|
|
498
|
+
|
|
499
|
+
if (model === cheapModels[provider]) {
|
|
500
|
+
const floor = floorModels[provider];
|
|
501
|
+
const escalated = available.includes(floor) ? floor : available[available.length - 1] ?? model;
|
|
502
|
+
process.stderr.write(
|
|
503
|
+
`[dual-brain] Warning: cost-saver selected ${model} for a critical-risk task. ` +
|
|
504
|
+
`Escalating to ${escalated} (safety floor).\n`
|
|
505
|
+
);
|
|
506
|
+
return escalated;
|
|
507
|
+
}
|
|
508
|
+
return model;
|
|
509
|
+
}
|
|
510
|
+
|
|
452
511
|
// ─── Exported: decideRoute ────────────────────────────────────────────────────
|
|
453
512
|
|
|
454
513
|
/**
|
|
@@ -506,7 +565,10 @@ export function decideRoute({ profile = {}, detection = {}, cwd } = {}) {
|
|
|
506
565
|
model = applyHealthDowngrade(model, healthScores[provider], provider, available[provider], isHighStakes);
|
|
507
566
|
|
|
508
567
|
// Apply profile mode bias (cost-saver / quality-first / preferences) using patched profile
|
|
509
|
-
model = applyProfileBias(model, profileWithEffectiveBias, provider, available[provider]);
|
|
568
|
+
model = applyProfileBias(model, profileWithEffectiveBias, provider, available[provider], detection.tier);
|
|
569
|
+
|
|
570
|
+
// Safety floor: critical-risk tasks must never use haiku/gpt-4.1-mini even in cost-saver mode
|
|
571
|
+
model = applyCriticalRiskFloor(model, provider, available[provider], detection.risk);
|
|
510
572
|
|
|
511
573
|
// Apply preferModel signal from preferences (override after all other picks)
|
|
512
574
|
if (prefSignals.preferModel) {
|
package/src/dispatch.mjs
CHANGED
|
@@ -647,13 +647,14 @@ async function dispatch(input = {}) {
|
|
|
647
647
|
}
|
|
648
648
|
_recordDispatchBudget(prompt);
|
|
649
649
|
return {
|
|
650
|
-
status: '
|
|
650
|
+
status: 'completed',
|
|
651
|
+
type: 'native-agent',
|
|
651
652
|
provider: effectiveProvider,
|
|
652
653
|
model: effectiveModel,
|
|
653
654
|
command: null,
|
|
654
655
|
nativeDispatch: nativeDescriptor,
|
|
655
|
-
exitCode:
|
|
656
|
-
summary: `
|
|
656
|
+
exitCode: 0,
|
|
657
|
+
summary: `Routed to ${effectiveProvider}/${effectiveModel} (${effectiveDecision.tier})`,
|
|
657
658
|
durationMs: 0,
|
|
658
659
|
usage: null,
|
|
659
660
|
error: null,
|
package/src/index.mjs
CHANGED
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
* orchestrate() convenience function for programmatic use.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
export { loadProfile, saveProfile, ensureProfile, runOnboarding, rememberPreference, forgetPreference, getActivePreferences, getAvailableProviders, isSoloBrain, getHeadModel, detectAuth, detectEnvironment, setupAuth, getActiveKey
|
|
9
|
+
export { loadProfile, saveProfile, ensureProfile, runOnboarding, rememberPreference, forgetPreference, getActivePreferences, getAvailableProviders, isSoloBrain, getHeadModel, detectAuth, detectEnvironment, setupAuth, getActiveKey } from './profile.mjs';
|
|
10
10
|
export { detectTask, classifyIntent, classifyRisk, estimateComplexity, inferTier, extractPaths } from './detect.mjs';
|
|
11
|
-
export { decideRoute, getModelCapabilities, getAvailableModels,
|
|
11
|
+
export { decideRoute, getModelCapabilities, getAvailableModels, shouldDualBrain, explainDecision } from './decide.mjs';
|
|
12
12
|
export { dispatch, buildCommand, detectRuntime, compressResult, dispatchDualBrain } from './dispatch.mjs';
|
|
13
13
|
export { loadPlaybook, listPlaybooks, executePlaybook, createRunArtifact } from './playbook.mjs';
|
|
14
14
|
export { getHealth, markHot, markDegraded, markHealthy, checkCooldown, getProviderScore, recordDispatch, getSessionStats, resetHealth, remainingCooldownMinutes } from './health.mjs';
|