dual-brain 0.1.12 → 0.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // dual-brain — CLI entry point. Commands: init, go, status, remember, forget
2
+ // dual-brain — CLI entry point. Commands: init, go, think, review, status, 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';
@@ -12,6 +12,7 @@ import {
12
12
  rememberPreference, forgetPreference, getActivePreferences,
13
13
  getAvailableProviders, isSoloBrain, getHeadModel,
14
14
  detectAuth, detectEnvironment, detectPlans,
15
+ detectCapabilities,
15
16
  saveSubscription, listSubscriptions,
16
17
  autoSetup,
17
18
  } from '../src/profile.mjs';
@@ -28,6 +29,8 @@ import {
28
29
 
29
30
  import { dispatch, detectRuntime, dispatchDualBrain } from '../src/dispatch.mjs';
30
31
 
32
+ import { runPipeline, buildExecutionPlan, formatExecutionPlan } from '../src/pipeline.mjs';
33
+
31
34
  import { loadRepoCache } from '../src/repo.mjs';
32
35
  import { loadSession, saveSession, formatSessionCard, importReplitSessions, getSessionMeta, saveSessionMeta, renameSession, pinSession, unpinSession, categorizeSession, enrichSessions, archiveSession, getArchivedSessions } from '../src/session.mjs';
33
36
 
@@ -156,7 +159,7 @@ dual-brain <command> [options]
156
159
 
157
160
  Commands:
158
161
  init First-time setup → flows into interactive REPL
159
- auth Show subscription and login status
162
+ auth Show provider login and plan status
160
163
  install Install Claude Code hooks into the current project
161
164
  go "task description" Detect → decide → dispatch a task
162
165
  --dry-run Show routing decision without executing
@@ -220,7 +223,7 @@ function detectReplitTools(cwd) {
220
223
  // ─── Subscription status table ────────────────────────────────────────────────
221
224
 
222
225
  /**
223
- * Print a subscription status table to stdout.
226
+ * Print a provider status table to stdout.
224
227
  */
225
228
  function printSubscriptionTable(auth, profile) {
226
229
  const W = 55;
@@ -234,10 +237,10 @@ function printSubscriptionTable(auth, profile) {
234
237
  const openaiSub = profile?.providers?.openai;
235
238
 
236
239
  const claudePlanLabel = claudeSub?.enabled
237
- ? ({ pro: 'Pro ($20/mo)', max5: 'Max x5 ($100/mo)', max20: 'Max x20 ($200/mo)', '$20': 'Pro ($20/mo)', '$100': 'Max x5 ($100/mo)', '$200': 'Max x20 ($200/mo)' }[claudeSub.plan] ?? claudeSub.plan)
240
+ ? ({ pro: 'Pro', max5: 'Max x5', max20: 'Max x20', '$20': 'Pro', '$100': 'Max x5', '$200': 'Max x20' }[claudeSub.plan] ?? claudeSub.plan) // doctor:verified — config value lookup
238
241
  : 'disabled';
239
242
  const openaiPlanLabel = openaiSub?.enabled
240
- ? ({ plus: 'Plus ($20/mo)', pro: 'Pro ($100/mo)', pro100: 'Pro ($100/mo)', pro200: 'Pro ($200/mo)', '$20': 'Plus ($20/mo)', '$100': 'Pro ($100/mo)', '$200': 'Pro ($200/mo)' }[openaiSub.plan] ?? openaiSub.plan)
243
+ ? ({ plus: 'Plus', pro: 'Pro', pro100: 'Pro', pro200: 'Pro (higher limits)', '$20': 'Plus', '$100': 'Pro', '$200': 'Pro (higher limits)' }[openaiSub.plan] ?? openaiSub.plan) // doctor:verified — config value lookup
241
244
  : 'disabled';
242
245
 
243
246
  const claudeLabel = claudeSub?.label ? ` [${claudeSub.label}]` : '';
@@ -254,7 +257,7 @@ function printSubscriptionTable(auth, profile) {
254
257
  const openaiLine2 = ` plan: ${openaiPlanLabel}${openaiLabel}`;
255
258
 
256
259
  console.log(`╔${hbar}╗`);
257
- console.log(`║${pad(' Subscription Status')}║`);
260
+ console.log(`║${pad(' Provider Status')}║`);
258
261
  console.log(`╠${hbar}╣`);
259
262
  console.log(`║${pad(claudeLine1)}║`);
260
263
  console.log(`║${pad(claudeLine2)}║`);
@@ -299,7 +302,7 @@ async function cmdInit(rl) {
299
302
  }
300
303
 
301
304
  /**
302
- * Show subscription status (replaces old API key auth display).
305
+ * Show provider login and plan status.
303
306
  */
304
307
  async function cmdAuth(subArgs = []) {
305
308
  const auth = await detectAuth();
@@ -325,89 +328,57 @@ async function cmdGo(args) {
325
328
  const prompt = args.find(a => !a.startsWith('--') && !a.startsWith('-') && a !== (filesRaw ?? ''));
326
329
  if (!prompt) err('Usage: dual-brain go "task description" [--dry-run] [--files a,b] [--verbose]');
327
330
 
328
- const cwd = process.cwd();
329
- const profile = await ensureProfile(cwd);
330
- const detection = detectTask({ prompt, files });
331
-
332
- // Print the one-sentence classification
333
- console.log(detection.explanation);
334
-
335
- // Verbose: emit detection trace before routing decision
336
- if (verbose) {
337
- vtrace(`Intent: ${detection.intent} | Risk: ${detection.risk} | Complexity: ${detection.complexity} | Effort: ${detection.effort ?? 'n/a'}`);
338
- vtrace(`Tier: ${detection.tier} | Files: ${detection.fileCount ?? files.length} | Requires write: ${detection.requiresWrite}`);
339
- }
340
-
341
- // Verbose: emit provider health scores before dispatch
342
- if (verbose) {
343
- const providers = getAvailableProviders(profile);
344
- const { states } = getHealth(cwd);
345
- const providerScores = ['claude', 'openai'].map(name => {
346
- const enabled = providers.some(p => p.name === name);
347
- if (!enabled) return `${name}=unavailable`;
348
- // Find any state entry for this provider
349
- const statuses = Object.entries(states)
350
- .filter(([k]) => k.startsWith(`${name}:`))
351
- .map(([, v]) => v.status);
352
- const worst = statuses.includes('hot') ? 'hot'
353
- : statuses.includes('probing') ? 'probing'
354
- : statuses.includes('degraded') ? 'degraded'
355
- : 'healthy';
356
- return `${name}=${worst}`;
357
- }).join(' ');
358
- vtrace(`Provider health: ${providerScores}`);
359
- }
360
-
361
- const decision = decideRoute({ profile, detection, cwd });
331
+ const cwd = process.cwd();
332
+ await ensureProfile(cwd);
362
333
 
363
- // Verbose: emit model selection and dual-brain rationale
364
- if (verbose) {
365
- const modelLabel = decision.effort ? `${decision.model} (${decision.effort})` : decision.model;
366
- const modelStatus = getAvailableModels(profile)[decision.provider]?.includes(decision.model)
367
- ? 'available, matches tier'
368
- : 'selected';
369
- vtrace(`Model selection: ${modelLabel} (${modelStatus})`);
370
- vtrace(`Dual-brain: ${decision.dualBrain ? 'yes' : 'no'} (${isSoloBrain(profile) ? 'solo provider' : 'dual provider'}, ${detection.risk} risk)`);
371
- }
334
+ if (verbose) console.log('\nDispatching...');
372
335
 
373
- // Print routing table (only in dry-run or verbose; silent in normal mode)
374
- if (dryRun || verbose) {
375
- console.log(` provider : ${decision.provider}`);
376
- console.log(` model : ${decision.model}${decision.effort ? ' (' + decision.effort + ')' : ''}`);
377
- console.log(` tier : ${decision.tier}`);
378
- console.log(` dual-brain : ${decision.dualBrain ? 'yes' : 'no'}`);
379
- console.log(` reason : ${decision.explanation}`);
380
- }
336
+ const { plan, result } = await runPipeline('go', prompt, {
337
+ files,
338
+ cwd,
339
+ verbose,
340
+ dryRun,
341
+ });
381
342
 
382
343
  if (dryRun) {
344
+ // formatExecutionPlan already printed by pipeline when verbose/dryRun=true
383
345
  console.log('\n(dry-run — not executing)');
384
346
  return;
385
347
  }
386
348
 
387
- if (verbose) console.log('\nDispatching...');
388
- let result;
389
- if (decision.dualBrain) {
390
- result = await dispatchDualBrain({ decision, prompt, files, cwd, verbose });
349
+ if (!result) return;
350
+
351
+ // Display result — dual-brain vs single-provider
352
+ if (result.consensus) {
391
353
  console.log(`\nConsensus: ${result.consensus}`);
392
354
  if (result.claude?.summary) console.log(`Claude : ${result.claude.summary}`);
393
355
  if (result.openai?.summary) console.log(`OpenAI : ${result.openai.summary}`);
394
- // Save session state
395
356
  saveSession({
396
357
  objective: prompt,
397
358
  branch: null,
398
359
  filesChanged: files,
399
360
  commandsRun: [`dual-brain go "${prompt}"`],
400
361
  lastResult: { status: 'success', summary: result.consensus || 'dual-brain complete' },
401
- provider: decision.provider,
362
+ provider: plan?._decision?.provider ?? 'claude',
402
363
  nextAction: null,
403
364
  }, cwd);
365
+ // ── Next steps suggestions (dual-brain consensus path) ──────────────────
366
+ try {
367
+ const { suggestNextSteps, formatNextSteps } = await import('../src/nextstep.mjs');
368
+ const steps = await suggestNextSteps(
369
+ { prompt, tier: plan?._decision?.tier ?? 'think', files, trigger: 'go' },
370
+ { success: true, filesChanged: files, error: null, duration: null },
371
+ cwd
372
+ );
373
+ if (steps?.steps?.length > 0) {
374
+ console.log('\n' + formatNextSteps(steps.steps, 3));
375
+ }
376
+ } catch { /* non-fatal */ }
404
377
  } else {
405
- result = await dispatch({ decision, prompt, files, cwd, verbose });
406
378
  const statusLine = result.status === 'completed' ? 'Done' : `Failed (exit ${result.exitCode})`;
407
- console.log(`\n${statusLine} in ${(result.durationMs / 1000).toFixed(1)}s`);
379
+ console.log(`\n${statusLine}${result.durationMs != null ? ` in ${(result.durationMs / 1000).toFixed(1)}s` : ''}`);
408
380
  if (result.summary) console.log(result.summary);
409
381
  if (result.error) process.stderr.write(`${result.error}\n`);
410
- // Save session state regardless of success/failure
411
382
  saveSession({
412
383
  objective: prompt,
413
384
  branch: null,
@@ -417,14 +388,92 @@ async function cmdGo(args) {
417
388
  status: result.status === 'completed' ? 'success' : 'failure',
418
389
  summary: result.summary || (result.status === 'completed' ? 'completed' : `exit ${result.exitCode}`),
419
390
  },
420
- provider: decision.provider,
391
+ provider: plan?._decision?.provider ?? 'claude',
421
392
  nextAction: null,
422
393
  }, cwd);
423
394
  if (result.status !== 'completed') process.exit(1);
424
395
  await offerAutoCommit(cwd);
396
+ // ── Next steps suggestions ──────────────────────────────────────────────
397
+ try {
398
+ const { suggestNextSteps, formatNextSteps } = await import('../src/nextstep.mjs');
399
+ const steps = await suggestNextSteps(
400
+ {
401
+ prompt,
402
+ tier: plan?._decision?.tier ?? 'execute',
403
+ files: result.filesChanged || files,
404
+ trigger: 'go',
405
+ },
406
+ {
407
+ success: result.status === 'completed',
408
+ filesChanged: result.filesChanged || files,
409
+ error: result.error,
410
+ duration: result.durationMs,
411
+ },
412
+ cwd
413
+ );
414
+ if (steps?.steps?.length > 0) {
415
+ console.log('\n' + formatNextSteps(steps.steps, 3));
416
+ }
417
+ } catch { /* non-fatal — module may not exist yet */ }
425
418
  }
426
419
  }
427
420
 
421
+ async function cmdThink(args) {
422
+ const question = args.find(a => !a.startsWith('--') && !a.startsWith('-'));
423
+ if (!question) err('Usage: dual-brain think "architecture question or design decision"');
424
+
425
+ const cwd = process.cwd();
426
+ await ensureProfile(cwd);
427
+
428
+ const { result, verification } = await runPipeline('think', question, {
429
+ cwd,
430
+ verbose: true,
431
+ });
432
+
433
+ if (!result) return;
434
+
435
+ if (result.consensus) {
436
+ console.log(`\nConsensus: ${result.consensus}`);
437
+ if (result.claude?.summary) console.log(`Claude : ${result.claude.summary}`);
438
+ if (result.openai?.summary) console.log(`OpenAI : ${result.openai.summary}`);
439
+ } else {
440
+ if (result.summary) console.log(`\n${result.summary}`);
441
+ if (result.error) process.stderr.write(`${result.error}\n`);
442
+ if (result.status && result.status !== 'completed') process.exit(1);
443
+ }
444
+
445
+ if (verification && !verification.ok) {
446
+ for (const note of verification.notes) process.stderr.write(` note: ${note}\n`);
447
+ }
448
+ }
449
+
450
+ async function cmdReview(_args) {
451
+ const cwd = process.cwd();
452
+ await ensureProfile(cwd);
453
+
454
+ const { result, verification } = await runPipeline('review', 'review current diff', {
455
+ cwd,
456
+ verbose: true,
457
+ });
458
+
459
+ if (!result) return;
460
+
461
+ if (result.consensus) {
462
+ console.log(`\nConsensus: ${result.consensus}`);
463
+ if (result.claude?.summary) console.log(`Claude : ${result.claude.summary}`);
464
+ if (result.openai?.summary) console.log(`OpenAI : ${result.openai.summary}`);
465
+ } else {
466
+ if (result.summary) console.log(`\n${result.summary}`);
467
+ if (result.error) process.stderr.write(`${result.error}\n`);
468
+ if (result.status && result.status !== 'completed') process.exit(1);
469
+ }
470
+
471
+ if (verification && !verification.ok) {
472
+ for (const note of verification.notes) process.stderr.write(` note: ${note}\n`);
473
+ }
474
+ }
475
+
476
+
428
477
  async function cmdStatus(args = []) {
429
478
  const verbose = args.includes('--verbose') || args.includes('-v');
430
479
  const cwd = process.cwd();
@@ -717,21 +766,21 @@ function profileExists(cwd) {
717
766
  // ─── Plan label helpers ───────────────────────────────────────────────────────
718
767
 
719
768
  const CLAUDE_PLAN_LABELS = {
720
- pro: 'Pro ($20/mo)',
721
- max5: 'Max x5 ($100/mo)',
722
- max20: 'Max x20 ($200/mo)',
723
- '$20': 'Pro ($20/mo)',
724
- '$100': 'Max x5 ($100/mo)',
725
- '$200': 'Max x20 ($200/mo)',
769
+ pro: 'Pro',
770
+ max5: 'Max x5',
771
+ max20: 'Max x20',
772
+ '$20': 'Pro', // doctor:verified — backward-compat key for legacy stored plan value
773
+ '$100': 'Max x5', // doctor:verified — backward-compat key for legacy stored plan value
774
+ '$200': 'Max x20', // doctor:verified — backward-compat key for legacy stored plan value
726
775
  };
727
776
  const OPENAI_PLAN_LABELS = {
728
- plus: 'Plus ($20/mo)',
729
- pro: 'Pro ($100/mo)',
730
- pro100: 'Pro ($100/mo)',
731
- pro200: 'Pro ($200/mo)',
732
- '$20': 'Plus ($20/mo)',
733
- '$100': 'Pro ($100/mo)',
734
- '$200': 'Pro ($200/mo)',
777
+ plus: 'Plus',
778
+ pro: 'Pro',
779
+ pro100: 'Pro',
780
+ pro200: 'Pro (higher limits)',
781
+ '$20': 'Plus', // doctor:verified — backward-compat key for legacy stored plan value
782
+ '$100': 'Pro', // doctor:verified — backward-compat key for legacy stored plan value
783
+ '$200': 'Pro (higher limits)', // doctor:verified — backward-compat key for legacy stored plan value
735
784
  };
736
785
 
737
786
  // ─── Screen: welcomeScreen ────────────────────────────────────────────────────
@@ -817,7 +866,7 @@ async function welcomeScreen(rl, ask) {
817
866
  }
818
867
 
819
868
  console.log(' [Enter] Save and go');
820
- console.log(' [c] Customize plan tier');
869
+ console.log(' [c] Customize work style');
821
870
  if (existingSessions.length > 0) {
822
871
  console.log(` [i] Import ${existingSessions.length} session${existingSessions.length !== 1 ? 's' : ''} from data-tools`);
823
872
  }
@@ -880,10 +929,10 @@ async function welcomeScreen(rl, ask) {
880
929
  // Claude plan picker
881
930
  if (claudeReady) {
882
931
  console.log('');
883
- console.log(separator('Claude subscription'));
884
- console.log(' (1) Pro ($20/mo)');
885
- console.log(' (2) Max x5 ($100/mo)');
886
- console.log(' (3) Max x20 ($200/mo)');
932
+ console.log(separator('Claude plan'));
933
+ console.log(' (1) Pro');
934
+ console.log(' (2) Max x5');
935
+ console.log(' (3) Max x20');
887
936
  console.log(' (4) Skip');
888
937
  const claudeChoice = (await ask('> ')).trim();
889
938
  const claudePlanMap = { '1': 'pro', '2': 'max5', '3': 'max20' };
@@ -900,10 +949,10 @@ async function welcomeScreen(rl, ask) {
900
949
  // OpenAI plan picker
901
950
  if (openaiReady) {
902
951
  console.log('');
903
- console.log(separator('OpenAI subscription'));
904
- console.log(' (1) Plus ($20/mo)');
905
- console.log(' (2) Pro ($100/mo)');
906
- console.log(' (3) Pro ($200/mo higher limits)');
952
+ console.log(separator('OpenAI plan'));
953
+ console.log(' (1) Plus');
954
+ console.log(' (2) Pro');
955
+ console.log(' (3) Pro (higher limits)');
907
956
  console.log(' (4) Skip');
908
957
  const openaiChoice = (await ask('> ')).trim();
909
958
  const openaiPlanMap = { '1': 'plus', '2': 'pro', '3': 'pro200' };
@@ -928,8 +977,8 @@ async function welcomeScreen(rl, ask) {
928
977
 
929
978
  // Team setup
930
979
  console.log('');
931
- console.log(' Team auth: label subscriptions and set expiry for auto-refresh.');
932
- console.log(' When a subscription expires, dual-brain will prompt re-login automatically.');
980
+ console.log(' Team auth: label providers and set expiry for auto-refresh.');
981
+ console.log(' When a provider link expires, dual-brain will prompt re-login automatically.');
933
982
  console.log('');
934
983
  console.log(' [Enter] Skip [t] Set up team auth');
935
984
  const teamChoice = (await ask(' Choice: ')).trim().toLowerCase();
@@ -937,7 +986,7 @@ async function welcomeScreen(rl, ask) {
937
986
  for (const provider of ['claude', 'openai']) {
938
987
  if (!existingProfile.providers[provider]?.enabled) continue;
939
988
  const provLabel = provider === 'claude' ? 'Claude' : 'OpenAI';
940
- const label = (await ask(` ${provLabel} label (e.g. "Josh's $100 sub"): `)).trim();
989
+ const label = (await ask(` ${provLabel} label (e.g. "Josh's work account"): `)).trim();
941
990
  if (label) existingProfile.providers[provider].label = label;
942
991
  const expiry = await askExpiry(ask, provLabel);
943
992
  if (expiry) existingProfile.providers[provider].expiresAt = expiry;
@@ -1457,11 +1506,39 @@ async function mainScreen(rl, ask) {
1457
1506
  statusRows.push(row(`\x1b[2m📦 data-tools v${dtVersion}\x1b[0m`));
1458
1507
  }
1459
1508
 
1509
+ // ── Observer observations (top 2, high priority first) ───────────────────
1510
+ let quickObservations = [];
1511
+ try {
1512
+ const observerMod = await import('../src/observer.mjs');
1513
+ const quickState = await observerMod.getQuickState(cwd);
1514
+ if (quickState?.observations?.length > 0) {
1515
+ const PRIO = { high: 0, medium: 1, low: 2 };
1516
+ const sorted = [...quickState.observations].sort(
1517
+ (a, b) => (PRIO[a.priority] ?? 2) - (PRIO[b.priority] ?? 2)
1518
+ );
1519
+ quickObservations = sorted.slice(0, 2);
1520
+ for (const obs of quickObservations) {
1521
+ let prefix;
1522
+ if (obs.priority === 'high') prefix = '🔴';
1523
+ else if (obs.priority === 'medium') prefix = '🟡';
1524
+ else prefix = '\x1b[2m💡\x1b[0m';
1525
+ statusRows.push(row(`${prefix} ${obs.message}`));
1526
+ }
1527
+ }
1528
+ } catch { /* non-fatal — module may not exist yet */ }
1529
+
1460
1530
  // ── Action cards (git state + open PRs) ──────────────────────────────────
1461
1531
  const repoState = detectRepoState(cwd);
1462
1532
  const openPRs = await detectOpenPRs(cwd);
1463
1533
  const actionRows = buildActionRows(repoState, row, openPRs);
1464
1534
 
1535
+ // ── High-priority observer action cards ───────────────────────────────────
1536
+ if (quickObservations.some(o => o.priority === 'high')) {
1537
+ const DIM = '\x1b[2m';
1538
+ const RESET = '\x1b[0m';
1539
+ actionRows.push(row(`${DIM}[r] Security review [t] Run tests [c] Commit${RESET}`));
1540
+ }
1541
+
1465
1542
  // ── Related sessions hint (only when no continuation card is showing) ─────
1466
1543
  if (!interrupted && recentSessions.length > 0) {
1467
1544
  try {
@@ -2423,21 +2500,15 @@ async function settingsScreen(rl, ask) {
2423
2500
 
2424
2501
  // ─── Helper: aggregatePlans ───────────────────────────────────────────────────
2425
2502
 
2426
- const PLAN_PRICES = {
2427
- pro: '$20', max5: '$100', max20: '$200',
2428
- plus: '$20', pro100: '$100', pro200: '$200',
2429
- };
2430
-
2431
2503
  function aggregatePlans(subs) {
2432
2504
  if (!subs || subs.length === 0) return '';
2433
2505
  const counts = {};
2434
2506
  for (const s of subs) {
2435
- const price = PLAN_PRICES[s.plan] || s.plan;
2436
- counts[price] = (counts[price] || 0) + 1;
2507
+ const label = s.plan || 'unknown';
2508
+ counts[label] = (counts[label] || 0) + 1;
2437
2509
  }
2438
2510
  return Object.entries(counts)
2439
- .sort((a, b) => parseInt(b[0].slice(1)) - parseInt(a[0].slice(1)))
2440
- .map(([price, count]) => `${price}×${count}`)
2511
+ .map(([label, count]) => count > 1 ? `${label}×${count}` : label)
2441
2512
  .join(' ');
2442
2513
  }
2443
2514
 
@@ -2499,13 +2570,13 @@ async function subscriptionsScreen(rl, ask) {
2499
2570
  const choice = (await ask(' Choice: ')).trim().toLowerCase();
2500
2571
 
2501
2572
  if (choice === '1') {
2502
- console.log('\n Linking Claude subscription...');
2573
+ console.log('\n Linking Claude account...');
2503
2574
  console.log(' A browser window will open — paste the code below when prompted.\n');
2504
2575
  const { spawnSync } = await import('node:child_process');
2505
2576
  const r = spawnSync('claude', ['auth', 'login'], { stdio: 'inherit', timeout: 60000 });
2506
2577
  if (r.status === 0) {
2507
2578
  console.log('\n ✅ Claude linked successfully!\n');
2508
- const label = (await ask(" Label (e.g. \"Josh's $100 sub\", or Enter to skip): ")).trim();
2579
+ const label = (await ask(" Label (e.g. \"Josh's work account\", or Enter to skip): ")).trim();
2509
2580
  const expiry = await askExpiry(ask, 'Claude');
2510
2581
  const newPlans = detectPlans();
2511
2582
  const plan = newPlans.claude?.plan || 'pro';
@@ -2527,7 +2598,7 @@ async function subscriptionsScreen(rl, ask) {
2527
2598
  }
2528
2599
 
2529
2600
  if (choice === '2') {
2530
- console.log('\n Linking Codex subscription...');
2601
+ console.log('\n Linking Codex account...');
2531
2602
  console.log(' A browser window will open — paste the code below when prompted.\n');
2532
2603
  const { spawnSync } = await import('node:child_process');
2533
2604
  const r = spawnSync('codex', ['login'], { stdio: 'inherit', timeout: 60000 });
@@ -2565,12 +2636,12 @@ async function subscriptionsScreen(rl, ask) {
2565
2636
  }
2566
2637
 
2567
2638
  if (allSubs.length === 0) {
2568
- console.log('\n No subscriptions to remove.\n');
2639
+ console.log('\n No linked accounts to remove.\n');
2569
2640
  await ask(' Press Enter to continue...');
2570
2641
  return { next: 'subscriptions' };
2571
2642
  }
2572
2643
 
2573
- console.log('\n Remove a subscription:\n');
2644
+ console.log('\n Remove a linked account:\n');
2574
2645
  allSubs.forEach(({ displayName, sub }, i) => {
2575
2646
  const planLabels = displayName === 'Claude' ? CLAUDE_PLAN_LABELS : OPENAI_PLAN_LABELS;
2576
2647
  const planLabel = planLabels[sub.plan] ?? sub.plan ?? 'unknown';
@@ -2617,14 +2688,13 @@ async function subscriptionsScreen(rl, ask) {
2617
2688
  * @param {object} rl readline interface
2618
2689
  * @returns {object|null} profile object to save, or null if cancelled/skipped
2619
2690
  */
2620
- async function runOnboardingWizard(detection, cwd, rl) {
2691
+ async function runOnboardingWizard(_detection, cwd, rl) {
2621
2692
  const ask = (q) => new Promise(res => rl.question(q, res));
2622
2693
  const version = readVersion();
2623
2694
 
2624
2695
  // ── Rounded box helpers (matching mainScreen style) ────────────────────────
2625
2696
  const W = 51;
2626
2697
  const wTop = ` ┌${'─'.repeat(W)}┐`;
2627
- const wSep = ` ├${'─'.repeat(W)}┤`;
2628
2698
  const wBottom = ` └${'─'.repeat(W)}┘`;
2629
2699
  const wPad = (s) => {
2630
2700
  const plain = s.replace(/\x1b\[[0-9;]*m/g, '');
@@ -2641,105 +2711,95 @@ async function runOnboardingWizard(detection, cwd, rl) {
2641
2711
  };
2642
2712
  const wRow = (s) => ` │ ${wPad(s)}│`;
2643
2713
 
2644
- const { auth, existingSessions } = detection;
2645
- const claudeReady = auth.claude.found;
2646
- const openaiReady = auth.openai.found;
2647
-
2648
- // ── Detect Codex CLI availability ─────────────────────────────────────────
2649
- let codexAvailable = false;
2650
- try {
2651
- const { spawnSync } = await import('node:child_process');
2652
- const r = spawnSync('which', ['codex'], { encoding: 'utf8' });
2653
- codexAvailable = r.status === 0;
2654
- } catch {}
2714
+ // ── Use detectCapabilities for broad detection (env vars, ~/.claude, CLI) ──
2715
+ const caps = await detectCapabilities(cwd);
2716
+ const claudeReady = caps.claude.available;
2717
+ const openaiReady = caps.openai.available;
2718
+ const codexAvailable = caps.codex.available;
2655
2719
 
2656
2720
  // ── Detect replit-tools ────────────────────────────────────────────────────
2657
2721
  const rt = detectReplitTools(cwd);
2658
2722
 
2659
2723
  const GREEN = '\x1b[32m✓\x1b[0m';
2660
2724
  const RED = '\x1b[31m✗\x1b[0m';
2725
+ const DIM = '\x1b[2m';
2726
+ const RESET = '\x1b[0m';
2661
2727
 
2662
2728
  // ══════════════════════════════════════════════════════════════════════════
2663
- // Step 1 — Auto-detect capabilities (no user input)
2729
+ // Step 1 — Auto-detect capabilities (instant, no spinner)
2664
2730
  // ══════════════════════════════════════════════════════════════════════════
2665
2731
  console.log('');
2666
2732
  console.log(wTop);
2667
2733
  console.log(wRow(`🧠 Dual-Brain v${version} — First-time Setup`));
2668
- console.log(wSep);
2669
- console.log(wRow('Checking your setup...'));
2670
- console.log(wSep);
2671
2734
  console.log(wRow(claudeReady
2672
- ? `${GREEN} Claude Code — available`
2735
+ ? `${GREEN} Claude Code`
2673
2736
  : `${RED} Claude Code — not found`));
2674
2737
  console.log(wRow(openaiReady
2675
- ? `${GREEN} OpenAI API — available`
2676
- : `${RED} OpenAI API — not found`));
2677
- console.log(wRow(codexAvailable
2678
- ? `${GREEN} Codex CLI available`
2679
- : `${RED} Codex CLI — not found`));
2738
+ ? `${GREEN} OpenAI API`
2739
+ : codexAvailable
2740
+ ? `${GREEN} OpenAI / Codex CLI`
2741
+ : `${DIM} OpenAInot configured${RESET}`));
2680
2742
  console.log(wRow(rt.installed
2681
- ? `${GREEN} replit-tools — available`
2682
- : `${RED} replit-tools — not found`));
2743
+ ? `${GREEN} replit-tools`
2744
+ : `${DIM} replit-tools — not found${RESET}`));
2683
2745
  console.log(wBottom);
2684
2746
 
2685
- if (!claudeReady && !openaiReady) {
2747
+ // ── Edge cases: communicate honestly, but always let them proceed ──────────
2748
+ console.log('');
2749
+ if (!claudeReady && !openaiReady && !codexAvailable) {
2750
+ console.log(' No AI providers detected — configure OPENAI_API_KEY or use');
2751
+ console.log(' within Claude Code. You can still continue and set up later.');
2752
+ console.log('');
2753
+ } else if (claudeReady && !openaiReady && !codexAvailable) {
2754
+ console.log(` ${DIM}Tip: Add OPENAI_API_KEY for dual-brain collaboration${RESET}`);
2755
+ console.log('');
2756
+ } else if (!claudeReady && (openaiReady || codexAvailable)) {
2757
+ console.log(` ${DIM}Note: Use within Claude Code for full dual-brain${RESET}`);
2686
2758
  console.log('');
2687
- console.log(' No AI provider found. Log in first:');
2688
- console.log(' claude auth login — for Claude');
2689
- console.log(' codex login — for OpenAI/Codex');
2690
- console.log(' Then re-run: dual-brain init\n');
2691
- return null;
2692
2759
  }
2693
2760
 
2694
2761
  // ══════════════════════════════════════════════════════════════════════════
2695
2762
  // Step 2 — ONE question: work style
2696
2763
  // ══════════════════════════════════════════════════════════════════════════
2697
- console.log('');
2698
2764
  console.log(wTop);
2699
- console.log(wRow(`🧠 Dual-Brain v${version} — Work Style`));
2700
- console.log(wSep);
2701
2765
  console.log(wRow('How do you want to work?'));
2702
- console.log(wSep);
2703
- console.log(wRow(' 1. ⚡ Fast quick answers, single model, skip reviews'));
2704
- console.log(wRow(' 2. ⚖️ Balanced — smart routing, reviews on important changes'));
2705
- console.log(wRow(' (recommended)'));
2706
- console.log(wRow(' 3. 🔥 Full Power — deep reasoning, dual-brain on everything'));
2707
- console.log(wRow(' that matters'));
2708
- console.log(wSep);
2709
- console.log(wRow('[Enter] Balanced'));
2766
+ console.log(wRow(''));
2767
+ console.log(wRow(' 1 ⚡ Fast single model, quick tasks, skip reviews'));
2768
+ console.log(wRow(' 2 ⚖️ Balanced — smart routing, reviews on important changes'));
2769
+ console.log(wRow(' 3 🔥 Full Power — deep reasoning, dual-brain when it matters'));
2710
2770
  console.log(wBottom);
2711
2771
  console.log('');
2712
2772
 
2713
- const styleChoice = (await ask(' Choice [1/2/3/Enter]: ')).trim();
2714
- const styleMap = { '1': 'cost-saver', '2': 'balanced', '3': 'quality-first' };
2773
+ const styleChoice = (await ask(' Choice [2]: ')).trim();
2774
+ const styleMap = { '1': 'cost-saver', '2': 'balanced', '3': 'quality-first' };
2775
+ const styleNames = { 'cost-saver': 'Fast', 'balanced': 'Balanced', 'quality-first': 'Full Power' };
2715
2776
  const chosenBias = styleMap[styleChoice] || 'balanced';
2777
+ const chosenName = styleNames[chosenBias];
2716
2778
 
2717
2779
  // ── Non-blocking note if metered API detected ──────────────────────────────
2718
- if (openaiReady && !codexAvailable) {
2780
+ if (openaiReady && caps.openai.metered) {
2781
+ console.log(` ${DIM}OpenAI API key detected — usage is metered, guardrails enabled${RESET}`);
2719
2782
  console.log('');
2720
- console.log(' 💡 OpenAI API detected — will confirm before expensive operations');
2721
2783
  }
2722
2784
 
2723
2785
  // ── Done ───────────────────────────────────────────────────────────────────
2724
- console.log('');
2725
2786
  console.log(wTop);
2726
- console.log(wRow(`✓ Ready. Type a task or press Enter for dashboard.`));
2787
+ console.log(wRow(`${GREEN} Ready ${chosenName} mode`));
2788
+ console.log(wRow(` Type a task to start, or press Enter for dashboard`));
2727
2789
  console.log(wBottom);
2728
2790
  console.log('');
2729
2791
 
2730
2792
  // ── Build and return the profile object ────────────────────────────────────
2731
2793
  const finalProfile = loadProfile(cwd);
2732
2794
 
2733
- if (claudeReady) {
2734
- finalProfile.providers.claude = { enabled: true };
2735
- }
2736
- if (openaiReady) {
2737
- finalProfile.providers.openai = { enabled: true };
2738
- }
2795
+ finalProfile.providers.claude = { enabled: claudeReady };
2796
+ finalProfile.providers.openai = { enabled: openaiReady || codexAvailable };
2797
+ finalProfile.apiGuardrail = caps.openai.metered;
2739
2798
 
2740
- const enabledCount = [claudeReady, openaiReady].filter(Boolean).length;
2741
- finalProfile.mode = enabledCount >= 2 ? chosenBias : claudeReady ? 'solo-claude' : 'solo-openai';
2742
- finalProfile.bias = chosenBias;
2799
+ const enabledCount = [claudeReady, openaiReady || codexAvailable].filter(Boolean).length;
2800
+ finalProfile.mode = enabledCount >= 2 ? 'dual' : claudeReady ? 'solo-claude' : 'solo-openai';
2801
+ finalProfile.bias = chosenBias;
2802
+ finalProfile.workStyle = chosenBias;
2743
2803
 
2744
2804
  return finalProfile;
2745
2805
  }
@@ -2780,11 +2840,11 @@ async function authScreen(rl, ask) {
2780
2840
  ` plan: ${openaiPlanLabel}${openaiSub?.label ? ` [${openaiSub.label}]` : ''}`,
2781
2841
  ];
2782
2842
 
2783
- console.log(box('Subscription Status', authLines));
2843
+ console.log(box('Provider Status', authLines));
2784
2844
  console.log('');
2785
2845
  console.log(menu([
2786
- { key: 'a', label: 'Manage subscriptions', section: '' },
2787
- { key: 'b', label: 'Back to dashboard', section: '' },
2846
+ { key: 'a', label: 'Manage linked accounts', section: '' },
2847
+ { key: 'b', label: 'Back to dashboard', section: '' },
2788
2848
  ]));
2789
2849
  console.log('');
2790
2850
 
@@ -4383,6 +4443,8 @@ async function main() {
4383
4443
  return;
4384
4444
  }
4385
4445
  if (cmd === 'go') { await cmdGo(args.slice(1)); return; }
4446
+ if (cmd === 'think') { await cmdThink(args.slice(1)); return; }
4447
+ if (cmd === 'review') { await cmdReview(args.slice(1)); return; }
4386
4448
  if (cmd === 'status') { await cmdStatus(args.slice(1)); return; }
4387
4449
  if (cmd === 'hot') { cmdHot(args[1]); return; }
4388
4450
  if (cmd === 'cool') { cmdCool(args[1]); return; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dual-brain",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "description": "AI orchestration across Claude + OpenAI subscriptions — smart routing, budget awareness, and dual-brain collaboration",
5
5
  "type": "module",
6
6
  "bin": {
package/src/decide.mjs CHANGED
@@ -26,8 +26,7 @@ const WORKSPACE = join(__dirname, '..');
26
26
  /**
27
27
  * Work styles control how aggressively the router uses stronger models,
28
28
  * challenger (dual-brain) reviews, and checkpoints.
29
- * These replace subscription-tier-based routing the user picks a style
30
- * regardless of what plan they are on.
29
+ * The user picks a style regardless of provider or plan — no price gating.
31
30
  */
32
31
  export const WORK_STYLES = {
33
32
  fast: {
@@ -193,7 +192,7 @@ export function getModelCapabilities(model) {
193
192
  * Return which models the user can access.
194
193
  * All known models are available by default; providers can explicitly restrict
195
194
  * via profile.providers.<provider>.models (array of allowed model short names).
196
- * This does NOT gate on subscription price — that was fake knowledge.
195
+ * This does NOT gate on price or configured plan we cannot verify those from here.
197
196
  * @param {{ providers?: { claude?: { enabled?: boolean, models?: string[] }, openai?: { enabled?: boolean, models?: string[] } } }} profile
198
197
  * @returns {{ claude: string[], openai: string[] }}
199
198
  */
package/src/profile.mjs CHANGED
@@ -131,7 +131,7 @@ function detectEnvironment() {
131
131
 
132
132
  /**
133
133
  * Detect what providers and tools are actually available.
134
- * Never makes network calls, never claims to know subscription tier or price.
134
+ * Never makes network calls, never claims to know configured plan or price.
135
135
  *
136
136
  * @param {string} [cwd]
137
137
  * @returns {Promise<{
@@ -338,7 +338,7 @@ function migrateProfile(profile) {
338
338
  }
339
339
  delete profile.plan;
340
340
  delete profile.price;
341
- delete profile.subscription;
341
+ delete profile.subscription; // doctor:verified — removing legacy field from stored config
342
342
  delete profile.budget;
343
343
  delete profile.detectedPlan;
344
344
  }
@@ -771,7 +771,7 @@ function detectPlans() {
771
771
  return { claude: null, openai: null };
772
772
  }
773
773
 
774
- /** @deprecated Plan/subscription tracking removed. Use provider enabled flag instead. */
774
+ /** @deprecated Plan tracking removed. Use provider enabled flag instead. */
775
775
  function saveSubscription(provider, config, cwd) {
776
776
  const profile = loadProfile(cwd);
777
777
  if (!profile.providers[provider]) profile.providers[provider] = { enabled: true };
@@ -780,7 +780,7 @@ function saveSubscription(provider, config, cwd) {
780
780
  return profile;
781
781
  }
782
782
 
783
- /** @deprecated Plan/subscription tracking removed. Use getAvailableProviders() instead. */
783
+ /** @deprecated Plan tracking removed. Use getAvailableProviders() instead. */
784
784
  function listSubscriptions(cwd) {
785
785
  const profile = loadProfile(cwd);
786
786
  return profile.providers || {};