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.
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(),
@@ -1823,6 +1977,462 @@ function writeAuditLog(brainRoot, sessionId, entries) {
1823
1977
  );
1824
1978
  writeFileSync11(logPath, lines.join("\n") + (lines.length > 0 ? "\n" : ""), "utf8");
1825
1979
  }
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
+
2201
+ // src/evolve.ts
2202
+ var MAX_ACTIONS = 10;
2203
+ var PROTECTED_REGIONS = ["brainstem", "limbic", "sensors"];
2204
+ var DEFAULT_MODEL = "gemini-2.0-flash-lite";
2205
+ var API_TIMEOUT = 3e4;
2206
+ var RETRY_DELAY = 5e3;
2207
+ async function runEvolve(brainRoot, dryRun) {
2208
+ const apiKey = process.env.GEMINI_API_KEY;
2209
+ if (!apiKey) {
2210
+ console.error("\u274C GEMINI_API_KEY not set. Get one at https://aistudio.google.com/apikey");
2211
+ return { actions: [], executed: 0, skipped: 0, dryRun };
2212
+ }
2213
+ const episodes = readEpisodes(brainRoot);
2214
+ const brain = scanBrain(brainRoot);
2215
+ const summary = buildBrainSummary(brain);
2216
+ const outcomeSummary = buildOutcomeSummary(brainRoot);
2217
+ const prompt = buildPrompt(summary, episodes, outcomeSummary);
2218
+ let rawActions;
2219
+ try {
2220
+ rawActions = await callGemini(prompt, apiKey);
2221
+ } catch (err) {
2222
+ const msg = err.message;
2223
+ console.log(`\u23ED\uFE0F evolve skipped: ${msg}`);
2224
+ logEpisode(brainRoot, "evolve-error", "", msg);
2225
+ return { actions: [], executed: 0, skipped: 0, dryRun };
2226
+ }
2227
+ const actions = validateActions(rawActions, brain);
2228
+ const skipped = rawActions.length - actions.length;
2229
+ if (actions.length === 0) {
2230
+ console.log("\u{1F9E0} evolve: no valid actions proposed");
2231
+ return { actions: [], executed: 0, skipped, dryRun };
2232
+ }
2233
+ if (dryRun) {
2234
+ console.log(`\u{1F9E0} evolve (dry-run): ${actions.length} action(s) proposed`);
2235
+ for (const action of actions) {
2236
+ console.log(` ${actionIcon(action.type)} ${action.type} ${action.path} \u2014 ${action.reason}`);
2237
+ }
2238
+ return { actions, executed: 0, skipped, dryRun: true };
2239
+ }
2240
+ const executed = executeActions(brainRoot, actions);
2241
+ logEpisode(brainRoot, "evolve", "", `${executed} action(s) executed, ${skipped} skipped`);
2242
+ console.log(`\u{1F9E0} evolve: ${executed} action(s) executed, ${skipped} skipped`);
2243
+ return { actions, executed, skipped, dryRun: false };
2244
+ }
2245
+ function buildBrainSummary(brain) {
2246
+ const lines = ["# Brain State\n"];
2247
+ for (const region of brain.regions) {
2248
+ const neurons = region.neurons;
2249
+ if (neurons.length === 0 && !region.hasBomb) continue;
2250
+ lines.push(`## ${region.name} (P${REGION_PRIORITY[region.name]})`);
2251
+ if (region.hasBomb) lines.push("\u26A0\uFE0F BOMB active \u2014 region blocked");
2252
+ for (const neuron of neurons) {
2253
+ const flags = [];
2254
+ if (neuron.isDormant) flags.push("dormant");
2255
+ if (neuron.hasBomb) flags.push("bomb");
2256
+ if (neuron.hasMemory) flags.push("memory");
2257
+ if (neuron.dopamine > 0) flags.push(`dopamine:${neuron.dopamine}`);
2258
+ const flagStr = flags.length > 0 ? ` [${flags.join(", ")}]` : "";
2259
+ lines.push(`- ${neuron.path} (counter:${neuron.counter}, intensity:${neuron.intensity})${flagStr}`);
2260
+ }
2261
+ lines.push("");
2262
+ }
2263
+ return lines.join("\n");
2264
+ }
2265
+ function buildPrompt(summary, episodes, outcomeSummary) {
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 || "";
2268
+ return `You are the evolve engine for a hebbian brain \u2014 a filesystem-based memory system for AI agents.
2269
+
2270
+ ## Axioms
2271
+ - Folder = Neuron, File = Firing Trace, Counter = Activation strength
2272
+ - 7 regions in subsumption cascade: brainstem(P0) > limbic(P1) > hippocampus(P2) > sensors(P3) > cortex(P4) > ego(P5) > prefrontal(P6)
2273
+ - Lower priority ALWAYS overrides higher priority
2274
+ - PROTECTED regions (brainstem, limbic, sensors): NEVER propose mutations for these
2275
+
2276
+ ## Current Brain
2277
+ ${summary}
2278
+
2279
+ ${outcomeSection}
2280
+ ## Recent Episodes (last ${episodes.length})
2281
+ ${episodeLines}
2282
+
2283
+ ## Available Actions
2284
+ - grow: Create a new neuron at the given path (region/name). Use for recurring patterns that deserve permanent memory.
2285
+ - fire: Increment an existing neuron's counter. Use for strengthening well-confirmed rules.
2286
+ - signal: Add dopamine (reward), bomb (block), or memory signal. Use sparingly.
2287
+ - prune: Decrement a neuron's counter. Use for rules that aren't working or cause issues.
2288
+ - decay: Mark inactive neurons as dormant. Use for stale rules with no recent activity.
2289
+
2290
+ ## Constraints
2291
+ - Max ${MAX_ACTIONS} actions per cycle
2292
+ - PREFER fire over grow \u2014 strengthen existing neurons before creating new ones
2293
+ - NEVER target brainstem, limbic, or sensors regions
2294
+ - Each action needs a "reason" explaining why
2295
+
2296
+ ## Task
2297
+ Analyze the brain state and recent episodes. Propose actions to improve the brain.
2298
+ Focus on: strengthening repeatedly-used rules, pruning ineffective ones, growing new neurons from repeated patterns.
2299
+
2300
+ Respond with a JSON array of actions:
2301
+ [{"type":"fire","path":"cortex/NO_console_log","reason":"fired 3 times in recent sessions"}]`;
2302
+ }
2303
+ async function callGemini(prompt, apiKey) {
2304
+ const model = process.env.EVOLVE_MODEL || DEFAULT_MODEL;
2305
+ const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`;
2306
+ const body = {
2307
+ contents: [{ parts: [{ text: prompt }] }],
2308
+ generationConfig: {
2309
+ responseMimeType: "application/json",
2310
+ temperature: 0.2
2311
+ }
2312
+ };
2313
+ let lastError = null;
2314
+ for (let attempt = 0; attempt < 2; attempt++) {
2315
+ if (attempt > 0) {
2316
+ await new Promise((r) => setTimeout(r, RETRY_DELAY));
2317
+ }
2318
+ try {
2319
+ const res = await fetch(url, {
2320
+ method: "POST",
2321
+ headers: { "Content-Type": "application/json" },
2322
+ body: JSON.stringify(body),
2323
+ signal: AbortSignal.timeout(API_TIMEOUT)
2324
+ });
2325
+ if (!res.ok) {
2326
+ lastError = new Error(`Gemini API ${res.status}: ${res.statusText}`);
2327
+ continue;
2328
+ }
2329
+ const data = await res.json();
2330
+ if (data.error) {
2331
+ lastError = new Error(`Gemini error: ${data.error.message || "unknown"}`);
2332
+ continue;
2333
+ }
2334
+ const text = data.candidates?.[0]?.content?.parts?.[0]?.text;
2335
+ if (!text) {
2336
+ lastError = new Error("Gemini returned empty response");
2337
+ continue;
2338
+ }
2339
+ return parseActions(text);
2340
+ } catch (err) {
2341
+ lastError = err;
2342
+ continue;
2343
+ }
2344
+ }
2345
+ throw lastError || new Error("Gemini call failed");
2346
+ }
2347
+ function parseActions(text) {
2348
+ let parsed;
2349
+ try {
2350
+ parsed = JSON.parse(text);
2351
+ } catch {
2352
+ throw new Error(`Failed to parse LLM response as JSON: ${text.slice(0, 100)}`);
2353
+ }
2354
+ if (!Array.isArray(parsed)) {
2355
+ throw new Error("LLM response is not an array");
2356
+ }
2357
+ const validTypes = /* @__PURE__ */ new Set(["grow", "fire", "signal", "prune", "decay"]);
2358
+ const actions = [];
2359
+ for (const item of parsed) {
2360
+ if (!item || typeof item !== "object") continue;
2361
+ const { type, path, reason, signal } = item;
2362
+ if (typeof type !== "string" || !validTypes.has(type)) continue;
2363
+ if (typeof path !== "string" || path.length === 0) continue;
2364
+ if (typeof reason !== "string") continue;
2365
+ const action = { type, path, reason };
2366
+ if (type === "signal" && typeof signal === "string") {
2367
+ action.signal = signal;
2368
+ }
2369
+ actions.push(action);
2370
+ }
2371
+ return actions;
2372
+ }
2373
+ function validateActions(actions, _brain) {
2374
+ return actions.filter((action) => {
2375
+ const region = action.path.split("/")[0];
2376
+ if (!region || PROTECTED_REGIONS.includes(region)) {
2377
+ console.log(` \u{1F6E1}\uFE0F blocked: ${action.type} ${action.path} (protected region)`);
2378
+ return false;
2379
+ }
2380
+ if (!REGIONS.includes(region)) {
2381
+ console.log(` \u26A0\uFE0F skipped: ${action.type} ${action.path} (invalid region)`);
2382
+ return false;
2383
+ }
2384
+ if (action.type === "signal" && action.signal && !["dopamine", "bomb", "memory"].includes(action.signal)) {
2385
+ console.log(` \u26A0\uFE0F skipped: signal ${action.path} (invalid signal type: ${action.signal})`);
2386
+ return false;
2387
+ }
2388
+ return true;
2389
+ }).slice(0, MAX_ACTIONS);
2390
+ }
2391
+ function executeActions(brainRoot, actions) {
2392
+ let executed = 0;
2393
+ for (const action of actions) {
2394
+ try {
2395
+ switch (action.type) {
2396
+ case "fire":
2397
+ fireNeuron(brainRoot, action.path);
2398
+ break;
2399
+ case "grow":
2400
+ growCandidate(brainRoot, action.path);
2401
+ break;
2402
+ case "signal":
2403
+ signalNeuron(brainRoot, action.path, action.signal || "dopamine");
2404
+ break;
2405
+ case "prune":
2406
+ rollbackNeuron(brainRoot, action.path);
2407
+ break;
2408
+ case "decay":
2409
+ runDecay(brainRoot, 0);
2410
+ break;
2411
+ }
2412
+ console.log(` ${actionIcon(action.type)} ${action.type} ${action.path}`);
2413
+ executed++;
2414
+ } catch (err) {
2415
+ console.log(` \u26A0\uFE0F failed: ${action.type} ${action.path} \u2014 ${err.message}`);
2416
+ }
2417
+ }
2418
+ return executed;
2419
+ }
2420
+ function actionIcon(type) {
2421
+ switch (type) {
2422
+ case "fire":
2423
+ return "\u{1F525}";
2424
+ case "grow":
2425
+ return "\u{1F331}";
2426
+ case "signal":
2427
+ return "\u26A1";
2428
+ case "prune":
2429
+ return "\u2702\uFE0F";
2430
+ case "decay":
2431
+ return "\u{1F4A4}";
2432
+ default:
2433
+ return "\u2753";
2434
+ }
2435
+ }
1826
2436
  export {
1827
2437
  DECAY_DAYS,
1828
2438
  DIGEST_LOG_DIR,
@@ -1835,15 +2445,23 @@ export {
1835
2445
  MAX_CORRECTIONS_PER_SESSION,
1836
2446
  MAX_DEPTH,
1837
2447
  MIN_CORRECTION_LENGTH,
2448
+ OUTCOME_TYPES,
2449
+ PROTECTED_REGIONS_CONTRA,
1838
2450
  REGIONS,
1839
2451
  REGION_ICONS,
1840
2452
  REGION_KO,
1841
2453
  REGION_PRIORITY,
2454
+ SESSION_STATE_DIR,
1842
2455
  SIGNAL_TYPES,
1843
2456
  SPOTLIGHT_DAYS,
1844
2457
  appendCorrection,
2458
+ buildOutcomeSummary,
2459
+ captureSessionStart,
1845
2460
  checkHooks,
2461
+ classifyOutcome,
1846
2462
  clearReports,
2463
+ contraNeuron,
2464
+ detectOutcome,
1847
2465
  digestTranscript,
1848
2466
  emitBootstrap,
1849
2467
  emitIndex,
@@ -1852,29 +2470,35 @@ export {
1852
2470
  ensureInbox,
1853
2471
  extractCorrections,
1854
2472
  fireNeuron,
2473
+ fromCandidatePath,
1855
2474
  getCurrentCounter,
1856
2475
  getLastActivity,
1857
2476
  getPendingReports,
1858
2477
  gitSnapshot,
2478
+ growCandidate,
1859
2479
  growNeuron,
1860
2480
  initBrain,
1861
2481
  installHooks,
1862
2482
  jaccardSimilarity,
2483
+ listCandidates,
1863
2484
  logEpisode,
1864
2485
  printDiag,
1865
2486
  processInbox,
2487
+ promoteCandidates,
1866
2488
  readEpisodes,
1867
2489
  readHookInput,
1868
2490
  resolveBrainRoot,
1869
2491
  rollbackNeuron,
1870
2492
  runDecay,
1871
2493
  runDedup,
2494
+ runEvolve,
1872
2495
  runSubsumption,
1873
2496
  scanBrain,
1874
2497
  signalNeuron,
1875
2498
  startAPI,
1876
2499
  startWatch,
1877
2500
  stem,
2501
+ toCandidatePath,
1878
2502
  tokenize,
1879
2503
  uninstallHooks,
1880
2504
  writeAllTiers