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.
@@ -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.14",
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",