dual-brain 0.3.1 → 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.
- package/bin/dual-brain.mjs +98 -2
- package/dist/src/auto-handoff.d.ts +68 -0
- package/dist/src/auto-handoff.js +309 -0
- package/dist/src/auto-handoff.js.map +1 -0
- package/install.mjs +13 -5
- package/package.json +3 -2
package/bin/dual-brain.mjs
CHANGED
|
@@ -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/install.mjs
CHANGED
|
@@ -916,7 +916,10 @@ function install(workspace, env, mode) {
|
|
|
916
916
|
'auto-update-wrapper.mjs',
|
|
917
917
|
'head-guard.mjs',
|
|
918
918
|
];
|
|
919
|
-
for (const h of HOOKS)
|
|
919
|
+
for (const h of HOOKS) {
|
|
920
|
+
const hSrc = join(__dirname, 'hooks', h);
|
|
921
|
+
if (existsSync(hSrc)) cpSync(hSrc, join(target, 'hooks', h));
|
|
922
|
+
}
|
|
920
923
|
|
|
921
924
|
// Copy bash hooks (auto-update.sh lives alongside .mjs hooks in the package)
|
|
922
925
|
const BASH_HOOKS = ['auto-update.sh'];
|
|
@@ -935,8 +938,12 @@ function install(workspace, env, mode) {
|
|
|
935
938
|
'hookify.orchestrator-gate.local.md',
|
|
936
939
|
'hookify.orchestrator-cost.local.md',
|
|
937
940
|
];
|
|
938
|
-
|
|
939
|
-
|
|
941
|
+
let rulesInstalled = 0;
|
|
942
|
+
for (const r of RULES) {
|
|
943
|
+
const rSrc = join(__dirname, r);
|
|
944
|
+
if (existsSync(rSrc)) { cpSync(rSrc, join(target, r)); rulesInstalled++; }
|
|
945
|
+
}
|
|
946
|
+
if (rulesInstalled) actions.push(`✓ ${rulesInstalled} hookify rules`);
|
|
940
947
|
|
|
941
948
|
const orch = generateOrchestrator(mode, workspace);
|
|
942
949
|
writeFileSync(join(target, 'orchestrator.json'), JSON.stringify(orch, null, 2) + '\n');
|
|
@@ -954,8 +961,9 @@ function install(workspace, env, mode) {
|
|
|
954
961
|
actions.push('✓ CLAUDE.md (session instructions)');
|
|
955
962
|
|
|
956
963
|
const rulesTarget = join(target, 'review-rules.md');
|
|
957
|
-
|
|
958
|
-
|
|
964
|
+
const rulesSrc = join(__dirname, 'review-rules.md');
|
|
965
|
+
if (existsSync(rulesSrc) && (!existsSync(rulesTarget) || force)) {
|
|
966
|
+
cpSync(rulesSrc, rulesTarget);
|
|
959
967
|
actions.push('✓ review-rules.md template');
|
|
960
968
|
} else {
|
|
961
969
|
actions.push('⊘ review-rules.md (kept yours)');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dual-brain",
|
|
3
|
-
"version": "0.3.
|
|
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",
|