dual-brain 3.0.1 → 3.2.0

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,13 +1,27 @@
1
1
  #!/usr/bin/env node
2
- import { readFileSync, writeFileSync, appendFileSync } from 'fs';
2
+ import { readFileSync, writeFileSync, appendFileSync, renameSync } from 'fs';
3
3
  import { createHash } from 'crypto';
4
4
  import { dirname, resolve, join } from 'path';
5
5
  import { fileURLToPath } from 'url';
6
6
 
7
7
  const __dirname = dirname(fileURLToPath(import.meta.url));
8
8
  const CONFIG_FILE = resolve(__dirname, '..', 'orchestrator.json');
9
+ const PROFILE_FILE = resolve(__dirname, '..', 'dual-brain.profile.json');
9
10
  const DRIFT_STATE = resolve(__dirname, '.drift-warned');
10
11
 
12
+ function loadProfile() {
13
+ try {
14
+ const data = JSON.parse(readFileSync(PROFILE_FILE, 'utf8'));
15
+ return data.active || 'balanced';
16
+ } catch { return 'balanced'; }
17
+ }
18
+
19
+ const PROFILE_SETTINGS = {
20
+ balanced: { demote_think: false, promote_execute: false, bias: 0 },
21
+ 'cost-saver': { demote_think: true, promote_execute: false, bias: -20 },
22
+ 'quality-first': { demote_think: false, promote_execute: true, bias: 10 },
23
+ };
24
+
11
25
  function checkPricingDrift(config) {
12
26
  const verified = config.pricing_verified;
13
27
  if (!verified) return null;
@@ -29,9 +43,12 @@ function checkPricingDrift(config) {
29
43
  return `**[Drift Warning]** Pricing was last verified ${age} days ago. Run \`node .claude/hooks/setup-wizard.mjs\` to update.`;
30
44
  }
31
45
 
46
+ const SESSION_ID = process.env.CLAUDE_SESSION_ID || process.ppid?.toString() || null;
47
+
32
48
  function logRecommendation(event) {
33
49
  const logFile = join(__dirname, `usage-${new Date().toISOString().slice(0, 10)}.jsonl`);
34
- const entry = JSON.stringify({
50
+ const profileName = event.profile || 'balanced';
51
+ const entryObj = {
35
52
  timestamp: new Date().toISOString(),
36
53
  type: 'tier_recommendation',
37
54
  detected_tier: event.tier,
@@ -39,13 +56,64 @@ function logRecommendation(event) {
39
56
  actual_model: event.actual,
40
57
  prompt_hash: event.promptHash,
41
58
  followed: event.followed,
42
- });
59
+ session_id: SESSION_ID,
60
+ profile: profileName,
61
+ };
62
+ const entry = JSON.stringify(entryObj);
43
63
  try {
44
64
  appendFileSync(logFile, entry + '\n');
45
65
  } catch {}
66
+
67
+ // Sync summary update (for dupe detection on next call)
68
+ try {
69
+ const today = new Date().toISOString().slice(0, 10);
70
+ const summaryFile = join(__dirname, `usage-summary-${today}.json`);
71
+ let summary;
72
+ try { summary = JSON.parse(readFileSync(summaryFile, 'utf8')); } catch { summary = { version: 1, recent_hashes: [] }; }
73
+ if (event.promptHash) {
74
+ summary.recent_hashes = summary.recent_hashes || [];
75
+ summary.recent_hashes.push({ hash: event.promptHash, ts: entryObj.timestamp });
76
+ const tenMinAgo = Date.now() - 10 * 60 * 1000;
77
+ summary.recent_hashes = summary.recent_hashes.filter(h => Date.parse(h.ts) >= tenMinAgo);
78
+ }
79
+ summary.updated_at = new Date().toISOString();
80
+ const tmp = summaryFile + '.tmp.' + process.pid;
81
+ writeFileSync(tmp, JSON.stringify(summary, null, 2) + '\n');
82
+ renameSync(tmp, summaryFile);
83
+ } catch {}
84
+
85
+ // Sync ledger write (append-only, fast)
86
+ try {
87
+ const ledgerEntry = JSON.stringify({
88
+ type: 'decision',
89
+ id: entryObj.timestamp.replace(/\W/g, '').slice(-12),
90
+ timestamp: entryObj.timestamp,
91
+ session_id: SESSION_ID,
92
+ profile: profileName,
93
+ tier: event.tier,
94
+ provider: detectProvider(event.actual),
95
+ model: event.actual || 'unknown',
96
+ recommended_model: event.recommended,
97
+ followed: event.followed,
98
+ prompt_hash: event.promptHash,
99
+ });
100
+ appendFileSync(join(__dirname, 'decision-ledger.jsonl'), ledgerEntry + '\n');
101
+ } catch {}
46
102
  }
47
103
 
48
104
  function checkDuplicate(promptHash) {
105
+ // Try summary checkpoint first (O(1))
106
+ try {
107
+ const summaryPath = join(__dirname, `usage-summary-${new Date().toISOString().slice(0, 10)}.json`);
108
+ const summary = JSON.parse(readFileSync(summaryPath, 'utf8'));
109
+ const tenMinAgo = Date.now() - 10 * 60 * 1000;
110
+ const match = (summary.recent_hashes || []).find(
111
+ h => h.hash === promptHash && Date.parse(h.ts) >= tenMinAgo
112
+ );
113
+ if (match) return { timestamp: match.ts, prompt_hash: promptHash };
114
+ } catch {}
115
+
116
+ // Fallback: scan log
49
117
  const logFile = join(__dirname, `usage-${new Date().toISOString().slice(0, 10)}.jsonl`);
50
118
  try {
51
119
  const lines = readFileSync(logFile, 'utf8').split('\n').filter(Boolean);
@@ -73,27 +141,34 @@ function detectProvider(model) {
73
141
  }
74
142
 
75
143
  function quickPressureCheck(tier) {
144
+ // Try summary checkpoint first (O(1))
145
+ try {
146
+ const today = new Date().toISOString().slice(0, 10);
147
+ const summaryPath = join(__dirname, `usage-summary-${today}.json`);
148
+ const summary = JSON.parse(readFileSync(summaryPath, 'utf8'));
149
+ const cutoff = Date.now() - 5 * 60 * 60 * 1000;
150
+ const claudeTs = (summary.pressure?.claude?.[tier] || []).filter(t => Date.parse(t) >= cutoff);
151
+ const openaiTs = (summary.pressure?.openai?.[tier] || []).filter(t => Date.parse(t) >= cutoff);
152
+ return { claudeCalls: claudeTs.length, openaiCalls: openaiTs.length };
153
+ } catch {}
154
+
155
+ // Fallback: scan log
76
156
  try {
77
157
  const today = new Date().toISOString().slice(0, 10);
78
158
  const logFile = join(__dirname, `usage-${today}.jsonl`);
79
159
  const lines = readFileSync(logFile, 'utf8').split('\n').filter(Boolean);
80
-
81
160
  const fiveHoursAgo = Date.now() - 5 * 60 * 60 * 1000;
82
161
  let claudeCalls = 0, openaiCalls = 0;
83
-
84
162
  for (const line of lines) {
85
163
  try {
86
164
  const entry = JSON.parse(line);
87
165
  if (Date.parse(entry.timestamp) < fiveHoursAgo) continue;
88
166
  if (entry.tier !== tier) continue;
89
-
90
- const provider = entry.provider ||
91
- (entry.model?.includes('gpt') ? 'openai' : 'claude');
167
+ const provider = entry.provider || (entry.model?.includes('gpt') ? 'openai' : 'claude');
92
168
  if (provider === 'claude') claudeCalls++;
93
169
  else openaiCalls++;
94
170
  } catch {}
95
171
  }
96
-
97
172
  return { claudeCalls, openaiCalls };
98
173
  } catch {
99
174
  return null;
@@ -162,6 +237,10 @@ try {
162
237
  return parts.join('\n\n');
163
238
  };
164
239
 
240
+ // Load profile early so all log entries can reference it
241
+ const profileName = loadProfile();
242
+ const profileSettings = PROFILE_SETTINGS[profileName] || PROFILE_SETTINGS.balanced;
243
+
165
244
  // Multi-tier detection — only when tier is not already resolved from subagent_defaults
166
245
  if (!tier) {
167
246
  const hasThink = THINK_WORDS.test(text);
@@ -186,6 +265,7 @@ try {
186
265
  actual: currentModel,
187
266
  promptHash,
188
267
  followed: false,
268
+ profile: profileName,
189
269
  });
190
270
  process.stdout.write(JSON.stringify({ systemMessage: fullMsg }));
191
271
  process.exit(0);
@@ -197,12 +277,21 @@ try {
197
277
  else tier = 'execute';
198
278
  }
199
279
 
280
+ // Apply profile-driven tier adjustments
281
+ if (profileSettings.demote_think && tier === 'think' && !THINK_WORDS.test(text)) {
282
+ tier = 'execute';
283
+ }
284
+ if (profileSettings.promote_execute && tier === 'execute' && THINK_WORDS.test(text)) {
285
+ tier = 'think';
286
+ }
287
+
200
288
  // Compute balance hint now that tier is resolved
201
289
  {
202
290
  const currentProvider = detectProvider(currentModel);
203
291
  if (currentProvider === 'claude') {
204
292
  const balance = quickPressureCheck(tier);
205
- if (balance && balance.claudeCalls > balance.openaiCalls * 2 && balance.claudeCalls > 10) {
293
+ const biasThreshold = profileSettings.bias >= 0 ? 10 : 20;
294
+ if (balance && balance.claudeCalls > balance.openaiCalls * 2 && balance.claudeCalls > biasThreshold) {
206
295
  const dispatchModel = tier === 'think' ? 'gpt-5.5' : tier === 'execute' ? 'gpt-5.4' : 'gpt-4.1-mini';
207
296
  balanceHint = `\n\n💡 **Balance tip:** Claude has ${balance.claudeCalls} ${tier} calls vs OpenAI's ${balance.openaiCalls} in the last 5hrs. Consider dispatching isolated work to GPT: \`node .claude/hooks/gpt-work-dispatcher.mjs --task "..." --model ${dispatchModel}\``;
208
297
  }
@@ -221,6 +310,7 @@ try {
221
310
  actual: currentModel,
222
311
  promptHash,
223
312
  followed: true,
313
+ profile: profileName,
224
314
  });
225
315
  const onlyWarnings = [duplicateWarning, driftWarning, balanceHint].filter(Boolean).join('\n\n');
226
316
  if (onlyWarnings) {
@@ -241,6 +331,7 @@ try {
241
331
  actual: currentModel,
242
332
  promptHash,
243
333
  followed: false,
334
+ profile: profileName,
244
335
  });
245
336
  process.stdout.write(JSON.stringify({ systemMessage: prependWarnings(msg) }));
246
337
  } else {
@@ -251,6 +342,7 @@ try {
251
342
  actual: currentModel,
252
343
  promptHash,
253
344
  followed: true,
345
+ profile: profileName,
254
346
  });
255
347
  const onlyWarnings = [duplicateWarning, driftWarning, balanceHint].filter(Boolean).join('\n\n');
256
348
  if (onlyWarnings) {
@@ -271,6 +363,7 @@ try {
271
363
  actual: currentModel,
272
364
  promptHash,
273
365
  followed: false,
366
+ profile: profileName,
274
367
  });
275
368
  process.stdout.write(JSON.stringify({ systemMessage: prependWarnings(msg) }));
276
369
  }
@@ -18,7 +18,7 @@
18
18
  */
19
19
 
20
20
  import { execSync, spawnSync } from 'child_process';
21
- import { appendFileSync } from 'fs';
21
+ import { appendFileSync, readFileSync } from 'fs';
22
22
  import { dirname, join } from 'path';
23
23
  import { fileURLToPath } from 'url';
24
24
 
@@ -117,10 +117,19 @@ function executeCodex(codexBin, model, prompt, cwd, timeoutMs) {
117
117
  .filter(m => m.type === 'item.completed' && m.item?.type === 'command_execution')
118
118
  .map(m => m.item);
119
119
 
120
+ // Estimate startup time: time to first agent message or completed item
121
+ const firstItemTs = messages.find(m => m.type === 'item.completed')?.timestamp;
122
+ let startupMs = null;
123
+ if (firstItemTs) {
124
+ startupMs = Date.parse(firstItemTs) - startTime;
125
+ if (startupMs < 0 || startupMs > durationMs) startupMs = null;
126
+ }
127
+
120
128
  return {
121
129
  success: proc.status === 0 && errors.length === 0,
122
130
  summary: agentMessages.join('\n\n'),
123
131
  durationMs,
132
+ startupMs,
124
133
  model,
125
134
  usage: usage || null,
126
135
  errors: errors.map(e => e.message || e.error?.message || 'unknown'),
@@ -134,10 +143,18 @@ function executeCodex(codexBin, model, prompt, cwd, timeoutMs) {
134
143
  // Usage logger
135
144
  // ---------------------------------------------------------------------------
136
145
 
146
+ function loadActiveProfile() {
147
+ try {
148
+ return JSON.parse(readFileSync(join(__dirname, '..', 'dual-brain.profile.json'), 'utf8')).active || 'balanced';
149
+ } catch { return 'balanced'; }
150
+ }
151
+
152
+ const SESSION_ID = process.env.CLAUDE_SESSION_ID || process.ppid?.toString() || null;
153
+
137
154
  function logUsageEvent(result, task) {
138
155
  const logFile = join(__dirname, `usage-${new Date().toISOString().slice(0, 10)}.jsonl`);
139
- const entry = JSON.stringify({
140
- schema_version: 2,
156
+ const entryObj = {
157
+ schema_version: 3,
141
158
  timestamp: new Date().toISOString(),
142
159
  provider: 'openai',
143
160
  tier: task.tier || 'execute',
@@ -145,14 +162,40 @@ function logUsageEvent(result, task) {
145
162
  model: result.model,
146
163
  status: result.success ? 'ok' : 'error',
147
164
  durationMs: result.durationMs,
165
+ codex_startup_ms: result.startupMs || null,
166
+ codex_total_ms: result.durationMs,
148
167
  input_tokens: result.usage?.input_tokens ?? null,
149
168
  output_tokens: result.usage?.output_tokens ?? null,
150
- session_id: process.env.CLAUDE_SESSION_ID || null,
169
+ session_id: SESSION_ID,
170
+ profile: result.profile || 'balanced',
151
171
  dispatcher: 'gpt-work-dispatcher',
152
- });
172
+ };
153
173
  try {
154
- appendFileSync(logFile, entry + '\n');
174
+ appendFileSync(logFile, JSON.stringify(entryObj) + '\n');
155
175
  } catch {}
176
+
177
+ // Update summary checkpoint with codex latency
178
+ import('./summary-checkpoint.mjs').then(({ updateSummary }) => {
179
+ updateSummary(entryObj);
180
+ }).catch(() => {});
181
+
182
+ // Record to decision ledger
183
+ import('./decision-ledger.mjs').then(({ recordDecision, recordOutcome }) => {
184
+ const id = recordDecision({
185
+ session_id: SESSION_ID,
186
+ profile: entryObj.profile,
187
+ tier: task.tier || 'execute',
188
+ provider: 'openai',
189
+ model: result.model,
190
+ });
191
+ recordOutcome(id, {
192
+ actual_duration_ms: result.durationMs,
193
+ codex_startup_ms: result.startupMs || null,
194
+ success: result.success,
195
+ actual_input_tokens: result.usage?.input_tokens || null,
196
+ actual_output_tokens: result.usage?.output_tokens || null,
197
+ });
198
+ }).catch(() => {});
156
199
  }
157
200
 
158
201
  // ---------------------------------------------------------------------------
@@ -171,6 +214,7 @@ export async function dispatchGptTask(task) {
171
214
  const model = task.model || 'gpt-5.4';
172
215
  const prompt = buildPrompt(task);
173
216
  const result = executeCodex(codexBin, model, prompt, task.cwd, task.timeoutMs);
217
+ result.profile = loadActiveProfile();
174
218
  logUsageEvent(result, task);
175
219
  return result;
176
220
  }
@@ -0,0 +1,203 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * profiles.mjs — Profile system for the Dual-Brain Orchestrator.
4
+ *
5
+ * Profiles configure routing posture, budget limits, and quality gate behavior.
6
+ * Active profile persists to .claude/dual-brain.profile.json.
7
+ *
8
+ * Exported API:
9
+ * PROFILES → built-in profile definitions
10
+ * getActiveProfile() → current profile name + merged settings
11
+ * setActiveProfile(name) → switch profile, returns success/error
12
+ * getProfileOverrides(key) → profile-driven overrides for a specific system
13
+ */
14
+
15
+ import { existsSync, readFileSync, renameSync, writeFileSync } from 'fs';
16
+ import { dirname, join } from 'path';
17
+ import { fileURLToPath } from 'url';
18
+
19
+ const __dirname = dirname(fileURLToPath(import.meta.url));
20
+ const PROFILE_FILE = join(__dirname, '..', 'dual-brain.profile.json');
21
+ const CONFIG_FILE = join(__dirname, '..', 'orchestrator.json');
22
+
23
+ const PROFILES = {
24
+ balanced: {
25
+ description: 'Standard routing — best model for each tier, normal budgets',
26
+ routing: {
27
+ prefer_provider: 'auto',
28
+ think_threshold: 'normal',
29
+ gpt_dispatch_bias: 0,
30
+ },
31
+ budgets: {
32
+ session_warn_usd: 5.00,
33
+ session_limit_usd: 10.00,
34
+ daily_warn_usd: 20.00,
35
+ daily_limit_usd: 50.00,
36
+ },
37
+ quality_gate: {
38
+ sensitivity_floor: 'medium',
39
+ dual_brain_minimum: 'high',
40
+ },
41
+ tier_overrides: null,
42
+ },
43
+
44
+ 'cost-saver': {
45
+ description: 'Minimize spend — prefer cheaper models, skip GPT for low risk',
46
+ routing: {
47
+ prefer_provider: 'cheapest',
48
+ think_threshold: 'strict',
49
+ gpt_dispatch_bias: -20,
50
+ },
51
+ budgets: {
52
+ session_warn_usd: 2.00,
53
+ session_limit_usd: 5.00,
54
+ daily_warn_usd: 8.00,
55
+ daily_limit_usd: 20.00,
56
+ },
57
+ quality_gate: {
58
+ sensitivity_floor: 'high',
59
+ dual_brain_minimum: 'critical',
60
+ },
61
+ tier_overrides: {
62
+ promote_execute_to_think: false,
63
+ demote_think_to_execute: true,
64
+ },
65
+ },
66
+
67
+ 'quality-first': {
68
+ description: 'Maximum quality — dual-brain for medium+, stricter reviews',
69
+ routing: {
70
+ prefer_provider: 'most-capable',
71
+ think_threshold: 'relaxed',
72
+ gpt_dispatch_bias: 10,
73
+ },
74
+ budgets: {
75
+ session_warn_usd: 15.00,
76
+ session_limit_usd: 30.00,
77
+ daily_warn_usd: 50.00,
78
+ daily_limit_usd: 100.00,
79
+ },
80
+ quality_gate: {
81
+ sensitivity_floor: 'low',
82
+ dual_brain_minimum: 'medium',
83
+ },
84
+ tier_overrides: {
85
+ promote_execute_to_think: true,
86
+ demote_think_to_execute: false,
87
+ },
88
+ },
89
+ };
90
+
91
+ function loadProfileFile() {
92
+ try {
93
+ return JSON.parse(readFileSync(PROFILE_FILE, 'utf8'));
94
+ } catch {
95
+ return null;
96
+ }
97
+ }
98
+
99
+ function loadConfig() {
100
+ try {
101
+ return JSON.parse(readFileSync(CONFIG_FILE, 'utf8'));
102
+ } catch {
103
+ return {};
104
+ }
105
+ }
106
+
107
+ function getActiveProfile() {
108
+ const saved = loadProfileFile();
109
+ const name = saved?.active || 'balanced';
110
+ const profile = PROFILES[name] || PROFILES.balanced;
111
+ const customOverrides = saved?.custom_overrides || {};
112
+
113
+ return {
114
+ name: PROFILES[name] ? name : 'balanced',
115
+ ...profile,
116
+ budgets: { ...profile.budgets, ...customOverrides.budgets },
117
+ routing: { ...profile.routing, ...customOverrides.routing },
118
+ switched_at: saved?.switched_at || null,
119
+ };
120
+ }
121
+
122
+ function setActiveProfile(name, customOverrides = null) {
123
+ if (!PROFILES[name]) {
124
+ return { ok: false, error: `Unknown profile: ${name}. Available: ${Object.keys(PROFILES).join(', ')}` };
125
+ }
126
+
127
+ const data = {
128
+ active: name,
129
+ switched_at: new Date().toISOString(),
130
+ };
131
+ if (customOverrides) data.custom_overrides = customOverrides;
132
+
133
+ try {
134
+ const tmp = PROFILE_FILE + '.tmp.' + process.pid;
135
+ writeFileSync(tmp, JSON.stringify(data, null, 2) + '\n');
136
+ renameSync(tmp, PROFILE_FILE);
137
+ return { ok: true, profile: PROFILES[name] };
138
+ } catch (err) {
139
+ return { ok: false, error: `Failed to write profile: ${err.message}` };
140
+ }
141
+ }
142
+
143
+ function setBudgetOverrides(sessionLimit, dailyLimit) {
144
+ const saved = loadProfileFile() || { active: 'balanced' };
145
+ saved.custom_overrides = saved.custom_overrides || {};
146
+ saved.custom_overrides.budgets = {};
147
+
148
+ if (sessionLimit != null) {
149
+ saved.custom_overrides.budgets.session_warn_usd = sessionLimit * 0.6;
150
+ saved.custom_overrides.budgets.session_limit_usd = sessionLimit;
151
+ }
152
+ if (dailyLimit != null) {
153
+ saved.custom_overrides.budgets.daily_warn_usd = dailyLimit * 0.6;
154
+ saved.custom_overrides.budgets.daily_limit_usd = dailyLimit;
155
+ }
156
+
157
+ saved.switched_at = saved.switched_at || new Date().toISOString();
158
+
159
+ try {
160
+ const tmp = PROFILE_FILE + '.tmp.' + process.pid;
161
+ writeFileSync(tmp, JSON.stringify(saved, null, 2) + '\n');
162
+ renameSync(tmp, PROFILE_FILE);
163
+ return { ok: true };
164
+ } catch (err) {
165
+ return { ok: false, error: err.message };
166
+ }
167
+ }
168
+
169
+ function getProfileOverrides(system) {
170
+ const profile = getActiveProfile();
171
+
172
+ switch (system) {
173
+ case 'enforce-tier':
174
+ return {
175
+ think_threshold: profile.routing.think_threshold,
176
+ tier_overrides: profile.tier_overrides,
177
+ gpt_dispatch_bias: profile.routing.gpt_dispatch_bias,
178
+ };
179
+
180
+ case 'budget-balancer':
181
+ return {
182
+ budgets: profile.budgets,
183
+ prefer_provider: profile.routing.prefer_provider,
184
+ };
185
+
186
+ case 'quality-gate':
187
+ return {
188
+ sensitivity_floor: profile.quality_gate.sensitivity_floor,
189
+ dual_brain_minimum: profile.quality_gate.dual_brain_minimum,
190
+ };
191
+
192
+ default:
193
+ return {};
194
+ }
195
+ }
196
+
197
+ export {
198
+ PROFILES,
199
+ getActiveProfile,
200
+ setActiveProfile,
201
+ setBudgetOverrides,
202
+ getProfileOverrides,
203
+ };
@@ -23,9 +23,31 @@ import { fileURLToPath } from 'url';
23
23
 
24
24
  const __dirname = dirname(fileURLToPath(import.meta.url));
25
25
  const ORCHESTRATOR_CONFIG = resolve(__dirname, '..', 'orchestrator.json');
26
+ const PROFILE_FILE = resolve(__dirname, '..', 'dual-brain.profile.json');
26
27
  const REVIEWS_DIR = resolve(__dirname, '..', 'reviews');
27
28
  const DUAL_BRAIN = resolve(__dirname, 'dual-brain-review.mjs');
28
29
 
30
+ const RISK_LEVELS = ['low', 'medium', 'high', 'critical'];
31
+
32
+ function loadProfileGateSettings() {
33
+ 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;
42
+ } catch {
43
+ return { sensitivity_floor: 'medium', dual_brain_minimum: 'high' };
44
+ }
45
+ }
46
+
47
+ function riskMeetsFloor(risk, floor) {
48
+ return RISK_LEVELS.indexOf(risk) >= RISK_LEVELS.indexOf(floor);
49
+ }
50
+
29
51
  function exit(obj) {
30
52
  process.stdout.write(JSON.stringify(obj) + '\n');
31
53
  process.exit(0);
@@ -162,14 +184,16 @@ function main() {
162
184
  // 5a. Score sensitivity BEFORE running any external review
163
185
  const sensitivity = scoreSensitivity(qualifyingFiles, config);
164
186
 
165
- // 5b. Low risk skip GPT review entirely
166
- if (sensitivity.gate === 'self-check') {
187
+ // 5b. Apply profile-driven sensitivity floor
188
+ const profileGate = loadProfileGateSettings();
189
+ if (!riskMeetsFloor(sensitivity.risk, profileGate.sensitivity_floor)) {
167
190
  exit({
168
191
  gate: 'pass',
169
- risk: 'low',
192
+ risk: sensitivity.risk,
170
193
  sensitivity_score: sensitivity.score,
171
194
  sensitivity_reasons: sensitivity.reasons,
172
- reason: 'low sensitivity — self-check only',
195
+ reason: `${sensitivity.risk} risk below profile floor (${profileGate.sensitivity_floor})`,
196
+ profile_floor: profileGate.sensitivity_floor,
173
197
  files: qualifyingFiles,
174
198
  });
175
199
  }
@@ -232,14 +256,18 @@ function main() {
232
256
  reviewResult.error === true ||
233
257
  !reviewResult.review;
234
258
 
259
+ // Profile can lower the dual-brain threshold
260
+ const needsDualBrain = riskMeetsFloor(sensitivity.risk, profileGate.dual_brain_minimum);
261
+
235
262
  let gateStatus;
236
- if (sensitivity.gate === 'dual-brain-required') {
237
- // Critical: always flag for dual-brain + user attention regardless of review outcome
263
+ if (sensitivity.gate === 'dual-brain-required' || (needsDualBrain && sensitivity.risk === 'critical')) {
238
264
  gateStatus = 'needs_dual_think';
239
265
  } else if (reviewUnavailable) {
240
266
  gateStatus = 'needs_human_review';
241
267
  } else if (reviewResult.issues_found) {
242
268
  gateStatus = 'issues_found';
269
+ } else if (needsDualBrain) {
270
+ gateStatus = 'reviewed';
243
271
  } else {
244
272
  gateStatus = sensitivity.gate === 'dual-brain-recommended' ? 'reviewed' : 'pass';
245
273
  }
@@ -119,7 +119,7 @@ async function main() {
119
119
 
120
120
  console.log(` ║ Quality gate: ${gateEnabled ? 'enabled' : 'disabled'}`.padEnd(53) + '║');
121
121
  console.log(' ╠══════════════════════════════════════════════════╣');
122
- console.log(' ║ Restart Claude Code to activate the orchestrator ║');
122
+ console.log(' ║ Hooks are active no restart needed ║');
123
123
  console.log(' ╚══════════════════════════════════════════════════╝');
124
124
  console.log('');
125
125