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.
@@ -95,6 +95,18 @@ async function getLivingDocs() {
95
95
  return _livingDocs;
96
96
  }
97
97
 
98
+ let _cognitiveLoopCache = null;
99
+ async function _getCognitiveLoop() {
100
+ if (!_cognitiveLoopCache) {
101
+ try {
102
+ _cognitiveLoopCache = await import('../src/cognitive-loop.mjs');
103
+ } catch {
104
+ _cognitiveLoopCache = null;
105
+ }
106
+ }
107
+ return _cognitiveLoopCache;
108
+ }
109
+
98
110
  let _fx = null;
99
111
  async function getFx() {
100
112
  if (_fx !== null) return _fx;
@@ -492,6 +504,54 @@ async function cmdGo(args, opts = {}) {
492
504
  } catch { /* non-fatal */ }
493
505
  }
494
506
 
507
+ // ── Cognitive loop: drive dispatch decisions ──────────────────────────────
508
+ let loopEnhancedPrompt = prompt;
509
+ let loopDispatchMeta = null;
510
+ try {
511
+ const cogLoop = await _getCognitiveLoop();
512
+ if (cogLoop) {
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
+
520
+ if (loopResult.phase === 'dispatch' && loopResult.nextDispatch) {
521
+ loopDispatchMeta = loopResult;
522
+ // Use the full envelope prompt (includes context, preventions, debrief)
523
+ const firstAgent = loopResult.nextDispatch.agents?.[0];
524
+ if (firstAgent?.prompt) {
525
+ loopEnhancedPrompt = firstAgent.prompt;
526
+ }
527
+ if (verbose && loopResult.plan) {
528
+ const wc = loopResult.plan.waves?.length || 0;
529
+ console.log(` [cognitive-loop] Plan: ${wc} wave(s), phase: ${loopResult.phase}`);
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}`));
549
+ }
550
+ }
551
+ } catch {
552
+ // Cognitive loop unavailable or errored — proceed with original prompt
553
+ }
554
+
495
555
  // ── Dispatch visualization ─────────────────────────────────────────────────
496
556
  const fxGo = await getFx();
497
557
  let dispatchSpinner = null;
@@ -499,7 +559,7 @@ async function cmdGo(args, opts = {}) {
499
559
  dispatchSpinner = fxGo.spinner(`Dispatching agent...`).start();
500
560
  }
501
561
 
502
- const { plan, result } = await runPipeline('go', prompt, {
562
+ const { plan, result } = await runPipeline('go', loopEnhancedPrompt, {
503
563
  files,
504
564
  cwd,
505
565
  verbose,
@@ -511,6 +571,23 @@ async function cmdGo(args, opts = {}) {
511
571
  dispatchSpinner.succeed(`Agent dispatched: ${prompt.slice(0, 50)}`);
512
572
  }
513
573
 
574
+ // ── Cognitive loop: advance after dispatch completes ─────────────────────────
575
+ if (loopDispatchMeta && result && !dryRun) {
576
+ try {
577
+ const cogLoop = await _getCognitiveLoop();
578
+ if (cogLoop) {
579
+ const waveId = loopDispatchMeta.nextDispatch.waveId;
580
+ const rawResults = [result.summary || result.output || ''];
581
+ const advanceResult = cogLoop.advance(rawResults, waveId, { files });
582
+ if (verbose && advanceResult) {
583
+ console.log(` [cognitive-loop] Next phase: ${advanceResult.phase}, rationale: ${advanceResult.rationale || '-'}`);
584
+ }
585
+ }
586
+ } catch {
587
+ // Non-fatal — loop advance failure doesn't affect the completed dispatch
588
+ }
589
+ }
590
+
514
591
  if (dryRun) {
515
592
  // formatExecutionPlan already printed by pipeline when verbose/dryRun=true
516
593
  console.log('\n(dry-run — not executing)');
@@ -2149,6 +2226,48 @@ function classifyInput(input) {
2149
2226
  }
2150
2227
 
2151
2228
  // ── HEAD cognitive pipeline: replaces regex-based cheap/full split ──────
2229
+ // Try cognitive loop first (wraps HEAD with wave planning + predictions)
2230
+ if (_cognitiveLoopCache) {
2231
+ try {
2232
+ const loopResult = _cognitiveLoopCache.enter(trimmed, {});
2233
+
2234
+ const judgment = {
2235
+ depth: loopResult.action?.depth || 'full',
2236
+ action: loopResult.action,
2237
+ shouldAskUser: loopResult.shouldAskUser,
2238
+ shouldDispatch: loopResult.phase === 'dispatch',
2239
+ shouldClarify: loopResult.action?.type === 'clarify',
2240
+ shouldThink: loopResult.action?.type === 'think',
2241
+ rationale: loopResult.rationale,
2242
+ confidence: loopResult.action?.confidence,
2243
+ obligations: loopResult.action?.obligations,
2244
+ surfaceNoticings: loopResult.surfaceNoticings,
2245
+ // Cognitive loop extensions
2246
+ _loopResult: loopResult,
2247
+ _plan: loopResult.plan,
2248
+ _nextDispatch: loopResult.nextDispatch,
2249
+ };
2250
+
2251
+ // Loop says respond — no dispatch needed
2252
+ if (loopResult.phase === 'respond') {
2253
+ return { tier: 'cheap', headJudgment: judgment };
2254
+ }
2255
+
2256
+ // Loop says dispatch — full tier, use plan's first agent tier to pick model
2257
+ if (loopResult.phase === 'dispatch') {
2258
+ const firstAgent = loopResult.nextDispatch?.agents?.[0];
2259
+ const model = firstAgent?.tier === 'deep' || firstAgent?.tier === 'opus' ? 'opus' : 'sonnet';
2260
+ return { tier: 'full', headJudgment: judgment, model };
2261
+ }
2262
+
2263
+ // Default: cheap
2264
+ return { tier: 'cheap', headJudgment: judgment };
2265
+ } catch {
2266
+ // Cognitive loop failed — fall through to direct HEAD
2267
+ }
2268
+ }
2269
+
2270
+ // Direct HEAD fallback (when cognitive loop unavailable or errored)
2152
2271
  const head = _headModuleCache;
2153
2272
  if (head) {
2154
2273
  const state = _getHeadState() || head.freshState();
@@ -2722,6 +2841,19 @@ async function mainScreen(rl, ask) {
2722
2841
  return signalLine(item.ok ? 'success' : 'warning', `${DIM}${item.text}${RST}`);
2723
2842
  });
2724
2843
 
2844
+ // ── Cognitive loop status (appended to signals) ────────────────────────────
2845
+ try {
2846
+ const cogLoop = await _getCognitiveLoop();
2847
+ if (cogLoop) {
2848
+ const loopStatus = cogLoop.getLoopStatus();
2849
+ if (loopStatus.hasActivePlan) {
2850
+ const wavePart = `${loopStatus.completedWaves}/${loopStatus.totalWaves} waves`;
2851
+ const replanPart = loopStatus.replans > 0 ? ` · ${loopStatus.replans} replan${loopStatus.replans !== 1 ? 's' : ''}` : '';
2852
+ recentLines.push(signalLine('info', `${DIM}[loop] ${wavePart}${replanPart}${RST}`));
2853
+ }
2854
+ }
2855
+ } catch { /* non-fatal */ }
2856
+
2725
2857
  // ── Resolve dashboard spinner before rendering ────────────────────────────
2726
2858
  if (_spinnerTimeout) clearTimeout(_spinnerTimeout);
2727
2859
  if (dashSpinner) dashSpinner.succeed('Dashboard ready');
@@ -3064,6 +3196,18 @@ async function mainScreen(rl, ask) {
3064
3196
  process.stdout.write('\n');
3065
3197
  }
3066
3198
 
3199
+ // Show cognitive loop plan info if available
3200
+ if (hj?._plan) {
3201
+ const plan = hj._plan;
3202
+ const waveCount = plan.waves?.length || 0;
3203
+ const agentCount = plan.waves?.reduce((sum, w) => sum + (w.agents?.length || 0), 0) || 0;
3204
+ process.stdout.write(`\n \x1b[2m[plan] ${waveCount} wave${waveCount !== 1 ? 's' : ''}, ${agentCount} agent${agentCount !== 1 ? 's' : ''}\x1b[0m`);
3205
+ if (hj._nextDispatch?.warnings?.length > 0) {
3206
+ process.stdout.write(` \x1b[33m${hj._nextDispatch.warnings.length} warning(s)\x1b[0m`);
3207
+ }
3208
+ process.stdout.write('\n');
3209
+ }
3210
+
3067
3211
  // HEAD's shouldAskUser gates the dispatch — dangerous/irreversible ops
3068
3212
  if (hj?.shouldAskUser) {
3069
3213
  const reason = hj.obligations?.find(o => o.type === 'askBeforeIrreversi')?.description || hj.rationale;
@@ -3073,14 +3217,14 @@ async function mainScreen(rl, ask) {
3073
3217
  process.stdout.write(` \x1b[36mEnter\x1b[0m proceed \x1b[36mn\x1b[0m cancel\n\n`);
3074
3218
  const confirm = (await ask(' > ')).trim().toLowerCase();
3075
3219
  if (confirm === 'n' || confirm === 'no') return { next: 'main' };
3076
- return { next: 'go', prompt: input, model };
3220
+ return { next: 'go', prompt: input, model, _loopResult: hj._loopResult };
3077
3221
  }
3078
3222
 
3079
3223
  // Automode: if HEAD says it's safe, just go — no confirmation needed
3080
3224
  const automode = profile.automode ?? profile.settings?.automode ?? false;
3081
3225
  if (automode) {
3082
3226
  process.stdout.write(`\n \x1b[36m⚡\x1b[0m ${summary} (${model}, depth: ${hj?.depth || '?'})\n`);
3083
- return { next: 'go', prompt: input, model };
3227
+ return { next: 'go', prompt: input, model, _loopResult: hj._loopResult };
3084
3228
  }
3085
3229
 
3086
3230
  // Manual mode — show depth, wait for confirmation
@@ -3089,7 +3233,7 @@ async function mainScreen(rl, ask) {
3089
3233
  process.stdout.write(` \x1b[36mEnter\x1b[0m go \x1b[36mn\x1b[0m cancel\n\n`);
3090
3234
  const confirm = (await ask(' > ')).trim().toLowerCase();
3091
3235
  if (confirm === 'n' || confirm === 'no') return { next: 'main' };
3092
- return { next: 'go', prompt: input, model };
3236
+ return { next: 'go', prompt: input, model, _loopResult: hj._loopResult };
3093
3237
  }
3094
3238
 
3095
3239
  // Default fallback
@@ -6202,6 +6346,7 @@ async function main() {
6202
6346
  primeAgentRegistry().catch(() => {});
6203
6347
  _primeRegistryCache().catch(() => {});
6204
6348
  _getHeadModule().catch(() => {});
6349
+ _getCognitiveLoop().catch(() => {});
6205
6350
 
6206
6351
  const args = process.argv.slice(2);
6207
6352
  const cmd = args[0];
@@ -1,59 +1,58 @@
1
1
  #!/usr/bin/env node
2
- // auto-update-wrapper.mjs — PostToolUse hook (Node.js entry point).
3
- // Runs once per session, checks for dual-brain updates, installs silently.
4
- // The parent exits immediately; all npm work happens in a detached child.
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 { spawn } from 'child_process';
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 = resolve(__dirname, '..');
13
- const STATE_DIR = join(WORKSPACE, '.dualbrain');
14
- const LOCK_FILE = join(STATE_DIR, '.update-checked');
15
- const TWENTY_FOUR_HOURS = 24 * 60 * 60 * 1000;
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 recently? ─────────────────────────────────────────────
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 < TWENTY_FOUR_HOURS) {
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. Write lock BEFORE spawning (prevents concurrent session races) ─────────
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.exit(0); // Can't write state — bail silently
36
+ process.stdout.write('{}\n');
37
+ process.exit(0);
35
38
  }
36
39
 
37
- // ── 3. Resolve local version ─────────────────────────────────────────────────
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) process.exit(0);
47
-
48
- // ── 4. Detach background worker — parent returns immediately ─────────────────
49
- // The worker script is inlined as a node -e string to avoid needing a temp file.
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
- // 3-second timeout npm check
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) process.exit(0);
65
-
66
- // Compare: is latest strictly greater than local?
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
- if (!versionGt(localVersion, latestVersion)) process.exit(0);
68
+ // ── 5. Install immediately — no detach, no background, just do it ────────────
69
+ process.stderr.write(`[dual-brain] updating ${localVersion} → ${latestVersion}\n`);
79
70
 
80
- // Newer version found print notice then install
81
- process.stderr.write('dual-brain: updating v' + localVersion + ' → ' + latestVersion + '...\\n');
82
-
83
- spawnSync('npx', ['-y', 'dual-brain@latest', '--quiet'], {
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
- const child = spawn(
90
- process.execPath,
91
- ['--input-type=module'],
92
- {
93
- detached: true,
94
- stdio: ['pipe', 'ignore', 'ignore'],
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
+ }