hebbian 0.8.2 → 0.10.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,19 @@ 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
+ }
675
884
  lines.push(MARKER_END);
676
885
  return lines.join("\n");
677
886
  }
@@ -766,7 +975,7 @@ function emitRegionRules(region) {
766
975
  function emitToTarget(brainRoot, target) {
767
976
  const brain = scanBrain(brainRoot);
768
977
  const result = runSubsumption(brain);
769
- const content = emitBootstrap(result, brain);
978
+ const content = emitBootstrap(result, brain, brainRoot);
770
979
  if (target === "all") {
771
980
  for (const [name, filePath] of Object.entries(EMIT_TARGETS)) {
772
981
  writeTarget(filePath, content);
@@ -782,33 +991,33 @@ function emitToTarget(brainRoot, target) {
782
991
  }
783
992
  function writeAllTiers(brainRoot, result, brain) {
784
993
  const indexContent = emitIndex(result, brain);
785
- writeFileSync6(join9(brainRoot, "_index.md"), indexContent, "utf8");
994
+ writeFileSync7(join11(brainRoot, "_index.md"), indexContent, "utf8");
786
995
  for (const region of result.activeRegions) {
787
- if (existsSync8(region.path)) {
996
+ if (existsSync10(region.path)) {
788
997
  const rulesContent = emitRegionRules(region);
789
- writeFileSync6(join9(region.path, "_rules.md"), rulesContent, "utf8");
998
+ writeFileSync7(join11(region.path, "_rules.md"), rulesContent, "utf8");
790
999
  }
791
1000
  }
792
1001
  }
793
1002
  function writeTarget(filePath, content) {
794
- const dir = dirname(filePath);
795
- if (dir !== "." && !existsSync8(dir)) {
796
- mkdirSync3(dir, { recursive: true });
1003
+ const dir = dirname2(filePath);
1004
+ if (dir !== "." && !existsSync10(dir)) {
1005
+ mkdirSync5(dir, { recursive: true });
797
1006
  }
798
- if (existsSync8(filePath)) {
799
- const existing = readFileSync2(filePath, "utf8");
1007
+ if (existsSync10(filePath)) {
1008
+ const existing = readFileSync3(filePath, "utf8");
800
1009
  const startIdx = existing.indexOf(MARKER_START);
801
1010
  const endIdx = existing.indexOf(MARKER_END);
802
1011
  if (startIdx !== -1 && endIdx !== -1) {
803
1012
  const before = existing.slice(0, startIdx);
804
1013
  const after = existing.slice(endIdx + MARKER_END.length);
805
- writeFileSync6(filePath, before + content + after, "utf8");
1014
+ writeFileSync7(filePath, before + content + after, "utf8");
806
1015
  return;
807
1016
  }
808
- writeFileSync6(filePath, content + "\n\n" + existing, "utf8");
1017
+ writeFileSync7(filePath, content + "\n\n" + existing, "utf8");
809
1018
  return;
810
1019
  }
811
- writeFileSync6(filePath, content, "utf8");
1020
+ writeFileSync7(filePath, content, "utf8");
812
1021
  }
813
1022
  function printDiag(brain, result) {
814
1023
  console.log("");
@@ -897,8 +1106,8 @@ function computeHash(result) {
897
1106
  }
898
1107
 
899
1108
  // 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";
1109
+ import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync8, readFileSync as readFileSync4, existsSync as existsSync11, readdirSync as readdirSync8, appendFileSync } from "fs";
1110
+ import { join as join12, dirname as dirname3 } from "path";
902
1111
  var REGION_TEMPLATES = {
903
1112
  brainstem: {
904
1113
  description: "Absolute principles. Immutable. Read-only conscience.\nP0 \u2014 highest priority. bomb here halts EVERYTHING.",
@@ -930,22 +1139,22 @@ var REGION_TEMPLATES = {
930
1139
  }
931
1140
  };
932
1141
  function initBrain(brainPath) {
933
- if (existsSync9(brainPath)) {
934
- const entries = readdirSync6(brainPath);
1142
+ if (existsSync11(brainPath)) {
1143
+ const entries = readdirSync8(brainPath);
935
1144
  if (entries.some((e) => REGIONS.includes(e))) {
936
1145
  console.log(`\u26A0\uFE0F Brain already exists at ${brainPath}`);
937
1146
  return;
938
1147
  }
939
1148
  }
940
- mkdirSync4(brainPath, { recursive: true });
1149
+ mkdirSync6(brainPath, { recursive: true });
941
1150
  for (const regionName of REGIONS) {
942
- const regionDir = join10(brainPath, regionName);
943
- mkdirSync4(regionDir, { recursive: true });
1151
+ const regionDir = join12(brainPath, regionName);
1152
+ mkdirSync6(regionDir, { recursive: true });
944
1153
  const template = REGION_TEMPLATES[regionName];
945
1154
  const icon = REGION_ICONS[regionName];
946
1155
  const ko = REGION_KO[regionName];
947
- writeFileSync7(
948
- join10(regionDir, "_rules.md"),
1156
+ writeFileSync8(
1157
+ join12(regionDir, "_rules.md"),
949
1158
  `# ${icon} ${regionName} (${ko})
950
1159
 
951
1160
  ${template.description}
@@ -953,15 +1162,15 @@ ${template.description}
953
1162
  "utf8"
954
1163
  );
955
1164
  for (const starter of template.starters) {
956
- const neuronDir = join10(regionDir, starter);
957
- mkdirSync4(neuronDir, { recursive: true });
958
- writeFileSync7(join10(neuronDir, "1.neuron"), "", "utf8");
1165
+ const neuronDir = join12(regionDir, starter);
1166
+ mkdirSync6(neuronDir, { recursive: true });
1167
+ writeFileSync8(join12(neuronDir, "1.neuron"), "", "utf8");
959
1168
  }
960
1169
  }
961
- mkdirSync4(join10(brainPath, "_agents", "global_inbox"), { recursive: true });
962
- mkdirSync4(join10(brainPath, "skills"), { recursive: true });
963
- writeFileSync7(
964
- join10(brainPath, "skills", "_rules.md"),
1170
+ mkdirSync6(join12(brainPath, "_agents", "global_inbox"), { recursive: true });
1171
+ mkdirSync6(join12(brainPath, "skills"), { recursive: true });
1172
+ writeFileSync8(
1173
+ join12(brainPath, "skills", "_rules.md"),
965
1174
  "# Skills Library\n\nExecutable patterns learned through experience.\nNot part of the subsumption cascade \u2014 retrieval only.\n",
966
1175
  "utf8"
967
1176
  );
@@ -974,13 +1183,13 @@ ${template.description}
974
1183
  console.log(` hebbian emit claude --brain ${brainPath}`);
975
1184
  }
976
1185
  function autoGitignore(brainPath) {
977
- let dir = dirname2(brainPath);
1186
+ let dir = dirname3(brainPath);
978
1187
  for (let i = 0; i < 10; i++) {
979
- if (existsSync9(join10(dir, ".git"))) {
980
- const gitignorePath = join10(dir, ".gitignore");
1188
+ if (existsSync11(join12(dir, ".git"))) {
1189
+ const gitignorePath = join12(dir, ".gitignore");
981
1190
  const brainDirName = brainPath.replace(dir + "/", "") + "/";
982
- if (existsSync9(gitignorePath)) {
983
- const content = readFileSync3(gitignorePath, "utf8");
1191
+ if (existsSync11(gitignorePath)) {
1192
+ const content = readFileSync4(gitignorePath, "utf8");
984
1193
  if (content.includes(brainDirName) || content.includes(brainDirName.replace(/\/$/, ""))) {
985
1194
  return;
986
1195
  }
@@ -992,7 +1201,7 @@ ${brainDirName}
992
1201
  console.log(` \u{1F4DD} Added ${brainDirName} to .gitignore`);
993
1202
  return;
994
1203
  }
995
- const parent = dirname2(dir);
1204
+ const parent = dirname3(dir);
996
1205
  if (parent === dir) break;
997
1206
  dir = parent;
998
1207
  }
@@ -1004,202 +1213,6 @@ import { createServer } from "http";
1004
1213
  // src/inbox.ts
1005
1214
  import { readFileSync as readFileSync5, writeFileSync as writeFileSync9, existsSync as existsSync12, mkdirSync as mkdirSync7 } from "fs";
1006
1215
  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
1216
  var INBOX_DIR = "_inbox";
1204
1217
  var CORRECTIONS_FILE = "corrections.jsonl";
1205
1218
  var DOPAMINE_ALLOWED_ROLES = ["pm", "admin", "lead"];
@@ -1762,24 +1775,21 @@ var NEGATION_PATTERNS = [
1762
1775
  /\bnever\b/i,
1763
1776
  /\binstead\b/i,
1764
1777
  /^no[,.\s!]/i,
1765
- /\bdon[''\u2019]?t\s+use\b/i,
1766
1778
  /\bavoid\b/i,
1767
- // Korean negation — specific verb forms only to avoid matching incidental 않/대신 in explanations
1768
- /하지\s*마/,
1769
- /안\s*돼/,
1770
- /쓰지\s*마/,
1771
- /[가-힣]+지\s*마/,
1772
- /하지\s*않/,
1773
- // "do not" specifically
1774
- /쓰지\s*않/
1775
- // "do not use" specifically
1779
+ // Korean negation — require AI-directed imperative context:
1780
+ // "X하지 마" (don't X) — must have a verb object before 지 마
1781
+ /[을를은는도이가]\s*[가-힣]+지\s*마/,
1782
+ // "X 하면 안 돼" (must not X) — conditional + prohibition
1783
+ /하면\s*안\s*돼/,
1784
+ // "X 쓰지 마" (don't use X) — explicit "don't use"
1785
+ /쓰지\s*마/
1776
1786
  ];
1777
1787
  var AFFIRMATION_PATTERNS = [
1778
- /\balways\b/i,
1779
1788
  /\bshould\s+always\b/i,
1780
1789
  /\buse\s+\w+\s+instead\b/i,
1781
- // Korean affirmation
1782
- /항상/
1790
+ // Korean affirmation — require directive context
1791
+ /항상\s*[가-힣]+[해하]/
1792
+ // "항상 X해" (always do X)
1783
1793
  ];
1784
1794
  var MUST_PATTERNS = [
1785
1795
  /\bmust\b/i,
@@ -1841,7 +1851,8 @@ function digestTranscript(brainRoot, transcriptPath, sessionId) {
1841
1851
  console.log(`\u{1F527} digest: ${toolFailures.length} tool failure(s), ${retries.length} retry pattern(s) logged`);
1842
1852
  }
1843
1853
  const corrections = extractCorrections(messages);
1844
- if (corrections.length === 0 && toolFailures.length === 0) {
1854
+ const fired = autoFireCandidates(brainRoot, corrections);
1855
+ if (corrections.length === 0 && toolFailures.length === 0 && fired === 0) {
1845
1856
  console.log(`\u{1F4DD} digest: no corrections found in session ${resolvedSessionId}`);
1846
1857
  writeAuditLog(brainRoot, resolvedSessionId, [], totalLines);
1847
1858
  return { corrections: 0, skipped: messages.length, toolFailures: toolFailures.length, transcriptPath, sessionId: resolvedSessionId };
@@ -1998,12 +2009,15 @@ function extractCorrections(messages) {
1998
2009
  const corrections = [];
1999
2010
  for (const text of messages) {
2000
2011
  if (corrections.length >= MAX_CORRECTIONS_PER_SESSION) break;
2012
+ const trimmed = text.trim();
2001
2013
  if (text.length < MIN_CORRECTION_LENGTH) continue;
2002
- if (/^[\/!]/.test(text.trim())) continue;
2003
- if (text.trim().endsWith("?")) continue;
2004
- if (/^<[a-zA-Z]/.test(text.trim())) continue;
2005
- if (/^Base directory for this skill:/i.test(text.trim())) continue;
2006
- if (/^[•·▸▶\-\*]\s/.test(text.trim())) continue;
2014
+ if (/^[\/!]/.test(trimmed)) continue;
2015
+ if (/[??]\s*[!!]?\s*$/.test(trimmed)) continue;
2016
+ if (/^<[a-zA-Z]/.test(trimmed)) continue;
2017
+ if (/^Base directory for this skill:/i.test(trimmed)) continue;
2018
+ if (/^[•·▸▶\-\*]\s/.test(trimmed)) continue;
2019
+ if (/<[a-zA-Z][a-zA-Z-]*>/.test(trimmed) && /<\/[a-zA-Z]/.test(trimmed)) continue;
2020
+ if (isNarrativeKorean(trimmed)) continue;
2007
2021
  const correction = detectCorrection(text);
2008
2022
  if (correction) {
2009
2023
  corrections.push(correction);
@@ -2011,12 +2025,63 @@ function extractCorrections(messages) {
2011
2025
  }
2012
2026
  return corrections;
2013
2027
  }
2028
+ function isNarrativeKorean(text) {
2029
+ const NARRATIVE_MARKERS = [
2030
+ /이유는/,
2031
+ // "the reason is..."
2032
+ /예를\s*들면/,
2033
+ // "for example..."
2034
+ /그래서/,
2035
+ // "so/therefore..."
2036
+ /그런데/,
2037
+ // "but then..."
2038
+ /왜냐하면/,
2039
+ // "because..."
2040
+ /거든/,
2041
+ // "...you see" (explanatory)
2042
+ /있었[던거어]/,
2043
+ // "there was..." (past narrative)
2044
+ /중인데/,
2045
+ // "...in the middle of" (ongoing situation)
2046
+ /거 같은데/,
2047
+ // "it seems like..." (speculation)
2048
+ /어떻게\s*생각/
2049
+ // "what do you think?" (asking opinion)
2050
+ ];
2051
+ const markerCount = NARRATIVE_MARKERS.filter((p) => p.test(text)).length;
2052
+ return markerCount >= 2;
2053
+ }
2054
+ function autoFireCandidates(brainRoot, corrections) {
2055
+ if (corrections.length > 0) return 0;
2056
+ const candidates = listCandidates(brainRoot);
2057
+ if (candidates.length === 0) return 0;
2058
+ let fired = 0;
2059
+ for (const cand of candidates) {
2060
+ try {
2061
+ const newCounter = fireNeuron(brainRoot, cand.candidatePath);
2062
+ fired++;
2063
+ if (newCounter >= CANDIDATE_THRESHOLD) {
2064
+ growCandidate(brainRoot, cand.targetPath);
2065
+ }
2066
+ } catch {
2067
+ }
2068
+ }
2069
+ if (fired > 0) {
2070
+ console.log(`\u{1F504} agent-evaluator: ${fired} candidate(s) advanced (session without corrections)`);
2071
+ }
2072
+ return fired;
2073
+ }
2014
2074
  function detectCorrection(text) {
2015
2075
  const isNegation = NEGATION_PATTERNS.some((p) => p.test(text));
2016
2076
  const isMust = MUST_PATTERNS.some((p) => p.test(text));
2017
2077
  const isWarn = WARN_PATTERNS.some((p) => p.test(text));
2018
2078
  const isAffirmation = AFFIRMATION_PATTERNS.some((p) => p.test(text));
2019
2079
  if (!isNegation && !isMust && !isWarn && !isAffirmation) return null;
2080
+ const categories = [isNegation, isMust, isWarn, isAffirmation].filter(Boolean).length;
2081
+ const koreanRatio = (text.match(/[\uAC00-\uD7AF]/g) || []).length / Math.max(text.length, 1);
2082
+ if (koreanRatio > 0.3 && categories < 2) {
2083
+ if (text.length > 100) return null;
2084
+ }
2020
2085
  let prefix;
2021
2086
  if (isNegation) prefix = "NO";
2022
2087
  else if (isMust) prefix = "MUST";