dual-brain 0.2.14 → 0.2.15
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 +130 -4
- package/hooks/diagnostic-companion.mjs +422 -0
- package/hooks/precompact.mjs +53 -0
- package/hooks/session-end.mjs +122 -0
- package/package.json +26 -2
- package/src/cognitive-loop.mjs +532 -0
- package/src/continuity.mjs +6 -6
- package/src/cost-tracker.mjs +3 -3
- package/src/debrief.mjs +228 -0
- package/src/doctor.mjs +13 -13
- package/src/envelope.mjs +139 -0
- package/src/head-protocol.mjs +128 -0
- package/src/head.mjs +114 -79
- package/src/inbox.mjs +195 -0
- package/src/ledger.mjs +2 -2
- package/src/living-docs.mjs +2 -2
- package/src/memory-tiers.mjs +193 -0
- package/src/narrative.mjs +169 -0
- package/src/predictive.mjs +250 -0
- package/src/provider-context.mjs +2 -2
- package/src/receipt.mjs +2 -2
- package/src/session-lock.mjs +154 -0
- package/src/simmer.mjs +241 -0
- package/src/wave-planner.mjs +294 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// session-end.mjs — Stop hook for dual-brain. Runs when Claude session ends.
|
|
3
|
+
// Generates receipt, records metrics, cleans up stale locks.
|
|
4
|
+
|
|
5
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
|
|
8
|
+
const WORKSPACE = join(new URL(import.meta.url).pathname, '..', '..', '..');
|
|
9
|
+
const DUALBRAIN = join(WORKSPACE, '.dualbrain');
|
|
10
|
+
const RECEIPTS_DIR = join(DUALBRAIN, 'receipts');
|
|
11
|
+
|
|
12
|
+
// Read hook input from stdin
|
|
13
|
+
let input = {};
|
|
14
|
+
try {
|
|
15
|
+
input = JSON.parse(readFileSync('/dev/stdin', 'utf8'));
|
|
16
|
+
} catch {
|
|
17
|
+
// Stop hook may not always get structured input
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function run() {
|
|
21
|
+
mkdirSync(RECEIPTS_DIR, { recursive: true });
|
|
22
|
+
|
|
23
|
+
// 1. Generate session receipt
|
|
24
|
+
const receipt = {
|
|
25
|
+
timestamp: new Date().toISOString(),
|
|
26
|
+
sessionId: input.session_id || 'unknown',
|
|
27
|
+
reason: input.stop_hook_reason || 'session_end',
|
|
28
|
+
metrics: {},
|
|
29
|
+
cleanup: [],
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// 2. Collect metrics from audit log
|
|
33
|
+
const auditFile = join(DUALBRAIN, 'audit', 'head-audit.jsonl');
|
|
34
|
+
if (existsSync(auditFile)) {
|
|
35
|
+
try {
|
|
36
|
+
const lines = readFileSync(auditFile, 'utf8').trim().split('\n').filter(Boolean);
|
|
37
|
+
const entries = lines.map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
|
|
38
|
+
|
|
39
|
+
// Filter to this session (last 2 hours as proxy)
|
|
40
|
+
const cutoff = Date.now() - 2 * 60 * 60 * 1000;
|
|
41
|
+
const sessionEntries = entries.filter(e => (e.timestamp || 0) > cutoff);
|
|
42
|
+
|
|
43
|
+
receipt.metrics.toolCalls = sessionEntries.length;
|
|
44
|
+
receipt.metrics.blocked = sessionEntries.filter(e => e.decision === 'block').length;
|
|
45
|
+
receipt.metrics.allowed = sessionEntries.filter(e => e.decision === 'allow').length;
|
|
46
|
+
receipt.metrics.agentDispatches = sessionEntries.filter(e => e.tool === 'Agent').length;
|
|
47
|
+
} catch {}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 3. Check for cost log
|
|
51
|
+
const costLog = join(DUALBRAIN, 'cost-log.jsonl');
|
|
52
|
+
if (existsSync(costLog)) {
|
|
53
|
+
try {
|
|
54
|
+
const lines = readFileSync(costLog, 'utf8').trim().split('\n').filter(Boolean);
|
|
55
|
+
const cutoff = Date.now() - 2 * 60 * 60 * 1000;
|
|
56
|
+
const recent = lines.map(l => { try { return JSON.parse(l); } catch { return null; } })
|
|
57
|
+
.filter(e => e && (e.timestamp || 0) > cutoff);
|
|
58
|
+
|
|
59
|
+
receipt.metrics.costEntries = recent.length;
|
|
60
|
+
} catch {}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 4. Clean up stale lock files
|
|
64
|
+
try {
|
|
65
|
+
const scanDirs = [DUALBRAIN, join(DUALBRAIN, 'doctor'), join(DUALBRAIN, 'receipts')];
|
|
66
|
+
for (const dir of scanDirs) {
|
|
67
|
+
if (!existsSync(dir)) continue;
|
|
68
|
+
const files = readdirSync(dir).filter(f => f.endsWith('.lock'));
|
|
69
|
+
for (const f of files) {
|
|
70
|
+
const lockPath = join(dir, f);
|
|
71
|
+
try {
|
|
72
|
+
const lockData = JSON.parse(readFileSync(lockPath, 'utf8'));
|
|
73
|
+
const age = Date.now() - (lockData.createdAt || 0);
|
|
74
|
+
if (age > 60000) { // older than 1 minute = stale
|
|
75
|
+
unlinkSync(lockPath);
|
|
76
|
+
receipt.cleanup.push(`removed stale lock: ${f}`);
|
|
77
|
+
}
|
|
78
|
+
} catch {
|
|
79
|
+
// Corrupt lock — remove
|
|
80
|
+
try { unlinkSync(lockPath); receipt.cleanup.push(`removed corrupt lock: ${f}`); } catch {}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
} catch {}
|
|
85
|
+
|
|
86
|
+
// 5. Record git state for next session
|
|
87
|
+
try {
|
|
88
|
+
const { execSync } = await import('node:child_process');
|
|
89
|
+
receipt.gitState = {
|
|
90
|
+
branch: execSync('git rev-parse --abbrev-ref HEAD', { cwd: WORKSPACE, encoding: 'utf8', timeout: 3000 }).trim(),
|
|
91
|
+
uncommitted: parseInt(execSync('git status --porcelain | wc -l', { cwd: WORKSPACE, encoding: 'utf8', timeout: 3000 }).trim()) || 0,
|
|
92
|
+
lastCommit: execSync('git log --oneline -1', { cwd: WORKSPACE, encoding: 'utf8', timeout: 3000 }).trim(),
|
|
93
|
+
};
|
|
94
|
+
} catch {}
|
|
95
|
+
|
|
96
|
+
// 6. Persist immersion state and release session lock
|
|
97
|
+
try {
|
|
98
|
+
const { persist } = await import('../src/narrative.mjs');
|
|
99
|
+
const { prune } = await import('../src/simmer.mjs');
|
|
100
|
+
const { release } = await import('../src/session-lock.mjs');
|
|
101
|
+
persist();
|
|
102
|
+
prune();
|
|
103
|
+
release();
|
|
104
|
+
receipt.immersion = { narrativePersisted: true };
|
|
105
|
+
} catch {}
|
|
106
|
+
|
|
107
|
+
// 7. Save receipt
|
|
108
|
+
const receiptFile = join(RECEIPTS_DIR, `receipt-${Date.now()}.json`);
|
|
109
|
+
writeFileSync(receiptFile, JSON.stringify(receipt, null, 2) + '\n');
|
|
110
|
+
|
|
111
|
+
// 8. Print summary to stderr (visible to user)
|
|
112
|
+
const summary = [];
|
|
113
|
+
if (receipt.metrics.toolCalls) summary.push(`${receipt.metrics.toolCalls} tool calls`);
|
|
114
|
+
if (receipt.metrics.agentDispatches) summary.push(`${receipt.metrics.agentDispatches} agents dispatched`);
|
|
115
|
+
if (receipt.cleanup.length) summary.push(`${receipt.cleanup.length} locks cleaned`);
|
|
116
|
+
|
|
117
|
+
if (summary.length) {
|
|
118
|
+
process.stderr.write(`[dual-brain] Session end: ${summary.join(', ')}\n`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
run().then(() => process.exit(0)).catch(() => process.exit(0));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dual-brain",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.15",
|
|
4
4
|
"description": "AI orchestration across Claude + OpenAI subscriptions — smart routing, budget awareness, and dual-brain collaboration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -34,7 +34,18 @@
|
|
|
34
34
|
"./templates": "./src/templates.mjs",
|
|
35
35
|
"./agents": "./src/agents/registry.mjs",
|
|
36
36
|
"./collaboration": "./src/collaboration.mjs",
|
|
37
|
-
"./provider-context": "./src/provider-context.mjs"
|
|
37
|
+
"./provider-context": "./src/provider-context.mjs",
|
|
38
|
+
"./cognitive-loop": "./src/cognitive-loop.mjs",
|
|
39
|
+
"./debrief": "./src/debrief.mjs",
|
|
40
|
+
"./wave-planner": "./src/wave-planner.mjs",
|
|
41
|
+
"./predictive": "./src/predictive.mjs",
|
|
42
|
+
"./inbox": "./src/inbox.mjs",
|
|
43
|
+
"./head-protocol": "./src/head-protocol.mjs",
|
|
44
|
+
"./narrative": "./src/narrative.mjs",
|
|
45
|
+
"./simmer": "./src/simmer.mjs",
|
|
46
|
+
"./memory-tiers": "./src/memory-tiers.mjs",
|
|
47
|
+
"./envelope": "./src/envelope.mjs",
|
|
48
|
+
"./session-lock": "./src/session-lock.mjs"
|
|
38
49
|
},
|
|
39
50
|
"keywords": [
|
|
40
51
|
"claude-code",
|
|
@@ -108,6 +119,17 @@
|
|
|
108
119
|
"src/agents/registry.mjs",
|
|
109
120
|
"src/collaboration.mjs",
|
|
110
121
|
"src/provider-context.mjs",
|
|
122
|
+
"src/cognitive-loop.mjs",
|
|
123
|
+
"src/debrief.mjs",
|
|
124
|
+
"src/wave-planner.mjs",
|
|
125
|
+
"src/predictive.mjs",
|
|
126
|
+
"src/inbox.mjs",
|
|
127
|
+
"src/head-protocol.mjs",
|
|
128
|
+
"src/narrative.mjs",
|
|
129
|
+
"src/simmer.mjs",
|
|
130
|
+
"src/memory-tiers.mjs",
|
|
131
|
+
"src/envelope.mjs",
|
|
132
|
+
"src/session-lock.mjs",
|
|
111
133
|
"bin/*.mjs",
|
|
112
134
|
"hooks/enforce-tier.mjs",
|
|
113
135
|
"hooks/cost-logger.mjs",
|
|
@@ -136,6 +158,8 @@
|
|
|
136
158
|
"hooks/model-registry.mjs",
|
|
137
159
|
"hooks/auto-update-wrapper.mjs",
|
|
138
160
|
"hooks/session-end.mjs",
|
|
161
|
+
"hooks/diagnostic-companion.mjs",
|
|
162
|
+
"hooks/precompact.mjs",
|
|
139
163
|
"hooks/head-guard.mjs",
|
|
140
164
|
"hooks/auto-update.sh",
|
|
141
165
|
"mcp-server/*.mjs",
|