hebbian 0.3.3 → 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,31 +1248,187 @@ 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
  }
1405
+ function readEpisodes(brainRoot) {
1406
+ const logDir = join13(brainRoot, SESSION_LOG_DIR);
1407
+ if (!existsSync12(logDir)) return [];
1408
+ const episodes = [];
1409
+ let entries;
1410
+ try {
1411
+ entries = readdirSync8(logDir);
1412
+ } catch {
1413
+ return [];
1414
+ }
1415
+ for (const entry of entries) {
1416
+ if (!entry.startsWith("memory") || !entry.endsWith(".neuron")) continue;
1417
+ try {
1418
+ const content = readFileSync4(join13(logDir, entry), "utf8");
1419
+ if (content.trim()) {
1420
+ episodes.push(JSON.parse(content));
1421
+ }
1422
+ } catch {
1423
+ }
1424
+ }
1425
+ episodes.sort((a, b) => a.ts.localeCompare(b.ts));
1426
+ return episodes;
1427
+ }
1240
1428
  function getNextSlot(logDir) {
1241
1429
  let maxSlot = 0;
1242
1430
  try {
1243
- for (const entry of readdirSync7(logDir)) {
1431
+ for (const entry of readdirSync8(logDir)) {
1244
1432
  if (entry.startsWith("memory") && entry.endsWith(".neuron")) {
1245
1433
  const n = parseInt(entry.replace("memory", "").replace(".neuron", ""), 10);
1246
1434
  if (!isNaN(n) && n > maxSlot) maxSlot = n;
@@ -1267,11 +1455,11 @@ __export(inbox_exports, {
1267
1455
  ensureInbox: () => ensureInbox,
1268
1456
  processInbox: () => processInbox
1269
1457
  });
1270
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync10, existsSync as existsSync12, mkdirSync as mkdirSync7 } from "fs";
1271
- 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";
1272
1460
  function processInbox(brainRoot) {
1273
- const inboxPath = join13(brainRoot, INBOX_DIR, CORRECTIONS_FILE);
1274
- if (!existsSync12(inboxPath)) {
1461
+ const inboxPath = join14(brainRoot, INBOX_DIR, CORRECTIONS_FILE);
1462
+ if (!existsSync13(inboxPath)) {
1275
1463
  return { processed: 0, skipped: 0, errors: [] };
1276
1464
  }
1277
1465
  const content = readFileSync5(inboxPath, "utf8").trim();
@@ -1326,16 +1514,18 @@ function processInbox(brainRoot) {
1326
1514
  }
1327
1515
  function applyCorrection(brainRoot, correction) {
1328
1516
  const neuronPath = correction.path;
1329
- const fullPath = join13(brainRoot, neuronPath);
1517
+ const fullPath = join14(brainRoot, neuronPath);
1330
1518
  const counterAdd = Math.max(1, correction.counter_add || 1);
1331
- if (existsSync12(fullPath)) {
1519
+ if (existsSync13(fullPath)) {
1332
1520
  for (let i = 0; i < counterAdd; i++) {
1333
1521
  fireNeuron(brainRoot, neuronPath);
1334
1522
  }
1335
1523
  } else {
1336
- growNeuron(brainRoot, neuronPath);
1337
- for (let i = 1; i < counterAdd; i++) {
1338
- 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
+ }
1339
1529
  }
1340
1530
  }
1341
1531
  if (correction.dopamine && correction.dopamine > 0) {
@@ -1354,12 +1544,12 @@ function isPathSafe(path) {
1354
1544
  return true;
1355
1545
  }
1356
1546
  function ensureInbox(brainRoot) {
1357
- const inboxDir = join13(brainRoot, INBOX_DIR);
1358
- if (!existsSync12(inboxDir)) {
1359
- mkdirSync7(inboxDir, { recursive: true });
1547
+ const inboxDir = join14(brainRoot, INBOX_DIR);
1548
+ if (!existsSync13(inboxDir)) {
1549
+ mkdirSync8(inboxDir, { recursive: true });
1360
1550
  }
1361
- const filePath = join13(inboxDir, CORRECTIONS_FILE);
1362
- if (!existsSync12(filePath)) {
1551
+ const filePath = join14(inboxDir, CORRECTIONS_FILE);
1552
+ if (!existsSync13(filePath)) {
1363
1553
  writeFileSync10(filePath, "", "utf8");
1364
1554
  }
1365
1555
  return filePath;
@@ -1375,7 +1565,7 @@ var init_inbox = __esm({
1375
1565
  "src/inbox.ts"() {
1376
1566
  "use strict";
1377
1567
  init_constants();
1378
- init_grow();
1568
+ init_candidates();
1379
1569
  init_fire();
1380
1570
  init_signal();
1381
1571
  init_episode();
@@ -1672,17 +1862,25 @@ __export(hooks_exports, {
1672
1862
  installHooks: () => installHooks,
1673
1863
  uninstallHooks: () => uninstallHooks
1674
1864
  });
1675
- 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";
1676
1866
  import { execSync as execSync2 } from "child_process";
1677
- import { join as join14, resolve as resolve2 } from "path";
1678
- function installHooks(brainRoot, projectRoot) {
1867
+ import { join as join15, resolve as resolve2 } from "path";
1868
+ function installHooks(brainRoot, projectRoot, global) {
1679
1869
  const root = projectRoot || process.cwd();
1680
1870
  const resolvedBrain = resolve2(brainRoot);
1681
- 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)) {
1682
1879
  initBrain(resolvedBrain);
1683
1880
  }
1684
- const settingsDir = join14(root, SETTINGS_DIR);
1685
- 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);
1686
1884
  const defaultBrain = resolve2(root, "brain");
1687
1885
  const brainFlag = resolvedBrain === defaultBrain ? "" : ` --brain ${resolvedBrain}`;
1688
1886
  let npxBin = "npx";
@@ -1691,7 +1889,7 @@ function installHooks(brainRoot, projectRoot) {
1691
1889
  } catch {
1692
1890
  }
1693
1891
  let settings = {};
1694
- if (existsSync13(settingsPath)) {
1892
+ if (existsSync14(settingsPath)) {
1695
1893
  try {
1696
1894
  settings = JSON.parse(readFileSync6(settingsPath, "utf8"));
1697
1895
  } catch {
@@ -1708,8 +1906,8 @@ function installHooks(brainRoot, projectRoot) {
1708
1906
  matcher: "startup|resume",
1709
1907
  entry: {
1710
1908
  type: "command",
1711
- command: `${npxBin} hebbian emit claude${brainFlag}`,
1712
- timeout: 10,
1909
+ command: `${npxBin} hebbian emit claude${brainFlag} && ${npxBin} hebbian session start${brainFlag}`,
1910
+ timeout: 15,
1713
1911
  statusMessage: `${HOOK_MARKER} refreshing brain`
1714
1912
  }
1715
1913
  },
@@ -1717,7 +1915,7 @@ function installHooks(brainRoot, projectRoot) {
1717
1915
  event: "Stop",
1718
1916
  entry: {
1719
1917
  type: "command",
1720
- command: `${npxBin} hebbian digest${brainFlag}`,
1918
+ command: `${npxBin} hebbian digest${brainFlag}; ${npxBin} hebbian session end${brainFlag}`,
1721
1919
  timeout: 30,
1722
1920
  statusMessage: `${HOOK_MARKER} digesting session`
1723
1921
  }
@@ -1740,18 +1938,20 @@ function installHooks(brainRoot, projectRoot) {
1740
1938
  hooks[event].push(group);
1741
1939
  }
1742
1940
  }
1743
- if (!existsSync13(settingsDir)) {
1744
- mkdirSync8(settingsDir, { recursive: true });
1941
+ if (!existsSync14(settingsDir)) {
1942
+ mkdirSync9(settingsDir, { recursive: true });
1745
1943
  }
1746
1944
  writeFileSync11(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
1747
1945
  console.log(`\u2705 hebbian hooks installed at ${settingsPath}`);
1748
1946
  console.log(` SessionStart \u2192 ${npxBin} hebbian emit claude${brainFlag}`);
1749
1947
  console.log(` Stop \u2192 ${npxBin} hebbian digest${brainFlag}`);
1750
1948
  }
1751
- function uninstallHooks(projectRoot) {
1949
+ function uninstallHooks(projectRoot, global) {
1752
1950
  const root = projectRoot || process.cwd();
1753
- const settingsPath = join14(root, SETTINGS_DIR, SETTINGS_FILE);
1754
- 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)) {
1755
1955
  console.log("No hooks installed (settings.local.json not found)");
1756
1956
  return;
1757
1957
  }
@@ -1784,15 +1984,17 @@ function uninstallHooks(projectRoot) {
1784
1984
  writeFileSync11(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
1785
1985
  console.log(`\u2705 removed ${removed} hebbian hook(s) from ${settingsPath}`);
1786
1986
  }
1787
- function checkHooks(projectRoot) {
1987
+ function checkHooks(projectRoot, global) {
1788
1988
  const root = projectRoot || process.cwd();
1789
- 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);
1790
1992
  const status = {
1791
1993
  installed: false,
1792
1994
  path: settingsPath,
1793
1995
  events: []
1794
1996
  };
1795
- if (!existsSync13(settingsPath)) {
1997
+ if (!existsSync14(settingsPath)) {
1796
1998
  console.log(`\u274C hebbian hooks not installed (${settingsPath} not found)`);
1797
1999
  return status;
1798
2000
  }
@@ -1828,9 +2030,9 @@ function checkHooks(projectRoot) {
1828
2030
  return status;
1829
2031
  }
1830
2032
  function hasBrainRegions(dir) {
1831
- if (!existsSync13(dir)) return false;
2033
+ if (!existsSync14(dir)) return false;
1832
2034
  try {
1833
- const entries = readdirSync8(dir);
2035
+ const entries = readdirSync9(dir);
1834
2036
  return REGIONS.some((r) => entries.includes(r));
1835
2037
  } catch {
1836
2038
  return false;
@@ -1854,8 +2056,8 @@ __export(digest_exports, {
1854
2056
  extractCorrections: () => extractCorrections,
1855
2057
  readHookInput: () => readHookInput
1856
2058
  });
1857
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync12, existsSync as existsSync14, mkdirSync as mkdirSync9 } from "fs";
1858
- 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";
1859
2061
  function readHookInput(stdin) {
1860
2062
  if (!stdin.trim()) return null;
1861
2063
  try {
@@ -1870,13 +2072,13 @@ function readHookInput(stdin) {
1870
2072
  }
1871
2073
  }
1872
2074
  function digestTranscript(brainRoot, transcriptPath, sessionId) {
1873
- if (!existsSync14(transcriptPath)) {
2075
+ if (!existsSync15(transcriptPath)) {
1874
2076
  throw new Error(`Transcript not found: ${transcriptPath}`);
1875
2077
  }
1876
2078
  const resolvedSessionId = sessionId || basename(transcriptPath, ".jsonl");
1877
- const logDir = join15(brainRoot, DIGEST_LOG_DIR);
1878
- const logPath = join15(logDir, `${resolvedSessionId}.jsonl`);
1879
- if (existsSync14(logPath)) {
2079
+ const logDir = join16(brainRoot, DIGEST_LOG_DIR);
2080
+ const logPath = join16(logDir, `${resolvedSessionId}.jsonl`);
2081
+ if (existsSync15(logPath)) {
1880
2082
  console.log(`\u23ED already digested session ${resolvedSessionId}, skip`);
1881
2083
  return { corrections: 0, skipped: 0, transcriptPath, sessionId: resolvedSessionId };
1882
2084
  }
@@ -1891,7 +2093,7 @@ function digestTranscript(brainRoot, transcriptPath, sessionId) {
1891
2093
  const auditEntries = [];
1892
2094
  for (const correction of corrections) {
1893
2095
  try {
1894
- growNeuron(brainRoot, correction.path);
2096
+ growCandidate(brainRoot, correction.path);
1895
2097
  logEpisode(brainRoot, "digest", correction.path, correction.text);
1896
2098
  auditEntries.push({ correction, applied: true });
1897
2099
  applied++;
@@ -2108,11 +2310,11 @@ function extractKeywords(text) {
2108
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));
2109
2311
  }
2110
2312
  function writeAuditLog(brainRoot, sessionId, entries) {
2111
- const logDir = join15(brainRoot, DIGEST_LOG_DIR);
2112
- if (!existsSync14(logDir)) {
2113
- mkdirSync9(logDir, { recursive: true });
2313
+ const logDir = join16(brainRoot, DIGEST_LOG_DIR);
2314
+ if (!existsSync15(logDir)) {
2315
+ mkdirSync10(logDir, { recursive: true });
2114
2316
  }
2115
- const logPath = join15(logDir, `${sessionId}.jsonl`);
2317
+ const logPath = join16(logDir, `${sessionId}.jsonl`);
2116
2318
  const lines = entries.map(
2117
2319
  (e) => JSON.stringify({
2118
2320
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -2130,7 +2332,7 @@ var init_digest = __esm({
2130
2332
  "src/digest.ts"() {
2131
2333
  "use strict";
2132
2334
  init_constants();
2133
- init_grow();
2335
+ init_candidates();
2134
2336
  init_episode();
2135
2337
  NEGATION_PATTERNS = [
2136
2338
  /\bdon[''\u2019]?t\b/i,
@@ -2171,11 +2373,649 @@ var init_digest = __esm({
2171
2373
  }
2172
2374
  });
2173
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
+
2613
+ // src/evolve.ts
2614
+ var evolve_exports = {};
2615
+ __export(evolve_exports, {
2616
+ buildBrainSummary: () => buildBrainSummary,
2617
+ buildPrompt: () => buildPrompt,
2618
+ callGemini: () => callGemini,
2619
+ executeActions: () => executeActions,
2620
+ parseActions: () => parseActions,
2621
+ runEvolve: () => runEvolve,
2622
+ validateActions: () => validateActions
2623
+ });
2624
+ async function runEvolve(brainRoot, dryRun) {
2625
+ const apiKey = process.env.GEMINI_API_KEY;
2626
+ if (!apiKey) {
2627
+ console.error("\u274C GEMINI_API_KEY not set. Get one at https://aistudio.google.com/apikey");
2628
+ return { actions: [], executed: 0, skipped: 0, dryRun };
2629
+ }
2630
+ const episodes = readEpisodes(brainRoot);
2631
+ const brain = scanBrain(brainRoot);
2632
+ const summary = buildBrainSummary(brain);
2633
+ const outcomeSummary = buildOutcomeSummary(brainRoot);
2634
+ const prompt = buildPrompt(summary, episodes, outcomeSummary);
2635
+ let rawActions;
2636
+ try {
2637
+ rawActions = await callGemini(prompt, apiKey);
2638
+ } catch (err) {
2639
+ const msg = err.message;
2640
+ console.log(`\u23ED\uFE0F evolve skipped: ${msg}`);
2641
+ logEpisode(brainRoot, "evolve-error", "", msg);
2642
+ return { actions: [], executed: 0, skipped: 0, dryRun };
2643
+ }
2644
+ const actions = validateActions(rawActions, brain);
2645
+ const skipped = rawActions.length - actions.length;
2646
+ if (actions.length === 0) {
2647
+ console.log("\u{1F9E0} evolve: no valid actions proposed");
2648
+ return { actions: [], executed: 0, skipped, dryRun };
2649
+ }
2650
+ if (dryRun) {
2651
+ console.log(`\u{1F9E0} evolve (dry-run): ${actions.length} action(s) proposed`);
2652
+ for (const action of actions) {
2653
+ console.log(` ${actionIcon(action.type)} ${action.type} ${action.path} \u2014 ${action.reason}`);
2654
+ }
2655
+ return { actions, executed: 0, skipped, dryRun: true };
2656
+ }
2657
+ const executed = executeActions(brainRoot, actions);
2658
+ logEpisode(brainRoot, "evolve", "", `${executed} action(s) executed, ${skipped} skipped`);
2659
+ console.log(`\u{1F9E0} evolve: ${executed} action(s) executed, ${skipped} skipped`);
2660
+ return { actions, executed, skipped, dryRun: false };
2661
+ }
2662
+ function buildBrainSummary(brain) {
2663
+ const lines = ["# Brain State\n"];
2664
+ for (const region of brain.regions) {
2665
+ const neurons = region.neurons;
2666
+ if (neurons.length === 0 && !region.hasBomb) continue;
2667
+ lines.push(`## ${region.name} (P${REGION_PRIORITY[region.name]})`);
2668
+ if (region.hasBomb) lines.push("\u26A0\uFE0F BOMB active \u2014 region blocked");
2669
+ for (const neuron of neurons) {
2670
+ const flags = [];
2671
+ if (neuron.isDormant) flags.push("dormant");
2672
+ if (neuron.hasBomb) flags.push("bomb");
2673
+ if (neuron.hasMemory) flags.push("memory");
2674
+ if (neuron.dopamine > 0) flags.push(`dopamine:${neuron.dopamine}`);
2675
+ const flagStr = flags.length > 0 ? ` [${flags.join(", ")}]` : "";
2676
+ lines.push(`- ${neuron.path} (counter:${neuron.counter}, intensity:${neuron.intensity})${flagStr}`);
2677
+ }
2678
+ lines.push("");
2679
+ }
2680
+ return lines.join("\n");
2681
+ }
2682
+ function buildPrompt(summary, episodes, outcomeSummary) {
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 || "";
2685
+ return `You are the evolve engine for a hebbian brain \u2014 a filesystem-based memory system for AI agents.
2686
+
2687
+ ## Axioms
2688
+ - Folder = Neuron, File = Firing Trace, Counter = Activation strength
2689
+ - 7 regions in subsumption cascade: brainstem(P0) > limbic(P1) > hippocampus(P2) > sensors(P3) > cortex(P4) > ego(P5) > prefrontal(P6)
2690
+ - Lower priority ALWAYS overrides higher priority
2691
+ - PROTECTED regions (brainstem, limbic, sensors): NEVER propose mutations for these
2692
+
2693
+ ## Current Brain
2694
+ ${summary}
2695
+
2696
+ ${outcomeSection}
2697
+ ## Recent Episodes (last ${episodes.length})
2698
+ ${episodeLines}
2699
+
2700
+ ## Available Actions
2701
+ - grow: Create a new neuron at the given path (region/name). Use for recurring patterns that deserve permanent memory.
2702
+ - fire: Increment an existing neuron's counter. Use for strengthening well-confirmed rules.
2703
+ - signal: Add dopamine (reward), bomb (block), or memory signal. Use sparingly.
2704
+ - prune: Decrement a neuron's counter. Use for rules that aren't working or cause issues.
2705
+ - decay: Mark inactive neurons as dormant. Use for stale rules with no recent activity.
2706
+
2707
+ ## Constraints
2708
+ - Max ${MAX_ACTIONS} actions per cycle
2709
+ - PREFER fire over grow \u2014 strengthen existing neurons before creating new ones
2710
+ - NEVER target brainstem, limbic, or sensors regions
2711
+ - Each action needs a "reason" explaining why
2712
+
2713
+ ## Task
2714
+ Analyze the brain state and recent episodes. Propose actions to improve the brain.
2715
+ Focus on: strengthening repeatedly-used rules, pruning ineffective ones, growing new neurons from repeated patterns.
2716
+
2717
+ Respond with a JSON array of actions:
2718
+ [{"type":"fire","path":"cortex/NO_console_log","reason":"fired 3 times in recent sessions"}]`;
2719
+ }
2720
+ async function callGemini(prompt, apiKey) {
2721
+ const model = process.env.EVOLVE_MODEL || DEFAULT_MODEL;
2722
+ const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`;
2723
+ const body = {
2724
+ contents: [{ parts: [{ text: prompt }] }],
2725
+ generationConfig: {
2726
+ responseMimeType: "application/json",
2727
+ temperature: 0.2
2728
+ }
2729
+ };
2730
+ let lastError = null;
2731
+ for (let attempt = 0; attempt < 2; attempt++) {
2732
+ if (attempt > 0) {
2733
+ await new Promise((r) => setTimeout(r, RETRY_DELAY));
2734
+ }
2735
+ try {
2736
+ const res = await fetch(url, {
2737
+ method: "POST",
2738
+ headers: { "Content-Type": "application/json" },
2739
+ body: JSON.stringify(body),
2740
+ signal: AbortSignal.timeout(API_TIMEOUT)
2741
+ });
2742
+ if (!res.ok) {
2743
+ lastError = new Error(`Gemini API ${res.status}: ${res.statusText}`);
2744
+ continue;
2745
+ }
2746
+ const data = await res.json();
2747
+ if (data.error) {
2748
+ lastError = new Error(`Gemini error: ${data.error.message || "unknown"}`);
2749
+ continue;
2750
+ }
2751
+ const text = data.candidates?.[0]?.content?.parts?.[0]?.text;
2752
+ if (!text) {
2753
+ lastError = new Error("Gemini returned empty response");
2754
+ continue;
2755
+ }
2756
+ return parseActions(text);
2757
+ } catch (err) {
2758
+ lastError = err;
2759
+ continue;
2760
+ }
2761
+ }
2762
+ throw lastError || new Error("Gemini call failed");
2763
+ }
2764
+ function parseActions(text) {
2765
+ let parsed;
2766
+ try {
2767
+ parsed = JSON.parse(text);
2768
+ } catch {
2769
+ throw new Error(`Failed to parse LLM response as JSON: ${text.slice(0, 100)}`);
2770
+ }
2771
+ if (!Array.isArray(parsed)) {
2772
+ throw new Error("LLM response is not an array");
2773
+ }
2774
+ const validTypes = /* @__PURE__ */ new Set(["grow", "fire", "signal", "prune", "decay"]);
2775
+ const actions = [];
2776
+ for (const item of parsed) {
2777
+ if (!item || typeof item !== "object") continue;
2778
+ const { type, path, reason, signal } = item;
2779
+ if (typeof type !== "string" || !validTypes.has(type)) continue;
2780
+ if (typeof path !== "string" || path.length === 0) continue;
2781
+ if (typeof reason !== "string") continue;
2782
+ const action = { type, path, reason };
2783
+ if (type === "signal" && typeof signal === "string") {
2784
+ action.signal = signal;
2785
+ }
2786
+ actions.push(action);
2787
+ }
2788
+ return actions;
2789
+ }
2790
+ function validateActions(actions, _brain) {
2791
+ return actions.filter((action) => {
2792
+ const region = action.path.split("/")[0];
2793
+ if (!region || PROTECTED_REGIONS.includes(region)) {
2794
+ console.log(` \u{1F6E1}\uFE0F blocked: ${action.type} ${action.path} (protected region)`);
2795
+ return false;
2796
+ }
2797
+ if (!REGIONS.includes(region)) {
2798
+ console.log(` \u26A0\uFE0F skipped: ${action.type} ${action.path} (invalid region)`);
2799
+ return false;
2800
+ }
2801
+ if (action.type === "signal" && action.signal && !["dopamine", "bomb", "memory"].includes(action.signal)) {
2802
+ console.log(` \u26A0\uFE0F skipped: signal ${action.path} (invalid signal type: ${action.signal})`);
2803
+ return false;
2804
+ }
2805
+ return true;
2806
+ }).slice(0, MAX_ACTIONS);
2807
+ }
2808
+ function executeActions(brainRoot, actions) {
2809
+ let executed = 0;
2810
+ for (const action of actions) {
2811
+ try {
2812
+ switch (action.type) {
2813
+ case "fire":
2814
+ fireNeuron(brainRoot, action.path);
2815
+ break;
2816
+ case "grow":
2817
+ growCandidate(brainRoot, action.path);
2818
+ break;
2819
+ case "signal":
2820
+ signalNeuron(brainRoot, action.path, action.signal || "dopamine");
2821
+ break;
2822
+ case "prune":
2823
+ rollbackNeuron(brainRoot, action.path);
2824
+ break;
2825
+ case "decay":
2826
+ runDecay(brainRoot, 0);
2827
+ break;
2828
+ }
2829
+ console.log(` ${actionIcon(action.type)} ${action.type} ${action.path}`);
2830
+ executed++;
2831
+ } catch (err) {
2832
+ console.log(` \u26A0\uFE0F failed: ${action.type} ${action.path} \u2014 ${err.message}`);
2833
+ }
2834
+ }
2835
+ return executed;
2836
+ }
2837
+ function actionIcon(type) {
2838
+ switch (type) {
2839
+ case "fire":
2840
+ return "\u{1F525}";
2841
+ case "grow":
2842
+ return "\u{1F331}";
2843
+ case "signal":
2844
+ return "\u26A1";
2845
+ case "prune":
2846
+ return "\u2702\uFE0F";
2847
+ case "decay":
2848
+ return "\u{1F4A4}";
2849
+ default:
2850
+ return "\u2753";
2851
+ }
2852
+ }
2853
+ var MAX_ACTIONS, PROTECTED_REGIONS, DEFAULT_MODEL, API_TIMEOUT, RETRY_DELAY;
2854
+ var init_evolve = __esm({
2855
+ "src/evolve.ts"() {
2856
+ "use strict";
2857
+ init_episode();
2858
+ init_scanner();
2859
+ init_constants();
2860
+ init_fire();
2861
+ init_candidates();
2862
+ init_signal();
2863
+ init_rollback();
2864
+ init_decay();
2865
+ init_outcome();
2866
+ MAX_ACTIONS = 10;
2867
+ PROTECTED_REGIONS = ["brainstem", "limbic", "sensors"];
2868
+ DEFAULT_MODEL = "gemini-2.0-flash-lite";
2869
+ API_TIMEOUT = 3e4;
2870
+ RETRY_DELAY = 5e3;
2871
+ }
2872
+ });
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
+
2174
3014
  // src/cli.ts
2175
3015
  init_constants();
2176
3016
  import { parseArgs } from "util";
2177
3017
  import { resolve as resolve3 } from "path";
2178
- var VERSION = "0.3.2";
3018
+ var VERSION = "0.5.0";
2179
3019
  var HELP = `
2180
3020
  hebbian v${VERSION} \u2014 Folder-as-neuron brain for any AI agent.
2181
3021
 
@@ -2199,6 +3039,11 @@ COMMANDS:
2199
3039
  inbox Process corrections inbox
2200
3040
  claude install|uninstall|status Manage Claude Code hooks
2201
3041
  digest [--transcript <path>] Extract corrections from conversation
3042
+ candidates [promote] List candidates or promote graduated ones
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)
2202
3047
  diag Print brain diagnostics
2203
3048
  stats Print brain statistics
2204
3049
 
@@ -2213,6 +3058,7 @@ EXAMPLES:
2213
3058
  hebbian fire cortex/frontend/NO_console_log --brain ./my-brain
2214
3059
  hebbian emit claude --brain ./my-brain
2215
3060
  hebbian emit all
3061
+ GEMINI_API_KEY=... hebbian evolve --dry-run
2216
3062
  `.trim();
2217
3063
  function readStdin() {
2218
3064
  return new Promise((resolve4) => {
@@ -2238,6 +3084,8 @@ async function main(argv) {
2238
3084
  days: { type: "string", short: "d" },
2239
3085
  port: { type: "string", short: "p" },
2240
3086
  transcript: { type: "string", short: "t" },
3087
+ "dry-run": { type: "boolean" },
3088
+ global: { type: "boolean", short: "g" },
2241
3089
  help: { type: "boolean", short: "h" },
2242
3090
  version: { type: "boolean", short: "v" }
2243
3091
  },
@@ -2358,18 +3206,23 @@ async function main(argv) {
2358
3206
  }
2359
3207
  case "claude": {
2360
3208
  const sub = positionals[1];
3209
+ const isGlobal = values.global === true;
2361
3210
  const { installHooks: installHooks2, uninstallHooks: uninstallHooks2, checkHooks: checkHooks2 } = await Promise.resolve().then(() => (init_hooks(), hooks_exports));
2362
3211
  switch (sub) {
2363
3212
  case "install": {
2364
- const installBrain = values.brain ? resolve3(values.brain) : resolve3("./brain");
2365
- 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);
2366
3219
  break;
2367
3220
  }
2368
3221
  case "uninstall":
2369
- uninstallHooks2();
3222
+ uninstallHooks2(void 0, isGlobal);
2370
3223
  break;
2371
3224
  case "status": {
2372
- checkHooks2();
3225
+ checkHooks2(void 0, isGlobal);
2373
3226
  console.log(` version: v${VERSION}`);
2374
3227
  const { checkForUpdates: checkUpdates, formatUpdateBanner: formatBanner } = await Promise.resolve().then(() => (init_update_check(), update_check_exports));
2375
3228
  const updateStatus = await checkUpdates(VERSION);
@@ -2378,7 +3231,7 @@ async function main(argv) {
2378
3231
  break;
2379
3232
  }
2380
3233
  default:
2381
- console.error("Usage: hebbian claude <install|uninstall|status>");
3234
+ console.error("Usage: hebbian claude <install|uninstall|status> [--global]");
2382
3235
  process.exit(1);
2383
3236
  }
2384
3237
  break;
@@ -2401,6 +3254,70 @@ async function main(argv) {
2401
3254
  }
2402
3255
  break;
2403
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
+ }
3277
+ case "evolve": {
3278
+ const dryRun = values["dry-run"] === true;
3279
+ const { runEvolve: runEvolve2 } = await Promise.resolve().then(() => (init_evolve(), evolve_exports));
3280
+ await runEvolve2(brainRoot, dryRun);
3281
+ break;
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
+ }
2404
3321
  case "diag":
2405
3322
  case "stats": {
2406
3323
  const { scanBrain: scanBrain2 } = await Promise.resolve().then(() => (init_scanner(), scanner_exports));