hebbian 0.3.4 → 0.5.0

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.
@@ -18,7 +18,7 @@ function resolveBrainRoot(brainFlag) {
18
18
  if (existsSync(resolve("./brain"))) return resolve("./brain");
19
19
  return resolve(process.env.HOME || "~", "hebbian", "brain");
20
20
  }
21
- var REGIONS, REGION_PRIORITY, REGION_ICONS, REGION_KO, EMIT_THRESHOLD, SPOTLIGHT_DAYS, JACCARD_THRESHOLD, MAX_DEPTH, EMIT_TARGETS, SIGNAL_TYPES, MARKER_START, MARKER_END, HOOK_MARKER, MAX_CORRECTIONS_PER_SESSION, MIN_CORRECTION_LENGTH, DIGEST_LOG_DIR;
21
+ var REGIONS, REGION_PRIORITY, REGION_ICONS, REGION_KO, EMIT_THRESHOLD, SPOTLIGHT_DAYS, JACCARD_THRESHOLD, MAX_DEPTH, EMIT_TARGETS, SIGNAL_TYPES, MARKER_START, MARKER_END, HOOK_MARKER, MAX_CORRECTIONS_PER_SESSION, MIN_CORRECTION_LENGTH, DIGEST_LOG_DIR, SESSION_STATE_DIR, PROTECTED_REGIONS_CONTRA;
22
22
  var init_constants = __esm({
23
23
  "src/constants.ts"() {
24
24
  "use strict";
@@ -76,6 +76,8 @@ var init_constants = __esm({
76
76
  MAX_CORRECTIONS_PER_SESSION = 10;
77
77
  MIN_CORRECTION_LENGTH = 15;
78
78
  DIGEST_LOG_DIR = "hippocampus/digest_log";
79
+ SESSION_STATE_DIR = "hippocampus/session_state";
80
+ PROTECTED_REGIONS_CONTRA = ["brainstem", "limbic", "sensors"];
79
81
  }
80
82
  });
81
83
 
@@ -417,7 +419,7 @@ function emitBootstrap(result, brain) {
417
419
  lines.push("|--------|---------|------------|");
418
420
  for (const region of result.activeRegions) {
419
421
  const active = region.neurons.filter((n) => !n.isDormant);
420
- const activation = active.reduce((sum, n) => sum + n.counter, 0);
422
+ const activation = active.reduce((sum, n) => sum + n.intensity, 0);
421
423
  const icon = REGION_ICONS[region.name] || "";
422
424
  lines.push(`| ${icon} ${region.name} | ${active.length} | ${activation} |`);
423
425
  }
@@ -439,7 +441,7 @@ function emitIndex(result, brain) {
439
441
  const allNeurons = result.activeRegions.flatMap(
440
442
  (r) => r.neurons.filter((n) => !n.isDormant && n.counter >= EMIT_THRESHOLD)
441
443
  );
442
- allNeurons.sort((a, b) => b.counter - a.counter);
444
+ allNeurons.sort((a, b) => b.intensity - a.intensity);
443
445
  lines.push("## Top 10 Active Neurons");
444
446
  lines.push("| # | Path | Counter | Strength |");
445
447
  lines.push("|---|------|---------|----------|");
@@ -465,7 +467,7 @@ function emitIndex(result, brain) {
465
467
  for (const region of result.activeRegions) {
466
468
  const active = region.neurons.filter((n) => !n.isDormant);
467
469
  const dormant = region.neurons.filter((n) => n.isDormant);
468
- const activation = active.reduce((sum, n) => sum + n.counter, 0);
470
+ const activation = active.reduce((sum, n) => sum + n.intensity, 0);
469
471
  const icon = REGION_ICONS[region.name] || "";
470
472
  lines.push(`| ${icon} ${region.name} | ${active.length} | ${dormant.length} | ${activation} | [_rules.md](${region.name}/_rules.md) |`);
471
473
  }
@@ -477,7 +479,7 @@ function emitRegionRules(region) {
477
479
  const ko = REGION_KO[region.name] || "";
478
480
  const active = region.neurons.filter((n) => !n.isDormant);
479
481
  const dormant = region.neurons.filter((n) => n.isDormant);
480
- const activation = active.reduce((sum, n) => sum + n.counter, 0);
482
+ const activation = active.reduce((sum, n) => sum + n.intensity, 0);
481
483
  const lines = [];
482
484
  lines.push(`# ${icon} ${region.name} (${ko})`);
483
485
  lines.push(`> Active: ${active.length} | Dormant: ${dormant.length} | Activation: ${activation}`);
@@ -491,7 +493,7 @@ function emitRegionRules(region) {
491
493
  }
492
494
  if (active.length > 0) {
493
495
  lines.push("## Rules");
494
- const sorted = [...active].sort((a, b) => b.counter - a.counter);
496
+ const sorted = [...active].sort((a, b) => b.intensity - a.intensity);
495
497
  for (const n of sorted) {
496
498
  const indent = " ".repeat(Math.min(n.depth, 4));
497
499
  const prefix = strengthPrefix(n.counter);
@@ -574,7 +576,7 @@ function printDiag(brain, result) {
574
576
  const icon = REGION_ICONS[region.name] || "";
575
577
  const active = region.neurons.filter((n) => !n.isDormant);
576
578
  const dormant = region.neurons.filter((n) => n.isDormant);
577
- const activation = active.reduce((sum, n) => sum + n.counter, 0);
579
+ const activation = active.reduce((sum, n) => sum + n.intensity, 0);
578
580
  const isBlocked = result.blockedRegions.some((r) => r.name === region.name);
579
581
  const status = region.hasBomb ? "\u{1F4A3} BOMB" : isBlocked ? "\u{1F6AB} BLOCKED" : "\u2705 ACTIVE";
580
582
  console.log(` ${icon} ${region.name} [${status}]`);
@@ -584,7 +586,8 @@ function printDiag(brain, result) {
584
586
  }
585
587
  const top3 = sortedActive(region.neurons, 3);
586
588
  for (const n of top3) {
587
- console.log(` \u251C ${n.path} (${n.counter})`);
589
+ const contraStr = n.contra > 0 ? ` contra:${n.contra}` : "";
590
+ console.log(` \u251C ${n.path} (counter:${n.counter}${contraStr} intensity:${n.intensity})`);
588
591
  }
589
592
  }
590
593
  console.log("");
@@ -593,7 +596,7 @@ function pathToSentence(path) {
593
596
  return path.replace(/\//g, " > ").replace(/_/g, " ");
594
597
  }
595
598
  function sortedActive(neurons, n) {
596
- return [...neurons].filter((neuron) => !neuron.isDormant).sort((a, b) => b.counter - a.counter).slice(0, n);
599
+ return [...neurons].filter((neuron) => !neuron.isDormant).sort((a, b) => b.intensity - a.intensity).slice(0, n);
597
600
  }
598
601
  function strengthPrefix(counter) {
599
602
  if (counter >= 10) return "**[ABSOLUTE]** ";
@@ -758,7 +761,9 @@ var init_update_check = __esm({
758
761
  // src/fire.ts
759
762
  var fire_exports = {};
760
763
  __export(fire_exports, {
764
+ contraNeuron: () => contraNeuron,
761
765
  fireNeuron: () => fireNeuron,
766
+ getCurrentContra: () => getCurrentContra,
762
767
  getCurrentCounter: () => getCurrentCounter
763
768
  });
764
769
  import { readdirSync as readdirSync3, renameSync, writeFileSync as writeFileSync4, existsSync as existsSync6, mkdirSync as mkdirSync4 } from "fs";
@@ -781,6 +786,33 @@ function fireNeuron(brainRoot, neuronPath) {
781
786
  console.log(`\u{1F525} fired: ${neuronPath} (${current} \u2192 ${newCounter})`);
782
787
  return newCounter;
783
788
  }
789
+ function contraNeuron(brainRoot, neuronPath) {
790
+ const fullPath = join5(brainRoot, neuronPath);
791
+ if (!existsSync6(fullPath)) {
792
+ return 0;
793
+ }
794
+ const current = getCurrentContra(fullPath);
795
+ const newContra = current + 1;
796
+ if (current > 0) {
797
+ renameSync(join5(fullPath, `${current}.contra`), join5(fullPath, `${newContra}.contra`));
798
+ } else {
799
+ writeFileSync4(join5(fullPath, `${newContra}.contra`), "", "utf8");
800
+ }
801
+ return newContra;
802
+ }
803
+ function getCurrentContra(dir) {
804
+ let max = 0;
805
+ try {
806
+ for (const entry of readdirSync3(dir)) {
807
+ if (entry.endsWith(".contra")) {
808
+ const n = parseInt(entry, 10);
809
+ if (!isNaN(n) && n > max) max = n;
810
+ }
811
+ }
812
+ } catch {
813
+ }
814
+ return max;
815
+ }
784
816
  function getCurrentCounter(dir) {
785
817
  let max = 0;
786
818
  try {
@@ -1216,41 +1248,174 @@ var init_watch = __esm({
1216
1248
  }
1217
1249
  });
1218
1250
 
1251
+ // src/candidates.ts
1252
+ var candidates_exports = {};
1253
+ __export(candidates_exports, {
1254
+ CANDIDATE_DECAY_DAYS: () => CANDIDATE_DECAY_DAYS,
1255
+ CANDIDATE_THRESHOLD: () => CANDIDATE_THRESHOLD,
1256
+ fromCandidatePath: () => fromCandidatePath,
1257
+ growCandidate: () => growCandidate,
1258
+ listCandidates: () => listCandidates,
1259
+ promoteCandidates: () => promoteCandidates,
1260
+ toCandidatePath: () => toCandidatePath
1261
+ });
1262
+ import { existsSync as existsSync11, mkdirSync as mkdirSync6, readdirSync as readdirSync7, renameSync as renameSync3, rmSync, statSync as statSync4 } from "fs";
1263
+ import { join as join12, dirname as dirname2, relative as relative3 } from "path";
1264
+ function toCandidatePath(neuronPath) {
1265
+ const slash = neuronPath.indexOf("/");
1266
+ if (slash === -1) throw new Error(`Invalid neuron path (missing region): ${neuronPath}`);
1267
+ return `${neuronPath.slice(0, slash)}/${CANDIDATE_SEGMENT}/${neuronPath.slice(slash + 1)}`;
1268
+ }
1269
+ function fromCandidatePath(candidatePath) {
1270
+ return candidatePath.replace(`/${CANDIDATE_SEGMENT}/`, "/");
1271
+ }
1272
+ function growCandidate(brainRoot, neuronPath) {
1273
+ const candidatePath = toCandidatePath(neuronPath);
1274
+ const result = growNeuron(brainRoot, candidatePath);
1275
+ if (result.counter >= CANDIDATE_THRESHOLD) {
1276
+ const ok = moveCandidate(brainRoot, candidatePath, neuronPath);
1277
+ return { ...result, path: ok ? neuronPath : result.path, promoted: ok };
1278
+ }
1279
+ console.log(` \u{1F331} candidate (${result.counter}/${CANDIDATE_THRESHOLD}): ${candidatePath}`);
1280
+ return { ...result, promoted: false };
1281
+ }
1282
+ function moveCandidate(brainRoot, candidatePath, targetPath) {
1283
+ const src = join12(brainRoot, candidatePath);
1284
+ if (!existsSync11(src)) return false;
1285
+ const dst = join12(brainRoot, targetPath);
1286
+ if (existsSync11(dst)) {
1287
+ fireNeuron(brainRoot, targetPath);
1288
+ rmSync(src, { recursive: true, force: true });
1289
+ } else {
1290
+ mkdirSync6(dirname2(dst), { recursive: true });
1291
+ renameSync3(src, dst);
1292
+ }
1293
+ console.log(`\u{1F393} promoted: ${candidatePath} \u2192 ${targetPath}`);
1294
+ return true;
1295
+ }
1296
+ function promoteCandidates(brainRoot) {
1297
+ const promoted = [];
1298
+ const decayed = [];
1299
+ const decayMs = CANDIDATE_DECAY_DAYS * 24 * 60 * 60 * 1e3;
1300
+ const now = Date.now();
1301
+ for (const region of REGIONS) {
1302
+ const candidateRoot = join12(brainRoot, region, CANDIDATE_SEGMENT);
1303
+ walkNeuronDirs(candidateRoot, (neuronDir) => {
1304
+ const rel = relative3(join12(brainRoot, region), neuronDir);
1305
+ const candidatePath = `${region}/${rel}`;
1306
+ const targetPath = fromCandidatePath(candidatePath);
1307
+ const counter = readCounter(neuronDir);
1308
+ const mtime = statSync4(neuronDir).mtimeMs;
1309
+ if (counter >= CANDIDATE_THRESHOLD) {
1310
+ moveCandidate(brainRoot, candidatePath, targetPath);
1311
+ promoted.push(targetPath);
1312
+ } else if (now - mtime > decayMs) {
1313
+ rmSync(neuronDir, { recursive: true, force: true });
1314
+ decayed.push(candidatePath);
1315
+ console.log(`\u{1F480} candidate decayed: ${candidatePath}`);
1316
+ }
1317
+ });
1318
+ }
1319
+ return { promoted, decayed };
1320
+ }
1321
+ function listCandidates(brainRoot) {
1322
+ const results = [];
1323
+ const now = Date.now();
1324
+ for (const region of REGIONS) {
1325
+ const candidateRoot = join12(brainRoot, region, CANDIDATE_SEGMENT);
1326
+ walkNeuronDirs(candidateRoot, (neuronDir) => {
1327
+ const rel = relative3(join12(brainRoot, region), neuronDir);
1328
+ const candidatePath = `${region}/${rel}`;
1329
+ const targetPath = fromCandidatePath(candidatePath);
1330
+ const counter = readCounter(neuronDir);
1331
+ const mtime = statSync4(neuronDir).mtimeMs;
1332
+ const daysInactive = Math.floor((now - mtime) / (24 * 60 * 60 * 1e3));
1333
+ results.push({ candidatePath, targetPath, counter, daysInactive });
1334
+ });
1335
+ }
1336
+ return results;
1337
+ }
1338
+ function walkNeuronDirs(dir, cb) {
1339
+ if (!existsSync11(dir)) return;
1340
+ try {
1341
+ const entries = readdirSync7(dir, { withFileTypes: true });
1342
+ const hasNeuron = entries.some((e) => e.isFile() && e.name.endsWith(".neuron"));
1343
+ if (hasNeuron) {
1344
+ cb(dir);
1345
+ return;
1346
+ }
1347
+ for (const entry of entries) {
1348
+ if (entry.isDirectory() && !entry.name.startsWith(".")) {
1349
+ walkNeuronDirs(join12(dir, entry.name), cb);
1350
+ }
1351
+ }
1352
+ } catch {
1353
+ }
1354
+ }
1355
+ function readCounter(dir) {
1356
+ try {
1357
+ const files = readdirSync7(dir).filter((f) => /^\d+\.neuron$/.test(f));
1358
+ if (files.length === 0) return 0;
1359
+ return Math.max(...files.map((f) => parseInt(f, 10)));
1360
+ } catch {
1361
+ return 0;
1362
+ }
1363
+ }
1364
+ var CANDIDATE_THRESHOLD, CANDIDATE_DECAY_DAYS, CANDIDATE_SEGMENT;
1365
+ var init_candidates = __esm({
1366
+ "src/candidates.ts"() {
1367
+ "use strict";
1368
+ init_constants();
1369
+ init_grow();
1370
+ init_fire();
1371
+ CANDIDATE_THRESHOLD = 3;
1372
+ CANDIDATE_DECAY_DAYS = 14;
1373
+ CANDIDATE_SEGMENT = "_candidates";
1374
+ }
1375
+ });
1376
+
1219
1377
  // src/episode.ts
1220
- import { readdirSync as readdirSync7, readFileSync as readFileSync4, writeFileSync as writeFileSync9, mkdirSync as mkdirSync6, existsSync as existsSync11 } from "fs";
1221
- import { join as join12 } from "path";
1222
- function logEpisode(brainRoot, type, path, detail) {
1223
- const logDir = join12(brainRoot, SESSION_LOG_DIR);
1224
- if (!existsSync11(logDir)) {
1225
- mkdirSync6(logDir, { recursive: true });
1378
+ var episode_exports = {};
1379
+ __export(episode_exports, {
1380
+ logEpisode: () => logEpisode,
1381
+ readEpisodes: () => readEpisodes
1382
+ });
1383
+ import { readdirSync as readdirSync8, readFileSync as readFileSync4, writeFileSync as writeFileSync9, mkdirSync as mkdirSync7, existsSync as existsSync12 } from "fs";
1384
+ import { join as join13 } from "path";
1385
+ function logEpisode(brainRoot, type, path, detail, extra) {
1386
+ const logDir = join13(brainRoot, SESSION_LOG_DIR);
1387
+ if (!existsSync12(logDir)) {
1388
+ mkdirSync7(logDir, { recursive: true });
1226
1389
  }
1227
1390
  const nextSlot = getNextSlot(logDir);
1228
1391
  const episode = {
1229
1392
  ts: (/* @__PURE__ */ new Date()).toISOString(),
1230
1393
  type,
1231
1394
  path,
1232
- detail
1395
+ detail,
1396
+ ...extra?.outcome ? { outcome: extra.outcome } : {},
1397
+ ...extra?.neurons ? { neurons: extra.neurons } : {}
1233
1398
  };
1234
1399
  writeFileSync9(
1235
- join12(logDir, `memory${nextSlot}.neuron`),
1400
+ join13(logDir, `memory${nextSlot}.neuron`),
1236
1401
  JSON.stringify(episode),
1237
1402
  "utf8"
1238
1403
  );
1239
1404
  }
1240
1405
  function readEpisodes(brainRoot) {
1241
- const logDir = join12(brainRoot, SESSION_LOG_DIR);
1242
- if (!existsSync11(logDir)) return [];
1406
+ const logDir = join13(brainRoot, SESSION_LOG_DIR);
1407
+ if (!existsSync12(logDir)) return [];
1243
1408
  const episodes = [];
1244
1409
  let entries;
1245
1410
  try {
1246
- entries = readdirSync7(logDir);
1411
+ entries = readdirSync8(logDir);
1247
1412
  } catch {
1248
1413
  return [];
1249
1414
  }
1250
1415
  for (const entry of entries) {
1251
1416
  if (!entry.startsWith("memory") || !entry.endsWith(".neuron")) continue;
1252
1417
  try {
1253
- const content = readFileSync4(join12(logDir, entry), "utf8");
1418
+ const content = readFileSync4(join13(logDir, entry), "utf8");
1254
1419
  if (content.trim()) {
1255
1420
  episodes.push(JSON.parse(content));
1256
1421
  }
@@ -1263,7 +1428,7 @@ function readEpisodes(brainRoot) {
1263
1428
  function getNextSlot(logDir) {
1264
1429
  let maxSlot = 0;
1265
1430
  try {
1266
- for (const entry of readdirSync7(logDir)) {
1431
+ for (const entry of readdirSync8(logDir)) {
1267
1432
  if (entry.startsWith("memory") && entry.endsWith(".neuron")) {
1268
1433
  const n = parseInt(entry.replace("memory", "").replace(".neuron", ""), 10);
1269
1434
  if (!isNaN(n) && n > maxSlot) maxSlot = n;
@@ -1290,11 +1455,11 @@ __export(inbox_exports, {
1290
1455
  ensureInbox: () => ensureInbox,
1291
1456
  processInbox: () => processInbox
1292
1457
  });
1293
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync10, existsSync as existsSync12, mkdirSync as mkdirSync7 } from "fs";
1294
- import { join as join13 } from "path";
1458
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync10, existsSync as existsSync13, mkdirSync as mkdirSync8 } from "fs";
1459
+ import { join as join14 } from "path";
1295
1460
  function processInbox(brainRoot) {
1296
- const inboxPath = join13(brainRoot, INBOX_DIR, CORRECTIONS_FILE);
1297
- if (!existsSync12(inboxPath)) {
1461
+ const inboxPath = join14(brainRoot, INBOX_DIR, CORRECTIONS_FILE);
1462
+ if (!existsSync13(inboxPath)) {
1298
1463
  return { processed: 0, skipped: 0, errors: [] };
1299
1464
  }
1300
1465
  const content = readFileSync5(inboxPath, "utf8").trim();
@@ -1349,16 +1514,18 @@ function processInbox(brainRoot) {
1349
1514
  }
1350
1515
  function applyCorrection(brainRoot, correction) {
1351
1516
  const neuronPath = correction.path;
1352
- const fullPath = join13(brainRoot, neuronPath);
1517
+ const fullPath = join14(brainRoot, neuronPath);
1353
1518
  const counterAdd = Math.max(1, correction.counter_add || 1);
1354
- if (existsSync12(fullPath)) {
1519
+ if (existsSync13(fullPath)) {
1355
1520
  for (let i = 0; i < counterAdd; i++) {
1356
1521
  fireNeuron(brainRoot, neuronPath);
1357
1522
  }
1358
1523
  } else {
1359
- growNeuron(brainRoot, neuronPath);
1360
- for (let i = 1; i < counterAdd; i++) {
1361
- fireNeuron(brainRoot, neuronPath);
1524
+ const candResult = growCandidate(brainRoot, neuronPath);
1525
+ if (candResult.promoted) {
1526
+ for (let i = 1; i < counterAdd; i++) {
1527
+ fireNeuron(brainRoot, neuronPath);
1528
+ }
1362
1529
  }
1363
1530
  }
1364
1531
  if (correction.dopamine && correction.dopamine > 0) {
@@ -1377,12 +1544,12 @@ function isPathSafe(path) {
1377
1544
  return true;
1378
1545
  }
1379
1546
  function ensureInbox(brainRoot) {
1380
- const inboxDir = join13(brainRoot, INBOX_DIR);
1381
- if (!existsSync12(inboxDir)) {
1382
- mkdirSync7(inboxDir, { recursive: true });
1547
+ const inboxDir = join14(brainRoot, INBOX_DIR);
1548
+ if (!existsSync13(inboxDir)) {
1549
+ mkdirSync8(inboxDir, { recursive: true });
1383
1550
  }
1384
- const filePath = join13(inboxDir, CORRECTIONS_FILE);
1385
- if (!existsSync12(filePath)) {
1551
+ const filePath = join14(inboxDir, CORRECTIONS_FILE);
1552
+ if (!existsSync13(filePath)) {
1386
1553
  writeFileSync10(filePath, "", "utf8");
1387
1554
  }
1388
1555
  return filePath;
@@ -1398,7 +1565,7 @@ var init_inbox = __esm({
1398
1565
  "src/inbox.ts"() {
1399
1566
  "use strict";
1400
1567
  init_constants();
1401
- init_grow();
1568
+ init_candidates();
1402
1569
  init_fire();
1403
1570
  init_signal();
1404
1571
  init_episode();
@@ -1695,17 +1862,25 @@ __export(hooks_exports, {
1695
1862
  installHooks: () => installHooks,
1696
1863
  uninstallHooks: () => uninstallHooks
1697
1864
  });
1698
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync11, existsSync as existsSync13, mkdirSync as mkdirSync8, readdirSync as readdirSync8 } from "fs";
1865
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync11, existsSync as existsSync14, mkdirSync as mkdirSync9, readdirSync as readdirSync9 } from "fs";
1699
1866
  import { execSync as execSync2 } from "child_process";
1700
- import { join as join14, resolve as resolve2 } from "path";
1701
- function installHooks(brainRoot, projectRoot) {
1867
+ import { join as join15, resolve as resolve2 } from "path";
1868
+ function installHooks(brainRoot, projectRoot, global) {
1702
1869
  const root = projectRoot || process.cwd();
1703
1870
  const resolvedBrain = resolve2(brainRoot);
1704
- if (!existsSync13(resolvedBrain) || !hasBrainRegions(resolvedBrain)) {
1871
+ if (global) {
1872
+ const home = process.env.HOME || "~";
1873
+ if (!brainRoot.startsWith("/") && !brainRoot.startsWith(home)) {
1874
+ console.error("\u274C --global requires an absolute --brain path (e.g. --brain ~/brain)");
1875
+ process.exit(1);
1876
+ }
1877
+ }
1878
+ if (!existsSync14(resolvedBrain) || !hasBrainRegions(resolvedBrain)) {
1705
1879
  initBrain(resolvedBrain);
1706
1880
  }
1707
- const settingsDir = join14(root, SETTINGS_DIR);
1708
- const settingsPath = join14(settingsDir, SETTINGS_FILE);
1881
+ const settingsDir = global ? resolve2(process.env.HOME || "~", SETTINGS_DIR) : join15(root, SETTINGS_DIR);
1882
+ const settingsFile = global ? "settings.json" : SETTINGS_FILE;
1883
+ const settingsPath = join15(settingsDir, settingsFile);
1709
1884
  const defaultBrain = resolve2(root, "brain");
1710
1885
  const brainFlag = resolvedBrain === defaultBrain ? "" : ` --brain ${resolvedBrain}`;
1711
1886
  let npxBin = "npx";
@@ -1714,7 +1889,7 @@ function installHooks(brainRoot, projectRoot) {
1714
1889
  } catch {
1715
1890
  }
1716
1891
  let settings = {};
1717
- if (existsSync13(settingsPath)) {
1892
+ if (existsSync14(settingsPath)) {
1718
1893
  try {
1719
1894
  settings = JSON.parse(readFileSync6(settingsPath, "utf8"));
1720
1895
  } catch {
@@ -1731,8 +1906,8 @@ function installHooks(brainRoot, projectRoot) {
1731
1906
  matcher: "startup|resume",
1732
1907
  entry: {
1733
1908
  type: "command",
1734
- command: `${npxBin} hebbian emit claude${brainFlag}`,
1735
- timeout: 10,
1909
+ command: `${npxBin} hebbian emit claude${brainFlag} && ${npxBin} hebbian session start${brainFlag}`,
1910
+ timeout: 15,
1736
1911
  statusMessage: `${HOOK_MARKER} refreshing brain`
1737
1912
  }
1738
1913
  },
@@ -1740,7 +1915,7 @@ function installHooks(brainRoot, projectRoot) {
1740
1915
  event: "Stop",
1741
1916
  entry: {
1742
1917
  type: "command",
1743
- command: `${npxBin} hebbian digest${brainFlag}`,
1918
+ command: `${npxBin} hebbian digest${brainFlag}; ${npxBin} hebbian session end${brainFlag}`,
1744
1919
  timeout: 30,
1745
1920
  statusMessage: `${HOOK_MARKER} digesting session`
1746
1921
  }
@@ -1763,18 +1938,20 @@ function installHooks(brainRoot, projectRoot) {
1763
1938
  hooks[event].push(group);
1764
1939
  }
1765
1940
  }
1766
- if (!existsSync13(settingsDir)) {
1767
- mkdirSync8(settingsDir, { recursive: true });
1941
+ if (!existsSync14(settingsDir)) {
1942
+ mkdirSync9(settingsDir, { recursive: true });
1768
1943
  }
1769
1944
  writeFileSync11(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
1770
1945
  console.log(`\u2705 hebbian hooks installed at ${settingsPath}`);
1771
1946
  console.log(` SessionStart \u2192 ${npxBin} hebbian emit claude${brainFlag}`);
1772
1947
  console.log(` Stop \u2192 ${npxBin} hebbian digest${brainFlag}`);
1773
1948
  }
1774
- function uninstallHooks(projectRoot) {
1949
+ function uninstallHooks(projectRoot, global) {
1775
1950
  const root = projectRoot || process.cwd();
1776
- const settingsPath = join14(root, SETTINGS_DIR, SETTINGS_FILE);
1777
- if (!existsSync13(settingsPath)) {
1951
+ const settingsDir = global ? resolve2(process.env.HOME || "~", SETTINGS_DIR) : join15(root, SETTINGS_DIR);
1952
+ const settingsFile = global ? "settings.json" : SETTINGS_FILE;
1953
+ const settingsPath = join15(settingsDir, settingsFile);
1954
+ if (!existsSync14(settingsPath)) {
1778
1955
  console.log("No hooks installed (settings.local.json not found)");
1779
1956
  return;
1780
1957
  }
@@ -1807,15 +1984,17 @@ function uninstallHooks(projectRoot) {
1807
1984
  writeFileSync11(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
1808
1985
  console.log(`\u2705 removed ${removed} hebbian hook(s) from ${settingsPath}`);
1809
1986
  }
1810
- function checkHooks(projectRoot) {
1987
+ function checkHooks(projectRoot, global) {
1811
1988
  const root = projectRoot || process.cwd();
1812
- const settingsPath = join14(root, SETTINGS_DIR, SETTINGS_FILE);
1989
+ const settingsDir = global ? resolve2(process.env.HOME || "~", SETTINGS_DIR) : join15(root, SETTINGS_DIR);
1990
+ const settingsFile = global ? "settings.json" : SETTINGS_FILE;
1991
+ const settingsPath = join15(settingsDir, settingsFile);
1813
1992
  const status = {
1814
1993
  installed: false,
1815
1994
  path: settingsPath,
1816
1995
  events: []
1817
1996
  };
1818
- if (!existsSync13(settingsPath)) {
1997
+ if (!existsSync14(settingsPath)) {
1819
1998
  console.log(`\u274C hebbian hooks not installed (${settingsPath} not found)`);
1820
1999
  return status;
1821
2000
  }
@@ -1851,9 +2030,9 @@ function checkHooks(projectRoot) {
1851
2030
  return status;
1852
2031
  }
1853
2032
  function hasBrainRegions(dir) {
1854
- if (!existsSync13(dir)) return false;
2033
+ if (!existsSync14(dir)) return false;
1855
2034
  try {
1856
- const entries = readdirSync8(dir);
2035
+ const entries = readdirSync9(dir);
1857
2036
  return REGIONS.some((r) => entries.includes(r));
1858
2037
  } catch {
1859
2038
  return false;
@@ -1877,8 +2056,8 @@ __export(digest_exports, {
1877
2056
  extractCorrections: () => extractCorrections,
1878
2057
  readHookInput: () => readHookInput
1879
2058
  });
1880
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync12, existsSync as existsSync14, mkdirSync as mkdirSync9 } from "fs";
1881
- import { join as join15, basename } from "path";
2059
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync12, existsSync as existsSync15, mkdirSync as mkdirSync10 } from "fs";
2060
+ import { join as join16, basename } from "path";
1882
2061
  function readHookInput(stdin) {
1883
2062
  if (!stdin.trim()) return null;
1884
2063
  try {
@@ -1893,13 +2072,13 @@ function readHookInput(stdin) {
1893
2072
  }
1894
2073
  }
1895
2074
  function digestTranscript(brainRoot, transcriptPath, sessionId) {
1896
- if (!existsSync14(transcriptPath)) {
2075
+ if (!existsSync15(transcriptPath)) {
1897
2076
  throw new Error(`Transcript not found: ${transcriptPath}`);
1898
2077
  }
1899
2078
  const resolvedSessionId = sessionId || basename(transcriptPath, ".jsonl");
1900
- const logDir = join15(brainRoot, DIGEST_LOG_DIR);
1901
- const logPath = join15(logDir, `${resolvedSessionId}.jsonl`);
1902
- if (existsSync14(logPath)) {
2079
+ const logDir = join16(brainRoot, DIGEST_LOG_DIR);
2080
+ const logPath = join16(logDir, `${resolvedSessionId}.jsonl`);
2081
+ if (existsSync15(logPath)) {
1903
2082
  console.log(`\u23ED already digested session ${resolvedSessionId}, skip`);
1904
2083
  return { corrections: 0, skipped: 0, transcriptPath, sessionId: resolvedSessionId };
1905
2084
  }
@@ -1914,7 +2093,7 @@ function digestTranscript(brainRoot, transcriptPath, sessionId) {
1914
2093
  const auditEntries = [];
1915
2094
  for (const correction of corrections) {
1916
2095
  try {
1917
- growNeuron(brainRoot, correction.path);
2096
+ growCandidate(brainRoot, correction.path);
1918
2097
  logEpisode(brainRoot, "digest", correction.path, correction.text);
1919
2098
  auditEntries.push({ correction, applied: true });
1920
2099
  applied++;
@@ -2131,11 +2310,11 @@ function extractKeywords(text) {
2131
2310
  return text.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[^a-zA-Z0-9\u3000-\u9FFF\uAC00-\uD7AF]+/g, " ").toLowerCase().split(/\s+/).filter((t) => t.length > 2 && !STOP_WORDS.has(t));
2132
2311
  }
2133
2312
  function writeAuditLog(brainRoot, sessionId, entries) {
2134
- const logDir = join15(brainRoot, DIGEST_LOG_DIR);
2135
- if (!existsSync14(logDir)) {
2136
- mkdirSync9(logDir, { recursive: true });
2313
+ const logDir = join16(brainRoot, DIGEST_LOG_DIR);
2314
+ if (!existsSync15(logDir)) {
2315
+ mkdirSync10(logDir, { recursive: true });
2137
2316
  }
2138
- const logPath = join15(logDir, `${sessionId}.jsonl`);
2317
+ const logPath = join16(logDir, `${sessionId}.jsonl`);
2139
2318
  const lines = entries.map(
2140
2319
  (e) => JSON.stringify({
2141
2320
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -2153,7 +2332,7 @@ var init_digest = __esm({
2153
2332
  "src/digest.ts"() {
2154
2333
  "use strict";
2155
2334
  init_constants();
2156
- init_grow();
2335
+ init_candidates();
2157
2336
  init_episode();
2158
2337
  NEGATION_PATTERNS = [
2159
2338
  /\bdon[''\u2019]?t\b/i,
@@ -2194,6 +2373,243 @@ var init_digest = __esm({
2194
2373
  }
2195
2374
  });
2196
2375
 
2376
+ // src/outcome.ts
2377
+ var outcome_exports = {};
2378
+ __export(outcome_exports, {
2379
+ buildOutcomeSummary: () => buildOutcomeSummary,
2380
+ captureSessionStart: () => captureSessionStart,
2381
+ classifyOutcome: () => classifyOutcome,
2382
+ detectOutcome: () => detectOutcome
2383
+ });
2384
+ import { execSync as execSync3 } from "child_process";
2385
+ import { existsSync as existsSync16, mkdirSync as mkdirSync11, writeFileSync as writeFileSync13, readFileSync as readFileSync8, readdirSync as readdirSync10, rmSync as rmSync2, statSync as statSync5 } from "fs";
2386
+ import { join as join17 } from "path";
2387
+ import { randomUUID } from "crypto";
2388
+ function captureSessionStart(brainRoot) {
2389
+ let sha;
2390
+ try {
2391
+ sha = execSync3("git rev-parse HEAD", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
2392
+ } catch {
2393
+ console.log("\u23ED\uFE0F session start: not a git repo, skipping");
2394
+ return null;
2395
+ }
2396
+ let status;
2397
+ try {
2398
+ const raw = execSync3("git status --porcelain", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
2399
+ status = raw ? raw.split("\n") : [];
2400
+ } catch {
2401
+ status = [];
2402
+ }
2403
+ const brain = scanBrain(brainRoot);
2404
+ const result = runSubsumption(brain);
2405
+ const neurons = [];
2406
+ for (const region of result.activeRegions) {
2407
+ for (const neuron of region.neurons) {
2408
+ if (!neuron.isDormant && neuron.counter > 0) {
2409
+ neurons.push(`${region.name}/${neuron.path}`);
2410
+ }
2411
+ }
2412
+ }
2413
+ const uuid = randomUUID();
2414
+ const stateDir = join17(brainRoot, SESSION_STATE_DIR);
2415
+ if (!existsSync16(stateDir)) {
2416
+ mkdirSync11(stateDir, { recursive: true });
2417
+ }
2418
+ const state = { ts: (/* @__PURE__ */ new Date()).toISOString(), sha, status, neurons, uuid };
2419
+ writeFileSync13(join17(stateDir, `state_${uuid}.json`), JSON.stringify(state), "utf8");
2420
+ console.log(`\u{1F4F8} session start: SHA ${sha.slice(0, 7)}, ${neurons.length} active neurons`);
2421
+ return state;
2422
+ }
2423
+ function detectOutcome(brainRoot) {
2424
+ const state = readLatestSessionState(brainRoot);
2425
+ if (!state) {
2426
+ console.log("\u23ED\uFE0F session end: no session state found, skipping");
2427
+ return null;
2428
+ }
2429
+ let currentSha;
2430
+ try {
2431
+ currentSha = execSync3("git rev-parse HEAD", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
2432
+ } catch {
2433
+ console.log("\u23ED\uFE0F session end: not a git repo, skipping");
2434
+ cleanupSessionState(brainRoot, state.uuid);
2435
+ return null;
2436
+ }
2437
+ let currentStatus;
2438
+ try {
2439
+ const raw = execSync3("git status --porcelain", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
2440
+ currentStatus = raw ? filterHebbianPaths(raw.split("\n")) : [];
2441
+ } catch {
2442
+ currentStatus = [];
2443
+ }
2444
+ const filteredStartStatus = filterHebbianPaths(state.status);
2445
+ const outcome = classifyOutcome(
2446
+ { ...state, status: filteredStartStatus },
2447
+ currentSha,
2448
+ currentStatus
2449
+ );
2450
+ if (!outcome) {
2451
+ console.log("\u{1F4CA} session end: no changes detected (no-op)");
2452
+ cleanupSessionState(brainRoot, state.uuid);
2453
+ return null;
2454
+ }
2455
+ const neurons = state.neurons;
2456
+ logEpisode(brainRoot, "session-end", "", `outcome:${outcome}`, { outcome, neurons });
2457
+ let result;
2458
+ if (outcome === "revert") {
2459
+ const { affected, skipped } = applyContra(brainRoot, neurons);
2460
+ result = {
2461
+ outcome: "revert",
2462
+ neuronsAffected: affected,
2463
+ protectedSkipped: skipped,
2464
+ detail: `${affected} neurons contra'd (${skipped} protected skipped)`
2465
+ };
2466
+ console.log(`\u{1F4CA} session end: revert \u2014 ${result.detail}`);
2467
+ } else {
2468
+ result = {
2469
+ outcome: "acceptance",
2470
+ neuronsAffected: 0,
2471
+ protectedSkipped: 0,
2472
+ detail: "changes accepted"
2473
+ };
2474
+ console.log("\u{1F4CA} session end: acceptance");
2475
+ }
2476
+ cleanupSessionState(brainRoot, state.uuid);
2477
+ return result;
2478
+ }
2479
+ function classifyOutcome(state, currentSha, currentStatus) {
2480
+ const headMoved = state.sha !== currentSha;
2481
+ const startStatusSet = new Set(state.status);
2482
+ const endStatusSet = new Set(currentStatus);
2483
+ const newItems = currentStatus.filter((s) => !startStatusSet.has(s));
2484
+ const removedItems = state.status.filter((s) => !endStatusSet.has(s));
2485
+ if (!headMoved) {
2486
+ if (newItems.length === 0 && removedItems.length === 0) {
2487
+ return null;
2488
+ }
2489
+ if (newItems.length > 0) {
2490
+ return "acceptance";
2491
+ }
2492
+ if (removedItems.length > 0) {
2493
+ return "revert";
2494
+ }
2495
+ return null;
2496
+ }
2497
+ if (newItems.length > 0) {
2498
+ return "acceptance";
2499
+ }
2500
+ try {
2501
+ const diffStat = execSync3(
2502
+ `git diff ${state.sha}..${currentSha} --stat`,
2503
+ { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }
2504
+ ).trim();
2505
+ const logOutput = execSync3(
2506
+ `git log --oneline ${state.sha}..${currentSha}`,
2507
+ { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }
2508
+ ).trim();
2509
+ if (/\brevert\b/i.test(logOutput)) {
2510
+ return "revert";
2511
+ }
2512
+ if (!diffStat) {
2513
+ return "revert";
2514
+ }
2515
+ return "acceptance";
2516
+ } catch {
2517
+ return null;
2518
+ }
2519
+ }
2520
+ function applyContra(brainRoot, neurons) {
2521
+ let affected = 0;
2522
+ let skipped = 0;
2523
+ for (const neuronPath of neurons) {
2524
+ const region = neuronPath.split("/")[0] || "";
2525
+ if (PROTECTED_REGIONS_CONTRA.includes(region)) {
2526
+ skipped++;
2527
+ continue;
2528
+ }
2529
+ const result = contraNeuron(brainRoot, neuronPath);
2530
+ if (result > 0) {
2531
+ affected++;
2532
+ }
2533
+ }
2534
+ return { affected, skipped };
2535
+ }
2536
+ function buildOutcomeSummary(brainRoot) {
2537
+ const episodes = readEpisodes(brainRoot);
2538
+ const outcomeEpisodes = episodes.filter((e) => e.outcome && e.neurons);
2539
+ if (outcomeEpisodes.length === 0) return "";
2540
+ const stats = /* @__PURE__ */ new Map();
2541
+ for (const ep of outcomeEpisodes) {
2542
+ for (const neuron of ep.neurons) {
2543
+ const existing = stats.get(neuron) || { sessions: 0, reverts: 0, acceptances: 0 };
2544
+ existing.sessions++;
2545
+ if (ep.outcome === "revert") existing.reverts++;
2546
+ if (ep.outcome === "acceptance") existing.acceptances++;
2547
+ stats.set(neuron, existing);
2548
+ }
2549
+ }
2550
+ const lines = ["## Outcome Signals (from session history)\n"];
2551
+ lines.push("Neurons with high contra_ratio (>0.5) are consistently present in reverted sessions. Consider pruning or modifying them.\n");
2552
+ const sorted = [...stats.entries()].sort((a, b) => {
2553
+ const ratioA = a[1].sessions > 0 ? a[1].reverts / a[1].sessions : 0;
2554
+ const ratioB = b[1].sessions > 0 ? b[1].reverts / b[1].sessions : 0;
2555
+ return ratioB - ratioA;
2556
+ });
2557
+ for (const [neuron, s] of sorted) {
2558
+ const ratio = s.sessions > 0 ? (s.reverts / s.sessions).toFixed(2) : "0.00";
2559
+ const trend = parseFloat(ratio) > 0.5 ? "\u2190 act on this" : parseFloat(ratio) > 0.3 ? "\u2190 watch" : "";
2560
+ lines.push(`- ${neuron}: sessions=${s.sessions} reverts=${s.reverts} acceptances=${s.acceptances} contra_ratio=${ratio} ${trend}`);
2561
+ }
2562
+ lines.push("");
2563
+ return lines.join("\n");
2564
+ }
2565
+ function readLatestSessionState(brainRoot) {
2566
+ const stateDir = join17(brainRoot, SESSION_STATE_DIR);
2567
+ if (!existsSync16(stateDir)) return null;
2568
+ let latest = null;
2569
+ try {
2570
+ for (const entry of readdirSync10(stateDir)) {
2571
+ if (!entry.startsWith("state_") || !entry.endsWith(".json")) continue;
2572
+ const fullPath = join17(stateDir, entry);
2573
+ const mtime = statSync5(fullPath).mtimeMs;
2574
+ if (!latest || mtime > latest.mtime) {
2575
+ latest = { path: fullPath, mtime };
2576
+ }
2577
+ }
2578
+ } catch {
2579
+ return null;
2580
+ }
2581
+ if (!latest) return null;
2582
+ try {
2583
+ return JSON.parse(readFileSync8(latest.path, "utf8"));
2584
+ } catch {
2585
+ return null;
2586
+ }
2587
+ }
2588
+ function filterHebbianPaths(statusLines) {
2589
+ const hebbianPatterns = ["hippocampus/session_state", "hippocampus/session_log", "hippocampus/digest_log", "_inbox/"];
2590
+ return statusLines.filter(
2591
+ (line) => !hebbianPatterns.some((p) => line.includes(p))
2592
+ );
2593
+ }
2594
+ function cleanupSessionState(brainRoot, uuid) {
2595
+ const stateDir = join17(brainRoot, SESSION_STATE_DIR);
2596
+ const filePath = join17(stateDir, `state_${uuid}.json`);
2597
+ try {
2598
+ if (existsSync16(filePath)) rmSync2(filePath);
2599
+ } catch {
2600
+ }
2601
+ }
2602
+ var init_outcome = __esm({
2603
+ "src/outcome.ts"() {
2604
+ "use strict";
2605
+ init_constants();
2606
+ init_scanner();
2607
+ init_subsumption();
2608
+ init_fire();
2609
+ init_episode();
2610
+ }
2611
+ });
2612
+
2197
2613
  // src/evolve.ts
2198
2614
  var evolve_exports = {};
2199
2615
  __export(evolve_exports, {
@@ -2214,7 +2630,8 @@ async function runEvolve(brainRoot, dryRun) {
2214
2630
  const episodes = readEpisodes(brainRoot);
2215
2631
  const brain = scanBrain(brainRoot);
2216
2632
  const summary = buildBrainSummary(brain);
2217
- const prompt = buildPrompt(summary, episodes);
2633
+ const outcomeSummary = buildOutcomeSummary(brainRoot);
2634
+ const prompt = buildPrompt(summary, episodes, outcomeSummary);
2218
2635
  let rawActions;
2219
2636
  try {
2220
2637
  rawActions = await callGemini(prompt, apiKey);
@@ -2262,8 +2679,9 @@ function buildBrainSummary(brain) {
2262
2679
  }
2263
2680
  return lines.join("\n");
2264
2681
  }
2265
- function buildPrompt(summary, episodes) {
2682
+ function buildPrompt(summary, episodes, outcomeSummary) {
2266
2683
  const episodeLines = episodes.length > 0 ? episodes.map((e) => `- [${e.ts}] ${e.type}: ${e.path} \u2014 ${e.detail}`).join("\n") : "(no recent episodes)";
2684
+ const outcomeSection = outcomeSummary || "";
2267
2685
  return `You are the evolve engine for a hebbian brain \u2014 a filesystem-based memory system for AI agents.
2268
2686
 
2269
2687
  ## Axioms
@@ -2275,6 +2693,7 @@ function buildPrompt(summary, episodes) {
2275
2693
  ## Current Brain
2276
2694
  ${summary}
2277
2695
 
2696
+ ${outcomeSection}
2278
2697
  ## Recent Episodes (last ${episodes.length})
2279
2698
  ${episodeLines}
2280
2699
 
@@ -2395,7 +2814,7 @@ function executeActions(brainRoot, actions) {
2395
2814
  fireNeuron(brainRoot, action.path);
2396
2815
  break;
2397
2816
  case "grow":
2398
- growNeuron(brainRoot, action.path);
2817
+ growCandidate(brainRoot, action.path);
2399
2818
  break;
2400
2819
  case "signal":
2401
2820
  signalNeuron(brainRoot, action.path, action.signal || "dopamine");
@@ -2439,10 +2858,11 @@ var init_evolve = __esm({
2439
2858
  init_scanner();
2440
2859
  init_constants();
2441
2860
  init_fire();
2442
- init_grow();
2861
+ init_candidates();
2443
2862
  init_signal();
2444
2863
  init_rollback();
2445
2864
  init_decay();
2865
+ init_outcome();
2446
2866
  MAX_ACTIONS = 10;
2447
2867
  PROTECTED_REGIONS = ["brainstem", "limbic", "sensors"];
2448
2868
  DEFAULT_MODEL = "gemini-2.0-flash-lite";
@@ -2451,11 +2871,151 @@ var init_evolve = __esm({
2451
2871
  }
2452
2872
  });
2453
2873
 
2874
+ // src/doctor.ts
2875
+ var doctor_exports = {};
2876
+ __export(doctor_exports, {
2877
+ runDoctor: () => runDoctor
2878
+ });
2879
+ import { existsSync as existsSync17, readFileSync as readFileSync9, readdirSync as readdirSync11 } from "fs";
2880
+ import { join as join18 } from "path";
2881
+ import { execSync as execSync4 } from "child_process";
2882
+ async function runDoctor(brainRoot) {
2883
+ let passed = 0, warnings = 0, failed = 0;
2884
+ const ok = (msg) => {
2885
+ console.log(` \u2705 ${msg}`);
2886
+ passed++;
2887
+ };
2888
+ const warn = (msg, fix) => {
2889
+ console.log(` \u26A0\uFE0F ${msg}`);
2890
+ if (fix) console.log(` \u2192 ${fix}`);
2891
+ warnings++;
2892
+ };
2893
+ const fail = (msg, fix) => {
2894
+ console.log(` \u274C ${msg}`);
2895
+ if (fix) console.log(` \u2192 ${fix}`);
2896
+ failed++;
2897
+ };
2898
+ console.log("\n\u{1FA7A} hebbian doctor\n");
2899
+ console.log("Node.js");
2900
+ const nodeVer = process.versions.node;
2901
+ const [major] = nodeVer.split(".").map(Number);
2902
+ if ((major ?? 0) >= 22) {
2903
+ ok(`Node.js ${nodeVer} (>= 22 required)`);
2904
+ } else {
2905
+ fail(`Node.js ${nodeVer} \u2014 need >= 22`, "nvm install 22 && nvm use 22");
2906
+ }
2907
+ console.log("\nnpm package");
2908
+ try {
2909
+ const pkgPath = new URL("../package.json", import.meta.url).pathname;
2910
+ const pkg = JSON.parse(readFileSync9(pkgPath, "utf8"));
2911
+ const local = pkg.version || "unknown";
2912
+ let remote = "";
2913
+ try {
2914
+ const out = execSync4("npm view hebbian version 2>/dev/null", { timeout: 5e3 }).toString().trim();
2915
+ remote = out;
2916
+ } catch {
2917
+ }
2918
+ if (remote && remote !== local) {
2919
+ warn(`hebbian ${local} installed, ${remote} available`, "npm i -g hebbian@latest");
2920
+ } else {
2921
+ ok(`hebbian ${local}${remote ? " (up to date)" : ""}`);
2922
+ }
2923
+ } catch {
2924
+ warn("Could not read package.json");
2925
+ }
2926
+ console.log("\nbrain structure");
2927
+ if (!existsSync17(brainRoot)) {
2928
+ fail(`Brain not found at ${brainRoot}`, "hebbian init ./brain");
2929
+ } else {
2930
+ ok(`Brain root: ${brainRoot}`);
2931
+ for (const region of REGIONS) {
2932
+ const regionDir = join18(brainRoot, region);
2933
+ if (existsSync17(regionDir)) {
2934
+ ok(`Region: ${region}`);
2935
+ } else {
2936
+ warn(`Missing region: ${region}`, `mkdir -p ${regionDir}`);
2937
+ }
2938
+ }
2939
+ }
2940
+ console.log("\nClaude Code hooks");
2941
+ const settingsPath = join18(process.cwd(), ".claude", "settings.local.json");
2942
+ if (!existsSync17(settingsPath)) {
2943
+ warn("No .claude/settings.local.json found", "hebbian claude install");
2944
+ } else {
2945
+ try {
2946
+ const settings = JSON.parse(readFileSync9(settingsPath, "utf8"));
2947
+ const hooks = settings.hooks || {};
2948
+ const hasStop = Object.entries(hooks).some(
2949
+ ([event, entries]) => event === "Stop" && Array.isArray(entries) && entries.some(
2950
+ (e) => typeof e === "object" && e !== null && "command" in e && typeof e.command === "string" && e.command.includes("hebbian digest")
2951
+ )
2952
+ );
2953
+ const hasStart = Object.entries(hooks).some(
2954
+ ([event, entries]) => event === "SessionStart" && Array.isArray(entries) && entries.some(
2955
+ (e) => typeof e === "object" && e !== null && "command" in e && typeof e.command === "string" && e.command.includes("hebbian emit")
2956
+ )
2957
+ );
2958
+ if (hasStop && hasStart) {
2959
+ ok("SessionStart + Stop hooks installed");
2960
+ } else {
2961
+ if (!hasStart) warn("SessionStart hook missing", "hebbian claude install");
2962
+ if (!hasStop) warn("Stop hook missing", "hebbian claude install");
2963
+ }
2964
+ } catch {
2965
+ fail("Malformed .claude/settings.local.json", "hebbian claude install");
2966
+ }
2967
+ }
2968
+ console.log("\nnpx resolution");
2969
+ try {
2970
+ const resolved = execSync4("which npx", { timeout: 3e3 }).toString().trim();
2971
+ ok(`npx: ${resolved}`);
2972
+ } catch {
2973
+ fail("npx not found in PATH", "Install Node.js from https://nodejs.org");
2974
+ }
2975
+ console.log("\ncandidates");
2976
+ try {
2977
+ let total = 0;
2978
+ for (const region of REGIONS) {
2979
+ const candidateDir = join18(brainRoot, region, "_candidates");
2980
+ if (existsSync17(candidateDir)) {
2981
+ const entries = readdirSync11(candidateDir, { withFileTypes: true });
2982
+ const count = entries.filter((e) => e.isDirectory()).length;
2983
+ total += count;
2984
+ }
2985
+ }
2986
+ if (total === 0) {
2987
+ ok("No pending candidates");
2988
+ } else {
2989
+ warn(`${total} candidate(s) pending`, "hebbian candidates \u2014 to view");
2990
+ }
2991
+ } catch {
2992
+ warn("Could not scan candidates");
2993
+ }
2994
+ console.log(`
2995
+ ${"\u2500".repeat(40)}`);
2996
+ console.log(` passed: ${passed} warnings: ${warnings} failed: ${failed}`);
2997
+ if (failed > 0) {
2998
+ console.log(" Fix the \u274C issues above, then re-run `hebbian doctor`");
2999
+ } else if (warnings > 0) {
3000
+ console.log(" Looking mostly good! Review \u26A0\uFE0F warnings above.");
3001
+ } else {
3002
+ console.log(" All checks passed. \u{1F389}");
3003
+ }
3004
+ console.log("");
3005
+ return { passed, warnings, failed };
3006
+ }
3007
+ var init_doctor = __esm({
3008
+ "src/doctor.ts"() {
3009
+ "use strict";
3010
+ init_constants();
3011
+ }
3012
+ });
3013
+
2454
3014
  // src/cli.ts
2455
3015
  init_constants();
2456
3016
  import { parseArgs } from "util";
2457
3017
  import { resolve as resolve3 } from "path";
2458
- var VERSION = "0.3.2";
3018
+ var VERSION = "0.5.0";
2459
3019
  var HELP = `
2460
3020
  hebbian v${VERSION} \u2014 Folder-as-neuron brain for any AI agent.
2461
3021
 
@@ -2479,7 +3039,11 @@ COMMANDS:
2479
3039
  inbox Process corrections inbox
2480
3040
  claude install|uninstall|status Manage Claude Code hooks
2481
3041
  digest [--transcript <path>] Extract corrections from conversation
3042
+ candidates [promote] List candidates or promote graduated ones
2482
3043
  evolve [--dry-run] LLM-powered brain evolution (Gemini)
3044
+ session start|end Capture/detect session outcomes
3045
+ sessions Show session outcome history
3046
+ doctor Self-diagnostic (hooks, brain, versions)
2483
3047
  diag Print brain diagnostics
2484
3048
  stats Print brain statistics
2485
3049
 
@@ -2521,6 +3085,7 @@ async function main(argv) {
2521
3085
  port: { type: "string", short: "p" },
2522
3086
  transcript: { type: "string", short: "t" },
2523
3087
  "dry-run": { type: "boolean" },
3088
+ global: { type: "boolean", short: "g" },
2524
3089
  help: { type: "boolean", short: "h" },
2525
3090
  version: { type: "boolean", short: "v" }
2526
3091
  },
@@ -2641,18 +3206,23 @@ async function main(argv) {
2641
3206
  }
2642
3207
  case "claude": {
2643
3208
  const sub = positionals[1];
3209
+ const isGlobal = values.global === true;
2644
3210
  const { installHooks: installHooks2, uninstallHooks: uninstallHooks2, checkHooks: checkHooks2 } = await Promise.resolve().then(() => (init_hooks(), hooks_exports));
2645
3211
  switch (sub) {
2646
3212
  case "install": {
2647
- const installBrain = values.brain ? resolve3(values.brain) : resolve3("./brain");
2648
- installHooks2(installBrain);
3213
+ const installBrain = values.brain ? resolve3(values.brain) : isGlobal ? process.env.HEBBIAN_BRAIN ? resolve3(process.env.HEBBIAN_BRAIN) : "" : resolve3("./brain");
3214
+ if (isGlobal && !installBrain) {
3215
+ console.error("\u274C --global requires --brain <path> or HEBBIAN_BRAIN env var");
3216
+ process.exit(1);
3217
+ }
3218
+ installHooks2(installBrain, void 0, isGlobal);
2649
3219
  break;
2650
3220
  }
2651
3221
  case "uninstall":
2652
- uninstallHooks2();
3222
+ uninstallHooks2(void 0, isGlobal);
2653
3223
  break;
2654
3224
  case "status": {
2655
- checkHooks2();
3225
+ checkHooks2(void 0, isGlobal);
2656
3226
  console.log(` version: v${VERSION}`);
2657
3227
  const { checkForUpdates: checkUpdates, formatUpdateBanner: formatBanner } = await Promise.resolve().then(() => (init_update_check(), update_check_exports));
2658
3228
  const updateStatus = await checkUpdates(VERSION);
@@ -2661,7 +3231,7 @@ async function main(argv) {
2661
3231
  break;
2662
3232
  }
2663
3233
  default:
2664
- console.error("Usage: hebbian claude <install|uninstall|status>");
3234
+ console.error("Usage: hebbian claude <install|uninstall|status> [--global]");
2665
3235
  process.exit(1);
2666
3236
  }
2667
3237
  break;
@@ -2684,12 +3254,70 @@ async function main(argv) {
2684
3254
  }
2685
3255
  break;
2686
3256
  }
3257
+ case "candidates": {
3258
+ const subCmd = positionals[1];
3259
+ const { listCandidates: listCandidates2, promoteCandidates: promoteCandidates2 } = await Promise.resolve().then(() => (init_candidates(), candidates_exports));
3260
+ if (subCmd === "promote") {
3261
+ const result = promoteCandidates2(brainRoot);
3262
+ console.log(`\u{1F393} promoted: ${result.promoted.length}, decayed: ${result.decayed.length}`);
3263
+ } else {
3264
+ const candidates = listCandidates2(brainRoot);
3265
+ if (candidates.length === 0) {
3266
+ console.log("No pending candidates");
3267
+ } else {
3268
+ console.log(`Candidates (promote at counter=${3}):`);
3269
+ for (const c of candidates) {
3270
+ const bar = "\u2588".repeat(c.counter) + "\u2591".repeat(Math.max(0, 3 - c.counter));
3271
+ console.log(` ${bar} ${c.counter}/3 ${c.targetPath} (${c.daysInactive}d idle)`);
3272
+ }
3273
+ }
3274
+ }
3275
+ break;
3276
+ }
2687
3277
  case "evolve": {
2688
3278
  const dryRun = values["dry-run"] === true;
2689
3279
  const { runEvolve: runEvolve2 } = await Promise.resolve().then(() => (init_evolve(), evolve_exports));
2690
3280
  await runEvolve2(brainRoot, dryRun);
2691
3281
  break;
2692
3282
  }
3283
+ case "session": {
3284
+ const sub = positionals[1];
3285
+ const { captureSessionStart: captureSessionStart2, detectOutcome: detectOutcome2 } = await Promise.resolve().then(() => (init_outcome(), outcome_exports));
3286
+ switch (sub) {
3287
+ case "start":
3288
+ captureSessionStart2(brainRoot);
3289
+ break;
3290
+ case "end":
3291
+ detectOutcome2(brainRoot);
3292
+ break;
3293
+ default:
3294
+ console.error("Usage: hebbian session <start|end>");
3295
+ process.exit(1);
3296
+ }
3297
+ break;
3298
+ }
3299
+ case "sessions": {
3300
+ const { readEpisodes: readEpisodes2 } = await Promise.resolve().then(() => (init_episode(), episode_exports));
3301
+ const episodes = readEpisodes2(brainRoot).filter((e) => e.outcome);
3302
+ if (episodes.length === 0) {
3303
+ console.log("No session outcomes recorded yet");
3304
+ } else {
3305
+ console.log("Session Outcomes:");
3306
+ for (const ep of episodes.reverse()) {
3307
+ const icon = ep.outcome === "revert" ? "\u{1F504}" : "\u2705";
3308
+ const neurons = ep.neurons ? `${ep.neurons.length} neurons` : "";
3309
+ console.log(` ${icon} ${ep.ts.slice(0, 19)} ${ep.outcome} ${neurons}`);
3310
+ }
3311
+ console.log(`
3312
+ Total: ${episodes.length} sessions`);
3313
+ }
3314
+ break;
3315
+ }
3316
+ case "doctor": {
3317
+ const { runDoctor: runDoctor2 } = await Promise.resolve().then(() => (init_doctor(), doctor_exports));
3318
+ await runDoctor2(brainRoot);
3319
+ break;
3320
+ }
2693
3321
  case "diag":
2694
3322
  case "stats": {
2695
3323
  const { scanBrain: scanBrain2 } = await Promise.resolve().then(() => (init_scanner(), scanner_exports));