hebbian 0.9.0 → 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.
@@ -435,6 +435,412 @@ var init_subsumption = __esm({
435
435
  }
436
436
  });
437
437
 
438
+ // src/similarity.ts
439
+ function tokenize(name) {
440
+ 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);
441
+ }
442
+ function stem(word) {
443
+ const suffixes = ["ing", "tion", "sion", "ness", "ment", "able", "ible", "ful", "less", "ous", "ive", "ity", "ies", "ed", "er", "es", "ly", "al", "en"];
444
+ for (const suffix of suffixes) {
445
+ if (word.length > suffix.length + 2 && word.endsWith(suffix)) {
446
+ return word.slice(0, -suffix.length);
447
+ }
448
+ }
449
+ return word;
450
+ }
451
+ function jaccardSimilarity(a, b) {
452
+ if (a.length === 0 && b.length === 0) return 1;
453
+ if (a.length === 0 || b.length === 0) return 0;
454
+ const setA = new Set(a);
455
+ const setB = new Set(b);
456
+ let intersection = 0;
457
+ for (const item of setA) {
458
+ if (setB.has(item)) intersection++;
459
+ }
460
+ const union = (/* @__PURE__ */ new Set([...setA, ...setB])).size;
461
+ return intersection / union;
462
+ }
463
+ var init_similarity = __esm({
464
+ "src/similarity.ts"() {
465
+ "use strict";
466
+ }
467
+ });
468
+
469
+ // src/fire.ts
470
+ var fire_exports = {};
471
+ __export(fire_exports, {
472
+ contraNeuron: () => contraNeuron,
473
+ fireNeuron: () => fireNeuron,
474
+ getCurrentContra: () => getCurrentContra,
475
+ getCurrentCounter: () => getCurrentCounter
476
+ });
477
+ import { readdirSync as readdirSync3, renameSync, writeFileSync as writeFileSync2, existsSync as existsSync4, mkdirSync as mkdirSync2 } from "fs";
478
+ import { join as join3 } from "path";
479
+ function fireNeuron(brainRoot, neuronPath) {
480
+ const fullPath = join3(brainRoot, neuronPath);
481
+ if (!existsSync4(fullPath)) {
482
+ mkdirSync2(fullPath, { recursive: true });
483
+ writeFileSync2(join3(fullPath, "1.neuron"), "", "utf8");
484
+ console.log(`\u{1F331} grew + fired: ${neuronPath} (1)`);
485
+ return 1;
486
+ }
487
+ const current = getCurrentCounter(fullPath);
488
+ const newCounter = current + 1;
489
+ if (current > 0) {
490
+ renameSync(join3(fullPath, `${current}.neuron`), join3(fullPath, `${newCounter}.neuron`));
491
+ } else {
492
+ writeFileSync2(join3(fullPath, `${newCounter}.neuron`), "", "utf8");
493
+ }
494
+ console.log(`\u{1F525} fired: ${neuronPath} (${current} \u2192 ${newCounter})`);
495
+ return newCounter;
496
+ }
497
+ function contraNeuron(brainRoot, neuronPath) {
498
+ const fullPath = join3(brainRoot, neuronPath);
499
+ if (!existsSync4(fullPath)) {
500
+ return 0;
501
+ }
502
+ const current = getCurrentContra(fullPath);
503
+ const newContra = current + 1;
504
+ if (current > 0) {
505
+ renameSync(join3(fullPath, `${current}.contra`), join3(fullPath, `${newContra}.contra`));
506
+ } else {
507
+ writeFileSync2(join3(fullPath, `${newContra}.contra`), "", "utf8");
508
+ }
509
+ return newContra;
510
+ }
511
+ function getCurrentContra(dir) {
512
+ let max = 0;
513
+ try {
514
+ for (const entry of readdirSync3(dir)) {
515
+ if (entry.endsWith(".contra")) {
516
+ const n = parseInt(entry, 10);
517
+ if (!isNaN(n) && n > max) max = n;
518
+ }
519
+ }
520
+ } catch {
521
+ }
522
+ return max;
523
+ }
524
+ function getCurrentCounter(dir) {
525
+ let max = 0;
526
+ try {
527
+ for (const entry of readdirSync3(dir)) {
528
+ if (entry.endsWith(".neuron") && !entry.startsWith("dopamine") && !entry.startsWith("memory") && entry !== "bomb.neuron") {
529
+ const n = parseInt(entry, 10);
530
+ if (!isNaN(n) && n > max) max = n;
531
+ }
532
+ }
533
+ } catch {
534
+ }
535
+ return max;
536
+ }
537
+ var init_fire = __esm({
538
+ "src/fire.ts"() {
539
+ "use strict";
540
+ }
541
+ });
542
+
543
+ // src/grow.ts
544
+ var grow_exports = {};
545
+ __export(grow_exports, {
546
+ growNeuron: () => growNeuron
547
+ });
548
+ import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3, existsSync as existsSync5, readdirSync as readdirSync4 } from "fs";
549
+ import { join as join4, relative as relative2 } from "path";
550
+ function growNeuron(brainRoot, neuronPath) {
551
+ const fullPath = join4(brainRoot, neuronPath);
552
+ if (existsSync5(fullPath)) {
553
+ const counter = fireNeuron(brainRoot, neuronPath);
554
+ return { action: "fired", path: neuronPath, counter };
555
+ }
556
+ if (neuronPath.includes("..") || neuronPath.startsWith("/")) {
557
+ throw new Error(`Invalid neuron path: "${neuronPath}" (path traversal not allowed)`);
558
+ }
559
+ const parts = neuronPath.split("/");
560
+ const regionName = parts[0];
561
+ if (regionName !== SKILLS_DIR && !REGIONS.includes(regionName)) {
562
+ throw new Error(`Invalid region: ${regionName}. Valid: ${REGIONS.join(", ")}, ${SKILLS_DIR}`);
563
+ }
564
+ const leafName = parts[parts.length - 1];
565
+ const newPrefix = leafName.match(/^(NO|DO|MUST|WARN)_/)?.[1] || "";
566
+ const newStripped = leafName.replace(/^(NO|DO|MUST|WARN)_/, "");
567
+ const newTokens = tokenize(newStripped);
568
+ const regionPath = join4(brainRoot, regionName);
569
+ if (existsSync5(regionPath)) {
570
+ const match = findSimilar(regionPath, regionPath, newTokens, newPrefix);
571
+ if (match) {
572
+ const matchRelPath = regionName + "/" + relative2(regionPath, match);
573
+ console.log(`\u{1F504} consolidation: "${neuronPath}" \u2248 "${matchRelPath}" (firing existing)`);
574
+ const counter = fireNeuron(brainRoot, matchRelPath);
575
+ return { action: "fired", path: matchRelPath, counter };
576
+ }
577
+ }
578
+ mkdirSync3(fullPath, { recursive: true });
579
+ writeFileSync3(join4(fullPath, "1.neuron"), "", "utf8");
580
+ console.log(`\u{1F331} grew: ${neuronPath} (1)`);
581
+ return { action: "grew", path: neuronPath, counter: 1 };
582
+ }
583
+ function findSimilar(dir, regionRoot, targetTokens, targetPrefix) {
584
+ let entries;
585
+ try {
586
+ entries = readdirSync4(dir, { withFileTypes: true });
587
+ } catch {
588
+ return null;
589
+ }
590
+ const hasNeuron = entries.some((e) => e.isFile() && e.name.endsWith(".neuron"));
591
+ if (hasNeuron) {
592
+ const folderName = dir.split("/").pop() || "";
593
+ const existingPrefix = folderName.match(/^(NO|DO|MUST|WARN)_/)?.[1] || "";
594
+ const existingStripped = folderName.replace(/^(NO|DO|MUST|WARN)_/, "");
595
+ const existingTokens = tokenize(existingStripped);
596
+ const similarity = jaccardSimilarity(targetTokens, existingTokens);
597
+ if (targetPrefix !== existingPrefix && targetTokens.length <= 2) {
598
+ } else if (similarity >= JACCARD_THRESHOLD) {
599
+ return dir;
600
+ }
601
+ }
602
+ for (const entry of entries) {
603
+ if (entry.name.startsWith("_") || entry.name.startsWith(".")) continue;
604
+ if (entry.isDirectory()) {
605
+ const match = findSimilar(join4(dir, entry.name), regionRoot, targetTokens, targetPrefix);
606
+ if (match) return match;
607
+ }
608
+ }
609
+ return null;
610
+ }
611
+ var init_grow = __esm({
612
+ "src/grow.ts"() {
613
+ "use strict";
614
+ init_constants();
615
+ init_similarity();
616
+ init_fire();
617
+ }
618
+ });
619
+
620
+ // src/episode.ts
621
+ var episode_exports = {};
622
+ __export(episode_exports, {
623
+ logEpisode: () => logEpisode,
624
+ readEpisodes: () => readEpisodes
625
+ });
626
+ import { readdirSync as readdirSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4, existsSync as existsSync6 } from "fs";
627
+ import { join as join5 } from "path";
628
+ function logEpisode(brainRoot, type, path, detail, extra) {
629
+ const logDir = join5(brainRoot, SESSION_LOG_DIR);
630
+ if (!existsSync6(logDir)) {
631
+ mkdirSync4(logDir, { recursive: true });
632
+ }
633
+ const nextSlot = getNextSlot(logDir);
634
+ const episode = {
635
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
636
+ type,
637
+ path,
638
+ detail,
639
+ ...extra?.outcome ? { outcome: extra.outcome } : {},
640
+ ...extra?.neurons ? { neurons: extra.neurons } : {}
641
+ };
642
+ writeFileSync4(
643
+ join5(logDir, `memory${nextSlot}.neuron`),
644
+ JSON.stringify(episode),
645
+ "utf8"
646
+ );
647
+ }
648
+ function readEpisodes(brainRoot) {
649
+ const logDir = join5(brainRoot, SESSION_LOG_DIR);
650
+ if (!existsSync6(logDir)) return [];
651
+ const episodes = [];
652
+ let entries;
653
+ try {
654
+ entries = readdirSync5(logDir);
655
+ } catch {
656
+ return [];
657
+ }
658
+ for (const entry of entries) {
659
+ if (!entry.startsWith("memory") || !entry.endsWith(".neuron")) continue;
660
+ try {
661
+ const content = readFileSync3(join5(logDir, entry), "utf8");
662
+ if (content.trim()) {
663
+ episodes.push(JSON.parse(content));
664
+ }
665
+ } catch {
666
+ }
667
+ }
668
+ episodes.sort((a, b) => a.ts.localeCompare(b.ts));
669
+ return episodes;
670
+ }
671
+ function getNextSlot(logDir) {
672
+ let maxSlot = 0;
673
+ try {
674
+ for (const entry of readdirSync5(logDir)) {
675
+ if (entry.startsWith("memory") && entry.endsWith(".neuron")) {
676
+ const n = parseInt(entry.replace("memory", "").replace(".neuron", ""), 10);
677
+ if (!isNaN(n) && n > maxSlot) maxSlot = n;
678
+ }
679
+ }
680
+ } catch {
681
+ }
682
+ const next = maxSlot + 1;
683
+ return next > MAX_EPISODES ? maxSlot % MAX_EPISODES + 1 : next;
684
+ }
685
+ var MAX_EPISODES, SESSION_LOG_DIR;
686
+ var init_episode = __esm({
687
+ "src/episode.ts"() {
688
+ "use strict";
689
+ MAX_EPISODES = 100;
690
+ SESSION_LOG_DIR = "hippocampus/session_log";
691
+ }
692
+ });
693
+
694
+ // src/candidates.ts
695
+ var candidates_exports = {};
696
+ __export(candidates_exports, {
697
+ CANDIDATE_DECAY_DAYS: () => CANDIDATE_DECAY_DAYS,
698
+ CANDIDATE_THRESHOLD: () => CANDIDATE_THRESHOLD,
699
+ fromCandidatePath: () => fromCandidatePath,
700
+ growCandidate: () => growCandidate,
701
+ listCandidates: () => listCandidates,
702
+ promoteCandidates: () => promoteCandidates,
703
+ propagateToShared: () => propagateToShared,
704
+ toCandidatePath: () => toCandidatePath
705
+ });
706
+ import { existsSync as existsSync7, mkdirSync as mkdirSync5, readdirSync as readdirSync6, renameSync as renameSync2, rmSync, statSync as statSync2 } from "fs";
707
+ import { join as join6, dirname as dirname2, relative as relative3 } from "path";
708
+ function toCandidatePath(neuronPath) {
709
+ const slash = neuronPath.indexOf("/");
710
+ if (slash === -1) throw new Error(`Invalid neuron path (missing region): ${neuronPath}`);
711
+ return `${neuronPath.slice(0, slash)}/${CANDIDATE_SEGMENT}/${neuronPath.slice(slash + 1)}`;
712
+ }
713
+ function fromCandidatePath(candidatePath) {
714
+ return candidatePath.replace(`/${CANDIDATE_SEGMENT}/`, "/");
715
+ }
716
+ function growCandidate(brainRoot, neuronPath) {
717
+ const candidatePath = toCandidatePath(neuronPath);
718
+ const result = growNeuron(brainRoot, candidatePath);
719
+ if (result.counter >= CANDIDATE_THRESHOLD) {
720
+ const ok = moveCandidate(brainRoot, candidatePath, neuronPath);
721
+ if (ok) propagateToShared(brainRoot, neuronPath);
722
+ return { ...result, path: ok ? neuronPath : result.path, promoted: ok };
723
+ }
724
+ console.log(` \u{1F331} candidate (${result.counter}/${CANDIDATE_THRESHOLD}): ${candidatePath}`);
725
+ return { ...result, promoted: false };
726
+ }
727
+ function moveCandidate(brainRoot, candidatePath, targetPath) {
728
+ const src = join6(brainRoot, candidatePath);
729
+ if (!existsSync7(src)) return false;
730
+ const dst = join6(brainRoot, targetPath);
731
+ if (existsSync7(dst)) {
732
+ fireNeuron(brainRoot, targetPath);
733
+ rmSync(src, { recursive: true, force: true });
734
+ } else {
735
+ mkdirSync5(dirname2(dst), { recursive: true });
736
+ renameSync2(src, dst);
737
+ }
738
+ console.log(`\u{1F393} promoted: ${candidatePath} \u2192 ${targetPath}`);
739
+ return true;
740
+ }
741
+ function promoteCandidates(brainRoot) {
742
+ const promoted = [];
743
+ const decayed = [];
744
+ const decayMs = CANDIDATE_DECAY_DAYS * 24 * 60 * 60 * 1e3;
745
+ const now = Date.now();
746
+ for (const region of REGIONS) {
747
+ const candidateRoot = join6(brainRoot, region, CANDIDATE_SEGMENT);
748
+ walkNeuronDirs(candidateRoot, (neuronDir) => {
749
+ const rel = relative3(join6(brainRoot, region), neuronDir);
750
+ const candidatePath = `${region}/${rel}`;
751
+ const targetPath = fromCandidatePath(candidatePath);
752
+ const counter = readCounter(neuronDir);
753
+ const mtime = statSync2(neuronDir).mtimeMs;
754
+ if (counter >= CANDIDATE_THRESHOLD) {
755
+ moveCandidate(brainRoot, candidatePath, targetPath);
756
+ propagateToShared(brainRoot, targetPath);
757
+ promoted.push(targetPath);
758
+ } else if (now - mtime > decayMs) {
759
+ rmSync(neuronDir, { recursive: true, force: true });
760
+ decayed.push(candidatePath);
761
+ console.log(`\u{1F480} candidate decayed: ${candidatePath}`);
762
+ }
763
+ });
764
+ }
765
+ return { promoted, decayed };
766
+ }
767
+ function listCandidates(brainRoot) {
768
+ const results = [];
769
+ const now = Date.now();
770
+ for (const region of REGIONS) {
771
+ const candidateRoot = join6(brainRoot, region, CANDIDATE_SEGMENT);
772
+ walkNeuronDirs(candidateRoot, (neuronDir) => {
773
+ const rel = relative3(join6(brainRoot, region), neuronDir);
774
+ const candidatePath = `${region}/${rel}`;
775
+ const targetPath = fromCandidatePath(candidatePath);
776
+ const counter = readCounter(neuronDir);
777
+ const mtime = statSync2(neuronDir).mtimeMs;
778
+ const daysInactive = Math.floor((now - mtime) / (24 * 60 * 60 * 1e3));
779
+ results.push({ candidatePath, targetPath, counter, daysInactive });
780
+ });
781
+ }
782
+ return results;
783
+ }
784
+ function walkNeuronDirs(dir, cb) {
785
+ if (!existsSync7(dir)) return;
786
+ try {
787
+ const entries = readdirSync6(dir, { withFileTypes: true });
788
+ const hasNeuron = entries.some((e) => e.isFile() && e.name.endsWith(".neuron"));
789
+ if (hasNeuron) {
790
+ cb(dir);
791
+ return;
792
+ }
793
+ for (const entry of entries) {
794
+ if (entry.isDirectory() && !entry.name.startsWith(".")) {
795
+ walkNeuronDirs(join6(dir, entry.name), cb);
796
+ }
797
+ }
798
+ } catch {
799
+ }
800
+ }
801
+ function readCounter(dir) {
802
+ try {
803
+ const files = readdirSync6(dir).filter((f) => /^\d+\.neuron$/.test(f));
804
+ if (files.length === 0) return 0;
805
+ return Math.max(...files.map((f) => parseInt(f, 10)));
806
+ } catch {
807
+ return 0;
808
+ }
809
+ }
810
+ function propagateToShared(brainRoot, targetPath) {
811
+ try {
812
+ const agentsIdx = brainRoot.indexOf("/agents/");
813
+ if (agentsIdx === -1) return false;
814
+ const multiBrainRoot = brainRoot.slice(0, agentsIdx);
815
+ const sharedRoot = join6(multiBrainRoot, "shared");
816
+ if (!existsSync7(sharedRoot)) return false;
817
+ const episodes = readEpisodes(brainRoot);
818
+ const neuronName = targetPath.split("/").pop() || "";
819
+ const hasRelevantEpisode = episodes.some(
820
+ (ep) => PROPAGATION_EPISODE_TYPES.includes(ep.type) && (ep.path.includes(neuronName) || ep.detail.includes(neuronName))
821
+ );
822
+ if (!hasRelevantEpisode) return false;
823
+ growNeuron(sharedRoot, targetPath);
824
+ console.log(` \u{1F4E1} propagated to shared: ${targetPath}`);
825
+ return true;
826
+ } catch {
827
+ return false;
828
+ }
829
+ }
830
+ var CANDIDATE_THRESHOLD, CANDIDATE_DECAY_DAYS, CANDIDATE_SEGMENT;
831
+ var init_candidates = __esm({
832
+ "src/candidates.ts"() {
833
+ "use strict";
834
+ init_constants();
835
+ init_grow();
836
+ init_fire();
837
+ init_episode();
838
+ CANDIDATE_THRESHOLD = 3;
839
+ CANDIDATE_DECAY_DAYS = 14;
840
+ CANDIDATE_SEGMENT = "_candidates";
841
+ }
842
+ });
843
+
438
844
  // src/emit.ts
439
845
  var emit_exports = {};
440
846
  __export(emit_exports, {
@@ -445,9 +851,9 @@ __export(emit_exports, {
445
851
  printDiag: () => printDiag,
446
852
  writeAllTiers: () => writeAllTiers
447
853
  });
448
- import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
449
- import { join as join3, dirname as dirname2 } from "path";
450
- function emitBootstrap(result, brain) {
854
+ import { existsSync as existsSync8, readFileSync as readFileSync4, writeFileSync as writeFileSync5, mkdirSync as mkdirSync6 } from "fs";
855
+ import { join as join7, dirname as dirname3 } from "path";
856
+ function emitBootstrap(result, brain, brainRoot) {
451
857
  const lines = [];
452
858
  const now = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d+Z$/, "");
453
859
  lines.push(MARKER_START);
@@ -503,6 +909,19 @@ function emitBootstrap(result, brain) {
503
909
  lines.push(`| ${icon} ${region.name} | ${active.length} | ${activation} |`);
504
910
  }
505
911
  lines.push("");
912
+ if (brainRoot) {
913
+ const candidates = listCandidates(brainRoot);
914
+ if (candidates.length > 0) {
915
+ const top = candidates.slice(0, 5);
916
+ lines.push("### Provisional Rules (evaluating)");
917
+ lines.push("Follow these during this session. They graduate after 3 sessions.");
918
+ for (const c of top) {
919
+ const bar = "\u2588".repeat(c.counter) + "\u2591".repeat(Math.max(0, 3 - c.counter));
920
+ lines.push(`- ${bar} ${pathToSentence(c.targetPath)}`);
921
+ }
922
+ lines.push("");
923
+ }
924
+ }
506
925
  lines.push(MARKER_END);
507
926
  return lines.join("\n");
508
927
  }
@@ -597,7 +1016,7 @@ function emitRegionRules(region) {
597
1016
  function emitToTarget(brainRoot, target) {
598
1017
  const brain = scanBrain(brainRoot);
599
1018
  const result = runSubsumption(brain);
600
- const content = emitBootstrap(result, brain);
1019
+ const content = emitBootstrap(result, brain, brainRoot);
601
1020
  if (target === "all") {
602
1021
  for (const [name, filePath] of Object.entries(EMIT_TARGETS)) {
603
1022
  writeTarget(filePath, content);
@@ -613,33 +1032,33 @@ function emitToTarget(brainRoot, target) {
613
1032
  }
614
1033
  function writeAllTiers(brainRoot, result, brain) {
615
1034
  const indexContent = emitIndex(result, brain);
616
- writeFileSync2(join3(brainRoot, "_index.md"), indexContent, "utf8");
1035
+ writeFileSync5(join7(brainRoot, "_index.md"), indexContent, "utf8");
617
1036
  for (const region of result.activeRegions) {
618
- if (existsSync4(region.path)) {
1037
+ if (existsSync8(region.path)) {
619
1038
  const rulesContent = emitRegionRules(region);
620
- writeFileSync2(join3(region.path, "_rules.md"), rulesContent, "utf8");
1039
+ writeFileSync5(join7(region.path, "_rules.md"), rulesContent, "utf8");
621
1040
  }
622
1041
  }
623
1042
  }
624
1043
  function writeTarget(filePath, content) {
625
- const dir = dirname2(filePath);
626
- if (dir !== "." && !existsSync4(dir)) {
627
- mkdirSync2(dir, { recursive: true });
1044
+ const dir = dirname3(filePath);
1045
+ if (dir !== "." && !existsSync8(dir)) {
1046
+ mkdirSync6(dir, { recursive: true });
628
1047
  }
629
- if (existsSync4(filePath)) {
630
- const existing = readFileSync3(filePath, "utf8");
1048
+ if (existsSync8(filePath)) {
1049
+ const existing = readFileSync4(filePath, "utf8");
631
1050
  const startIdx = existing.indexOf(MARKER_START);
632
1051
  const endIdx = existing.indexOf(MARKER_END);
633
1052
  if (startIdx !== -1 && endIdx !== -1) {
634
1053
  const before = existing.slice(0, startIdx);
635
1054
  const after = existing.slice(endIdx + MARKER_END.length);
636
- writeFileSync2(filePath, before + content + after, "utf8");
1055
+ writeFileSync5(filePath, before + content + after, "utf8");
637
1056
  return;
638
1057
  }
639
- writeFileSync2(filePath, content + "\n\n" + existing, "utf8");
1058
+ writeFileSync5(filePath, content + "\n\n" + existing, "utf8");
640
1059
  return;
641
1060
  }
642
- writeFileSync2(filePath, content, "utf8");
1061
+ writeFileSync5(filePath, content, "utf8");
643
1062
  }
644
1063
  function printDiag(brain, result) {
645
1064
  console.log("");
@@ -688,6 +1107,7 @@ var init_emit = __esm({
688
1107
  init_scanner();
689
1108
  init_subsumption();
690
1109
  init_constants();
1110
+ init_candidates();
691
1111
  }
692
1112
  });
693
1113
 
@@ -697,19 +1117,19 @@ __export(update_check_exports, {
697
1117
  checkForUpdates: () => checkForUpdates,
698
1118
  formatUpdateBanner: () => formatUpdateBanner
699
1119
  });
700
- import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3, statSync as statSync2, unlinkSync } from "fs";
701
- import { join as join4 } from "path";
1120
+ import { existsSync as existsSync9, mkdirSync as mkdirSync7, readFileSync as readFileSync5, writeFileSync as writeFileSync6, statSync as statSync3, unlinkSync } from "fs";
1121
+ import { join as join8 } from "path";
702
1122
  function getStateDir() {
703
- return join4(process.env.HOME || "~", ".hebbian");
1123
+ return join8(process.env.HOME || "~", ".hebbian");
704
1124
  }
705
1125
  function ensureStateDir(stateDir) {
706
- if (!existsSync5(stateDir)) {
707
- mkdirSync3(stateDir, { recursive: true });
1126
+ if (!existsSync9(stateDir)) {
1127
+ mkdirSync7(stateDir, { recursive: true });
708
1128
  }
709
1129
  }
710
1130
  function isCacheStale(cachePath, type) {
711
1131
  try {
712
- const mtime = statSync2(cachePath).mtimeMs;
1132
+ const mtime = statSync3(cachePath).mtimeMs;
713
1133
  const ageMinutes = (Date.now() - mtime) / 1e3 / 60;
714
1134
  const ttl = type === "UP_TO_DATE" ? TTL_UP_TO_DATE : TTL_UPGRADE_AVAILABLE;
715
1135
  return ageMinutes > ttl;
@@ -718,10 +1138,10 @@ function isCacheStale(cachePath, type) {
718
1138
  }
719
1139
  }
720
1140
  function readCache(stateDir) {
721
- const cachePath = join4(stateDir, "last-update-check");
722
- if (!existsSync5(cachePath)) return null;
1141
+ const cachePath = join8(stateDir, "last-update-check");
1142
+ if (!existsSync9(cachePath)) return null;
723
1143
  try {
724
- const line = readFileSync4(cachePath, "utf8").trim();
1144
+ const line = readFileSync5(cachePath, "utf8").trim();
725
1145
  if (line.startsWith("UP_TO_DATE")) {
726
1146
  if (isCacheStale(cachePath, "UP_TO_DATE")) return null;
727
1147
  const ver = line.split(/\s+/)[1];
@@ -739,13 +1159,13 @@ function readCache(stateDir) {
739
1159
  }
740
1160
  function writeCache(stateDir, line) {
741
1161
  ensureStateDir(stateDir);
742
- writeFileSync3(join4(stateDir, "last-update-check"), line, "utf8");
1162
+ writeFileSync6(join8(stateDir, "last-update-check"), line, "utf8");
743
1163
  }
744
1164
  function isSnoozed(stateDir, remoteVersion) {
745
- const snoozePath = join4(stateDir, "update-snoozed");
746
- if (!existsSync5(snoozePath)) return false;
1165
+ const snoozePath = join8(stateDir, "update-snoozed");
1166
+ if (!existsSync9(snoozePath)) return false;
747
1167
  try {
748
- const [ver, levelStr, epochStr] = readFileSync4(snoozePath, "utf8").trim().split(/\s+/);
1168
+ const [ver, levelStr, epochStr] = readFileSync5(snoozePath, "utf8").trim().split(/\s+/);
749
1169
  if (ver !== remoteVersion) {
750
1170
  unlinkSync(snoozePath);
751
1171
  return false;
@@ -789,233 +1209,51 @@ async function checkForUpdates(currentVersion) {
789
1209
  }
790
1210
  if (cached.type === "UPGRADE_AVAILABLE" && cached.current && cached.latest) {
791
1211
  if (cached.current === currentVersion && !isSnoozed(stateDir, cached.latest)) {
792
- return { type: "upgrade_available", current: currentVersion, latest: cached.latest };
793
- }
794
- }
795
- }
796
- const latest = await fetchLatestVersion();
797
- if (!latest) {
798
- writeCache(stateDir, `UP_TO_DATE ${currentVersion}`);
799
- return { type: "up_to_date" };
800
- }
801
- if (latest === currentVersion) {
802
- writeCache(stateDir, `UP_TO_DATE ${currentVersion}`);
803
- return { type: "up_to_date" };
804
- }
805
- writeCache(stateDir, `UPGRADE_AVAILABLE ${currentVersion} ${latest}`);
806
- if (isSnoozed(stateDir, latest)) {
807
- return { type: "up_to_date" };
808
- }
809
- return { type: "upgrade_available", current: currentVersion, latest };
810
- }
811
- function formatUpdateBanner(status) {
812
- if (status.type !== "upgrade_available") return null;
813
- return [
814
- ``,
815
- ` \u26A1 hebbian v${status.latest} available (current: v${status.current})`,
816
- ` npm i -g hebbian@latest`,
817
- ``
818
- ].join("\n");
819
- }
820
- var PACKAGE_NAME, NPM_REGISTRY_URL, FETCH_TIMEOUT_MS, TTL_UP_TO_DATE, TTL_UPGRADE_AVAILABLE, SNOOZE_DURATIONS;
821
- var init_update_check = __esm({
822
- "src/update-check.ts"() {
823
- "use strict";
824
- PACKAGE_NAME = "hebbian";
825
- NPM_REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
826
- FETCH_TIMEOUT_MS = 5e3;
827
- TTL_UP_TO_DATE = 60;
828
- TTL_UPGRADE_AVAILABLE = 720;
829
- SNOOZE_DURATIONS = {
830
- 1: 86400,
831
- // 24h
832
- 2: 172800,
833
- // 48h
834
- 3: 604800
835
- // 7d (and beyond)
836
- };
837
- }
838
- });
839
-
840
- // src/fire.ts
841
- var fire_exports = {};
842
- __export(fire_exports, {
843
- contraNeuron: () => contraNeuron,
844
- fireNeuron: () => fireNeuron,
845
- getCurrentContra: () => getCurrentContra,
846
- getCurrentCounter: () => getCurrentCounter
847
- });
848
- import { readdirSync as readdirSync3, renameSync, writeFileSync as writeFileSync4, existsSync as existsSync6, mkdirSync as mkdirSync4 } from "fs";
849
- import { join as join5 } from "path";
850
- function fireNeuron(brainRoot, neuronPath) {
851
- const fullPath = join5(brainRoot, neuronPath);
852
- if (!existsSync6(fullPath)) {
853
- mkdirSync4(fullPath, { recursive: true });
854
- writeFileSync4(join5(fullPath, "1.neuron"), "", "utf8");
855
- console.log(`\u{1F331} grew + fired: ${neuronPath} (1)`);
856
- return 1;
857
- }
858
- const current = getCurrentCounter(fullPath);
859
- const newCounter = current + 1;
860
- if (current > 0) {
861
- renameSync(join5(fullPath, `${current}.neuron`), join5(fullPath, `${newCounter}.neuron`));
862
- } else {
863
- writeFileSync4(join5(fullPath, `${newCounter}.neuron`), "", "utf8");
864
- }
865
- console.log(`\u{1F525} fired: ${neuronPath} (${current} \u2192 ${newCounter})`);
866
- return newCounter;
867
- }
868
- function contraNeuron(brainRoot, neuronPath) {
869
- const fullPath = join5(brainRoot, neuronPath);
870
- if (!existsSync6(fullPath)) {
871
- return 0;
872
- }
873
- const current = getCurrentContra(fullPath);
874
- const newContra = current + 1;
875
- if (current > 0) {
876
- renameSync(join5(fullPath, `${current}.contra`), join5(fullPath, `${newContra}.contra`));
877
- } else {
878
- writeFileSync4(join5(fullPath, `${newContra}.contra`), "", "utf8");
879
- }
880
- return newContra;
881
- }
882
- function getCurrentContra(dir) {
883
- let max = 0;
884
- try {
885
- for (const entry of readdirSync3(dir)) {
886
- if (entry.endsWith(".contra")) {
887
- const n = parseInt(entry, 10);
888
- if (!isNaN(n) && n > max) max = n;
889
- }
890
- }
891
- } catch {
892
- }
893
- return max;
894
- }
895
- function getCurrentCounter(dir) {
896
- let max = 0;
897
- try {
898
- for (const entry of readdirSync3(dir)) {
899
- if (entry.endsWith(".neuron") && !entry.startsWith("dopamine") && !entry.startsWith("memory") && entry !== "bomb.neuron") {
900
- const n = parseInt(entry, 10);
901
- if (!isNaN(n) && n > max) max = n;
902
- }
903
- }
904
- } catch {
905
- }
906
- return max;
907
- }
908
- var init_fire = __esm({
909
- "src/fire.ts"() {
910
- "use strict";
911
- }
912
- });
913
-
914
- // src/similarity.ts
915
- function tokenize(name) {
916
- 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);
917
- }
918
- function stem(word) {
919
- const suffixes = ["ing", "tion", "sion", "ness", "ment", "able", "ible", "ful", "less", "ous", "ive", "ity", "ies", "ed", "er", "es", "ly", "al", "en"];
920
- for (const suffix of suffixes) {
921
- if (word.length > suffix.length + 2 && word.endsWith(suffix)) {
922
- return word.slice(0, -suffix.length);
923
- }
924
- }
925
- return word;
926
- }
927
- function jaccardSimilarity(a, b) {
928
- if (a.length === 0 && b.length === 0) return 1;
929
- if (a.length === 0 || b.length === 0) return 0;
930
- const setA = new Set(a);
931
- const setB = new Set(b);
932
- let intersection = 0;
933
- for (const item of setA) {
934
- if (setB.has(item)) intersection++;
935
- }
936
- const union = (/* @__PURE__ */ new Set([...setA, ...setB])).size;
937
- return intersection / union;
938
- }
939
- var init_similarity = __esm({
940
- "src/similarity.ts"() {
941
- "use strict";
942
- }
943
- });
944
-
945
- // src/grow.ts
946
- var grow_exports = {};
947
- __export(grow_exports, {
948
- growNeuron: () => growNeuron
949
- });
950
- import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync5, existsSync as existsSync7, readdirSync as readdirSync4 } from "fs";
951
- import { join as join6, relative as relative2 } from "path";
952
- function growNeuron(brainRoot, neuronPath) {
953
- const fullPath = join6(brainRoot, neuronPath);
954
- if (existsSync7(fullPath)) {
955
- const counter = fireNeuron(brainRoot, neuronPath);
956
- return { action: "fired", path: neuronPath, counter };
957
- }
958
- if (neuronPath.includes("..") || neuronPath.startsWith("/")) {
959
- throw new Error(`Invalid neuron path: "${neuronPath}" (path traversal not allowed)`);
960
- }
961
- const parts = neuronPath.split("/");
962
- const regionName = parts[0];
963
- if (regionName !== SKILLS_DIR && !REGIONS.includes(regionName)) {
964
- throw new Error(`Invalid region: ${regionName}. Valid: ${REGIONS.join(", ")}, ${SKILLS_DIR}`);
965
- }
966
- const leafName = parts[parts.length - 1];
967
- const newPrefix = leafName.match(/^(NO|DO|MUST|WARN)_/)?.[1] || "";
968
- const newStripped = leafName.replace(/^(NO|DO|MUST|WARN)_/, "");
969
- const newTokens = tokenize(newStripped);
970
- const regionPath = join6(brainRoot, regionName);
971
- if (existsSync7(regionPath)) {
972
- const match = findSimilar(regionPath, regionPath, newTokens, newPrefix);
973
- if (match) {
974
- const matchRelPath = regionName + "/" + relative2(regionPath, match);
975
- console.log(`\u{1F504} consolidation: "${neuronPath}" \u2248 "${matchRelPath}" (firing existing)`);
976
- const counter = fireNeuron(brainRoot, matchRelPath);
977
- return { action: "fired", path: matchRelPath, counter };
978
- }
979
- }
980
- mkdirSync5(fullPath, { recursive: true });
981
- writeFileSync5(join6(fullPath, "1.neuron"), "", "utf8");
982
- console.log(`\u{1F331} grew: ${neuronPath} (1)`);
983
- return { action: "grew", path: neuronPath, counter: 1 };
984
- }
985
- function findSimilar(dir, regionRoot, targetTokens, targetPrefix) {
986
- let entries;
987
- try {
988
- entries = readdirSync4(dir, { withFileTypes: true });
989
- } catch {
990
- return null;
991
- }
992
- const hasNeuron = entries.some((e) => e.isFile() && e.name.endsWith(".neuron"));
993
- if (hasNeuron) {
994
- const folderName = dir.split("/").pop() || "";
995
- const existingPrefix = folderName.match(/^(NO|DO|MUST|WARN)_/)?.[1] || "";
996
- const existingStripped = folderName.replace(/^(NO|DO|MUST|WARN)_/, "");
997
- const existingTokens = tokenize(existingStripped);
998
- const similarity = jaccardSimilarity(targetTokens, existingTokens);
999
- if (targetPrefix !== existingPrefix && targetTokens.length <= 2) {
1000
- } else if (similarity >= JACCARD_THRESHOLD) {
1001
- return dir;
1002
- }
1003
- }
1004
- for (const entry of entries) {
1005
- if (entry.name.startsWith("_") || entry.name.startsWith(".")) continue;
1006
- if (entry.isDirectory()) {
1007
- const match = findSimilar(join6(dir, entry.name), regionRoot, targetTokens, targetPrefix);
1008
- if (match) return match;
1212
+ return { type: "upgrade_available", current: currentVersion, latest: cached.latest };
1213
+ }
1009
1214
  }
1010
1215
  }
1011
- return null;
1216
+ const latest = await fetchLatestVersion();
1217
+ if (!latest) {
1218
+ writeCache(stateDir, `UP_TO_DATE ${currentVersion}`);
1219
+ return { type: "up_to_date" };
1220
+ }
1221
+ if (latest === currentVersion) {
1222
+ writeCache(stateDir, `UP_TO_DATE ${currentVersion}`);
1223
+ return { type: "up_to_date" };
1224
+ }
1225
+ writeCache(stateDir, `UPGRADE_AVAILABLE ${currentVersion} ${latest}`);
1226
+ if (isSnoozed(stateDir, latest)) {
1227
+ return { type: "up_to_date" };
1228
+ }
1229
+ return { type: "upgrade_available", current: currentVersion, latest };
1012
1230
  }
1013
- var init_grow = __esm({
1014
- "src/grow.ts"() {
1231
+ function formatUpdateBanner(status) {
1232
+ if (status.type !== "upgrade_available") return null;
1233
+ return [
1234
+ ``,
1235
+ ` \u26A1 hebbian v${status.latest} available (current: v${status.current})`,
1236
+ ` npm i -g hebbian@latest`,
1237
+ ``
1238
+ ].join("\n");
1239
+ }
1240
+ var PACKAGE_NAME, NPM_REGISTRY_URL, FETCH_TIMEOUT_MS, TTL_UP_TO_DATE, TTL_UPGRADE_AVAILABLE, SNOOZE_DURATIONS;
1241
+ var init_update_check = __esm({
1242
+ "src/update-check.ts"() {
1015
1243
  "use strict";
1016
- init_constants();
1017
- init_similarity();
1018
- init_fire();
1244
+ PACKAGE_NAME = "hebbian";
1245
+ NPM_REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
1246
+ FETCH_TIMEOUT_MS = 5e3;
1247
+ TTL_UP_TO_DATE = 60;
1248
+ TTL_UPGRADE_AVAILABLE = 720;
1249
+ SNOOZE_DURATIONS = {
1250
+ 1: 86400,
1251
+ // 24h
1252
+ 2: 172800,
1253
+ // 48h
1254
+ 3: 604800
1255
+ // 7d (and beyond)
1256
+ };
1019
1257
  }
1020
1258
  });
1021
1259
 
@@ -1024,10 +1262,10 @@ var rollback_exports = {};
1024
1262
  __export(rollback_exports, {
1025
1263
  rollbackNeuron: () => rollbackNeuron
1026
1264
  });
1027
- import { renameSync as renameSync2 } from "fs";
1028
- import { join as join7 } from "path";
1265
+ import { renameSync as renameSync3 } from "fs";
1266
+ import { join as join9 } from "path";
1029
1267
  function rollbackNeuron(brainRoot, neuronPath) {
1030
- const fullPath = join7(brainRoot, neuronPath);
1268
+ const fullPath = join9(brainRoot, neuronPath);
1031
1269
  const current = getCurrentCounter(fullPath);
1032
1270
  if (current === 0) {
1033
1271
  throw new Error(`Neuron not found: ${neuronPath}`);
@@ -1036,7 +1274,7 @@ function rollbackNeuron(brainRoot, neuronPath) {
1036
1274
  throw new Error(`Counter already at minimum (1): ${neuronPath}`);
1037
1275
  }
1038
1276
  const newCounter = current - 1;
1039
- renameSync2(join7(fullPath, `${current}.neuron`), join7(fullPath, `${newCounter}.neuron`));
1277
+ renameSync3(join9(fullPath, `${current}.neuron`), join9(fullPath, `${newCounter}.neuron`));
1040
1278
  console.log(`\u23EA rollback: ${neuronPath} (${current} \u2192 ${newCounter})`);
1041
1279
  return newCounter;
1042
1280
  }
@@ -1052,31 +1290,31 @@ var signal_exports = {};
1052
1290
  __export(signal_exports, {
1053
1291
  signalNeuron: () => signalNeuron
1054
1292
  });
1055
- import { writeFileSync as writeFileSync6, existsSync as existsSync8, readdirSync as readdirSync5 } from "fs";
1056
- import { join as join8 } from "path";
1293
+ import { writeFileSync as writeFileSync7, existsSync as existsSync10, readdirSync as readdirSync7 } from "fs";
1294
+ import { join as join10 } from "path";
1057
1295
  function signalNeuron(brainRoot, neuronPath, signalType) {
1058
1296
  if (!SIGNAL_TYPES.includes(signalType)) {
1059
1297
  throw new Error(`Invalid signal type: ${signalType}. Valid: ${SIGNAL_TYPES.join(", ")}`);
1060
1298
  }
1061
- const fullPath = join8(brainRoot, neuronPath);
1062
- if (!existsSync8(fullPath)) {
1299
+ const fullPath = join10(brainRoot, neuronPath);
1300
+ if (!existsSync10(fullPath)) {
1063
1301
  throw new Error(`Neuron not found: ${neuronPath}`);
1064
1302
  }
1065
1303
  switch (signalType) {
1066
1304
  case "bomb": {
1067
- writeFileSync6(join8(fullPath, "bomb.neuron"), "", "utf8");
1305
+ writeFileSync7(join10(fullPath, "bomb.neuron"), "", "utf8");
1068
1306
  console.log(`\u{1F4A3} bomb planted: ${neuronPath}`);
1069
1307
  break;
1070
1308
  }
1071
1309
  case "dopamine": {
1072
1310
  const level = getNextSignalLevel(fullPath, "dopamine");
1073
- writeFileSync6(join8(fullPath, `dopamine${level}.neuron`), "", "utf8");
1311
+ writeFileSync7(join10(fullPath, `dopamine${level}.neuron`), "", "utf8");
1074
1312
  console.log(`\u{1F7E2} dopamine +${level}: ${neuronPath}`);
1075
1313
  break;
1076
1314
  }
1077
1315
  case "memory": {
1078
1316
  const level = getNextSignalLevel(fullPath, "memory");
1079
- writeFileSync6(join8(fullPath, `memory${level}.neuron`), "", "utf8");
1317
+ writeFileSync7(join10(fullPath, `memory${level}.neuron`), "", "utf8");
1080
1318
  console.log(`\u{1F4BE} memory +${level}: ${neuronPath}`);
1081
1319
  break;
1082
1320
  }
@@ -1085,7 +1323,7 @@ function signalNeuron(brainRoot, neuronPath, signalType) {
1085
1323
  function getNextSignalLevel(dir, prefix) {
1086
1324
  let max = 0;
1087
1325
  try {
1088
- for (const entry of readdirSync5(dir)) {
1326
+ for (const entry of readdirSync7(dir)) {
1089
1327
  if (entry.startsWith(prefix) && entry.endsWith(".neuron")) {
1090
1328
  const n = parseInt(entry.replace(prefix, ""), 10);
1091
1329
  if (!isNaN(n) && n > max) max = n;
@@ -1107,15 +1345,15 @@ var decay_exports = {};
1107
1345
  __export(decay_exports, {
1108
1346
  runDecay: () => runDecay
1109
1347
  });
1110
- import { readdirSync as readdirSync6, statSync as statSync3, writeFileSync as writeFileSync7, existsSync as existsSync9 } from "fs";
1111
- import { join as join9 } from "path";
1348
+ import { readdirSync as readdirSync8, statSync as statSync4, writeFileSync as writeFileSync8, existsSync as existsSync11 } from "fs";
1349
+ import { join as join11 } from "path";
1112
1350
  function runDecay(brainRoot, days) {
1113
1351
  const threshold = Date.now() - days * 24 * 60 * 60 * 1e3;
1114
1352
  let scanned = 0;
1115
1353
  let decayed = 0;
1116
1354
  for (const regionName of REGIONS) {
1117
- const regionPath = join9(brainRoot, regionName);
1118
- if (!existsSync9(regionPath)) continue;
1355
+ const regionPath = join11(brainRoot, regionName);
1356
+ if (!existsSync11(regionPath)) continue;
1119
1357
  const result = decayWalk(regionPath, threshold, 0);
1120
1358
  scanned += result.scanned;
1121
1359
  decayed += result.decayed;
@@ -1129,7 +1367,7 @@ function decayWalk(dir, threshold, depth) {
1129
1367
  let decayed = 0;
1130
1368
  let entries;
1131
1369
  try {
1132
- entries = readdirSync6(dir, { withFileTypes: true });
1370
+ entries = readdirSync8(dir, { withFileTypes: true });
1133
1371
  } catch {
1134
1372
  return { scanned: 0, decayed: 0 };
1135
1373
  }
@@ -1141,7 +1379,7 @@ function decayWalk(dir, threshold, depth) {
1141
1379
  if (entry.name.endsWith(".neuron")) {
1142
1380
  hasNeuronFile = true;
1143
1381
  try {
1144
- const st = statSync3(join9(dir, entry.name));
1382
+ const st = statSync4(join11(dir, entry.name));
1145
1383
  if (st.mtimeMs > latestMod) latestMod = st.mtimeMs;
1146
1384
  } catch {
1147
1385
  }
@@ -1155,8 +1393,8 @@ function decayWalk(dir, threshold, depth) {
1155
1393
  scanned++;
1156
1394
  if (!isDormant && latestMod < threshold) {
1157
1395
  const age = Math.floor((Date.now() - latestMod) / (24 * 60 * 60 * 1e3));
1158
- writeFileSync7(
1159
- join9(dir, "decay.dormant"),
1396
+ writeFileSync8(
1397
+ join11(dir, "decay.dormant"),
1160
1398
  `Dormant since ${(/* @__PURE__ */ new Date()).toISOString()} (${age} days inactive)`,
1161
1399
  "utf8"
1162
1400
  );
@@ -1166,7 +1404,7 @@ function decayWalk(dir, threshold, depth) {
1166
1404
  for (const entry of entries) {
1167
1405
  if (entry.name.startsWith("_") || entry.name.startsWith(".")) continue;
1168
1406
  if (entry.isDirectory()) {
1169
- const sub = decayWalk(join9(dir, entry.name), threshold, depth + 1);
1407
+ const sub = decayWalk(join11(dir, entry.name), threshold, depth + 1);
1170
1408
  scanned += sub.scanned;
1171
1409
  decayed += sub.decayed;
1172
1410
  }
@@ -1185,8 +1423,8 @@ var dedup_exports = {};
1185
1423
  __export(dedup_exports, {
1186
1424
  runDedup: () => runDedup
1187
1425
  });
1188
- import { writeFileSync as writeFileSync8 } from "fs";
1189
- import { join as join10 } from "path";
1426
+ import { writeFileSync as writeFileSync9 } from "fs";
1427
+ import { join as join12 } from "path";
1190
1428
  function runDedup(brainRoot) {
1191
1429
  const brain = scanBrain(brainRoot);
1192
1430
  let scanned = 0;
@@ -1208,8 +1446,8 @@ function runDedup(brainRoot) {
1208
1446
  const [keep, drop] = ni.counter >= nj.counter ? [ni, nj] : [nj, ni];
1209
1447
  const relKeep = `${region.name}/${keep.path}`;
1210
1448
  fireNeuron(brainRoot, relKeep);
1211
- writeFileSync8(
1212
- join10(drop.fullPath, "dedup.dormant"),
1449
+ writeFileSync9(
1450
+ join12(drop.fullPath, "dedup.dormant"),
1213
1451
  `Merged into ${keep.path} on ${(/* @__PURE__ */ new Date()).toISOString()}`,
1214
1452
  "utf8"
1215
1453
  );
@@ -1239,10 +1477,10 @@ __export(snapshot_exports, {
1239
1477
  gitSnapshot: () => gitSnapshot
1240
1478
  });
1241
1479
  import { execSync } from "child_process";
1242
- import { existsSync as existsSync10 } from "fs";
1243
- import { join as join11 } from "path";
1480
+ import { existsSync as existsSync12 } from "fs";
1481
+ import { join as join13 } from "path";
1244
1482
  function gitSnapshot(brainRoot) {
1245
- if (!existsSync10(join11(brainRoot, ".git"))) {
1483
+ if (!existsSync12(join13(brainRoot, ".git"))) {
1246
1484
  try {
1247
1485
  execSync("git rev-parse --is-inside-work-tree", { cwd: brainRoot, stdio: "pipe" });
1248
1486
  } catch {
@@ -1330,230 +1568,6 @@ var init_watch = __esm({
1330
1568
  }
1331
1569
  });
1332
1570
 
1333
- // src/episode.ts
1334
- var episode_exports = {};
1335
- __export(episode_exports, {
1336
- logEpisode: () => logEpisode,
1337
- readEpisodes: () => readEpisodes
1338
- });
1339
- import { readdirSync as readdirSync7, readFileSync as readFileSync5, writeFileSync as writeFileSync9, mkdirSync as mkdirSync6, existsSync as existsSync11 } from "fs";
1340
- import { join as join12 } from "path";
1341
- function logEpisode(brainRoot, type, path, detail, extra) {
1342
- const logDir = join12(brainRoot, SESSION_LOG_DIR);
1343
- if (!existsSync11(logDir)) {
1344
- mkdirSync6(logDir, { recursive: true });
1345
- }
1346
- const nextSlot = getNextSlot(logDir);
1347
- const episode = {
1348
- ts: (/* @__PURE__ */ new Date()).toISOString(),
1349
- type,
1350
- path,
1351
- detail,
1352
- ...extra?.outcome ? { outcome: extra.outcome } : {},
1353
- ...extra?.neurons ? { neurons: extra.neurons } : {}
1354
- };
1355
- writeFileSync9(
1356
- join12(logDir, `memory${nextSlot}.neuron`),
1357
- JSON.stringify(episode),
1358
- "utf8"
1359
- );
1360
- }
1361
- function readEpisodes(brainRoot) {
1362
- const logDir = join12(brainRoot, SESSION_LOG_DIR);
1363
- if (!existsSync11(logDir)) return [];
1364
- const episodes = [];
1365
- let entries;
1366
- try {
1367
- entries = readdirSync7(logDir);
1368
- } catch {
1369
- return [];
1370
- }
1371
- for (const entry of entries) {
1372
- if (!entry.startsWith("memory") || !entry.endsWith(".neuron")) continue;
1373
- try {
1374
- const content = readFileSync5(join12(logDir, entry), "utf8");
1375
- if (content.trim()) {
1376
- episodes.push(JSON.parse(content));
1377
- }
1378
- } catch {
1379
- }
1380
- }
1381
- episodes.sort((a, b) => a.ts.localeCompare(b.ts));
1382
- return episodes;
1383
- }
1384
- function getNextSlot(logDir) {
1385
- let maxSlot = 0;
1386
- try {
1387
- for (const entry of readdirSync7(logDir)) {
1388
- if (entry.startsWith("memory") && entry.endsWith(".neuron")) {
1389
- const n = parseInt(entry.replace("memory", "").replace(".neuron", ""), 10);
1390
- if (!isNaN(n) && n > maxSlot) maxSlot = n;
1391
- }
1392
- }
1393
- } catch {
1394
- }
1395
- const next = maxSlot + 1;
1396
- return next > MAX_EPISODES ? maxSlot % MAX_EPISODES + 1 : next;
1397
- }
1398
- var MAX_EPISODES, SESSION_LOG_DIR;
1399
- var init_episode = __esm({
1400
- "src/episode.ts"() {
1401
- "use strict";
1402
- MAX_EPISODES = 100;
1403
- SESSION_LOG_DIR = "hippocampus/session_log";
1404
- }
1405
- });
1406
-
1407
- // src/candidates.ts
1408
- var candidates_exports = {};
1409
- __export(candidates_exports, {
1410
- CANDIDATE_DECAY_DAYS: () => CANDIDATE_DECAY_DAYS,
1411
- CANDIDATE_THRESHOLD: () => CANDIDATE_THRESHOLD,
1412
- fromCandidatePath: () => fromCandidatePath,
1413
- growCandidate: () => growCandidate,
1414
- listCandidates: () => listCandidates,
1415
- promoteCandidates: () => promoteCandidates,
1416
- propagateToShared: () => propagateToShared,
1417
- toCandidatePath: () => toCandidatePath
1418
- });
1419
- import { existsSync as existsSync12, mkdirSync as mkdirSync7, readdirSync as readdirSync8, renameSync as renameSync3, rmSync, statSync as statSync4 } from "fs";
1420
- import { join as join13, dirname as dirname3, relative as relative3 } from "path";
1421
- function toCandidatePath(neuronPath) {
1422
- const slash = neuronPath.indexOf("/");
1423
- if (slash === -1) throw new Error(`Invalid neuron path (missing region): ${neuronPath}`);
1424
- return `${neuronPath.slice(0, slash)}/${CANDIDATE_SEGMENT}/${neuronPath.slice(slash + 1)}`;
1425
- }
1426
- function fromCandidatePath(candidatePath) {
1427
- return candidatePath.replace(`/${CANDIDATE_SEGMENT}/`, "/");
1428
- }
1429
- function growCandidate(brainRoot, neuronPath) {
1430
- const candidatePath = toCandidatePath(neuronPath);
1431
- const result = growNeuron(brainRoot, candidatePath);
1432
- if (result.counter >= CANDIDATE_THRESHOLD) {
1433
- const ok = moveCandidate(brainRoot, candidatePath, neuronPath);
1434
- if (ok) propagateToShared(brainRoot, neuronPath);
1435
- return { ...result, path: ok ? neuronPath : result.path, promoted: ok };
1436
- }
1437
- console.log(` \u{1F331} candidate (${result.counter}/${CANDIDATE_THRESHOLD}): ${candidatePath}`);
1438
- return { ...result, promoted: false };
1439
- }
1440
- function moveCandidate(brainRoot, candidatePath, targetPath) {
1441
- const src = join13(brainRoot, candidatePath);
1442
- if (!existsSync12(src)) return false;
1443
- const dst = join13(brainRoot, targetPath);
1444
- if (existsSync12(dst)) {
1445
- fireNeuron(brainRoot, targetPath);
1446
- rmSync(src, { recursive: true, force: true });
1447
- } else {
1448
- mkdirSync7(dirname3(dst), { recursive: true });
1449
- renameSync3(src, dst);
1450
- }
1451
- console.log(`\u{1F393} promoted: ${candidatePath} \u2192 ${targetPath}`);
1452
- return true;
1453
- }
1454
- function promoteCandidates(brainRoot) {
1455
- const promoted = [];
1456
- const decayed = [];
1457
- const decayMs = CANDIDATE_DECAY_DAYS * 24 * 60 * 60 * 1e3;
1458
- const now = Date.now();
1459
- for (const region of REGIONS) {
1460
- const candidateRoot = join13(brainRoot, region, CANDIDATE_SEGMENT);
1461
- walkNeuronDirs(candidateRoot, (neuronDir) => {
1462
- const rel = relative3(join13(brainRoot, region), neuronDir);
1463
- const candidatePath = `${region}/${rel}`;
1464
- const targetPath = fromCandidatePath(candidatePath);
1465
- const counter = readCounter(neuronDir);
1466
- const mtime = statSync4(neuronDir).mtimeMs;
1467
- if (counter >= CANDIDATE_THRESHOLD) {
1468
- moveCandidate(brainRoot, candidatePath, targetPath);
1469
- propagateToShared(brainRoot, targetPath);
1470
- promoted.push(targetPath);
1471
- } else if (now - mtime > decayMs) {
1472
- rmSync(neuronDir, { recursive: true, force: true });
1473
- decayed.push(candidatePath);
1474
- console.log(`\u{1F480} candidate decayed: ${candidatePath}`);
1475
- }
1476
- });
1477
- }
1478
- return { promoted, decayed };
1479
- }
1480
- function listCandidates(brainRoot) {
1481
- const results = [];
1482
- const now = Date.now();
1483
- for (const region of REGIONS) {
1484
- const candidateRoot = join13(brainRoot, region, CANDIDATE_SEGMENT);
1485
- walkNeuronDirs(candidateRoot, (neuronDir) => {
1486
- const rel = relative3(join13(brainRoot, region), neuronDir);
1487
- const candidatePath = `${region}/${rel}`;
1488
- const targetPath = fromCandidatePath(candidatePath);
1489
- const counter = readCounter(neuronDir);
1490
- const mtime = statSync4(neuronDir).mtimeMs;
1491
- const daysInactive = Math.floor((now - mtime) / (24 * 60 * 60 * 1e3));
1492
- results.push({ candidatePath, targetPath, counter, daysInactive });
1493
- });
1494
- }
1495
- return results;
1496
- }
1497
- function walkNeuronDirs(dir, cb) {
1498
- if (!existsSync12(dir)) return;
1499
- try {
1500
- const entries = readdirSync8(dir, { withFileTypes: true });
1501
- const hasNeuron = entries.some((e) => e.isFile() && e.name.endsWith(".neuron"));
1502
- if (hasNeuron) {
1503
- cb(dir);
1504
- return;
1505
- }
1506
- for (const entry of entries) {
1507
- if (entry.isDirectory() && !entry.name.startsWith(".")) {
1508
- walkNeuronDirs(join13(dir, entry.name), cb);
1509
- }
1510
- }
1511
- } catch {
1512
- }
1513
- }
1514
- function readCounter(dir) {
1515
- try {
1516
- const files = readdirSync8(dir).filter((f) => /^\d+\.neuron$/.test(f));
1517
- if (files.length === 0) return 0;
1518
- return Math.max(...files.map((f) => parseInt(f, 10)));
1519
- } catch {
1520
- return 0;
1521
- }
1522
- }
1523
- function propagateToShared(brainRoot, targetPath) {
1524
- try {
1525
- const agentsIdx = brainRoot.indexOf("/agents/");
1526
- if (agentsIdx === -1) return false;
1527
- const multiBrainRoot = brainRoot.slice(0, agentsIdx);
1528
- const sharedRoot = join13(multiBrainRoot, "shared");
1529
- if (!existsSync12(sharedRoot)) return false;
1530
- const episodes = readEpisodes(brainRoot);
1531
- const neuronName = targetPath.split("/").pop() || "";
1532
- const hasRelevantEpisode = episodes.some(
1533
- (ep) => PROPAGATION_EPISODE_TYPES.includes(ep.type) && (ep.path.includes(neuronName) || ep.detail.includes(neuronName))
1534
- );
1535
- if (!hasRelevantEpisode) return false;
1536
- growNeuron(sharedRoot, targetPath);
1537
- console.log(` \u{1F4E1} propagated to shared: ${targetPath}`);
1538
- return true;
1539
- } catch {
1540
- return false;
1541
- }
1542
- }
1543
- var CANDIDATE_THRESHOLD, CANDIDATE_DECAY_DAYS, CANDIDATE_SEGMENT;
1544
- var init_candidates = __esm({
1545
- "src/candidates.ts"() {
1546
- "use strict";
1547
- init_constants();
1548
- init_grow();
1549
- init_fire();
1550
- init_episode();
1551
- CANDIDATE_THRESHOLD = 3;
1552
- CANDIDATE_DECAY_DAYS = 14;
1553
- CANDIDATE_SEGMENT = "_candidates";
1554
- }
1555
- });
1556
-
1557
1571
  // src/inbox.ts
1558
1572
  var inbox_exports = {};
1559
1573
  __export(inbox_exports, {
@@ -2225,7 +2239,8 @@ function digestTranscript(brainRoot, transcriptPath, sessionId) {
2225
2239
  console.log(`\u{1F527} digest: ${toolFailures.length} tool failure(s), ${retries.length} retry pattern(s) logged`);
2226
2240
  }
2227
2241
  const corrections = extractCorrections(messages);
2228
- if (corrections.length === 0 && toolFailures.length === 0) {
2242
+ const fired = autoFireCandidates(brainRoot, corrections);
2243
+ if (corrections.length === 0 && toolFailures.length === 0 && fired === 0) {
2229
2244
  console.log(`\u{1F4DD} digest: no corrections found in session ${resolvedSessionId}`);
2230
2245
  writeAuditLog(brainRoot, resolvedSessionId, [], totalLines);
2231
2246
  return { corrections: 0, skipped: messages.length, toolFailures: toolFailures.length, transcriptPath, sessionId: resolvedSessionId };
@@ -2415,6 +2430,26 @@ function isNarrativeKorean(text) {
2415
2430
  const markerCount = NARRATIVE_MARKERS.filter((p) => p.test(text)).length;
2416
2431
  return markerCount >= 2;
2417
2432
  }
2433
+ function autoFireCandidates(brainRoot, corrections) {
2434
+ if (corrections.length > 0) return 0;
2435
+ const candidates = listCandidates(brainRoot);
2436
+ if (candidates.length === 0) return 0;
2437
+ let fired = 0;
2438
+ for (const cand of candidates) {
2439
+ try {
2440
+ const newCounter = fireNeuron(brainRoot, cand.candidatePath);
2441
+ fired++;
2442
+ if (newCounter >= CANDIDATE_THRESHOLD) {
2443
+ growCandidate(brainRoot, cand.targetPath);
2444
+ }
2445
+ } catch {
2446
+ }
2447
+ }
2448
+ if (fired > 0) {
2449
+ console.log(`\u{1F504} agent-evaluator: ${fired} candidate(s) advanced (session without corrections)`);
2450
+ }
2451
+ return fired;
2452
+ }
2418
2453
  function detectCorrection(text) {
2419
2454
  const isNegation = NEGATION_PATTERNS.some((p) => p.test(text));
2420
2455
  const isMust = MUST_PATTERNS.some((p) => p.test(text));
@@ -2616,6 +2651,7 @@ var init_digest = __esm({
2616
2651
  "use strict";
2617
2652
  init_constants();
2618
2653
  init_candidates();
2654
+ init_fire();
2619
2655
  init_episode();
2620
2656
  NEGATION_PATTERNS = [
2621
2657
  /\bdon[''\u2019]?t\b/i,