hebbian 0.3.2 → 0.3.4

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.
@@ -555,6 +555,8 @@ function writeTarget(filePath, content) {
555
555
  writeFileSync2(filePath, before + content + after, "utf8");
556
556
  return;
557
557
  }
558
+ writeFileSync2(filePath, content + "\n\n" + existing, "utf8");
559
+ return;
558
560
  }
559
561
  writeFileSync2(filePath, content, "utf8");
560
562
  }
@@ -607,28 +609,174 @@ var init_emit = __esm({
607
609
  }
608
610
  });
609
611
 
612
+ // src/update-check.ts
613
+ var update_check_exports = {};
614
+ __export(update_check_exports, {
615
+ checkForUpdates: () => checkForUpdates,
616
+ formatUpdateBanner: () => formatUpdateBanner
617
+ });
618
+ import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3, statSync as statSync2, unlinkSync } from "fs";
619
+ import { join as join4 } from "path";
620
+ function getStateDir() {
621
+ return join4(process.env.HOME || "~", ".hebbian");
622
+ }
623
+ function ensureStateDir(stateDir) {
624
+ if (!existsSync5(stateDir)) {
625
+ mkdirSync3(stateDir, { recursive: true });
626
+ }
627
+ }
628
+ function isCacheStale(cachePath, type) {
629
+ try {
630
+ const mtime = statSync2(cachePath).mtimeMs;
631
+ const ageMinutes = (Date.now() - mtime) / 1e3 / 60;
632
+ const ttl = type === "UP_TO_DATE" ? TTL_UP_TO_DATE : TTL_UPGRADE_AVAILABLE;
633
+ return ageMinutes > ttl;
634
+ } catch {
635
+ return true;
636
+ }
637
+ }
638
+ function readCache(stateDir) {
639
+ const cachePath = join4(stateDir, "last-update-check");
640
+ if (!existsSync5(cachePath)) return null;
641
+ try {
642
+ const line = readFileSync3(cachePath, "utf8").trim();
643
+ if (line.startsWith("UP_TO_DATE")) {
644
+ if (isCacheStale(cachePath, "UP_TO_DATE")) return null;
645
+ const ver = line.split(/\s+/)[1];
646
+ return { type: "UP_TO_DATE", current: ver };
647
+ }
648
+ if (line.startsWith("UPGRADE_AVAILABLE")) {
649
+ if (isCacheStale(cachePath, "UPGRADE_AVAILABLE")) return null;
650
+ const [, current, latest] = line.split(/\s+/);
651
+ return { type: "UPGRADE_AVAILABLE", current, latest };
652
+ }
653
+ return null;
654
+ } catch {
655
+ return null;
656
+ }
657
+ }
658
+ function writeCache(stateDir, line) {
659
+ ensureStateDir(stateDir);
660
+ writeFileSync3(join4(stateDir, "last-update-check"), line, "utf8");
661
+ }
662
+ function isSnoozed(stateDir, remoteVersion) {
663
+ const snoozePath = join4(stateDir, "update-snoozed");
664
+ if (!existsSync5(snoozePath)) return false;
665
+ try {
666
+ const [ver, levelStr, epochStr] = readFileSync3(snoozePath, "utf8").trim().split(/\s+/);
667
+ if (ver !== remoteVersion) {
668
+ unlinkSync(snoozePath);
669
+ return false;
670
+ }
671
+ const level = parseInt(levelStr || "1", 10);
672
+ const epoch = parseInt(epochStr || "0", 10);
673
+ const now = Math.floor(Date.now() / 1e3);
674
+ const duration = SNOOZE_DURATIONS[Math.min(level, 3)] ?? SNOOZE_DURATIONS[3];
675
+ return now < epoch + duration;
676
+ } catch {
677
+ return false;
678
+ }
679
+ }
680
+ async function fetchLatestVersion() {
681
+ try {
682
+ const controller = new AbortController();
683
+ const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
684
+ const res = await fetch(NPM_REGISTRY_URL, {
685
+ signal: controller.signal,
686
+ headers: { Accept: "application/json" }
687
+ });
688
+ clearTimeout(timeout);
689
+ if (!res.ok) return null;
690
+ const data = await res.json();
691
+ const version = data.version;
692
+ if (!version || !/^\d+\.\d+[\d.]*$/.test(version)) return null;
693
+ return version;
694
+ } catch {
695
+ return null;
696
+ }
697
+ }
698
+ async function checkForUpdates(currentVersion) {
699
+ if (process.env.HEBBIAN_UPDATE_CHECK === "false") {
700
+ return { type: "skipped" };
701
+ }
702
+ const stateDir = getStateDir();
703
+ const cached = readCache(stateDir);
704
+ if (cached) {
705
+ if (cached.type === "UP_TO_DATE") {
706
+ return { type: "up_to_date" };
707
+ }
708
+ if (cached.type === "UPGRADE_AVAILABLE" && cached.current && cached.latest) {
709
+ if (cached.current === currentVersion && !isSnoozed(stateDir, cached.latest)) {
710
+ return { type: "upgrade_available", current: currentVersion, latest: cached.latest };
711
+ }
712
+ }
713
+ }
714
+ const latest = await fetchLatestVersion();
715
+ if (!latest) {
716
+ writeCache(stateDir, `UP_TO_DATE ${currentVersion}`);
717
+ return { type: "up_to_date" };
718
+ }
719
+ if (latest === currentVersion) {
720
+ writeCache(stateDir, `UP_TO_DATE ${currentVersion}`);
721
+ return { type: "up_to_date" };
722
+ }
723
+ writeCache(stateDir, `UPGRADE_AVAILABLE ${currentVersion} ${latest}`);
724
+ if (isSnoozed(stateDir, latest)) {
725
+ return { type: "up_to_date" };
726
+ }
727
+ return { type: "upgrade_available", current: currentVersion, latest };
728
+ }
729
+ function formatUpdateBanner(status) {
730
+ if (status.type !== "upgrade_available") return null;
731
+ return [
732
+ ``,
733
+ ` \u26A1 hebbian v${status.latest} available (current: v${status.current})`,
734
+ ` npm i -g hebbian@latest`,
735
+ ``
736
+ ].join("\n");
737
+ }
738
+ var PACKAGE_NAME, NPM_REGISTRY_URL, FETCH_TIMEOUT_MS, TTL_UP_TO_DATE, TTL_UPGRADE_AVAILABLE, SNOOZE_DURATIONS;
739
+ var init_update_check = __esm({
740
+ "src/update-check.ts"() {
741
+ "use strict";
742
+ PACKAGE_NAME = "hebbian";
743
+ NPM_REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
744
+ FETCH_TIMEOUT_MS = 5e3;
745
+ TTL_UP_TO_DATE = 60;
746
+ TTL_UPGRADE_AVAILABLE = 720;
747
+ SNOOZE_DURATIONS = {
748
+ 1: 86400,
749
+ // 24h
750
+ 2: 172800,
751
+ // 48h
752
+ 3: 604800
753
+ // 7d (and beyond)
754
+ };
755
+ }
756
+ });
757
+
610
758
  // src/fire.ts
611
759
  var fire_exports = {};
612
760
  __export(fire_exports, {
613
761
  fireNeuron: () => fireNeuron,
614
762
  getCurrentCounter: () => getCurrentCounter
615
763
  });
616
- import { readdirSync as readdirSync3, renameSync, writeFileSync as writeFileSync3, existsSync as existsSync5, mkdirSync as mkdirSync3 } from "fs";
617
- import { join as join4 } from "path";
764
+ import { readdirSync as readdirSync3, renameSync, writeFileSync as writeFileSync4, existsSync as existsSync6, mkdirSync as mkdirSync4 } from "fs";
765
+ import { join as join5 } from "path";
618
766
  function fireNeuron(brainRoot, neuronPath) {
619
- const fullPath = join4(brainRoot, neuronPath);
620
- if (!existsSync5(fullPath)) {
621
- mkdirSync3(fullPath, { recursive: true });
622
- writeFileSync3(join4(fullPath, "1.neuron"), "", "utf8");
767
+ const fullPath = join5(brainRoot, neuronPath);
768
+ if (!existsSync6(fullPath)) {
769
+ mkdirSync4(fullPath, { recursive: true });
770
+ writeFileSync4(join5(fullPath, "1.neuron"), "", "utf8");
623
771
  console.log(`\u{1F331} grew + fired: ${neuronPath} (1)`);
624
772
  return 1;
625
773
  }
626
774
  const current = getCurrentCounter(fullPath);
627
775
  const newCounter = current + 1;
628
776
  if (current > 0) {
629
- renameSync(join4(fullPath, `${current}.neuron`), join4(fullPath, `${newCounter}.neuron`));
777
+ renameSync(join5(fullPath, `${current}.neuron`), join5(fullPath, `${newCounter}.neuron`));
630
778
  } else {
631
- writeFileSync3(join4(fullPath, `${newCounter}.neuron`), "", "utf8");
779
+ writeFileSync4(join5(fullPath, `${newCounter}.neuron`), "", "utf8");
632
780
  }
633
781
  console.log(`\u{1F525} fired: ${neuronPath} (${current} \u2192 ${newCounter})`);
634
782
  return newCounter;
@@ -654,7 +802,7 @@ var init_fire = __esm({
654
802
 
655
803
  // src/similarity.ts
656
804
  function tokenize(name) {
657
- return name.replace(/([a-z])([A-Z])/g, "$1_$2").replace(/[_\-\s]+/g, " ").toLowerCase().split(" ").map(stem).filter((t) => t.length > 1);
805
+ return name.replace(/([a-z])([A-Z])/g, "$1_$2").replace(/[^a-zA-Z0-9\u3000-\u9FFF\uAC00-\uD7AF]+/g, " ").toLowerCase().split(" ").map(stem).filter((t) => t.length > 1);
658
806
  }
659
807
  function stem(word) {
660
808
  const suffixes = ["ing", "tion", "sion", "ness", "ment", "able", "ible", "ful", "less", "ous", "ive", "ity", "ies", "ed", "er", "es", "ly", "al", "en"];
@@ -688,11 +836,11 @@ var grow_exports = {};
688
836
  __export(grow_exports, {
689
837
  growNeuron: () => growNeuron
690
838
  });
691
- import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync4, existsSync as existsSync6, readdirSync as readdirSync4 } from "fs";
692
- import { join as join5, relative as relative2 } from "path";
839
+ import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync5, existsSync as existsSync7, readdirSync as readdirSync4 } from "fs";
840
+ import { join as join6, relative as relative2 } from "path";
693
841
  function growNeuron(brainRoot, neuronPath) {
694
- const fullPath = join5(brainRoot, neuronPath);
695
- if (existsSync6(fullPath)) {
842
+ const fullPath = join6(brainRoot, neuronPath);
843
+ if (existsSync7(fullPath)) {
696
844
  const counter = fireNeuron(brainRoot, neuronPath);
697
845
  return { action: "fired", path: neuronPath, counter };
698
846
  }
@@ -702,10 +850,12 @@ function growNeuron(brainRoot, neuronPath) {
702
850
  throw new Error(`Invalid region: ${regionName}. Valid: ${REGIONS.join(", ")}`);
703
851
  }
704
852
  const leafName = parts[parts.length - 1];
705
- const newTokens = tokenize(leafName);
706
- const regionPath = join5(brainRoot, regionName);
707
- if (existsSync6(regionPath)) {
708
- const match = findSimilar(regionPath, regionPath, newTokens);
853
+ const newPrefix = leafName.match(/^(NO|DO|MUST|WARN)_/)?.[1] || "";
854
+ const newStripped = leafName.replace(/^(NO|DO|MUST|WARN)_/, "");
855
+ const newTokens = tokenize(newStripped);
856
+ const regionPath = join6(brainRoot, regionName);
857
+ if (existsSync7(regionPath)) {
858
+ const match = findSimilar(regionPath, regionPath, newTokens, newPrefix);
709
859
  if (match) {
710
860
  const matchRelPath = regionName + "/" + relative2(regionPath, match);
711
861
  console.log(`\u{1F504} consolidation: "${neuronPath}" \u2248 "${matchRelPath}" (firing existing)`);
@@ -713,12 +863,12 @@ function growNeuron(brainRoot, neuronPath) {
713
863
  return { action: "fired", path: matchRelPath, counter };
714
864
  }
715
865
  }
716
- mkdirSync4(fullPath, { recursive: true });
717
- writeFileSync4(join5(fullPath, "1.neuron"), "", "utf8");
866
+ mkdirSync5(fullPath, { recursive: true });
867
+ writeFileSync5(join6(fullPath, "1.neuron"), "", "utf8");
718
868
  console.log(`\u{1F331} grew: ${neuronPath} (1)`);
719
869
  return { action: "grew", path: neuronPath, counter: 1 };
720
870
  }
721
- function findSimilar(dir, regionRoot, targetTokens) {
871
+ function findSimilar(dir, regionRoot, targetTokens, targetPrefix) {
722
872
  let entries;
723
873
  try {
724
874
  entries = readdirSync4(dir, { withFileTypes: true });
@@ -728,16 +878,19 @@ function findSimilar(dir, regionRoot, targetTokens) {
728
878
  const hasNeuron = entries.some((e) => e.isFile() && e.name.endsWith(".neuron"));
729
879
  if (hasNeuron) {
730
880
  const folderName = dir.split("/").pop() || "";
731
- const existingTokens = tokenize(folderName);
881
+ const existingPrefix = folderName.match(/^(NO|DO|MUST|WARN)_/)?.[1] || "";
882
+ const existingStripped = folderName.replace(/^(NO|DO|MUST|WARN)_/, "");
883
+ const existingTokens = tokenize(existingStripped);
732
884
  const similarity = jaccardSimilarity(targetTokens, existingTokens);
733
- if (similarity >= JACCARD_THRESHOLD) {
885
+ if (targetPrefix !== existingPrefix && targetTokens.length <= 2) {
886
+ } else if (similarity >= JACCARD_THRESHOLD) {
734
887
  return dir;
735
888
  }
736
889
  }
737
890
  for (const entry of entries) {
738
891
  if (entry.name.startsWith("_") || entry.name.startsWith(".")) continue;
739
892
  if (entry.isDirectory()) {
740
- const match = findSimilar(join5(dir, entry.name), regionRoot, targetTokens);
893
+ const match = findSimilar(join6(dir, entry.name), regionRoot, targetTokens, targetPrefix);
741
894
  if (match) return match;
742
895
  }
743
896
  }
@@ -758,9 +911,9 @@ __export(rollback_exports, {
758
911
  rollbackNeuron: () => rollbackNeuron
759
912
  });
760
913
  import { renameSync as renameSync2 } from "fs";
761
- import { join as join6 } from "path";
914
+ import { join as join7 } from "path";
762
915
  function rollbackNeuron(brainRoot, neuronPath) {
763
- const fullPath = join6(brainRoot, neuronPath);
916
+ const fullPath = join7(brainRoot, neuronPath);
764
917
  const current = getCurrentCounter(fullPath);
765
918
  if (current === 0) {
766
919
  throw new Error(`Neuron not found: ${neuronPath}`);
@@ -769,7 +922,7 @@ function rollbackNeuron(brainRoot, neuronPath) {
769
922
  throw new Error(`Counter already at minimum (1): ${neuronPath}`);
770
923
  }
771
924
  const newCounter = current - 1;
772
- renameSync2(join6(fullPath, `${current}.neuron`), join6(fullPath, `${newCounter}.neuron`));
925
+ renameSync2(join7(fullPath, `${current}.neuron`), join7(fullPath, `${newCounter}.neuron`));
773
926
  console.log(`\u23EA rollback: ${neuronPath} (${current} \u2192 ${newCounter})`);
774
927
  return newCounter;
775
928
  }
@@ -785,31 +938,31 @@ var signal_exports = {};
785
938
  __export(signal_exports, {
786
939
  signalNeuron: () => signalNeuron
787
940
  });
788
- import { writeFileSync as writeFileSync5, existsSync as existsSync7, readdirSync as readdirSync5 } from "fs";
789
- import { join as join7 } from "path";
941
+ import { writeFileSync as writeFileSync6, existsSync as existsSync8, readdirSync as readdirSync5 } from "fs";
942
+ import { join as join8 } from "path";
790
943
  function signalNeuron(brainRoot, neuronPath, signalType) {
791
944
  if (!SIGNAL_TYPES.includes(signalType)) {
792
945
  throw new Error(`Invalid signal type: ${signalType}. Valid: ${SIGNAL_TYPES.join(", ")}`);
793
946
  }
794
- const fullPath = join7(brainRoot, neuronPath);
795
- if (!existsSync7(fullPath)) {
947
+ const fullPath = join8(brainRoot, neuronPath);
948
+ if (!existsSync8(fullPath)) {
796
949
  throw new Error(`Neuron not found: ${neuronPath}`);
797
950
  }
798
951
  switch (signalType) {
799
952
  case "bomb": {
800
- writeFileSync5(join7(fullPath, "bomb.neuron"), "", "utf8");
953
+ writeFileSync6(join8(fullPath, "bomb.neuron"), "", "utf8");
801
954
  console.log(`\u{1F4A3} bomb planted: ${neuronPath}`);
802
955
  break;
803
956
  }
804
957
  case "dopamine": {
805
958
  const level = getNextSignalLevel(fullPath, "dopamine");
806
- writeFileSync5(join7(fullPath, `dopamine${level}.neuron`), "", "utf8");
959
+ writeFileSync6(join8(fullPath, `dopamine${level}.neuron`), "", "utf8");
807
960
  console.log(`\u{1F7E2} dopamine +${level}: ${neuronPath}`);
808
961
  break;
809
962
  }
810
963
  case "memory": {
811
964
  const level = getNextSignalLevel(fullPath, "memory");
812
- writeFileSync5(join7(fullPath, `memory${level}.neuron`), "", "utf8");
965
+ writeFileSync6(join8(fullPath, `memory${level}.neuron`), "", "utf8");
813
966
  console.log(`\u{1F4BE} memory +${level}: ${neuronPath}`);
814
967
  break;
815
968
  }
@@ -840,15 +993,15 @@ var decay_exports = {};
840
993
  __export(decay_exports, {
841
994
  runDecay: () => runDecay
842
995
  });
843
- import { readdirSync as readdirSync6, statSync as statSync2, writeFileSync as writeFileSync6, existsSync as existsSync8 } from "fs";
844
- import { join as join8 } from "path";
996
+ import { readdirSync as readdirSync6, statSync as statSync3, writeFileSync as writeFileSync7, existsSync as existsSync9 } from "fs";
997
+ import { join as join9 } from "path";
845
998
  function runDecay(brainRoot, days) {
846
999
  const threshold = Date.now() - days * 24 * 60 * 60 * 1e3;
847
1000
  let scanned = 0;
848
1001
  let decayed = 0;
849
1002
  for (const regionName of REGIONS) {
850
- const regionPath = join8(brainRoot, regionName);
851
- if (!existsSync8(regionPath)) continue;
1003
+ const regionPath = join9(brainRoot, regionName);
1004
+ if (!existsSync9(regionPath)) continue;
852
1005
  const result = decayWalk(regionPath, threshold, 0);
853
1006
  scanned += result.scanned;
854
1007
  decayed += result.decayed;
@@ -874,7 +1027,7 @@ function decayWalk(dir, threshold, depth) {
874
1027
  if (entry.name.endsWith(".neuron")) {
875
1028
  hasNeuronFile = true;
876
1029
  try {
877
- const st = statSync2(join8(dir, entry.name));
1030
+ const st = statSync3(join9(dir, entry.name));
878
1031
  if (st.mtimeMs > latestMod) latestMod = st.mtimeMs;
879
1032
  } catch {
880
1033
  }
@@ -888,8 +1041,8 @@ function decayWalk(dir, threshold, depth) {
888
1041
  scanned++;
889
1042
  if (!isDormant && latestMod < threshold) {
890
1043
  const age = Math.floor((Date.now() - latestMod) / (24 * 60 * 60 * 1e3));
891
- writeFileSync6(
892
- join8(dir, "decay.dormant"),
1044
+ writeFileSync7(
1045
+ join9(dir, "decay.dormant"),
893
1046
  `Dormant since ${(/* @__PURE__ */ new Date()).toISOString()} (${age} days inactive)`,
894
1047
  "utf8"
895
1048
  );
@@ -899,7 +1052,7 @@ function decayWalk(dir, threshold, depth) {
899
1052
  for (const entry of entries) {
900
1053
  if (entry.name.startsWith("_") || entry.name.startsWith(".")) continue;
901
1054
  if (entry.isDirectory()) {
902
- const sub = decayWalk(join8(dir, entry.name), threshold, depth + 1);
1055
+ const sub = decayWalk(join9(dir, entry.name), threshold, depth + 1);
903
1056
  scanned += sub.scanned;
904
1057
  decayed += sub.decayed;
905
1058
  }
@@ -918,8 +1071,8 @@ var dedup_exports = {};
918
1071
  __export(dedup_exports, {
919
1072
  runDedup: () => runDedup
920
1073
  });
921
- import { writeFileSync as writeFileSync7 } from "fs";
922
- import { join as join9 } from "path";
1074
+ import { writeFileSync as writeFileSync8 } from "fs";
1075
+ import { join as join10 } from "path";
923
1076
  function runDedup(brainRoot) {
924
1077
  const brain = scanBrain(brainRoot);
925
1078
  let scanned = 0;
@@ -941,8 +1094,8 @@ function runDedup(brainRoot) {
941
1094
  const [keep, drop] = ni.counter >= nj.counter ? [ni, nj] : [nj, ni];
942
1095
  const relKeep = `${region.name}/${keep.path}`;
943
1096
  fireNeuron(brainRoot, relKeep);
944
- writeFileSync7(
945
- join9(drop.fullPath, "dedup.dormant"),
1097
+ writeFileSync8(
1098
+ join10(drop.fullPath, "dedup.dormant"),
946
1099
  `Merged into ${keep.path} on ${(/* @__PURE__ */ new Date()).toISOString()}`,
947
1100
  "utf8"
948
1101
  );
@@ -972,10 +1125,10 @@ __export(snapshot_exports, {
972
1125
  gitSnapshot: () => gitSnapshot
973
1126
  });
974
1127
  import { execSync } from "child_process";
975
- import { existsSync as existsSync9 } from "fs";
976
- import { join as join10 } from "path";
1128
+ import { existsSync as existsSync10 } from "fs";
1129
+ import { join as join11 } from "path";
977
1130
  function gitSnapshot(brainRoot) {
978
- if (!existsSync9(join10(brainRoot, ".git"))) {
1131
+ if (!existsSync10(join11(brainRoot, ".git"))) {
979
1132
  try {
980
1133
  execSync("git rev-parse --is-inside-work-tree", { cwd: brainRoot, stdio: "pipe" });
981
1134
  } catch {
@@ -1064,12 +1217,12 @@ var init_watch = __esm({
1064
1217
  });
1065
1218
 
1066
1219
  // src/episode.ts
1067
- import { readdirSync as readdirSync7, readFileSync as readFileSync3, writeFileSync as writeFileSync8, mkdirSync as mkdirSync5, existsSync as existsSync10 } from "fs";
1068
- import { join as join11 } from "path";
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";
1069
1222
  function logEpisode(brainRoot, type, path, detail) {
1070
- const logDir = join11(brainRoot, SESSION_LOG_DIR);
1071
- if (!existsSync10(logDir)) {
1072
- mkdirSync5(logDir, { recursive: true });
1223
+ const logDir = join12(brainRoot, SESSION_LOG_DIR);
1224
+ if (!existsSync11(logDir)) {
1225
+ mkdirSync6(logDir, { recursive: true });
1073
1226
  }
1074
1227
  const nextSlot = getNextSlot(logDir);
1075
1228
  const episode = {
@@ -1078,12 +1231,35 @@ function logEpisode(brainRoot, type, path, detail) {
1078
1231
  path,
1079
1232
  detail
1080
1233
  };
1081
- writeFileSync8(
1082
- join11(logDir, `memory${nextSlot}.neuron`),
1234
+ writeFileSync9(
1235
+ join12(logDir, `memory${nextSlot}.neuron`),
1083
1236
  JSON.stringify(episode),
1084
1237
  "utf8"
1085
1238
  );
1086
1239
  }
1240
+ function readEpisodes(brainRoot) {
1241
+ const logDir = join12(brainRoot, SESSION_LOG_DIR);
1242
+ if (!existsSync11(logDir)) return [];
1243
+ const episodes = [];
1244
+ let entries;
1245
+ try {
1246
+ entries = readdirSync7(logDir);
1247
+ } catch {
1248
+ return [];
1249
+ }
1250
+ for (const entry of entries) {
1251
+ if (!entry.startsWith("memory") || !entry.endsWith(".neuron")) continue;
1252
+ try {
1253
+ const content = readFileSync4(join12(logDir, entry), "utf8");
1254
+ if (content.trim()) {
1255
+ episodes.push(JSON.parse(content));
1256
+ }
1257
+ } catch {
1258
+ }
1259
+ }
1260
+ episodes.sort((a, b) => a.ts.localeCompare(b.ts));
1261
+ return episodes;
1262
+ }
1087
1263
  function getNextSlot(logDir) {
1088
1264
  let maxSlot = 0;
1089
1265
  try {
@@ -1114,14 +1290,14 @@ __export(inbox_exports, {
1114
1290
  ensureInbox: () => ensureInbox,
1115
1291
  processInbox: () => processInbox
1116
1292
  });
1117
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync9, existsSync as existsSync11, mkdirSync as mkdirSync6 } from "fs";
1118
- import { join as join12 } from "path";
1293
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync10, existsSync as existsSync12, mkdirSync as mkdirSync7 } from "fs";
1294
+ import { join as join13 } from "path";
1119
1295
  function processInbox(brainRoot) {
1120
- const inboxPath = join12(brainRoot, INBOX_DIR, CORRECTIONS_FILE);
1121
- if (!existsSync11(inboxPath)) {
1296
+ const inboxPath = join13(brainRoot, INBOX_DIR, CORRECTIONS_FILE);
1297
+ if (!existsSync12(inboxPath)) {
1122
1298
  return { processed: 0, skipped: 0, errors: [] };
1123
1299
  }
1124
- const content = readFileSync4(inboxPath, "utf8").trim();
1300
+ const content = readFileSync5(inboxPath, "utf8").trim();
1125
1301
  if (!content) {
1126
1302
  return { processed: 0, skipped: 0, errors: [] };
1127
1303
  }
@@ -1162,7 +1338,7 @@ function processInbox(brainRoot) {
1162
1338
  skipped++;
1163
1339
  }
1164
1340
  }
1165
- writeFileSync9(inboxPath, "", "utf8");
1341
+ writeFileSync10(inboxPath, "", "utf8");
1166
1342
  console.log(`\u{1F4E5} inbox: processed ${processed}, skipped ${skipped}`);
1167
1343
  if (errors.length > 0) {
1168
1344
  for (const err of errors) {
@@ -1173,9 +1349,9 @@ function processInbox(brainRoot) {
1173
1349
  }
1174
1350
  function applyCorrection(brainRoot, correction) {
1175
1351
  const neuronPath = correction.path;
1176
- const fullPath = join12(brainRoot, neuronPath);
1352
+ const fullPath = join13(brainRoot, neuronPath);
1177
1353
  const counterAdd = Math.max(1, correction.counter_add || 1);
1178
- if (existsSync11(fullPath)) {
1354
+ if (existsSync12(fullPath)) {
1179
1355
  for (let i = 0; i < counterAdd; i++) {
1180
1356
  fireNeuron(brainRoot, neuronPath);
1181
1357
  }
@@ -1201,21 +1377,21 @@ function isPathSafe(path) {
1201
1377
  return true;
1202
1378
  }
1203
1379
  function ensureInbox(brainRoot) {
1204
- const inboxDir = join12(brainRoot, INBOX_DIR);
1205
- if (!existsSync11(inboxDir)) {
1206
- mkdirSync6(inboxDir, { recursive: true });
1380
+ const inboxDir = join13(brainRoot, INBOX_DIR);
1381
+ if (!existsSync12(inboxDir)) {
1382
+ mkdirSync7(inboxDir, { recursive: true });
1207
1383
  }
1208
- const filePath = join12(inboxDir, CORRECTIONS_FILE);
1209
- if (!existsSync11(filePath)) {
1210
- writeFileSync9(filePath, "", "utf8");
1384
+ const filePath = join13(inboxDir, CORRECTIONS_FILE);
1385
+ if (!existsSync12(filePath)) {
1386
+ writeFileSync10(filePath, "", "utf8");
1211
1387
  }
1212
1388
  return filePath;
1213
1389
  }
1214
1390
  function appendCorrection(brainRoot, correction) {
1215
1391
  const filePath = ensureInbox(brainRoot);
1216
1392
  const line = JSON.stringify(correction) + "\n";
1217
- const existing = readFileSync4(filePath, "utf8");
1218
- writeFileSync9(filePath, existing + line, "utf8");
1393
+ const existing = readFileSync5(filePath, "utf8");
1394
+ writeFileSync10(filePath, existing + line, "utf8");
1219
1395
  }
1220
1396
  var INBOX_DIR, CORRECTIONS_FILE, DOPAMINE_ALLOWED_ROLES;
1221
1397
  var init_inbox = __esm({
@@ -1519,22 +1695,28 @@ __export(hooks_exports, {
1519
1695
  installHooks: () => installHooks,
1520
1696
  uninstallHooks: () => uninstallHooks
1521
1697
  });
1522
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync10, existsSync as existsSync12, mkdirSync as mkdirSync7, readdirSync as readdirSync8 } from "fs";
1523
- import { join as join13, resolve as resolve2 } from "path";
1698
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync11, existsSync as existsSync13, mkdirSync as mkdirSync8, readdirSync as readdirSync8 } from "fs";
1699
+ import { execSync as execSync2 } from "child_process";
1700
+ import { join as join14, resolve as resolve2 } from "path";
1524
1701
  function installHooks(brainRoot, projectRoot) {
1525
1702
  const root = projectRoot || process.cwd();
1526
1703
  const resolvedBrain = resolve2(brainRoot);
1527
- if (!existsSync12(resolvedBrain) || !hasBrainRegions(resolvedBrain)) {
1704
+ if (!existsSync13(resolvedBrain) || !hasBrainRegions(resolvedBrain)) {
1528
1705
  initBrain(resolvedBrain);
1529
1706
  }
1530
- const settingsDir = join13(root, SETTINGS_DIR);
1531
- const settingsPath = join13(settingsDir, SETTINGS_FILE);
1707
+ const settingsDir = join14(root, SETTINGS_DIR);
1708
+ const settingsPath = join14(settingsDir, SETTINGS_FILE);
1532
1709
  const defaultBrain = resolve2(root, "brain");
1533
1710
  const brainFlag = resolvedBrain === defaultBrain ? "" : ` --brain ${resolvedBrain}`;
1711
+ let npxBin = "npx";
1712
+ try {
1713
+ npxBin = execSync2("which npx", { encoding: "utf8" }).trim();
1714
+ } catch {
1715
+ }
1534
1716
  let settings = {};
1535
- if (existsSync12(settingsPath)) {
1717
+ if (existsSync13(settingsPath)) {
1536
1718
  try {
1537
- settings = JSON.parse(readFileSync5(settingsPath, "utf8"));
1719
+ settings = JSON.parse(readFileSync6(settingsPath, "utf8"));
1538
1720
  } catch {
1539
1721
  console.log(`\u26A0\uFE0F settings.local.json was malformed, overwriting`);
1540
1722
  }
@@ -1549,7 +1731,7 @@ function installHooks(brainRoot, projectRoot) {
1549
1731
  matcher: "startup|resume",
1550
1732
  entry: {
1551
1733
  type: "command",
1552
- command: `hebbian emit claude${brainFlag}`,
1734
+ command: `${npxBin} hebbian emit claude${brainFlag}`,
1553
1735
  timeout: 10,
1554
1736
  statusMessage: `${HOOK_MARKER} refreshing brain`
1555
1737
  }
@@ -1558,7 +1740,7 @@ function installHooks(brainRoot, projectRoot) {
1558
1740
  event: "Stop",
1559
1741
  entry: {
1560
1742
  type: "command",
1561
- command: `hebbian digest${brainFlag}`,
1743
+ command: `${npxBin} hebbian digest${brainFlag}`,
1562
1744
  timeout: 30,
1563
1745
  statusMessage: `${HOOK_MARKER} digesting session`
1564
1746
  }
@@ -1581,24 +1763,24 @@ function installHooks(brainRoot, projectRoot) {
1581
1763
  hooks[event].push(group);
1582
1764
  }
1583
1765
  }
1584
- if (!existsSync12(settingsDir)) {
1585
- mkdirSync7(settingsDir, { recursive: true });
1766
+ if (!existsSync13(settingsDir)) {
1767
+ mkdirSync8(settingsDir, { recursive: true });
1586
1768
  }
1587
- writeFileSync10(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
1769
+ writeFileSync11(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
1588
1770
  console.log(`\u2705 hebbian hooks installed at ${settingsPath}`);
1589
- console.log(` SessionStart \u2192 hebbian emit claude${brainFlag}`);
1590
- console.log(` Stop \u2192 hebbian digest${brainFlag}`);
1771
+ console.log(` SessionStart \u2192 ${npxBin} hebbian emit claude${brainFlag}`);
1772
+ console.log(` Stop \u2192 ${npxBin} hebbian digest${brainFlag}`);
1591
1773
  }
1592
1774
  function uninstallHooks(projectRoot) {
1593
1775
  const root = projectRoot || process.cwd();
1594
- const settingsPath = join13(root, SETTINGS_DIR, SETTINGS_FILE);
1595
- if (!existsSync12(settingsPath)) {
1776
+ const settingsPath = join14(root, SETTINGS_DIR, SETTINGS_FILE);
1777
+ if (!existsSync13(settingsPath)) {
1596
1778
  console.log("No hooks installed (settings.local.json not found)");
1597
1779
  return;
1598
1780
  }
1599
1781
  let settings;
1600
1782
  try {
1601
- settings = JSON.parse(readFileSync5(settingsPath, "utf8"));
1783
+ settings = JSON.parse(readFileSync6(settingsPath, "utf8"));
1602
1784
  } catch {
1603
1785
  console.log("settings.local.json is malformed, nothing to uninstall");
1604
1786
  return;
@@ -1622,24 +1804,24 @@ function uninstallHooks(projectRoot) {
1622
1804
  if (Object.keys(hooks).length === 0) {
1623
1805
  delete settings.hooks;
1624
1806
  }
1625
- writeFileSync10(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
1807
+ writeFileSync11(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
1626
1808
  console.log(`\u2705 removed ${removed} hebbian hook(s) from ${settingsPath}`);
1627
1809
  }
1628
1810
  function checkHooks(projectRoot) {
1629
1811
  const root = projectRoot || process.cwd();
1630
- const settingsPath = join13(root, SETTINGS_DIR, SETTINGS_FILE);
1812
+ const settingsPath = join14(root, SETTINGS_DIR, SETTINGS_FILE);
1631
1813
  const status = {
1632
1814
  installed: false,
1633
1815
  path: settingsPath,
1634
1816
  events: []
1635
1817
  };
1636
- if (!existsSync12(settingsPath)) {
1818
+ if (!existsSync13(settingsPath)) {
1637
1819
  console.log(`\u274C hebbian hooks not installed (${settingsPath} not found)`);
1638
1820
  return status;
1639
1821
  }
1640
1822
  let settings;
1641
1823
  try {
1642
- settings = JSON.parse(readFileSync5(settingsPath, "utf8"));
1824
+ settings = JSON.parse(readFileSync6(settingsPath, "utf8"));
1643
1825
  } catch {
1644
1826
  console.log(`\u274C settings.local.json is malformed`);
1645
1827
  return status;
@@ -1669,7 +1851,7 @@ function checkHooks(projectRoot) {
1669
1851
  return status;
1670
1852
  }
1671
1853
  function hasBrainRegions(dir) {
1672
- if (!existsSync12(dir)) return false;
1854
+ if (!existsSync13(dir)) return false;
1673
1855
  try {
1674
1856
  const entries = readdirSync8(dir);
1675
1857
  return REGIONS.some((r) => entries.includes(r));
@@ -1695,8 +1877,8 @@ __export(digest_exports, {
1695
1877
  extractCorrections: () => extractCorrections,
1696
1878
  readHookInput: () => readHookInput
1697
1879
  });
1698
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync11, existsSync as existsSync13, mkdirSync as mkdirSync8 } from "fs";
1699
- import { join as join14, basename } from "path";
1880
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync12, existsSync as existsSync14, mkdirSync as mkdirSync9 } from "fs";
1881
+ import { join as join15, basename } from "path";
1700
1882
  function readHookInput(stdin) {
1701
1883
  if (!stdin.trim()) return null;
1702
1884
  try {
@@ -1711,13 +1893,13 @@ function readHookInput(stdin) {
1711
1893
  }
1712
1894
  }
1713
1895
  function digestTranscript(brainRoot, transcriptPath, sessionId) {
1714
- if (!existsSync13(transcriptPath)) {
1896
+ if (!existsSync14(transcriptPath)) {
1715
1897
  throw new Error(`Transcript not found: ${transcriptPath}`);
1716
1898
  }
1717
1899
  const resolvedSessionId = sessionId || basename(transcriptPath, ".jsonl");
1718
- const logDir = join14(brainRoot, DIGEST_LOG_DIR);
1719
- const logPath = join14(logDir, `${resolvedSessionId}.jsonl`);
1720
- if (existsSync13(logPath)) {
1900
+ const logDir = join15(brainRoot, DIGEST_LOG_DIR);
1901
+ const logPath = join15(logDir, `${resolvedSessionId}.jsonl`);
1902
+ if (existsSync14(logPath)) {
1721
1903
  console.log(`\u23ED already digested session ${resolvedSessionId}, skip`);
1722
1904
  return { corrections: 0, skipped: 0, transcriptPath, sessionId: resolvedSessionId };
1723
1905
  }
@@ -1751,7 +1933,7 @@ function digestTranscript(brainRoot, transcriptPath, sessionId) {
1751
1933
  };
1752
1934
  }
1753
1935
  function parseTranscript(transcriptPath) {
1754
- const content = readFileSync6(transcriptPath, "utf8");
1936
+ const content = readFileSync7(transcriptPath, "utf8");
1755
1937
  const lines = content.split("\n").filter(Boolean);
1756
1938
  const messages = [];
1757
1939
  for (const line of lines) {
@@ -1793,17 +1975,24 @@ function extractCorrections(messages) {
1793
1975
  }
1794
1976
  function detectCorrection(text) {
1795
1977
  const isNegation = NEGATION_PATTERNS.some((p) => p.test(text));
1978
+ const isMust = MUST_PATTERNS.some((p) => p.test(text));
1979
+ const isWarn = WARN_PATTERNS.some((p) => p.test(text));
1796
1980
  const isAffirmation = AFFIRMATION_PATTERNS.some((p) => p.test(text));
1797
- if (!isNegation && !isAffirmation) return null;
1798
- const prefix = isNegation ? "NO" : "DO";
1981
+ if (!isNegation && !isMust && !isWarn && !isAffirmation) return null;
1982
+ let prefix;
1983
+ if (isNegation) prefix = "NO";
1984
+ else if (isMust) prefix = "MUST";
1985
+ else if (isWarn) prefix = "WARN";
1986
+ else prefix = "DO";
1799
1987
  const keywords = extractKeywords(text);
1800
1988
  if (keywords.length === 0) return null;
1801
- const pathSegment = `${prefix}_${keywords.slice(0, 4).join("_")}`;
1989
+ const pathSegment = `${prefix}_${keywords.slice(0, 3).join("_")}`;
1802
1990
  const path = `cortex/${pathSegment}`;
1803
1991
  return { text, path, prefix, keywords };
1804
1992
  }
1805
1993
  function extractKeywords(text) {
1806
1994
  const STOP_WORDS = /* @__PURE__ */ new Set([
1995
+ // English stop words
1807
1996
  "the",
1808
1997
  "a",
1809
1998
  "an",
@@ -1885,7 +2074,6 @@ function extractKeywords(text) {
1885
2074
  "those",
1886
2075
  "it",
1887
2076
  "its",
1888
- "i",
1889
2077
  "me",
1890
2078
  "my",
1891
2079
  "we",
@@ -1914,17 +2102,40 @@ function extractKeywords(text) {
1914
2102
  "should",
1915
2103
  "like",
1916
2104
  "want",
1917
- "think"
2105
+ "think",
2106
+ "way",
2107
+ "make",
2108
+ "sure",
2109
+ "keep",
2110
+ "try",
2111
+ "let",
2112
+ "get",
2113
+ "put",
2114
+ "set",
2115
+ "new",
2116
+ "also",
2117
+ "using",
2118
+ "used",
2119
+ "when",
2120
+ "where",
2121
+ "how",
2122
+ "why",
2123
+ "here",
2124
+ "there",
2125
+ "careful",
2126
+ "warning",
2127
+ "watch",
2128
+ "out",
2129
+ "required"
1918
2130
  ]);
1919
- const tokens = tokenize(text);
1920
- return tokens.filter((t) => !STOP_WORDS.has(t) && t.length > 2);
2131
+ 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));
1921
2132
  }
1922
2133
  function writeAuditLog(brainRoot, sessionId, entries) {
1923
- const logDir = join14(brainRoot, DIGEST_LOG_DIR);
1924
- if (!existsSync13(logDir)) {
1925
- mkdirSync8(logDir, { recursive: true });
2134
+ const logDir = join15(brainRoot, DIGEST_LOG_DIR);
2135
+ if (!existsSync14(logDir)) {
2136
+ mkdirSync9(logDir, { recursive: true });
1926
2137
  }
1927
- const logPath = join14(logDir, `${sessionId}.jsonl`);
2138
+ const logPath = join15(logDir, `${sessionId}.jsonl`);
1928
2139
  const lines = entries.map(
1929
2140
  (e) => JSON.stringify({
1930
2141
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -1935,16 +2146,15 @@ function writeAuditLog(brainRoot, sessionId, entries) {
1935
2146
  applied: e.applied
1936
2147
  })
1937
2148
  );
1938
- writeFileSync11(logPath, lines.join("\n") + (lines.length > 0 ? "\n" : ""), "utf8");
2149
+ writeFileSync12(logPath, lines.join("\n") + (lines.length > 0 ? "\n" : ""), "utf8");
1939
2150
  }
1940
- var NEGATION_PATTERNS, AFFIRMATION_PATTERNS;
2151
+ var NEGATION_PATTERNS, AFFIRMATION_PATTERNS, MUST_PATTERNS, WARN_PATTERNS;
1941
2152
  var init_digest = __esm({
1942
2153
  "src/digest.ts"() {
1943
2154
  "use strict";
1944
2155
  init_constants();
1945
2156
  init_grow();
1946
2157
  init_episode();
1947
- init_similarity();
1948
2158
  NEGATION_PATTERNS = [
1949
2159
  /\bdon[''\u2019]?t\b/i,
1950
2160
  /\bdo not\b/i,
@@ -1963,13 +2173,281 @@ var init_digest = __esm({
1963
2173
  ];
1964
2174
  AFFIRMATION_PATTERNS = [
1965
2175
  /\balways\b/i,
1966
- /\bmust\b/i,
1967
2176
  /\bshould\s+always\b/i,
1968
2177
  /\buse\s+\w+\s+instead\b/i,
1969
2178
  // Korean affirmation
1970
- /항상/,
2179
+ /항상/
2180
+ ];
2181
+ MUST_PATTERNS = [
2182
+ /\bmust\b/i,
2183
+ /\brequired\b/i,
2184
+ // Korean
1971
2185
  /반드시/
1972
2186
  ];
2187
+ WARN_PATTERNS = [
2188
+ /\bcareful\b/i,
2189
+ /\bwatch\s+out\b/i,
2190
+ /\bwarning\b/i,
2191
+ // Korean
2192
+ /주의/
2193
+ ];
2194
+ }
2195
+ });
2196
+
2197
+ // src/evolve.ts
2198
+ var evolve_exports = {};
2199
+ __export(evolve_exports, {
2200
+ buildBrainSummary: () => buildBrainSummary,
2201
+ buildPrompt: () => buildPrompt,
2202
+ callGemini: () => callGemini,
2203
+ executeActions: () => executeActions,
2204
+ parseActions: () => parseActions,
2205
+ runEvolve: () => runEvolve,
2206
+ validateActions: () => validateActions
2207
+ });
2208
+ async function runEvolve(brainRoot, dryRun) {
2209
+ const apiKey = process.env.GEMINI_API_KEY;
2210
+ if (!apiKey) {
2211
+ console.error("\u274C GEMINI_API_KEY not set. Get one at https://aistudio.google.com/apikey");
2212
+ return { actions: [], executed: 0, skipped: 0, dryRun };
2213
+ }
2214
+ const episodes = readEpisodes(brainRoot);
2215
+ const brain = scanBrain(brainRoot);
2216
+ const summary = buildBrainSummary(brain);
2217
+ const prompt = buildPrompt(summary, episodes);
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) {
2266
+ const episodeLines = episodes.length > 0 ? episodes.map((e) => `- [${e.ts}] ${e.type}: ${e.path} \u2014 ${e.detail}`).join("\n") : "(no recent episodes)";
2267
+ return `You are the evolve engine for a hebbian brain \u2014 a filesystem-based memory system for AI agents.
2268
+
2269
+ ## Axioms
2270
+ - Folder = Neuron, File = Firing Trace, Counter = Activation strength
2271
+ - 7 regions in subsumption cascade: brainstem(P0) > limbic(P1) > hippocampus(P2) > sensors(P3) > cortex(P4) > ego(P5) > prefrontal(P6)
2272
+ - Lower priority ALWAYS overrides higher priority
2273
+ - PROTECTED regions (brainstem, limbic, sensors): NEVER propose mutations for these
2274
+
2275
+ ## Current Brain
2276
+ ${summary}
2277
+
2278
+ ## Recent Episodes (last ${episodes.length})
2279
+ ${episodeLines}
2280
+
2281
+ ## Available Actions
2282
+ - grow: Create a new neuron at the given path (region/name). Use for recurring patterns that deserve permanent memory.
2283
+ - fire: Increment an existing neuron's counter. Use for strengthening well-confirmed rules.
2284
+ - signal: Add dopamine (reward), bomb (block), or memory signal. Use sparingly.
2285
+ - prune: Decrement a neuron's counter. Use for rules that aren't working or cause issues.
2286
+ - decay: Mark inactive neurons as dormant. Use for stale rules with no recent activity.
2287
+
2288
+ ## Constraints
2289
+ - Max ${MAX_ACTIONS} actions per cycle
2290
+ - PREFER fire over grow \u2014 strengthen existing neurons before creating new ones
2291
+ - NEVER target brainstem, limbic, or sensors regions
2292
+ - Each action needs a "reason" explaining why
2293
+
2294
+ ## Task
2295
+ Analyze the brain state and recent episodes. Propose actions to improve the brain.
2296
+ Focus on: strengthening repeatedly-used rules, pruning ineffective ones, growing new neurons from repeated patterns.
2297
+
2298
+ Respond with a JSON array of actions:
2299
+ [{"type":"fire","path":"cortex/NO_console_log","reason":"fired 3 times in recent sessions"}]`;
2300
+ }
2301
+ async function callGemini(prompt, apiKey) {
2302
+ const model = process.env.EVOLVE_MODEL || DEFAULT_MODEL;
2303
+ const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`;
2304
+ const body = {
2305
+ contents: [{ parts: [{ text: prompt }] }],
2306
+ generationConfig: {
2307
+ responseMimeType: "application/json",
2308
+ temperature: 0.2
2309
+ }
2310
+ };
2311
+ let lastError = null;
2312
+ for (let attempt = 0; attempt < 2; attempt++) {
2313
+ if (attempt > 0) {
2314
+ await new Promise((r) => setTimeout(r, RETRY_DELAY));
2315
+ }
2316
+ try {
2317
+ const res = await fetch(url, {
2318
+ method: "POST",
2319
+ headers: { "Content-Type": "application/json" },
2320
+ body: JSON.stringify(body),
2321
+ signal: AbortSignal.timeout(API_TIMEOUT)
2322
+ });
2323
+ if (!res.ok) {
2324
+ lastError = new Error(`Gemini API ${res.status}: ${res.statusText}`);
2325
+ continue;
2326
+ }
2327
+ const data = await res.json();
2328
+ if (data.error) {
2329
+ lastError = new Error(`Gemini error: ${data.error.message || "unknown"}`);
2330
+ continue;
2331
+ }
2332
+ const text = data.candidates?.[0]?.content?.parts?.[0]?.text;
2333
+ if (!text) {
2334
+ lastError = new Error("Gemini returned empty response");
2335
+ continue;
2336
+ }
2337
+ return parseActions(text);
2338
+ } catch (err) {
2339
+ lastError = err;
2340
+ continue;
2341
+ }
2342
+ }
2343
+ throw lastError || new Error("Gemini call failed");
2344
+ }
2345
+ function parseActions(text) {
2346
+ let parsed;
2347
+ try {
2348
+ parsed = JSON.parse(text);
2349
+ } catch {
2350
+ throw new Error(`Failed to parse LLM response as JSON: ${text.slice(0, 100)}`);
2351
+ }
2352
+ if (!Array.isArray(parsed)) {
2353
+ throw new Error("LLM response is not an array");
2354
+ }
2355
+ const validTypes = /* @__PURE__ */ new Set(["grow", "fire", "signal", "prune", "decay"]);
2356
+ const actions = [];
2357
+ for (const item of parsed) {
2358
+ if (!item || typeof item !== "object") continue;
2359
+ const { type, path, reason, signal } = item;
2360
+ if (typeof type !== "string" || !validTypes.has(type)) continue;
2361
+ if (typeof path !== "string" || path.length === 0) continue;
2362
+ if (typeof reason !== "string") continue;
2363
+ const action = { type, path, reason };
2364
+ if (type === "signal" && typeof signal === "string") {
2365
+ action.signal = signal;
2366
+ }
2367
+ actions.push(action);
2368
+ }
2369
+ return actions;
2370
+ }
2371
+ function validateActions(actions, _brain) {
2372
+ return actions.filter((action) => {
2373
+ const region = action.path.split("/")[0];
2374
+ if (!region || PROTECTED_REGIONS.includes(region)) {
2375
+ console.log(` \u{1F6E1}\uFE0F blocked: ${action.type} ${action.path} (protected region)`);
2376
+ return false;
2377
+ }
2378
+ if (!REGIONS.includes(region)) {
2379
+ console.log(` \u26A0\uFE0F skipped: ${action.type} ${action.path} (invalid region)`);
2380
+ return false;
2381
+ }
2382
+ if (action.type === "signal" && action.signal && !["dopamine", "bomb", "memory"].includes(action.signal)) {
2383
+ console.log(` \u26A0\uFE0F skipped: signal ${action.path} (invalid signal type: ${action.signal})`);
2384
+ return false;
2385
+ }
2386
+ return true;
2387
+ }).slice(0, MAX_ACTIONS);
2388
+ }
2389
+ function executeActions(brainRoot, actions) {
2390
+ let executed = 0;
2391
+ for (const action of actions) {
2392
+ try {
2393
+ switch (action.type) {
2394
+ case "fire":
2395
+ fireNeuron(brainRoot, action.path);
2396
+ break;
2397
+ case "grow":
2398
+ growNeuron(brainRoot, action.path);
2399
+ break;
2400
+ case "signal":
2401
+ signalNeuron(brainRoot, action.path, action.signal || "dopamine");
2402
+ break;
2403
+ case "prune":
2404
+ rollbackNeuron(brainRoot, action.path);
2405
+ break;
2406
+ case "decay":
2407
+ runDecay(brainRoot, 0);
2408
+ break;
2409
+ }
2410
+ console.log(` ${actionIcon(action.type)} ${action.type} ${action.path}`);
2411
+ executed++;
2412
+ } catch (err) {
2413
+ console.log(` \u26A0\uFE0F failed: ${action.type} ${action.path} \u2014 ${err.message}`);
2414
+ }
2415
+ }
2416
+ return executed;
2417
+ }
2418
+ function actionIcon(type) {
2419
+ switch (type) {
2420
+ case "fire":
2421
+ return "\u{1F525}";
2422
+ case "grow":
2423
+ return "\u{1F331}";
2424
+ case "signal":
2425
+ return "\u26A1";
2426
+ case "prune":
2427
+ return "\u2702\uFE0F";
2428
+ case "decay":
2429
+ return "\u{1F4A4}";
2430
+ default:
2431
+ return "\u2753";
2432
+ }
2433
+ }
2434
+ var MAX_ACTIONS, PROTECTED_REGIONS, DEFAULT_MODEL, API_TIMEOUT, RETRY_DELAY;
2435
+ var init_evolve = __esm({
2436
+ "src/evolve.ts"() {
2437
+ "use strict";
2438
+ init_episode();
2439
+ init_scanner();
2440
+ init_constants();
2441
+ init_fire();
2442
+ init_grow();
2443
+ init_signal();
2444
+ init_rollback();
2445
+ init_decay();
2446
+ MAX_ACTIONS = 10;
2447
+ PROTECTED_REGIONS = ["brainstem", "limbic", "sensors"];
2448
+ DEFAULT_MODEL = "gemini-2.0-flash-lite";
2449
+ API_TIMEOUT = 3e4;
2450
+ RETRY_DELAY = 5e3;
1973
2451
  }
1974
2452
  });
1975
2453
 
@@ -2001,6 +2479,7 @@ COMMANDS:
2001
2479
  inbox Process corrections inbox
2002
2480
  claude install|uninstall|status Manage Claude Code hooks
2003
2481
  digest [--transcript <path>] Extract corrections from conversation
2482
+ evolve [--dry-run] LLM-powered brain evolution (Gemini)
2004
2483
  diag Print brain diagnostics
2005
2484
  stats Print brain statistics
2006
2485
 
@@ -2015,6 +2494,7 @@ EXAMPLES:
2015
2494
  hebbian fire cortex/frontend/NO_console_log --brain ./my-brain
2016
2495
  hebbian emit claude --brain ./my-brain
2017
2496
  hebbian emit all
2497
+ GEMINI_API_KEY=... hebbian evolve --dry-run
2018
2498
  `.trim();
2019
2499
  function readStdin() {
2020
2500
  return new Promise((resolve4) => {
@@ -2040,6 +2520,7 @@ async function main(argv) {
2040
2520
  days: { type: "string", short: "d" },
2041
2521
  port: { type: "string", short: "p" },
2042
2522
  transcript: { type: "string", short: "t" },
2523
+ "dry-run": { type: "boolean" },
2043
2524
  help: { type: "boolean", short: "h" },
2044
2525
  version: { type: "boolean", short: "v" }
2045
2526
  },
@@ -2075,6 +2556,12 @@ async function main(argv) {
2075
2556
  }
2076
2557
  const { emitToTarget: emitToTarget2 } = await Promise.resolve().then(() => (init_emit(), emit_exports));
2077
2558
  await emitToTarget2(brainRoot, target);
2559
+ const { checkForUpdates: checkForUpdates2, formatUpdateBanner: formatUpdateBanner2 } = await Promise.resolve().then(() => (init_update_check(), update_check_exports));
2560
+ checkForUpdates2(VERSION).then((status) => {
2561
+ const banner = formatUpdateBanner2(status);
2562
+ if (banner) console.error(banner);
2563
+ }).catch(() => {
2564
+ });
2078
2565
  break;
2079
2566
  }
2080
2567
  case "fire": {
@@ -2164,9 +2651,15 @@ async function main(argv) {
2164
2651
  case "uninstall":
2165
2652
  uninstallHooks2();
2166
2653
  break;
2167
- case "status":
2654
+ case "status": {
2168
2655
  checkHooks2();
2656
+ console.log(` version: v${VERSION}`);
2657
+ const { checkForUpdates: checkUpdates, formatUpdateBanner: formatBanner } = await Promise.resolve().then(() => (init_update_check(), update_check_exports));
2658
+ const updateStatus = await checkUpdates(VERSION);
2659
+ const updateBanner = formatBanner(updateStatus);
2660
+ if (updateBanner) console.error(updateBanner);
2169
2661
  break;
2662
+ }
2170
2663
  default:
2171
2664
  console.error("Usage: hebbian claude <install|uninstall|status>");
2172
2665
  process.exit(1);
@@ -2191,6 +2684,12 @@ async function main(argv) {
2191
2684
  }
2192
2685
  break;
2193
2686
  }
2687
+ case "evolve": {
2688
+ const dryRun = values["dry-run"] === true;
2689
+ const { runEvolve: runEvolve2 } = await Promise.resolve().then(() => (init_evolve(), evolve_exports));
2690
+ await runEvolve2(brainRoot, dryRun);
2691
+ break;
2692
+ }
2194
2693
  case "diag":
2195
2694
  case "stats": {
2196
2695
  const { scanBrain: scanBrain2 } = await Promise.resolve().then(() => (init_scanner(), scanner_exports));