dual-brain 0.2.14 → 0.2.16

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,422 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * diagnostic-companion.mjs — PostToolUse hook for the Dual-Brain orchestrator.
4
+ *
5
+ * Observes ALL tool calls (HEAD + subagents) and detects inefficient patterns:
6
+ * - Sequential dispatches that could be parallel
7
+ * - Re-reading files without edits between
8
+ * - Assumption leaps (dispatching work without prior research)
9
+ * - Scope creep beyond declared plan
10
+ * - Ceremony (excessive config reads without dispatching)
11
+ * - Stuck loops (same tool called repeatedly with similar inputs)
12
+ *
13
+ * Output: JSON to stdout. High-severity issues for HEAD get a systemMessage.
14
+ * Subagent observations are logged silently (never interfere with workers).
15
+ */
16
+
17
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
18
+ import { join } from 'node:path';
19
+
20
+ const STATE_DIR = join(process.cwd(), '.dualbrain', 'diagnostic');
21
+ const STATE_FILE = join(STATE_DIR, 'current.json');
22
+
23
+ const MAX_TOOL_CALLS = 100;
24
+ const MAX_NOTICINGS = 50;
25
+ const SESSION_GAP_MS = 30 * 60 * 1000; // 30 minutes
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // State management
29
+ // ---------------------------------------------------------------------------
30
+
31
+ function freshState() {
32
+ return {
33
+ sessionId: Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
34
+ startedAt: Date.now(),
35
+ toolCalls: [],
36
+ noticings: [],
37
+ stats: {
38
+ totalCalls: 0,
39
+ readCount: 0,
40
+ dispatchCount: 0,
41
+ uniqueFiles: [],
42
+ },
43
+ };
44
+ }
45
+
46
+ function loadState() {
47
+ try {
48
+ if (existsSync(STATE_FILE)) {
49
+ const data = JSON.parse(readFileSync(STATE_FILE, 'utf8'));
50
+ // Reset if session gap > 30 minutes
51
+ if (Date.now() - (data.lastActivity || data.startedAt || 0) > SESSION_GAP_MS) {
52
+ return freshState();
53
+ }
54
+ return data;
55
+ }
56
+ } catch {}
57
+ return freshState();
58
+ }
59
+
60
+ function saveState(state) {
61
+ state.lastActivity = Date.now();
62
+ // Cap arrays
63
+ if (state.toolCalls.length > MAX_TOOL_CALLS) {
64
+ state.toolCalls = state.toolCalls.slice(-MAX_TOOL_CALLS);
65
+ }
66
+ if (state.noticings.length > MAX_NOTICINGS) {
67
+ state.noticings = state.noticings.slice(-MAX_NOTICINGS);
68
+ }
69
+ mkdirSync(STATE_DIR, { recursive: true });
70
+ writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
71
+ }
72
+
73
+ // ---------------------------------------------------------------------------
74
+ // Record a tool call
75
+ // ---------------------------------------------------------------------------
76
+
77
+ function recordToolCall(state, toolName, toolInput, agentId) {
78
+ const meta = {};
79
+
80
+ // Extract relevant metadata based on tool type
81
+ if (toolName === 'Read' || toolName === 'Edit' || toolName === 'Write') {
82
+ meta.file = toolInput?.file_path || toolInput?.path || null;
83
+ }
84
+ if (toolName === 'Agent') {
85
+ meta.tier = toolInput?.tier || toolInput?.mode || 'unknown';
86
+ meta.prompt = (toolInput?.prompt || toolInput?.message || '').slice(0, 100);
87
+ }
88
+ if (toolName === 'Bash') {
89
+ meta.command = (toolInput?.command || '').slice(0, 100);
90
+ }
91
+ if (toolName === 'Grep' || toolName === 'Glob') {
92
+ meta.pattern = (toolInput?.pattern || toolInput?.query || '').slice(0, 60);
93
+ }
94
+
95
+ const entry = {
96
+ ts: Date.now(),
97
+ tool: toolName,
98
+ agentId: agentId || null,
99
+ meta,
100
+ };
101
+
102
+ state.toolCalls.push(entry);
103
+
104
+ // Update stats
105
+ state.stats.totalCalls++;
106
+ if (toolName === 'Read') state.stats.readCount++;
107
+ if (toolName === 'Agent') state.stats.dispatchCount++;
108
+ if (meta.file && !state.stats.uniqueFiles.includes(meta.file)) {
109
+ state.stats.uniqueFiles.push(meta.file);
110
+ }
111
+
112
+ return entry;
113
+ }
114
+
115
+ // ---------------------------------------------------------------------------
116
+ // Pattern detectors
117
+ // ---------------------------------------------------------------------------
118
+
119
+ function detectSequentialDispatch(state) {
120
+ const dispatches = state.toolCalls
121
+ .filter(c => c.tool === 'Agent' && !c.agentId) // HEAD-level dispatches only
122
+ .slice(-5);
123
+
124
+ if (dispatches.length < 2) return null;
125
+
126
+ // Check if last 2+ dispatches happened within 30s with no dependency signals
127
+ for (let i = dispatches.length - 1; i >= 1; i--) {
128
+ const curr = dispatches[i];
129
+ const prev = dispatches[i - 1];
130
+ if (curr.ts - prev.ts < 30_000) {
131
+ // Check if prompts reference each other (crude dependency check)
132
+ const currPrompt = (curr.meta.prompt || '').toLowerCase();
133
+ const prevPrompt = (prev.meta.prompt || '').toLowerCase();
134
+ // If neither references the other's key terms, likely independent
135
+ const prevWords = prevPrompt.split(/\s+/).filter(w => w.length > 5);
136
+ const hasOverlap = prevWords.some(w => currPrompt.includes(w));
137
+ if (!hasOverlap) {
138
+ return {
139
+ ts: Date.now(),
140
+ type: 'sequential-dispatch',
141
+ severity: 'high',
142
+ observation: 'These dispatches appear independent — consider parallel execution.',
143
+ surfaced: false,
144
+ };
145
+ }
146
+ }
147
+ }
148
+ return null;
149
+ }
150
+
151
+ function detectReReads(state) {
152
+ // Find files read 2+ times without an edit between
153
+ const fileReads = new Map(); // file -> count since last edit
154
+
155
+ for (const call of state.toolCalls) {
156
+ const file = call.meta?.file;
157
+ if (!file) continue;
158
+
159
+ if (call.tool === 'Edit' || call.tool === 'Write') {
160
+ // Reset count for this file
161
+ fileReads.delete(file);
162
+ } else if (call.tool === 'Read') {
163
+ fileReads.set(file, (fileReads.get(file) || 0) + 1);
164
+ }
165
+ }
166
+
167
+ // Find worst offender
168
+ let worst = null;
169
+ let worstCount = 1;
170
+ for (const [file, count] of fileReads) {
171
+ if (count >= 2 && count > worstCount) {
172
+ worst = file;
173
+ worstCount = count;
174
+ }
175
+ }
176
+
177
+ if (worst) {
178
+ return {
179
+ ts: Date.now(),
180
+ type: 're-read',
181
+ severity: 'medium',
182
+ observation: `File ${worst} read ${worstCount} times — consider caching the content or dispatching a single agent.`,
183
+ surfaced: false,
184
+ };
185
+ }
186
+ return null;
187
+ }
188
+
189
+ function detectAssumptionLeap(state) {
190
+ // Check if last Agent dispatch was preceded by any Read/search in recent window
191
+ const recentCalls = state.toolCalls.slice(-10);
192
+ const lastDispatch = [...recentCalls].reverse().find(c => c.tool === 'Agent' && !c.agentId);
193
+ if (!lastDispatch) return null;
194
+
195
+ // Check if dispatch tier is execute/edit
196
+ const tier = (lastDispatch.meta.tier || '').toLowerCase();
197
+ if (!tier.includes('execute') && !tier.includes('edit') && !tier.includes('implement')) return null;
198
+
199
+ // Look for reads/searches before this dispatch in recent window
200
+ const dispatchIdx = recentCalls.indexOf(lastDispatch);
201
+ const preceding = recentCalls.slice(Math.max(0, dispatchIdx - 8), dispatchIdx);
202
+ const hasResearch = preceding.some(c =>
203
+ c.tool === 'Read' || c.tool === 'Grep' || c.tool === 'Glob' ||
204
+ (c.tool === 'Agent' && (c.meta.tier || '').toLowerCase().includes('search'))
205
+ );
206
+
207
+ if (!hasResearch) {
208
+ return {
209
+ ts: Date.now(),
210
+ type: 'assumption-leap',
211
+ severity: 'high',
212
+ observation: 'Dispatching work without prior research — consider a search agent first.',
213
+ surfaced: false,
214
+ };
215
+ }
216
+ return null;
217
+ }
218
+
219
+ function detectScopeCreep(state) {
220
+ const totalCalls = state.toolCalls.length;
221
+ if (totalCalls < 10) return null;
222
+
223
+ const earlyWindow = state.toolCalls.slice(0, Math.ceil(totalCalls * 0.2));
224
+ const earlyFiles = new Set();
225
+ for (const c of earlyWindow) {
226
+ if (c.meta?.file) earlyFiles.add(c.meta.file);
227
+ }
228
+
229
+ const declaredScope = Math.max(earlyFiles.size, 1);
230
+ const currentScope = state.stats.uniqueFiles.length;
231
+
232
+ if (currentScope >= declaredScope * 2 && currentScope > 4) {
233
+ return {
234
+ ts: Date.now(),
235
+ type: 'scope-creep',
236
+ severity: 'medium',
237
+ observation: `Scope has grown beyond declared plan. Started with ~${declaredScope} files, now touching ${currentScope}.`,
238
+ surfaced: false,
239
+ };
240
+ }
241
+ return null;
242
+ }
243
+
244
+ function detectCeremony(state) {
245
+ // More than 5 Reads of config/settings without a dispatch in between
246
+ const recentCalls = state.toolCalls.slice(-15);
247
+ let readStreak = 0;
248
+
249
+ for (let i = recentCalls.length - 1; i >= 0; i--) {
250
+ const call = recentCalls[i];
251
+ if (call.tool === 'Agent') break;
252
+ if (call.tool === 'Read') {
253
+ const file = call.meta?.file || '';
254
+ if (/config|settings|\.json|\.env|\.ya?ml/i.test(file)) {
255
+ readStreak++;
256
+ }
257
+ }
258
+ }
259
+
260
+ if (readStreak > 5) {
261
+ return {
262
+ ts: Date.now(),
263
+ type: 'ceremony',
264
+ severity: 'low',
265
+ observation: 'Consider dispatching a research agent instead of manual exploration.',
266
+ surfaced: false,
267
+ };
268
+ }
269
+ return null;
270
+ }
271
+
272
+ function detectStuckLoop(state) {
273
+ const recentCalls = state.toolCalls.slice(-10);
274
+ if (recentCalls.length < 3) return null;
275
+
276
+ // Group by tool + simplified input signature
277
+ const signatures = new Map();
278
+ for (const call of recentCalls) {
279
+ let sig = call.tool;
280
+ if (call.meta?.file) sig += ':' + call.meta.file;
281
+ else if (call.meta?.command) sig += ':' + call.meta.command.slice(0, 40);
282
+ else if (call.meta?.pattern) sig += ':' + call.meta.pattern;
283
+
284
+ signatures.set(sig, (signatures.get(sig) || 0) + 1);
285
+ }
286
+
287
+ for (const [sig, count] of signatures) {
288
+ if (count >= 5) {
289
+ return {
290
+ ts: Date.now(),
291
+ type: 'stuck-loop',
292
+ severity: 'high',
293
+ observation: `Possible stuck loop — try a different approach. (${sig.split(':')[0]} called ${count} times with similar inputs)`,
294
+ surfaced: false,
295
+ };
296
+ }
297
+ }
298
+ return null;
299
+ }
300
+
301
+ // ---------------------------------------------------------------------------
302
+ // Run all detectors
303
+ // ---------------------------------------------------------------------------
304
+
305
+ function runDetectors(state) {
306
+ const results = [];
307
+ const detectors = [
308
+ detectSequentialDispatch,
309
+ detectReReads,
310
+ detectAssumptionLeap,
311
+ detectScopeCreep,
312
+ detectCeremony,
313
+ detectStuckLoop,
314
+ ];
315
+
316
+ for (const detector of detectors) {
317
+ try {
318
+ const result = detector(state);
319
+ if (result) {
320
+ // Deduplicate: don't re-add if same type was noticed in last 60s
321
+ const recent = state.noticings.filter(
322
+ n => n.type === result.type && Date.now() - n.ts < 60_000
323
+ );
324
+ if (recent.length === 0) {
325
+ results.push(result);
326
+ }
327
+ }
328
+ } catch {}
329
+ }
330
+
331
+ return results;
332
+ }
333
+
334
+ // ---------------------------------------------------------------------------
335
+ // Public API: readDiagnosticNoticings (for head.mjs integration)
336
+ // ---------------------------------------------------------------------------
337
+
338
+ /**
339
+ * Read unsurfaced diagnostic noticings and mark them as surfaced.
340
+ * Called by head.mjs notice() to feed diagnostic observations into deliberation.
341
+ */
342
+ export function readDiagnosticNoticings() {
343
+ try {
344
+ if (!existsSync(STATE_FILE)) return [];
345
+ const state = JSON.parse(readFileSync(STATE_FILE, 'utf8'));
346
+ const unsurfaced = (state.noticings || []).filter(n => !n.surfaced);
347
+ if (unsurfaced.length === 0) return [];
348
+
349
+ // Mark as surfaced
350
+ for (const n of state.noticings) {
351
+ if (!n.surfaced) n.surfaced = true;
352
+ }
353
+ writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
354
+
355
+ return unsurfaced;
356
+ } catch {
357
+ return [];
358
+ }
359
+ }
360
+
361
+ // ---------------------------------------------------------------------------
362
+ // Main — read stdin, record, detect, respond
363
+ // ---------------------------------------------------------------------------
364
+
365
+ async function main() {
366
+ let raw = '';
367
+ try {
368
+ for await (const chunk of process.stdin) {
369
+ raw += chunk;
370
+ if (raw.length > 64 * 1024) break;
371
+ }
372
+ } catch {}
373
+
374
+ let payload = {};
375
+ try {
376
+ payload = JSON.parse(raw);
377
+ } catch {}
378
+
379
+ const toolName = payload?.tool_name || payload?.toolName || 'unknown';
380
+ const toolInput = payload?.tool_input || payload?.toolInput || {};
381
+ const agentId = payload?.agent_id || payload?.agentId || null;
382
+
383
+ // Load state
384
+ const state = loadState();
385
+
386
+ // Record the tool call
387
+ recordToolCall(state, toolName, toolInput, agentId);
388
+
389
+ // Run pattern detectors
390
+ const newNoticings = runDetectors(state);
391
+
392
+ // Add new noticings to state
393
+ for (const n of newNoticings) {
394
+ state.noticings.push(n);
395
+ }
396
+
397
+ // Save state
398
+ saveState(state);
399
+
400
+ // Determine output
401
+ // For HEAD (no agent_id): high-severity → systemMessage
402
+ // For subagents (agent_id present): only log, never inject systemMessage
403
+ let output = {};
404
+
405
+ if (!agentId) {
406
+ const highSeverity = newNoticings.filter(n => n.severity === 'high');
407
+ if (highSeverity.length > 0) {
408
+ const messages = highSeverity.map(n => `[Diagnostic] ${n.observation}`);
409
+ output = { systemMessage: messages.join('\n') };
410
+ // Mark as surfaced
411
+ for (const n of highSeverity) {
412
+ n.surfaced = true;
413
+ }
414
+ saveState(state);
415
+ }
416
+ }
417
+
418
+ process.stdout.write(JSON.stringify(output) + '\n');
419
+ process.exit(0);
420
+ }
421
+
422
+ main();
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env node
2
+ // precompact.mjs — Fires before context compression to persist critical state.
3
+ // Ensures HEAD's running narrative, simmer buffer, and loop state survive
4
+ // context window compression without loss.
5
+
6
+ import { persist as persistNarrative, load as loadNarrative } from '../src/narrative.mjs';
7
+ import { active as activeSimmer, prune as pruneSimmer } from '../src/simmer.mjs';
8
+ import { getLoopStatus } from '../src/cognitive-loop.mjs';
9
+ import { existsSync, writeFileSync, mkdirSync } from 'node:fs';
10
+ import { join } from 'node:path';
11
+
12
+ const STATE_DIR = join(process.cwd(), '.dualbrain');
13
+ const SURVIVAL_FILE = join(STATE_DIR, 'precompact-survival.json');
14
+
15
+ async function main() {
16
+ // Read stdin (hook payload) — we don't need it but must consume
17
+ let raw = '';
18
+ try {
19
+ for await (const chunk of process.stdin) {
20
+ raw += chunk;
21
+ if (raw.length > 16 * 1024) break;
22
+ }
23
+ } catch {}
24
+
25
+ // Persist narrative (already on disk, but archive a snapshot)
26
+ const narrativeText = persistNarrative();
27
+
28
+ // Prune dead simmer items before compression
29
+ pruneSimmer();
30
+ const simmering = activeSimmer();
31
+
32
+ // Get loop status for survival kit
33
+ const loopStatus = getLoopStatus();
34
+
35
+ // Write survival kit — this can be loaded to reconstruct context after compression
36
+ const survivalKit = {
37
+ timestamp: Date.now(),
38
+ reason: 'precompact',
39
+ narrative: narrativeText.slice(0, 1500),
40
+ simmerCount: simmering.length,
41
+ topSimmer: simmering.slice(0, 5).map(i => ({ idea: i.idea.slice(0, 100), heat: i.heat })),
42
+ loopStatus,
43
+ };
44
+
45
+ mkdirSync(STATE_DIR, { recursive: true });
46
+ writeFileSync(SURVIVAL_FILE, JSON.stringify(survivalKit, null, 2));
47
+
48
+ // Output: no systemMessage needed — this is a persistence hook, not advisory
49
+ process.stdout.write(JSON.stringify({}) + '\n');
50
+ process.exit(0);
51
+ }
52
+
53
+ main();
@@ -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.16",
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",