hebbian 0.3.3 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +155 -70
- package/dist/bin/hebbian.js +988 -71
- package/dist/bin/hebbian.js.map +1 -1
- package/dist/candidates.d.ts +32 -0
- package/dist/candidates.d.ts.map +1 -0
- package/dist/constants.d.ts +4 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/doctor.d.ts +7 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/emit.d.ts.map +1 -1
- package/dist/episode.d.ts +6 -1
- package/dist/episode.d.ts.map +1 -1
- package/dist/evolve.d.ts +22 -0
- package/dist/evolve.d.ts.map +1 -0
- package/dist/fire.d.ts +10 -0
- package/dist/fire.d.ts.map +1 -1
- package/dist/hooks.d.ts +3 -3
- package/dist/hooks.d.ts.map +1 -1
- package/dist/inbox.d.ts.map +1 -1
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +690 -66
- package/dist/index.js.map +1 -1
- package/dist/outcome.d.ts +42 -0
- package/dist/outcome.d.ts.map +1 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -56,6 +56,9 @@ var HOOK_MARKER = "[hebbian]";
|
|
|
56
56
|
var MAX_CORRECTIONS_PER_SESSION = 10;
|
|
57
57
|
var MIN_CORRECTION_LENGTH = 15;
|
|
58
58
|
var DIGEST_LOG_DIR = "hippocampus/digest_log";
|
|
59
|
+
var OUTCOME_TYPES = ["revert", "acceptance"];
|
|
60
|
+
var SESSION_STATE_DIR = "hippocampus/session_state";
|
|
61
|
+
var PROTECTED_REGIONS_CONTRA = ["brainstem", "limbic", "sensors"];
|
|
59
62
|
function resolveBrainRoot(brainFlag) {
|
|
60
63
|
if (brainFlag) return resolve(brainFlag);
|
|
61
64
|
if (process.env.HEBBIAN_BRAIN) return resolve(process.env.HEBBIAN_BRAIN);
|
|
@@ -284,6 +287,33 @@ function fireNeuron(brainRoot, neuronPath) {
|
|
|
284
287
|
console.log(`\u{1F525} fired: ${neuronPath} (${current} \u2192 ${newCounter})`);
|
|
285
288
|
return newCounter;
|
|
286
289
|
}
|
|
290
|
+
function contraNeuron(brainRoot, neuronPath) {
|
|
291
|
+
const fullPath = join2(brainRoot, neuronPath);
|
|
292
|
+
if (!existsSync3(fullPath)) {
|
|
293
|
+
return 0;
|
|
294
|
+
}
|
|
295
|
+
const current = getCurrentContra(fullPath);
|
|
296
|
+
const newContra = current + 1;
|
|
297
|
+
if (current > 0) {
|
|
298
|
+
renameSync(join2(fullPath, `${current}.contra`), join2(fullPath, `${newContra}.contra`));
|
|
299
|
+
} else {
|
|
300
|
+
writeFileSync(join2(fullPath, `${newContra}.contra`), "", "utf8");
|
|
301
|
+
}
|
|
302
|
+
return newContra;
|
|
303
|
+
}
|
|
304
|
+
function getCurrentContra(dir) {
|
|
305
|
+
let max = 0;
|
|
306
|
+
try {
|
|
307
|
+
for (const entry of readdirSync2(dir)) {
|
|
308
|
+
if (entry.endsWith(".contra")) {
|
|
309
|
+
const n = parseInt(entry, 10);
|
|
310
|
+
if (!isNaN(n) && n > max) max = n;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
} catch {
|
|
314
|
+
}
|
|
315
|
+
return max;
|
|
316
|
+
}
|
|
287
317
|
function getCurrentCounter(dir) {
|
|
288
318
|
let max = 0;
|
|
289
319
|
try {
|
|
@@ -619,7 +649,7 @@ function emitBootstrap(result, brain) {
|
|
|
619
649
|
lines.push("|--------|---------|------------|");
|
|
620
650
|
for (const region of result.activeRegions) {
|
|
621
651
|
const active = region.neurons.filter((n) => !n.isDormant);
|
|
622
|
-
const activation = active.reduce((sum, n) => sum + n.
|
|
652
|
+
const activation = active.reduce((sum, n) => sum + n.intensity, 0);
|
|
623
653
|
const icon = REGION_ICONS[region.name] || "";
|
|
624
654
|
lines.push(`| ${icon} ${region.name} | ${active.length} | ${activation} |`);
|
|
625
655
|
}
|
|
@@ -641,7 +671,7 @@ function emitIndex(result, brain) {
|
|
|
641
671
|
const allNeurons = result.activeRegions.flatMap(
|
|
642
672
|
(r) => r.neurons.filter((n) => !n.isDormant && n.counter >= EMIT_THRESHOLD)
|
|
643
673
|
);
|
|
644
|
-
allNeurons.sort((a, b) => b.
|
|
674
|
+
allNeurons.sort((a, b) => b.intensity - a.intensity);
|
|
645
675
|
lines.push("## Top 10 Active Neurons");
|
|
646
676
|
lines.push("| # | Path | Counter | Strength |");
|
|
647
677
|
lines.push("|---|------|---------|----------|");
|
|
@@ -667,7 +697,7 @@ function emitIndex(result, brain) {
|
|
|
667
697
|
for (const region of result.activeRegions) {
|
|
668
698
|
const active = region.neurons.filter((n) => !n.isDormant);
|
|
669
699
|
const dormant = region.neurons.filter((n) => n.isDormant);
|
|
670
|
-
const activation = active.reduce((sum, n) => sum + n.
|
|
700
|
+
const activation = active.reduce((sum, n) => sum + n.intensity, 0);
|
|
671
701
|
const icon = REGION_ICONS[region.name] || "";
|
|
672
702
|
lines.push(`| ${icon} ${region.name} | ${active.length} | ${dormant.length} | ${activation} | [_rules.md](${region.name}/_rules.md) |`);
|
|
673
703
|
}
|
|
@@ -679,7 +709,7 @@ function emitRegionRules(region) {
|
|
|
679
709
|
const ko = REGION_KO[region.name] || "";
|
|
680
710
|
const active = region.neurons.filter((n) => !n.isDormant);
|
|
681
711
|
const dormant = region.neurons.filter((n) => n.isDormant);
|
|
682
|
-
const activation = active.reduce((sum, n) => sum + n.
|
|
712
|
+
const activation = active.reduce((sum, n) => sum + n.intensity, 0);
|
|
683
713
|
const lines = [];
|
|
684
714
|
lines.push(`# ${icon} ${region.name} (${ko})`);
|
|
685
715
|
lines.push(`> Active: ${active.length} | Dormant: ${dormant.length} | Activation: ${activation}`);
|
|
@@ -693,7 +723,7 @@ function emitRegionRules(region) {
|
|
|
693
723
|
}
|
|
694
724
|
if (active.length > 0) {
|
|
695
725
|
lines.push("## Rules");
|
|
696
|
-
const sorted = [...active].sort((a, b) => b.
|
|
726
|
+
const sorted = [...active].sort((a, b) => b.intensity - a.intensity);
|
|
697
727
|
for (const n of sorted) {
|
|
698
728
|
const indent = " ".repeat(Math.min(n.depth, 4));
|
|
699
729
|
const prefix = strengthPrefix(n.counter);
|
|
@@ -776,7 +806,7 @@ function printDiag(brain, result) {
|
|
|
776
806
|
const icon = REGION_ICONS[region.name] || "";
|
|
777
807
|
const active = region.neurons.filter((n) => !n.isDormant);
|
|
778
808
|
const dormant = region.neurons.filter((n) => n.isDormant);
|
|
779
|
-
const activation = active.reduce((sum, n) => sum + n.
|
|
809
|
+
const activation = active.reduce((sum, n) => sum + n.intensity, 0);
|
|
780
810
|
const isBlocked = result.blockedRegions.some((r) => r.name === region.name);
|
|
781
811
|
const status = region.hasBomb ? "\u{1F4A3} BOMB" : isBlocked ? "\u{1F6AB} BLOCKED" : "\u2705 ACTIVE";
|
|
782
812
|
console.log(` ${icon} ${region.name} [${status}]`);
|
|
@@ -786,7 +816,8 @@ function printDiag(brain, result) {
|
|
|
786
816
|
}
|
|
787
817
|
const top3 = sortedActive(region.neurons, 3);
|
|
788
818
|
for (const n of top3) {
|
|
789
|
-
|
|
819
|
+
const contraStr = n.contra > 0 ? ` contra:${n.contra}` : "";
|
|
820
|
+
console.log(` \u251C ${n.path} (counter:${n.counter}${contraStr} intensity:${n.intensity})`);
|
|
790
821
|
}
|
|
791
822
|
}
|
|
792
823
|
console.log("");
|
|
@@ -795,7 +826,7 @@ function pathToSentence(path) {
|
|
|
795
826
|
return path.replace(/\//g, " > ").replace(/_/g, " ");
|
|
796
827
|
}
|
|
797
828
|
function sortedActive(neurons, n) {
|
|
798
|
-
return [...neurons].filter((neuron) => !neuron.isDormant).sort((a, b) => b.
|
|
829
|
+
return [...neurons].filter((neuron) => !neuron.isDormant).sort((a, b) => b.intensity - a.intensity).slice(0, n);
|
|
799
830
|
}
|
|
800
831
|
function strengthPrefix(counter) {
|
|
801
832
|
if (counter >= 10) return "**[ABSOLUTE]** ";
|
|
@@ -922,46 +953,155 @@ ${template.description}
|
|
|
922
953
|
import { createServer } from "http";
|
|
923
954
|
|
|
924
955
|
// src/inbox.ts
|
|
925
|
-
import { readFileSync as readFileSync4, writeFileSync as writeFileSync9, existsSync as
|
|
926
|
-
import { join as
|
|
956
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync9, existsSync as existsSync12, mkdirSync as mkdirSync7 } from "fs";
|
|
957
|
+
import { join as join13 } from "path";
|
|
958
|
+
|
|
959
|
+
// src/candidates.ts
|
|
960
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync5, readdirSync as readdirSync7, renameSync as renameSync3, rmSync, statSync as statSync3 } from "fs";
|
|
961
|
+
import { join as join11, dirname as dirname2, relative as relative3 } from "path";
|
|
962
|
+
var CANDIDATE_THRESHOLD = 3;
|
|
963
|
+
var CANDIDATE_DECAY_DAYS = 14;
|
|
964
|
+
var CANDIDATE_SEGMENT = "_candidates";
|
|
965
|
+
function toCandidatePath(neuronPath) {
|
|
966
|
+
const slash = neuronPath.indexOf("/");
|
|
967
|
+
if (slash === -1) throw new Error(`Invalid neuron path (missing region): ${neuronPath}`);
|
|
968
|
+
return `${neuronPath.slice(0, slash)}/${CANDIDATE_SEGMENT}/${neuronPath.slice(slash + 1)}`;
|
|
969
|
+
}
|
|
970
|
+
function fromCandidatePath(candidatePath) {
|
|
971
|
+
return candidatePath.replace(`/${CANDIDATE_SEGMENT}/`, "/");
|
|
972
|
+
}
|
|
973
|
+
function growCandidate(brainRoot, neuronPath) {
|
|
974
|
+
const candidatePath = toCandidatePath(neuronPath);
|
|
975
|
+
const result = growNeuron(brainRoot, candidatePath);
|
|
976
|
+
if (result.counter >= CANDIDATE_THRESHOLD) {
|
|
977
|
+
const ok = moveCandidate(brainRoot, candidatePath, neuronPath);
|
|
978
|
+
return { ...result, path: ok ? neuronPath : result.path, promoted: ok };
|
|
979
|
+
}
|
|
980
|
+
console.log(` \u{1F331} candidate (${result.counter}/${CANDIDATE_THRESHOLD}): ${candidatePath}`);
|
|
981
|
+
return { ...result, promoted: false };
|
|
982
|
+
}
|
|
983
|
+
function moveCandidate(brainRoot, candidatePath, targetPath) {
|
|
984
|
+
const src = join11(brainRoot, candidatePath);
|
|
985
|
+
if (!existsSync10(src)) return false;
|
|
986
|
+
const dst = join11(brainRoot, targetPath);
|
|
987
|
+
if (existsSync10(dst)) {
|
|
988
|
+
fireNeuron(brainRoot, targetPath);
|
|
989
|
+
rmSync(src, { recursive: true, force: true });
|
|
990
|
+
} else {
|
|
991
|
+
mkdirSync5(dirname2(dst), { recursive: true });
|
|
992
|
+
renameSync3(src, dst);
|
|
993
|
+
}
|
|
994
|
+
console.log(`\u{1F393} promoted: ${candidatePath} \u2192 ${targetPath}`);
|
|
995
|
+
return true;
|
|
996
|
+
}
|
|
997
|
+
function promoteCandidates(brainRoot) {
|
|
998
|
+
const promoted = [];
|
|
999
|
+
const decayed = [];
|
|
1000
|
+
const decayMs = CANDIDATE_DECAY_DAYS * 24 * 60 * 60 * 1e3;
|
|
1001
|
+
const now = Date.now();
|
|
1002
|
+
for (const region of REGIONS) {
|
|
1003
|
+
const candidateRoot = join11(brainRoot, region, CANDIDATE_SEGMENT);
|
|
1004
|
+
walkNeuronDirs(candidateRoot, (neuronDir) => {
|
|
1005
|
+
const rel = relative3(join11(brainRoot, region), neuronDir);
|
|
1006
|
+
const candidatePath = `${region}/${rel}`;
|
|
1007
|
+
const targetPath = fromCandidatePath(candidatePath);
|
|
1008
|
+
const counter = readCounter(neuronDir);
|
|
1009
|
+
const mtime = statSync3(neuronDir).mtimeMs;
|
|
1010
|
+
if (counter >= CANDIDATE_THRESHOLD) {
|
|
1011
|
+
moveCandidate(brainRoot, candidatePath, targetPath);
|
|
1012
|
+
promoted.push(targetPath);
|
|
1013
|
+
} else if (now - mtime > decayMs) {
|
|
1014
|
+
rmSync(neuronDir, { recursive: true, force: true });
|
|
1015
|
+
decayed.push(candidatePath);
|
|
1016
|
+
console.log(`\u{1F480} candidate decayed: ${candidatePath}`);
|
|
1017
|
+
}
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
return { promoted, decayed };
|
|
1021
|
+
}
|
|
1022
|
+
function listCandidates(brainRoot) {
|
|
1023
|
+
const results = [];
|
|
1024
|
+
const now = Date.now();
|
|
1025
|
+
for (const region of REGIONS) {
|
|
1026
|
+
const candidateRoot = join11(brainRoot, region, CANDIDATE_SEGMENT);
|
|
1027
|
+
walkNeuronDirs(candidateRoot, (neuronDir) => {
|
|
1028
|
+
const rel = relative3(join11(brainRoot, region), neuronDir);
|
|
1029
|
+
const candidatePath = `${region}/${rel}`;
|
|
1030
|
+
const targetPath = fromCandidatePath(candidatePath);
|
|
1031
|
+
const counter = readCounter(neuronDir);
|
|
1032
|
+
const mtime = statSync3(neuronDir).mtimeMs;
|
|
1033
|
+
const daysInactive = Math.floor((now - mtime) / (24 * 60 * 60 * 1e3));
|
|
1034
|
+
results.push({ candidatePath, targetPath, counter, daysInactive });
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1037
|
+
return results;
|
|
1038
|
+
}
|
|
1039
|
+
function walkNeuronDirs(dir, cb) {
|
|
1040
|
+
if (!existsSync10(dir)) return;
|
|
1041
|
+
try {
|
|
1042
|
+
const entries = readdirSync7(dir, { withFileTypes: true });
|
|
1043
|
+
const hasNeuron = entries.some((e) => e.isFile() && e.name.endsWith(".neuron"));
|
|
1044
|
+
if (hasNeuron) {
|
|
1045
|
+
cb(dir);
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
for (const entry of entries) {
|
|
1049
|
+
if (entry.isDirectory() && !entry.name.startsWith(".")) {
|
|
1050
|
+
walkNeuronDirs(join11(dir, entry.name), cb);
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
} catch {
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
function readCounter(dir) {
|
|
1057
|
+
try {
|
|
1058
|
+
const files = readdirSync7(dir).filter((f) => /^\d+\.neuron$/.test(f));
|
|
1059
|
+
if (files.length === 0) return 0;
|
|
1060
|
+
return Math.max(...files.map((f) => parseInt(f, 10)));
|
|
1061
|
+
} catch {
|
|
1062
|
+
return 0;
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
927
1065
|
|
|
928
1066
|
// src/episode.ts
|
|
929
|
-
import { readdirSync as
|
|
930
|
-
import { join as
|
|
1067
|
+
import { readdirSync as readdirSync8, readFileSync as readFileSync3, writeFileSync as writeFileSync8, mkdirSync as mkdirSync6, existsSync as existsSync11 } from "fs";
|
|
1068
|
+
import { join as join12 } from "path";
|
|
931
1069
|
var MAX_EPISODES = 100;
|
|
932
1070
|
var SESSION_LOG_DIR = "hippocampus/session_log";
|
|
933
|
-
function logEpisode(brainRoot, type, path, detail) {
|
|
934
|
-
const logDir =
|
|
935
|
-
if (!
|
|
936
|
-
|
|
1071
|
+
function logEpisode(brainRoot, type, path, detail, extra) {
|
|
1072
|
+
const logDir = join12(brainRoot, SESSION_LOG_DIR);
|
|
1073
|
+
if (!existsSync11(logDir)) {
|
|
1074
|
+
mkdirSync6(logDir, { recursive: true });
|
|
937
1075
|
}
|
|
938
1076
|
const nextSlot = getNextSlot(logDir);
|
|
939
1077
|
const episode = {
|
|
940
1078
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
941
1079
|
type,
|
|
942
1080
|
path,
|
|
943
|
-
detail
|
|
1081
|
+
detail,
|
|
1082
|
+
...extra?.outcome ? { outcome: extra.outcome } : {},
|
|
1083
|
+
...extra?.neurons ? { neurons: extra.neurons } : {}
|
|
944
1084
|
};
|
|
945
1085
|
writeFileSync8(
|
|
946
|
-
|
|
1086
|
+
join12(logDir, `memory${nextSlot}.neuron`),
|
|
947
1087
|
JSON.stringify(episode),
|
|
948
1088
|
"utf8"
|
|
949
1089
|
);
|
|
950
1090
|
}
|
|
951
1091
|
function readEpisodes(brainRoot) {
|
|
952
|
-
const logDir =
|
|
953
|
-
if (!
|
|
1092
|
+
const logDir = join12(brainRoot, SESSION_LOG_DIR);
|
|
1093
|
+
if (!existsSync11(logDir)) return [];
|
|
954
1094
|
const episodes = [];
|
|
955
1095
|
let entries;
|
|
956
1096
|
try {
|
|
957
|
-
entries =
|
|
1097
|
+
entries = readdirSync8(logDir);
|
|
958
1098
|
} catch {
|
|
959
1099
|
return [];
|
|
960
1100
|
}
|
|
961
1101
|
for (const entry of entries) {
|
|
962
1102
|
if (!entry.startsWith("memory") || !entry.endsWith(".neuron")) continue;
|
|
963
1103
|
try {
|
|
964
|
-
const content = readFileSync3(
|
|
1104
|
+
const content = readFileSync3(join12(logDir, entry), "utf8");
|
|
965
1105
|
if (content.trim()) {
|
|
966
1106
|
episodes.push(JSON.parse(content));
|
|
967
1107
|
}
|
|
@@ -974,7 +1114,7 @@ function readEpisodes(brainRoot) {
|
|
|
974
1114
|
function getNextSlot(logDir) {
|
|
975
1115
|
let maxSlot = 0;
|
|
976
1116
|
try {
|
|
977
|
-
for (const entry of
|
|
1117
|
+
for (const entry of readdirSync8(logDir)) {
|
|
978
1118
|
if (entry.startsWith("memory") && entry.endsWith(".neuron")) {
|
|
979
1119
|
const n = parseInt(entry.replace("memory", "").replace(".neuron", ""), 10);
|
|
980
1120
|
if (!isNaN(n) && n > maxSlot) maxSlot = n;
|
|
@@ -991,8 +1131,8 @@ var INBOX_DIR = "_inbox";
|
|
|
991
1131
|
var CORRECTIONS_FILE = "corrections.jsonl";
|
|
992
1132
|
var DOPAMINE_ALLOWED_ROLES = ["pm", "admin", "lead"];
|
|
993
1133
|
function processInbox(brainRoot) {
|
|
994
|
-
const inboxPath =
|
|
995
|
-
if (!
|
|
1134
|
+
const inboxPath = join13(brainRoot, INBOX_DIR, CORRECTIONS_FILE);
|
|
1135
|
+
if (!existsSync12(inboxPath)) {
|
|
996
1136
|
return { processed: 0, skipped: 0, errors: [] };
|
|
997
1137
|
}
|
|
998
1138
|
const content = readFileSync4(inboxPath, "utf8").trim();
|
|
@@ -1047,16 +1187,18 @@ function processInbox(brainRoot) {
|
|
|
1047
1187
|
}
|
|
1048
1188
|
function applyCorrection(brainRoot, correction) {
|
|
1049
1189
|
const neuronPath = correction.path;
|
|
1050
|
-
const fullPath =
|
|
1190
|
+
const fullPath = join13(brainRoot, neuronPath);
|
|
1051
1191
|
const counterAdd = Math.max(1, correction.counter_add || 1);
|
|
1052
|
-
if (
|
|
1192
|
+
if (existsSync12(fullPath)) {
|
|
1053
1193
|
for (let i = 0; i < counterAdd; i++) {
|
|
1054
1194
|
fireNeuron(brainRoot, neuronPath);
|
|
1055
1195
|
}
|
|
1056
1196
|
} else {
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1197
|
+
const candResult = growCandidate(brainRoot, neuronPath);
|
|
1198
|
+
if (candResult.promoted) {
|
|
1199
|
+
for (let i = 1; i < counterAdd; i++) {
|
|
1200
|
+
fireNeuron(brainRoot, neuronPath);
|
|
1201
|
+
}
|
|
1060
1202
|
}
|
|
1061
1203
|
}
|
|
1062
1204
|
if (correction.dopamine && correction.dopamine > 0) {
|
|
@@ -1075,12 +1217,12 @@ function isPathSafe(path) {
|
|
|
1075
1217
|
return true;
|
|
1076
1218
|
}
|
|
1077
1219
|
function ensureInbox(brainRoot) {
|
|
1078
|
-
const inboxDir =
|
|
1079
|
-
if (!
|
|
1080
|
-
|
|
1220
|
+
const inboxDir = join13(brainRoot, INBOX_DIR);
|
|
1221
|
+
if (!existsSync12(inboxDir)) {
|
|
1222
|
+
mkdirSync7(inboxDir, { recursive: true });
|
|
1081
1223
|
}
|
|
1082
|
-
const filePath =
|
|
1083
|
-
if (!
|
|
1224
|
+
const filePath = join13(inboxDir, CORRECTIONS_FILE);
|
|
1225
|
+
if (!existsSync12(filePath)) {
|
|
1084
1226
|
writeFileSync9(filePath, "", "utf8");
|
|
1085
1227
|
}
|
|
1086
1228
|
return filePath;
|
|
@@ -1348,19 +1490,27 @@ function clearReports() {
|
|
|
1348
1490
|
}
|
|
1349
1491
|
|
|
1350
1492
|
// src/hooks.ts
|
|
1351
|
-
import { readFileSync as readFileSync5, writeFileSync as writeFileSync10, existsSync as
|
|
1493
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync10, existsSync as existsSync13, mkdirSync as mkdirSync8, readdirSync as readdirSync9 } from "fs";
|
|
1352
1494
|
import { execSync as execSync2 } from "child_process";
|
|
1353
|
-
import { join as
|
|
1495
|
+
import { join as join14, resolve as resolve2 } from "path";
|
|
1354
1496
|
var SETTINGS_DIR = ".claude";
|
|
1355
1497
|
var SETTINGS_FILE = "settings.local.json";
|
|
1356
|
-
function installHooks(brainRoot, projectRoot) {
|
|
1498
|
+
function installHooks(brainRoot, projectRoot, global) {
|
|
1357
1499
|
const root = projectRoot || process.cwd();
|
|
1358
1500
|
const resolvedBrain = resolve2(brainRoot);
|
|
1359
|
-
if (
|
|
1501
|
+
if (global) {
|
|
1502
|
+
const home = process.env.HOME || "~";
|
|
1503
|
+
if (!brainRoot.startsWith("/") && !brainRoot.startsWith(home)) {
|
|
1504
|
+
console.error("\u274C --global requires an absolute --brain path (e.g. --brain ~/brain)");
|
|
1505
|
+
process.exit(1);
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
if (!existsSync13(resolvedBrain) || !hasBrainRegions(resolvedBrain)) {
|
|
1360
1509
|
initBrain(resolvedBrain);
|
|
1361
1510
|
}
|
|
1362
|
-
const settingsDir =
|
|
1363
|
-
const
|
|
1511
|
+
const settingsDir = global ? resolve2(process.env.HOME || "~", SETTINGS_DIR) : join14(root, SETTINGS_DIR);
|
|
1512
|
+
const settingsFile = global ? "settings.json" : SETTINGS_FILE;
|
|
1513
|
+
const settingsPath = join14(settingsDir, settingsFile);
|
|
1364
1514
|
const defaultBrain = resolve2(root, "brain");
|
|
1365
1515
|
const brainFlag = resolvedBrain === defaultBrain ? "" : ` --brain ${resolvedBrain}`;
|
|
1366
1516
|
let npxBin = "npx";
|
|
@@ -1369,7 +1519,7 @@ function installHooks(brainRoot, projectRoot) {
|
|
|
1369
1519
|
} catch {
|
|
1370
1520
|
}
|
|
1371
1521
|
let settings = {};
|
|
1372
|
-
if (
|
|
1522
|
+
if (existsSync13(settingsPath)) {
|
|
1373
1523
|
try {
|
|
1374
1524
|
settings = JSON.parse(readFileSync5(settingsPath, "utf8"));
|
|
1375
1525
|
} catch {
|
|
@@ -1386,8 +1536,8 @@ function installHooks(brainRoot, projectRoot) {
|
|
|
1386
1536
|
matcher: "startup|resume",
|
|
1387
1537
|
entry: {
|
|
1388
1538
|
type: "command",
|
|
1389
|
-
command: `${npxBin} hebbian emit claude${brainFlag}`,
|
|
1390
|
-
timeout:
|
|
1539
|
+
command: `${npxBin} hebbian emit claude${brainFlag} && ${npxBin} hebbian session start${brainFlag}`,
|
|
1540
|
+
timeout: 15,
|
|
1391
1541
|
statusMessage: `${HOOK_MARKER} refreshing brain`
|
|
1392
1542
|
}
|
|
1393
1543
|
},
|
|
@@ -1395,7 +1545,7 @@ function installHooks(brainRoot, projectRoot) {
|
|
|
1395
1545
|
event: "Stop",
|
|
1396
1546
|
entry: {
|
|
1397
1547
|
type: "command",
|
|
1398
|
-
command: `${npxBin} hebbian digest${brainFlag}`,
|
|
1548
|
+
command: `${npxBin} hebbian digest${brainFlag}; ${npxBin} hebbian session end${brainFlag}`,
|
|
1399
1549
|
timeout: 30,
|
|
1400
1550
|
statusMessage: `${HOOK_MARKER} digesting session`
|
|
1401
1551
|
}
|
|
@@ -1418,18 +1568,20 @@ function installHooks(brainRoot, projectRoot) {
|
|
|
1418
1568
|
hooks[event].push(group);
|
|
1419
1569
|
}
|
|
1420
1570
|
}
|
|
1421
|
-
if (!
|
|
1422
|
-
|
|
1571
|
+
if (!existsSync13(settingsDir)) {
|
|
1572
|
+
mkdirSync8(settingsDir, { recursive: true });
|
|
1423
1573
|
}
|
|
1424
1574
|
writeFileSync10(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
1425
1575
|
console.log(`\u2705 hebbian hooks installed at ${settingsPath}`);
|
|
1426
1576
|
console.log(` SessionStart \u2192 ${npxBin} hebbian emit claude${brainFlag}`);
|
|
1427
1577
|
console.log(` Stop \u2192 ${npxBin} hebbian digest${brainFlag}`);
|
|
1428
1578
|
}
|
|
1429
|
-
function uninstallHooks(projectRoot) {
|
|
1579
|
+
function uninstallHooks(projectRoot, global) {
|
|
1430
1580
|
const root = projectRoot || process.cwd();
|
|
1431
|
-
const
|
|
1432
|
-
|
|
1581
|
+
const settingsDir = global ? resolve2(process.env.HOME || "~", SETTINGS_DIR) : join14(root, SETTINGS_DIR);
|
|
1582
|
+
const settingsFile = global ? "settings.json" : SETTINGS_FILE;
|
|
1583
|
+
const settingsPath = join14(settingsDir, settingsFile);
|
|
1584
|
+
if (!existsSync13(settingsPath)) {
|
|
1433
1585
|
console.log("No hooks installed (settings.local.json not found)");
|
|
1434
1586
|
return;
|
|
1435
1587
|
}
|
|
@@ -1462,15 +1614,17 @@ function uninstallHooks(projectRoot) {
|
|
|
1462
1614
|
writeFileSync10(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
1463
1615
|
console.log(`\u2705 removed ${removed} hebbian hook(s) from ${settingsPath}`);
|
|
1464
1616
|
}
|
|
1465
|
-
function checkHooks(projectRoot) {
|
|
1617
|
+
function checkHooks(projectRoot, global) {
|
|
1466
1618
|
const root = projectRoot || process.cwd();
|
|
1467
|
-
const
|
|
1619
|
+
const settingsDir = global ? resolve2(process.env.HOME || "~", SETTINGS_DIR) : join14(root, SETTINGS_DIR);
|
|
1620
|
+
const settingsFile = global ? "settings.json" : SETTINGS_FILE;
|
|
1621
|
+
const settingsPath = join14(settingsDir, settingsFile);
|
|
1468
1622
|
const status = {
|
|
1469
1623
|
installed: false,
|
|
1470
1624
|
path: settingsPath,
|
|
1471
1625
|
events: []
|
|
1472
1626
|
};
|
|
1473
|
-
if (!
|
|
1627
|
+
if (!existsSync13(settingsPath)) {
|
|
1474
1628
|
console.log(`\u274C hebbian hooks not installed (${settingsPath} not found)`);
|
|
1475
1629
|
return status;
|
|
1476
1630
|
}
|
|
@@ -1506,9 +1660,9 @@ function checkHooks(projectRoot) {
|
|
|
1506
1660
|
return status;
|
|
1507
1661
|
}
|
|
1508
1662
|
function hasBrainRegions(dir) {
|
|
1509
|
-
if (!
|
|
1663
|
+
if (!existsSync13(dir)) return false;
|
|
1510
1664
|
try {
|
|
1511
|
-
const entries =
|
|
1665
|
+
const entries = readdirSync9(dir);
|
|
1512
1666
|
return REGIONS.some((r) => entries.includes(r));
|
|
1513
1667
|
} catch {
|
|
1514
1668
|
return false;
|
|
@@ -1516,8 +1670,8 @@ function hasBrainRegions(dir) {
|
|
|
1516
1670
|
}
|
|
1517
1671
|
|
|
1518
1672
|
// src/digest.ts
|
|
1519
|
-
import { readFileSync as readFileSync6, writeFileSync as writeFileSync11, existsSync as
|
|
1520
|
-
import { join as
|
|
1673
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync11, existsSync as existsSync14, mkdirSync as mkdirSync9 } from "fs";
|
|
1674
|
+
import { join as join15, basename } from "path";
|
|
1521
1675
|
var NEGATION_PATTERNS = [
|
|
1522
1676
|
/\bdon[''\u2019]?t\b/i,
|
|
1523
1677
|
/\bdo not\b/i,
|
|
@@ -1568,13 +1722,13 @@ function readHookInput(stdin) {
|
|
|
1568
1722
|
}
|
|
1569
1723
|
}
|
|
1570
1724
|
function digestTranscript(brainRoot, transcriptPath, sessionId) {
|
|
1571
|
-
if (!
|
|
1725
|
+
if (!existsSync14(transcriptPath)) {
|
|
1572
1726
|
throw new Error(`Transcript not found: ${transcriptPath}`);
|
|
1573
1727
|
}
|
|
1574
1728
|
const resolvedSessionId = sessionId || basename(transcriptPath, ".jsonl");
|
|
1575
|
-
const logDir =
|
|
1576
|
-
const logPath =
|
|
1577
|
-
if (
|
|
1729
|
+
const logDir = join15(brainRoot, DIGEST_LOG_DIR);
|
|
1730
|
+
const logPath = join15(logDir, `${resolvedSessionId}.jsonl`);
|
|
1731
|
+
if (existsSync14(logPath)) {
|
|
1578
1732
|
console.log(`\u23ED already digested session ${resolvedSessionId}, skip`);
|
|
1579
1733
|
return { corrections: 0, skipped: 0, transcriptPath, sessionId: resolvedSessionId };
|
|
1580
1734
|
}
|
|
@@ -1589,7 +1743,7 @@ function digestTranscript(brainRoot, transcriptPath, sessionId) {
|
|
|
1589
1743
|
const auditEntries = [];
|
|
1590
1744
|
for (const correction of corrections) {
|
|
1591
1745
|
try {
|
|
1592
|
-
|
|
1746
|
+
growCandidate(brainRoot, correction.path);
|
|
1593
1747
|
logEpisode(brainRoot, "digest", correction.path, correction.text);
|
|
1594
1748
|
auditEntries.push({ correction, applied: true });
|
|
1595
1749
|
applied++;
|
|
@@ -1806,11 +1960,11 @@ function extractKeywords(text) {
|
|
|
1806
1960
|
return text.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[^a-zA-Z0-9\u3000-\u9FFF\uAC00-\uD7AF]+/g, " ").toLowerCase().split(/\s+/).filter((t) => t.length > 2 && !STOP_WORDS.has(t));
|
|
1807
1961
|
}
|
|
1808
1962
|
function writeAuditLog(brainRoot, sessionId, entries) {
|
|
1809
|
-
const logDir =
|
|
1810
|
-
if (!
|
|
1811
|
-
|
|
1963
|
+
const logDir = join15(brainRoot, DIGEST_LOG_DIR);
|
|
1964
|
+
if (!existsSync14(logDir)) {
|
|
1965
|
+
mkdirSync9(logDir, { recursive: true });
|
|
1812
1966
|
}
|
|
1813
|
-
const logPath =
|
|
1967
|
+
const logPath = join15(logDir, `${sessionId}.jsonl`);
|
|
1814
1968
|
const lines = entries.map(
|
|
1815
1969
|
(e) => JSON.stringify({
|
|
1816
1970
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -1823,6 +1977,462 @@ function writeAuditLog(brainRoot, sessionId, entries) {
|
|
|
1823
1977
|
);
|
|
1824
1978
|
writeFileSync11(logPath, lines.join("\n") + (lines.length > 0 ? "\n" : ""), "utf8");
|
|
1825
1979
|
}
|
|
1980
|
+
|
|
1981
|
+
// src/outcome.ts
|
|
1982
|
+
import { execSync as execSync3 } from "child_process";
|
|
1983
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync10, writeFileSync as writeFileSync12, readFileSync as readFileSync7, readdirSync as readdirSync10, rmSync as rmSync2, statSync as statSync4 } from "fs";
|
|
1984
|
+
import { join as join16 } from "path";
|
|
1985
|
+
import { randomUUID } from "crypto";
|
|
1986
|
+
function captureSessionStart(brainRoot) {
|
|
1987
|
+
let sha;
|
|
1988
|
+
try {
|
|
1989
|
+
sha = execSync3("git rev-parse HEAD", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
1990
|
+
} catch {
|
|
1991
|
+
console.log("\u23ED\uFE0F session start: not a git repo, skipping");
|
|
1992
|
+
return null;
|
|
1993
|
+
}
|
|
1994
|
+
let status;
|
|
1995
|
+
try {
|
|
1996
|
+
const raw = execSync3("git status --porcelain", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
1997
|
+
status = raw ? raw.split("\n") : [];
|
|
1998
|
+
} catch {
|
|
1999
|
+
status = [];
|
|
2000
|
+
}
|
|
2001
|
+
const brain = scanBrain(brainRoot);
|
|
2002
|
+
const result = runSubsumption(brain);
|
|
2003
|
+
const neurons = [];
|
|
2004
|
+
for (const region of result.activeRegions) {
|
|
2005
|
+
for (const neuron of region.neurons) {
|
|
2006
|
+
if (!neuron.isDormant && neuron.counter > 0) {
|
|
2007
|
+
neurons.push(`${region.name}/${neuron.path}`);
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
const uuid = randomUUID();
|
|
2012
|
+
const stateDir = join16(brainRoot, SESSION_STATE_DIR);
|
|
2013
|
+
if (!existsSync15(stateDir)) {
|
|
2014
|
+
mkdirSync10(stateDir, { recursive: true });
|
|
2015
|
+
}
|
|
2016
|
+
const state = { ts: (/* @__PURE__ */ new Date()).toISOString(), sha, status, neurons, uuid };
|
|
2017
|
+
writeFileSync12(join16(stateDir, `state_${uuid}.json`), JSON.stringify(state), "utf8");
|
|
2018
|
+
console.log(`\u{1F4F8} session start: SHA ${sha.slice(0, 7)}, ${neurons.length} active neurons`);
|
|
2019
|
+
return state;
|
|
2020
|
+
}
|
|
2021
|
+
function detectOutcome(brainRoot) {
|
|
2022
|
+
const state = readLatestSessionState(brainRoot);
|
|
2023
|
+
if (!state) {
|
|
2024
|
+
console.log("\u23ED\uFE0F session end: no session state found, skipping");
|
|
2025
|
+
return null;
|
|
2026
|
+
}
|
|
2027
|
+
let currentSha;
|
|
2028
|
+
try {
|
|
2029
|
+
currentSha = execSync3("git rev-parse HEAD", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
2030
|
+
} catch {
|
|
2031
|
+
console.log("\u23ED\uFE0F session end: not a git repo, skipping");
|
|
2032
|
+
cleanupSessionState(brainRoot, state.uuid);
|
|
2033
|
+
return null;
|
|
2034
|
+
}
|
|
2035
|
+
let currentStatus;
|
|
2036
|
+
try {
|
|
2037
|
+
const raw = execSync3("git status --porcelain", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
2038
|
+
currentStatus = raw ? filterHebbianPaths(raw.split("\n")) : [];
|
|
2039
|
+
} catch {
|
|
2040
|
+
currentStatus = [];
|
|
2041
|
+
}
|
|
2042
|
+
const filteredStartStatus = filterHebbianPaths(state.status);
|
|
2043
|
+
const outcome = classifyOutcome(
|
|
2044
|
+
{ ...state, status: filteredStartStatus },
|
|
2045
|
+
currentSha,
|
|
2046
|
+
currentStatus
|
|
2047
|
+
);
|
|
2048
|
+
if (!outcome) {
|
|
2049
|
+
console.log("\u{1F4CA} session end: no changes detected (no-op)");
|
|
2050
|
+
cleanupSessionState(brainRoot, state.uuid);
|
|
2051
|
+
return null;
|
|
2052
|
+
}
|
|
2053
|
+
const neurons = state.neurons;
|
|
2054
|
+
logEpisode(brainRoot, "session-end", "", `outcome:${outcome}`, { outcome, neurons });
|
|
2055
|
+
let result;
|
|
2056
|
+
if (outcome === "revert") {
|
|
2057
|
+
const { affected, skipped } = applyContra(brainRoot, neurons);
|
|
2058
|
+
result = {
|
|
2059
|
+
outcome: "revert",
|
|
2060
|
+
neuronsAffected: affected,
|
|
2061
|
+
protectedSkipped: skipped,
|
|
2062
|
+
detail: `${affected} neurons contra'd (${skipped} protected skipped)`
|
|
2063
|
+
};
|
|
2064
|
+
console.log(`\u{1F4CA} session end: revert \u2014 ${result.detail}`);
|
|
2065
|
+
} else {
|
|
2066
|
+
result = {
|
|
2067
|
+
outcome: "acceptance",
|
|
2068
|
+
neuronsAffected: 0,
|
|
2069
|
+
protectedSkipped: 0,
|
|
2070
|
+
detail: "changes accepted"
|
|
2071
|
+
};
|
|
2072
|
+
console.log("\u{1F4CA} session end: acceptance");
|
|
2073
|
+
}
|
|
2074
|
+
cleanupSessionState(brainRoot, state.uuid);
|
|
2075
|
+
return result;
|
|
2076
|
+
}
|
|
2077
|
+
function classifyOutcome(state, currentSha, currentStatus) {
|
|
2078
|
+
const headMoved = state.sha !== currentSha;
|
|
2079
|
+
const startStatusSet = new Set(state.status);
|
|
2080
|
+
const endStatusSet = new Set(currentStatus);
|
|
2081
|
+
const newItems = currentStatus.filter((s) => !startStatusSet.has(s));
|
|
2082
|
+
const removedItems = state.status.filter((s) => !endStatusSet.has(s));
|
|
2083
|
+
if (!headMoved) {
|
|
2084
|
+
if (newItems.length === 0 && removedItems.length === 0) {
|
|
2085
|
+
return null;
|
|
2086
|
+
}
|
|
2087
|
+
if (newItems.length > 0) {
|
|
2088
|
+
return "acceptance";
|
|
2089
|
+
}
|
|
2090
|
+
if (removedItems.length > 0) {
|
|
2091
|
+
return "revert";
|
|
2092
|
+
}
|
|
2093
|
+
return null;
|
|
2094
|
+
}
|
|
2095
|
+
if (newItems.length > 0) {
|
|
2096
|
+
return "acceptance";
|
|
2097
|
+
}
|
|
2098
|
+
try {
|
|
2099
|
+
const diffStat = execSync3(
|
|
2100
|
+
`git diff ${state.sha}..${currentSha} --stat`,
|
|
2101
|
+
{ encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }
|
|
2102
|
+
).trim();
|
|
2103
|
+
const logOutput = execSync3(
|
|
2104
|
+
`git log --oneline ${state.sha}..${currentSha}`,
|
|
2105
|
+
{ encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }
|
|
2106
|
+
).trim();
|
|
2107
|
+
if (/\brevert\b/i.test(logOutput)) {
|
|
2108
|
+
return "revert";
|
|
2109
|
+
}
|
|
2110
|
+
if (!diffStat) {
|
|
2111
|
+
return "revert";
|
|
2112
|
+
}
|
|
2113
|
+
return "acceptance";
|
|
2114
|
+
} catch {
|
|
2115
|
+
return null;
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
function applyContra(brainRoot, neurons) {
|
|
2119
|
+
let affected = 0;
|
|
2120
|
+
let skipped = 0;
|
|
2121
|
+
for (const neuronPath of neurons) {
|
|
2122
|
+
const region = neuronPath.split("/")[0] || "";
|
|
2123
|
+
if (PROTECTED_REGIONS_CONTRA.includes(region)) {
|
|
2124
|
+
skipped++;
|
|
2125
|
+
continue;
|
|
2126
|
+
}
|
|
2127
|
+
const result = contraNeuron(brainRoot, neuronPath);
|
|
2128
|
+
if (result > 0) {
|
|
2129
|
+
affected++;
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
return { affected, skipped };
|
|
2133
|
+
}
|
|
2134
|
+
function buildOutcomeSummary(brainRoot) {
|
|
2135
|
+
const episodes = readEpisodes(brainRoot);
|
|
2136
|
+
const outcomeEpisodes = episodes.filter((e) => e.outcome && e.neurons);
|
|
2137
|
+
if (outcomeEpisodes.length === 0) return "";
|
|
2138
|
+
const stats = /* @__PURE__ */ new Map();
|
|
2139
|
+
for (const ep of outcomeEpisodes) {
|
|
2140
|
+
for (const neuron of ep.neurons) {
|
|
2141
|
+
const existing = stats.get(neuron) || { sessions: 0, reverts: 0, acceptances: 0 };
|
|
2142
|
+
existing.sessions++;
|
|
2143
|
+
if (ep.outcome === "revert") existing.reverts++;
|
|
2144
|
+
if (ep.outcome === "acceptance") existing.acceptances++;
|
|
2145
|
+
stats.set(neuron, existing);
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
const lines = ["## Outcome Signals (from session history)\n"];
|
|
2149
|
+
lines.push("Neurons with high contra_ratio (>0.5) are consistently present in reverted sessions. Consider pruning or modifying them.\n");
|
|
2150
|
+
const sorted = [...stats.entries()].sort((a, b) => {
|
|
2151
|
+
const ratioA = a[1].sessions > 0 ? a[1].reverts / a[1].sessions : 0;
|
|
2152
|
+
const ratioB = b[1].sessions > 0 ? b[1].reverts / b[1].sessions : 0;
|
|
2153
|
+
return ratioB - ratioA;
|
|
2154
|
+
});
|
|
2155
|
+
for (const [neuron, s] of sorted) {
|
|
2156
|
+
const ratio = s.sessions > 0 ? (s.reverts / s.sessions).toFixed(2) : "0.00";
|
|
2157
|
+
const trend = parseFloat(ratio) > 0.5 ? "\u2190 act on this" : parseFloat(ratio) > 0.3 ? "\u2190 watch" : "";
|
|
2158
|
+
lines.push(`- ${neuron}: sessions=${s.sessions} reverts=${s.reverts} acceptances=${s.acceptances} contra_ratio=${ratio} ${trend}`);
|
|
2159
|
+
}
|
|
2160
|
+
lines.push("");
|
|
2161
|
+
return lines.join("\n");
|
|
2162
|
+
}
|
|
2163
|
+
function readLatestSessionState(brainRoot) {
|
|
2164
|
+
const stateDir = join16(brainRoot, SESSION_STATE_DIR);
|
|
2165
|
+
if (!existsSync15(stateDir)) return null;
|
|
2166
|
+
let latest = null;
|
|
2167
|
+
try {
|
|
2168
|
+
for (const entry of readdirSync10(stateDir)) {
|
|
2169
|
+
if (!entry.startsWith("state_") || !entry.endsWith(".json")) continue;
|
|
2170
|
+
const fullPath = join16(stateDir, entry);
|
|
2171
|
+
const mtime = statSync4(fullPath).mtimeMs;
|
|
2172
|
+
if (!latest || mtime > latest.mtime) {
|
|
2173
|
+
latest = { path: fullPath, mtime };
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
} catch {
|
|
2177
|
+
return null;
|
|
2178
|
+
}
|
|
2179
|
+
if (!latest) return null;
|
|
2180
|
+
try {
|
|
2181
|
+
return JSON.parse(readFileSync7(latest.path, "utf8"));
|
|
2182
|
+
} catch {
|
|
2183
|
+
return null;
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
function filterHebbianPaths(statusLines) {
|
|
2187
|
+
const hebbianPatterns = ["hippocampus/session_state", "hippocampus/session_log", "hippocampus/digest_log", "_inbox/"];
|
|
2188
|
+
return statusLines.filter(
|
|
2189
|
+
(line) => !hebbianPatterns.some((p) => line.includes(p))
|
|
2190
|
+
);
|
|
2191
|
+
}
|
|
2192
|
+
function cleanupSessionState(brainRoot, uuid) {
|
|
2193
|
+
const stateDir = join16(brainRoot, SESSION_STATE_DIR);
|
|
2194
|
+
const filePath = join16(stateDir, `state_${uuid}.json`);
|
|
2195
|
+
try {
|
|
2196
|
+
if (existsSync15(filePath)) rmSync2(filePath);
|
|
2197
|
+
} catch {
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
// src/evolve.ts
|
|
2202
|
+
var MAX_ACTIONS = 10;
|
|
2203
|
+
var PROTECTED_REGIONS = ["brainstem", "limbic", "sensors"];
|
|
2204
|
+
var DEFAULT_MODEL = "gemini-2.0-flash-lite";
|
|
2205
|
+
var API_TIMEOUT = 3e4;
|
|
2206
|
+
var RETRY_DELAY = 5e3;
|
|
2207
|
+
async function runEvolve(brainRoot, dryRun) {
|
|
2208
|
+
const apiKey = process.env.GEMINI_API_KEY;
|
|
2209
|
+
if (!apiKey) {
|
|
2210
|
+
console.error("\u274C GEMINI_API_KEY not set. Get one at https://aistudio.google.com/apikey");
|
|
2211
|
+
return { actions: [], executed: 0, skipped: 0, dryRun };
|
|
2212
|
+
}
|
|
2213
|
+
const episodes = readEpisodes(brainRoot);
|
|
2214
|
+
const brain = scanBrain(brainRoot);
|
|
2215
|
+
const summary = buildBrainSummary(brain);
|
|
2216
|
+
const outcomeSummary = buildOutcomeSummary(brainRoot);
|
|
2217
|
+
const prompt = buildPrompt(summary, episodes, outcomeSummary);
|
|
2218
|
+
let rawActions;
|
|
2219
|
+
try {
|
|
2220
|
+
rawActions = await callGemini(prompt, apiKey);
|
|
2221
|
+
} catch (err) {
|
|
2222
|
+
const msg = err.message;
|
|
2223
|
+
console.log(`\u23ED\uFE0F evolve skipped: ${msg}`);
|
|
2224
|
+
logEpisode(brainRoot, "evolve-error", "", msg);
|
|
2225
|
+
return { actions: [], executed: 0, skipped: 0, dryRun };
|
|
2226
|
+
}
|
|
2227
|
+
const actions = validateActions(rawActions, brain);
|
|
2228
|
+
const skipped = rawActions.length - actions.length;
|
|
2229
|
+
if (actions.length === 0) {
|
|
2230
|
+
console.log("\u{1F9E0} evolve: no valid actions proposed");
|
|
2231
|
+
return { actions: [], executed: 0, skipped, dryRun };
|
|
2232
|
+
}
|
|
2233
|
+
if (dryRun) {
|
|
2234
|
+
console.log(`\u{1F9E0} evolve (dry-run): ${actions.length} action(s) proposed`);
|
|
2235
|
+
for (const action of actions) {
|
|
2236
|
+
console.log(` ${actionIcon(action.type)} ${action.type} ${action.path} \u2014 ${action.reason}`);
|
|
2237
|
+
}
|
|
2238
|
+
return { actions, executed: 0, skipped, dryRun: true };
|
|
2239
|
+
}
|
|
2240
|
+
const executed = executeActions(brainRoot, actions);
|
|
2241
|
+
logEpisode(brainRoot, "evolve", "", `${executed} action(s) executed, ${skipped} skipped`);
|
|
2242
|
+
console.log(`\u{1F9E0} evolve: ${executed} action(s) executed, ${skipped} skipped`);
|
|
2243
|
+
return { actions, executed, skipped, dryRun: false };
|
|
2244
|
+
}
|
|
2245
|
+
function buildBrainSummary(brain) {
|
|
2246
|
+
const lines = ["# Brain State\n"];
|
|
2247
|
+
for (const region of brain.regions) {
|
|
2248
|
+
const neurons = region.neurons;
|
|
2249
|
+
if (neurons.length === 0 && !region.hasBomb) continue;
|
|
2250
|
+
lines.push(`## ${region.name} (P${REGION_PRIORITY[region.name]})`);
|
|
2251
|
+
if (region.hasBomb) lines.push("\u26A0\uFE0F BOMB active \u2014 region blocked");
|
|
2252
|
+
for (const neuron of neurons) {
|
|
2253
|
+
const flags = [];
|
|
2254
|
+
if (neuron.isDormant) flags.push("dormant");
|
|
2255
|
+
if (neuron.hasBomb) flags.push("bomb");
|
|
2256
|
+
if (neuron.hasMemory) flags.push("memory");
|
|
2257
|
+
if (neuron.dopamine > 0) flags.push(`dopamine:${neuron.dopamine}`);
|
|
2258
|
+
const flagStr = flags.length > 0 ? ` [${flags.join(", ")}]` : "";
|
|
2259
|
+
lines.push(`- ${neuron.path} (counter:${neuron.counter}, intensity:${neuron.intensity})${flagStr}`);
|
|
2260
|
+
}
|
|
2261
|
+
lines.push("");
|
|
2262
|
+
}
|
|
2263
|
+
return lines.join("\n");
|
|
2264
|
+
}
|
|
2265
|
+
function buildPrompt(summary, episodes, outcomeSummary) {
|
|
2266
|
+
const episodeLines = episodes.length > 0 ? episodes.map((e) => `- [${e.ts}] ${e.type}: ${e.path} \u2014 ${e.detail}`).join("\n") : "(no recent episodes)";
|
|
2267
|
+
const outcomeSection = outcomeSummary || "";
|
|
2268
|
+
return `You are the evolve engine for a hebbian brain \u2014 a filesystem-based memory system for AI agents.
|
|
2269
|
+
|
|
2270
|
+
## Axioms
|
|
2271
|
+
- Folder = Neuron, File = Firing Trace, Counter = Activation strength
|
|
2272
|
+
- 7 regions in subsumption cascade: brainstem(P0) > limbic(P1) > hippocampus(P2) > sensors(P3) > cortex(P4) > ego(P5) > prefrontal(P6)
|
|
2273
|
+
- Lower priority ALWAYS overrides higher priority
|
|
2274
|
+
- PROTECTED regions (brainstem, limbic, sensors): NEVER propose mutations for these
|
|
2275
|
+
|
|
2276
|
+
## Current Brain
|
|
2277
|
+
${summary}
|
|
2278
|
+
|
|
2279
|
+
${outcomeSection}
|
|
2280
|
+
## Recent Episodes (last ${episodes.length})
|
|
2281
|
+
${episodeLines}
|
|
2282
|
+
|
|
2283
|
+
## Available Actions
|
|
2284
|
+
- grow: Create a new neuron at the given path (region/name). Use for recurring patterns that deserve permanent memory.
|
|
2285
|
+
- fire: Increment an existing neuron's counter. Use for strengthening well-confirmed rules.
|
|
2286
|
+
- signal: Add dopamine (reward), bomb (block), or memory signal. Use sparingly.
|
|
2287
|
+
- prune: Decrement a neuron's counter. Use for rules that aren't working or cause issues.
|
|
2288
|
+
- decay: Mark inactive neurons as dormant. Use for stale rules with no recent activity.
|
|
2289
|
+
|
|
2290
|
+
## Constraints
|
|
2291
|
+
- Max ${MAX_ACTIONS} actions per cycle
|
|
2292
|
+
- PREFER fire over grow \u2014 strengthen existing neurons before creating new ones
|
|
2293
|
+
- NEVER target brainstem, limbic, or sensors regions
|
|
2294
|
+
- Each action needs a "reason" explaining why
|
|
2295
|
+
|
|
2296
|
+
## Task
|
|
2297
|
+
Analyze the brain state and recent episodes. Propose actions to improve the brain.
|
|
2298
|
+
Focus on: strengthening repeatedly-used rules, pruning ineffective ones, growing new neurons from repeated patterns.
|
|
2299
|
+
|
|
2300
|
+
Respond with a JSON array of actions:
|
|
2301
|
+
[{"type":"fire","path":"cortex/NO_console_log","reason":"fired 3 times in recent sessions"}]`;
|
|
2302
|
+
}
|
|
2303
|
+
async function callGemini(prompt, apiKey) {
|
|
2304
|
+
const model = process.env.EVOLVE_MODEL || DEFAULT_MODEL;
|
|
2305
|
+
const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`;
|
|
2306
|
+
const body = {
|
|
2307
|
+
contents: [{ parts: [{ text: prompt }] }],
|
|
2308
|
+
generationConfig: {
|
|
2309
|
+
responseMimeType: "application/json",
|
|
2310
|
+
temperature: 0.2
|
|
2311
|
+
}
|
|
2312
|
+
};
|
|
2313
|
+
let lastError = null;
|
|
2314
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
2315
|
+
if (attempt > 0) {
|
|
2316
|
+
await new Promise((r) => setTimeout(r, RETRY_DELAY));
|
|
2317
|
+
}
|
|
2318
|
+
try {
|
|
2319
|
+
const res = await fetch(url, {
|
|
2320
|
+
method: "POST",
|
|
2321
|
+
headers: { "Content-Type": "application/json" },
|
|
2322
|
+
body: JSON.stringify(body),
|
|
2323
|
+
signal: AbortSignal.timeout(API_TIMEOUT)
|
|
2324
|
+
});
|
|
2325
|
+
if (!res.ok) {
|
|
2326
|
+
lastError = new Error(`Gemini API ${res.status}: ${res.statusText}`);
|
|
2327
|
+
continue;
|
|
2328
|
+
}
|
|
2329
|
+
const data = await res.json();
|
|
2330
|
+
if (data.error) {
|
|
2331
|
+
lastError = new Error(`Gemini error: ${data.error.message || "unknown"}`);
|
|
2332
|
+
continue;
|
|
2333
|
+
}
|
|
2334
|
+
const text = data.candidates?.[0]?.content?.parts?.[0]?.text;
|
|
2335
|
+
if (!text) {
|
|
2336
|
+
lastError = new Error("Gemini returned empty response");
|
|
2337
|
+
continue;
|
|
2338
|
+
}
|
|
2339
|
+
return parseActions(text);
|
|
2340
|
+
} catch (err) {
|
|
2341
|
+
lastError = err;
|
|
2342
|
+
continue;
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
throw lastError || new Error("Gemini call failed");
|
|
2346
|
+
}
|
|
2347
|
+
function parseActions(text) {
|
|
2348
|
+
let parsed;
|
|
2349
|
+
try {
|
|
2350
|
+
parsed = JSON.parse(text);
|
|
2351
|
+
} catch {
|
|
2352
|
+
throw new Error(`Failed to parse LLM response as JSON: ${text.slice(0, 100)}`);
|
|
2353
|
+
}
|
|
2354
|
+
if (!Array.isArray(parsed)) {
|
|
2355
|
+
throw new Error("LLM response is not an array");
|
|
2356
|
+
}
|
|
2357
|
+
const validTypes = /* @__PURE__ */ new Set(["grow", "fire", "signal", "prune", "decay"]);
|
|
2358
|
+
const actions = [];
|
|
2359
|
+
for (const item of parsed) {
|
|
2360
|
+
if (!item || typeof item !== "object") continue;
|
|
2361
|
+
const { type, path, reason, signal } = item;
|
|
2362
|
+
if (typeof type !== "string" || !validTypes.has(type)) continue;
|
|
2363
|
+
if (typeof path !== "string" || path.length === 0) continue;
|
|
2364
|
+
if (typeof reason !== "string") continue;
|
|
2365
|
+
const action = { type, path, reason };
|
|
2366
|
+
if (type === "signal" && typeof signal === "string") {
|
|
2367
|
+
action.signal = signal;
|
|
2368
|
+
}
|
|
2369
|
+
actions.push(action);
|
|
2370
|
+
}
|
|
2371
|
+
return actions;
|
|
2372
|
+
}
|
|
2373
|
+
function validateActions(actions, _brain) {
|
|
2374
|
+
return actions.filter((action) => {
|
|
2375
|
+
const region = action.path.split("/")[0];
|
|
2376
|
+
if (!region || PROTECTED_REGIONS.includes(region)) {
|
|
2377
|
+
console.log(` \u{1F6E1}\uFE0F blocked: ${action.type} ${action.path} (protected region)`);
|
|
2378
|
+
return false;
|
|
2379
|
+
}
|
|
2380
|
+
if (!REGIONS.includes(region)) {
|
|
2381
|
+
console.log(` \u26A0\uFE0F skipped: ${action.type} ${action.path} (invalid region)`);
|
|
2382
|
+
return false;
|
|
2383
|
+
}
|
|
2384
|
+
if (action.type === "signal" && action.signal && !["dopamine", "bomb", "memory"].includes(action.signal)) {
|
|
2385
|
+
console.log(` \u26A0\uFE0F skipped: signal ${action.path} (invalid signal type: ${action.signal})`);
|
|
2386
|
+
return false;
|
|
2387
|
+
}
|
|
2388
|
+
return true;
|
|
2389
|
+
}).slice(0, MAX_ACTIONS);
|
|
2390
|
+
}
|
|
2391
|
+
function executeActions(brainRoot, actions) {
|
|
2392
|
+
let executed = 0;
|
|
2393
|
+
for (const action of actions) {
|
|
2394
|
+
try {
|
|
2395
|
+
switch (action.type) {
|
|
2396
|
+
case "fire":
|
|
2397
|
+
fireNeuron(brainRoot, action.path);
|
|
2398
|
+
break;
|
|
2399
|
+
case "grow":
|
|
2400
|
+
growCandidate(brainRoot, action.path);
|
|
2401
|
+
break;
|
|
2402
|
+
case "signal":
|
|
2403
|
+
signalNeuron(brainRoot, action.path, action.signal || "dopamine");
|
|
2404
|
+
break;
|
|
2405
|
+
case "prune":
|
|
2406
|
+
rollbackNeuron(brainRoot, action.path);
|
|
2407
|
+
break;
|
|
2408
|
+
case "decay":
|
|
2409
|
+
runDecay(brainRoot, 0);
|
|
2410
|
+
break;
|
|
2411
|
+
}
|
|
2412
|
+
console.log(` ${actionIcon(action.type)} ${action.type} ${action.path}`);
|
|
2413
|
+
executed++;
|
|
2414
|
+
} catch (err) {
|
|
2415
|
+
console.log(` \u26A0\uFE0F failed: ${action.type} ${action.path} \u2014 ${err.message}`);
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
return executed;
|
|
2419
|
+
}
|
|
2420
|
+
function actionIcon(type) {
|
|
2421
|
+
switch (type) {
|
|
2422
|
+
case "fire":
|
|
2423
|
+
return "\u{1F525}";
|
|
2424
|
+
case "grow":
|
|
2425
|
+
return "\u{1F331}";
|
|
2426
|
+
case "signal":
|
|
2427
|
+
return "\u26A1";
|
|
2428
|
+
case "prune":
|
|
2429
|
+
return "\u2702\uFE0F";
|
|
2430
|
+
case "decay":
|
|
2431
|
+
return "\u{1F4A4}";
|
|
2432
|
+
default:
|
|
2433
|
+
return "\u2753";
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
1826
2436
|
export {
|
|
1827
2437
|
DECAY_DAYS,
|
|
1828
2438
|
DIGEST_LOG_DIR,
|
|
@@ -1835,15 +2445,23 @@ export {
|
|
|
1835
2445
|
MAX_CORRECTIONS_PER_SESSION,
|
|
1836
2446
|
MAX_DEPTH,
|
|
1837
2447
|
MIN_CORRECTION_LENGTH,
|
|
2448
|
+
OUTCOME_TYPES,
|
|
2449
|
+
PROTECTED_REGIONS_CONTRA,
|
|
1838
2450
|
REGIONS,
|
|
1839
2451
|
REGION_ICONS,
|
|
1840
2452
|
REGION_KO,
|
|
1841
2453
|
REGION_PRIORITY,
|
|
2454
|
+
SESSION_STATE_DIR,
|
|
1842
2455
|
SIGNAL_TYPES,
|
|
1843
2456
|
SPOTLIGHT_DAYS,
|
|
1844
2457
|
appendCorrection,
|
|
2458
|
+
buildOutcomeSummary,
|
|
2459
|
+
captureSessionStart,
|
|
1845
2460
|
checkHooks,
|
|
2461
|
+
classifyOutcome,
|
|
1846
2462
|
clearReports,
|
|
2463
|
+
contraNeuron,
|
|
2464
|
+
detectOutcome,
|
|
1847
2465
|
digestTranscript,
|
|
1848
2466
|
emitBootstrap,
|
|
1849
2467
|
emitIndex,
|
|
@@ -1852,29 +2470,35 @@ export {
|
|
|
1852
2470
|
ensureInbox,
|
|
1853
2471
|
extractCorrections,
|
|
1854
2472
|
fireNeuron,
|
|
2473
|
+
fromCandidatePath,
|
|
1855
2474
|
getCurrentCounter,
|
|
1856
2475
|
getLastActivity,
|
|
1857
2476
|
getPendingReports,
|
|
1858
2477
|
gitSnapshot,
|
|
2478
|
+
growCandidate,
|
|
1859
2479
|
growNeuron,
|
|
1860
2480
|
initBrain,
|
|
1861
2481
|
installHooks,
|
|
1862
2482
|
jaccardSimilarity,
|
|
2483
|
+
listCandidates,
|
|
1863
2484
|
logEpisode,
|
|
1864
2485
|
printDiag,
|
|
1865
2486
|
processInbox,
|
|
2487
|
+
promoteCandidates,
|
|
1866
2488
|
readEpisodes,
|
|
1867
2489
|
readHookInput,
|
|
1868
2490
|
resolveBrainRoot,
|
|
1869
2491
|
rollbackNeuron,
|
|
1870
2492
|
runDecay,
|
|
1871
2493
|
runDedup,
|
|
2494
|
+
runEvolve,
|
|
1872
2495
|
runSubsumption,
|
|
1873
2496
|
scanBrain,
|
|
1874
2497
|
signalNeuron,
|
|
1875
2498
|
startAPI,
|
|
1876
2499
|
startWatch,
|
|
1877
2500
|
stem,
|
|
2501
|
+
toCandidatePath,
|
|
1878
2502
|
tokenize,
|
|
1879
2503
|
uninstallHooks,
|
|
1880
2504
|
writeAllTiers
|