dual-brain 0.2.4 → 0.2.6
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 +366 -43
- package/package.json +10 -2
- package/src/awareness.mjs +71 -6
- package/src/checkpoint.mjs +109 -0
- package/src/ci-triage.mjs +191 -0
- package/src/continuity.mjs +291 -0
- package/src/detect.mjs +38 -0
- package/src/dispatch.mjs +73 -7
- package/src/doctor.mjs +6 -6
- package/src/health.mjs +37 -0
- package/src/pipeline.mjs +60 -3
- package/src/pr-agent.mjs +214 -0
- package/src/profile.mjs +39 -124
- package/src/replit.mjs +1 -1
- package/src/repo.mjs +153 -0
package/bin/dual-brain.mjs
CHANGED
|
@@ -242,11 +242,19 @@ Commands:
|
|
|
242
242
|
init First-time setup → flows into interactive REPL
|
|
243
243
|
auth Show provider login and plan status
|
|
244
244
|
install Install Claude Code hooks into the current project
|
|
245
|
+
install --global Write hooks into ~/.claude/settings.json (absolute paths,
|
|
246
|
+
fires from any working directory after shell restart)
|
|
247
|
+
uninstall --global Remove dual-brain hooks from ~/.claude/settings.json
|
|
245
248
|
go "task description" Detect → decide → dispatch (alias for do)
|
|
246
249
|
--dry-run Show routing decision without executing
|
|
247
250
|
--files a.mjs,b.mjs Provide file context for risk classification
|
|
248
251
|
--verbose, -v Print routing trace (intent, risk, health, model selection)
|
|
249
252
|
think "question" Multi-round architecture decision with dual-brain
|
|
253
|
+
pr Show PR status for current branch
|
|
254
|
+
pr create Create PR from current branch with auto-generated description
|
|
255
|
+
--draft Create as a draft PR
|
|
256
|
+
pr list List open PRs (--closed, --all for other states)
|
|
257
|
+
pr view <N> View PR #N details
|
|
250
258
|
status Provider health, session stats, available models
|
|
251
259
|
--verbose, -v Also print profile file path and raw profile object
|
|
252
260
|
hot <provider> Manually mark all model classes for provider as hot
|
|
@@ -373,6 +381,26 @@ async function cmdInit(rl) {
|
|
|
373
381
|
// --- Step 2b: Install hooks ---
|
|
374
382
|
await cmdInstall(cwd);
|
|
375
383
|
|
|
384
|
+
// --- Step 2c: Suggest global install if not already done ---
|
|
385
|
+
try {
|
|
386
|
+
const { homedir } = await import('node:os');
|
|
387
|
+
const globalSettingsPath = join(homedir(), '.claude', 'settings.json');
|
|
388
|
+
const DB_MARKER = '# dual-brain-managed';
|
|
389
|
+
let alreadyGlobal = false;
|
|
390
|
+
if (existsSync(globalSettingsPath)) {
|
|
391
|
+
try {
|
|
392
|
+
const gs = JSON.parse(readFileSync(globalSettingsPath, 'utf8'));
|
|
393
|
+
const allHooks = [...(gs.hooks?.PreToolUse || []), ...(gs.hooks?.PostToolUse || [])];
|
|
394
|
+
alreadyGlobal = allHooks.some(e => e.hooks?.some(h => h.command?.includes(DB_MARKER)));
|
|
395
|
+
} catch {}
|
|
396
|
+
}
|
|
397
|
+
if (!alreadyGlobal) {
|
|
398
|
+
console.log('');
|
|
399
|
+
console.log(' Tip: run "dual-brain install --global" to load these hooks from');
|
|
400
|
+
console.log(' any directory — so dual-brain works when Replit restarts a shell.');
|
|
401
|
+
}
|
|
402
|
+
} catch {}
|
|
403
|
+
|
|
376
404
|
// --- Step 3: Show dashboard ---
|
|
377
405
|
console.log('');
|
|
378
406
|
const repo = loadRepoCache(cwd);
|
|
@@ -918,9 +946,7 @@ async function cmdStatus(args = []) {
|
|
|
918
946
|
const archive = replit.getSessionArchive(cwd);
|
|
919
947
|
const archiveCount = Array.isArray(archive) ? archive.length : (archive?.count ?? 0);
|
|
920
948
|
console.log(` session archive: ${archiveCount} session${archiveCount !== 1 ? 's' : ''}`);
|
|
921
|
-
|
|
922
|
-
const anthropicPresent = replit.hasSecret('ANTHROPIC_API_KEY');
|
|
923
|
-
console.log(` secrets : OPENAI_API_KEY=${openaiPresent ? 'set' : 'unset'} ANTHROPIC_API_KEY=${anthropicPresent ? 'set' : 'unset'}`);
|
|
949
|
+
// Subscription-only: no API key secrets to check
|
|
924
950
|
}
|
|
925
951
|
} catch { /* replit.mjs not available or not in Replit — skip silently */ }
|
|
926
952
|
|
|
@@ -991,6 +1017,139 @@ async function cmdInstall(cwd) {
|
|
|
991
1017
|
}
|
|
992
1018
|
}
|
|
993
1019
|
|
|
1020
|
+
async function installGlobal() {
|
|
1021
|
+
const { homedir } = await import('node:os');
|
|
1022
|
+
const globalClaudeDir = join(homedir(), '.claude');
|
|
1023
|
+
const globalSettingsPath = join(globalClaudeDir, 'settings.json');
|
|
1024
|
+
|
|
1025
|
+
// Resolve absolute path to hooks directory via import.meta.url
|
|
1026
|
+
const pkgRoot = join(__dirname, '..');
|
|
1027
|
+
const hooksDir = join(pkgRoot, '.claude', 'hooks');
|
|
1028
|
+
|
|
1029
|
+
// Warn if running from npx (ephemeral path)
|
|
1030
|
+
if (pkgRoot.includes('.npm/_npx') || pkgRoot.includes('npx-')) {
|
|
1031
|
+
console.log(' Warning: Running from npx — paths will break after this session.');
|
|
1032
|
+
console.log(' Install globally first: npm i -g dual-brain');
|
|
1033
|
+
console.log(' Then run: dual-brain install --global');
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
// Verify hooks exist at resolved path
|
|
1038
|
+
if (!existsSync(join(hooksDir, 'head-guard.mjs'))) {
|
|
1039
|
+
console.log(' Error: Could not resolve hook files at: ' + hooksDir);
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
// Load existing settings (merge, never clobber)
|
|
1044
|
+
let existing = {};
|
|
1045
|
+
if (existsSync(globalSettingsPath)) {
|
|
1046
|
+
try { existing = JSON.parse(readFileSync(globalSettingsPath, 'utf8')); } catch {}
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
// Ensure hooks structure exists
|
|
1050
|
+
if (!existing.hooks) existing.hooks = {};
|
|
1051
|
+
if (!existing.hooks.PreToolUse) existing.hooks.PreToolUse = [];
|
|
1052
|
+
if (!existing.hooks.PostToolUse) existing.hooks.PostToolUse = [];
|
|
1053
|
+
|
|
1054
|
+
// Define dual-brain hooks with ownership marker
|
|
1055
|
+
const DB_MARKER = '# dual-brain-managed';
|
|
1056
|
+
const preToolHooks = [
|
|
1057
|
+
{ matcher: 'Edit', hooks: [{ type: 'command', command: `node ${join(hooksDir, 'head-guard.mjs')} ${DB_MARKER}` }] },
|
|
1058
|
+
{ matcher: 'Write', hooks: [{ type: 'command', command: `node ${join(hooksDir, 'head-guard.mjs')} ${DB_MARKER}` }] },
|
|
1059
|
+
{ matcher: 'NotebookEdit',hooks: [{ type: 'command', command: `node ${join(hooksDir, 'head-guard.mjs')} ${DB_MARKER}` }] },
|
|
1060
|
+
{ matcher: 'Bash', hooks: [{ type: 'command', command: `node ${join(hooksDir, 'head-guard.mjs')} ${DB_MARKER}` }] },
|
|
1061
|
+
{ matcher: 'Agent', hooks: [{ type: 'command', command: `node ${join(hooksDir, 'enforce-tier.mjs')} ${DB_MARKER}` }] },
|
|
1062
|
+
];
|
|
1063
|
+
const postToolHooks = [
|
|
1064
|
+
{ matcher: '', hooks: [{ type: 'command', command: `node ${join(hooksDir, 'cost-logger.mjs')} ${DB_MARKER}` }] },
|
|
1065
|
+
{ matcher: '', hooks: [{ type: 'command', command: `node ${join(hooksDir, 'auto-update-wrapper.mjs')} ${DB_MARKER}` }] },
|
|
1066
|
+
];
|
|
1067
|
+
|
|
1068
|
+
// Remove any existing dual-brain hooks (idempotent)
|
|
1069
|
+
const isDBHook = (entry) => entry.hooks?.some(h => h.command?.includes(DB_MARKER));
|
|
1070
|
+
existing.hooks.PreToolUse = existing.hooks.PreToolUse.filter(e => !isDBHook(e));
|
|
1071
|
+
existing.hooks.PostToolUse = existing.hooks.PostToolUse.filter(e => !isDBHook(e));
|
|
1072
|
+
|
|
1073
|
+
// Add dual-brain hooks
|
|
1074
|
+
existing.hooks.PreToolUse.push(...preToolHooks);
|
|
1075
|
+
existing.hooks.PostToolUse.push(...postToolHooks);
|
|
1076
|
+
|
|
1077
|
+
// Write merged settings
|
|
1078
|
+
mkdirSync(globalClaudeDir, { recursive: true });
|
|
1079
|
+
writeFileSync(globalSettingsPath, JSON.stringify(existing, null, 2) + '\n');
|
|
1080
|
+
|
|
1081
|
+
// Write minimal global CLAUDE.md (only if none exists, or append section)
|
|
1082
|
+
const globalClaudeMd = join(globalClaudeDir, 'CLAUDE.md');
|
|
1083
|
+
const dbSection = `\n## Dual-Brain Global Hooks\n\nThis machine has dual-brain hooks installed globally.\nProject-local .claude/CLAUDE.md and settings take precedence.\nManaged by: dual-brain install --global\n`;
|
|
1084
|
+
|
|
1085
|
+
if (!existsSync(globalClaudeMd)) {
|
|
1086
|
+
writeFileSync(globalClaudeMd, dbSection);
|
|
1087
|
+
} else {
|
|
1088
|
+
const content = readFileSync(globalClaudeMd, 'utf8');
|
|
1089
|
+
if (!content.includes('Dual-Brain Global Hooks')) {
|
|
1090
|
+
writeFileSync(globalClaudeMd, content + '\n' + dbSection);
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
console.log(' + dual-brain hooks installed globally');
|
|
1095
|
+
console.log(' hooks dir: ' + hooksDir);
|
|
1096
|
+
console.log(' settings: ' + globalSettingsPath);
|
|
1097
|
+
console.log('');
|
|
1098
|
+
console.log(' All new Claude sessions will load dual-brain hooks.');
|
|
1099
|
+
console.log(' Run "dual-brain uninstall --global" to remove.');
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
async function uninstallGlobal() {
|
|
1103
|
+
const { homedir } = await import('node:os');
|
|
1104
|
+
const globalSettingsPath = join(homedir(), '.claude', 'settings.json');
|
|
1105
|
+
|
|
1106
|
+
if (!existsSync(globalSettingsPath)) {
|
|
1107
|
+
console.log(' No global settings found.');
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
let settings = {};
|
|
1112
|
+
try { settings = JSON.parse(readFileSync(globalSettingsPath, 'utf8')); } catch { return; }
|
|
1113
|
+
|
|
1114
|
+
const DB_MARKER = '# dual-brain-managed';
|
|
1115
|
+
const isDBHook = (entry) => entry.hooks?.some(h => h.command?.includes(DB_MARKER));
|
|
1116
|
+
|
|
1117
|
+
let removed = 0;
|
|
1118
|
+
if (settings.hooks?.PreToolUse) {
|
|
1119
|
+
const before = settings.hooks.PreToolUse.length;
|
|
1120
|
+
settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter(e => !isDBHook(e));
|
|
1121
|
+
removed += before - settings.hooks.PreToolUse.length;
|
|
1122
|
+
}
|
|
1123
|
+
if (settings.hooks?.PostToolUse) {
|
|
1124
|
+
const before = settings.hooks.PostToolUse.length;
|
|
1125
|
+
settings.hooks.PostToolUse = settings.hooks.PostToolUse.filter(e => !isDBHook(e));
|
|
1126
|
+
removed += before - settings.hooks.PostToolUse.length;
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// Clean up empty arrays/objects
|
|
1130
|
+
if (settings.hooks?.PreToolUse?.length === 0) delete settings.hooks.PreToolUse;
|
|
1131
|
+
if (settings.hooks?.PostToolUse?.length === 0) delete settings.hooks.PostToolUse;
|
|
1132
|
+
if (Object.keys(settings.hooks || {}).length === 0) delete settings.hooks;
|
|
1133
|
+
|
|
1134
|
+
writeFileSync(globalSettingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
1135
|
+
|
|
1136
|
+
// Remove dual-brain section from global CLAUDE.md
|
|
1137
|
+
const globalClaudeMd = join(homedir(), '.claude', 'CLAUDE.md');
|
|
1138
|
+
if (existsSync(globalClaudeMd)) {
|
|
1139
|
+
let content = readFileSync(globalClaudeMd, 'utf8');
|
|
1140
|
+
const dbSectionRegex = /\n## Dual-Brain Global Hooks\n[\s\S]*?Managed by: dual-brain install --global\n/;
|
|
1141
|
+
content = content.replace(dbSectionRegex, '');
|
|
1142
|
+
if (content.trim()) {
|
|
1143
|
+
writeFileSync(globalClaudeMd, content);
|
|
1144
|
+
} else {
|
|
1145
|
+
unlinkSync(globalClaudeMd);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
console.log(` - removed ${removed} dual-brain hook${removed === 1 ? '' : 's'} from global settings`);
|
|
1150
|
+
console.log(' Other settings preserved.');
|
|
1151
|
+
}
|
|
1152
|
+
|
|
994
1153
|
function cmdRemember(text) {
|
|
995
1154
|
if (!text) err('Usage: dual-brain remember "preference text"');
|
|
996
1155
|
const profile = rememberPreference(text, { scope: 'project', cwd: process.cwd() });
|
|
@@ -1050,6 +1209,182 @@ function cmdBreakGlass(reason) {
|
|
|
1050
1209
|
console.log('└' + '─'.repeat(inner) + '┘');
|
|
1051
1210
|
}
|
|
1052
1211
|
|
|
1212
|
+
// ─── PR command ───────────────────────────────────────────────────────────────
|
|
1213
|
+
|
|
1214
|
+
async function cmdPR(args) {
|
|
1215
|
+
const cwd = process.cwd();
|
|
1216
|
+
const sub = args[0] ?? '';
|
|
1217
|
+
|
|
1218
|
+
// Lazy import — only loaded when 'pr' is invoked
|
|
1219
|
+
let prAgent;
|
|
1220
|
+
try {
|
|
1221
|
+
prAgent = await import('../src/pr-agent.mjs');
|
|
1222
|
+
} catch (e) {
|
|
1223
|
+
console.error('pr-agent module not available:', e.message);
|
|
1224
|
+
process.exit(1);
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
const gh = prAgent.hasGitHub();
|
|
1228
|
+
if (!gh.available) {
|
|
1229
|
+
console.error('gh CLI not found. Install GitHub CLI: https://cli.github.com');
|
|
1230
|
+
process.exit(1);
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
// ── dual-brain pr (show current branch PR status) ──────────────────────────
|
|
1234
|
+
if (!sub || sub === 'status') {
|
|
1235
|
+
if (!gh.authenticated) {
|
|
1236
|
+
console.log('gh CLI is available but not authenticated. Run: gh auth login');
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1239
|
+
const info = prAgent.getBranchInfo(cwd);
|
|
1240
|
+
console.log('\n── PR status ─────────────────────────────────────────────\n');
|
|
1241
|
+
console.log(` Branch: ${info.branch ?? '(unknown)'}`);
|
|
1242
|
+
console.log(` Base: ${info.defaultBranch}`);
|
|
1243
|
+
console.log(` Ahead: ${info.ahead} commit(s)`);
|
|
1244
|
+
console.log(` Behind: ${info.behind} commit(s)`);
|
|
1245
|
+
|
|
1246
|
+
if (info.isDefault) {
|
|
1247
|
+
console.log('\n On default branch — create a feature branch first.');
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
// Check for an existing PR on this branch
|
|
1252
|
+
try {
|
|
1253
|
+
const { execSync: _exec } = await import('node:child_process');
|
|
1254
|
+
const json = _exec(`gh pr list --head "${info.branch}" --json number,title,state,url`, {
|
|
1255
|
+
cwd, encoding: 'utf8', timeout: 10000,
|
|
1256
|
+
});
|
|
1257
|
+
const prs = JSON.parse(json);
|
|
1258
|
+
if (prs.length > 0) {
|
|
1259
|
+
const pr = prs[0];
|
|
1260
|
+
console.log(`\n PR #${pr.number}: ${pr.title}`);
|
|
1261
|
+
console.log(` State: ${pr.state}`);
|
|
1262
|
+
console.log(` URL: ${pr.url}`);
|
|
1263
|
+
} else {
|
|
1264
|
+
console.log('\n No PR open for this branch.');
|
|
1265
|
+
console.log(` Create one: dual-brain pr create`);
|
|
1266
|
+
}
|
|
1267
|
+
} catch {
|
|
1268
|
+
console.log('\n (Could not check for existing PR — run: gh pr status)');
|
|
1269
|
+
}
|
|
1270
|
+
console.log('');
|
|
1271
|
+
return;
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
// ── dual-brain pr list ───────────────────────────────────────────────────────
|
|
1275
|
+
if (sub === 'list') {
|
|
1276
|
+
if (!gh.authenticated) {
|
|
1277
|
+
console.log('gh CLI not authenticated. Run: gh auth login');
|
|
1278
|
+
return;
|
|
1279
|
+
}
|
|
1280
|
+
const state = args.includes('--closed') ? 'closed' : args.includes('--all') ? 'all' : 'open';
|
|
1281
|
+
const prs = prAgent.listPRs(cwd, { state, limit: 20 });
|
|
1282
|
+
if (prs.length === 0) {
|
|
1283
|
+
console.log(`No ${state} PRs found.`);
|
|
1284
|
+
return;
|
|
1285
|
+
}
|
|
1286
|
+
console.log(`\n── ${state} PRs ──────────────────────────────────────────────\n`);
|
|
1287
|
+
for (const pr of prs) {
|
|
1288
|
+
const draft = pr.isDraft ? ' [draft]' : '';
|
|
1289
|
+
const date = pr.createdAt ? new Date(pr.createdAt).toLocaleDateString() : '';
|
|
1290
|
+
console.log(` #${pr.number} ${pr.title}${draft}`);
|
|
1291
|
+
console.log(` ${pr.headRefName} by ${pr.author?.login ?? '?'} ${date}`);
|
|
1292
|
+
}
|
|
1293
|
+
console.log('');
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
// ── dual-brain pr view <N> ───────────────────────────────────────────────────
|
|
1298
|
+
if (sub === 'view') {
|
|
1299
|
+
const prNum = args[1];
|
|
1300
|
+
if (!prNum || isNaN(Number(prNum))) {
|
|
1301
|
+
console.error('Usage: dual-brain pr view <PR-number>');
|
|
1302
|
+
process.exit(1);
|
|
1303
|
+
}
|
|
1304
|
+
const details = prAgent.getPRDetails(prNum, cwd);
|
|
1305
|
+
if (!details) {
|
|
1306
|
+
console.error(`PR #${prNum} not found or gh CLI error.`);
|
|
1307
|
+
process.exit(1);
|
|
1308
|
+
}
|
|
1309
|
+
console.log(`\n── PR #${prNum}: ${details.title} ─────────────────────────────\n`);
|
|
1310
|
+
console.log(` State: ${details.state}`);
|
|
1311
|
+
console.log(` Branch: ${details.headRefName} → ${details.baseRefName}`);
|
|
1312
|
+
console.log(` Changes: +${details.additions} -${details.deletions} (${details.changedFiles} files)`);
|
|
1313
|
+
if (details.statusCheckRollup?.length) {
|
|
1314
|
+
const passing = details.statusCheckRollup.filter(c => c.conclusion === 'SUCCESS').length;
|
|
1315
|
+
const total = details.statusCheckRollup.length;
|
|
1316
|
+
console.log(` Checks: ${passing}/${total} passing`);
|
|
1317
|
+
}
|
|
1318
|
+
if (details.body) {
|
|
1319
|
+
console.log('\n Body:\n');
|
|
1320
|
+
console.log(details.body.split('\n').map(l => ` ${l}`).join('\n').slice(0, 1500));
|
|
1321
|
+
}
|
|
1322
|
+
console.log('');
|
|
1323
|
+
return;
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
// ── dual-brain pr create ─────────────────────────────────────────────────────
|
|
1327
|
+
if (sub === 'create') {
|
|
1328
|
+
if (!gh.authenticated) {
|
|
1329
|
+
console.log('gh CLI not authenticated. Run: gh auth login');
|
|
1330
|
+
return;
|
|
1331
|
+
}
|
|
1332
|
+
const info = prAgent.getBranchInfo(cwd);
|
|
1333
|
+
if (info.isDefault || !info.branch) {
|
|
1334
|
+
console.error('You are on the default branch. Switch to a feature branch before creating a PR.');
|
|
1335
|
+
process.exit(1);
|
|
1336
|
+
}
|
|
1337
|
+
if (info.ahead === 0) {
|
|
1338
|
+
console.error('No commits ahead of the base branch. Make changes and commit first.');
|
|
1339
|
+
process.exit(1);
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
const diff = prAgent.getDiffSummary(info.defaultBranch, cwd);
|
|
1343
|
+
const draft = args.includes('--draft');
|
|
1344
|
+
|
|
1345
|
+
// Auto-generate a title from the branch name
|
|
1346
|
+
const rawTitle = info.branch.replace(/^db\//, '').replace(/-/g, ' ');
|
|
1347
|
+
const title = rawTitle.charAt(0).toUpperCase() + rawTitle.slice(1);
|
|
1348
|
+
|
|
1349
|
+
// Auto-generate body from diff
|
|
1350
|
+
const body = prAgent.buildPRBody(title, {
|
|
1351
|
+
filesChanged: diff.files,
|
|
1352
|
+
});
|
|
1353
|
+
|
|
1354
|
+
console.log(`\n── Creating PR from ${info.branch} → ${info.defaultBranch} ────────────\n`);
|
|
1355
|
+
console.log(` Title: ${title}`);
|
|
1356
|
+
console.log(` Files: ${diff.fileCount} changed`);
|
|
1357
|
+
if (diff.summary) console.log(` Diff: ${diff.summary}`);
|
|
1358
|
+
if (draft) console.log(' Mode: draft');
|
|
1359
|
+
console.log('');
|
|
1360
|
+
|
|
1361
|
+
const result = prAgent.createPR({
|
|
1362
|
+
title,
|
|
1363
|
+
body,
|
|
1364
|
+
baseBranch: info.defaultBranch,
|
|
1365
|
+
draft,
|
|
1366
|
+
cwd,
|
|
1367
|
+
});
|
|
1368
|
+
|
|
1369
|
+
if (result.success) {
|
|
1370
|
+
console.log(` PR created: ${result.url}`);
|
|
1371
|
+
} else {
|
|
1372
|
+
console.error(` Failed to create PR: ${result.error}`);
|
|
1373
|
+
process.exit(1);
|
|
1374
|
+
}
|
|
1375
|
+
console.log('');
|
|
1376
|
+
return;
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
// Unknown sub-subcommand
|
|
1380
|
+
console.log(`Unknown pr subcommand: "${sub}"`);
|
|
1381
|
+
console.log('Usage:');
|
|
1382
|
+
console.log(' dual-brain pr Show PR status for current branch');
|
|
1383
|
+
console.log(' dual-brain pr create Create PR from current branch');
|
|
1384
|
+
console.log(' dual-brain pr list List open PRs');
|
|
1385
|
+
console.log(' dual-brain pr view <N> View PR details');
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1053
1388
|
// ─── Screen helpers ───────────────────────────────────────────────────────────
|
|
1054
1389
|
|
|
1055
1390
|
/**
|
|
@@ -1644,13 +1979,9 @@ function buildProviderStatusLine(profile, auth, envReport = null) {
|
|
|
1644
1979
|
const GREEN = '\x1b[32m●\x1b[0m';
|
|
1645
1980
|
const RED = '\x1b[31m●\x1b[0m';
|
|
1646
1981
|
|
|
1647
|
-
//
|
|
1648
|
-
const claudeAvailable =
|
|
1649
|
-
|
|
1650
|
-
: auth.claude.found;
|
|
1651
|
-
const openaiAvailable = envReport
|
|
1652
|
-
? envReport.secrets.OPENAI_API_KEY || auth.openai.found
|
|
1653
|
-
: auth.openai.found;
|
|
1982
|
+
// Subscription-only detection — no API key secrets
|
|
1983
|
+
const claudeAvailable = auth.claude.found;
|
|
1984
|
+
const openaiAvailable = auth.openai.found;
|
|
1654
1985
|
|
|
1655
1986
|
const claudeDot = claudeAvailable ? GREEN : RED;
|
|
1656
1987
|
const openaiDot = openaiAvailable ? GREEN : RED;
|
|
@@ -1971,13 +2302,9 @@ async function mainScreen(rl, ask) {
|
|
|
1971
2302
|
envReport = scanEnvironment(cwd);
|
|
1972
2303
|
} catch { /* non-fatal */ }
|
|
1973
2304
|
|
|
1974
|
-
// ── Studio Console: resolve provider availability
|
|
1975
|
-
const claudeAvail =
|
|
1976
|
-
|
|
1977
|
-
: auth.claude.found;
|
|
1978
|
-
const openaiAvail = envReport
|
|
1979
|
-
? envReport.secrets?.OPENAI_API_KEY || auth.openai.found
|
|
1980
|
-
: auth.openai.found;
|
|
2305
|
+
// ── Studio Console: resolve provider availability (subscription-only) ───
|
|
2306
|
+
const claudeAvail = auth.claude.found;
|
|
2307
|
+
const openaiAvail = auth.openai.found;
|
|
1981
2308
|
|
|
1982
2309
|
// ── Box 2 — Workspace: gather git data ───────────────────────────────────
|
|
1983
2310
|
let gitBranch = 'unknown';
|
|
@@ -3771,12 +4098,10 @@ async function runOnboardingWizard(_detection, cwd, rl) {
|
|
|
3771
4098
|
let claudeAuthLabel = null;
|
|
3772
4099
|
let claudeAuthType = null;
|
|
3773
4100
|
if (claudeReady) {
|
|
3774
|
-
if (caps.claude.source === 'claude-code') {
|
|
4101
|
+
if (caps.claude.source === 'claude-code' || caps.claude.source === 'claude-dir') {
|
|
3775
4102
|
claudeAuthLabel = 'CLI OAuth'; claudeAuthType = 'cli_oauth';
|
|
3776
|
-
} else if (caps.claude.source === 'env-key') {
|
|
3777
|
-
claudeAuthLabel = 'API key'; claudeAuthType = 'api_key';
|
|
3778
4103
|
} else {
|
|
3779
|
-
claudeAuthLabel = caps.claude.source || 'detected'; claudeAuthType = '
|
|
4104
|
+
claudeAuthLabel = caps.claude.source || 'detected'; claudeAuthType = 'cli_oauth';
|
|
3780
4105
|
}
|
|
3781
4106
|
fx.success(`Claude CLI found · ${claudeAuthLabel}`);
|
|
3782
4107
|
}
|
|
@@ -3784,10 +4109,7 @@ async function runOnboardingWizard(_detection, cwd, rl) {
|
|
|
3784
4109
|
// OpenAI / Codex
|
|
3785
4110
|
let openaiAuthLabel = null;
|
|
3786
4111
|
let openaiAuthType = null;
|
|
3787
|
-
if (openaiReady) {
|
|
3788
|
-
openaiAuthLabel = 'API key'; openaiAuthType = 'api_key';
|
|
3789
|
-
fx.success('OpenAI detected · API key');
|
|
3790
|
-
} else if (codexAvailable) {
|
|
4112
|
+
if (openaiReady || codexAvailable) {
|
|
3791
4113
|
openaiAuthLabel = 'CLI OAuth'; openaiAuthType = 'cli_oauth';
|
|
3792
4114
|
fx.success('OpenAI Codex CLI found · authenticated');
|
|
3793
4115
|
}
|
|
@@ -3830,7 +4152,6 @@ async function runOnboardingWizard(_detection, cwd, rl) {
|
|
|
3830
4152
|
} else if (noProvChoice === 'o') {
|
|
3831
4153
|
process.stdout.write('\n');
|
|
3832
4154
|
dimLine('Run: codex login');
|
|
3833
|
-
dimLine('Or add OPENAI_API_KEY to Replit Secrets if using API key auth.');
|
|
3834
4155
|
dimLine('Then re-run: dual-brain init');
|
|
3835
4156
|
process.stdout.write('\n');
|
|
3836
4157
|
}
|
|
@@ -3850,9 +4171,7 @@ async function runOnboardingWizard(_detection, cwd, rl) {
|
|
|
3850
4171
|
if (claudeReady) {
|
|
3851
4172
|
process.stdout.write(` ${GRAY}Claude${RST} ${claudeAuthLabel} ${GREEN}✓ authenticated${RST}\n`);
|
|
3852
4173
|
}
|
|
3853
|
-
if (openaiReady) {
|
|
3854
|
-
process.stdout.write(` ${GRAY}OpenAI${RST} API key ${GREEN}✓ OPENAI_API_KEY${RST}\n`);
|
|
3855
|
-
} else if (codexAvailable) {
|
|
4174
|
+
if (openaiReady || codexAvailable) {
|
|
3856
4175
|
process.stdout.write(` ${GRAY}OpenAI${RST} CLI OAuth ${GREEN}✓ authenticated${RST}\n`);
|
|
3857
4176
|
}
|
|
3858
4177
|
|
|
@@ -3877,8 +4196,8 @@ async function runOnboardingWizard(_detection, cwd, rl) {
|
|
|
3877
4196
|
process.stdout.write('\n');
|
|
3878
4197
|
} else if (provChoice === 'a') {
|
|
3879
4198
|
process.stdout.write('\n');
|
|
3880
|
-
if (!claudeReady) dimLine('Claude: run `claude
|
|
3881
|
-
if (!openaiReady && !codexAvailable) dimLine('OpenAI:
|
|
4199
|
+
if (!claudeReady) dimLine('Claude: run `claude login` to authenticate');
|
|
4200
|
+
if (!openaiReady && !codexAvailable) dimLine('OpenAI: run `codex login` to authenticate');
|
|
3882
4201
|
process.stdout.write('\n');
|
|
3883
4202
|
process.stdout.write(` ${GRAY}[Enter]${RST} continue with current providers\n\n`);
|
|
3884
4203
|
await singleKey(['\r', 'q']);
|
|
@@ -3901,10 +4220,10 @@ async function runOnboardingWizard(_detection, cwd, rl) {
|
|
|
3901
4220
|
}
|
|
3902
4221
|
if (finalOpenaiEnabled) {
|
|
3903
4222
|
credEntries.push({
|
|
3904
|
-
id:
|
|
4223
|
+
id: 'openai-codex',
|
|
3905
4224
|
provider: 'openai',
|
|
3906
|
-
auth_type:
|
|
3907
|
-
source:
|
|
4225
|
+
auth_type: 'cli_oauth',
|
|
4226
|
+
source: 'cli_oauth',
|
|
3908
4227
|
owner: 'user',
|
|
3909
4228
|
scope: 'local',
|
|
3910
4229
|
plan_hint: null,
|
|
@@ -3926,12 +4245,6 @@ async function runOnboardingWizard(_detection, cwd, rl) {
|
|
|
3926
4245
|
const styleMap = { '1': 'auto', '2': 'quality-first', '3': 'cost-saver', '\r': 'auto' };
|
|
3927
4246
|
const chosenBias = styleMap[styleKey] || 'auto';
|
|
3928
4247
|
|
|
3929
|
-
// Metered API note (non-blocking)
|
|
3930
|
-
if (openaiReady && caps.openai.metered) {
|
|
3931
|
-
process.stdout.write('\n');
|
|
3932
|
-
dimLine('OpenAI API key detected — usage is metered, guardrails enabled');
|
|
3933
|
-
}
|
|
3934
|
-
|
|
3935
4248
|
process.stdout.write('\n');
|
|
3936
4249
|
|
|
3937
4250
|
// Init living docs (non-fatal)
|
|
@@ -3957,7 +4270,7 @@ async function runOnboardingWizard(_detection, cwd, rl) {
|
|
|
3957
4270
|
|
|
3958
4271
|
finalProfile.providers.claude = { enabled: finalClaudeEnabled };
|
|
3959
4272
|
finalProfile.providers.openai = { enabled: finalOpenaiEnabled };
|
|
3960
|
-
finalProfile.apiGuardrail =
|
|
4273
|
+
finalProfile.apiGuardrail = false;
|
|
3961
4274
|
finalProfile.setupComplete = true;
|
|
3962
4275
|
|
|
3963
4276
|
const enabledCount = [finalClaudeEnabled, finalOpenaiEnabled].filter(Boolean).length;
|
|
@@ -5640,7 +5953,16 @@ async function main() {
|
|
|
5640
5953
|
}
|
|
5641
5954
|
|
|
5642
5955
|
// One-shot commands — run and exit
|
|
5643
|
-
if (cmd === 'install')
|
|
5956
|
+
if (cmd === 'install') {
|
|
5957
|
+
if (args.includes('--global')) { await installGlobal(); return; }
|
|
5958
|
+
await cmdInstall();
|
|
5959
|
+
return;
|
|
5960
|
+
}
|
|
5961
|
+
if (cmd === 'uninstall') {
|
|
5962
|
+
if (args.includes('--global')) { await uninstallGlobal(); return; }
|
|
5963
|
+
console.log('Usage: dual-brain uninstall --global');
|
|
5964
|
+
return;
|
|
5965
|
+
}
|
|
5644
5966
|
if (cmd === 'auth') {
|
|
5645
5967
|
await cmdAuth(args.slice(1));
|
|
5646
5968
|
return;
|
|
@@ -5651,6 +5973,7 @@ async function main() {
|
|
|
5651
5973
|
if (cmd === 'think') { await cmdThink(args.slice(1)); return; }
|
|
5652
5974
|
if (cmd === 'review') { await cmdReview(args.slice(1)); return; }
|
|
5653
5975
|
if (cmd === 'ship') { await cmdShip(); return; }
|
|
5976
|
+
if (cmd === 'pr') { await cmdPR(args.slice(1)); return; }
|
|
5654
5977
|
if (cmd === 'status') { await cmdStatus(args.slice(1)); return; }
|
|
5655
5978
|
if (cmd === 'hot') { cmdHot(args[1]); return; }
|
|
5656
5979
|
if (cmd === 'cool') { cmdCool(args[1]); return; }
|
|
@@ -5715,7 +6038,7 @@ fi
|
|
|
5715
6038
|
// If cmd is not a recognized subcommand, treat the entire arg list as a task.
|
|
5716
6039
|
// e.g. `dual-brain fix failing tests` → same as `dual-brain go "fix failing tests"`
|
|
5717
6040
|
const KNOWN_COMMANDS = new Set([
|
|
5718
|
-
'init', 'install', 'auth', 'go', 'do', 'plan', 'ship', 'think', 'review', 'status', 'hot', 'cool',
|
|
6041
|
+
'init', 'install', 'uninstall', 'auth', 'go', 'do', 'plan', 'ship', 'think', 'review', 'pr', 'status', 'hot', 'cool',
|
|
5719
6042
|
'remember', 'forget', 'break-glass', 'specialists', 'search', 'shell-hook', 'watch',
|
|
5720
6043
|
'--help', '-h', '--version', '-v',
|
|
5721
6044
|
...Object.keys(loadSpecialistRegistry()),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dual-brain",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
4
4
|
"description": "AI orchestration across Claude + OpenAI subscriptions — smart routing, budget awareness, and dual-brain collaboration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -23,7 +23,11 @@
|
|
|
23
23
|
"./calibration": "./src/calibration.mjs",
|
|
24
24
|
"./models": "./src/models.mjs",
|
|
25
25
|
"./prompt-intel": "./src/prompt-intel.mjs",
|
|
26
|
-
"./replit": "./src/replit.mjs"
|
|
26
|
+
"./replit": "./src/replit.mjs",
|
|
27
|
+
"./continuity": "./src/continuity.mjs",
|
|
28
|
+
"./checkpoint": "./src/checkpoint.mjs",
|
|
29
|
+
"./pr-agent": "./src/pr-agent.mjs",
|
|
30
|
+
"./ci-triage": "./src/ci-triage.mjs"
|
|
27
31
|
},
|
|
28
32
|
"keywords": [
|
|
29
33
|
"claude-code",
|
|
@@ -86,6 +90,10 @@
|
|
|
86
90
|
"src/prompt-intel.mjs",
|
|
87
91
|
"src/replit.mjs",
|
|
88
92
|
"src/fx.mjs",
|
|
93
|
+
"src/continuity.mjs",
|
|
94
|
+
"src/checkpoint.mjs",
|
|
95
|
+
"src/ci-triage.mjs",
|
|
96
|
+
"src/pr-agent.mjs",
|
|
89
97
|
"bin/*.mjs",
|
|
90
98
|
"hooks/enforce-tier.mjs",
|
|
91
99
|
"hooks/cost-logger.mjs",
|
package/src/awareness.mjs
CHANGED
|
@@ -49,8 +49,6 @@ function detectContainerType() {
|
|
|
49
49
|
|
|
50
50
|
function scanSecrets() {
|
|
51
51
|
const keys = [
|
|
52
|
-
'OPENAI_API_KEY',
|
|
53
|
-
'ANTHROPIC_API_KEY',
|
|
54
52
|
'NPM_TOKEN',
|
|
55
53
|
'DATABASE_URL',
|
|
56
54
|
'GITHUB_TOKEN',
|
|
@@ -282,8 +280,6 @@ export function formatEnvironment(report) {
|
|
|
282
280
|
if (toolEntries.length) lines.push(`Tools: ${toolEntries.join(' ')}`);
|
|
283
281
|
|
|
284
282
|
const secretMap = {
|
|
285
|
-
OPENAI_API_KEY: 'OpenAI',
|
|
286
|
-
ANTHROPIC_API_KEY: 'Anthropic',
|
|
287
283
|
NPM_TOKEN: 'npm',
|
|
288
284
|
GITHUB_TOKEN: 'GitHub',
|
|
289
285
|
DATABASE_URL: 'PostgreSQL',
|
|
@@ -335,8 +331,6 @@ export function getCapabilitySummary(report) {
|
|
|
335
331
|
if (report.tools.gh?.available) caps.push('github-cli');
|
|
336
332
|
if (report.tools.rg?.available) caps.push('ripgrep');
|
|
337
333
|
|
|
338
|
-
if (report.secrets.OPENAI_API_KEY) caps.push('openai-key');
|
|
339
|
-
if (report.secrets.ANTHROPIC_API_KEY) caps.push('anthropic-key');
|
|
340
334
|
|
|
341
335
|
if (report.replitTools.installed) {
|
|
342
336
|
for (const c of report.replitTools.capabilities) {
|
|
@@ -358,3 +352,74 @@ export function invalidateCache() {
|
|
|
358
352
|
_cache = null;
|
|
359
353
|
_cacheTime = 0;
|
|
360
354
|
}
|
|
355
|
+
|
|
356
|
+
// ─── Ambiguity Detection ──────────────────────────────────────────────────────
|
|
357
|
+
|
|
358
|
+
const TECHNICAL_TERMS = /\b(fix|bug|error|test|deploy|refactor|import|export|function|class|module|api|endpoint|auth|token|database|query|schema|migration|build|lint|type|interface|component|route|handler|middleware|config|env|secret|key|file|path|directory|repo|branch|commit|merge|pull|push|install|upgrade|package|dependency|version|release|publish|log|trace|debug|stack|exception|undefined|null|async|await|promise|fetch|request|response|status|server|client|socket|cache|session)\b/i;
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Detect whether a prompt is ambiguous and needs clarification before dispatch.
|
|
362
|
+
*
|
|
363
|
+
* A prompt is considered ambiguous when ALL of the following are true:
|
|
364
|
+
* 1. It is very short (under 4 words)
|
|
365
|
+
* 2. No file context is provided
|
|
366
|
+
* 3. It lacks specific technical terms that narrow the intent
|
|
367
|
+
*
|
|
368
|
+
* @param {string} prompt — the user's raw prompt
|
|
369
|
+
* @param {{ files?: string[] }} [context] — optional context (e.g. file paths)
|
|
370
|
+
* @returns {{ isAmbiguous: boolean, reason: string|null }}
|
|
371
|
+
*/
|
|
372
|
+
export function detectAmbiguity(prompt, context = {}) {
|
|
373
|
+
if (!prompt || typeof prompt !== 'string') {
|
|
374
|
+
return { isAmbiguous: true, reason: 'missing context: empty prompt' };
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const words = prompt.trim().split(/\s+/).filter(Boolean);
|
|
378
|
+
const isTooShort = words.length < 4;
|
|
379
|
+
const hasFileContext = Array.isArray(context?.files) && context.files.length > 0;
|
|
380
|
+
const hasTechnicalTerms = TECHNICAL_TERMS.test(prompt);
|
|
381
|
+
|
|
382
|
+
if (isTooShort && !hasFileContext && !hasTechnicalTerms) {
|
|
383
|
+
return {
|
|
384
|
+
isAmbiguous: true,
|
|
385
|
+
reason: `unclear: prompt is vague ("${prompt.trim()}") — missing context about what to change and where`,
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return { isAmbiguous: false, reason: null };
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Detect whether a user prompt is too vague to act on confidently.
|
|
394
|
+
*
|
|
395
|
+
* Checks for:
|
|
396
|
+
* - Very short prompts (under 10 chars)
|
|
397
|
+
* - No file paths, function names, or specific identifiers
|
|
398
|
+
* - Pronoun-only references without antecedents ("fix that thing", "change it")
|
|
399
|
+
*
|
|
400
|
+
* @param {string} prompt — the user's raw prompt
|
|
401
|
+
* @returns {{ ambiguous: boolean, reason: string|null, confidence: number }}
|
|
402
|
+
*/
|
|
403
|
+
export function isAmbiguous(prompt) {
|
|
404
|
+
if (!prompt || typeof prompt !== 'string') return { ambiguous: true, reason: 'empty-prompt', confidence: 1.0 };
|
|
405
|
+
|
|
406
|
+
const trimmed = prompt.trim();
|
|
407
|
+
if (trimmed.length < 10) return { ambiguous: true, reason: 'too-short', confidence: 0.9 };
|
|
408
|
+
|
|
409
|
+
// Check for file paths, function names, specific identifiers
|
|
410
|
+
const hasSpecifics = /[a-zA-Z_]\w*\.(mjs|js|ts|tsx|jsx|py|go|rs|java|rb|css|html|json|yaml|yml|md|sh)/.test(trimmed)
|
|
411
|
+
|| /[a-zA-Z_]\w*\(\)/.test(trimmed) // function calls
|
|
412
|
+
|| /`[^`]+`/.test(trimmed) // backtick-quoted identifiers
|
|
413
|
+
|| /"[^"]{3,}"/.test(trimmed) // quoted strings
|
|
414
|
+
|| /\b(line|function|class|method|variable|module|component|endpoint|route|table|column)\s+\w+/i.test(trimmed);
|
|
415
|
+
|
|
416
|
+
// Vague pronoun patterns
|
|
417
|
+
const vaguePatterns = /^(fix|change|update|do|make|help|look at|check)\s+(this|that|it|the thing|stuff|things?)$/i;
|
|
418
|
+
if (vaguePatterns.test(trimmed)) return { ambiguous: true, reason: 'vague-reference', confidence: 0.85 };
|
|
419
|
+
|
|
420
|
+
if (!hasSpecifics && trimmed.split(/\s+/).length < 5) {
|
|
421
|
+
return { ambiguous: true, reason: 'lacks-specifics', confidence: 0.7 };
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return { ambiguous: false, reason: null, confidence: 0.1 };
|
|
425
|
+
}
|