dual-brain 0.2.26 → 0.2.28
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/bin/dual-brain.mjs +82 -0
- package/package.json +12 -2
- package/src/decide.mjs +45 -0
- package/src/dispatch.mjs +46 -0
- package/src/handoff.mjs +85 -0
- package/src/outcome.mjs +28 -0
- package/src/revert.mjs +149 -0
- package/src/routing-advisor.mjs +63 -1
- package/src/self-correct.mjs +145 -0
- package/src/settings-tui.mjs +373 -0
- package/src/strategy.mjs +235 -0
package/src/strategy.mjs
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
// strategy.mjs — Dispatch strategy library + selection
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
// ─── Strategy definitions ──────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
export const STRATEGIES = {
|
|
8
|
+
direct: {
|
|
9
|
+
id: 'direct',
|
|
10
|
+
label: 'Direct dispatch',
|
|
11
|
+
description: 'Single agent, single task. Best for clear, focused work.',
|
|
12
|
+
applicability: { maxFiles: 3, maxComplexity: 'moderate', maxRisk: 'medium' },
|
|
13
|
+
cost: 1.0,
|
|
14
|
+
},
|
|
15
|
+
cascade: {
|
|
16
|
+
id: 'cascade',
|
|
17
|
+
label: 'Think → Execute cascade',
|
|
18
|
+
description: 'Cheap thinker refines spec, then worker executes. Best for routine-but-multi-step tasks.',
|
|
19
|
+
applicability: { minFiles: 1, minComplexity: 'moderate', maxRisk: 'high' },
|
|
20
|
+
cost: 1.3,
|
|
21
|
+
},
|
|
22
|
+
split: {
|
|
23
|
+
id: 'split',
|
|
24
|
+
label: 'Decompose → parallel dispatch',
|
|
25
|
+
description: 'Break into sub-tasks, dispatch each at optimal tier. Best for large multi-file changes.',
|
|
26
|
+
applicability: { minFiles: 4, minComplexity: 'complex' },
|
|
27
|
+
cost: 2.0,
|
|
28
|
+
},
|
|
29
|
+
'dual-review': {
|
|
30
|
+
id: 'dual-review',
|
|
31
|
+
label: 'Execute → adversarial review',
|
|
32
|
+
description: 'Worker implements, second model reviews. Best for high-risk/security code.',
|
|
33
|
+
applicability: { minRisk: 'high' },
|
|
34
|
+
cost: 1.5,
|
|
35
|
+
},
|
|
36
|
+
'architect-editor': {
|
|
37
|
+
id: 'architect-editor',
|
|
38
|
+
label: 'Architect reasons → editor implements',
|
|
39
|
+
description: 'Opus/o3 reasons freely, sonnet/haiku formats the edits. Best for complex architecture + implementation.',
|
|
40
|
+
applicability: { minComplexity: 'complex', minFiles: 3 },
|
|
41
|
+
cost: 1.8,
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// ─── Helpers ───────────────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
const COMPLEXITY_RANK = { trivial: 0, simple: 1, moderate: 2, complex: 3 };
|
|
48
|
+
const RISK_RANK = { low: 0, medium: 1, high: 2, critical: 3 };
|
|
49
|
+
|
|
50
|
+
const COST_CAPS = {
|
|
51
|
+
frugal: 1.0,
|
|
52
|
+
'cost-saver': 1.3,
|
|
53
|
+
balanced: 2.0,
|
|
54
|
+
'quality-first': 3.0,
|
|
55
|
+
maximum: Infinity,
|
|
56
|
+
aggressive: Infinity, // maps to maximum behaviour
|
|
57
|
+
fullpower: Infinity,
|
|
58
|
+
fast: 1.3,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const SECURITY_KEYWORDS = /\b(auth|security|billing|payment|credential|secret|token|encrypt|permission|oauth|jwt)\b/i;
|
|
62
|
+
|
|
63
|
+
function costCap(workStyle) {
|
|
64
|
+
return COST_CAPS[workStyle] ?? 2.0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function fileCount(detection) {
|
|
68
|
+
return detection?.fileCount ?? detection?.files ?? 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function complexityRank(detection) {
|
|
72
|
+
return COMPLEXITY_RANK[detection?.complexity] ?? 1;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function riskRank(detection) {
|
|
76
|
+
return RISK_RANK[detection?.risk] ?? 0;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function prompt(detection) {
|
|
80
|
+
return detection?.prompt ?? detection?.description ?? '';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ─── Scoring ───────────────────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
function scoreStrategies(detection, workStyle) {
|
|
86
|
+
const files = fileCount(detection);
|
|
87
|
+
const cRank = complexityRank(detection);
|
|
88
|
+
const rRank = riskRank(detection);
|
|
89
|
+
const text = prompt(detection);
|
|
90
|
+
const frugal = workStyle === 'frugal';
|
|
91
|
+
const saver = workStyle === 'cost-saver' || workStyle === 'fast';
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
direct: 0.5,
|
|
95
|
+
|
|
96
|
+
cascade: 0
|
|
97
|
+
+ (cRank >= COMPLEXITY_RANK.moderate ? 0.3 : 0)
|
|
98
|
+
+ (files >= 2 ? 0.2 : 0)
|
|
99
|
+
- (frugal ? 0.5 : 0),
|
|
100
|
+
|
|
101
|
+
split: 0
|
|
102
|
+
+ (files >= 4 ? 0.4 : 0)
|
|
103
|
+
+ (cRank >= COMPLEXITY_RANK.complex ? 0.3 : 0)
|
|
104
|
+
- (frugal || saver ? 0.5 : 0),
|
|
105
|
+
|
|
106
|
+
'dual-review': 0
|
|
107
|
+
+ (rRank >= RISK_RANK.high ? 0.5 : 0)
|
|
108
|
+
+ (SECURITY_KEYWORDS.test(text) ? 0.3 : 0)
|
|
109
|
+
- (frugal ? 0.3 : 0),
|
|
110
|
+
|
|
111
|
+
'architect-editor': 0
|
|
112
|
+
+ (cRank >= COMPLEXITY_RANK.complex && files >= 3 ? 0.4 : 0)
|
|
113
|
+
- (saver ? 0.3 : 0),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ─── Export 1: selectStrategy ─────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Select the best dispatch strategy for a task.
|
|
121
|
+
* @param {object} detection — from detect.mjs (detectTask output)
|
|
122
|
+
* @param {object} decision — from decide.mjs (decideRoute output)
|
|
123
|
+
* @param {object} profile — user profile (workStyle, etc.)
|
|
124
|
+
* @returns {{ strategy: string, reason: string, alternatives: string[] }}
|
|
125
|
+
*/
|
|
126
|
+
export function selectStrategy(detection, decision, profile) {
|
|
127
|
+
try {
|
|
128
|
+
const workStyle = profile?.workStyle ?? profile?.bias ?? 'balanced';
|
|
129
|
+
const cap = costCap(workStyle);
|
|
130
|
+
const scores = scoreStrategies(detection, workStyle);
|
|
131
|
+
|
|
132
|
+
// Filter by cost cap, then rank
|
|
133
|
+
const ranked = Object.entries(scores)
|
|
134
|
+
.filter(([id]) => STRATEGIES[id].cost <= cap)
|
|
135
|
+
.sort(([, a], [, b]) => b - a);
|
|
136
|
+
|
|
137
|
+
if (!ranked.length) {
|
|
138
|
+
// Fallback — always allow direct
|
|
139
|
+
return { strategy: 'direct', reason: 'Cost cap allows only direct dispatch.', alternatives: [] };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const [bestId] = ranked[0];
|
|
143
|
+
const alternatives = ranked.slice(1).map(([id]) => id);
|
|
144
|
+
|
|
145
|
+
const reasons = {
|
|
146
|
+
direct: 'Clear, focused task within single-agent scope.',
|
|
147
|
+
cascade: 'Multi-step task benefits from spec refinement before execution.',
|
|
148
|
+
split: 'Large file count warrants decomposition into parallel sub-tasks.',
|
|
149
|
+
'dual-review': 'High-risk or security-sensitive work requires adversarial review.',
|
|
150
|
+
'architect-editor': 'Complex architecture + implementation benefits from dual-model reasoning.',
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
strategy: bestId,
|
|
155
|
+
reason: reasons[bestId] ?? 'Best match for task profile.',
|
|
156
|
+
alternatives,
|
|
157
|
+
};
|
|
158
|
+
} catch {
|
|
159
|
+
return { strategy: 'direct', reason: 'Fallback to direct dispatch.', alternatives: [] };
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ─── Export 2: describeStrategy ───────────────────────────────────────────────
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Human-readable description of a strategy.
|
|
167
|
+
* @param {string} strategyId
|
|
168
|
+
* @returns {string}
|
|
169
|
+
*/
|
|
170
|
+
export function describeStrategy(strategyId) {
|
|
171
|
+
const s = STRATEGIES[strategyId];
|
|
172
|
+
if (!s) return `Unknown strategy: ${strategyId}`;
|
|
173
|
+
return `${s.label} (cost ×${s.cost})\n${s.description}`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ─── Export 3: getStrategyForTask ─────────────────────────────────────────────
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Convenience: load profile + decision context, select strategy, return with execution plan.
|
|
180
|
+
* @param {object} detection — from detect.mjs
|
|
181
|
+
* @param {string} [cwd] — working directory (for profile loading)
|
|
182
|
+
* @returns {{ strategy: string, reason: string, alternatives: string[], plan: { steps: object[] } }}
|
|
183
|
+
*/
|
|
184
|
+
export function getStrategyForTask(detection, cwd) {
|
|
185
|
+
const dir = cwd ?? process.cwd();
|
|
186
|
+
let profile = {};
|
|
187
|
+
try {
|
|
188
|
+
const p = join(dir, '.dualbrain', 'config.json');
|
|
189
|
+
if (existsSync(p)) profile = JSON.parse(readFileSync(p, 'utf8'));
|
|
190
|
+
} catch { /* non-throwing */ }
|
|
191
|
+
|
|
192
|
+
// Minimal decision stub (model resolved from profile if available)
|
|
193
|
+
const decision = { model: profile?.models?.execute ?? 'sonnet' };
|
|
194
|
+
const selected = selectStrategy(detection, decision, profile);
|
|
195
|
+
|
|
196
|
+
return { ...selected, plan: buildPlan(selected.strategy, decision) };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ─── Plan builder ─────────────────────────────────────────────────────────────
|
|
200
|
+
|
|
201
|
+
function buildPlan(strategyId, decision) {
|
|
202
|
+
const m = decision?.model ?? 'sonnet';
|
|
203
|
+
const plans = {
|
|
204
|
+
direct: [
|
|
205
|
+
{ role: 'worker', model: m, description: 'Execute task' },
|
|
206
|
+
],
|
|
207
|
+
cascade: [
|
|
208
|
+
{ role: 'thinker', model: 'sonnet', description: 'Refine spec' },
|
|
209
|
+
{ role: 'worker', model: 'from-think', description: 'Execute refined spec' },
|
|
210
|
+
],
|
|
211
|
+
split: [
|
|
212
|
+
{ role: 'thinker', model: 'sonnet', description: 'Decompose into sub-tasks' },
|
|
213
|
+
{ role: 'worker', model: 'varies', description: 'Execute each sub-task' },
|
|
214
|
+
],
|
|
215
|
+
'dual-review': [
|
|
216
|
+
{ role: 'worker', model: m, description: 'Implement' },
|
|
217
|
+
{ role: 'reviewer', model: 'sonnet', description: 'Adversarial review' },
|
|
218
|
+
],
|
|
219
|
+
'architect-editor': [
|
|
220
|
+
{ role: 'thinker', model: 'opus', description: 'Architect solution' },
|
|
221
|
+
{ role: 'worker', model: 'haiku', description: 'Format edits' },
|
|
222
|
+
],
|
|
223
|
+
};
|
|
224
|
+
return { steps: plans[strategyId] ?? plans.direct };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ─── Export 4: listStrategies ─────────────────────────────────────────────────
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* List all strategies for display.
|
|
231
|
+
* @returns {{ id: string, label: string, description: string, cost: number }[]}
|
|
232
|
+
*/
|
|
233
|
+
export function listStrategies() {
|
|
234
|
+
return Object.values(STRATEGIES).map(({ id, label, description, cost }) => ({ id, label, description, cost }));
|
|
235
|
+
}
|