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.
@@ -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
- const openaiPresent = replit.hasSecret('OPENAI_API_KEY');
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
- // Use envReport secrets when available; fall back to auth detection
1648
- const claudeAvailable = envReport
1649
- ? envReport.secrets.ANTHROPIC_API_KEY || auth.claude.found
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 = envReport
1976
- ? envReport.secrets?.ANTHROPIC_API_KEY || auth.claude.found
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 = 'unknown';
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 auth login` to authenticate');
3881
- if (!openaiReady && !codexAvailable) dimLine('OpenAI: set OPENAI_API_KEY or run `codex login`');
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: openaiReady ? 'openai-apikey' : 'openai-codex',
4223
+ id: 'openai-codex',
3905
4224
  provider: 'openai',
3906
- auth_type: openaiAuthType || 'api_key',
3907
- source: openaiReady ? 'env_var' : 'cli_oauth',
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 = caps.openai.metered;
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') { await cmdInstall(); return; }
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.4",
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
+ }