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.
package/dist/index.js CHANGED
@@ -56,6 +56,9 @@ var HOOK_MARKER = "[hebbian]";
56
56
  var MAX_CORRECTIONS_PER_SESSION = 10;
57
57
  var MIN_CORRECTION_LENGTH = 15;
58
58
  var DIGEST_LOG_DIR = "hippocampus/digest_log";
59
+ var OUTCOME_TYPES = ["revert", "acceptance"];
60
+ var SESSION_STATE_DIR = "hippocampus/session_state";
61
+ var PROTECTED_REGIONS_CONTRA = ["brainstem", "limbic", "sensors"];
59
62
  function resolveBrainRoot(brainFlag) {
60
63
  if (brainFlag) return resolve(brainFlag);
61
64
  if (process.env.HEBBIAN_BRAIN) return resolve(process.env.HEBBIAN_BRAIN);
@@ -284,6 +287,33 @@ function fireNeuron(brainRoot, neuronPath) {
284
287
  console.log(`\u{1F525} fired: ${neuronPath} (${current} \u2192 ${newCounter})`);
285
288
  return newCounter;
286
289
  }
290
+ function contraNeuron(brainRoot, neuronPath) {
291
+ const fullPath = join2(brainRoot, neuronPath);
292
+ if (!existsSync3(fullPath)) {
293
+ return 0;
294
+ }
295
+ const current = getCurrentContra(fullPath);
296
+ const newContra = current + 1;
297
+ if (current > 0) {
298
+ renameSync(join2(fullPath, `${current}.contra`), join2(fullPath, `${newContra}.contra`));
299
+ } else {
300
+ writeFileSync(join2(fullPath, `${newContra}.contra`), "", "utf8");
301
+ }
302
+ return newContra;
303
+ }
304
+ function getCurrentContra(dir) {
305
+ let max = 0;
306
+ try {
307
+ for (const entry of readdirSync2(dir)) {
308
+ if (entry.endsWith(".contra")) {
309
+ const n = parseInt(entry, 10);
310
+ if (!isNaN(n) && n > max) max = n;
311
+ }
312
+ }
313
+ } catch {
314
+ }
315
+ return max;
316
+ }
287
317
  function getCurrentCounter(dir) {
288
318
  let max = 0;
289
319
  try {
@@ -619,7 +649,7 @@ function emitBootstrap(result, brain) {
619
649
  lines.push("|--------|---------|------------|");
620
650
  for (const region of result.activeRegions) {
621
651
  const active = region.neurons.filter((n) => !n.isDormant);
622
- const activation = active.reduce((sum, n) => sum + n.counter, 0);
652
+ const activation = active.reduce((sum, n) => sum + n.intensity, 0);
623
653
  const icon = REGION_ICONS[region.name] || "";
624
654
  lines.push(`| ${icon} ${region.name} | ${active.length} | ${activation} |`);
625
655
  }
@@ -641,7 +671,7 @@ function emitIndex(result, brain) {
641
671
  const allNeurons = result.activeRegions.flatMap(
642
672
  (r) => r.neurons.filter((n) => !n.isDormant && n.counter >= EMIT_THRESHOLD)
643
673
  );
644
- allNeurons.sort((a, b) => b.counter - a.counter);
674
+ allNeurons.sort((a, b) => b.intensity - a.intensity);
645
675
  lines.push("## Top 10 Active Neurons");
646
676
  lines.push("| # | Path | Counter | Strength |");
647
677
  lines.push("|---|------|---------|----------|");
@@ -667,7 +697,7 @@ function emitIndex(result, brain) {
667
697
  for (const region of result.activeRegions) {
668
698
  const active = region.neurons.filter((n) => !n.isDormant);
669
699
  const dormant = region.neurons.filter((n) => n.isDormant);
670
- const activation = active.reduce((sum, n) => sum + n.counter, 0);
700
+ const activation = active.reduce((sum, n) => sum + n.intensity, 0);
671
701
  const icon = REGION_ICONS[region.name] || "";
672
702
  lines.push(`| ${icon} ${region.name} | ${active.length} | ${dormant.length} | ${activation} | [_rules.md](${region.name}/_rules.md) |`);
673
703
  }
@@ -679,7 +709,7 @@ function emitRegionRules(region) {
679
709
  const ko = REGION_KO[region.name] || "";
680
710
  const active = region.neurons.filter((n) => !n.isDormant);
681
711
  const dormant = region.neurons.filter((n) => n.isDormant);
682
- const activation = active.reduce((sum, n) => sum + n.counter, 0);
712
+ const activation = active.reduce((sum, n) => sum + n.intensity, 0);
683
713
  const lines = [];
684
714
  lines.push(`# ${icon} ${region.name} (${ko})`);
685
715
  lines.push(`> Active: ${active.length} | Dormant: ${dormant.length} | Activation: ${activation}`);
@@ -693,7 +723,7 @@ function emitRegionRules(region) {
693
723
  }
694
724
  if (active.length > 0) {
695
725
  lines.push("## Rules");
696
- const sorted = [...active].sort((a, b) => b.counter - a.counter);
726
+ const sorted = [...active].sort((a, b) => b.intensity - a.intensity);
697
727
  for (const n of sorted) {
698
728
  const indent = " ".repeat(Math.min(n.depth, 4));
699
729
  const prefix = strengthPrefix(n.counter);
@@ -776,7 +806,7 @@ function printDiag(brain, result) {
776
806
  const icon = REGION_ICONS[region.name] || "";
777
807
  const active = region.neurons.filter((n) => !n.isDormant);
778
808
  const dormant = region.neurons.filter((n) => n.isDormant);
779
- const activation = active.reduce((sum, n) => sum + n.counter, 0);
809
+ const activation = active.reduce((sum, n) => sum + n.intensity, 0);
780
810
  const isBlocked = result.blockedRegions.some((r) => r.name === region.name);
781
811
  const status = region.hasBomb ? "\u{1F4A3} BOMB" : isBlocked ? "\u{1F6AB} BLOCKED" : "\u2705 ACTIVE";
782
812
  console.log(` ${icon} ${region.name} [${status}]`);
@@ -786,7 +816,8 @@ function printDiag(brain, result) {
786
816
  }
787
817
  const top3 = sortedActive(region.neurons, 3);
788
818
  for (const n of top3) {
789
- console.log(` \u251C ${n.path} (${n.counter})`);
819
+ const contraStr = n.contra > 0 ? ` contra:${n.contra}` : "";
820
+ console.log(` \u251C ${n.path} (counter:${n.counter}${contraStr} intensity:${n.intensity})`);
790
821
  }
791
822
  }
792
823
  console.log("");
@@ -795,7 +826,7 @@ function pathToSentence(path) {
795
826
  return path.replace(/\//g, " > ").replace(/_/g, " ");
796
827
  }
797
828
  function sortedActive(neurons, n) {
798
- return [...neurons].filter((neuron) => !neuron.isDormant).sort((a, b) => b.counter - a.counter).slice(0, n);
829
+ return [...neurons].filter((neuron) => !neuron.isDormant).sort((a, b) => b.intensity - a.intensity).slice(0, n);
799
830
  }
800
831
  function strengthPrefix(counter) {
801
832
  if (counter >= 10) return "**[ABSOLUTE]** ";
@@ -922,46 +953,155 @@ ${template.description}
922
953
  import { createServer } from "http";
923
954
 
924
955
  // src/inbox.ts
925
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync9, existsSync as existsSync11, mkdirSync as mkdirSync6 } from "fs";
926
- import { join as join12 } from "path";
956
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync9, existsSync as existsSync12, mkdirSync as mkdirSync7 } from "fs";
957
+ import { join as join13 } from "path";
958
+
959
+ // src/candidates.ts
960
+ import { existsSync as existsSync10, mkdirSync as mkdirSync5, readdirSync as readdirSync7, renameSync as renameSync3, rmSync, statSync as statSync3 } from "fs";
961
+ import { join as join11, dirname as dirname2, relative as relative3 } from "path";
962
+ var CANDIDATE_THRESHOLD = 3;
963
+ var CANDIDATE_DECAY_DAYS = 14;
964
+ var CANDIDATE_SEGMENT = "_candidates";
965
+ function toCandidatePath(neuronPath) {
966
+ const slash = neuronPath.indexOf("/");
967
+ if (slash === -1) throw new Error(`Invalid neuron path (missing region): ${neuronPath}`);
968
+ return `${neuronPath.slice(0, slash)}/${CANDIDATE_SEGMENT}/${neuronPath.slice(slash + 1)}`;
969
+ }
970
+ function fromCandidatePath(candidatePath) {
971
+ return candidatePath.replace(`/${CANDIDATE_SEGMENT}/`, "/");
972
+ }
973
+ function growCandidate(brainRoot, neuronPath) {
974
+ const candidatePath = toCandidatePath(neuronPath);
975
+ const result = growNeuron(brainRoot, candidatePath);
976
+ if (result.counter >= CANDIDATE_THRESHOLD) {
977
+ const ok = moveCandidate(brainRoot, candidatePath, neuronPath);
978
+ return { ...result, path: ok ? neuronPath : result.path, promoted: ok };
979
+ }
980
+ console.log(` \u{1F331} candidate (${result.counter}/${CANDIDATE_THRESHOLD}): ${candidatePath}`);
981
+ return { ...result, promoted: false };
982
+ }
983
+ function moveCandidate(brainRoot, candidatePath, targetPath) {
984
+ const src = join11(brainRoot, candidatePath);
985
+ if (!existsSync10(src)) return false;
986
+ const dst = join11(brainRoot, targetPath);
987
+ if (existsSync10(dst)) {
988
+ fireNeuron(brainRoot, targetPath);
989
+ rmSync(src, { recursive: true, force: true });
990
+ } else {
991
+ mkdirSync5(dirname2(dst), { recursive: true });
992
+ renameSync3(src, dst);
993
+ }
994
+ console.log(`\u{1F393} promoted: ${candidatePath} \u2192 ${targetPath}`);
995
+ return true;
996
+ }
997
+ function promoteCandidates(brainRoot) {
998
+ const promoted = [];
999
+ const decayed = [];
1000
+ const decayMs = CANDIDATE_DECAY_DAYS * 24 * 60 * 60 * 1e3;
1001
+ const now = Date.now();
1002
+ for (const region of REGIONS) {
1003
+ const candidateRoot = join11(brainRoot, region, CANDIDATE_SEGMENT);
1004
+ walkNeuronDirs(candidateRoot, (neuronDir) => {
1005
+ const rel = relative3(join11(brainRoot, region), neuronDir);
1006
+ const candidatePath = `${region}/${rel}`;
1007
+ const targetPath = fromCandidatePath(candidatePath);
1008
+ const counter = readCounter(neuronDir);
1009
+ const mtime = statSync3(neuronDir).mtimeMs;
1010
+ if (counter >= CANDIDATE_THRESHOLD) {
1011
+ moveCandidate(brainRoot, candidatePath, targetPath);
1012
+ promoted.push(targetPath);
1013
+ } else if (now - mtime > decayMs) {
1014
+ rmSync(neuronDir, { recursive: true, force: true });
1015
+ decayed.push(candidatePath);
1016
+ console.log(`\u{1F480} candidate decayed: ${candidatePath}`);
1017
+ }
1018
+ });
1019
+ }
1020
+ return { promoted, decayed };
1021
+ }
1022
+ function listCandidates(brainRoot) {
1023
+ const results = [];
1024
+ const now = Date.now();
1025
+ for (const region of REGIONS) {
1026
+ const candidateRoot = join11(brainRoot, region, CANDIDATE_SEGMENT);
1027
+ walkNeuronDirs(candidateRoot, (neuronDir) => {
1028
+ const rel = relative3(join11(brainRoot, region), neuronDir);
1029
+ const candidatePath = `${region}/${rel}`;
1030
+ const targetPath = fromCandidatePath(candidatePath);
1031
+ const counter = readCounter(neuronDir);
1032
+ const mtime = statSync3(neuronDir).mtimeMs;
1033
+ const daysInactive = Math.floor((now - mtime) / (24 * 60 * 60 * 1e3));
1034
+ results.push({ candidatePath, targetPath, counter, daysInactive });
1035
+ });
1036
+ }
1037
+ return results;
1038
+ }
1039
+ function walkNeuronDirs(dir, cb) {
1040
+ if (!existsSync10(dir)) return;
1041
+ try {
1042
+ const entries = readdirSync7(dir, { withFileTypes: true });
1043
+ const hasNeuron = entries.some((e) => e.isFile() && e.name.endsWith(".neuron"));
1044
+ if (hasNeuron) {
1045
+ cb(dir);
1046
+ return;
1047
+ }
1048
+ for (const entry of entries) {
1049
+ if (entry.isDirectory() && !entry.name.startsWith(".")) {
1050
+ walkNeuronDirs(join11(dir, entry.name), cb);
1051
+ }
1052
+ }
1053
+ } catch {
1054
+ }
1055
+ }
1056
+ function readCounter(dir) {
1057
+ try {
1058
+ const files = readdirSync7(dir).filter((f) => /^\d+\.neuron$/.test(f));
1059
+ if (files.length === 0) return 0;
1060
+ return Math.max(...files.map((f) => parseInt(f, 10)));
1061
+ } catch {
1062
+ return 0;
1063
+ }
1064
+ }
927
1065
 
928
1066
  // src/episode.ts
929
- import { readdirSync as readdirSync7, readFileSync as readFileSync3, writeFileSync as writeFileSync8, mkdirSync as mkdirSync5, existsSync as existsSync10 } from "fs";
930
- import { join as join11 } from "path";
1067
+ import { readdirSync as readdirSync8, readFileSync as readFileSync3, writeFileSync as writeFileSync8, mkdirSync as mkdirSync6, existsSync as existsSync11 } from "fs";
1068
+ import { join as join12 } from "path";
931
1069
  var MAX_EPISODES = 100;
932
1070
  var SESSION_LOG_DIR = "hippocampus/session_log";
933
- function logEpisode(brainRoot, type, path, detail) {
934
- const logDir = join11(brainRoot, SESSION_LOG_DIR);
935
- if (!existsSync10(logDir)) {
936
- mkdirSync5(logDir, { recursive: true });
1071
+ function logEpisode(brainRoot, type, path, detail, extra) {
1072
+ const logDir = join12(brainRoot, SESSION_LOG_DIR);
1073
+ if (!existsSync11(logDir)) {
1074
+ mkdirSync6(logDir, { recursive: true });
937
1075
  }
938
1076
  const nextSlot = getNextSlot(logDir);
939
1077
  const episode = {
940
1078
  ts: (/* @__PURE__ */ new Date()).toISOString(),
941
1079
  type,
942
1080
  path,
943
- detail
1081
+ detail,
1082
+ ...extra?.outcome ? { outcome: extra.outcome } : {},
1083
+ ...extra?.neurons ? { neurons: extra.neurons } : {}
944
1084
  };
945
1085
  writeFileSync8(
946
- join11(logDir, `memory${nextSlot}.neuron`),
1086
+ join12(logDir, `memory${nextSlot}.neuron`),
947
1087
  JSON.stringify(episode),
948
1088
  "utf8"
949
1089
  );
950
1090
  }
951
1091
  function readEpisodes(brainRoot) {
952
- const logDir = join11(brainRoot, SESSION_LOG_DIR);
953
- if (!existsSync10(logDir)) return [];
1092
+ const logDir = join12(brainRoot, SESSION_LOG_DIR);
1093
+ if (!existsSync11(logDir)) return [];
954
1094
  const episodes = [];
955
1095
  let entries;
956
1096
  try {
957
- entries = readdirSync7(logDir);
1097
+ entries = readdirSync8(logDir);
958
1098
  } catch {
959
1099
  return [];
960
1100
  }
961
1101
  for (const entry of entries) {
962
1102
  if (!entry.startsWith("memory") || !entry.endsWith(".neuron")) continue;
963
1103
  try {
964
- const content = readFileSync3(join11(logDir, entry), "utf8");
1104
+ const content = readFileSync3(join12(logDir, entry), "utf8");
965
1105
  if (content.trim()) {
966
1106
  episodes.push(JSON.parse(content));
967
1107
  }
@@ -974,7 +1114,7 @@ function readEpisodes(brainRoot) {
974
1114
  function getNextSlot(logDir) {
975
1115
  let maxSlot = 0;
976
1116
  try {
977
- for (const entry of readdirSync7(logDir)) {
1117
+ for (const entry of readdirSync8(logDir)) {
978
1118
  if (entry.startsWith("memory") && entry.endsWith(".neuron")) {
979
1119
  const n = parseInt(entry.replace("memory", "").replace(".neuron", ""), 10);
980
1120
  if (!isNaN(n) && n > maxSlot) maxSlot = n;
@@ -991,8 +1131,8 @@ var INBOX_DIR = "_inbox";
991
1131
  var CORRECTIONS_FILE = "corrections.jsonl";
992
1132
  var DOPAMINE_ALLOWED_ROLES = ["pm", "admin", "lead"];
993
1133
  function processInbox(brainRoot) {
994
- const inboxPath = join12(brainRoot, INBOX_DIR, CORRECTIONS_FILE);
995
- if (!existsSync11(inboxPath)) {
1134
+ const inboxPath = join13(brainRoot, INBOX_DIR, CORRECTIONS_FILE);
1135
+ if (!existsSync12(inboxPath)) {
996
1136
  return { processed: 0, skipped: 0, errors: [] };
997
1137
  }
998
1138
  const content = readFileSync4(inboxPath, "utf8").trim();
@@ -1047,16 +1187,18 @@ function processInbox(brainRoot) {
1047
1187
  }
1048
1188
  function applyCorrection(brainRoot, correction) {
1049
1189
  const neuronPath = correction.path;
1050
- const fullPath = join12(brainRoot, neuronPath);
1190
+ const fullPath = join13(brainRoot, neuronPath);
1051
1191
  const counterAdd = Math.max(1, correction.counter_add || 1);
1052
- if (existsSync11(fullPath)) {
1192
+ if (existsSync12(fullPath)) {
1053
1193
  for (let i = 0; i < counterAdd; i++) {
1054
1194
  fireNeuron(brainRoot, neuronPath);
1055
1195
  }
1056
1196
  } else {
1057
- growNeuron(brainRoot, neuronPath);
1058
- for (let i = 1; i < counterAdd; i++) {
1059
- fireNeuron(brainRoot, neuronPath);
1197
+ const candResult = growCandidate(brainRoot, neuronPath);
1198
+ if (candResult.promoted) {
1199
+ for (let i = 1; i < counterAdd; i++) {
1200
+ fireNeuron(brainRoot, neuronPath);
1201
+ }
1060
1202
  }
1061
1203
  }
1062
1204
  if (correction.dopamine && correction.dopamine > 0) {
@@ -1075,12 +1217,12 @@ function isPathSafe(path) {
1075
1217
  return true;
1076
1218
  }
1077
1219
  function ensureInbox(brainRoot) {
1078
- const inboxDir = join12(brainRoot, INBOX_DIR);
1079
- if (!existsSync11(inboxDir)) {
1080
- mkdirSync6(inboxDir, { recursive: true });
1220
+ const inboxDir = join13(brainRoot, INBOX_DIR);
1221
+ if (!existsSync12(inboxDir)) {
1222
+ mkdirSync7(inboxDir, { recursive: true });
1081
1223
  }
1082
- const filePath = join12(inboxDir, CORRECTIONS_FILE);
1083
- if (!existsSync11(filePath)) {
1224
+ const filePath = join13(inboxDir, CORRECTIONS_FILE);
1225
+ if (!existsSync12(filePath)) {
1084
1226
  writeFileSync9(filePath, "", "utf8");
1085
1227
  }
1086
1228
  return filePath;
@@ -1348,19 +1490,27 @@ function clearReports() {
1348
1490
  }
1349
1491
 
1350
1492
  // src/hooks.ts
1351
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync10, existsSync as existsSync12, mkdirSync as mkdirSync7, readdirSync as readdirSync8 } from "fs";
1493
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync10, existsSync as existsSync13, mkdirSync as mkdirSync8, readdirSync as readdirSync9 } from "fs";
1352
1494
  import { execSync as execSync2 } from "child_process";
1353
- import { join as join13, resolve as resolve2 } from "path";
1495
+ import { join as join14, resolve as resolve2 } from "path";
1354
1496
  var SETTINGS_DIR = ".claude";
1355
1497
  var SETTINGS_FILE = "settings.local.json";
1356
- function installHooks(brainRoot, projectRoot) {
1498
+ function installHooks(brainRoot, projectRoot, global) {
1357
1499
  const root = projectRoot || process.cwd();
1358
1500
  const resolvedBrain = resolve2(brainRoot);
1359
- if (!existsSync12(resolvedBrain) || !hasBrainRegions(resolvedBrain)) {
1501
+ if (global) {
1502
+ const home = process.env.HOME || "~";
1503
+ if (!brainRoot.startsWith("/") && !brainRoot.startsWith(home)) {
1504
+ console.error("\u274C --global requires an absolute --brain path (e.g. --brain ~/brain)");
1505
+ process.exit(1);
1506
+ }
1507
+ }
1508
+ if (!existsSync13(resolvedBrain) || !hasBrainRegions(resolvedBrain)) {
1360
1509
  initBrain(resolvedBrain);
1361
1510
  }
1362
- const settingsDir = join13(root, SETTINGS_DIR);
1363
- const settingsPath = join13(settingsDir, SETTINGS_FILE);
1511
+ const settingsDir = global ? resolve2(process.env.HOME || "~", SETTINGS_DIR) : join14(root, SETTINGS_DIR);
1512
+ const settingsFile = global ? "settings.json" : SETTINGS_FILE;
1513
+ const settingsPath = join14(settingsDir, settingsFile);
1364
1514
  const defaultBrain = resolve2(root, "brain");
1365
1515
  const brainFlag = resolvedBrain === defaultBrain ? "" : ` --brain ${resolvedBrain}`;
1366
1516
  let npxBin = "npx";
@@ -1369,7 +1519,7 @@ function installHooks(brainRoot, projectRoot) {
1369
1519
  } catch {
1370
1520
  }
1371
1521
  let settings = {};
1372
- if (existsSync12(settingsPath)) {
1522
+ if (existsSync13(settingsPath)) {
1373
1523
  try {
1374
1524
  settings = JSON.parse(readFileSync5(settingsPath, "utf8"));
1375
1525
  } catch {
@@ -1386,8 +1536,8 @@ function installHooks(brainRoot, projectRoot) {
1386
1536
  matcher: "startup|resume",
1387
1537
  entry: {
1388
1538
  type: "command",
1389
- command: `${npxBin} hebbian emit claude${brainFlag}`,
1390
- timeout: 10,
1539
+ command: `${npxBin} hebbian emit claude${brainFlag} && ${npxBin} hebbian session start${brainFlag}`,
1540
+ timeout: 15,
1391
1541
  statusMessage: `${HOOK_MARKER} refreshing brain`
1392
1542
  }
1393
1543
  },
@@ -1395,7 +1545,7 @@ function installHooks(brainRoot, projectRoot) {
1395
1545
  event: "Stop",
1396
1546
  entry: {
1397
1547
  type: "command",
1398
- command: `${npxBin} hebbian digest${brainFlag}`,
1548
+ command: `${npxBin} hebbian digest${brainFlag}; ${npxBin} hebbian session end${brainFlag}`,
1399
1549
  timeout: 30,
1400
1550
  statusMessage: `${HOOK_MARKER} digesting session`
1401
1551
  }
@@ -1418,18 +1568,20 @@ function installHooks(brainRoot, projectRoot) {
1418
1568
  hooks[event].push(group);
1419
1569
  }
1420
1570
  }
1421
- if (!existsSync12(settingsDir)) {
1422
- mkdirSync7(settingsDir, { recursive: true });
1571
+ if (!existsSync13(settingsDir)) {
1572
+ mkdirSync8(settingsDir, { recursive: true });
1423
1573
  }
1424
1574
  writeFileSync10(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
1425
1575
  console.log(`\u2705 hebbian hooks installed at ${settingsPath}`);
1426
1576
  console.log(` SessionStart \u2192 ${npxBin} hebbian emit claude${brainFlag}`);
1427
1577
  console.log(` Stop \u2192 ${npxBin} hebbian digest${brainFlag}`);
1428
1578
  }
1429
- function uninstallHooks(projectRoot) {
1579
+ function uninstallHooks(projectRoot, global) {
1430
1580
  const root = projectRoot || process.cwd();
1431
- const settingsPath = join13(root, SETTINGS_DIR, SETTINGS_FILE);
1432
- if (!existsSync12(settingsPath)) {
1581
+ const settingsDir = global ? resolve2(process.env.HOME || "~", SETTINGS_DIR) : join14(root, SETTINGS_DIR);
1582
+ const settingsFile = global ? "settings.json" : SETTINGS_FILE;
1583
+ const settingsPath = join14(settingsDir, settingsFile);
1584
+ if (!existsSync13(settingsPath)) {
1433
1585
  console.log("No hooks installed (settings.local.json not found)");
1434
1586
  return;
1435
1587
  }
@@ -1462,15 +1614,17 @@ function uninstallHooks(projectRoot) {
1462
1614
  writeFileSync10(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
1463
1615
  console.log(`\u2705 removed ${removed} hebbian hook(s) from ${settingsPath}`);
1464
1616
  }
1465
- function checkHooks(projectRoot) {
1617
+ function checkHooks(projectRoot, global) {
1466
1618
  const root = projectRoot || process.cwd();
1467
- const settingsPath = join13(root, SETTINGS_DIR, SETTINGS_FILE);
1619
+ const settingsDir = global ? resolve2(process.env.HOME || "~", SETTINGS_DIR) : join14(root, SETTINGS_DIR);
1620
+ const settingsFile = global ? "settings.json" : SETTINGS_FILE;
1621
+ const settingsPath = join14(settingsDir, settingsFile);
1468
1622
  const status = {
1469
1623
  installed: false,
1470
1624
  path: settingsPath,
1471
1625
  events: []
1472
1626
  };
1473
- if (!existsSync12(settingsPath)) {
1627
+ if (!existsSync13(settingsPath)) {
1474
1628
  console.log(`\u274C hebbian hooks not installed (${settingsPath} not found)`);
1475
1629
  return status;
1476
1630
  }
@@ -1506,9 +1660,9 @@ function checkHooks(projectRoot) {
1506
1660
  return status;
1507
1661
  }
1508
1662
  function hasBrainRegions(dir) {
1509
- if (!existsSync12(dir)) return false;
1663
+ if (!existsSync13(dir)) return false;
1510
1664
  try {
1511
- const entries = readdirSync8(dir);
1665
+ const entries = readdirSync9(dir);
1512
1666
  return REGIONS.some((r) => entries.includes(r));
1513
1667
  } catch {
1514
1668
  return false;
@@ -1516,8 +1670,8 @@ function hasBrainRegions(dir) {
1516
1670
  }
1517
1671
 
1518
1672
  // src/digest.ts
1519
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync11, existsSync as existsSync13, mkdirSync as mkdirSync8 } from "fs";
1520
- import { join as join14, basename } from "path";
1673
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync11, existsSync as existsSync14, mkdirSync as mkdirSync9 } from "fs";
1674
+ import { join as join15, basename } from "path";
1521
1675
  var NEGATION_PATTERNS = [
1522
1676
  /\bdon[''\u2019]?t\b/i,
1523
1677
  /\bdo not\b/i,
@@ -1568,13 +1722,13 @@ function readHookInput(stdin) {
1568
1722
  }
1569
1723
  }
1570
1724
  function digestTranscript(brainRoot, transcriptPath, sessionId) {
1571
- if (!existsSync13(transcriptPath)) {
1725
+ if (!existsSync14(transcriptPath)) {
1572
1726
  throw new Error(`Transcript not found: ${transcriptPath}`);
1573
1727
  }
1574
1728
  const resolvedSessionId = sessionId || basename(transcriptPath, ".jsonl");
1575
- const logDir = join14(brainRoot, DIGEST_LOG_DIR);
1576
- const logPath = join14(logDir, `${resolvedSessionId}.jsonl`);
1577
- if (existsSync13(logPath)) {
1729
+ const logDir = join15(brainRoot, DIGEST_LOG_DIR);
1730
+ const logPath = join15(logDir, `${resolvedSessionId}.jsonl`);
1731
+ if (existsSync14(logPath)) {
1578
1732
  console.log(`\u23ED already digested session ${resolvedSessionId}, skip`);
1579
1733
  return { corrections: 0, skipped: 0, transcriptPath, sessionId: resolvedSessionId };
1580
1734
  }
@@ -1589,7 +1743,7 @@ function digestTranscript(brainRoot, transcriptPath, sessionId) {
1589
1743
  const auditEntries = [];
1590
1744
  for (const correction of corrections) {
1591
1745
  try {
1592
- growNeuron(brainRoot, correction.path);
1746
+ growCandidate(brainRoot, correction.path);
1593
1747
  logEpisode(brainRoot, "digest", correction.path, correction.text);
1594
1748
  auditEntries.push({ correction, applied: true });
1595
1749
  applied++;
@@ -1806,11 +1960,11 @@ function extractKeywords(text) {
1806
1960
  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));
1807
1961
  }
1808
1962
  function writeAuditLog(brainRoot, sessionId, entries) {
1809
- const logDir = join14(brainRoot, DIGEST_LOG_DIR);
1810
- if (!existsSync13(logDir)) {
1811
- mkdirSync8(logDir, { recursive: true });
1963
+ const logDir = join15(brainRoot, DIGEST_LOG_DIR);
1964
+ if (!existsSync14(logDir)) {
1965
+ mkdirSync9(logDir, { recursive: true });
1812
1966
  }
1813
- const logPath = join14(logDir, `${sessionId}.jsonl`);
1967
+ const logPath = join15(logDir, `${sessionId}.jsonl`);
1814
1968
  const lines = entries.map(
1815
1969
  (e) => JSON.stringify({
1816
1970
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -1824,6 +1978,226 @@ function writeAuditLog(brainRoot, sessionId, entries) {
1824
1978
  writeFileSync11(logPath, lines.join("\n") + (lines.length > 0 ? "\n" : ""), "utf8");
1825
1979
  }
1826
1980
 
1981
+ // src/outcome.ts
1982
+ import { execSync as execSync3 } from "child_process";
1983
+ import { existsSync as existsSync15, mkdirSync as mkdirSync10, writeFileSync as writeFileSync12, readFileSync as readFileSync7, readdirSync as readdirSync10, rmSync as rmSync2, statSync as statSync4 } from "fs";
1984
+ import { join as join16 } from "path";
1985
+ import { randomUUID } from "crypto";
1986
+ function captureSessionStart(brainRoot) {
1987
+ let sha;
1988
+ try {
1989
+ sha = execSync3("git rev-parse HEAD", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
1990
+ } catch {
1991
+ console.log("\u23ED\uFE0F session start: not a git repo, skipping");
1992
+ return null;
1993
+ }
1994
+ let status;
1995
+ try {
1996
+ const raw = execSync3("git status --porcelain", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
1997
+ status = raw ? raw.split("\n") : [];
1998
+ } catch {
1999
+ status = [];
2000
+ }
2001
+ const brain = scanBrain(brainRoot);
2002
+ const result = runSubsumption(brain);
2003
+ const neurons = [];
2004
+ for (const region of result.activeRegions) {
2005
+ for (const neuron of region.neurons) {
2006
+ if (!neuron.isDormant && neuron.counter > 0) {
2007
+ neurons.push(`${region.name}/${neuron.path}`);
2008
+ }
2009
+ }
2010
+ }
2011
+ const uuid = randomUUID();
2012
+ const stateDir = join16(brainRoot, SESSION_STATE_DIR);
2013
+ if (!existsSync15(stateDir)) {
2014
+ mkdirSync10(stateDir, { recursive: true });
2015
+ }
2016
+ const state = { ts: (/* @__PURE__ */ new Date()).toISOString(), sha, status, neurons, uuid };
2017
+ writeFileSync12(join16(stateDir, `state_${uuid}.json`), JSON.stringify(state), "utf8");
2018
+ console.log(`\u{1F4F8} session start: SHA ${sha.slice(0, 7)}, ${neurons.length} active neurons`);
2019
+ return state;
2020
+ }
2021
+ function detectOutcome(brainRoot) {
2022
+ const state = readLatestSessionState(brainRoot);
2023
+ if (!state) {
2024
+ console.log("\u23ED\uFE0F session end: no session state found, skipping");
2025
+ return null;
2026
+ }
2027
+ let currentSha;
2028
+ try {
2029
+ currentSha = execSync3("git rev-parse HEAD", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
2030
+ } catch {
2031
+ console.log("\u23ED\uFE0F session end: not a git repo, skipping");
2032
+ cleanupSessionState(brainRoot, state.uuid);
2033
+ return null;
2034
+ }
2035
+ let currentStatus;
2036
+ try {
2037
+ const raw = execSync3("git status --porcelain", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
2038
+ currentStatus = raw ? filterHebbianPaths(raw.split("\n")) : [];
2039
+ } catch {
2040
+ currentStatus = [];
2041
+ }
2042
+ const filteredStartStatus = filterHebbianPaths(state.status);
2043
+ const outcome = classifyOutcome(
2044
+ { ...state, status: filteredStartStatus },
2045
+ currentSha,
2046
+ currentStatus
2047
+ );
2048
+ if (!outcome) {
2049
+ console.log("\u{1F4CA} session end: no changes detected (no-op)");
2050
+ cleanupSessionState(brainRoot, state.uuid);
2051
+ return null;
2052
+ }
2053
+ const neurons = state.neurons;
2054
+ logEpisode(brainRoot, "session-end", "", `outcome:${outcome}`, { outcome, neurons });
2055
+ let result;
2056
+ if (outcome === "revert") {
2057
+ const { affected, skipped } = applyContra(brainRoot, neurons);
2058
+ result = {
2059
+ outcome: "revert",
2060
+ neuronsAffected: affected,
2061
+ protectedSkipped: skipped,
2062
+ detail: `${affected} neurons contra'd (${skipped} protected skipped)`
2063
+ };
2064
+ console.log(`\u{1F4CA} session end: revert \u2014 ${result.detail}`);
2065
+ } else {
2066
+ result = {
2067
+ outcome: "acceptance",
2068
+ neuronsAffected: 0,
2069
+ protectedSkipped: 0,
2070
+ detail: "changes accepted"
2071
+ };
2072
+ console.log("\u{1F4CA} session end: acceptance");
2073
+ }
2074
+ cleanupSessionState(brainRoot, state.uuid);
2075
+ return result;
2076
+ }
2077
+ function classifyOutcome(state, currentSha, currentStatus) {
2078
+ const headMoved = state.sha !== currentSha;
2079
+ const startStatusSet = new Set(state.status);
2080
+ const endStatusSet = new Set(currentStatus);
2081
+ const newItems = currentStatus.filter((s) => !startStatusSet.has(s));
2082
+ const removedItems = state.status.filter((s) => !endStatusSet.has(s));
2083
+ if (!headMoved) {
2084
+ if (newItems.length === 0 && removedItems.length === 0) {
2085
+ return null;
2086
+ }
2087
+ if (newItems.length > 0) {
2088
+ return "acceptance";
2089
+ }
2090
+ if (removedItems.length > 0) {
2091
+ return "revert";
2092
+ }
2093
+ return null;
2094
+ }
2095
+ if (newItems.length > 0) {
2096
+ return "acceptance";
2097
+ }
2098
+ try {
2099
+ const diffStat = execSync3(
2100
+ `git diff ${state.sha}..${currentSha} --stat`,
2101
+ { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }
2102
+ ).trim();
2103
+ const logOutput = execSync3(
2104
+ `git log --oneline ${state.sha}..${currentSha}`,
2105
+ { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }
2106
+ ).trim();
2107
+ if (/\brevert\b/i.test(logOutput)) {
2108
+ return "revert";
2109
+ }
2110
+ if (!diffStat) {
2111
+ return "revert";
2112
+ }
2113
+ return "acceptance";
2114
+ } catch {
2115
+ return null;
2116
+ }
2117
+ }
2118
+ function applyContra(brainRoot, neurons) {
2119
+ let affected = 0;
2120
+ let skipped = 0;
2121
+ for (const neuronPath of neurons) {
2122
+ const region = neuronPath.split("/")[0] || "";
2123
+ if (PROTECTED_REGIONS_CONTRA.includes(region)) {
2124
+ skipped++;
2125
+ continue;
2126
+ }
2127
+ const result = contraNeuron(brainRoot, neuronPath);
2128
+ if (result > 0) {
2129
+ affected++;
2130
+ }
2131
+ }
2132
+ return { affected, skipped };
2133
+ }
2134
+ function buildOutcomeSummary(brainRoot) {
2135
+ const episodes = readEpisodes(brainRoot);
2136
+ const outcomeEpisodes = episodes.filter((e) => e.outcome && e.neurons);
2137
+ if (outcomeEpisodes.length === 0) return "";
2138
+ const stats = /* @__PURE__ */ new Map();
2139
+ for (const ep of outcomeEpisodes) {
2140
+ for (const neuron of ep.neurons) {
2141
+ const existing = stats.get(neuron) || { sessions: 0, reverts: 0, acceptances: 0 };
2142
+ existing.sessions++;
2143
+ if (ep.outcome === "revert") existing.reverts++;
2144
+ if (ep.outcome === "acceptance") existing.acceptances++;
2145
+ stats.set(neuron, existing);
2146
+ }
2147
+ }
2148
+ const lines = ["## Outcome Signals (from session history)\n"];
2149
+ lines.push("Neurons with high contra_ratio (>0.5) are consistently present in reverted sessions. Consider pruning or modifying them.\n");
2150
+ const sorted = [...stats.entries()].sort((a, b) => {
2151
+ const ratioA = a[1].sessions > 0 ? a[1].reverts / a[1].sessions : 0;
2152
+ const ratioB = b[1].sessions > 0 ? b[1].reverts / b[1].sessions : 0;
2153
+ return ratioB - ratioA;
2154
+ });
2155
+ for (const [neuron, s] of sorted) {
2156
+ const ratio = s.sessions > 0 ? (s.reverts / s.sessions).toFixed(2) : "0.00";
2157
+ const trend = parseFloat(ratio) > 0.5 ? "\u2190 act on this" : parseFloat(ratio) > 0.3 ? "\u2190 watch" : "";
2158
+ lines.push(`- ${neuron}: sessions=${s.sessions} reverts=${s.reverts} acceptances=${s.acceptances} contra_ratio=${ratio} ${trend}`);
2159
+ }
2160
+ lines.push("");
2161
+ return lines.join("\n");
2162
+ }
2163
+ function readLatestSessionState(brainRoot) {
2164
+ const stateDir = join16(brainRoot, SESSION_STATE_DIR);
2165
+ if (!existsSync15(stateDir)) return null;
2166
+ let latest = null;
2167
+ try {
2168
+ for (const entry of readdirSync10(stateDir)) {
2169
+ if (!entry.startsWith("state_") || !entry.endsWith(".json")) continue;
2170
+ const fullPath = join16(stateDir, entry);
2171
+ const mtime = statSync4(fullPath).mtimeMs;
2172
+ if (!latest || mtime > latest.mtime) {
2173
+ latest = { path: fullPath, mtime };
2174
+ }
2175
+ }
2176
+ } catch {
2177
+ return null;
2178
+ }
2179
+ if (!latest) return null;
2180
+ try {
2181
+ return JSON.parse(readFileSync7(latest.path, "utf8"));
2182
+ } catch {
2183
+ return null;
2184
+ }
2185
+ }
2186
+ function filterHebbianPaths(statusLines) {
2187
+ const hebbianPatterns = ["hippocampus/session_state", "hippocampus/session_log", "hippocampus/digest_log", "_inbox/"];
2188
+ return statusLines.filter(
2189
+ (line) => !hebbianPatterns.some((p) => line.includes(p))
2190
+ );
2191
+ }
2192
+ function cleanupSessionState(brainRoot, uuid) {
2193
+ const stateDir = join16(brainRoot, SESSION_STATE_DIR);
2194
+ const filePath = join16(stateDir, `state_${uuid}.json`);
2195
+ try {
2196
+ if (existsSync15(filePath)) rmSync2(filePath);
2197
+ } catch {
2198
+ }
2199
+ }
2200
+
1827
2201
  // src/evolve.ts
1828
2202
  var MAX_ACTIONS = 10;
1829
2203
  var PROTECTED_REGIONS = ["brainstem", "limbic", "sensors"];
@@ -1839,7 +2213,8 @@ async function runEvolve(brainRoot, dryRun) {
1839
2213
  const episodes = readEpisodes(brainRoot);
1840
2214
  const brain = scanBrain(brainRoot);
1841
2215
  const summary = buildBrainSummary(brain);
1842
- const prompt = buildPrompt(summary, episodes);
2216
+ const outcomeSummary = buildOutcomeSummary(brainRoot);
2217
+ const prompt = buildPrompt(summary, episodes, outcomeSummary);
1843
2218
  let rawActions;
1844
2219
  try {
1845
2220
  rawActions = await callGemini(prompt, apiKey);
@@ -1887,8 +2262,9 @@ function buildBrainSummary(brain) {
1887
2262
  }
1888
2263
  return lines.join("\n");
1889
2264
  }
1890
- function buildPrompt(summary, episodes) {
2265
+ function buildPrompt(summary, episodes, outcomeSummary) {
1891
2266
  const episodeLines = episodes.length > 0 ? episodes.map((e) => `- [${e.ts}] ${e.type}: ${e.path} \u2014 ${e.detail}`).join("\n") : "(no recent episodes)";
2267
+ const outcomeSection = outcomeSummary || "";
1892
2268
  return `You are the evolve engine for a hebbian brain \u2014 a filesystem-based memory system for AI agents.
1893
2269
 
1894
2270
  ## Axioms
@@ -1900,6 +2276,7 @@ function buildPrompt(summary, episodes) {
1900
2276
  ## Current Brain
1901
2277
  ${summary}
1902
2278
 
2279
+ ${outcomeSection}
1903
2280
  ## Recent Episodes (last ${episodes.length})
1904
2281
  ${episodeLines}
1905
2282
 
@@ -2020,7 +2397,7 @@ function executeActions(brainRoot, actions) {
2020
2397
  fireNeuron(brainRoot, action.path);
2021
2398
  break;
2022
2399
  case "grow":
2023
- growNeuron(brainRoot, action.path);
2400
+ growCandidate(brainRoot, action.path);
2024
2401
  break;
2025
2402
  case "signal":
2026
2403
  signalNeuron(brainRoot, action.path, action.signal || "dopamine");
@@ -2068,15 +2445,23 @@ export {
2068
2445
  MAX_CORRECTIONS_PER_SESSION,
2069
2446
  MAX_DEPTH,
2070
2447
  MIN_CORRECTION_LENGTH,
2448
+ OUTCOME_TYPES,
2449
+ PROTECTED_REGIONS_CONTRA,
2071
2450
  REGIONS,
2072
2451
  REGION_ICONS,
2073
2452
  REGION_KO,
2074
2453
  REGION_PRIORITY,
2454
+ SESSION_STATE_DIR,
2075
2455
  SIGNAL_TYPES,
2076
2456
  SPOTLIGHT_DAYS,
2077
2457
  appendCorrection,
2458
+ buildOutcomeSummary,
2459
+ captureSessionStart,
2078
2460
  checkHooks,
2461
+ classifyOutcome,
2079
2462
  clearReports,
2463
+ contraNeuron,
2464
+ detectOutcome,
2080
2465
  digestTranscript,
2081
2466
  emitBootstrap,
2082
2467
  emitIndex,
@@ -2085,17 +2470,21 @@ export {
2085
2470
  ensureInbox,
2086
2471
  extractCorrections,
2087
2472
  fireNeuron,
2473
+ fromCandidatePath,
2088
2474
  getCurrentCounter,
2089
2475
  getLastActivity,
2090
2476
  getPendingReports,
2091
2477
  gitSnapshot,
2478
+ growCandidate,
2092
2479
  growNeuron,
2093
2480
  initBrain,
2094
2481
  installHooks,
2095
2482
  jaccardSimilarity,
2483
+ listCandidates,
2096
2484
  logEpisode,
2097
2485
  printDiag,
2098
2486
  processInbox,
2487
+ promoteCandidates,
2099
2488
  readEpisodes,
2100
2489
  readHookInput,
2101
2490
  resolveBrainRoot,
@@ -2109,6 +2498,7 @@ export {
2109
2498
  startAPI,
2110
2499
  startWatch,
2111
2500
  stem,
2501
+ toCandidatePath,
2112
2502
  tokenize,
2113
2503
  uninstallHooks,
2114
2504
  writeAllTiers