dual-brain 0.3.2 → 0.3.3

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.
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // dual-brain — CLI entry point. Commands: init, go, think, review, status, remember, forget
2
+ // dual-brain — CLI entry point. Commands: init, go, think, review, status, handoff, remember, forget
3
3
 
4
4
  import { appendFileSync, existsSync, readFileSync, mkdirSync, writeFileSync, statSync, readdirSync, unlinkSync, watch as fsWatch } from 'node:fs';
5
5
  import { join, dirname, basename, extname } from 'node:path';
@@ -294,6 +294,9 @@ Commands:
294
294
  --dry-run Show routing decision without executing
295
295
  --files a.mjs,b.mjs Provide file context for risk classification
296
296
  --verbose, -v Print routing trace (intent, risk, health, model selection)
297
+ handoff Cross-provider switch (auto-detect limited provider)
298
+ --to claude|codex Force handoff to a specific provider
299
+ --show Show current handoff context without switching
297
300
  think "question" Multi-round architecture decision with dual-brain
298
301
  pr Show PR status for current branch
299
302
  pr create Create PR from current branch with auto-generated description
@@ -1138,6 +1141,98 @@ const PROVIDER_MODEL_CLASSES = {
1138
1141
  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'],
1139
1142
  };
1140
1143
 
1144
+ async function cmdHandoff(args = []) {
1145
+ const cwd = process.cwd();
1146
+ const fxH = await getFx();
1147
+ const toProvider = flag(args, '--to');
1148
+ const showOnly = args.includes('--show');
1149
+
1150
+ let autoHandoff;
1151
+ try {
1152
+ autoHandoff = await import('../dist/src/auto-handoff.js');
1153
+ } catch (e) {
1154
+ err(`Could not load auto-handoff module: ${e.message}`);
1155
+ }
1156
+
1157
+ const { executeHandoff, detectLimitReached, exportSessionContext, getHandoffUX } = autoHandoff;
1158
+
1159
+ // --show: display current handoff context without switching
1160
+ if (showOnly) {
1161
+ const context = exportSessionContext(cwd);
1162
+ console.log(box('Handoff Context'));
1163
+ console.log('');
1164
+ if (context) {
1165
+ console.log(JSON.stringify(context, null, 2));
1166
+ } else {
1167
+ fxH.info('No active session context to export.');
1168
+ }
1169
+ return;
1170
+ }
1171
+
1172
+ const profile = loadProfile(cwd);
1173
+ const providers = getAvailableProviders(profile);
1174
+
1175
+ if (providers.length < 2 && !toProvider) {
1176
+ fxH.warn('Only one provider configured — nothing to hand off to.');
1177
+ fxH.info('Run: dual-brain init to configure a second provider.');
1178
+ return;
1179
+ }
1180
+
1181
+ // Detect which provider is limited
1182
+ const limitStatus = detectLimitReached(cwd);
1183
+ const ux = getHandoffUX(limitStatus, toProvider || null);
1184
+
1185
+ // Display UX message
1186
+ if (ux.title) console.log(box(ux.title));
1187
+ if (ux.message) {
1188
+ console.log('');
1189
+ fxH.info(ux.message);
1190
+ }
1191
+ if (ux.detail) {
1192
+ fxH.dim(ux.detail);
1193
+ }
1194
+ console.log('');
1195
+
1196
+ // If --to is specified, force handoff regardless of limit status
1197
+ if (toProvider) {
1198
+ const target = toProvider.toLowerCase();
1199
+ if (target !== 'claude' && target !== 'codex') {
1200
+ err('--to must be "claude" or "codex"');
1201
+ }
1202
+ const result = executeHandoff(cwd, target);
1203
+ if (result.success) {
1204
+ fxH.success(`Handed off to ${target}.`);
1205
+ if (result.command) {
1206
+ console.log('');
1207
+ fxH.info(`Resume with: ${result.command}`);
1208
+ }
1209
+ } else {
1210
+ fxH.error(result.error || `Handoff to ${target} failed.`);
1211
+ }
1212
+ return;
1213
+ }
1214
+
1215
+ // Auto-detect: if a provider is limited, switch to the other
1216
+ if (limitStatus.limited) {
1217
+ const target = limitStatus.switchTo;
1218
+ fxH.warn(`${limitStatus.provider} is limited — switching to ${target}`);
1219
+ console.log('');
1220
+ const result = executeHandoff(cwd, target);
1221
+ if (result.success) {
1222
+ fxH.success(`Handed off to ${target}.`);
1223
+ if (result.command) {
1224
+ console.log('');
1225
+ fxH.info(`Resume with: ${result.command}`);
1226
+ }
1227
+ } else {
1228
+ fxH.error(result.error || `Handoff to ${target} failed.`);
1229
+ }
1230
+ } else {
1231
+ fxH.success('No provider is currently limited. No handoff needed.');
1232
+ fxH.dim('Use --to claude or --to codex to force a switch.');
1233
+ }
1234
+ }
1235
+
1141
1236
  function cmdHot(providerArg) {
1142
1237
  if (!providerArg) err('Usage: dual-brain hot <provider> (claude | openai)');
1143
1238
  const provider = providerArg.toLowerCase();
@@ -6769,6 +6864,7 @@ async function main() {
6769
6864
  if (cmd === 'ship') { await cmdShip(); return; }
6770
6865
  if (cmd === 'pr') { await cmdPR(args.slice(1)); return; }
6771
6866
  if (cmd === 'status') { await cmdStatus(args.slice(1)); return; }
6867
+ if (cmd === 'handoff') { await cmdHandoff(args.slice(1)); return; }
6772
6868
  if (cmd === 'hot') { cmdHot(args[1]); return; }
6773
6869
  if (cmd === 'cool') { cmdCool(args[1]); return; }
6774
6870
  if (cmd === 'remember') { cmdRemember(args[1]); return; }
@@ -6832,7 +6928,7 @@ fi
6832
6928
  // If cmd is not a recognized subcommand, treat the entire arg list as a task.
6833
6929
  // e.g. `dual-brain fix failing tests` → same as `dual-brain go "fix failing tests"`
6834
6930
  const KNOWN_COMMANDS = new Set([
6835
- 'init', 'install', 'uninstall', 'auth', 'go', 'do', 'plan', 'ship', 'think', 'review', 'pr', 'status', 'hot', 'cool',
6931
+ 'init', 'install', 'uninstall', 'auth', 'go', 'do', 'plan', 'ship', 'think', 'review', 'pr', 'status', 'handoff', 'hot', 'cool',
6836
6932
  'remember', 'forget', 'break-glass', 'specialists', 'search', 'shell-hook', 'watch',
6837
6933
  '--help', '-h', '--version', '-v',
6838
6934
  ...Object.keys(loadSpecialistRegistry()),
@@ -0,0 +1,68 @@
1
+ /**
2
+ * auto-handoff.ts — Automatic cross-provider handoff system.
3
+ *
4
+ * When one provider hits its rate limit, this module exports the conversation
5
+ * context and switches to the other provider seamlessly.
6
+ */
7
+ export interface LimitStatus {
8
+ limited: boolean;
9
+ provider: string;
10
+ resetsAt?: string;
11
+ otherAvailable: boolean;
12
+ otherProvider?: string;
13
+ }
14
+ export interface HandoffContext {
15
+ objective: string;
16
+ filesChanged: string[];
17
+ recentDecisions: Array<{
18
+ model: string;
19
+ tier: string;
20
+ timestamp: string;
21
+ }>;
22
+ conversationSummary: string;
23
+ currentPhase: string;
24
+ exportedAt: string;
25
+ }
26
+ export interface HandoffOpts {
27
+ fromProvider: string;
28
+ cwd?: string;
29
+ auto?: boolean;
30
+ }
31
+ export interface HandoffResult {
32
+ success: boolean;
33
+ command?: string[];
34
+ contextFile?: string;
35
+ message: string;
36
+ }
37
+ export interface HandoffUXMessage {
38
+ text: string;
39
+ action: 'auto-switch' | 'prompt-setup' | 'wait';
40
+ command?: string;
41
+ }
42
+ /**
43
+ * Check if a provider is rate-limited.
44
+ * Reads from provider-manager state and checks for common rate limit patterns.
45
+ */
46
+ export declare function detectLimitReached(provider: string, cwd?: string): LimitStatus;
47
+ /**
48
+ * Read the current session state and produce a portable context object.
49
+ */
50
+ export declare function exportSessionContext(cwd?: string): HandoffContext;
51
+ /**
52
+ * Generate a prompt string that can be fed to the other provider to resume work.
53
+ */
54
+ export declare function buildHandoffPrompt(context: HandoffContext): string;
55
+ /**
56
+ * Execute the full handoff flow:
57
+ * 1. Detect which provider to switch TO
58
+ * 2. Export context
59
+ * 3. Build the handoff prompt
60
+ * 4. Return the command to launch the other CLI
61
+ *
62
+ * Does NOT actually spawn the process (caller decides).
63
+ */
64
+ export declare function executeHandoff(opts: HandoffOpts): HandoffResult;
65
+ /**
66
+ * Return the user-facing message for a rate limit situation.
67
+ */
68
+ export declare function getHandoffUX(status: LimitStatus): HandoffUXMessage;
@@ -0,0 +1,309 @@
1
+ /**
2
+ * auto-handoff.ts — Automatic cross-provider handoff system.
3
+ *
4
+ * When one provider hits its rate limit, this module exports the conversation
5
+ * context and switches to the other provider seamlessly.
6
+ */
7
+ import { existsSync, mkdirSync, readFileSync } from 'node:fs';
8
+ import { join } from 'node:path';
9
+ import { atomicWriteJson } from './integrity.js';
10
+ import { getProviderState } from './provider-manager.js';
11
+ import { loadSession } from './session.js';
12
+ // ─── Constants ──────────────────────────────────────────────────────────────
13
+ const HANDOFF_DIR = '.dual-brain/handoff';
14
+ const HANDOFF_FILE = 'latest.json';
15
+ const DISPATCH_LOG = '.dual-brain/dispatch-log.ndjson';
16
+ /** Map provider names to CLI commands. */
17
+ const PROVIDER_CLI = {
18
+ anthropic: 'claude',
19
+ openai: 'codex',
20
+ };
21
+ /** Map provider names to their "other". */
22
+ const OTHER_PROVIDER = {
23
+ anthropic: 'openai',
24
+ openai: 'anthropic',
25
+ claude: 'openai',
26
+ };
27
+ // ─── Helpers ────────────────────────────────────────────────────────────────
28
+ function normalizeProvider(provider) {
29
+ return provider === 'claude' ? 'anthropic' : provider;
30
+ }
31
+ function handoffDir(cwd) {
32
+ return join(cwd || process.cwd(), HANDOFF_DIR);
33
+ }
34
+ function handoffFile(cwd) {
35
+ return join(handoffDir(cwd), HANDOFF_FILE);
36
+ }
37
+ function ensureHandoffDir(cwd) {
38
+ const dir = handoffDir(cwd);
39
+ if (!existsSync(dir)) {
40
+ mkdirSync(dir, { recursive: true });
41
+ }
42
+ }
43
+ /**
44
+ * Read recent routing decisions from the dispatch log.
45
+ */
46
+ function readRecentDecisions(cwd, limit = 5) {
47
+ try {
48
+ const logPath = join(cwd || process.cwd(), DISPATCH_LOG);
49
+ if (!existsSync(logPath))
50
+ return [];
51
+ const content = readFileSync(logPath, 'utf8');
52
+ const lines = content.trim().split('\n').filter(Boolean);
53
+ // Take the last N entries
54
+ const recent = lines.slice(-limit);
55
+ const decisions = [];
56
+ for (const line of recent) {
57
+ try {
58
+ const entry = JSON.parse(line);
59
+ decisions.push({
60
+ model: entry.model || 'unknown',
61
+ tier: entry.tier || entry.provider || 'unknown',
62
+ timestamp: entry.timestamp || new Date().toISOString(),
63
+ });
64
+ }
65
+ catch {
66
+ // Skip malformed lines
67
+ }
68
+ }
69
+ return decisions;
70
+ }
71
+ catch {
72
+ return [];
73
+ }
74
+ }
75
+ /**
76
+ * Format a relative time string for when the limit resets.
77
+ */
78
+ function formatResetTime(resetsAt) {
79
+ const delta = new Date(resetsAt).getTime() - Date.now();
80
+ if (delta <= 0)
81
+ return 'now';
82
+ const minutes = Math.ceil(delta / 60_000);
83
+ if (minutes < 60)
84
+ return `${minutes}m`;
85
+ const hours = Math.ceil(minutes / 60);
86
+ return `${hours}h`;
87
+ }
88
+ // ─── Exported Functions ─────────────────────────────────────────────────────
89
+ /**
90
+ * Check if a provider is rate-limited.
91
+ * Reads from provider-manager state and checks for common rate limit patterns.
92
+ */
93
+ export function detectLimitReached(provider, cwd) {
94
+ try {
95
+ const normalized = normalizeProvider(provider);
96
+ const state = getProviderState(normalized, cwd);
97
+ const other = OTHER_PROVIDER[normalized] || (normalized === 'anthropic' ? 'openai' : 'anthropic');
98
+ const limited = state.status === 'rate-limited' || state.status === 'down';
99
+ let otherAvailable = false;
100
+ if (limited) {
101
+ const otherState = getProviderState(other, cwd);
102
+ otherAvailable = otherState.status === 'healthy' || otherState.status === 'degraded';
103
+ }
104
+ return {
105
+ limited,
106
+ provider: normalized,
107
+ resetsAt: state.cooldownUntil || undefined,
108
+ otherAvailable,
109
+ otherProvider: other,
110
+ };
111
+ }
112
+ catch {
113
+ return {
114
+ limited: false,
115
+ provider: normalizeProvider(provider),
116
+ otherAvailable: false,
117
+ };
118
+ }
119
+ }
120
+ /**
121
+ * Read the current session state and produce a portable context object.
122
+ */
123
+ export function exportSessionContext(cwd) {
124
+ try {
125
+ const session = loadSession(cwd || process.cwd());
126
+ const decisions = readRecentDecisions(cwd);
127
+ const objective = session?.objective || '(no objective recorded)';
128
+ const filesChanged = session?.filesChanged || [];
129
+ const currentPhase = session?.nextAction || 'unknown';
130
+ // Build a conversation summary from session state
131
+ const summaryParts = [];
132
+ if (session?.objective) {
133
+ summaryParts.push(`Working on: ${session.objective}`);
134
+ }
135
+ if (session?.lastResult?.summary) {
136
+ summaryParts.push(`Last result: ${session.lastResult.summary}`);
137
+ }
138
+ if (session?.commandsRun?.length) {
139
+ const recentCmds = session.commandsRun.slice(-3);
140
+ summaryParts.push(`Recent commands: ${recentCmds.join(', ')}`);
141
+ }
142
+ if (session?.branch) {
143
+ summaryParts.push(`Branch: ${session.branch}`);
144
+ }
145
+ const conversationSummary = summaryParts.length > 0
146
+ ? summaryParts.join('. ')
147
+ : '(no session context available)';
148
+ return {
149
+ objective,
150
+ filesChanged,
151
+ recentDecisions: decisions,
152
+ conversationSummary,
153
+ currentPhase,
154
+ exportedAt: new Date().toISOString(),
155
+ };
156
+ }
157
+ catch {
158
+ return {
159
+ objective: '(failed to export context)',
160
+ filesChanged: [],
161
+ recentDecisions: [],
162
+ conversationSummary: '(export error)',
163
+ currentPhase: 'unknown',
164
+ exportedAt: new Date().toISOString(),
165
+ };
166
+ }
167
+ }
168
+ /**
169
+ * Generate a prompt string that can be fed to the other provider to resume work.
170
+ */
171
+ export function buildHandoffPrompt(context) {
172
+ const lines = [
173
+ '# Handoff Context — Resuming Work From Another Provider',
174
+ '',
175
+ '## Objective',
176
+ context.objective,
177
+ '',
178
+ ];
179
+ if (context.filesChanged.length > 0) {
180
+ lines.push('## Files Changed');
181
+ for (const f of context.filesChanged) {
182
+ lines.push(`- ${f}`);
183
+ }
184
+ lines.push('');
185
+ }
186
+ if (context.conversationSummary && context.conversationSummary !== '(no session context available)') {
187
+ lines.push('## Session Summary');
188
+ lines.push(context.conversationSummary);
189
+ lines.push('');
190
+ }
191
+ if (context.recentDecisions.length > 0) {
192
+ lines.push('## Recent Routing Decisions');
193
+ for (const d of context.recentDecisions) {
194
+ lines.push(`- ${d.model} (${d.tier}) at ${d.timestamp}`);
195
+ }
196
+ lines.push('');
197
+ }
198
+ if (context.currentPhase && context.currentPhase !== 'unknown') {
199
+ lines.push('## Current Phase');
200
+ lines.push(context.currentPhase);
201
+ lines.push('');
202
+ }
203
+ lines.push('## Instructions');
204
+ lines.push('Continue working on the objective above. The previous provider hit its rate limit.');
205
+ lines.push('Pick up where it left off — do not ask the user to repeat themselves.');
206
+ lines.push(`Context exported at: ${context.exportedAt}`);
207
+ return lines.join('\n');
208
+ }
209
+ /**
210
+ * Execute the full handoff flow:
211
+ * 1. Detect which provider to switch TO
212
+ * 2. Export context
213
+ * 3. Build the handoff prompt
214
+ * 4. Return the command to launch the other CLI
215
+ *
216
+ * Does NOT actually spawn the process (caller decides).
217
+ */
218
+ export function executeHandoff(opts) {
219
+ try {
220
+ const { fromProvider, cwd, auto } = opts;
221
+ const normalized = normalizeProvider(fromProvider);
222
+ const other = OTHER_PROVIDER[normalized] || (normalized === 'anthropic' ? 'openai' : 'anthropic');
223
+ // Check that the other provider is available
224
+ const otherState = getProviderState(other, cwd);
225
+ if (otherState.status === 'rate-limited' || otherState.status === 'down') {
226
+ return {
227
+ success: false,
228
+ message: `Cannot handoff: ${other} is also ${otherState.status}. Both providers unavailable.`,
229
+ };
230
+ }
231
+ // Export session context
232
+ const context = exportSessionContext(cwd);
233
+ // Build handoff prompt
234
+ const prompt = buildHandoffPrompt(context);
235
+ // Write context to handoff file
236
+ ensureHandoffDir(cwd);
237
+ const contextFilePath = handoffFile(cwd);
238
+ const handoffData = {
239
+ context,
240
+ prompt,
241
+ fromProvider: normalized,
242
+ toProvider: other,
243
+ auto: auto ?? false,
244
+ createdAt: new Date().toISOString(),
245
+ };
246
+ atomicWriteJson(contextFilePath, handoffData);
247
+ // Build the CLI command
248
+ const cli = PROVIDER_CLI[other] || other;
249
+ const command = [cli, '--resume', contextFilePath];
250
+ const autoLabel = auto ? ' (auto)' : '';
251
+ return {
252
+ success: true,
253
+ command,
254
+ contextFile: contextFilePath,
255
+ message: `Handoff${autoLabel}: ${normalized} → ${other}. Context saved to ${contextFilePath}`,
256
+ };
257
+ }
258
+ catch (err) {
259
+ const message = err instanceof Error ? err.message : String(err);
260
+ return {
261
+ success: false,
262
+ message: `Handoff failed: ${message}`,
263
+ };
264
+ }
265
+ }
266
+ /**
267
+ * Return the user-facing message for a rate limit situation.
268
+ */
269
+ export function getHandoffUX(status) {
270
+ const providerLabel = PROVIDER_CLI[status.provider] || status.provider;
271
+ const otherLabel = status.otherProvider ? (PROVIDER_CLI[status.otherProvider] || status.otherProvider) : 'other';
272
+ // Both providers limited
273
+ if (status.limited && !status.otherAvailable) {
274
+ // Check if we have a reset time
275
+ if (status.resetsAt) {
276
+ const timeStr = formatResetTime(status.resetsAt);
277
+ return {
278
+ text: `⚡ Both providers at limit. Resets in ${timeStr}.`,
279
+ action: 'wait',
280
+ };
281
+ }
282
+ return {
283
+ text: `⚡ Both providers at limit. Resets in ~5m.`,
284
+ action: 'wait',
285
+ };
286
+ }
287
+ // Limited but other is available
288
+ if (status.limited && status.otherAvailable && status.otherProvider) {
289
+ return {
290
+ text: `⚡ ${providerLabel} limit reached → switching to ${otherLabel}`,
291
+ action: 'auto-switch',
292
+ command: `dual-brain handoff --to ${status.otherProvider}`,
293
+ };
294
+ }
295
+ // Limited but other provider not configured
296
+ if (status.limited && !status.otherAvailable) {
297
+ return {
298
+ text: `⚡ ${providerLabel} limit reached. Set up ${otherLabel} to continue? Run: dual-brain init`,
299
+ action: 'prompt-setup',
300
+ command: 'dual-brain init',
301
+ };
302
+ }
303
+ // Not limited (shouldn't normally be called, but handle gracefully)
304
+ return {
305
+ text: `${providerLabel} is operating normally.`,
306
+ action: 'wait',
307
+ };
308
+ }
309
+ //# sourceMappingURL=auto-handoff.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auto-handoff.js","sourceRoot":"","sources":["../../src/auto-handoff.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dual-brain",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "AI orchestration across Claude + OpenAI subscriptions — smart routing, budget awareness, and dual-brain collaboration",
5
5
  "type": "module",
6
6
  "bin": {
@@ -18,7 +18,8 @@
18
18
  "./model-profiles": { "types": "./dist/src/model-profiles.d.ts", "default": "./dist/src/model-profiles.js" },
19
19
  "./signal": { "types": "./dist/src/signal.d.ts", "default": "./dist/src/signal.js" },
20
20
  "./routing-advisor": { "types": "./dist/src/routing-advisor.d.ts", "default": "./dist/src/routing-advisor.js" },
21
- "./types": { "types": "./dist/src/types.d.ts", "default": "./dist/src/types.js" }
21
+ "./types": { "types": "./dist/src/types.d.ts", "default": "./dist/src/types.js" },
22
+ "./auto-handoff": { "types": "./dist/src/auto-handoff.d.ts", "default": "./dist/src/auto-handoff.js" }
22
23
  },
23
24
  "keywords": [
24
25
  "claude-code",