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.
- package/README.md +33 -30
- package/dist/bin/hebbian.js +288 -136
- package/dist/bin/hebbian.js.map +1 -1
- package/dist/digest.d.ts.map +1 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/emit.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +116 -20
- package/dist/index.js.map +1 -1
- package/dist/learn.d.ts +17 -0
- package/dist/learn.d.ts.map +1 -0
- package/package.json +1 -1
package/dist/bin/hebbian.js
CHANGED
|
@@ -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
|
|
478
|
-
import { join as
|
|
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 =
|
|
481
|
-
if (!
|
|
482
|
-
|
|
483
|
-
|
|
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(
|
|
564
|
+
renameSync(join4(fullPath, `${current}.neuron`), join4(fullPath, `${newCounter}.neuron`));
|
|
491
565
|
} else {
|
|
492
|
-
|
|
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 =
|
|
499
|
-
if (!
|
|
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(
|
|
579
|
+
renameSync(join4(fullPath, `${current}.contra`), join4(fullPath, `${newContra}.contra`));
|
|
506
580
|
} else {
|
|
507
|
-
|
|
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
|
|
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
|
|
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
|
|
549
|
-
import { join as
|
|
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 =
|
|
552
|
-
if (
|
|
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 =
|
|
569
|
-
if (
|
|
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
|
-
|
|
579
|
-
|
|
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 =
|
|
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(
|
|
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 (
|
|
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
|
|
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
|
|
2461
|
-
if (
|
|
2462
|
-
if (text.length >
|
|
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 —
|
|
2665
|
-
// "X하지 마" (don't X) — must have a verb object before 지 마
|
|
2701
|
+
// Korean negation — imperative corrections:
|
|
2666
2702
|
/[을를은는도이가]\s*[가-힣]+지\s*마/,
|
|
2667
|
-
// "X
|
|
2703
|
+
// "X하지 마" (don't X) with particle
|
|
2668
2704
|
/하면\s*안\s*돼/,
|
|
2669
|
-
// "X
|
|
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 —
|
|
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
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
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
|
|
3347
|
-
([
|
|
3348
|
-
(
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
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 &&
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
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
|
-
|
|
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
|
|
3701
|
-
evolve prune [--dry-run] Pruning mode \u2014 remove stale
|
|
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));
|