dual-brain 3.1.0 → 3.3.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.
- package/CLAUDE.md +33 -1
- package/hooks/budget-balancer.mjs +45 -6
- package/hooks/control-panel.mjs +489 -0
- package/hooks/cost-logger.mjs +51 -26
- package/hooks/decision-ledger.mjs +299 -0
- package/hooks/dual-brain-review.mjs +106 -17
- package/hooks/dual-brain-think.mjs +81 -17
- package/hooks/enforce-tier.mjs +103 -10
- package/hooks/gpt-work-dispatcher.mjs +50 -6
- package/hooks/profiles.mjs +203 -0
- package/hooks/quality-gate.mjs +34 -6
- package/hooks/summary-checkpoint.mjs +231 -0
- package/install.mjs +402 -33
- package/package.json +2 -2
- package/hooks/usage-2026-05-14.jsonl +0 -5
|
@@ -60,8 +60,33 @@ function findCodex() {
|
|
|
60
60
|
// Prompt builder
|
|
61
61
|
// ---------------------------------------------------------------------------
|
|
62
62
|
|
|
63
|
-
function buildGptPrompt({ question, context, files }) {
|
|
63
|
+
function buildGptPrompt({ question, context, files, round, claudePerspective }) {
|
|
64
|
+
if (round === 2 && claudePerspective) {
|
|
65
|
+
return `You are GPT-5.5 in a collaborative architectural discussion with Claude (Opus).
|
|
66
|
+
You gave your initial analysis on a question. Claude has now provided its independent perspective.
|
|
67
|
+
This is a professional dialogue — two experts refining a decision together.
|
|
68
|
+
|
|
69
|
+
Original question: ${question}
|
|
70
|
+
${context ? `\nContext: ${context}` : ''}
|
|
71
|
+
|
|
72
|
+
Claude's perspective:
|
|
73
|
+
${claudePerspective}
|
|
74
|
+
|
|
75
|
+
Now respond as a colleague, not a critic. Structure your response:
|
|
76
|
+
1. AGREEMENTS: Where Claude's analysis strengthens or confirms your thinking
|
|
77
|
+
2. PUSHBACK: Where you disagree — be specific about WHY with evidence or reasoning
|
|
78
|
+
3. NEW INSIGHTS: Anything Claude's perspective surfaced that you missed
|
|
79
|
+
4. REFINED RECOMMENDATION: Your updated recommendation incorporating both perspectives
|
|
80
|
+
5. REMAINING CONCERNS: Open questions neither of you fully resolved
|
|
81
|
+
6. CONFIDENCE DELTA: Has your confidence changed? Why?
|
|
82
|
+
|
|
83
|
+
Be direct and substantive. If Claude is right about something you got wrong, say so.
|
|
84
|
+
If you still disagree after considering their points, explain what specific evidence would change your mind.`;
|
|
85
|
+
}
|
|
86
|
+
|
|
64
87
|
return `You are GPT-5.5, providing an independent architectural perspective.
|
|
88
|
+
This is Round 1 of a dual-brain analysis — Claude (Opus) will independently analyze the same question,
|
|
89
|
+
then send you their perspective for a collaborative discussion in Round 2.
|
|
65
90
|
|
|
66
91
|
Question: ${question}
|
|
67
92
|
${context ? `\nContext: ${context}` : ''}
|
|
@@ -165,7 +190,7 @@ function logUsage({ durationMs, usage, success }) {
|
|
|
165
190
|
// Core exported function
|
|
166
191
|
// ---------------------------------------------------------------------------
|
|
167
192
|
|
|
168
|
-
export async function dualThink({ question, context, files } = {}) {
|
|
193
|
+
export async function dualThink({ question, context, files, round, claudePerspective } = {}) {
|
|
169
194
|
if (!question) {
|
|
170
195
|
return {
|
|
171
196
|
gpt: null,
|
|
@@ -174,6 +199,8 @@ export async function dualThink({ question, context, files } = {}) {
|
|
|
174
199
|
};
|
|
175
200
|
}
|
|
176
201
|
|
|
202
|
+
const effectiveRound = (round === 2 && claudePerspective) ? 2 : 1;
|
|
203
|
+
|
|
177
204
|
const codexBin = findCodex();
|
|
178
205
|
if (!codexBin) {
|
|
179
206
|
return {
|
|
@@ -183,7 +210,6 @@ export async function dualThink({ question, context, files } = {}) {
|
|
|
183
210
|
};
|
|
184
211
|
}
|
|
185
212
|
|
|
186
|
-
// Check Codex auth before running
|
|
187
213
|
try {
|
|
188
214
|
execSync(`${codexBin} login status`, {
|
|
189
215
|
encoding: 'utf8',
|
|
@@ -198,7 +224,7 @@ export async function dualThink({ question, context, files } = {}) {
|
|
|
198
224
|
};
|
|
199
225
|
}
|
|
200
226
|
|
|
201
|
-
const prompt = buildGptPrompt({ question, context, files });
|
|
227
|
+
const prompt = buildGptPrompt({ question, context, files, round: effectiveRound, claudePerspective });
|
|
202
228
|
const raw = runGptAnalysis(codexBin, prompt);
|
|
203
229
|
|
|
204
230
|
logUsage({ durationMs: raw.durationMs, usage: raw.usage, success: raw.success });
|
|
@@ -207,18 +233,44 @@ export async function dualThink({ question, context, files } = {}) {
|
|
|
207
233
|
return {
|
|
208
234
|
gpt: null,
|
|
209
235
|
error: raw.error || 'GPT analysis failed',
|
|
210
|
-
fallback:
|
|
236
|
+
fallback: effectiveRound === 2
|
|
237
|
+
? 'GPT rebuttal unavailable — synthesize from Round 1 analysis alone'
|
|
238
|
+
: 'Proceed with single-brain analysis on Claude Opus',
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (effectiveRound === 2) {
|
|
243
|
+
return {
|
|
244
|
+
round: 2,
|
|
245
|
+
gpt: {
|
|
246
|
+
rebuttal: raw.text,
|
|
247
|
+
model: MODEL,
|
|
248
|
+
durationMs: raw.durationMs,
|
|
249
|
+
tokens: raw.usage,
|
|
250
|
+
},
|
|
251
|
+
instructions: `GPT has responded to your analysis. Now synthesize both rounds into a FINAL DECISION:
|
|
252
|
+
1. Where you both agree → high confidence, proceed
|
|
253
|
+
2. Where GPT pushed back on your points → re-evaluate honestly
|
|
254
|
+
3. Where you still disagree → state why and what evidence would resolve it
|
|
255
|
+
4. Final recommendation with combined confidence level`,
|
|
256
|
+
question,
|
|
211
257
|
};
|
|
212
258
|
}
|
|
213
259
|
|
|
214
260
|
return {
|
|
261
|
+
round: 1,
|
|
215
262
|
gpt: {
|
|
216
263
|
recommendation: raw.text,
|
|
217
264
|
model: MODEL,
|
|
218
265
|
durationMs: raw.durationMs,
|
|
219
266
|
tokens: raw.usage,
|
|
220
267
|
},
|
|
221
|
-
instructions:
|
|
268
|
+
instructions: `Round 1 complete. Now:
|
|
269
|
+
1. Provide YOUR independent analysis of the same question (same structure: recommendation, rationale, alternatives, risks, confidence, verification)
|
|
270
|
+
2. Then call Round 2 to send your perspective back to GPT:
|
|
271
|
+
node .claude/hooks/dual-brain-think.mjs --question "<same question>" --round 2 --claude-says "<your analysis summary>"
|
|
272
|
+
3. GPT will respond to your specific points — agreements, pushback, and refined recommendation
|
|
273
|
+
4. You then synthesize both rounds into the final decision`,
|
|
222
274
|
question,
|
|
223
275
|
context: context || null,
|
|
224
276
|
};
|
|
@@ -268,32 +320,41 @@ function printResult(result, question) {
|
|
|
268
320
|
const TOP = '╔══════════════════════════════════════════════════╗';
|
|
269
321
|
const BOT = '╚══════════════════════════════════════════════════╝';
|
|
270
322
|
|
|
323
|
+
const roundLabel = result.round === 2 ? 'Round 2 — Rebuttal' : 'Round 1 — Initial';
|
|
324
|
+
|
|
271
325
|
console.log(TOP);
|
|
272
|
-
console.log(
|
|
326
|
+
console.log(`║ 🧠 Dual-Brain Think · ${roundLabel}`.padEnd(51) + '║');
|
|
273
327
|
console.log(BAR);
|
|
274
|
-
// Truncate question to fit the box
|
|
275
328
|
const q = question.length > 44 ? question.slice(0, 41) + '...' : question;
|
|
276
329
|
console.log(`║ Question: ${q.padEnd(38)} ║`);
|
|
277
330
|
console.log(BAR);
|
|
278
331
|
|
|
279
332
|
if (!result.gpt) {
|
|
280
|
-
|
|
281
|
-
console.log(`║ ERROR: ${(result.error || 'Unknown error').padEnd(41)} ║`);
|
|
333
|
+
console.log(`║ ❌ ${(result.error || 'Unknown error').padEnd(45)} ║`);
|
|
282
334
|
console.log(BAR);
|
|
283
|
-
console.log(`║
|
|
335
|
+
console.log(`║ ↩️ ${(result.fallback || '').padEnd(45)} ║`);
|
|
284
336
|
console.log(BOT);
|
|
285
337
|
return;
|
|
286
338
|
}
|
|
287
339
|
|
|
288
|
-
const
|
|
289
|
-
|
|
340
|
+
const gptData = result.gpt;
|
|
341
|
+
const durSec = (gptData.durationMs / 1000).toFixed(1);
|
|
342
|
+
console.log(`║ 🤖 GPT-5.5 (${durSec}s):`.padEnd(51) + '║');
|
|
290
343
|
console.log(BAR);
|
|
291
344
|
console.log('');
|
|
292
|
-
console.log(
|
|
345
|
+
console.log(gptData.recommendation || gptData.rebuttal);
|
|
293
346
|
console.log('');
|
|
294
347
|
console.log(BAR);
|
|
295
|
-
|
|
296
|
-
|
|
348
|
+
|
|
349
|
+
if (result.round === 2) {
|
|
350
|
+
console.log('║ 🔄 Synthesize both rounds into final decision. ║');
|
|
351
|
+
console.log('║ Where you agree → high confidence. ║');
|
|
352
|
+
console.log('║ Where you disagree → state what would resolve it.║');
|
|
353
|
+
} else {
|
|
354
|
+
console.log('║ 📝 Your turn: analyze independently, then call ║');
|
|
355
|
+
console.log('║ Round 2 with --round 2 --claude-says "..." ║');
|
|
356
|
+
console.log('║ for GPT\'s rebuttal to your analysis. ║');
|
|
357
|
+
}
|
|
297
358
|
console.log(BOT);
|
|
298
359
|
}
|
|
299
360
|
|
|
@@ -306,7 +367,8 @@ if (import.meta.url === `file://${process.argv[1]}`) {
|
|
|
306
367
|
|
|
307
368
|
if (!args.question) {
|
|
308
369
|
console.error(
|
|
309
|
-
'Usage: node dual-brain-think.mjs --question "<question>" [--context "<
|
|
370
|
+
'Usage: node dual-brain-think.mjs --question "<question>" [--context "<ctx>"] [--files f1,f2]\n' +
|
|
371
|
+
' node dual-brain-think.mjs --question "<question>" --round 2 --claude-says "<analysis>"'
|
|
310
372
|
);
|
|
311
373
|
process.exit(1);
|
|
312
374
|
}
|
|
@@ -315,6 +377,8 @@ if (import.meta.url === `file://${process.argv[1]}`) {
|
|
|
315
377
|
question: args.question,
|
|
316
378
|
context: args.context,
|
|
317
379
|
files: args.files,
|
|
380
|
+
round: args.round ? parseInt(args.round, 10) : 1,
|
|
381
|
+
claudePerspective: args['claude-says'] || null,
|
|
318
382
|
});
|
|
319
383
|
|
|
320
384
|
printResult(result, args.question);
|
package/hooks/enforce-tier.mjs
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
140
|
-
schema_version:
|
|
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:
|
|
169
|
+
session_id: SESSION_ID,
|
|
170
|
+
profile: result.profile || 'balanced',
|
|
151
171
|
dispatcher: 'gpt-work-dispatcher',
|
|
152
|
-
}
|
|
172
|
+
};
|
|
153
173
|
try {
|
|
154
|
-
appendFileSync(logFile,
|
|
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
|
}
|