dual-brain 3.7.0 → 3.7.1

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.
@@ -8,6 +8,7 @@
8
8
  * Output contract: must print "{}" to stdout and exit 0 within ~100 ms.
9
9
  */
10
10
 
11
+ import { createHash } from "crypto";
11
12
  import { appendFileSync, mkdirSync, readFileSync, writeFileSync } from "fs";
12
13
  import { dirname, join } from "path";
13
14
  import { fileURLToPath } from "url";
@@ -25,8 +26,8 @@ mkdirSync(__dirname, { recursive: true });
25
26
  function loadActiveProfile() {
26
27
  try {
27
28
  const data = JSON.parse(readFileSync(PROFILE_FILE, 'utf8'));
28
- return data.active || 'balanced';
29
- } catch { return 'balanced'; }
29
+ return data.active || 'auto';
30
+ } catch { return 'auto'; }
30
31
  }
31
32
 
32
33
  const SESSION_ID = process.env.CLAUDE_SESSION_ID || process.ppid?.toString() || null;
@@ -261,6 +262,15 @@ async function main() {
261
262
  updateSummary(entryObj);
262
263
  } catch {}
263
264
 
265
+ // Record failures for adaptive routing (failure-loop detection)
266
+ if (status === 'error' && toolName === 'Agent') {
267
+ try {
268
+ const { recordFailure } = await import('./failure-detector.mjs');
269
+ const promptHash = createHash('md5').update(JSON.stringify(toolInput)).digest('hex').slice(0, 12);
270
+ recordFailure(promptHash, tier, payload?.error || 'agent_error');
271
+ } catch {}
272
+ }
273
+
264
274
  const budgetMsg = await checkBudget();
265
275
 
266
276
  // PostToolUse hooks must emit a JSON object to stdout
@@ -135,7 +135,7 @@ function hasIssues(text) {
135
135
  if (hasIssueIndicators) return true;
136
136
 
137
137
  // No concrete issues — check if review explicitly says it's clean
138
- const good = ['lgtm', 'looks good', 'no issues', 'no problems', 'no concerns', 'all good', 'clean'];
138
+ const good = ['lgtm', 'looks good', 'no issues', 'no problems', 'no concerns', 'all good', 'clean', 'approved', 'ship it', 'ready to merge', 'good to go', 'looks fine', 'no blockers'];
139
139
  if (good.some(g => lower.includes(g))) return false;
140
140
 
141
141
  // Ambiguous — default to flagging for human review
@@ -4,7 +4,7 @@ import { createHash } from 'crypto';
4
4
  import { dirname, resolve, join } from 'path';
5
5
  import { fileURLToPath } from 'url';
6
6
  import { classifyRisk, extractPaths } from './risk-classifier.mjs';
7
- import { checkFailureLoop } from './failure-detector.mjs';
7
+ import { checkFailureLoop, recordFailure } from './failure-detector.mjs';
8
8
 
9
9
  const __dirname = dirname(fileURLToPath(import.meta.url));
10
10
  const CONFIG_FILE = resolve(__dirname, '..', 'orchestrator.json');
@@ -28,7 +28,7 @@ function checkFailureLoop(promptHash) {
28
28
  const entry = JSON.parse(line);
29
29
  if (entry.prompt_hash !== promptHash) continue;
30
30
  if (Date.parse(entry.timestamp) < twoHoursAgo) continue;
31
- if (entry.success === false || entry.followed === false) {
31
+ if (entry.success === false) {
32
32
  failures++;
33
33
  lastTier = entry.tier;
34
34
  }
@@ -21,9 +21,10 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
21
21
  import { dirname, extname, join, resolve } from 'path';
22
22
  import { fileURLToPath } from 'url';
23
23
 
24
+ import { getProfileOverrides as _getProfileOverrides } from './profiles.mjs';
25
+
24
26
  const __dirname = dirname(fileURLToPath(import.meta.url));
25
27
  const ORCHESTRATOR_CONFIG = resolve(__dirname, '..', 'orchestrator.json');
26
- const PROFILE_FILE = resolve(__dirname, '..', 'dual-brain.profile.json');
27
28
  const REVIEWS_DIR = resolve(__dirname, '..', 'reviews');
28
29
  const DUAL_BRAIN = resolve(__dirname, 'dual-brain-review.mjs');
29
30
 
@@ -31,14 +32,7 @@ const RISK_LEVELS = ['low', 'medium', 'high', 'critical'];
31
32
 
32
33
  function loadProfileGateSettings() {
33
34
  try {
34
- const data = JSON.parse(readFileSync(PROFILE_FILE, 'utf8'));
35
- const name = data.active || 'balanced';
36
- const defaults = {
37
- balanced: { sensitivity_floor: 'medium', dual_brain_minimum: 'high' },
38
- 'cost-saver': { sensitivity_floor: 'high', dual_brain_minimum: 'critical' },
39
- 'quality-first': { sensitivity_floor: 'low', dual_brain_minimum: 'medium' },
40
- };
41
- return defaults[name] || defaults.balanced;
35
+ return _getProfileOverrides('quality-gate');
42
36
  } catch {
43
37
  return { sensitivity_floor: 'medium', dual_brain_minimum: 'high' };
44
38
  }
@@ -310,6 +310,33 @@ test('orchestrator.json: dual_thinking configured', () => {
310
310
  return true;
311
311
  });
312
312
 
313
+ // ─── Test 15: profile consistency across modules ────────────────────────────
314
+ test('profiles: consistent across modules', () => {
315
+ const profilesSrc = readFileSync(resolve(__dirname, 'profiles.mjs'), 'utf8');
316
+ const profileNames = ['auto', 'balanced', 'cost-saver', 'quality-first'];
317
+ for (const name of profileNames) {
318
+ if (!profilesSrc.includes(`${name}:`) && !profilesSrc.includes(`'${name}':`)) return `profiles.mjs missing: ${name}`;
319
+ }
320
+
321
+ const installSrc = readFileSync(resolve(__dirname, '..', 'install.mjs'), 'utf8');
322
+ for (const name of profileNames) {
323
+ if (!installSrc.includes(`${name}:`) && !installSrc.includes(`'${name}':`)) return `install.mjs missing profile: ${name}`;
324
+ }
325
+
326
+ const enforceSrc = readFileSync(resolve(__dirname, 'enforce-tier.mjs'), 'utf8');
327
+ if (!enforceSrc.includes('auto:')) return 'enforce-tier.mjs missing auto in PROFILE_SETTINGS';
328
+
329
+ return true;
330
+ });
331
+
332
+ // ─── Test 16: failure-detector only counts real failures ─────────────────────
333
+ test('failure-detector: ignores followed=false', () => {
334
+ const src = readFileSync(resolve(__dirname, 'failure-detector.mjs'), 'utf8');
335
+ if (src.includes('followed === false')) return 'still conflates followed=false with failure';
336
+ if (!src.includes('success === false')) return 'missing success===false check';
337
+ return true;
338
+ });
339
+
313
340
  // ─── Summary ─────────────────────────────────────────────────────────────────
314
341
  const total = passed + failed;
315
342
  console.log(`\n${passed}/${total} tests passed`);
package/install.mjs CHANGED
@@ -424,6 +424,12 @@ function profilePath(workspace) {
424
424
  }
425
425
 
426
426
  const PROFILES = {
427
+ auto: {
428
+ description: 'Adapts routing based on task risk, provider health, and outcomes',
429
+ routing: { prefer_provider: 'auto', think_threshold: 'adaptive', gpt_dispatch_bias: 0 },
430
+ budgets: { session_warn_usd: 5, session_limit_usd: 10, daily_warn_usd: 20, daily_limit_usd: 50 },
431
+ quality_gate: { sensitivity_floor: 'medium', dual_brain_minimum: 'high' },
432
+ },
427
433
  balanced: {
428
434
  description: 'Auto-routes by complexity, uses both providers evenly',
429
435
  routing: { prefer_provider: 'auto', think_threshold: 'normal', gpt_dispatch_bias: 0 },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dual-brain",
3
- "version": "3.7.0",
3
+ "version": "3.7.1",
4
4
  "description": "Dual-provider orchestration for Claude Code — tiered routing, budget balancing, and GPT dual-brain review across Claude + OpenAI subscriptions",
5
5
  "type": "module",
6
6
  "bin": {