dual-brain 0.2.4 → 0.2.5
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 +349 -2
- package/package.json +10 -2
- package/src/awareness.mjs +36 -0
- 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 -6
- package/src/health.mjs +35 -0
- package/src/pipeline.mjs +58 -1
- package/src/pr-agent.mjs +214 -0
- 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);
|
|
@@ -991,6 +1019,139 @@ async function cmdInstall(cwd) {
|
|
|
991
1019
|
}
|
|
992
1020
|
}
|
|
993
1021
|
|
|
1022
|
+
async function installGlobal() {
|
|
1023
|
+
const { homedir } = await import('node:os');
|
|
1024
|
+
const globalClaudeDir = join(homedir(), '.claude');
|
|
1025
|
+
const globalSettingsPath = join(globalClaudeDir, 'settings.json');
|
|
1026
|
+
|
|
1027
|
+
// Resolve absolute path to hooks directory via import.meta.url
|
|
1028
|
+
const pkgRoot = join(__dirname, '..');
|
|
1029
|
+
const hooksDir = join(pkgRoot, '.claude', 'hooks');
|
|
1030
|
+
|
|
1031
|
+
// Warn if running from npx (ephemeral path)
|
|
1032
|
+
if (pkgRoot.includes('.npm/_npx') || pkgRoot.includes('npx-')) {
|
|
1033
|
+
console.log(' Warning: Running from npx — paths will break after this session.');
|
|
1034
|
+
console.log(' Install globally first: npm i -g dual-brain');
|
|
1035
|
+
console.log(' Then run: dual-brain install --global');
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// Verify hooks exist at resolved path
|
|
1040
|
+
if (!existsSync(join(hooksDir, 'head-guard.mjs'))) {
|
|
1041
|
+
console.log(' Error: Could not resolve hook files at: ' + hooksDir);
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// Load existing settings (merge, never clobber)
|
|
1046
|
+
let existing = {};
|
|
1047
|
+
if (existsSync(globalSettingsPath)) {
|
|
1048
|
+
try { existing = JSON.parse(readFileSync(globalSettingsPath, 'utf8')); } catch {}
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
// Ensure hooks structure exists
|
|
1052
|
+
if (!existing.hooks) existing.hooks = {};
|
|
1053
|
+
if (!existing.hooks.PreToolUse) existing.hooks.PreToolUse = [];
|
|
1054
|
+
if (!existing.hooks.PostToolUse) existing.hooks.PostToolUse = [];
|
|
1055
|
+
|
|
1056
|
+
// Define dual-brain hooks with ownership marker
|
|
1057
|
+
const DB_MARKER = '# dual-brain-managed';
|
|
1058
|
+
const preToolHooks = [
|
|
1059
|
+
{ matcher: 'Edit', hooks: [{ type: 'command', command: `node ${join(hooksDir, 'head-guard.mjs')} ${DB_MARKER}` }] },
|
|
1060
|
+
{ matcher: 'Write', hooks: [{ type: 'command', command: `node ${join(hooksDir, 'head-guard.mjs')} ${DB_MARKER}` }] },
|
|
1061
|
+
{ matcher: 'NotebookEdit',hooks: [{ type: 'command', command: `node ${join(hooksDir, 'head-guard.mjs')} ${DB_MARKER}` }] },
|
|
1062
|
+
{ matcher: 'Bash', hooks: [{ type: 'command', command: `node ${join(hooksDir, 'head-guard.mjs')} ${DB_MARKER}` }] },
|
|
1063
|
+
{ matcher: 'Agent', hooks: [{ type: 'command', command: `node ${join(hooksDir, 'enforce-tier.mjs')} ${DB_MARKER}` }] },
|
|
1064
|
+
];
|
|
1065
|
+
const postToolHooks = [
|
|
1066
|
+
{ matcher: '', hooks: [{ type: 'command', command: `node ${join(hooksDir, 'cost-logger.mjs')} ${DB_MARKER}` }] },
|
|
1067
|
+
{ matcher: '', hooks: [{ type: 'command', command: `node ${join(hooksDir, 'auto-update-wrapper.mjs')} ${DB_MARKER}` }] },
|
|
1068
|
+
];
|
|
1069
|
+
|
|
1070
|
+
// Remove any existing dual-brain hooks (idempotent)
|
|
1071
|
+
const isDBHook = (entry) => entry.hooks?.some(h => h.command?.includes(DB_MARKER));
|
|
1072
|
+
existing.hooks.PreToolUse = existing.hooks.PreToolUse.filter(e => !isDBHook(e));
|
|
1073
|
+
existing.hooks.PostToolUse = existing.hooks.PostToolUse.filter(e => !isDBHook(e));
|
|
1074
|
+
|
|
1075
|
+
// Add dual-brain hooks
|
|
1076
|
+
existing.hooks.PreToolUse.push(...preToolHooks);
|
|
1077
|
+
existing.hooks.PostToolUse.push(...postToolHooks);
|
|
1078
|
+
|
|
1079
|
+
// Write merged settings
|
|
1080
|
+
mkdirSync(globalClaudeDir, { recursive: true });
|
|
1081
|
+
writeFileSync(globalSettingsPath, JSON.stringify(existing, null, 2) + '\n');
|
|
1082
|
+
|
|
1083
|
+
// Write minimal global CLAUDE.md (only if none exists, or append section)
|
|
1084
|
+
const globalClaudeMd = join(globalClaudeDir, 'CLAUDE.md');
|
|
1085
|
+
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`;
|
|
1086
|
+
|
|
1087
|
+
if (!existsSync(globalClaudeMd)) {
|
|
1088
|
+
writeFileSync(globalClaudeMd, dbSection);
|
|
1089
|
+
} else {
|
|
1090
|
+
const content = readFileSync(globalClaudeMd, 'utf8');
|
|
1091
|
+
if (!content.includes('Dual-Brain Global Hooks')) {
|
|
1092
|
+
writeFileSync(globalClaudeMd, content + '\n' + dbSection);
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
console.log(' + dual-brain hooks installed globally');
|
|
1097
|
+
console.log(' hooks dir: ' + hooksDir);
|
|
1098
|
+
console.log(' settings: ' + globalSettingsPath);
|
|
1099
|
+
console.log('');
|
|
1100
|
+
console.log(' All new Claude sessions will load dual-brain hooks.');
|
|
1101
|
+
console.log(' Run "dual-brain uninstall --global" to remove.');
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
async function uninstallGlobal() {
|
|
1105
|
+
const { homedir } = await import('node:os');
|
|
1106
|
+
const globalSettingsPath = join(homedir(), '.claude', 'settings.json');
|
|
1107
|
+
|
|
1108
|
+
if (!existsSync(globalSettingsPath)) {
|
|
1109
|
+
console.log(' No global settings found.');
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
let settings = {};
|
|
1114
|
+
try { settings = JSON.parse(readFileSync(globalSettingsPath, 'utf8')); } catch { return; }
|
|
1115
|
+
|
|
1116
|
+
const DB_MARKER = '# dual-brain-managed';
|
|
1117
|
+
const isDBHook = (entry) => entry.hooks?.some(h => h.command?.includes(DB_MARKER));
|
|
1118
|
+
|
|
1119
|
+
let removed = 0;
|
|
1120
|
+
if (settings.hooks?.PreToolUse) {
|
|
1121
|
+
const before = settings.hooks.PreToolUse.length;
|
|
1122
|
+
settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter(e => !isDBHook(e));
|
|
1123
|
+
removed += before - settings.hooks.PreToolUse.length;
|
|
1124
|
+
}
|
|
1125
|
+
if (settings.hooks?.PostToolUse) {
|
|
1126
|
+
const before = settings.hooks.PostToolUse.length;
|
|
1127
|
+
settings.hooks.PostToolUse = settings.hooks.PostToolUse.filter(e => !isDBHook(e));
|
|
1128
|
+
removed += before - settings.hooks.PostToolUse.length;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
// Clean up empty arrays/objects
|
|
1132
|
+
if (settings.hooks?.PreToolUse?.length === 0) delete settings.hooks.PreToolUse;
|
|
1133
|
+
if (settings.hooks?.PostToolUse?.length === 0) delete settings.hooks.PostToolUse;
|
|
1134
|
+
if (Object.keys(settings.hooks || {}).length === 0) delete settings.hooks;
|
|
1135
|
+
|
|
1136
|
+
writeFileSync(globalSettingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
1137
|
+
|
|
1138
|
+
// Remove dual-brain section from global CLAUDE.md
|
|
1139
|
+
const globalClaudeMd = join(homedir(), '.claude', 'CLAUDE.md');
|
|
1140
|
+
if (existsSync(globalClaudeMd)) {
|
|
1141
|
+
let content = readFileSync(globalClaudeMd, 'utf8');
|
|
1142
|
+
const dbSectionRegex = /\n## Dual-Brain Global Hooks\n[\s\S]*?Managed by: dual-brain install --global\n/;
|
|
1143
|
+
content = content.replace(dbSectionRegex, '');
|
|
1144
|
+
if (content.trim()) {
|
|
1145
|
+
writeFileSync(globalClaudeMd, content);
|
|
1146
|
+
} else {
|
|
1147
|
+
unlinkSync(globalClaudeMd);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
console.log(` - removed ${removed} dual-brain hook${removed === 1 ? '' : 's'} from global settings`);
|
|
1152
|
+
console.log(' Other settings preserved.');
|
|
1153
|
+
}
|
|
1154
|
+
|
|
994
1155
|
function cmdRemember(text) {
|
|
995
1156
|
if (!text) err('Usage: dual-brain remember "preference text"');
|
|
996
1157
|
const profile = rememberPreference(text, { scope: 'project', cwd: process.cwd() });
|
|
@@ -1050,6 +1211,182 @@ function cmdBreakGlass(reason) {
|
|
|
1050
1211
|
console.log('└' + '─'.repeat(inner) + '┘');
|
|
1051
1212
|
}
|
|
1052
1213
|
|
|
1214
|
+
// ─── PR command ───────────────────────────────────────────────────────────────
|
|
1215
|
+
|
|
1216
|
+
async function cmdPR(args) {
|
|
1217
|
+
const cwd = process.cwd();
|
|
1218
|
+
const sub = args[0] ?? '';
|
|
1219
|
+
|
|
1220
|
+
// Lazy import — only loaded when 'pr' is invoked
|
|
1221
|
+
let prAgent;
|
|
1222
|
+
try {
|
|
1223
|
+
prAgent = await import('../src/pr-agent.mjs');
|
|
1224
|
+
} catch (e) {
|
|
1225
|
+
console.error('pr-agent module not available:', e.message);
|
|
1226
|
+
process.exit(1);
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
const gh = prAgent.hasGitHub();
|
|
1230
|
+
if (!gh.available) {
|
|
1231
|
+
console.error('gh CLI not found. Install GitHub CLI: https://cli.github.com');
|
|
1232
|
+
process.exit(1);
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
// ── dual-brain pr (show current branch PR status) ──────────────────────────
|
|
1236
|
+
if (!sub || sub === 'status') {
|
|
1237
|
+
if (!gh.authenticated) {
|
|
1238
|
+
console.log('gh CLI is available but not authenticated. Run: gh auth login');
|
|
1239
|
+
return;
|
|
1240
|
+
}
|
|
1241
|
+
const info = prAgent.getBranchInfo(cwd);
|
|
1242
|
+
console.log('\n── PR status ─────────────────────────────────────────────\n');
|
|
1243
|
+
console.log(` Branch: ${info.branch ?? '(unknown)'}`);
|
|
1244
|
+
console.log(` Base: ${info.defaultBranch}`);
|
|
1245
|
+
console.log(` Ahead: ${info.ahead} commit(s)`);
|
|
1246
|
+
console.log(` Behind: ${info.behind} commit(s)`);
|
|
1247
|
+
|
|
1248
|
+
if (info.isDefault) {
|
|
1249
|
+
console.log('\n On default branch — create a feature branch first.');
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
// Check for an existing PR on this branch
|
|
1254
|
+
try {
|
|
1255
|
+
const { execSync: _exec } = await import('node:child_process');
|
|
1256
|
+
const json = _exec(`gh pr list --head "${info.branch}" --json number,title,state,url`, {
|
|
1257
|
+
cwd, encoding: 'utf8', timeout: 10000,
|
|
1258
|
+
});
|
|
1259
|
+
const prs = JSON.parse(json);
|
|
1260
|
+
if (prs.length > 0) {
|
|
1261
|
+
const pr = prs[0];
|
|
1262
|
+
console.log(`\n PR #${pr.number}: ${pr.title}`);
|
|
1263
|
+
console.log(` State: ${pr.state}`);
|
|
1264
|
+
console.log(` URL: ${pr.url}`);
|
|
1265
|
+
} else {
|
|
1266
|
+
console.log('\n No PR open for this branch.');
|
|
1267
|
+
console.log(` Create one: dual-brain pr create`);
|
|
1268
|
+
}
|
|
1269
|
+
} catch {
|
|
1270
|
+
console.log('\n (Could not check for existing PR — run: gh pr status)');
|
|
1271
|
+
}
|
|
1272
|
+
console.log('');
|
|
1273
|
+
return;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
// ── dual-brain pr list ───────────────────────────────────────────────────────
|
|
1277
|
+
if (sub === 'list') {
|
|
1278
|
+
if (!gh.authenticated) {
|
|
1279
|
+
console.log('gh CLI not authenticated. Run: gh auth login');
|
|
1280
|
+
return;
|
|
1281
|
+
}
|
|
1282
|
+
const state = args.includes('--closed') ? 'closed' : args.includes('--all') ? 'all' : 'open';
|
|
1283
|
+
const prs = prAgent.listPRs(cwd, { state, limit: 20 });
|
|
1284
|
+
if (prs.length === 0) {
|
|
1285
|
+
console.log(`No ${state} PRs found.`);
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1288
|
+
console.log(`\n── ${state} PRs ──────────────────────────────────────────────\n`);
|
|
1289
|
+
for (const pr of prs) {
|
|
1290
|
+
const draft = pr.isDraft ? ' [draft]' : '';
|
|
1291
|
+
const date = pr.createdAt ? new Date(pr.createdAt).toLocaleDateString() : '';
|
|
1292
|
+
console.log(` #${pr.number} ${pr.title}${draft}`);
|
|
1293
|
+
console.log(` ${pr.headRefName} by ${pr.author?.login ?? '?'} ${date}`);
|
|
1294
|
+
}
|
|
1295
|
+
console.log('');
|
|
1296
|
+
return;
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
// ── dual-brain pr view <N> ───────────────────────────────────────────────────
|
|
1300
|
+
if (sub === 'view') {
|
|
1301
|
+
const prNum = args[1];
|
|
1302
|
+
if (!prNum || isNaN(Number(prNum))) {
|
|
1303
|
+
console.error('Usage: dual-brain pr view <PR-number>');
|
|
1304
|
+
process.exit(1);
|
|
1305
|
+
}
|
|
1306
|
+
const details = prAgent.getPRDetails(prNum, cwd);
|
|
1307
|
+
if (!details) {
|
|
1308
|
+
console.error(`PR #${prNum} not found or gh CLI error.`);
|
|
1309
|
+
process.exit(1);
|
|
1310
|
+
}
|
|
1311
|
+
console.log(`\n── PR #${prNum}: ${details.title} ─────────────────────────────\n`);
|
|
1312
|
+
console.log(` State: ${details.state}`);
|
|
1313
|
+
console.log(` Branch: ${details.headRefName} → ${details.baseRefName}`);
|
|
1314
|
+
console.log(` Changes: +${details.additions} -${details.deletions} (${details.changedFiles} files)`);
|
|
1315
|
+
if (details.statusCheckRollup?.length) {
|
|
1316
|
+
const passing = details.statusCheckRollup.filter(c => c.conclusion === 'SUCCESS').length;
|
|
1317
|
+
const total = details.statusCheckRollup.length;
|
|
1318
|
+
console.log(` Checks: ${passing}/${total} passing`);
|
|
1319
|
+
}
|
|
1320
|
+
if (details.body) {
|
|
1321
|
+
console.log('\n Body:\n');
|
|
1322
|
+
console.log(details.body.split('\n').map(l => ` ${l}`).join('\n').slice(0, 1500));
|
|
1323
|
+
}
|
|
1324
|
+
console.log('');
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
// ── dual-brain pr create ─────────────────────────────────────────────────────
|
|
1329
|
+
if (sub === 'create') {
|
|
1330
|
+
if (!gh.authenticated) {
|
|
1331
|
+
console.log('gh CLI not authenticated. Run: gh auth login');
|
|
1332
|
+
return;
|
|
1333
|
+
}
|
|
1334
|
+
const info = prAgent.getBranchInfo(cwd);
|
|
1335
|
+
if (info.isDefault || !info.branch) {
|
|
1336
|
+
console.error('You are on the default branch. Switch to a feature branch before creating a PR.');
|
|
1337
|
+
process.exit(1);
|
|
1338
|
+
}
|
|
1339
|
+
if (info.ahead === 0) {
|
|
1340
|
+
console.error('No commits ahead of the base branch. Make changes and commit first.');
|
|
1341
|
+
process.exit(1);
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
const diff = prAgent.getDiffSummary(info.defaultBranch, cwd);
|
|
1345
|
+
const draft = args.includes('--draft');
|
|
1346
|
+
|
|
1347
|
+
// Auto-generate a title from the branch name
|
|
1348
|
+
const rawTitle = info.branch.replace(/^db\//, '').replace(/-/g, ' ');
|
|
1349
|
+
const title = rawTitle.charAt(0).toUpperCase() + rawTitle.slice(1);
|
|
1350
|
+
|
|
1351
|
+
// Auto-generate body from diff
|
|
1352
|
+
const body = prAgent.buildPRBody(title, {
|
|
1353
|
+
filesChanged: diff.files,
|
|
1354
|
+
});
|
|
1355
|
+
|
|
1356
|
+
console.log(`\n── Creating PR from ${info.branch} → ${info.defaultBranch} ────────────\n`);
|
|
1357
|
+
console.log(` Title: ${title}`);
|
|
1358
|
+
console.log(` Files: ${diff.fileCount} changed`);
|
|
1359
|
+
if (diff.summary) console.log(` Diff: ${diff.summary}`);
|
|
1360
|
+
if (draft) console.log(' Mode: draft');
|
|
1361
|
+
console.log('');
|
|
1362
|
+
|
|
1363
|
+
const result = prAgent.createPR({
|
|
1364
|
+
title,
|
|
1365
|
+
body,
|
|
1366
|
+
baseBranch: info.defaultBranch,
|
|
1367
|
+
draft,
|
|
1368
|
+
cwd,
|
|
1369
|
+
});
|
|
1370
|
+
|
|
1371
|
+
if (result.success) {
|
|
1372
|
+
console.log(` PR created: ${result.url}`);
|
|
1373
|
+
} else {
|
|
1374
|
+
console.error(` Failed to create PR: ${result.error}`);
|
|
1375
|
+
process.exit(1);
|
|
1376
|
+
}
|
|
1377
|
+
console.log('');
|
|
1378
|
+
return;
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
// Unknown sub-subcommand
|
|
1382
|
+
console.log(`Unknown pr subcommand: "${sub}"`);
|
|
1383
|
+
console.log('Usage:');
|
|
1384
|
+
console.log(' dual-brain pr Show PR status for current branch');
|
|
1385
|
+
console.log(' dual-brain pr create Create PR from current branch');
|
|
1386
|
+
console.log(' dual-brain pr list List open PRs');
|
|
1387
|
+
console.log(' dual-brain pr view <N> View PR details');
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1053
1390
|
// ─── Screen helpers ───────────────────────────────────────────────────────────
|
|
1054
1391
|
|
|
1055
1392
|
/**
|
|
@@ -5640,7 +5977,16 @@ async function main() {
|
|
|
5640
5977
|
}
|
|
5641
5978
|
|
|
5642
5979
|
// One-shot commands — run and exit
|
|
5643
|
-
if (cmd === 'install')
|
|
5980
|
+
if (cmd === 'install') {
|
|
5981
|
+
if (args.includes('--global')) { await installGlobal(); return; }
|
|
5982
|
+
await cmdInstall();
|
|
5983
|
+
return;
|
|
5984
|
+
}
|
|
5985
|
+
if (cmd === 'uninstall') {
|
|
5986
|
+
if (args.includes('--global')) { await uninstallGlobal(); return; }
|
|
5987
|
+
console.log('Usage: dual-brain uninstall --global');
|
|
5988
|
+
return;
|
|
5989
|
+
}
|
|
5644
5990
|
if (cmd === 'auth') {
|
|
5645
5991
|
await cmdAuth(args.slice(1));
|
|
5646
5992
|
return;
|
|
@@ -5651,6 +5997,7 @@ async function main() {
|
|
|
5651
5997
|
if (cmd === 'think') { await cmdThink(args.slice(1)); return; }
|
|
5652
5998
|
if (cmd === 'review') { await cmdReview(args.slice(1)); return; }
|
|
5653
5999
|
if (cmd === 'ship') { await cmdShip(); return; }
|
|
6000
|
+
if (cmd === 'pr') { await cmdPR(args.slice(1)); return; }
|
|
5654
6001
|
if (cmd === 'status') { await cmdStatus(args.slice(1)); return; }
|
|
5655
6002
|
if (cmd === 'hot') { cmdHot(args[1]); return; }
|
|
5656
6003
|
if (cmd === 'cool') { cmdCool(args[1]); return; }
|
|
@@ -5715,7 +6062,7 @@ fi
|
|
|
5715
6062
|
// If cmd is not a recognized subcommand, treat the entire arg list as a task.
|
|
5716
6063
|
// e.g. `dual-brain fix failing tests` → same as `dual-brain go "fix failing tests"`
|
|
5717
6064
|
const KNOWN_COMMANDS = new Set([
|
|
5718
|
-
'init', 'install', 'auth', 'go', 'do', 'plan', 'ship', 'think', 'review', 'status', 'hot', 'cool',
|
|
6065
|
+
'init', 'install', 'uninstall', 'auth', 'go', 'do', 'plan', 'ship', 'think', 'review', 'pr', 'status', 'hot', 'cool',
|
|
5719
6066
|
'remember', 'forget', 'break-glass', 'specialists', 'search', 'shell-hook', 'watch',
|
|
5720
6067
|
'--help', '-h', '--version', '-v',
|
|
5721
6068
|
...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.5",
|
|
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
|
@@ -358,3 +358,39 @@ export function invalidateCache() {
|
|
|
358
358
|
_cache = null;
|
|
359
359
|
_cacheTime = 0;
|
|
360
360
|
}
|
|
361
|
+
|
|
362
|
+
// ─── Ambiguity Detection ──────────────────────────────────────────────────────
|
|
363
|
+
|
|
364
|
+
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;
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Detect whether a prompt is ambiguous and needs clarification before dispatch.
|
|
368
|
+
*
|
|
369
|
+
* A prompt is considered ambiguous when ALL of the following are true:
|
|
370
|
+
* 1. It is very short (under 4 words)
|
|
371
|
+
* 2. No file context is provided
|
|
372
|
+
* 3. It lacks specific technical terms that narrow the intent
|
|
373
|
+
*
|
|
374
|
+
* @param {string} prompt — the user's raw prompt
|
|
375
|
+
* @param {{ files?: string[] }} [context] — optional context (e.g. file paths)
|
|
376
|
+
* @returns {{ isAmbiguous: boolean, reason: string|null }}
|
|
377
|
+
*/
|
|
378
|
+
export function detectAmbiguity(prompt, context = {}) {
|
|
379
|
+
if (!prompt || typeof prompt !== 'string') {
|
|
380
|
+
return { isAmbiguous: true, reason: 'missing context: empty prompt' };
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const words = prompt.trim().split(/\s+/).filter(Boolean);
|
|
384
|
+
const isTooShort = words.length < 4;
|
|
385
|
+
const hasFileContext = Array.isArray(context?.files) && context.files.length > 0;
|
|
386
|
+
const hasTechnicalTerms = TECHNICAL_TERMS.test(prompt);
|
|
387
|
+
|
|
388
|
+
if (isTooShort && !hasFileContext && !hasTechnicalTerms) {
|
|
389
|
+
return {
|
|
390
|
+
isAmbiguous: true,
|
|
391
|
+
reason: `unclear: prompt is vague ("${prompt.trim()}") — missing context about what to change and where`,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return { isAmbiguous: false, reason: null };
|
|
396
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// checkpoint.mjs — Checkpoint wrapper for dual-brain execution safety.
|
|
2
|
+
// Wraps Replit's native checkpoint system with a git-based fallback.
|
|
3
|
+
// Exports: hasCheckpoints, createCheckpoint, listCheckpoints, getLastCheckpoint
|
|
4
|
+
|
|
5
|
+
import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { execSync } from 'node:child_process';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Check if checkpoint capability is available.
|
|
11
|
+
* @returns {boolean}
|
|
12
|
+
*/
|
|
13
|
+
export function hasCheckpoints() {
|
|
14
|
+
try {
|
|
15
|
+
// Check for Replit checkpoint binary
|
|
16
|
+
if (existsSync('/usr/local/bin/replit-checkpoint')) return true;
|
|
17
|
+
execSync('which replit-checkpoint', { stdio: 'pipe', timeout: 2000 });
|
|
18
|
+
return true;
|
|
19
|
+
} catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Create a checkpoint before a risky operation.
|
|
26
|
+
* @param {string} label — human-readable label like "before auth refactor"
|
|
27
|
+
* @param {object} [opts]
|
|
28
|
+
* @param {string} [opts.cwd]
|
|
29
|
+
* @returns {{ success: boolean, id: string|null, label: string, timestamp: string }}
|
|
30
|
+
*/
|
|
31
|
+
export function createCheckpoint(label, opts = {}) {
|
|
32
|
+
const cwd = opts.cwd || process.cwd();
|
|
33
|
+
const timestamp = new Date().toISOString();
|
|
34
|
+
const id = `cp-${Date.now()}`;
|
|
35
|
+
|
|
36
|
+
// Try Replit checkpoint first
|
|
37
|
+
if (hasCheckpoints()) {
|
|
38
|
+
try {
|
|
39
|
+
execSync('replit-checkpoint create', { cwd, stdio: 'pipe', timeout: 10000 });
|
|
40
|
+
_logCheckpoint({ id, label, timestamp, type: 'replit', status: 'created' }, cwd);
|
|
41
|
+
return { success: true, id, label, timestamp };
|
|
42
|
+
} catch {
|
|
43
|
+
// Fall through to git-based checkpoint
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Fallback: git stash + tag
|
|
48
|
+
try {
|
|
49
|
+
// Stash any uncommitted changes
|
|
50
|
+
const status = execSync('git status --porcelain', { cwd, encoding: 'utf8', timeout: 5000 }).trim();
|
|
51
|
+
if (status) {
|
|
52
|
+
execSync(`git stash push -m "dual-brain-checkpoint: ${label}"`, { cwd, stdio: 'pipe', timeout: 10000 });
|
|
53
|
+
execSync('git stash pop', { cwd, stdio: 'pipe', timeout: 10000 });
|
|
54
|
+
}
|
|
55
|
+
// Create a lightweight tag
|
|
56
|
+
const safeLabel = label.replace(/[^a-zA-Z0-9-_]/g, '-').slice(0, 50);
|
|
57
|
+
const tagName = `db-checkpoint/${safeLabel}-${Date.now()}`;
|
|
58
|
+
execSync(`git tag "${tagName}"`, { cwd, stdio: 'pipe', timeout: 5000 });
|
|
59
|
+
_logCheckpoint({ id, label, timestamp, type: 'git-tag', tag: tagName, status: 'created' }, cwd);
|
|
60
|
+
return { success: true, id, label, timestamp };
|
|
61
|
+
} catch {
|
|
62
|
+
_logCheckpoint({ id, label, timestamp, type: 'failed', status: 'failed' }, cwd);
|
|
63
|
+
return { success: false, id: null, label, timestamp };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* List recent checkpoints (most recent first, up to 20).
|
|
69
|
+
* @param {string} [cwd]
|
|
70
|
+
* @returns {object[]}
|
|
71
|
+
*/
|
|
72
|
+
export function listCheckpoints(cwd) {
|
|
73
|
+
const logPath = join(cwd || process.cwd(), '.dual-brain', 'checkpoints.jsonl');
|
|
74
|
+
if (!existsSync(logPath)) return [];
|
|
75
|
+
try {
|
|
76
|
+
return readFileSync(logPath, 'utf8')
|
|
77
|
+
.trim()
|
|
78
|
+
.split('\n')
|
|
79
|
+
.filter(Boolean)
|
|
80
|
+
.map(line => JSON.parse(line))
|
|
81
|
+
.reverse()
|
|
82
|
+
.slice(0, 20);
|
|
83
|
+
} catch {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Get the most recent checkpoint.
|
|
90
|
+
* @param {string} [cwd]
|
|
91
|
+
* @returns {object|null}
|
|
92
|
+
*/
|
|
93
|
+
export function getLastCheckpoint(cwd) {
|
|
94
|
+
const checkpoints = listCheckpoints(cwd);
|
|
95
|
+
return checkpoints[0] || null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ─── Internal helpers ─────────────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
function _logCheckpoint(entry, cwd) {
|
|
101
|
+
const dir = join(cwd || process.cwd(), '.dual-brain');
|
|
102
|
+
mkdirSync(dir, { recursive: true });
|
|
103
|
+
const logPath = join(dir, 'checkpoints.jsonl');
|
|
104
|
+
const line = JSON.stringify(entry) + '\n';
|
|
105
|
+
try {
|
|
106
|
+
const existing = existsSync(logPath) ? readFileSync(logPath, 'utf8') : '';
|
|
107
|
+
writeFileSync(logPath, existing + line);
|
|
108
|
+
} catch {}
|
|
109
|
+
}
|