hebbian 0.10.0 → 0.11.1

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,80 @@ var init_subsumption = __esm({
435
435
  }
436
436
  });
437
437
 
438
+ // src/episode.ts
439
+ var episode_exports = {};
440
+ __export(episode_exports, {
441
+ logEpisode: () => logEpisode,
442
+ readEpisodes: () => readEpisodes
443
+ });
444
+ import { readdirSync as readdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync4 } from "fs";
445
+ import { join as join3 } from "path";
446
+ function logEpisode(brainRoot, type, path, detail, extra) {
447
+ const logDir = join3(brainRoot, SESSION_LOG_DIR);
448
+ if (!existsSync4(logDir)) {
449
+ mkdirSync2(logDir, { recursive: true });
450
+ }
451
+ const nextSlot = getNextSlot(logDir);
452
+ const episode = {
453
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
454
+ type,
455
+ path,
456
+ detail,
457
+ ...extra?.outcome ? { outcome: extra.outcome } : {},
458
+ ...extra?.neurons ? { neurons: extra.neurons } : {}
459
+ };
460
+ writeFileSync2(
461
+ join3(logDir, `memory${nextSlot}.neuron`),
462
+ JSON.stringify(episode),
463
+ "utf8"
464
+ );
465
+ }
466
+ function readEpisodes(brainRoot) {
467
+ const logDir = join3(brainRoot, SESSION_LOG_DIR);
468
+ if (!existsSync4(logDir)) return [];
469
+ const episodes = [];
470
+ let entries;
471
+ try {
472
+ entries = readdirSync3(logDir);
473
+ } catch {
474
+ return [];
475
+ }
476
+ for (const entry of entries) {
477
+ if (!entry.startsWith("memory") || !entry.endsWith(".neuron")) continue;
478
+ try {
479
+ const content = readFileSync3(join3(logDir, entry), "utf8");
480
+ if (content.trim()) {
481
+ episodes.push(JSON.parse(content));
482
+ }
483
+ } catch {
484
+ }
485
+ }
486
+ episodes.sort((a, b) => a.ts.localeCompare(b.ts));
487
+ return episodes;
488
+ }
489
+ function getNextSlot(logDir) {
490
+ let maxSlot = 0;
491
+ try {
492
+ for (const entry of readdirSync3(logDir)) {
493
+ if (entry.startsWith("memory") && entry.endsWith(".neuron")) {
494
+ const n = parseInt(entry.replace("memory", "").replace(".neuron", ""), 10);
495
+ if (!isNaN(n) && n > maxSlot) maxSlot = n;
496
+ }
497
+ }
498
+ } catch {
499
+ }
500
+ const next = maxSlot + 1;
501
+ return next > MAX_EPISODES ? maxSlot % MAX_EPISODES + 1 : next;
502
+ }
503
+ var MAX_EPISODES, SESSION_LOG_DIR;
504
+ var init_episode = __esm({
505
+ "src/episode.ts"() {
506
+ "use strict";
507
+ MAX_EPISODES = 100;
508
+ SESSION_LOG_DIR = "hippocampus/session_log";
509
+ }
510
+ });
511
+
438
512
  // src/similarity.ts
439
513
  function tokenize(name) {
440
514
  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);
@@ -474,44 +548,44 @@ __export(fire_exports, {
474
548
  getCurrentContra: () => getCurrentContra,
475
549
  getCurrentCounter: () => getCurrentCounter
476
550
  });
477
- import { readdirSync as readdirSync3, renameSync, writeFileSync as writeFileSync2, existsSync as existsSync4, mkdirSync as mkdirSync2 } from "fs";
478
- import { join as join3 } from "path";
551
+ import { readdirSync as readdirSync4, renameSync, writeFileSync as writeFileSync3, existsSync as existsSync5, mkdirSync as mkdirSync3 } from "fs";
552
+ import { join as join4 } from "path";
479
553
  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");
554
+ const fullPath = join4(brainRoot, neuronPath);
555
+ if (!existsSync5(fullPath)) {
556
+ mkdirSync3(fullPath, { recursive: true });
557
+ writeFileSync3(join4(fullPath, "1.neuron"), "", "utf8");
484
558
  console.log(`\u{1F331} grew + fired: ${neuronPath} (1)`);
485
559
  return 1;
486
560
  }
487
561
  const current = getCurrentCounter(fullPath);
488
562
  const newCounter = current + 1;
489
563
  if (current > 0) {
490
- renameSync(join3(fullPath, `${current}.neuron`), join3(fullPath, `${newCounter}.neuron`));
564
+ renameSync(join4(fullPath, `${current}.neuron`), join4(fullPath, `${newCounter}.neuron`));
491
565
  } else {
492
- writeFileSync2(join3(fullPath, `${newCounter}.neuron`), "", "utf8");
566
+ writeFileSync3(join4(fullPath, `${newCounter}.neuron`), "", "utf8");
493
567
  }
494
568
  console.log(`\u{1F525} fired: ${neuronPath} (${current} \u2192 ${newCounter})`);
495
569
  return newCounter;
496
570
  }
497
571
  function contraNeuron(brainRoot, neuronPath) {
498
- const fullPath = join3(brainRoot, neuronPath);
499
- if (!existsSync4(fullPath)) {
572
+ const fullPath = join4(brainRoot, neuronPath);
573
+ if (!existsSync5(fullPath)) {
500
574
  return 0;
501
575
  }
502
576
  const current = getCurrentContra(fullPath);
503
577
  const newContra = current + 1;
504
578
  if (current > 0) {
505
- renameSync(join3(fullPath, `${current}.contra`), join3(fullPath, `${newContra}.contra`));
579
+ renameSync(join4(fullPath, `${current}.contra`), join4(fullPath, `${newContra}.contra`));
506
580
  } else {
507
- writeFileSync2(join3(fullPath, `${newContra}.contra`), "", "utf8");
581
+ writeFileSync3(join4(fullPath, `${newContra}.contra`), "", "utf8");
508
582
  }
509
583
  return newContra;
510
584
  }
511
585
  function getCurrentContra(dir) {
512
586
  let max = 0;
513
587
  try {
514
- for (const entry of readdirSync3(dir)) {
588
+ for (const entry of readdirSync4(dir)) {
515
589
  if (entry.endsWith(".contra")) {
516
590
  const n = parseInt(entry, 10);
517
591
  if (!isNaN(n) && n > max) max = n;
@@ -524,7 +598,7 @@ function getCurrentContra(dir) {
524
598
  function getCurrentCounter(dir) {
525
599
  let max = 0;
526
600
  try {
527
- for (const entry of readdirSync3(dir)) {
601
+ for (const entry of readdirSync4(dir)) {
528
602
  if (entry.endsWith(".neuron") && !entry.startsWith("dopamine") && !entry.startsWith("memory") && entry !== "bomb.neuron") {
529
603
  const n = parseInt(entry, 10);
530
604
  if (!isNaN(n) && n > max) max = n;
@@ -545,11 +619,11 @@ var grow_exports = {};
545
619
  __export(grow_exports, {
546
620
  growNeuron: () => growNeuron
547
621
  });
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";
622
+ import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync4, existsSync as existsSync6, readdirSync as readdirSync5 } from "fs";
623
+ import { join as join5, relative as relative2 } from "path";
550
624
  function growNeuron(brainRoot, neuronPath) {
551
- const fullPath = join4(brainRoot, neuronPath);
552
- if (existsSync5(fullPath)) {
625
+ const fullPath = join5(brainRoot, neuronPath);
626
+ if (existsSync6(fullPath)) {
553
627
  const counter = fireNeuron(brainRoot, neuronPath);
554
628
  return { action: "fired", path: neuronPath, counter };
555
629
  }
@@ -565,8 +639,8 @@ function growNeuron(brainRoot, neuronPath) {
565
639
  const newPrefix = leafName.match(/^(NO|DO|MUST|WARN)_/)?.[1] || "";
566
640
  const newStripped = leafName.replace(/^(NO|DO|MUST|WARN)_/, "");
567
641
  const newTokens = tokenize(newStripped);
568
- const regionPath = join4(brainRoot, regionName);
569
- if (existsSync5(regionPath)) {
642
+ const regionPath = join5(brainRoot, regionName);
643
+ if (existsSync6(regionPath)) {
570
644
  const match = findSimilar(regionPath, regionPath, newTokens, newPrefix);
571
645
  if (match) {
572
646
  const matchRelPath = regionName + "/" + relative2(regionPath, match);
@@ -575,15 +649,15 @@ function growNeuron(brainRoot, neuronPath) {
575
649
  return { action: "fired", path: matchRelPath, counter };
576
650
  }
577
651
  }
578
- mkdirSync3(fullPath, { recursive: true });
579
- writeFileSync3(join4(fullPath, "1.neuron"), "", "utf8");
652
+ mkdirSync4(fullPath, { recursive: true });
653
+ writeFileSync4(join5(fullPath, "1.neuron"), "", "utf8");
580
654
  console.log(`\u{1F331} grew: ${neuronPath} (1)`);
581
655
  return { action: "grew", path: neuronPath, counter: 1 };
582
656
  }
583
657
  function findSimilar(dir, regionRoot, targetTokens, targetPrefix) {
584
658
  let entries;
585
659
  try {
586
- entries = readdirSync4(dir, { withFileTypes: true });
660
+ entries = readdirSync5(dir, { withFileTypes: true });
587
661
  } catch {
588
662
  return null;
589
663
  }
@@ -602,7 +676,7 @@ function findSimilar(dir, regionRoot, targetTokens, targetPrefix) {
602
676
  for (const entry of entries) {
603
677
  if (entry.name.startsWith("_") || entry.name.startsWith(".")) continue;
604
678
  if (entry.isDirectory()) {
605
- const match = findSimilar(join4(dir, entry.name), regionRoot, targetTokens, targetPrefix);
679
+ const match = findSimilar(join5(dir, entry.name), regionRoot, targetTokens, targetPrefix);
606
680
  if (match) return match;
607
681
  }
608
682
  }
@@ -617,80 +691,6 @@ var init_grow = __esm({
617
691
  }
618
692
  });
619
693
 
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
694
  // src/candidates.ts
695
695
  var candidates_exports = {};
696
696
  __export(candidates_exports, {
@@ -922,6 +922,42 @@ function emitBootstrap(result, brain, brainRoot) {
922
922
  lines.push("");
923
923
  }
924
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
+ const episodes = readEpisodes(brainRoot);
938
+ const recentEpisodes = episodes.sort((a, b) => b.ts.localeCompare(a.ts)).slice(0, 5);
939
+ if (recentEpisodes.length > 0) {
940
+ lines.push("### Recent Memory");
941
+ for (const ep of recentEpisodes) {
942
+ const typeIcon = ep.type === "learn" ? "\u{1F4DD}" : ep.type === "tool-failure" ? "\u{1F527}" : "\u{1F504}";
943
+ const shortDetail = ep.detail.slice(0, 60).replace(/\n/g, " ");
944
+ lines.push(`- ${typeIcon} ${ep.type}: ${ep.path} \u2014 ${shortDetail}`);
945
+ }
946
+ lines.push("");
947
+ }
948
+ lines.push("### Self-Evolution");
949
+ lines.push("At session start, review Active Rules and Provisional Rules above.");
950
+ lines.push("If a rule seems wrong or outdated:");
951
+ lines.push("```");
952
+ lines.push(`hebbian rollback <neuron-path> ${brainFlag}`);
953
+ lines.push("```");
954
+ lines.push("If a provisional rule has been working well across sessions:");
955
+ lines.push("```");
956
+ lines.push(`hebbian fire <neuron-path> ${brainFlag}`);
957
+ lines.push("```");
958
+ lines.push("Run silently. Trust your judgment \u2014 you are the evaluator.");
959
+ lines.push("");
960
+ }
925
961
  lines.push(MARKER_END);
926
962
  return lines.join("\n");
927
963
  }
@@ -1107,6 +1143,7 @@ var init_emit = __esm({
1107
1143
  init_scanner();
1108
1144
  init_subsumption();
1109
1145
  init_constants();
1146
+ init_episode();
1110
1147
  init_candidates();
1111
1148
  }
1112
1149
  });
@@ -2396,7 +2433,7 @@ function extractCorrections(messages) {
2396
2433
  if (/^Base directory for this skill:/i.test(trimmed)) continue;
2397
2434
  if (/^[•·▸▶\-\*]\s/.test(trimmed)) continue;
2398
2435
  if (/<[a-zA-Z][a-zA-Z-]*>/.test(trimmed) && /<\/[a-zA-Z]/.test(trimmed)) continue;
2399
- if (isNarrativeKorean(trimmed)) continue;
2436
+ if (isNarrative(trimmed)) continue;
2400
2437
  const correction = detectCorrection(text);
2401
2438
  if (correction) {
2402
2439
  corrections.push(correction);
@@ -2404,7 +2441,7 @@ function extractCorrections(messages) {
2404
2441
  }
2405
2442
  return corrections;
2406
2443
  }
2407
- function isNarrativeKorean(text) {
2444
+ function isNarrative(text) {
2408
2445
  const NARRATIVE_MARKERS = [
2409
2446
  /이유는/,
2410
2447
  // "the reason is..."
@@ -2457,9 +2494,9 @@ function detectCorrection(text) {
2457
2494
  const isAffirmation = AFFIRMATION_PATTERNS.some((p) => p.test(text));
2458
2495
  if (!isNegation && !isMust && !isWarn && !isAffirmation) return null;
2459
2496
  const categories = [isNegation, isMust, isWarn, isAffirmation].filter(Boolean).length;
2460
- const koreanRatio = (text.match(/[\uAC00-\uD7AF]/g) || []).length / Math.max(text.length, 1);
2461
- if (koreanRatio > 0.3 && categories < 2) {
2462
- if (text.length > 100) return null;
2497
+ const latinRatio = (text.match(/[a-zA-Z]/g) || []).length / Math.max(text.length, 1);
2498
+ if (latinRatio < 0.3 && categories < 2) {
2499
+ if (text.length > 150) return null;
2463
2500
  }
2464
2501
  let prefix;
2465
2502
  if (isNegation) prefix = "NO";
@@ -2661,33 +2698,65 @@ var init_digest = __esm({
2661
2698
  /\binstead\b/i,
2662
2699
  /^no[,.\s!]/i,
2663
2700
  /\bavoid\b/i,
2664
- // Korean negation — require AI-directed imperative context:
2665
- // "X하지 마" (don't X) — must have a verb object before 지 마
2701
+ // Korean negation — imperative corrections:
2666
2702
  /[을를은는도이가]\s*[가-힣]+지\s*마/,
2667
- // "X 하면 안 돼" (must not X) conditional + prohibition
2703
+ // "X하지 " (don't X) with particle
2668
2704
  /하면\s*안\s*돼/,
2669
- // "X 쓰지 " (don't use X) — explicit "don't use"
2670
- /쓰지\s*마/
2705
+ // "X 하면 안 돼" (must not X)
2706
+ /쓰지\s*마/,
2707
+ // "쓰지 마" (don't use)
2708
+ /그만/,
2709
+ // "그만" (stop) — 그만해, 그만 좀
2710
+ /[을를은는]\s*빼/,
2711
+ // "X 빼" (remove X) with particle
2712
+ /지워[줘]?|삭제해/,
2713
+ // "지워/삭제해" (delete it) — not 지우고 (connective)
2714
+ /[가-힣]+지\s*말고/,
2715
+ // "X지 말고" (instead of X-ing)
2716
+ /그거\s*아니/,
2717
+ // "그거 아니야" (that's not right)
2718
+ /ㄴㄴ|노노/,
2719
+ // "ㄴㄴ/노노" (no no — internet-style)
2720
+ /안\s*돼[^요]?\s*[!.]/
2721
+ // "안 돼!" standalone prohibition
2671
2722
  ];
2672
2723
  AFFIRMATION_PATTERNS = [
2673
2724
  /\bshould\s+always\b/i,
2674
2725
  /\buse\s+\w+\s+instead\b/i,
2675
- // Korean affirmation — require directive context
2676
- /항상\s*[가-힣]+[해하]/
2726
+ // Korean affirmation — directive commands:
2727
+ /항상\s*[가-힣]+[해하]/,
2677
2728
  // "항상 X해" (always do X)
2729
+ /[을를]\s*[가-힣]*해\s*줘/,
2730
+ // "X를 해줘" (do X for me) — literal 해줘, not bare 줘
2731
+ /으로\s*해/,
2732
+ // "X으로 해" (do it as X) — literal 으로, not char class
2733
+ /이렇게\s*해/
2734
+ // "이렇게 해" (do it like this)
2678
2735
  ];
2679
2736
  MUST_PATTERNS = [
2680
2737
  /\bmust\b/i,
2681
2738
  /\brequired\b/i,
2682
- // Korean
2683
- /반드시/
2739
+ // Korean — strong directives:
2740
+ /반드시/,
2741
+ // "반드시" (absolutely must)
2742
+ /꼭\s*[가-힣]/,
2743
+ // "꼭 X해" (definitely do X)
2744
+ /무조건/,
2745
+ // "무조건" (unconditionally)
2746
+ /필수/
2747
+ // "필수" (mandatory)
2684
2748
  ];
2685
2749
  WARN_PATTERNS = [
2686
2750
  /\bcareful\b/i,
2687
2751
  /\bwatch\s+out\b/i,
2688
2752
  /\bwarning\b/i,
2689
- // Korean
2690
- /주의/
2753
+ // Korean — cautionary:
2754
+ /주의/,
2755
+ // "주의" (caution)
2756
+ /조심/,
2757
+ // "조심" (be careful)
2758
+ /위험/
2759
+ // "위험" (dangerous)
2691
2760
  ];
2692
2761
  MAX_FAILURES_PER_SESSION = 20;
2693
2762
  SOFT_ERROR_PATTERNS = [
@@ -2701,6 +2770,48 @@ var init_digest = __esm({
2701
2770
  }
2702
2771
  });
2703
2772
 
2773
+ // src/learn.ts
2774
+ var learn_exports = {};
2775
+ __export(learn_exports, {
2776
+ learn: () => learn
2777
+ });
2778
+ function learn(brainRoot, opts) {
2779
+ let prefix;
2780
+ let keywords;
2781
+ let source;
2782
+ if (opts.prefix && opts.keywords && opts.keywords.length > 0) {
2783
+ prefix = opts.prefix.toUpperCase();
2784
+ if (!VALID_PREFIXES.has(prefix)) {
2785
+ prefix = "DO";
2786
+ }
2787
+ keywords = opts.keywords.slice(0, 3).map((k) => k.toLowerCase().replace(/[\s\/\\\.,:;!?'"<>{}()\[\]]/g, ""));
2788
+ source = "agent";
2789
+ } else {
2790
+ const corrections = extractCorrections([opts.text]);
2791
+ if (corrections.length === 0) return null;
2792
+ const c = corrections[0];
2793
+ prefix = c.prefix;
2794
+ keywords = c.keywords;
2795
+ source = "regex";
2796
+ }
2797
+ if (keywords.length === 0) return null;
2798
+ const pathSegment = `${prefix}_${keywords.slice(0, 3).join("_")}`;
2799
+ const path = `cortex/${pathSegment}`;
2800
+ growCandidate(brainRoot, path);
2801
+ logEpisode(brainRoot, "learn", path, opts.text);
2802
+ return { path, prefix, keywords, source };
2803
+ }
2804
+ var VALID_PREFIXES;
2805
+ var init_learn = __esm({
2806
+ "src/learn.ts"() {
2807
+ "use strict";
2808
+ init_candidates();
2809
+ init_episode();
2810
+ init_digest();
2811
+ VALID_PREFIXES = /* @__PURE__ */ new Set(["NO", "DO", "MUST", "WARN"]);
2812
+ }
2813
+ });
2814
+
2704
2815
  // src/outcome.ts
2705
2816
  var outcome_exports = {};
2706
2817
  __export(outcome_exports, {
@@ -3276,6 +3387,7 @@ __export(doctor_exports, {
3276
3387
  });
3277
3388
  import { existsSync as existsSync18, readFileSync as readFileSync11, readdirSync as readdirSync11 } from "fs";
3278
3389
  import { join as join19 } from "path";
3390
+ import { homedir } from "os";
3279
3391
  import { execSync as execSync4 } from "child_process";
3280
3392
  async function runDoctor(brainRoot) {
3281
3393
  let passed = 0, warnings = 0, failed = 0;
@@ -3336,33 +3448,49 @@ async function runDoctor(brainRoot) {
3336
3448
  }
3337
3449
  }
3338
3450
  console.log("\nClaude Code hooks");
3339
- const settingsPath = join19(process.cwd(), ".claude", "settings.local.json");
3340
- if (!existsSync18(settingsPath)) {
3341
- warn("No .claude/settings.local.json found", "hebbian claude install");
3342
- } else {
3451
+ const localSettingsPath = join19(process.cwd(), ".claude", "settings.local.json");
3452
+ const globalSettingsPath = join19(homedir(), ".claude", "settings.json");
3453
+ let hasStop = false;
3454
+ let hasStart = false;
3455
+ let hookSource = "";
3456
+ for (const settingsPath of [localSettingsPath, globalSettingsPath]) {
3457
+ if (!existsSync18(settingsPath)) continue;
3343
3458
  try {
3344
3459
  const settings = JSON.parse(readFileSync11(settingsPath, "utf8"));
3345
3460
  const hooks = settings.hooks || {};
3346
- const hasStop = Object.entries(hooks).some(
3347
- ([event, entries]) => event === "Stop" && Array.isArray(entries) && entries.some(
3348
- (e) => typeof e === "object" && e !== null && "command" in e && typeof e.command === "string" && e.command.includes("hebbian digest")
3349
- )
3350
- );
3351
- const hasStart = Object.entries(hooks).some(
3352
- ([event, entries]) => event === "SessionStart" && Array.isArray(entries) && entries.some(
3353
- (e) => typeof e === "object" && e !== null && "command" in e && typeof e.command === "string" && e.command.includes("hebbian emit")
3354
- )
3461
+ const findCommand = (event, keyword) => Object.entries(hooks).some(
3462
+ ([ev, entries]) => ev === event && Array.isArray(entries) && entries.some((entry) => {
3463
+ if (typeof entry !== "object" || entry === null) return false;
3464
+ const e = entry;
3465
+ if (typeof e.command === "string" && e.command.includes(keyword)) return true;
3466
+ if (Array.isArray(e.hooks)) {
3467
+ return e.hooks.some(
3468
+ (h) => typeof h === "object" && h !== null && typeof h.command === "string" && h.command.includes(keyword)
3469
+ );
3470
+ }
3471
+ return false;
3472
+ })
3355
3473
  );
3356
- if (hasStop && hasStart) {
3357
- ok("SessionStart + Stop hooks installed");
3358
- } else {
3359
- if (!hasStart) warn("SessionStart hook missing", "hebbian claude install");
3360
- if (!hasStop) warn("Stop hook missing", "hebbian claude install");
3474
+ if (!hasStop && findCommand("Stop", "hebbian digest")) {
3475
+ hasStop = true;
3476
+ hookSource = settingsPath === globalSettingsPath ? "global" : "local";
3477
+ }
3478
+ if (!hasStart && findCommand("SessionStart", "hebbian emit")) {
3479
+ hasStart = true;
3480
+ if (!hookSource) hookSource = settingsPath === globalSettingsPath ? "global" : "local";
3361
3481
  }
3362
3482
  } catch {
3363
- fail("Malformed .claude/settings.local.json", "hebbian claude install");
3483
+ warn(`Malformed ${settingsPath === globalSettingsPath ? "~/.claude/settings.json" : ".claude/settings.local.json"}`, "Check JSON syntax");
3364
3484
  }
3365
3485
  }
3486
+ if (hasStop && hasStart) {
3487
+ ok(`SessionStart + Stop hooks installed (${hookSource})`);
3488
+ } else if (!hasStop && !hasStart) {
3489
+ warn("No hebbian hooks found (checked local + global)", "hebbian claude install");
3490
+ } else {
3491
+ if (!hasStart) warn("SessionStart hook missing", "hebbian claude install");
3492
+ if (!hasStop) warn("Stop hook missing", "hebbian claude install");
3493
+ }
3366
3494
  console.log("\nnpx resolution");
3367
3495
  try {
3368
3496
  const resolved = execSync4("which npx", { timeout: 3e3 }).toString().trim();
@@ -3696,9 +3824,10 @@ COMMANDS:
3696
3824
  inbox Process corrections inbox
3697
3825
  claude install|uninstall|status Manage Claude Code hooks
3698
3826
  digest [--transcript <path>] Extract corrections from conversation
3827
+ learn "<text>" [--prefix P] Agent-driven learning (any language)
3699
3828
  candidates [promote] List candidates or promote graduated ones
3700
- evolve [--dry-run] LLM-powered brain evolution (Gemini)
3701
- evolve prune [--dry-run] Pruning mode \u2014 remove stale/redundant neurons
3829
+ evolve [--dry-run] (optional) LLM-powered evolution (Gemini)
3830
+ evolve prune [--dry-run] (optional) Pruning mode \u2014 remove stale neurons
3702
3831
  session start|end Capture/detect session outcomes
3703
3832
  sessions Show session outcome history
3704
3833
  doctor Self-diagnostic (hooks, brain, versions)
@@ -3716,7 +3845,7 @@ EXAMPLES:
3716
3845
  hebbian fire cortex/frontend/NO_console_log --brain ./my-brain
3717
3846
  hebbian emit claude --brain ./my-brain
3718
3847
  hebbian emit all
3719
- GEMINI_API_KEY=... hebbian evolve --dry-run
3848
+ GEMINI_API_KEY=... hebbian evolve --dry-run # optional \u2014 self-learning works without this
3720
3849
  `.trim();
3721
3850
  function readStdin() {
3722
3851
  return new Promise((resolve4) => {
@@ -3742,6 +3871,8 @@ async function main(argv) {
3742
3871
  days: { type: "string", short: "d" },
3743
3872
  port: { type: "string", short: "p" },
3744
3873
  transcript: { type: "string", short: "t" },
3874
+ prefix: { type: "string" },
3875
+ keywords: { type: "string", short: "k" },
3745
3876
  "dry-run": { type: "boolean" },
3746
3877
  global: { type: "boolean", short: "g" },
3747
3878
  agent: { type: "string", short: "a" },
@@ -3918,6 +4049,27 @@ async function main(argv) {
3918
4049
  }
3919
4050
  break;
3920
4051
  }
4052
+ case "learn": {
4053
+ const text = positionals.slice(1).join(" ");
4054
+ if (!text) {
4055
+ console.error('Usage: hebbian learn "correction text" [--prefix NO|DO|MUST|WARN] [--keywords "k1,k2,k3"]');
4056
+ process.exit(1);
4057
+ }
4058
+ const { learn: learn2 } = await Promise.resolve().then(() => (init_learn(), learn_exports));
4059
+ const prefixFlag = values.prefix;
4060
+ const keywordsFlag = values.keywords;
4061
+ const result = learn2(brainRoot, {
4062
+ text,
4063
+ prefix: prefixFlag,
4064
+ keywords: keywordsFlag ? keywordsFlag.split(",").map((k) => k.trim()) : void 0
4065
+ });
4066
+ if (result) {
4067
+ console.log(`\u{1F4DD} learned: ${result.path} (${result.source})`);
4068
+ } else {
4069
+ console.log("\u23ED\uFE0F no correction detected");
4070
+ }
4071
+ break;
4072
+ }
3921
4073
  case "candidates": {
3922
4074
  const subCmd = positionals[1];
3923
4075
  const { listCandidates: listCandidates2, promoteCandidates: promoteCandidates2 } = await Promise.resolve().then(() => (init_candidates(), candidates_exports));