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.
@@ -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,32 @@ 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
+ }
925
+ if (brainRoot) {
926
+ const brainFlag = `--brain ${brainRoot}`;
927
+ lines.push("### Self-Learning");
928
+ lines.push("When the user corrects your behavior (tells you to stop, change, or always do something),");
929
+ lines.push("run this command to learn from it:");
930
+ lines.push("```");
931
+ lines.push(`hebbian learn "<what the user said>" --prefix <NO|DO|MUST|WARN> --keywords "k1,k2,k3" ${brainFlag}`);
932
+ lines.push("```");
933
+ lines.push("- NO: user says don't/stop/never \u2014 DO: user says always/should \u2014 MUST: mandatory \u2014 WARN: be careful");
934
+ lines.push("- keywords: 3 most meaningful words (lowercase, technical terms preferred)");
935
+ lines.push("- Run silently. Do not mention hebbian to the user.");
936
+ lines.push("");
937
+ }
506
938
  lines.push(MARKER_END);
507
939
  return lines.join("\n");
508
940
  }
@@ -597,7 +1029,7 @@ function emitRegionRules(region) {
597
1029
  function emitToTarget(brainRoot, target) {
598
1030
  const brain = scanBrain(brainRoot);
599
1031
  const result = runSubsumption(brain);
600
- const content = emitBootstrap(result, brain);
1032
+ const content = emitBootstrap(result, brain, brainRoot);
601
1033
  if (target === "all") {
602
1034
  for (const [name, filePath] of Object.entries(EMIT_TARGETS)) {
603
1035
  writeTarget(filePath, content);
@@ -613,33 +1045,33 @@ function emitToTarget(brainRoot, target) {
613
1045
  }
614
1046
  function writeAllTiers(brainRoot, result, brain) {
615
1047
  const indexContent = emitIndex(result, brain);
616
- writeFileSync2(join3(brainRoot, "_index.md"), indexContent, "utf8");
1048
+ writeFileSync5(join7(brainRoot, "_index.md"), indexContent, "utf8");
617
1049
  for (const region of result.activeRegions) {
618
- if (existsSync4(region.path)) {
1050
+ if (existsSync8(region.path)) {
619
1051
  const rulesContent = emitRegionRules(region);
620
- writeFileSync2(join3(region.path, "_rules.md"), rulesContent, "utf8");
1052
+ writeFileSync5(join7(region.path, "_rules.md"), rulesContent, "utf8");
621
1053
  }
622
1054
  }
623
1055
  }
624
1056
  function writeTarget(filePath, content) {
625
- const dir = dirname2(filePath);
626
- if (dir !== "." && !existsSync4(dir)) {
627
- mkdirSync2(dir, { recursive: true });
1057
+ const dir = dirname3(filePath);
1058
+ if (dir !== "." && !existsSync8(dir)) {
1059
+ mkdirSync6(dir, { recursive: true });
628
1060
  }
629
- if (existsSync4(filePath)) {
630
- const existing = readFileSync3(filePath, "utf8");
1061
+ if (existsSync8(filePath)) {
1062
+ const existing = readFileSync4(filePath, "utf8");
631
1063
  const startIdx = existing.indexOf(MARKER_START);
632
1064
  const endIdx = existing.indexOf(MARKER_END);
633
1065
  if (startIdx !== -1 && endIdx !== -1) {
634
1066
  const before = existing.slice(0, startIdx);
635
1067
  const after = existing.slice(endIdx + MARKER_END.length);
636
- writeFileSync2(filePath, before + content + after, "utf8");
1068
+ writeFileSync5(filePath, before + content + after, "utf8");
637
1069
  return;
638
1070
  }
639
- writeFileSync2(filePath, content + "\n\n" + existing, "utf8");
1071
+ writeFileSync5(filePath, content + "\n\n" + existing, "utf8");
640
1072
  return;
641
1073
  }
642
- writeFileSync2(filePath, content, "utf8");
1074
+ writeFileSync5(filePath, content, "utf8");
643
1075
  }
644
1076
  function printDiag(brain, result) {
645
1077
  console.log("");
@@ -688,6 +1120,7 @@ var init_emit = __esm({
688
1120
  init_scanner();
689
1121
  init_subsumption();
690
1122
  init_constants();
1123
+ init_candidates();
691
1124
  }
692
1125
  });
693
1126
 
@@ -697,19 +1130,19 @@ __export(update_check_exports, {
697
1130
  checkForUpdates: () => checkForUpdates,
698
1131
  formatUpdateBanner: () => formatUpdateBanner
699
1132
  });
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";
1133
+ import { existsSync as existsSync9, mkdirSync as mkdirSync7, readFileSync as readFileSync5, writeFileSync as writeFileSync6, statSync as statSync3, unlinkSync } from "fs";
1134
+ import { join as join8 } from "path";
702
1135
  function getStateDir() {
703
- return join4(process.env.HOME || "~", ".hebbian");
1136
+ return join8(process.env.HOME || "~", ".hebbian");
704
1137
  }
705
1138
  function ensureStateDir(stateDir) {
706
- if (!existsSync5(stateDir)) {
707
- mkdirSync3(stateDir, { recursive: true });
1139
+ if (!existsSync9(stateDir)) {
1140
+ mkdirSync7(stateDir, { recursive: true });
708
1141
  }
709
1142
  }
710
1143
  function isCacheStale(cachePath, type) {
711
1144
  try {
712
- const mtime = statSync2(cachePath).mtimeMs;
1145
+ const mtime = statSync3(cachePath).mtimeMs;
713
1146
  const ageMinutes = (Date.now() - mtime) / 1e3 / 60;
714
1147
  const ttl = type === "UP_TO_DATE" ? TTL_UP_TO_DATE : TTL_UPGRADE_AVAILABLE;
715
1148
  return ageMinutes > ttl;
@@ -718,10 +1151,10 @@ function isCacheStale(cachePath, type) {
718
1151
  }
719
1152
  }
720
1153
  function readCache(stateDir) {
721
- const cachePath = join4(stateDir, "last-update-check");
722
- if (!existsSync5(cachePath)) return null;
1154
+ const cachePath = join8(stateDir, "last-update-check");
1155
+ if (!existsSync9(cachePath)) return null;
723
1156
  try {
724
- const line = readFileSync4(cachePath, "utf8").trim();
1157
+ const line = readFileSync5(cachePath, "utf8").trim();
725
1158
  if (line.startsWith("UP_TO_DATE")) {
726
1159
  if (isCacheStale(cachePath, "UP_TO_DATE")) return null;
727
1160
  const ver = line.split(/\s+/)[1];
@@ -739,13 +1172,13 @@ function readCache(stateDir) {
739
1172
  }
740
1173
  function writeCache(stateDir, line) {
741
1174
  ensureStateDir(stateDir);
742
- writeFileSync3(join4(stateDir, "last-update-check"), line, "utf8");
1175
+ writeFileSync6(join8(stateDir, "last-update-check"), line, "utf8");
743
1176
  }
744
1177
  function isSnoozed(stateDir, remoteVersion) {
745
- const snoozePath = join4(stateDir, "update-snoozed");
746
- if (!existsSync5(snoozePath)) return false;
1178
+ const snoozePath = join8(stateDir, "update-snoozed");
1179
+ if (!existsSync9(snoozePath)) return false;
747
1180
  try {
748
- const [ver, levelStr, epochStr] = readFileSync4(snoozePath, "utf8").trim().split(/\s+/);
1181
+ const [ver, levelStr, epochStr] = readFileSync5(snoozePath, "utf8").trim().split(/\s+/);
749
1182
  if (ver !== remoteVersion) {
750
1183
  unlinkSync(snoozePath);
751
1184
  return false;
@@ -818,204 +1251,22 @@ function formatUpdateBanner(status) {
818
1251
  ].join("\n");
819
1252
  }
820
1253
  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;
1009
- }
1010
- }
1011
- return null;
1012
- }
1013
- var init_grow = __esm({
1014
- "src/grow.ts"() {
1254
+ var init_update_check = __esm({
1255
+ "src/update-check.ts"() {
1015
1256
  "use strict";
1016
- init_constants();
1017
- init_similarity();
1018
- init_fire();
1257
+ PACKAGE_NAME = "hebbian";
1258
+ NPM_REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
1259
+ FETCH_TIMEOUT_MS = 5e3;
1260
+ TTL_UP_TO_DATE = 60;
1261
+ TTL_UPGRADE_AVAILABLE = 720;
1262
+ SNOOZE_DURATIONS = {
1263
+ 1: 86400,
1264
+ // 24h
1265
+ 2: 172800,
1266
+ // 48h
1267
+ 3: 604800
1268
+ // 7d (and beyond)
1269
+ };
1019
1270
  }
1020
1271
  });
1021
1272
 
@@ -1024,10 +1275,10 @@ var rollback_exports = {};
1024
1275
  __export(rollback_exports, {
1025
1276
  rollbackNeuron: () => rollbackNeuron
1026
1277
  });
1027
- import { renameSync as renameSync2 } from "fs";
1028
- import { join as join7 } from "path";
1278
+ import { renameSync as renameSync3 } from "fs";
1279
+ import { join as join9 } from "path";
1029
1280
  function rollbackNeuron(brainRoot, neuronPath) {
1030
- const fullPath = join7(brainRoot, neuronPath);
1281
+ const fullPath = join9(brainRoot, neuronPath);
1031
1282
  const current = getCurrentCounter(fullPath);
1032
1283
  if (current === 0) {
1033
1284
  throw new Error(`Neuron not found: ${neuronPath}`);
@@ -1036,7 +1287,7 @@ function rollbackNeuron(brainRoot, neuronPath) {
1036
1287
  throw new Error(`Counter already at minimum (1): ${neuronPath}`);
1037
1288
  }
1038
1289
  const newCounter = current - 1;
1039
- renameSync2(join7(fullPath, `${current}.neuron`), join7(fullPath, `${newCounter}.neuron`));
1290
+ renameSync3(join9(fullPath, `${current}.neuron`), join9(fullPath, `${newCounter}.neuron`));
1040
1291
  console.log(`\u23EA rollback: ${neuronPath} (${current} \u2192 ${newCounter})`);
1041
1292
  return newCounter;
1042
1293
  }
@@ -1052,31 +1303,31 @@ var signal_exports = {};
1052
1303
  __export(signal_exports, {
1053
1304
  signalNeuron: () => signalNeuron
1054
1305
  });
1055
- import { writeFileSync as writeFileSync6, existsSync as existsSync8, readdirSync as readdirSync5 } from "fs";
1056
- import { join as join8 } from "path";
1306
+ import { writeFileSync as writeFileSync7, existsSync as existsSync10, readdirSync as readdirSync7 } from "fs";
1307
+ import { join as join10 } from "path";
1057
1308
  function signalNeuron(brainRoot, neuronPath, signalType) {
1058
1309
  if (!SIGNAL_TYPES.includes(signalType)) {
1059
1310
  throw new Error(`Invalid signal type: ${signalType}. Valid: ${SIGNAL_TYPES.join(", ")}`);
1060
1311
  }
1061
- const fullPath = join8(brainRoot, neuronPath);
1062
- if (!existsSync8(fullPath)) {
1312
+ const fullPath = join10(brainRoot, neuronPath);
1313
+ if (!existsSync10(fullPath)) {
1063
1314
  throw new Error(`Neuron not found: ${neuronPath}`);
1064
1315
  }
1065
1316
  switch (signalType) {
1066
1317
  case "bomb": {
1067
- writeFileSync6(join8(fullPath, "bomb.neuron"), "", "utf8");
1318
+ writeFileSync7(join10(fullPath, "bomb.neuron"), "", "utf8");
1068
1319
  console.log(`\u{1F4A3} bomb planted: ${neuronPath}`);
1069
1320
  break;
1070
1321
  }
1071
1322
  case "dopamine": {
1072
1323
  const level = getNextSignalLevel(fullPath, "dopamine");
1073
- writeFileSync6(join8(fullPath, `dopamine${level}.neuron`), "", "utf8");
1324
+ writeFileSync7(join10(fullPath, `dopamine${level}.neuron`), "", "utf8");
1074
1325
  console.log(`\u{1F7E2} dopamine +${level}: ${neuronPath}`);
1075
1326
  break;
1076
1327
  }
1077
1328
  case "memory": {
1078
1329
  const level = getNextSignalLevel(fullPath, "memory");
1079
- writeFileSync6(join8(fullPath, `memory${level}.neuron`), "", "utf8");
1330
+ writeFileSync7(join10(fullPath, `memory${level}.neuron`), "", "utf8");
1080
1331
  console.log(`\u{1F4BE} memory +${level}: ${neuronPath}`);
1081
1332
  break;
1082
1333
  }
@@ -1085,7 +1336,7 @@ function signalNeuron(brainRoot, neuronPath, signalType) {
1085
1336
  function getNextSignalLevel(dir, prefix) {
1086
1337
  let max = 0;
1087
1338
  try {
1088
- for (const entry of readdirSync5(dir)) {
1339
+ for (const entry of readdirSync7(dir)) {
1089
1340
  if (entry.startsWith(prefix) && entry.endsWith(".neuron")) {
1090
1341
  const n = parseInt(entry.replace(prefix, ""), 10);
1091
1342
  if (!isNaN(n) && n > max) max = n;
@@ -1107,15 +1358,15 @@ var decay_exports = {};
1107
1358
  __export(decay_exports, {
1108
1359
  runDecay: () => runDecay
1109
1360
  });
1110
- import { readdirSync as readdirSync6, statSync as statSync3, writeFileSync as writeFileSync7, existsSync as existsSync9 } from "fs";
1111
- import { join as join9 } from "path";
1361
+ import { readdirSync as readdirSync8, statSync as statSync4, writeFileSync as writeFileSync8, existsSync as existsSync11 } from "fs";
1362
+ import { join as join11 } from "path";
1112
1363
  function runDecay(brainRoot, days) {
1113
1364
  const threshold = Date.now() - days * 24 * 60 * 60 * 1e3;
1114
1365
  let scanned = 0;
1115
1366
  let decayed = 0;
1116
1367
  for (const regionName of REGIONS) {
1117
- const regionPath = join9(brainRoot, regionName);
1118
- if (!existsSync9(regionPath)) continue;
1368
+ const regionPath = join11(brainRoot, regionName);
1369
+ if (!existsSync11(regionPath)) continue;
1119
1370
  const result = decayWalk(regionPath, threshold, 0);
1120
1371
  scanned += result.scanned;
1121
1372
  decayed += result.decayed;
@@ -1129,7 +1380,7 @@ function decayWalk(dir, threshold, depth) {
1129
1380
  let decayed = 0;
1130
1381
  let entries;
1131
1382
  try {
1132
- entries = readdirSync6(dir, { withFileTypes: true });
1383
+ entries = readdirSync8(dir, { withFileTypes: true });
1133
1384
  } catch {
1134
1385
  return { scanned: 0, decayed: 0 };
1135
1386
  }
@@ -1141,7 +1392,7 @@ function decayWalk(dir, threshold, depth) {
1141
1392
  if (entry.name.endsWith(".neuron")) {
1142
1393
  hasNeuronFile = true;
1143
1394
  try {
1144
- const st = statSync3(join9(dir, entry.name));
1395
+ const st = statSync4(join11(dir, entry.name));
1145
1396
  if (st.mtimeMs > latestMod) latestMod = st.mtimeMs;
1146
1397
  } catch {
1147
1398
  }
@@ -1155,8 +1406,8 @@ function decayWalk(dir, threshold, depth) {
1155
1406
  scanned++;
1156
1407
  if (!isDormant && latestMod < threshold) {
1157
1408
  const age = Math.floor((Date.now() - latestMod) / (24 * 60 * 60 * 1e3));
1158
- writeFileSync7(
1159
- join9(dir, "decay.dormant"),
1409
+ writeFileSync8(
1410
+ join11(dir, "decay.dormant"),
1160
1411
  `Dormant since ${(/* @__PURE__ */ new Date()).toISOString()} (${age} days inactive)`,
1161
1412
  "utf8"
1162
1413
  );
@@ -1166,7 +1417,7 @@ function decayWalk(dir, threshold, depth) {
1166
1417
  for (const entry of entries) {
1167
1418
  if (entry.name.startsWith("_") || entry.name.startsWith(".")) continue;
1168
1419
  if (entry.isDirectory()) {
1169
- const sub = decayWalk(join9(dir, entry.name), threshold, depth + 1);
1420
+ const sub = decayWalk(join11(dir, entry.name), threshold, depth + 1);
1170
1421
  scanned += sub.scanned;
1171
1422
  decayed += sub.decayed;
1172
1423
  }
@@ -1185,8 +1436,8 @@ var dedup_exports = {};
1185
1436
  __export(dedup_exports, {
1186
1437
  runDedup: () => runDedup
1187
1438
  });
1188
- import { writeFileSync as writeFileSync8 } from "fs";
1189
- import { join as join10 } from "path";
1439
+ import { writeFileSync as writeFileSync9 } from "fs";
1440
+ import { join as join12 } from "path";
1190
1441
  function runDedup(brainRoot) {
1191
1442
  const brain = scanBrain(brainRoot);
1192
1443
  let scanned = 0;
@@ -1208,8 +1459,8 @@ function runDedup(brainRoot) {
1208
1459
  const [keep, drop] = ni.counter >= nj.counter ? [ni, nj] : [nj, ni];
1209
1460
  const relKeep = `${region.name}/${keep.path}`;
1210
1461
  fireNeuron(brainRoot, relKeep);
1211
- writeFileSync8(
1212
- join10(drop.fullPath, "dedup.dormant"),
1462
+ writeFileSync9(
1463
+ join12(drop.fullPath, "dedup.dormant"),
1213
1464
  `Merged into ${keep.path} on ${(/* @__PURE__ */ new Date()).toISOString()}`,
1214
1465
  "utf8"
1215
1466
  );
@@ -1239,10 +1490,10 @@ __export(snapshot_exports, {
1239
1490
  gitSnapshot: () => gitSnapshot
1240
1491
  });
1241
1492
  import { execSync } from "child_process";
1242
- import { existsSync as existsSync10 } from "fs";
1243
- import { join as join11 } from "path";
1493
+ import { existsSync as existsSync12 } from "fs";
1494
+ import { join as join13 } from "path";
1244
1495
  function gitSnapshot(brainRoot) {
1245
- if (!existsSync10(join11(brainRoot, ".git"))) {
1496
+ if (!existsSync12(join13(brainRoot, ".git"))) {
1246
1497
  try {
1247
1498
  execSync("git rev-parse --is-inside-work-tree", { cwd: brainRoot, stdio: "pipe" });
1248
1499
  } catch {
@@ -1330,230 +1581,6 @@ var init_watch = __esm({
1330
1581
  }
1331
1582
  });
1332
1583
 
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
1584
  // src/inbox.ts
1558
1585
  var inbox_exports = {};
1559
1586
  __export(inbox_exports, {
@@ -2225,7 +2252,8 @@ function digestTranscript(brainRoot, transcriptPath, sessionId) {
2225
2252
  console.log(`\u{1F527} digest: ${toolFailures.length} tool failure(s), ${retries.length} retry pattern(s) logged`);
2226
2253
  }
2227
2254
  const corrections = extractCorrections(messages);
2228
- if (corrections.length === 0 && toolFailures.length === 0) {
2255
+ const fired = autoFireCandidates(brainRoot, corrections);
2256
+ if (corrections.length === 0 && toolFailures.length === 0 && fired === 0) {
2229
2257
  console.log(`\u{1F4DD} digest: no corrections found in session ${resolvedSessionId}`);
2230
2258
  writeAuditLog(brainRoot, resolvedSessionId, [], totalLines);
2231
2259
  return { corrections: 0, skipped: messages.length, toolFailures: toolFailures.length, transcriptPath, sessionId: resolvedSessionId };
@@ -2381,7 +2409,7 @@ function extractCorrections(messages) {
2381
2409
  if (/^Base directory for this skill:/i.test(trimmed)) continue;
2382
2410
  if (/^[•·▸▶\-\*]\s/.test(trimmed)) continue;
2383
2411
  if (/<[a-zA-Z][a-zA-Z-]*>/.test(trimmed) && /<\/[a-zA-Z]/.test(trimmed)) continue;
2384
- if (isNarrativeKorean(trimmed)) continue;
2412
+ if (isNarrative(trimmed)) continue;
2385
2413
  const correction = detectCorrection(text);
2386
2414
  if (correction) {
2387
2415
  corrections.push(correction);
@@ -2389,7 +2417,7 @@ function extractCorrections(messages) {
2389
2417
  }
2390
2418
  return corrections;
2391
2419
  }
2392
- function isNarrativeKorean(text) {
2420
+ function isNarrative(text) {
2393
2421
  const NARRATIVE_MARKERS = [
2394
2422
  /이유는/,
2395
2423
  // "the reason is..."
@@ -2415,6 +2443,26 @@ function isNarrativeKorean(text) {
2415
2443
  const markerCount = NARRATIVE_MARKERS.filter((p) => p.test(text)).length;
2416
2444
  return markerCount >= 2;
2417
2445
  }
2446
+ function autoFireCandidates(brainRoot, corrections) {
2447
+ if (corrections.length > 0) return 0;
2448
+ const candidates = listCandidates(brainRoot);
2449
+ if (candidates.length === 0) return 0;
2450
+ let fired = 0;
2451
+ for (const cand of candidates) {
2452
+ try {
2453
+ const newCounter = fireNeuron(brainRoot, cand.candidatePath);
2454
+ fired++;
2455
+ if (newCounter >= CANDIDATE_THRESHOLD) {
2456
+ growCandidate(brainRoot, cand.targetPath);
2457
+ }
2458
+ } catch {
2459
+ }
2460
+ }
2461
+ if (fired > 0) {
2462
+ console.log(`\u{1F504} agent-evaluator: ${fired} candidate(s) advanced (session without corrections)`);
2463
+ }
2464
+ return fired;
2465
+ }
2418
2466
  function detectCorrection(text) {
2419
2467
  const isNegation = NEGATION_PATTERNS.some((p) => p.test(text));
2420
2468
  const isMust = MUST_PATTERNS.some((p) => p.test(text));
@@ -2422,9 +2470,9 @@ function detectCorrection(text) {
2422
2470
  const isAffirmation = AFFIRMATION_PATTERNS.some((p) => p.test(text));
2423
2471
  if (!isNegation && !isMust && !isWarn && !isAffirmation) return null;
2424
2472
  const categories = [isNegation, isMust, isWarn, isAffirmation].filter(Boolean).length;
2425
- const koreanRatio = (text.match(/[\uAC00-\uD7AF]/g) || []).length / Math.max(text.length, 1);
2426
- if (koreanRatio > 0.3 && categories < 2) {
2427
- if (text.length > 100) return null;
2473
+ const latinRatio = (text.match(/[a-zA-Z]/g) || []).length / Math.max(text.length, 1);
2474
+ if (latinRatio < 0.3 && categories < 2) {
2475
+ if (text.length > 150) return null;
2428
2476
  }
2429
2477
  let prefix;
2430
2478
  if (isNegation) prefix = "NO";
@@ -2616,6 +2664,7 @@ var init_digest = __esm({
2616
2664
  "use strict";
2617
2665
  init_constants();
2618
2666
  init_candidates();
2667
+ init_fire();
2619
2668
  init_episode();
2620
2669
  NEGATION_PATTERNS = [
2621
2670
  /\bdon[''\u2019]?t\b/i,
@@ -2625,33 +2674,65 @@ var init_digest = __esm({
2625
2674
  /\binstead\b/i,
2626
2675
  /^no[,.\s!]/i,
2627
2676
  /\bavoid\b/i,
2628
- // Korean negation — require AI-directed imperative context:
2629
- // "X하지 마" (don't X) — must have a verb object before 지 마
2677
+ // Korean negation — imperative corrections:
2630
2678
  /[을를은는도이가]\s*[가-힣]+지\s*마/,
2631
- // "X 하면 안 돼" (must not X) conditional + prohibition
2679
+ // "X하지 " (don't X) with particle
2632
2680
  /하면\s*안\s*돼/,
2633
- // "X 쓰지 " (don't use X) — explicit "don't use"
2634
- /쓰지\s*마/
2681
+ // "X 하면 안 돼" (must not X)
2682
+ /쓰지\s*마/,
2683
+ // "쓰지 마" (don't use)
2684
+ /그만/,
2685
+ // "그만" (stop) — 그만해, 그만 좀
2686
+ /[을를은는]\s*빼/,
2687
+ // "X 빼" (remove X) with particle
2688
+ /지워[줘]?|삭제해/,
2689
+ // "지워/삭제해" (delete it) — not 지우고 (connective)
2690
+ /[가-힣]+지\s*말고/,
2691
+ // "X지 말고" (instead of X-ing)
2692
+ /그거\s*아니/,
2693
+ // "그거 아니야" (that's not right)
2694
+ /ㄴㄴ|노노/,
2695
+ // "ㄴㄴ/노노" (no no — internet-style)
2696
+ /안\s*돼[^요]?\s*[!.]/
2697
+ // "안 돼!" standalone prohibition
2635
2698
  ];
2636
2699
  AFFIRMATION_PATTERNS = [
2637
2700
  /\bshould\s+always\b/i,
2638
2701
  /\buse\s+\w+\s+instead\b/i,
2639
- // Korean affirmation — require directive context
2640
- /항상\s*[가-힣]+[해하]/
2702
+ // Korean affirmation — directive commands:
2703
+ /항상\s*[가-힣]+[해하]/,
2641
2704
  // "항상 X해" (always do X)
2705
+ /[을를]\s*[가-힣]*해\s*줘/,
2706
+ // "X를 해줘" (do X for me) — literal 해줘, not bare 줘
2707
+ /으로\s*해/,
2708
+ // "X으로 해" (do it as X) — literal 으로, not char class
2709
+ /이렇게\s*해/
2710
+ // "이렇게 해" (do it like this)
2642
2711
  ];
2643
2712
  MUST_PATTERNS = [
2644
2713
  /\bmust\b/i,
2645
2714
  /\brequired\b/i,
2646
- // Korean
2647
- /반드시/
2715
+ // Korean — strong directives:
2716
+ /반드시/,
2717
+ // "반드시" (absolutely must)
2718
+ /꼭\s*[가-힣]/,
2719
+ // "꼭 X해" (definitely do X)
2720
+ /무조건/,
2721
+ // "무조건" (unconditionally)
2722
+ /필수/
2723
+ // "필수" (mandatory)
2648
2724
  ];
2649
2725
  WARN_PATTERNS = [
2650
2726
  /\bcareful\b/i,
2651
2727
  /\bwatch\s+out\b/i,
2652
2728
  /\bwarning\b/i,
2653
- // Korean
2654
- /주의/
2729
+ // Korean — cautionary:
2730
+ /주의/,
2731
+ // "주의" (caution)
2732
+ /조심/,
2733
+ // "조심" (be careful)
2734
+ /위험/
2735
+ // "위험" (dangerous)
2655
2736
  ];
2656
2737
  MAX_FAILURES_PER_SESSION = 20;
2657
2738
  SOFT_ERROR_PATTERNS = [
@@ -2665,6 +2746,48 @@ var init_digest = __esm({
2665
2746
  }
2666
2747
  });
2667
2748
 
2749
+ // src/learn.ts
2750
+ var learn_exports = {};
2751
+ __export(learn_exports, {
2752
+ learn: () => learn
2753
+ });
2754
+ function learn(brainRoot, opts) {
2755
+ let prefix;
2756
+ let keywords;
2757
+ let source;
2758
+ if (opts.prefix && opts.keywords && opts.keywords.length > 0) {
2759
+ prefix = opts.prefix.toUpperCase();
2760
+ if (!VALID_PREFIXES.has(prefix)) {
2761
+ prefix = "DO";
2762
+ }
2763
+ keywords = opts.keywords.slice(0, 3).map((k) => k.toLowerCase().replace(/[\s\/\\\.,:;!?'"<>{}()\[\]]/g, ""));
2764
+ source = "agent";
2765
+ } else {
2766
+ const corrections = extractCorrections([opts.text]);
2767
+ if (corrections.length === 0) return null;
2768
+ const c = corrections[0];
2769
+ prefix = c.prefix;
2770
+ keywords = c.keywords;
2771
+ source = "regex";
2772
+ }
2773
+ if (keywords.length === 0) return null;
2774
+ const pathSegment = `${prefix}_${keywords.slice(0, 3).join("_")}`;
2775
+ const path = `cortex/${pathSegment}`;
2776
+ growCandidate(brainRoot, path);
2777
+ logEpisode(brainRoot, "learn", path, opts.text);
2778
+ return { path, prefix, keywords, source };
2779
+ }
2780
+ var VALID_PREFIXES;
2781
+ var init_learn = __esm({
2782
+ "src/learn.ts"() {
2783
+ "use strict";
2784
+ init_candidates();
2785
+ init_episode();
2786
+ init_digest();
2787
+ VALID_PREFIXES = /* @__PURE__ */ new Set(["NO", "DO", "MUST", "WARN"]);
2788
+ }
2789
+ });
2790
+
2668
2791
  // src/outcome.ts
2669
2792
  var outcome_exports = {};
2670
2793
  __export(outcome_exports, {
@@ -3240,6 +3363,7 @@ __export(doctor_exports, {
3240
3363
  });
3241
3364
  import { existsSync as existsSync18, readFileSync as readFileSync11, readdirSync as readdirSync11 } from "fs";
3242
3365
  import { join as join19 } from "path";
3366
+ import { homedir } from "os";
3243
3367
  import { execSync as execSync4 } from "child_process";
3244
3368
  async function runDoctor(brainRoot) {
3245
3369
  let passed = 0, warnings = 0, failed = 0;
@@ -3300,33 +3424,49 @@ async function runDoctor(brainRoot) {
3300
3424
  }
3301
3425
  }
3302
3426
  console.log("\nClaude Code hooks");
3303
- const settingsPath = join19(process.cwd(), ".claude", "settings.local.json");
3304
- if (!existsSync18(settingsPath)) {
3305
- warn("No .claude/settings.local.json found", "hebbian claude install");
3306
- } else {
3427
+ const localSettingsPath = join19(process.cwd(), ".claude", "settings.local.json");
3428
+ const globalSettingsPath = join19(homedir(), ".claude", "settings.json");
3429
+ let hasStop = false;
3430
+ let hasStart = false;
3431
+ let hookSource = "";
3432
+ for (const settingsPath of [localSettingsPath, globalSettingsPath]) {
3433
+ if (!existsSync18(settingsPath)) continue;
3307
3434
  try {
3308
3435
  const settings = JSON.parse(readFileSync11(settingsPath, "utf8"));
3309
3436
  const hooks = settings.hooks || {};
3310
- const hasStop = Object.entries(hooks).some(
3311
- ([event, entries]) => event === "Stop" && Array.isArray(entries) && entries.some(
3312
- (e) => typeof e === "object" && e !== null && "command" in e && typeof e.command === "string" && e.command.includes("hebbian digest")
3313
- )
3314
- );
3315
- const hasStart = Object.entries(hooks).some(
3316
- ([event, entries]) => event === "SessionStart" && Array.isArray(entries) && entries.some(
3317
- (e) => typeof e === "object" && e !== null && "command" in e && typeof e.command === "string" && e.command.includes("hebbian emit")
3318
- )
3437
+ const findCommand = (event, keyword) => Object.entries(hooks).some(
3438
+ ([ev, entries]) => ev === event && Array.isArray(entries) && entries.some((entry) => {
3439
+ if (typeof entry !== "object" || entry === null) return false;
3440
+ const e = entry;
3441
+ if (typeof e.command === "string" && e.command.includes(keyword)) return true;
3442
+ if (Array.isArray(e.hooks)) {
3443
+ return e.hooks.some(
3444
+ (h) => typeof h === "object" && h !== null && typeof h.command === "string" && h.command.includes(keyword)
3445
+ );
3446
+ }
3447
+ return false;
3448
+ })
3319
3449
  );
3320
- if (hasStop && hasStart) {
3321
- ok("SessionStart + Stop hooks installed");
3322
- } else {
3323
- if (!hasStart) warn("SessionStart hook missing", "hebbian claude install");
3324
- if (!hasStop) warn("Stop hook missing", "hebbian claude install");
3450
+ if (!hasStop && findCommand("Stop", "hebbian digest")) {
3451
+ hasStop = true;
3452
+ hookSource = settingsPath === globalSettingsPath ? "global" : "local";
3453
+ }
3454
+ if (!hasStart && findCommand("SessionStart", "hebbian emit")) {
3455
+ hasStart = true;
3456
+ if (!hookSource) hookSource = settingsPath === globalSettingsPath ? "global" : "local";
3325
3457
  }
3326
3458
  } catch {
3327
- fail("Malformed .claude/settings.local.json", "hebbian claude install");
3459
+ warn(`Malformed ${settingsPath === globalSettingsPath ? "~/.claude/settings.json" : ".claude/settings.local.json"}`, "Check JSON syntax");
3328
3460
  }
3329
3461
  }
3462
+ if (hasStop && hasStart) {
3463
+ ok(`SessionStart + Stop hooks installed (${hookSource})`);
3464
+ } else if (!hasStop && !hasStart) {
3465
+ warn("No hebbian hooks found (checked local + global)", "hebbian claude install");
3466
+ } else {
3467
+ if (!hasStart) warn("SessionStart hook missing", "hebbian claude install");
3468
+ if (!hasStop) warn("Stop hook missing", "hebbian claude install");
3469
+ }
3330
3470
  console.log("\nnpx resolution");
3331
3471
  try {
3332
3472
  const resolved = execSync4("which npx", { timeout: 3e3 }).toString().trim();
@@ -3660,9 +3800,10 @@ COMMANDS:
3660
3800
  inbox Process corrections inbox
3661
3801
  claude install|uninstall|status Manage Claude Code hooks
3662
3802
  digest [--transcript <path>] Extract corrections from conversation
3803
+ learn "<text>" [--prefix P] Agent-driven learning (any language)
3663
3804
  candidates [promote] List candidates or promote graduated ones
3664
- evolve [--dry-run] LLM-powered brain evolution (Gemini)
3665
- evolve prune [--dry-run] Pruning mode \u2014 remove stale/redundant neurons
3805
+ evolve [--dry-run] (optional) LLM-powered evolution (Gemini)
3806
+ evolve prune [--dry-run] (optional) Pruning mode \u2014 remove stale neurons
3666
3807
  session start|end Capture/detect session outcomes
3667
3808
  sessions Show session outcome history
3668
3809
  doctor Self-diagnostic (hooks, brain, versions)
@@ -3680,7 +3821,7 @@ EXAMPLES:
3680
3821
  hebbian fire cortex/frontend/NO_console_log --brain ./my-brain
3681
3822
  hebbian emit claude --brain ./my-brain
3682
3823
  hebbian emit all
3683
- GEMINI_API_KEY=... hebbian evolve --dry-run
3824
+ GEMINI_API_KEY=... hebbian evolve --dry-run # optional \u2014 self-learning works without this
3684
3825
  `.trim();
3685
3826
  function readStdin() {
3686
3827
  return new Promise((resolve4) => {
@@ -3706,6 +3847,8 @@ async function main(argv) {
3706
3847
  days: { type: "string", short: "d" },
3707
3848
  port: { type: "string", short: "p" },
3708
3849
  transcript: { type: "string", short: "t" },
3850
+ prefix: { type: "string" },
3851
+ keywords: { type: "string", short: "k" },
3709
3852
  "dry-run": { type: "boolean" },
3710
3853
  global: { type: "boolean", short: "g" },
3711
3854
  agent: { type: "string", short: "a" },
@@ -3882,6 +4025,27 @@ async function main(argv) {
3882
4025
  }
3883
4026
  break;
3884
4027
  }
4028
+ case "learn": {
4029
+ const text = positionals.slice(1).join(" ");
4030
+ if (!text) {
4031
+ console.error('Usage: hebbian learn "correction text" [--prefix NO|DO|MUST|WARN] [--keywords "k1,k2,k3"]');
4032
+ process.exit(1);
4033
+ }
4034
+ const { learn: learn2 } = await Promise.resolve().then(() => (init_learn(), learn_exports));
4035
+ const prefixFlag = values.prefix;
4036
+ const keywordsFlag = values.keywords;
4037
+ const result = learn2(brainRoot, {
4038
+ text,
4039
+ prefix: prefixFlag,
4040
+ keywords: keywordsFlag ? keywordsFlag.split(",").map((k) => k.trim()) : void 0
4041
+ });
4042
+ if (result) {
4043
+ console.log(`\u{1F4DD} learned: ${result.path} (${result.source})`);
4044
+ } else {
4045
+ console.log("\u23ED\uFE0F no correction detected");
4046
+ }
4047
+ break;
4048
+ }
3885
4049
  case "candidates": {
3886
4050
  const subCmd = positionals[1];
3887
4051
  const { listCandidates: listCandidates2, promoteCandidates: promoteCandidates2 } = await Promise.resolve().then(() => (init_candidates(), candidates_exports));