dual-brain 0.2.9 → 0.2.10
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 +208 -42
- package/package.json +8 -2
- package/src/agents/registry.mjs +405 -0
- package/src/collaboration.mjs +545 -0
- package/src/detect.mjs +73 -1
- package/src/dispatch.mjs +47 -5
- package/src/head.mjs +705 -263
- package/src/pipeline.mjs +387 -163
- package/src/provider-context.mjs +257 -0
package/bin/dual-brain.mjs
CHANGED
|
@@ -18,7 +18,21 @@ import {
|
|
|
18
18
|
loadCredentials, saveCredentials, getCredentialSummary, detectCredentials, addCredential, removeCredential, checkCredentialHealth,
|
|
19
19
|
} from '../src/profile.mjs';
|
|
20
20
|
|
|
21
|
-
import { detectTask } from '../src/detect.mjs';
|
|
21
|
+
import { detectTask, primeAgentRegistry } from '../src/detect.mjs';
|
|
22
|
+
|
|
23
|
+
// ─── Agent/skill registry cache (populated at startup) ───────────────────────
|
|
24
|
+
// These are set by _primeRegistryCache() so classifyInput can use them
|
|
25
|
+
// synchronously without async overhead on each keystroke.
|
|
26
|
+
let _cachedMatchSkill = null;
|
|
27
|
+
let _cachedSkillToTaskBrief = null;
|
|
28
|
+
|
|
29
|
+
async function _primeRegistryCache() {
|
|
30
|
+
try {
|
|
31
|
+
const reg = await import('../src/agents/registry.mjs');
|
|
32
|
+
_cachedMatchSkill = reg.matchSkill;
|
|
33
|
+
_cachedSkillToTaskBrief = reg.skillToTaskBrief;
|
|
34
|
+
} catch {}
|
|
35
|
+
}
|
|
22
36
|
|
|
23
37
|
import {
|
|
24
38
|
decideRoute, getAvailableModels,
|
|
@@ -2034,11 +2048,55 @@ function makeBoxRow(content, W) {
|
|
|
2034
2048
|
|
|
2035
2049
|
// ─── Command palette: input classifier ───────────────────────────────────────
|
|
2036
2050
|
|
|
2051
|
+
// HEAD state — loaded lazily, shared across REPL turns
|
|
2052
|
+
let _headState = null;
|
|
2053
|
+
let _headModuleCache = null;
|
|
2054
|
+
|
|
2055
|
+
async function _getHeadModule() {
|
|
2056
|
+
if (!_headModuleCache) {
|
|
2057
|
+
try {
|
|
2058
|
+
_headModuleCache = await import('../src/head.mjs');
|
|
2059
|
+
} catch {
|
|
2060
|
+
_headModuleCache = null;
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
return _headModuleCache;
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
function _getHeadState() {
|
|
2067
|
+
if (!_headState) {
|
|
2068
|
+
try {
|
|
2069
|
+
const head = _headModuleCache;
|
|
2070
|
+
_headState = head ? head.loadState() : null;
|
|
2071
|
+
} catch {
|
|
2072
|
+
_headState = null;
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
return _headState;
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
const FREE_COMMANDS = new Map([
|
|
2079
|
+
['resume', 'resume'], ['r', 'resume'],
|
|
2080
|
+
['status', 'status'], ['sessions', 'sessions'], ['ss', 'sessions'],
|
|
2081
|
+
['settings', 'settings'], ['s', 'settings'],
|
|
2082
|
+
['team', 'team'], ['t', 'team'],
|
|
2083
|
+
['doctor', 'doctor'], ['d', 'doctor'],
|
|
2084
|
+
['health', 'health'], ['h', 'health'],
|
|
2085
|
+
['projects', 'projects'], ['p', 'projects'],
|
|
2086
|
+
['help', 'help'], ['?', 'help'],
|
|
2087
|
+
['quit', 'quit'], ['q', 'quit'], ['exit', 'quit'],
|
|
2088
|
+
['budget', 'budget'], ['b', 'budget'],
|
|
2089
|
+
]);
|
|
2090
|
+
|
|
2037
2091
|
/**
|
|
2038
|
-
* Classify user input
|
|
2039
|
-
*
|
|
2040
|
-
*
|
|
2041
|
-
*
|
|
2092
|
+
* Classify user input using HEAD's cognitive pipeline.
|
|
2093
|
+
* Returns a tier-compatible object that maps HEAD's deliberation to the
|
|
2094
|
+
* existing REPL routing: free/skill/cheap/full, plus HEAD judgment metadata.
|
|
2095
|
+
*
|
|
2096
|
+
* { tier: 'free', command, args } — deterministic, zero tokens
|
|
2097
|
+
* { tier: 'skill', skill, args, command } — slash command
|
|
2098
|
+
* { tier: 'cheap', headJudgment } — question → haiku
|
|
2099
|
+
* { tier: 'full', headJudgment, model } — work task → dispatch
|
|
2042
2100
|
*/
|
|
2043
2101
|
function classifyInput(input) {
|
|
2044
2102
|
const trimmed = input.trim();
|
|
@@ -2047,37 +2105,23 @@ function classifyInput(input) {
|
|
|
2047
2105
|
const cmd = parts[0].toLowerCase();
|
|
2048
2106
|
const args = parts.slice(1);
|
|
2049
2107
|
|
|
2050
|
-
// Tier
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
['d', 'doctor'],
|
|
2063
|
-
['health', 'health'],
|
|
2064
|
-
['h', 'health'],
|
|
2065
|
-
['projects', 'projects'],
|
|
2066
|
-
['p', 'projects'],
|
|
2067
|
-
['help', 'help'],
|
|
2068
|
-
['?', 'help'],
|
|
2069
|
-
['quit', 'quit'],
|
|
2070
|
-
['q', 'quit'],
|
|
2071
|
-
['exit', 'quit'],
|
|
2072
|
-
['budget', 'budget'],
|
|
2073
|
-
['b', 'budget'],
|
|
2074
|
-
]);
|
|
2108
|
+
// Tier 0: SKILL — slash commands (checked first, deterministic)
|
|
2109
|
+
if (trimmed.startsWith('/')) {
|
|
2110
|
+
try {
|
|
2111
|
+
if (typeof _cachedMatchSkill === 'function') {
|
|
2112
|
+
const skill = _cachedMatchSkill(trimmed);
|
|
2113
|
+
if (skill) {
|
|
2114
|
+
const skillArgs = trimmed.replace(/^\/\w+\s*/, '');
|
|
2115
|
+
return { tier: 'skill', skill, args: skillArgs, command: skill.command };
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
} catch {}
|
|
2119
|
+
}
|
|
2075
2120
|
|
|
2121
|
+
// Tier 1: FREE — exact command matches (zero tokens, no HEAD needed)
|
|
2076
2122
|
if (FREE_COMMANDS.has(cmd)) {
|
|
2077
2123
|
return { tier: 'free', command: FREE_COMMANDS.get(cmd), args };
|
|
2078
2124
|
}
|
|
2079
|
-
|
|
2080
|
-
// Multi-word free commands
|
|
2081
2125
|
if (lower.startsWith('search ')) {
|
|
2082
2126
|
return { tier: 'free', command: 'search', args: parts.slice(1) };
|
|
2083
2127
|
}
|
|
@@ -2085,14 +2129,65 @@ function classifyInput(input) {
|
|
|
2085
2129
|
return { tier: 'free', command: 'init --replit', args: [] };
|
|
2086
2130
|
}
|
|
2087
2131
|
|
|
2088
|
-
//
|
|
2132
|
+
// ── HEAD cognitive pipeline: replaces regex-based cheap/full split ──────
|
|
2133
|
+
const head = _headModuleCache;
|
|
2134
|
+
if (head) {
|
|
2135
|
+
const state = _getHeadState() || head.freshState();
|
|
2136
|
+
const turn = head.processTurn(state, trimmed, {});
|
|
2137
|
+
_headState = state; // persist across turns
|
|
2138
|
+
|
|
2139
|
+
const judgment = {
|
|
2140
|
+
depth: turn.depth,
|
|
2141
|
+
action: turn.action,
|
|
2142
|
+
shouldAskUser: turn.shouldAskUser,
|
|
2143
|
+
shouldDispatch: turn.shouldDispatch,
|
|
2144
|
+
shouldClarify: turn.shouldClarify,
|
|
2145
|
+
shouldThink: turn.shouldThink,
|
|
2146
|
+
rationale: turn.rationale,
|
|
2147
|
+
confidence: turn.result.confidence,
|
|
2148
|
+
obligations: turn.result.obligations,
|
|
2149
|
+
surfaceNoticings: turn.result.surfaceNoticings,
|
|
2150
|
+
};
|
|
2151
|
+
|
|
2152
|
+
// Map HEAD's depth → tier + model
|
|
2153
|
+
if (turn.depth === 'reflexive' && !turn.shouldDispatch) {
|
|
2154
|
+
return { tier: 'cheap', headJudgment: judgment };
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
// HEAD says clarify → cheap tier (ask a question, don't dispatch work)
|
|
2158
|
+
if (turn.shouldClarify) {
|
|
2159
|
+
return { tier: 'cheap', headJudgment: judgment };
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
// HEAD says think/plan → full tier with opus
|
|
2163
|
+
if (turn.shouldThink) {
|
|
2164
|
+
return { tier: 'full', headJudgment: judgment, model: 'opus' };
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
// HEAD says dispatch → full tier, model based on depth
|
|
2168
|
+
if (turn.shouldDispatch) {
|
|
2169
|
+
const model = turn.depth === 'deep' ? 'opus' : 'sonnet';
|
|
2170
|
+
return { tier: 'full', headJudgment: judgment, model };
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
// HEAD says respond (not dispatch) → cheap
|
|
2174
|
+
if (turn.action.type === 'respond') {
|
|
2175
|
+
return { tier: 'cheap', headJudgment: judgment };
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
// Default: let depth drive it
|
|
2179
|
+
if (turn.depth === 'light' || turn.depth === 'reflexive') {
|
|
2180
|
+
return { tier: 'cheap', headJudgment: judgment };
|
|
2181
|
+
}
|
|
2182
|
+
return { tier: 'full', headJudgment: judgment };
|
|
2183
|
+
}
|
|
2184
|
+
|
|
2185
|
+
// ── Fallback: HEAD not loaded, use simple heuristics ───────────────────
|
|
2089
2186
|
const QUESTION_WORDS = /^(why|what|how|where|when|who|is my|check|show me|explain|tell me|list|am i|are there|does|did|can i|will|should i)/i;
|
|
2090
|
-
|
|
2091
|
-
if (QUESTION_WORDS.test(lower) || QUESTION_CONTAINS.test(lower)) {
|
|
2187
|
+
if (QUESTION_WORDS.test(lower)) {
|
|
2092
2188
|
return { tier: 'cheap' };
|
|
2093
2189
|
}
|
|
2094
2190
|
|
|
2095
|
-
// Tier 3: FULL — everything else is a work task
|
|
2096
2191
|
return { tier: 'full' };
|
|
2097
2192
|
}
|
|
2098
2193
|
|
|
@@ -2855,20 +2950,85 @@ async function mainScreen(rl, ask) {
|
|
|
2855
2950
|
// fallthrough: unknown free command → treat as full task
|
|
2856
2951
|
}
|
|
2857
2952
|
|
|
2858
|
-
// Tier
|
|
2953
|
+
// Tier 0.5: SKILL — slash command routed through agent registry
|
|
2954
|
+
if (classified.tier === 'skill') {
|
|
2955
|
+
const skill = classified.skill;
|
|
2956
|
+
const skillArgs = classified.args || '';
|
|
2957
|
+
|
|
2958
|
+
// Free skills (e.g. /status) run deterministically with no agent
|
|
2959
|
+
if (skill.tier === 'free' || !skill.agent) {
|
|
2960
|
+
if (skill.command === 'status') {
|
|
2961
|
+
await cmdStatus([]);
|
|
2962
|
+
await ask('\n Press Enter to continue...');
|
|
2963
|
+
return { next: 'main' };
|
|
2964
|
+
}
|
|
2965
|
+
return { next: 'main' };
|
|
2966
|
+
}
|
|
2967
|
+
|
|
2968
|
+
// Build the task brief from the skill declaration
|
|
2969
|
+
let brief = null;
|
|
2970
|
+
try {
|
|
2971
|
+
if (typeof _cachedSkillToTaskBrief === 'function') {
|
|
2972
|
+
brief = _cachedSkillToTaskBrief(input, skillArgs);
|
|
2973
|
+
}
|
|
2974
|
+
} catch {}
|
|
2975
|
+
|
|
2976
|
+
const model = brief?.model || skill.model || 'sonnet';
|
|
2977
|
+
const prompt = brief?.objective || `/${skill.command} ${skillArgs}`.trim();
|
|
2978
|
+
|
|
2979
|
+
process.stdout.write(`\n Skill: /${skill.command} Agent: ${skill.agent} Model: ${model}\n`);
|
|
2980
|
+
if (skill.description) process.stdout.write(` ${skill.description}\n`);
|
|
2981
|
+
process.stdout.write(` [Enter] to run, [n] to cancel\n\n`);
|
|
2982
|
+
const skillConfirm = (await ask(' > ')).trim().toLowerCase();
|
|
2983
|
+
if (skillConfirm === 'n' || skillConfirm === 'no') return { next: 'main' };
|
|
2984
|
+
|
|
2985
|
+
return { next: 'go', prompt, model };
|
|
2986
|
+
}
|
|
2987
|
+
|
|
2988
|
+
// Tier 2: CHEAP — question/diagnostic/reflexive
|
|
2859
2989
|
if (classified.tier === 'cheap') {
|
|
2860
|
-
|
|
2861
|
-
|
|
2990
|
+
const hj = classified.headJudgment;
|
|
2991
|
+
const model = hj ? 'haiku' : 'haiku';
|
|
2992
|
+
if (hj?.surfaceNoticings?.length > 0) {
|
|
2993
|
+
for (const n of hj.surfaceNoticings) {
|
|
2994
|
+
process.stdout.write(`\n \x1b[33m[HEAD]\x1b[0m ${n.observation}\n`);
|
|
2995
|
+
}
|
|
2996
|
+
}
|
|
2997
|
+
process.stdout.write(`\n Routing to ${model} for quick answer...\n`);
|
|
2998
|
+
return { next: 'go', prompt: input, model };
|
|
2862
2999
|
}
|
|
2863
3000
|
|
|
2864
|
-
// Tier 3: FULL — work task,
|
|
3001
|
+
// Tier 3: FULL — work task, HEAD-informed dispatch
|
|
2865
3002
|
if (classified.tier === 'full') {
|
|
3003
|
+
const hj = classified.headJudgment;
|
|
3004
|
+
const model = classified.model || 'sonnet';
|
|
2866
3005
|
const summary = input.length > 60 ? input.slice(0, 57) + '...' : input;
|
|
3006
|
+
|
|
3007
|
+
// Surface HEAD noticings before confirming
|
|
3008
|
+
if (hj?.surfaceNoticings?.length > 0) {
|
|
3009
|
+
for (const n of hj.surfaceNoticings) {
|
|
3010
|
+
process.stdout.write(`\n \x1b[33m[HEAD]\x1b[0m ${n.observation}`);
|
|
3011
|
+
}
|
|
3012
|
+
process.stdout.write('\n');
|
|
3013
|
+
}
|
|
3014
|
+
|
|
3015
|
+
// HEAD's shouldAskUser gates the dispatch — dangerous/irreversible ops
|
|
3016
|
+
if (hj?.shouldAskUser) {
|
|
3017
|
+
const reason = hj.obligations?.find(o => o.type === 'askBeforeIrreversi')?.description || hj.rationale;
|
|
3018
|
+
process.stdout.write(`\n \x1b[31m[HEAD GATE]\x1b[0m ${reason}\n`);
|
|
3019
|
+
process.stdout.write(` Task: ${summary}\n`);
|
|
3020
|
+
process.stdout.write(` Depth: ${hj.depth} Model: ${model} [Enter] to proceed, [n] to cancel\n\n`);
|
|
3021
|
+
const confirm = (await ask(' > ')).trim().toLowerCase();
|
|
3022
|
+
if (confirm === 'n' || confirm === 'no') return { next: 'main' };
|
|
3023
|
+
return { next: 'go', prompt: input, model };
|
|
3024
|
+
}
|
|
3025
|
+
|
|
3026
|
+
// Normal dispatch — show depth but don't block
|
|
2867
3027
|
process.stdout.write(`\n Launch coding session: ${summary}\n`);
|
|
2868
|
-
process.stdout.write(` Model:
|
|
3028
|
+
process.stdout.write(` Depth: ${hj?.depth || '?'} Model: ${model} [Enter] to proceed, [n] to cancel\n\n`);
|
|
2869
3029
|
const confirm = (await ask(' > ')).trim().toLowerCase();
|
|
2870
3030
|
if (confirm === 'n' || confirm === 'no') return { next: 'main' };
|
|
2871
|
-
return { next: 'go', prompt: input };
|
|
3031
|
+
return { next: 'go', prompt: input, model };
|
|
2872
3032
|
}
|
|
2873
3033
|
|
|
2874
3034
|
// Default fallback
|
|
@@ -5967,6 +6127,12 @@ async function cmdSpecialistGo(specialist, args) {
|
|
|
5967
6127
|
// ─── Entry point ─────────────────────────────────────────────────────────────
|
|
5968
6128
|
|
|
5969
6129
|
async function main() {
|
|
6130
|
+
// Prime agent + skill registries early so detectTask and classifyInput
|
|
6131
|
+
// can match agents/skills synchronously during interactive sessions.
|
|
6132
|
+
primeAgentRegistry().catch(() => {});
|
|
6133
|
+
_primeRegistryCache().catch(() => {});
|
|
6134
|
+
_getHeadModule().catch(() => {});
|
|
6135
|
+
|
|
5970
6136
|
const args = process.argv.slice(2);
|
|
5971
6137
|
const cmd = args[0];
|
|
5972
6138
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dual-brain",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.10",
|
|
4
4
|
"description": "AI orchestration across Claude + OpenAI subscriptions — smart routing, budget awareness, and dual-brain collaboration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -31,7 +31,10 @@
|
|
|
31
31
|
"./integrity": "./src/integrity.mjs",
|
|
32
32
|
"./prompt-audit": "./src/prompt-audit.mjs",
|
|
33
33
|
"./head": "./src/head.mjs",
|
|
34
|
-
"./templates": "./src/templates.mjs"
|
|
34
|
+
"./templates": "./src/templates.mjs",
|
|
35
|
+
"./agents": "./src/agents/registry.mjs",
|
|
36
|
+
"./collaboration": "./src/collaboration.mjs",
|
|
37
|
+
"./provider-context": "./src/provider-context.mjs"
|
|
35
38
|
},
|
|
36
39
|
"keywords": [
|
|
37
40
|
"claude-code",
|
|
@@ -102,6 +105,9 @@
|
|
|
102
105
|
"src/prompt-audit.mjs",
|
|
103
106
|
"src/head.mjs",
|
|
104
107
|
"src/templates.mjs",
|
|
108
|
+
"src/agents/registry.mjs",
|
|
109
|
+
"src/collaboration.mjs",
|
|
110
|
+
"src/provider-context.mjs",
|
|
105
111
|
"bin/*.mjs",
|
|
106
112
|
"hooks/enforce-tier.mjs",
|
|
107
113
|
"hooks/cost-logger.mjs",
|