dual-brain 0.2.15 → 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.
- package/bin/dual-brain.mjs +28 -9
- package/hooks/auto-update-wrapper.mjs +56 -58
- package/hooks/diagnostic-companion.mjs +1 -1
- package/package.json +1 -1
- package/src/cognitive-loop.mjs +25 -0
- package/src/continuity.mjs +3 -2
package/bin/dual-brain.mjs
CHANGED
|
@@ -504,29 +504,48 @@ async function cmdGo(args, opts = {}) {
|
|
|
504
504
|
} catch { /* non-fatal */ }
|
|
505
505
|
}
|
|
506
506
|
|
|
507
|
-
// ── Cognitive loop:
|
|
507
|
+
// ── Cognitive loop: drive dispatch decisions ──────────────────────────────
|
|
508
508
|
let loopEnhancedPrompt = prompt;
|
|
509
509
|
let loopDispatchMeta = null;
|
|
510
510
|
try {
|
|
511
511
|
const cogLoop = await _getCognitiveLoop();
|
|
512
512
|
if (cogLoop) {
|
|
513
513
|
const loopResult = cogLoop.enter(prompt, { files });
|
|
514
|
+
|
|
515
|
+
if (loopResult.phase === 'readonly') {
|
|
516
|
+
console.log('\n⚠ Another dual-brain session is active. This session is read-only.');
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
|
|
514
520
|
if (loopResult.phase === 'dispatch' && loopResult.nextDispatch) {
|
|
515
521
|
loopDispatchMeta = loopResult;
|
|
516
|
-
//
|
|
522
|
+
// Use the full envelope prompt (includes context, preventions, debrief)
|
|
517
523
|
const firstAgent = loopResult.nextDispatch.agents?.[0];
|
|
518
|
-
if (firstAgent) {
|
|
519
|
-
|
|
520
|
-
if (firstAgent.preventions) extras.push(firstAgent.preventions);
|
|
521
|
-
if (firstAgent.debriefInstruction) extras.push(firstAgent.debriefInstruction);
|
|
522
|
-
if (extras.length > 0) {
|
|
523
|
-
loopEnhancedPrompt = prompt + '\n\n' + extras.join('\n\n');
|
|
524
|
-
}
|
|
524
|
+
if (firstAgent?.prompt) {
|
|
525
|
+
loopEnhancedPrompt = firstAgent.prompt;
|
|
525
526
|
}
|
|
526
527
|
if (verbose && loopResult.plan) {
|
|
527
528
|
const wc = loopResult.plan.waves?.length || 0;
|
|
528
529
|
console.log(` [cognitive-loop] Plan: ${wc} wave(s), phase: ${loopResult.phase}`);
|
|
529
530
|
}
|
|
531
|
+
} else if (loopResult.phase === 'blocked') {
|
|
532
|
+
console.log(`\n⚠ Dispatch blocked: ${loopResult.suggestion || 'readiness check failed'}`);
|
|
533
|
+
if (loopResult.surfaceNoticings?.length) {
|
|
534
|
+
loopResult.surfaceNoticings.forEach(n => console.log(` → ${n}`));
|
|
535
|
+
}
|
|
536
|
+
return;
|
|
537
|
+
} else if (loopResult.phase === 'respond') {
|
|
538
|
+
// HEAD decided no dispatch needed — show rationale
|
|
539
|
+
if (loopResult.rationale) console.log(`\n${loopResult.rationale}`);
|
|
540
|
+
if (loopResult.surfaceNoticings?.length) {
|
|
541
|
+
loopResult.surfaceNoticings.forEach(n => console.log(` → ${n}`));
|
|
542
|
+
}
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Surface noticings (includes update notices, diagnostics)
|
|
547
|
+
if (loopResult.surfaceNoticings?.length && verbose) {
|
|
548
|
+
loopResult.surfaceNoticings.forEach(n => console.log(` → ${n}`));
|
|
530
549
|
}
|
|
531
550
|
}
|
|
532
551
|
} catch {
|
|
@@ -1,59 +1,58 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// auto-update-wrapper.mjs — PostToolUse hook
|
|
3
|
-
//
|
|
4
|
-
//
|
|
2
|
+
// auto-update-wrapper.mjs — PostToolUse hook.
|
|
3
|
+
// Checks for updates once per session. If found, installs immediately (no wait,
|
|
4
|
+
// no question) and writes a notice so HEAD can mention it naturally.
|
|
5
|
+
// "Oh, I updated to 0.2.16" — not a prompt, just awareness.
|
|
5
6
|
|
|
6
|
-
import {
|
|
7
|
+
import { spawnSync } from 'child_process';
|
|
7
8
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
8
9
|
import { join, dirname, resolve } from 'path';
|
|
9
10
|
import { fileURLToPath } from 'url';
|
|
10
11
|
|
|
11
12
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
|
-
const WORKSPACE
|
|
13
|
-
const STATE_DIR
|
|
14
|
-
const LOCK_FILE
|
|
15
|
-
const
|
|
13
|
+
const WORKSPACE = resolve(__dirname, '..');
|
|
14
|
+
const STATE_DIR = join(WORKSPACE, '.dualbrain');
|
|
15
|
+
const LOCK_FILE = join(STATE_DIR, '.update-checked');
|
|
16
|
+
const NOTICE_FILE = join(STATE_DIR, '.update-notice');
|
|
17
|
+
const SESSION_TTL = 30 * 60 * 1000; // Once per 30 min session
|
|
16
18
|
|
|
17
|
-
// ── 1. Already checked
|
|
19
|
+
// ── 1. Already checked this session? ─────────────────────────────────────────
|
|
18
20
|
if (existsSync(LOCK_FILE)) {
|
|
19
21
|
try {
|
|
20
22
|
const lastCheck = parseInt(readFileSync(LOCK_FILE, 'utf8').trim(), 10);
|
|
21
|
-
if (Number.isFinite(lastCheck) && Date.now() - lastCheck <
|
|
23
|
+
if (Number.isFinite(lastCheck) && Date.now() - lastCheck < SESSION_TTL) {
|
|
24
|
+
// Output empty JSON (no hook action)
|
|
25
|
+
process.stdout.write('{}\n');
|
|
22
26
|
process.exit(0);
|
|
23
27
|
}
|
|
24
|
-
} catch {
|
|
25
|
-
// Corrupt lock file — proceed with check
|
|
26
|
-
}
|
|
28
|
+
} catch {}
|
|
27
29
|
}
|
|
28
30
|
|
|
29
|
-
// ── 2.
|
|
31
|
+
// ── 2. Mark as checked ───────────────────────────────────────────────────────
|
|
30
32
|
try {
|
|
31
33
|
mkdirSync(STATE_DIR, { recursive: true });
|
|
32
34
|
writeFileSync(LOCK_FILE, String(Date.now()));
|
|
33
35
|
} catch {
|
|
34
|
-
process.
|
|
36
|
+
process.stdout.write('{}\n');
|
|
37
|
+
process.exit(0);
|
|
35
38
|
}
|
|
36
39
|
|
|
37
|
-
// ── 3.
|
|
40
|
+
// ── 3. Get local version ─────────────────────────────────────────────────────
|
|
38
41
|
let localVersion = '';
|
|
39
42
|
try {
|
|
40
43
|
const pkg = JSON.parse(readFileSync(join(WORKSPACE, 'package.json'), 'utf8'));
|
|
41
44
|
localVersion = pkg.version || '';
|
|
42
45
|
} catch {
|
|
46
|
+
process.stdout.write('{}\n');
|
|
43
47
|
process.exit(0);
|
|
44
48
|
}
|
|
45
49
|
|
|
46
|
-
if (!localVersion)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const workerScript = `
|
|
51
|
-
import { spawnSync } from 'child_process';
|
|
52
|
-
import { readFileSync } from 'fs';
|
|
53
|
-
|
|
54
|
-
const localVersion = ${JSON.stringify(localVersion)};
|
|
50
|
+
if (!localVersion) {
|
|
51
|
+
process.stdout.write('{}\n');
|
|
52
|
+
process.exit(0);
|
|
53
|
+
}
|
|
55
54
|
|
|
56
|
-
//
|
|
55
|
+
// ── 4. Check registry (fast, 3s timeout) ─────────────────────────────────────
|
|
57
56
|
const npmResult = spawnSync('npm', ['view', 'dual-brain', 'version'], {
|
|
58
57
|
encoding: 'utf8',
|
|
59
58
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
@@ -61,42 +60,41 @@ const npmResult = spawnSync('npm', ['view', 'dual-brain', 'version'], {
|
|
|
61
60
|
});
|
|
62
61
|
|
|
63
62
|
const latestVersion = (npmResult.stdout || '').trim();
|
|
64
|
-
if (!latestVersion
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
function versionGt(a, b) {
|
|
68
|
-
const ap = a.replace(/^v/i, '').split('.').map(n => parseInt(n, 10) || 0);
|
|
69
|
-
const bp = b.replace(/^v/i, '').split('.').map(n => parseInt(n, 10) || 0);
|
|
70
|
-
const len = Math.max(ap.length, bp.length);
|
|
71
|
-
for (let i = 0; i < len; i++) {
|
|
72
|
-
const diff = (bp[i] || 0) - (ap[i] || 0);
|
|
73
|
-
if (diff !== 0) return diff > 0;
|
|
74
|
-
}
|
|
75
|
-
return false;
|
|
63
|
+
if (!latestVersion || !_isNewer(localVersion, latestVersion)) {
|
|
64
|
+
process.stdout.write('{}\n');
|
|
65
|
+
process.exit(0);
|
|
76
66
|
}
|
|
77
67
|
|
|
78
|
-
|
|
68
|
+
// ── 5. Install immediately — no detach, no background, just do it ────────────
|
|
69
|
+
process.stderr.write(`[dual-brain] updating ${localVersion} → ${latestVersion}\n`);
|
|
79
70
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
stdio: 'ignore',
|
|
85
|
-
timeout: 120000,
|
|
71
|
+
const installResult = spawnSync('npm', ['install', '-g', `dual-brain@${latestVersion}`], {
|
|
72
|
+
encoding: 'utf8',
|
|
73
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
74
|
+
timeout: 30000,
|
|
86
75
|
});
|
|
87
|
-
`;
|
|
88
76
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
child.stdin.write(workerScript);
|
|
99
|
-
child.stdin.end();
|
|
100
|
-
child.unref(); // Let parent exit without waiting
|
|
77
|
+
if (installResult.status === 0) {
|
|
78
|
+
// Write notice for HEAD to pick up
|
|
79
|
+
const notice = { from: localVersion, to: latestVersion, ts: Date.now() };
|
|
80
|
+
writeFileSync(NOTICE_FILE, JSON.stringify(notice));
|
|
81
|
+
process.stderr.write(`[dual-brain] updated to ${latestVersion}\n`);
|
|
82
|
+
} else {
|
|
83
|
+
process.stderr.write(`[dual-brain] update failed, continuing with ${localVersion}\n`);
|
|
84
|
+
}
|
|
101
85
|
|
|
86
|
+
process.stdout.write('{}\n');
|
|
102
87
|
process.exit(0);
|
|
88
|
+
|
|
89
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
function _isNewer(local, remote) {
|
|
92
|
+
const lp = local.split('.').map(Number);
|
|
93
|
+
const rp = remote.split('.').map(Number);
|
|
94
|
+
for (let i = 0; i < Math.max(lp.length, rp.length); i++) {
|
|
95
|
+
const diff = (rp[i] || 0) - (lp[i] || 0);
|
|
96
|
+
if (diff > 0) return true;
|
|
97
|
+
if (diff < 0) return false;
|
|
98
|
+
}
|
|
99
|
+
return false;
|
|
100
|
+
}
|
package/package.json
CHANGED
package/src/cognitive-loop.mjs
CHANGED
|
@@ -86,6 +86,9 @@ export function enter(userMessage, context = {}) {
|
|
|
86
86
|
const headState = loadState();
|
|
87
87
|
const loopState = loadLoopState();
|
|
88
88
|
|
|
89
|
+
// Check if we just auto-updated — surface it naturally
|
|
90
|
+
_checkUpdateNotice(context);
|
|
91
|
+
|
|
89
92
|
// Immersion: load memory tiers so HEAD is "in the song"
|
|
90
93
|
const memory = memoryTiers.assemble({
|
|
91
94
|
userMessage,
|
|
@@ -119,6 +122,12 @@ export function enter(userMessage, context = {}) {
|
|
|
119
122
|
confidence: turn.result.confidence.score,
|
|
120
123
|
});
|
|
121
124
|
|
|
125
|
+
// Surface update notice as a noticing
|
|
126
|
+
if (context._updateNotice) {
|
|
127
|
+
turn.result.surfaceNoticings = turn.result.surfaceNoticings || [];
|
|
128
|
+
turn.result.surfaceNoticings.unshift(context._updateNotice);
|
|
129
|
+
}
|
|
130
|
+
|
|
122
131
|
// Narrative evolution: update HEAD's running understanding
|
|
123
132
|
_evolveNarrative(turn, userMessage, context);
|
|
124
133
|
|
|
@@ -504,6 +513,22 @@ function _postWaveReflection(waveSummary, loopState) {
|
|
|
504
513
|
narrative.evolve(parts.join(' '));
|
|
505
514
|
}
|
|
506
515
|
|
|
516
|
+
/**
|
|
517
|
+
* Check for update notice and surface it to HEAD's awareness.
|
|
518
|
+
*/
|
|
519
|
+
function _checkUpdateNotice(context) {
|
|
520
|
+
try {
|
|
521
|
+
const noticeFile = join(LOOP_STATE_DIR, '.update-notice');
|
|
522
|
+
if (!existsSync(noticeFile)) return;
|
|
523
|
+
const notice = JSON.parse(readFileSync(noticeFile, 'utf8'));
|
|
524
|
+
if (Date.now() - notice.ts < 5 * 60 * 1000) {
|
|
525
|
+
context._updateNotice = `Updated dual-brain ${notice.from} → ${notice.to}`;
|
|
526
|
+
}
|
|
527
|
+
// Clear it after reading (one-shot)
|
|
528
|
+
try { writeFileSync(noticeFile, ''); } catch {}
|
|
529
|
+
} catch {}
|
|
530
|
+
}
|
|
531
|
+
|
|
507
532
|
// ── Query functions ─────────────────────────────────────────────────────────
|
|
508
533
|
|
|
509
534
|
export function getActivePlan() {
|
package/src/continuity.mjs
CHANGED
|
@@ -32,13 +32,13 @@ import { join } from 'node:path';
|
|
|
32
32
|
*/
|
|
33
33
|
export function generateHandoff(sessionState) {
|
|
34
34
|
return {
|
|
35
|
-
version:
|
|
35
|
+
version: 2,
|
|
36
36
|
timestamp: new Date().toISOString(),
|
|
37
37
|
task: sessionState.taskDescription || null,
|
|
38
38
|
progress: {
|
|
39
39
|
filesChanged: (sessionState.filesChanged || []).slice(0, 20),
|
|
40
40
|
testsRun: sessionState.testsRun || [],
|
|
41
|
-
decisions: (sessionState.decisions || []).slice(0, 5),
|
|
41
|
+
decisions: (sessionState.decisions || []).slice(0, 5),
|
|
42
42
|
},
|
|
43
43
|
unresolved: (sessionState.unresolved || []).slice(0, 5),
|
|
44
44
|
routing: {
|
|
@@ -48,6 +48,7 @@ export function generateHandoff(sessionState) {
|
|
|
48
48
|
},
|
|
49
49
|
preferences: sessionState.activePreferences || [],
|
|
50
50
|
resumeHint: sessionState.resumeHint || null,
|
|
51
|
+
narrative: sessionState.narrative || null,
|
|
51
52
|
};
|
|
52
53
|
}
|
|
53
54
|
|