dual-brain 0.2.8 → 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.
@@ -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 into one of three tiers:
2039
- * { tier: 'free', command, args } — deterministic, zero tokens
2040
- * { tier: 'cheap' } — question haiku
2041
- * { tier: 'full' } — work task → confirm then dispatch
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 1: FREEexact command matches
2051
- const FREE_COMMANDS = new Map([
2052
- ['resume', 'resume'],
2053
- ['r', 'resume'],
2054
- ['status', 'status'],
2055
- ['sessions', 'sessions'],
2056
- ['ss', 'sessions'],
2057
- ['settings', 'settings'],
2058
- ['s', 'settings'],
2059
- ['team', 'team'],
2060
- ['t', 'team'],
2061
- ['doctor', 'doctor'],
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: SKILLslash 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
- // Tier 2: CHEAP question / diagnostic patterns → haiku
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
- const QUESTION_CONTAINS = /\b(why|what|how is|how are|where is|where are|explain|tell me|show me)\b/i;
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 2: CHEAPquestion/diagnostic, route to haiku
2953
+ // Tier 0.5: SKILLslash 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
- process.stdout.write(`\n Routing to haiku for quick answer...\n`);
2861
- return { next: 'go', prompt: input, model: 'haiku' };
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, confirm before dispatching
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: sonnet [Enter] to proceed, [n] to cancel\n\n`);
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.8",
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",
@@ -129,6 +135,7 @@
129
135
  "hooks/task-classifier.mjs",
130
136
  "hooks/model-registry.mjs",
131
137
  "hooks/auto-update-wrapper.mjs",
138
+ "hooks/session-end.mjs",
132
139
  "hooks/head-guard.mjs",
133
140
  "hooks/auto-update.sh",
134
141
  "mcp-server/*.mjs",