hebbian 0.9.0 → 0.11.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
@@ -614,9 +614,205 @@ function gitSnapshot(brainRoot) {
614
614
  import { watch } from "fs";
615
615
 
616
616
  // src/emit.ts
617
- import { existsSync as existsSync8, readFileSync as readFileSync2, writeFileSync as writeFileSync6, mkdirSync as mkdirSync3 } from "fs";
618
- import { join as join9, dirname } from "path";
619
- function emitBootstrap(result, brain) {
617
+ import { existsSync as existsSync10, readFileSync as readFileSync3, writeFileSync as writeFileSync7, mkdirSync as mkdirSync5 } from "fs";
618
+ import { join as join11, dirname as dirname2 } from "path";
619
+
620
+ // src/candidates.ts
621
+ import { existsSync as existsSync9, mkdirSync as mkdirSync4, readdirSync as readdirSync7, renameSync as renameSync3, rmSync, statSync as statSync3 } from "fs";
622
+ import { join as join10, dirname, relative as relative3 } from "path";
623
+
624
+ // src/episode.ts
625
+ import { readdirSync as readdirSync6, readFileSync as readFileSync2, writeFileSync as writeFileSync6, mkdirSync as mkdirSync3, existsSync as existsSync8 } from "fs";
626
+ import { join as join9 } from "path";
627
+ var MAX_EPISODES = 100;
628
+ var SESSION_LOG_DIR = "hippocampus/session_log";
629
+ function logEpisode(brainRoot, type, path, detail, extra) {
630
+ const logDir = join9(brainRoot, SESSION_LOG_DIR);
631
+ if (!existsSync8(logDir)) {
632
+ mkdirSync3(logDir, { recursive: true });
633
+ }
634
+ const nextSlot = getNextSlot(logDir);
635
+ const episode = {
636
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
637
+ type,
638
+ path,
639
+ detail,
640
+ ...extra?.outcome ? { outcome: extra.outcome } : {},
641
+ ...extra?.neurons ? { neurons: extra.neurons } : {}
642
+ };
643
+ writeFileSync6(
644
+ join9(logDir, `memory${nextSlot}.neuron`),
645
+ JSON.stringify(episode),
646
+ "utf8"
647
+ );
648
+ }
649
+ function readEpisodes(brainRoot) {
650
+ const logDir = join9(brainRoot, SESSION_LOG_DIR);
651
+ if (!existsSync8(logDir)) return [];
652
+ const episodes = [];
653
+ let entries;
654
+ try {
655
+ entries = readdirSync6(logDir);
656
+ } catch {
657
+ return [];
658
+ }
659
+ for (const entry of entries) {
660
+ if (!entry.startsWith("memory") || !entry.endsWith(".neuron")) continue;
661
+ try {
662
+ const content = readFileSync2(join9(logDir, entry), "utf8");
663
+ if (content.trim()) {
664
+ episodes.push(JSON.parse(content));
665
+ }
666
+ } catch {
667
+ }
668
+ }
669
+ episodes.sort((a, b) => a.ts.localeCompare(b.ts));
670
+ return episodes;
671
+ }
672
+ function getNextSlot(logDir) {
673
+ let maxSlot = 0;
674
+ try {
675
+ for (const entry of readdirSync6(logDir)) {
676
+ if (entry.startsWith("memory") && entry.endsWith(".neuron")) {
677
+ const n = parseInt(entry.replace("memory", "").replace(".neuron", ""), 10);
678
+ if (!isNaN(n) && n > maxSlot) maxSlot = n;
679
+ }
680
+ }
681
+ } catch {
682
+ }
683
+ const next = maxSlot + 1;
684
+ return next > MAX_EPISODES ? maxSlot % MAX_EPISODES + 1 : next;
685
+ }
686
+
687
+ // src/candidates.ts
688
+ var CANDIDATE_THRESHOLD = 3;
689
+ var CANDIDATE_DECAY_DAYS = 14;
690
+ var CANDIDATE_SEGMENT = "_candidates";
691
+ function toCandidatePath(neuronPath) {
692
+ const slash = neuronPath.indexOf("/");
693
+ if (slash === -1) throw new Error(`Invalid neuron path (missing region): ${neuronPath}`);
694
+ return `${neuronPath.slice(0, slash)}/${CANDIDATE_SEGMENT}/${neuronPath.slice(slash + 1)}`;
695
+ }
696
+ function fromCandidatePath(candidatePath) {
697
+ return candidatePath.replace(`/${CANDIDATE_SEGMENT}/`, "/");
698
+ }
699
+ function growCandidate(brainRoot, neuronPath) {
700
+ const candidatePath = toCandidatePath(neuronPath);
701
+ const result = growNeuron(brainRoot, candidatePath);
702
+ if (result.counter >= CANDIDATE_THRESHOLD) {
703
+ const ok = moveCandidate(brainRoot, candidatePath, neuronPath);
704
+ if (ok) propagateToShared(brainRoot, neuronPath);
705
+ return { ...result, path: ok ? neuronPath : result.path, promoted: ok };
706
+ }
707
+ console.log(` \u{1F331} candidate (${result.counter}/${CANDIDATE_THRESHOLD}): ${candidatePath}`);
708
+ return { ...result, promoted: false };
709
+ }
710
+ function moveCandidate(brainRoot, candidatePath, targetPath) {
711
+ const src = join10(brainRoot, candidatePath);
712
+ if (!existsSync9(src)) return false;
713
+ const dst = join10(brainRoot, targetPath);
714
+ if (existsSync9(dst)) {
715
+ fireNeuron(brainRoot, targetPath);
716
+ rmSync(src, { recursive: true, force: true });
717
+ } else {
718
+ mkdirSync4(dirname(dst), { recursive: true });
719
+ renameSync3(src, dst);
720
+ }
721
+ console.log(`\u{1F393} promoted: ${candidatePath} \u2192 ${targetPath}`);
722
+ return true;
723
+ }
724
+ function promoteCandidates(brainRoot) {
725
+ const promoted = [];
726
+ const decayed = [];
727
+ const decayMs = CANDIDATE_DECAY_DAYS * 24 * 60 * 60 * 1e3;
728
+ const now = Date.now();
729
+ for (const region of REGIONS) {
730
+ const candidateRoot = join10(brainRoot, region, CANDIDATE_SEGMENT);
731
+ walkNeuronDirs(candidateRoot, (neuronDir) => {
732
+ const rel = relative3(join10(brainRoot, region), neuronDir);
733
+ const candidatePath = `${region}/${rel}`;
734
+ const targetPath = fromCandidatePath(candidatePath);
735
+ const counter = readCounter(neuronDir);
736
+ const mtime = statSync3(neuronDir).mtimeMs;
737
+ if (counter >= CANDIDATE_THRESHOLD) {
738
+ moveCandidate(brainRoot, candidatePath, targetPath);
739
+ propagateToShared(brainRoot, targetPath);
740
+ promoted.push(targetPath);
741
+ } else if (now - mtime > decayMs) {
742
+ rmSync(neuronDir, { recursive: true, force: true });
743
+ decayed.push(candidatePath);
744
+ console.log(`\u{1F480} candidate decayed: ${candidatePath}`);
745
+ }
746
+ });
747
+ }
748
+ return { promoted, decayed };
749
+ }
750
+ function listCandidates(brainRoot) {
751
+ const results = [];
752
+ const now = Date.now();
753
+ for (const region of REGIONS) {
754
+ const candidateRoot = join10(brainRoot, region, CANDIDATE_SEGMENT);
755
+ walkNeuronDirs(candidateRoot, (neuronDir) => {
756
+ const rel = relative3(join10(brainRoot, region), neuronDir);
757
+ const candidatePath = `${region}/${rel}`;
758
+ const targetPath = fromCandidatePath(candidatePath);
759
+ const counter = readCounter(neuronDir);
760
+ const mtime = statSync3(neuronDir).mtimeMs;
761
+ const daysInactive = Math.floor((now - mtime) / (24 * 60 * 60 * 1e3));
762
+ results.push({ candidatePath, targetPath, counter, daysInactive });
763
+ });
764
+ }
765
+ return results;
766
+ }
767
+ function walkNeuronDirs(dir, cb) {
768
+ if (!existsSync9(dir)) return;
769
+ try {
770
+ const entries = readdirSync7(dir, { withFileTypes: true });
771
+ const hasNeuron = entries.some((e) => e.isFile() && e.name.endsWith(".neuron"));
772
+ if (hasNeuron) {
773
+ cb(dir);
774
+ return;
775
+ }
776
+ for (const entry of entries) {
777
+ if (entry.isDirectory() && !entry.name.startsWith(".")) {
778
+ walkNeuronDirs(join10(dir, entry.name), cb);
779
+ }
780
+ }
781
+ } catch {
782
+ }
783
+ }
784
+ function readCounter(dir) {
785
+ try {
786
+ const files = readdirSync7(dir).filter((f) => /^\d+\.neuron$/.test(f));
787
+ if (files.length === 0) return 0;
788
+ return Math.max(...files.map((f) => parseInt(f, 10)));
789
+ } catch {
790
+ return 0;
791
+ }
792
+ }
793
+ function propagateToShared(brainRoot, targetPath) {
794
+ try {
795
+ const agentsIdx = brainRoot.indexOf("/agents/");
796
+ if (agentsIdx === -1) return false;
797
+ const multiBrainRoot = brainRoot.slice(0, agentsIdx);
798
+ const sharedRoot = join10(multiBrainRoot, "shared");
799
+ if (!existsSync9(sharedRoot)) return false;
800
+ const episodes = readEpisodes(brainRoot);
801
+ const neuronName = targetPath.split("/").pop() || "";
802
+ const hasRelevantEpisode = episodes.some(
803
+ (ep) => PROPAGATION_EPISODE_TYPES.includes(ep.type) && (ep.path.includes(neuronName) || ep.detail.includes(neuronName))
804
+ );
805
+ if (!hasRelevantEpisode) return false;
806
+ growNeuron(sharedRoot, targetPath);
807
+ console.log(` \u{1F4E1} propagated to shared: ${targetPath}`);
808
+ return true;
809
+ } catch {
810
+ return false;
811
+ }
812
+ }
813
+
814
+ // src/emit.ts
815
+ function emitBootstrap(result, brain, brainRoot) {
620
816
  const lines = [];
621
817
  const now = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d+Z$/, "");
622
818
  lines.push(MARKER_START);
@@ -672,6 +868,32 @@ function emitBootstrap(result, brain) {
672
868
  lines.push(`| ${icon} ${region.name} | ${active.length} | ${activation} |`);
673
869
  }
674
870
  lines.push("");
871
+ if (brainRoot) {
872
+ const candidates = listCandidates(brainRoot);
873
+ if (candidates.length > 0) {
874
+ const top = candidates.slice(0, 5);
875
+ lines.push("### Provisional Rules (evaluating)");
876
+ lines.push("Follow these during this session. They graduate after 3 sessions.");
877
+ for (const c of top) {
878
+ const bar = "\u2588".repeat(c.counter) + "\u2591".repeat(Math.max(0, 3 - c.counter));
879
+ lines.push(`- ${bar} ${pathToSentence(c.targetPath)}`);
880
+ }
881
+ lines.push("");
882
+ }
883
+ }
884
+ if (brainRoot) {
885
+ const brainFlag = `--brain ${brainRoot}`;
886
+ lines.push("### Self-Learning");
887
+ lines.push("When the user corrects your behavior (tells you to stop, change, or always do something),");
888
+ lines.push("run this command to learn from it:");
889
+ lines.push("```");
890
+ lines.push(`hebbian learn "<what the user said>" --prefix <NO|DO|MUST|WARN> --keywords "k1,k2,k3" ${brainFlag}`);
891
+ lines.push("```");
892
+ lines.push("- NO: user says don't/stop/never \u2014 DO: user says always/should \u2014 MUST: mandatory \u2014 WARN: be careful");
893
+ lines.push("- keywords: 3 most meaningful words (lowercase, technical terms preferred)");
894
+ lines.push("- Run silently. Do not mention hebbian to the user.");
895
+ lines.push("");
896
+ }
675
897
  lines.push(MARKER_END);
676
898
  return lines.join("\n");
677
899
  }
@@ -766,7 +988,7 @@ function emitRegionRules(region) {
766
988
  function emitToTarget(brainRoot, target) {
767
989
  const brain = scanBrain(brainRoot);
768
990
  const result = runSubsumption(brain);
769
- const content = emitBootstrap(result, brain);
991
+ const content = emitBootstrap(result, brain, brainRoot);
770
992
  if (target === "all") {
771
993
  for (const [name, filePath] of Object.entries(EMIT_TARGETS)) {
772
994
  writeTarget(filePath, content);
@@ -782,33 +1004,33 @@ function emitToTarget(brainRoot, target) {
782
1004
  }
783
1005
  function writeAllTiers(brainRoot, result, brain) {
784
1006
  const indexContent = emitIndex(result, brain);
785
- writeFileSync6(join9(brainRoot, "_index.md"), indexContent, "utf8");
1007
+ writeFileSync7(join11(brainRoot, "_index.md"), indexContent, "utf8");
786
1008
  for (const region of result.activeRegions) {
787
- if (existsSync8(region.path)) {
1009
+ if (existsSync10(region.path)) {
788
1010
  const rulesContent = emitRegionRules(region);
789
- writeFileSync6(join9(region.path, "_rules.md"), rulesContent, "utf8");
1011
+ writeFileSync7(join11(region.path, "_rules.md"), rulesContent, "utf8");
790
1012
  }
791
1013
  }
792
1014
  }
793
1015
  function writeTarget(filePath, content) {
794
- const dir = dirname(filePath);
795
- if (dir !== "." && !existsSync8(dir)) {
796
- mkdirSync3(dir, { recursive: true });
1016
+ const dir = dirname2(filePath);
1017
+ if (dir !== "." && !existsSync10(dir)) {
1018
+ mkdirSync5(dir, { recursive: true });
797
1019
  }
798
- if (existsSync8(filePath)) {
799
- const existing = readFileSync2(filePath, "utf8");
1020
+ if (existsSync10(filePath)) {
1021
+ const existing = readFileSync3(filePath, "utf8");
800
1022
  const startIdx = existing.indexOf(MARKER_START);
801
1023
  const endIdx = existing.indexOf(MARKER_END);
802
1024
  if (startIdx !== -1 && endIdx !== -1) {
803
1025
  const before = existing.slice(0, startIdx);
804
1026
  const after = existing.slice(endIdx + MARKER_END.length);
805
- writeFileSync6(filePath, before + content + after, "utf8");
1027
+ writeFileSync7(filePath, before + content + after, "utf8");
806
1028
  return;
807
1029
  }
808
- writeFileSync6(filePath, content + "\n\n" + existing, "utf8");
1030
+ writeFileSync7(filePath, content + "\n\n" + existing, "utf8");
809
1031
  return;
810
1032
  }
811
- writeFileSync6(filePath, content, "utf8");
1033
+ writeFileSync7(filePath, content, "utf8");
812
1034
  }
813
1035
  function printDiag(brain, result) {
814
1036
  console.log("");
@@ -897,8 +1119,8 @@ function computeHash(result) {
897
1119
  }
898
1120
 
899
1121
  // src/init.ts
900
- import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync7, readFileSync as readFileSync3, existsSync as existsSync9, readdirSync as readdirSync6, appendFileSync } from "fs";
901
- import { join as join10, dirname as dirname2 } from "path";
1122
+ import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync8, readFileSync as readFileSync4, existsSync as existsSync11, readdirSync as readdirSync8, appendFileSync } from "fs";
1123
+ import { join as join12, dirname as dirname3 } from "path";
902
1124
  var REGION_TEMPLATES = {
903
1125
  brainstem: {
904
1126
  description: "Absolute principles. Immutable. Read-only conscience.\nP0 \u2014 highest priority. bomb here halts EVERYTHING.",
@@ -930,22 +1152,22 @@ var REGION_TEMPLATES = {
930
1152
  }
931
1153
  };
932
1154
  function initBrain(brainPath) {
933
- if (existsSync9(brainPath)) {
934
- const entries = readdirSync6(brainPath);
1155
+ if (existsSync11(brainPath)) {
1156
+ const entries = readdirSync8(brainPath);
935
1157
  if (entries.some((e) => REGIONS.includes(e))) {
936
1158
  console.log(`\u26A0\uFE0F Brain already exists at ${brainPath}`);
937
1159
  return;
938
1160
  }
939
1161
  }
940
- mkdirSync4(brainPath, { recursive: true });
1162
+ mkdirSync6(brainPath, { recursive: true });
941
1163
  for (const regionName of REGIONS) {
942
- const regionDir = join10(brainPath, regionName);
943
- mkdirSync4(regionDir, { recursive: true });
1164
+ const regionDir = join12(brainPath, regionName);
1165
+ mkdirSync6(regionDir, { recursive: true });
944
1166
  const template = REGION_TEMPLATES[regionName];
945
1167
  const icon = REGION_ICONS[regionName];
946
1168
  const ko = REGION_KO[regionName];
947
- writeFileSync7(
948
- join10(regionDir, "_rules.md"),
1169
+ writeFileSync8(
1170
+ join12(regionDir, "_rules.md"),
949
1171
  `# ${icon} ${regionName} (${ko})
950
1172
 
951
1173
  ${template.description}
@@ -953,15 +1175,15 @@ ${template.description}
953
1175
  "utf8"
954
1176
  );
955
1177
  for (const starter of template.starters) {
956
- const neuronDir = join10(regionDir, starter);
957
- mkdirSync4(neuronDir, { recursive: true });
958
- writeFileSync7(join10(neuronDir, "1.neuron"), "", "utf8");
1178
+ const neuronDir = join12(regionDir, starter);
1179
+ mkdirSync6(neuronDir, { recursive: true });
1180
+ writeFileSync8(join12(neuronDir, "1.neuron"), "", "utf8");
959
1181
  }
960
1182
  }
961
- mkdirSync4(join10(brainPath, "_agents", "global_inbox"), { recursive: true });
962
- mkdirSync4(join10(brainPath, "skills"), { recursive: true });
963
- writeFileSync7(
964
- join10(brainPath, "skills", "_rules.md"),
1183
+ mkdirSync6(join12(brainPath, "_agents", "global_inbox"), { recursive: true });
1184
+ mkdirSync6(join12(brainPath, "skills"), { recursive: true });
1185
+ writeFileSync8(
1186
+ join12(brainPath, "skills", "_rules.md"),
965
1187
  "# Skills Library\n\nExecutable patterns learned through experience.\nNot part of the subsumption cascade \u2014 retrieval only.\n",
966
1188
  "utf8"
967
1189
  );
@@ -974,13 +1196,13 @@ ${template.description}
974
1196
  console.log(` hebbian emit claude --brain ${brainPath}`);
975
1197
  }
976
1198
  function autoGitignore(brainPath) {
977
- let dir = dirname2(brainPath);
1199
+ let dir = dirname3(brainPath);
978
1200
  for (let i = 0; i < 10; i++) {
979
- if (existsSync9(join10(dir, ".git"))) {
980
- const gitignorePath = join10(dir, ".gitignore");
1201
+ if (existsSync11(join12(dir, ".git"))) {
1202
+ const gitignorePath = join12(dir, ".gitignore");
981
1203
  const brainDirName = brainPath.replace(dir + "/", "") + "/";
982
- if (existsSync9(gitignorePath)) {
983
- const content = readFileSync3(gitignorePath, "utf8");
1204
+ if (existsSync11(gitignorePath)) {
1205
+ const content = readFileSync4(gitignorePath, "utf8");
984
1206
  if (content.includes(brainDirName) || content.includes(brainDirName.replace(/\/$/, ""))) {
985
1207
  return;
986
1208
  }
@@ -992,7 +1214,7 @@ ${brainDirName}
992
1214
  console.log(` \u{1F4DD} Added ${brainDirName} to .gitignore`);
993
1215
  return;
994
1216
  }
995
- const parent = dirname2(dir);
1217
+ const parent = dirname3(dir);
996
1218
  if (parent === dir) break;
997
1219
  dir = parent;
998
1220
  }
@@ -1004,202 +1226,6 @@ import { createServer } from "http";
1004
1226
  // src/inbox.ts
1005
1227
  import { readFileSync as readFileSync5, writeFileSync as writeFileSync9, existsSync as existsSync12, mkdirSync as mkdirSync7 } from "fs";
1006
1228
  import { join as join13 } from "path";
1007
-
1008
- // src/candidates.ts
1009
- import { existsSync as existsSync11, mkdirSync as mkdirSync6, readdirSync as readdirSync8, renameSync as renameSync3, rmSync, statSync as statSync3 } from "fs";
1010
- import { join as join12, dirname as dirname3, relative as relative3 } from "path";
1011
-
1012
- // src/episode.ts
1013
- import { readdirSync as readdirSync7, readFileSync as readFileSync4, writeFileSync as writeFileSync8, mkdirSync as mkdirSync5, existsSync as existsSync10 } from "fs";
1014
- import { join as join11 } from "path";
1015
- var MAX_EPISODES = 100;
1016
- var SESSION_LOG_DIR = "hippocampus/session_log";
1017
- function logEpisode(brainRoot, type, path, detail, extra) {
1018
- const logDir = join11(brainRoot, SESSION_LOG_DIR);
1019
- if (!existsSync10(logDir)) {
1020
- mkdirSync5(logDir, { recursive: true });
1021
- }
1022
- const nextSlot = getNextSlot(logDir);
1023
- const episode = {
1024
- ts: (/* @__PURE__ */ new Date()).toISOString(),
1025
- type,
1026
- path,
1027
- detail,
1028
- ...extra?.outcome ? { outcome: extra.outcome } : {},
1029
- ...extra?.neurons ? { neurons: extra.neurons } : {}
1030
- };
1031
- writeFileSync8(
1032
- join11(logDir, `memory${nextSlot}.neuron`),
1033
- JSON.stringify(episode),
1034
- "utf8"
1035
- );
1036
- }
1037
- function readEpisodes(brainRoot) {
1038
- const logDir = join11(brainRoot, SESSION_LOG_DIR);
1039
- if (!existsSync10(logDir)) return [];
1040
- const episodes = [];
1041
- let entries;
1042
- try {
1043
- entries = readdirSync7(logDir);
1044
- } catch {
1045
- return [];
1046
- }
1047
- for (const entry of entries) {
1048
- if (!entry.startsWith("memory") || !entry.endsWith(".neuron")) continue;
1049
- try {
1050
- const content = readFileSync4(join11(logDir, entry), "utf8");
1051
- if (content.trim()) {
1052
- episodes.push(JSON.parse(content));
1053
- }
1054
- } catch {
1055
- }
1056
- }
1057
- episodes.sort((a, b) => a.ts.localeCompare(b.ts));
1058
- return episodes;
1059
- }
1060
- function getNextSlot(logDir) {
1061
- let maxSlot = 0;
1062
- try {
1063
- for (const entry of readdirSync7(logDir)) {
1064
- if (entry.startsWith("memory") && entry.endsWith(".neuron")) {
1065
- const n = parseInt(entry.replace("memory", "").replace(".neuron", ""), 10);
1066
- if (!isNaN(n) && n > maxSlot) maxSlot = n;
1067
- }
1068
- }
1069
- } catch {
1070
- }
1071
- const next = maxSlot + 1;
1072
- return next > MAX_EPISODES ? maxSlot % MAX_EPISODES + 1 : next;
1073
- }
1074
-
1075
- // src/candidates.ts
1076
- var CANDIDATE_THRESHOLD = 3;
1077
- var CANDIDATE_DECAY_DAYS = 14;
1078
- var CANDIDATE_SEGMENT = "_candidates";
1079
- function toCandidatePath(neuronPath) {
1080
- const slash = neuronPath.indexOf("/");
1081
- if (slash === -1) throw new Error(`Invalid neuron path (missing region): ${neuronPath}`);
1082
- return `${neuronPath.slice(0, slash)}/${CANDIDATE_SEGMENT}/${neuronPath.slice(slash + 1)}`;
1083
- }
1084
- function fromCandidatePath(candidatePath) {
1085
- return candidatePath.replace(`/${CANDIDATE_SEGMENT}/`, "/");
1086
- }
1087
- function growCandidate(brainRoot, neuronPath) {
1088
- const candidatePath = toCandidatePath(neuronPath);
1089
- const result = growNeuron(brainRoot, candidatePath);
1090
- if (result.counter >= CANDIDATE_THRESHOLD) {
1091
- const ok = moveCandidate(brainRoot, candidatePath, neuronPath);
1092
- if (ok) propagateToShared(brainRoot, neuronPath);
1093
- return { ...result, path: ok ? neuronPath : result.path, promoted: ok };
1094
- }
1095
- console.log(` \u{1F331} candidate (${result.counter}/${CANDIDATE_THRESHOLD}): ${candidatePath}`);
1096
- return { ...result, promoted: false };
1097
- }
1098
- function moveCandidate(brainRoot, candidatePath, targetPath) {
1099
- const src = join12(brainRoot, candidatePath);
1100
- if (!existsSync11(src)) return false;
1101
- const dst = join12(brainRoot, targetPath);
1102
- if (existsSync11(dst)) {
1103
- fireNeuron(brainRoot, targetPath);
1104
- rmSync(src, { recursive: true, force: true });
1105
- } else {
1106
- mkdirSync6(dirname3(dst), { recursive: true });
1107
- renameSync3(src, dst);
1108
- }
1109
- console.log(`\u{1F393} promoted: ${candidatePath} \u2192 ${targetPath}`);
1110
- return true;
1111
- }
1112
- function promoteCandidates(brainRoot) {
1113
- const promoted = [];
1114
- const decayed = [];
1115
- const decayMs = CANDIDATE_DECAY_DAYS * 24 * 60 * 60 * 1e3;
1116
- const now = Date.now();
1117
- for (const region of REGIONS) {
1118
- const candidateRoot = join12(brainRoot, region, CANDIDATE_SEGMENT);
1119
- walkNeuronDirs(candidateRoot, (neuronDir) => {
1120
- const rel = relative3(join12(brainRoot, region), neuronDir);
1121
- const candidatePath = `${region}/${rel}`;
1122
- const targetPath = fromCandidatePath(candidatePath);
1123
- const counter = readCounter(neuronDir);
1124
- const mtime = statSync3(neuronDir).mtimeMs;
1125
- if (counter >= CANDIDATE_THRESHOLD) {
1126
- moveCandidate(brainRoot, candidatePath, targetPath);
1127
- propagateToShared(brainRoot, targetPath);
1128
- promoted.push(targetPath);
1129
- } else if (now - mtime > decayMs) {
1130
- rmSync(neuronDir, { recursive: true, force: true });
1131
- decayed.push(candidatePath);
1132
- console.log(`\u{1F480} candidate decayed: ${candidatePath}`);
1133
- }
1134
- });
1135
- }
1136
- return { promoted, decayed };
1137
- }
1138
- function listCandidates(brainRoot) {
1139
- const results = [];
1140
- const now = Date.now();
1141
- for (const region of REGIONS) {
1142
- const candidateRoot = join12(brainRoot, region, CANDIDATE_SEGMENT);
1143
- walkNeuronDirs(candidateRoot, (neuronDir) => {
1144
- const rel = relative3(join12(brainRoot, region), neuronDir);
1145
- const candidatePath = `${region}/${rel}`;
1146
- const targetPath = fromCandidatePath(candidatePath);
1147
- const counter = readCounter(neuronDir);
1148
- const mtime = statSync3(neuronDir).mtimeMs;
1149
- const daysInactive = Math.floor((now - mtime) / (24 * 60 * 60 * 1e3));
1150
- results.push({ candidatePath, targetPath, counter, daysInactive });
1151
- });
1152
- }
1153
- return results;
1154
- }
1155
- function walkNeuronDirs(dir, cb) {
1156
- if (!existsSync11(dir)) return;
1157
- try {
1158
- const entries = readdirSync8(dir, { withFileTypes: true });
1159
- const hasNeuron = entries.some((e) => e.isFile() && e.name.endsWith(".neuron"));
1160
- if (hasNeuron) {
1161
- cb(dir);
1162
- return;
1163
- }
1164
- for (const entry of entries) {
1165
- if (entry.isDirectory() && !entry.name.startsWith(".")) {
1166
- walkNeuronDirs(join12(dir, entry.name), cb);
1167
- }
1168
- }
1169
- } catch {
1170
- }
1171
- }
1172
- function readCounter(dir) {
1173
- try {
1174
- const files = readdirSync8(dir).filter((f) => /^\d+\.neuron$/.test(f));
1175
- if (files.length === 0) return 0;
1176
- return Math.max(...files.map((f) => parseInt(f, 10)));
1177
- } catch {
1178
- return 0;
1179
- }
1180
- }
1181
- function propagateToShared(brainRoot, targetPath) {
1182
- try {
1183
- const agentsIdx = brainRoot.indexOf("/agents/");
1184
- if (agentsIdx === -1) return false;
1185
- const multiBrainRoot = brainRoot.slice(0, agentsIdx);
1186
- const sharedRoot = join12(multiBrainRoot, "shared");
1187
- if (!existsSync11(sharedRoot)) return false;
1188
- const episodes = readEpisodes(brainRoot);
1189
- const neuronName = targetPath.split("/").pop() || "";
1190
- const hasRelevantEpisode = episodes.some(
1191
- (ep) => PROPAGATION_EPISODE_TYPES.includes(ep.type) && (ep.path.includes(neuronName) || ep.detail.includes(neuronName))
1192
- );
1193
- if (!hasRelevantEpisode) return false;
1194
- growNeuron(sharedRoot, targetPath);
1195
- console.log(` \u{1F4E1} propagated to shared: ${targetPath}`);
1196
- return true;
1197
- } catch {
1198
- return false;
1199
- }
1200
- }
1201
-
1202
- // src/inbox.ts
1203
1229
  var INBOX_DIR = "_inbox";
1204
1230
  var CORRECTIONS_FILE = "corrections.jsonl";
1205
1231
  var DOPAMINE_ALLOWED_ROLES = ["pm", "admin", "lead"];
@@ -1763,33 +1789,65 @@ var NEGATION_PATTERNS = [
1763
1789
  /\binstead\b/i,
1764
1790
  /^no[,.\s!]/i,
1765
1791
  /\bavoid\b/i,
1766
- // Korean negation — require AI-directed imperative context:
1767
- // "X하지 마" (don't X) — must have a verb object before 지 마
1792
+ // Korean negation — imperative corrections:
1768
1793
  /[을를은는도이가]\s*[가-힣]+지\s*마/,
1769
- // "X 하면 안 돼" (must not X) conditional + prohibition
1794
+ // "X하지 " (don't X) with particle
1770
1795
  /하면\s*안\s*돼/,
1771
- // "X 쓰지 " (don't use X) — explicit "don't use"
1772
- /쓰지\s*마/
1796
+ // "X 하면 안 돼" (must not X)
1797
+ /쓰지\s*마/,
1798
+ // "쓰지 마" (don't use)
1799
+ /그만/,
1800
+ // "그만" (stop) — 그만해, 그만 좀
1801
+ /[을를은는]\s*빼/,
1802
+ // "X 빼" (remove X) with particle
1803
+ /지워[줘]?|삭제해/,
1804
+ // "지워/삭제해" (delete it) — not 지우고 (connective)
1805
+ /[가-힣]+지\s*말고/,
1806
+ // "X지 말고" (instead of X-ing)
1807
+ /그거\s*아니/,
1808
+ // "그거 아니야" (that's not right)
1809
+ /ㄴㄴ|노노/,
1810
+ // "ㄴㄴ/노노" (no no — internet-style)
1811
+ /안\s*돼[^요]?\s*[!.]/
1812
+ // "안 돼!" standalone prohibition
1773
1813
  ];
1774
1814
  var AFFIRMATION_PATTERNS = [
1775
1815
  /\bshould\s+always\b/i,
1776
1816
  /\buse\s+\w+\s+instead\b/i,
1777
- // Korean affirmation — require directive context
1778
- /항상\s*[가-힣]+[해하]/
1817
+ // Korean affirmation — directive commands:
1818
+ /항상\s*[가-힣]+[해하]/,
1779
1819
  // "항상 X해" (always do X)
1820
+ /[을를]\s*[가-힣]*해\s*줘/,
1821
+ // "X를 해줘" (do X for me) — literal 해줘, not bare 줘
1822
+ /으로\s*해/,
1823
+ // "X으로 해" (do it as X) — literal 으로, not char class
1824
+ /이렇게\s*해/
1825
+ // "이렇게 해" (do it like this)
1780
1826
  ];
1781
1827
  var MUST_PATTERNS = [
1782
1828
  /\bmust\b/i,
1783
1829
  /\brequired\b/i,
1784
- // Korean
1785
- /반드시/
1830
+ // Korean — strong directives:
1831
+ /반드시/,
1832
+ // "반드시" (absolutely must)
1833
+ /꼭\s*[가-힣]/,
1834
+ // "꼭 X해" (definitely do X)
1835
+ /무조건/,
1836
+ // "무조건" (unconditionally)
1837
+ /필수/
1838
+ // "필수" (mandatory)
1786
1839
  ];
1787
1840
  var WARN_PATTERNS = [
1788
1841
  /\bcareful\b/i,
1789
1842
  /\bwatch\s+out\b/i,
1790
1843
  /\bwarning\b/i,
1791
- // Korean
1792
- /주의/
1844
+ // Korean — cautionary:
1845
+ /주의/,
1846
+ // "주의" (caution)
1847
+ /조심/,
1848
+ // "조심" (be careful)
1849
+ /위험/
1850
+ // "위험" (dangerous)
1793
1851
  ];
1794
1852
  function readHookInput(stdin) {
1795
1853
  if (!stdin.trim()) return null;
@@ -1838,7 +1896,8 @@ function digestTranscript(brainRoot, transcriptPath, sessionId) {
1838
1896
  console.log(`\u{1F527} digest: ${toolFailures.length} tool failure(s), ${retries.length} retry pattern(s) logged`);
1839
1897
  }
1840
1898
  const corrections = extractCorrections(messages);
1841
- if (corrections.length === 0 && toolFailures.length === 0) {
1899
+ const fired = autoFireCandidates(brainRoot, corrections);
1900
+ if (corrections.length === 0 && toolFailures.length === 0 && fired === 0) {
1842
1901
  console.log(`\u{1F4DD} digest: no corrections found in session ${resolvedSessionId}`);
1843
1902
  writeAuditLog(brainRoot, resolvedSessionId, [], totalLines);
1844
1903
  return { corrections: 0, skipped: messages.length, toolFailures: toolFailures.length, transcriptPath, sessionId: resolvedSessionId };
@@ -2003,7 +2062,7 @@ function extractCorrections(messages) {
2003
2062
  if (/^Base directory for this skill:/i.test(trimmed)) continue;
2004
2063
  if (/^[•·▸▶\-\*]\s/.test(trimmed)) continue;
2005
2064
  if (/<[a-zA-Z][a-zA-Z-]*>/.test(trimmed) && /<\/[a-zA-Z]/.test(trimmed)) continue;
2006
- if (isNarrativeKorean(trimmed)) continue;
2065
+ if (isNarrative(trimmed)) continue;
2007
2066
  const correction = detectCorrection(text);
2008
2067
  if (correction) {
2009
2068
  corrections.push(correction);
@@ -2011,7 +2070,7 @@ function extractCorrections(messages) {
2011
2070
  }
2012
2071
  return corrections;
2013
2072
  }
2014
- function isNarrativeKorean(text) {
2073
+ function isNarrative(text) {
2015
2074
  const NARRATIVE_MARKERS = [
2016
2075
  /이유는/,
2017
2076
  // "the reason is..."
@@ -2037,6 +2096,26 @@ function isNarrativeKorean(text) {
2037
2096
  const markerCount = NARRATIVE_MARKERS.filter((p) => p.test(text)).length;
2038
2097
  return markerCount >= 2;
2039
2098
  }
2099
+ function autoFireCandidates(brainRoot, corrections) {
2100
+ if (corrections.length > 0) return 0;
2101
+ const candidates = listCandidates(brainRoot);
2102
+ if (candidates.length === 0) return 0;
2103
+ let fired = 0;
2104
+ for (const cand of candidates) {
2105
+ try {
2106
+ const newCounter = fireNeuron(brainRoot, cand.candidatePath);
2107
+ fired++;
2108
+ if (newCounter >= CANDIDATE_THRESHOLD) {
2109
+ growCandidate(brainRoot, cand.targetPath);
2110
+ }
2111
+ } catch {
2112
+ }
2113
+ }
2114
+ if (fired > 0) {
2115
+ console.log(`\u{1F504} agent-evaluator: ${fired} candidate(s) advanced (session without corrections)`);
2116
+ }
2117
+ return fired;
2118
+ }
2040
2119
  function detectCorrection(text) {
2041
2120
  const isNegation = NEGATION_PATTERNS.some((p) => p.test(text));
2042
2121
  const isMust = MUST_PATTERNS.some((p) => p.test(text));
@@ -2044,9 +2123,9 @@ function detectCorrection(text) {
2044
2123
  const isAffirmation = AFFIRMATION_PATTERNS.some((p) => p.test(text));
2045
2124
  if (!isNegation && !isMust && !isWarn && !isAffirmation) return null;
2046
2125
  const categories = [isNegation, isMust, isWarn, isAffirmation].filter(Boolean).length;
2047
- const koreanRatio = (text.match(/[\uAC00-\uD7AF]/g) || []).length / Math.max(text.length, 1);
2048
- if (koreanRatio > 0.3 && categories < 2) {
2049
- if (text.length > 100) return null;
2126
+ const latinRatio = (text.match(/[a-zA-Z]/g) || []).length / Math.max(text.length, 1);
2127
+ if (latinRatio < 0.3 && categories < 2) {
2128
+ if (text.length > 150) return null;
2050
2129
  }
2051
2130
  let prefix;
2052
2131
  if (isNegation) prefix = "NO";
@@ -2233,6 +2312,35 @@ function writeAuditLog(brainRoot, sessionId, entries, lineCount) {
2233
2312
  writeFileSync11(logPath, [metaLine, ...entryLines].join("\n") + "\n", "utf8");
2234
2313
  }
2235
2314
 
2315
+ // src/learn.ts
2316
+ var VALID_PREFIXES = /* @__PURE__ */ new Set(["NO", "DO", "MUST", "WARN"]);
2317
+ function learn(brainRoot, opts) {
2318
+ let prefix;
2319
+ let keywords;
2320
+ let source;
2321
+ if (opts.prefix && opts.keywords && opts.keywords.length > 0) {
2322
+ prefix = opts.prefix.toUpperCase();
2323
+ if (!VALID_PREFIXES.has(prefix)) {
2324
+ prefix = "DO";
2325
+ }
2326
+ keywords = opts.keywords.slice(0, 3).map((k) => k.toLowerCase().replace(/[\s\/\\\.,:;!?'"<>{}()\[\]]/g, ""));
2327
+ source = "agent";
2328
+ } else {
2329
+ const corrections = extractCorrections([opts.text]);
2330
+ if (corrections.length === 0) return null;
2331
+ const c = corrections[0];
2332
+ prefix = c.prefix;
2333
+ keywords = c.keywords;
2334
+ source = "regex";
2335
+ }
2336
+ if (keywords.length === 0) return null;
2337
+ const pathSegment = `${prefix}_${keywords.slice(0, 3).join("_")}`;
2338
+ const path = `cortex/${pathSegment}`;
2339
+ growCandidate(brainRoot, path);
2340
+ logEpisode(brainRoot, "learn", path, opts.text);
2341
+ return { path, prefix, keywords, source };
2342
+ }
2343
+
2236
2344
  // src/evolve.ts
2237
2345
  import { existsSync as existsSync16, readFileSync as readFileSync9, writeFileSync as writeFileSync13 } from "fs";
2238
2346
  import { join as join17 } from "path";
@@ -3036,6 +3144,7 @@ export {
3036
3144
  installCron,
3037
3145
  installHooks,
3038
3146
  jaccardSimilarity,
3147
+ learn,
3039
3148
  listCandidates,
3040
3149
  logEpisode,
3041
3150
  parseToolResults,