hebbian 0.3.4 → 0.5.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 +155 -70
- package/dist/api.d.ts.map +1 -1
- package/dist/bin/hebbian.js +754 -85
- 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/digest.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 +1 -1
- package/dist/evolve.d.ts.map +1 -1
- package/dist/fire.d.ts +10 -0
- package/dist/fire.d.ts.map +1 -1
- package/dist/grow.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 +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +506 -73
- 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 {
|
|
@@ -370,6 +400,9 @@ function growNeuron(brainRoot, neuronPath) {
|
|
|
370
400
|
const counter = fireNeuron(brainRoot, neuronPath);
|
|
371
401
|
return { action: "fired", path: neuronPath, counter };
|
|
372
402
|
}
|
|
403
|
+
if (neuronPath.includes("..") || neuronPath.startsWith("/")) {
|
|
404
|
+
throw new Error(`Invalid neuron path: "${neuronPath}" (path traversal not allowed)`);
|
|
405
|
+
}
|
|
373
406
|
const parts = neuronPath.split("/");
|
|
374
407
|
const regionName = parts[0];
|
|
375
408
|
if (!REGIONS.includes(regionName)) {
|
|
@@ -619,7 +652,7 @@ function emitBootstrap(result, brain) {
|
|
|
619
652
|
lines.push("|--------|---------|------------|");
|
|
620
653
|
for (const region of result.activeRegions) {
|
|
621
654
|
const active = region.neurons.filter((n) => !n.isDormant);
|
|
622
|
-
const activation = active.reduce((sum, n) => sum + n.
|
|
655
|
+
const activation = active.reduce((sum, n) => sum + n.intensity, 0);
|
|
623
656
|
const icon = REGION_ICONS[region.name] || "";
|
|
624
657
|
lines.push(`| ${icon} ${region.name} | ${active.length} | ${activation} |`);
|
|
625
658
|
}
|
|
@@ -641,7 +674,7 @@ function emitIndex(result, brain) {
|
|
|
641
674
|
const allNeurons = result.activeRegions.flatMap(
|
|
642
675
|
(r) => r.neurons.filter((n) => !n.isDormant && n.counter >= EMIT_THRESHOLD)
|
|
643
676
|
);
|
|
644
|
-
allNeurons.sort((a, b) => b.
|
|
677
|
+
allNeurons.sort((a, b) => b.intensity - a.intensity);
|
|
645
678
|
lines.push("## Top 10 Active Neurons");
|
|
646
679
|
lines.push("| # | Path | Counter | Strength |");
|
|
647
680
|
lines.push("|---|------|---------|----------|");
|
|
@@ -667,7 +700,7 @@ function emitIndex(result, brain) {
|
|
|
667
700
|
for (const region of result.activeRegions) {
|
|
668
701
|
const active = region.neurons.filter((n) => !n.isDormant);
|
|
669
702
|
const dormant = region.neurons.filter((n) => n.isDormant);
|
|
670
|
-
const activation = active.reduce((sum, n) => sum + n.
|
|
703
|
+
const activation = active.reduce((sum, n) => sum + n.intensity, 0);
|
|
671
704
|
const icon = REGION_ICONS[region.name] || "";
|
|
672
705
|
lines.push(`| ${icon} ${region.name} | ${active.length} | ${dormant.length} | ${activation} | [_rules.md](${region.name}/_rules.md) |`);
|
|
673
706
|
}
|
|
@@ -679,7 +712,7 @@ function emitRegionRules(region) {
|
|
|
679
712
|
const ko = REGION_KO[region.name] || "";
|
|
680
713
|
const active = region.neurons.filter((n) => !n.isDormant);
|
|
681
714
|
const dormant = region.neurons.filter((n) => n.isDormant);
|
|
682
|
-
const activation = active.reduce((sum, n) => sum + n.
|
|
715
|
+
const activation = active.reduce((sum, n) => sum + n.intensity, 0);
|
|
683
716
|
const lines = [];
|
|
684
717
|
lines.push(`# ${icon} ${region.name} (${ko})`);
|
|
685
718
|
lines.push(`> Active: ${active.length} | Dormant: ${dormant.length} | Activation: ${activation}`);
|
|
@@ -693,7 +726,7 @@ function emitRegionRules(region) {
|
|
|
693
726
|
}
|
|
694
727
|
if (active.length > 0) {
|
|
695
728
|
lines.push("## Rules");
|
|
696
|
-
const sorted = [...active].sort((a, b) => b.
|
|
729
|
+
const sorted = [...active].sort((a, b) => b.intensity - a.intensity);
|
|
697
730
|
for (const n of sorted) {
|
|
698
731
|
const indent = " ".repeat(Math.min(n.depth, 4));
|
|
699
732
|
const prefix = strengthPrefix(n.counter);
|
|
@@ -776,7 +809,7 @@ function printDiag(brain, result) {
|
|
|
776
809
|
const icon = REGION_ICONS[region.name] || "";
|
|
777
810
|
const active = region.neurons.filter((n) => !n.isDormant);
|
|
778
811
|
const dormant = region.neurons.filter((n) => n.isDormant);
|
|
779
|
-
const activation = active.reduce((sum, n) => sum + n.
|
|
812
|
+
const activation = active.reduce((sum, n) => sum + n.intensity, 0);
|
|
780
813
|
const isBlocked = result.blockedRegions.some((r) => r.name === region.name);
|
|
781
814
|
const status = region.hasBomb ? "\u{1F4A3} BOMB" : isBlocked ? "\u{1F6AB} BLOCKED" : "\u2705 ACTIVE";
|
|
782
815
|
console.log(` ${icon} ${region.name} [${status}]`);
|
|
@@ -786,7 +819,8 @@ function printDiag(brain, result) {
|
|
|
786
819
|
}
|
|
787
820
|
const top3 = sortedActive(region.neurons, 3);
|
|
788
821
|
for (const n of top3) {
|
|
789
|
-
|
|
822
|
+
const contraStr = n.contra > 0 ? ` contra:${n.contra}` : "";
|
|
823
|
+
console.log(` \u251C ${n.path} (counter:${n.counter}${contraStr} intensity:${n.intensity})`);
|
|
790
824
|
}
|
|
791
825
|
}
|
|
792
826
|
console.log("");
|
|
@@ -795,7 +829,7 @@ function pathToSentence(path) {
|
|
|
795
829
|
return path.replace(/\//g, " > ").replace(/_/g, " ");
|
|
796
830
|
}
|
|
797
831
|
function sortedActive(neurons, n) {
|
|
798
|
-
return [...neurons].filter((neuron) => !neuron.isDormant).sort((a, b) => b.
|
|
832
|
+
return [...neurons].filter((neuron) => !neuron.isDormant).sort((a, b) => b.intensity - a.intensity).slice(0, n);
|
|
799
833
|
}
|
|
800
834
|
function strengthPrefix(counter) {
|
|
801
835
|
if (counter >= 10) return "**[ABSOLUTE]** ";
|
|
@@ -922,46 +956,155 @@ ${template.description}
|
|
|
922
956
|
import { createServer } from "http";
|
|
923
957
|
|
|
924
958
|
// src/inbox.ts
|
|
925
|
-
import { readFileSync as readFileSync4, writeFileSync as writeFileSync9, existsSync as
|
|
926
|
-
import { join as
|
|
959
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync9, existsSync as existsSync12, mkdirSync as mkdirSync7 } from "fs";
|
|
960
|
+
import { join as join13 } from "path";
|
|
961
|
+
|
|
962
|
+
// src/candidates.ts
|
|
963
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync5, readdirSync as readdirSync7, renameSync as renameSync3, rmSync, statSync as statSync3 } from "fs";
|
|
964
|
+
import { join as join11, dirname as dirname2, relative as relative3 } from "path";
|
|
965
|
+
var CANDIDATE_THRESHOLD = 3;
|
|
966
|
+
var CANDIDATE_DECAY_DAYS = 14;
|
|
967
|
+
var CANDIDATE_SEGMENT = "_candidates";
|
|
968
|
+
function toCandidatePath(neuronPath) {
|
|
969
|
+
const slash = neuronPath.indexOf("/");
|
|
970
|
+
if (slash === -1) throw new Error(`Invalid neuron path (missing region): ${neuronPath}`);
|
|
971
|
+
return `${neuronPath.slice(0, slash)}/${CANDIDATE_SEGMENT}/${neuronPath.slice(slash + 1)}`;
|
|
972
|
+
}
|
|
973
|
+
function fromCandidatePath(candidatePath) {
|
|
974
|
+
return candidatePath.replace(`/${CANDIDATE_SEGMENT}/`, "/");
|
|
975
|
+
}
|
|
976
|
+
function growCandidate(brainRoot, neuronPath) {
|
|
977
|
+
const candidatePath = toCandidatePath(neuronPath);
|
|
978
|
+
const result = growNeuron(brainRoot, candidatePath);
|
|
979
|
+
if (result.counter >= CANDIDATE_THRESHOLD) {
|
|
980
|
+
const ok = moveCandidate(brainRoot, candidatePath, neuronPath);
|
|
981
|
+
return { ...result, path: ok ? neuronPath : result.path, promoted: ok };
|
|
982
|
+
}
|
|
983
|
+
console.log(` \u{1F331} candidate (${result.counter}/${CANDIDATE_THRESHOLD}): ${candidatePath}`);
|
|
984
|
+
return { ...result, promoted: false };
|
|
985
|
+
}
|
|
986
|
+
function moveCandidate(brainRoot, candidatePath, targetPath) {
|
|
987
|
+
const src = join11(brainRoot, candidatePath);
|
|
988
|
+
if (!existsSync10(src)) return false;
|
|
989
|
+
const dst = join11(brainRoot, targetPath);
|
|
990
|
+
if (existsSync10(dst)) {
|
|
991
|
+
fireNeuron(brainRoot, targetPath);
|
|
992
|
+
rmSync(src, { recursive: true, force: true });
|
|
993
|
+
} else {
|
|
994
|
+
mkdirSync5(dirname2(dst), { recursive: true });
|
|
995
|
+
renameSync3(src, dst);
|
|
996
|
+
}
|
|
997
|
+
console.log(`\u{1F393} promoted: ${candidatePath} \u2192 ${targetPath}`);
|
|
998
|
+
return true;
|
|
999
|
+
}
|
|
1000
|
+
function promoteCandidates(brainRoot) {
|
|
1001
|
+
const promoted = [];
|
|
1002
|
+
const decayed = [];
|
|
1003
|
+
const decayMs = CANDIDATE_DECAY_DAYS * 24 * 60 * 60 * 1e3;
|
|
1004
|
+
const now = Date.now();
|
|
1005
|
+
for (const region of REGIONS) {
|
|
1006
|
+
const candidateRoot = join11(brainRoot, region, CANDIDATE_SEGMENT);
|
|
1007
|
+
walkNeuronDirs(candidateRoot, (neuronDir) => {
|
|
1008
|
+
const rel = relative3(join11(brainRoot, region), neuronDir);
|
|
1009
|
+
const candidatePath = `${region}/${rel}`;
|
|
1010
|
+
const targetPath = fromCandidatePath(candidatePath);
|
|
1011
|
+
const counter = readCounter(neuronDir);
|
|
1012
|
+
const mtime = statSync3(neuronDir).mtimeMs;
|
|
1013
|
+
if (counter >= CANDIDATE_THRESHOLD) {
|
|
1014
|
+
moveCandidate(brainRoot, candidatePath, targetPath);
|
|
1015
|
+
promoted.push(targetPath);
|
|
1016
|
+
} else if (now - mtime > decayMs) {
|
|
1017
|
+
rmSync(neuronDir, { recursive: true, force: true });
|
|
1018
|
+
decayed.push(candidatePath);
|
|
1019
|
+
console.log(`\u{1F480} candidate decayed: ${candidatePath}`);
|
|
1020
|
+
}
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
return { promoted, decayed };
|
|
1024
|
+
}
|
|
1025
|
+
function listCandidates(brainRoot) {
|
|
1026
|
+
const results = [];
|
|
1027
|
+
const now = Date.now();
|
|
1028
|
+
for (const region of REGIONS) {
|
|
1029
|
+
const candidateRoot = join11(brainRoot, region, CANDIDATE_SEGMENT);
|
|
1030
|
+
walkNeuronDirs(candidateRoot, (neuronDir) => {
|
|
1031
|
+
const rel = relative3(join11(brainRoot, region), neuronDir);
|
|
1032
|
+
const candidatePath = `${region}/${rel}`;
|
|
1033
|
+
const targetPath = fromCandidatePath(candidatePath);
|
|
1034
|
+
const counter = readCounter(neuronDir);
|
|
1035
|
+
const mtime = statSync3(neuronDir).mtimeMs;
|
|
1036
|
+
const daysInactive = Math.floor((now - mtime) / (24 * 60 * 60 * 1e3));
|
|
1037
|
+
results.push({ candidatePath, targetPath, counter, daysInactive });
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1040
|
+
return results;
|
|
1041
|
+
}
|
|
1042
|
+
function walkNeuronDirs(dir, cb) {
|
|
1043
|
+
if (!existsSync10(dir)) return;
|
|
1044
|
+
try {
|
|
1045
|
+
const entries = readdirSync7(dir, { withFileTypes: true });
|
|
1046
|
+
const hasNeuron = entries.some((e) => e.isFile() && e.name.endsWith(".neuron"));
|
|
1047
|
+
if (hasNeuron) {
|
|
1048
|
+
cb(dir);
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
for (const entry of entries) {
|
|
1052
|
+
if (entry.isDirectory() && !entry.name.startsWith(".")) {
|
|
1053
|
+
walkNeuronDirs(join11(dir, entry.name), cb);
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
} catch {
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
function readCounter(dir) {
|
|
1060
|
+
try {
|
|
1061
|
+
const files = readdirSync7(dir).filter((f) => /^\d+\.neuron$/.test(f));
|
|
1062
|
+
if (files.length === 0) return 0;
|
|
1063
|
+
return Math.max(...files.map((f) => parseInt(f, 10)));
|
|
1064
|
+
} catch {
|
|
1065
|
+
return 0;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
927
1068
|
|
|
928
1069
|
// src/episode.ts
|
|
929
|
-
import { readdirSync as
|
|
930
|
-
import { join as
|
|
1070
|
+
import { readdirSync as readdirSync8, readFileSync as readFileSync3, writeFileSync as writeFileSync8, mkdirSync as mkdirSync6, existsSync as existsSync11 } from "fs";
|
|
1071
|
+
import { join as join12 } from "path";
|
|
931
1072
|
var MAX_EPISODES = 100;
|
|
932
1073
|
var SESSION_LOG_DIR = "hippocampus/session_log";
|
|
933
|
-
function logEpisode(brainRoot, type, path, detail) {
|
|
934
|
-
const logDir =
|
|
935
|
-
if (!
|
|
936
|
-
|
|
1074
|
+
function logEpisode(brainRoot, type, path, detail, extra) {
|
|
1075
|
+
const logDir = join12(brainRoot, SESSION_LOG_DIR);
|
|
1076
|
+
if (!existsSync11(logDir)) {
|
|
1077
|
+
mkdirSync6(logDir, { recursive: true });
|
|
937
1078
|
}
|
|
938
1079
|
const nextSlot = getNextSlot(logDir);
|
|
939
1080
|
const episode = {
|
|
940
1081
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
941
1082
|
type,
|
|
942
1083
|
path,
|
|
943
|
-
detail
|
|
1084
|
+
detail,
|
|
1085
|
+
...extra?.outcome ? { outcome: extra.outcome } : {},
|
|
1086
|
+
...extra?.neurons ? { neurons: extra.neurons } : {}
|
|
944
1087
|
};
|
|
945
1088
|
writeFileSync8(
|
|
946
|
-
|
|
1089
|
+
join12(logDir, `memory${nextSlot}.neuron`),
|
|
947
1090
|
JSON.stringify(episode),
|
|
948
1091
|
"utf8"
|
|
949
1092
|
);
|
|
950
1093
|
}
|
|
951
1094
|
function readEpisodes(brainRoot) {
|
|
952
|
-
const logDir =
|
|
953
|
-
if (!
|
|
1095
|
+
const logDir = join12(brainRoot, SESSION_LOG_DIR);
|
|
1096
|
+
if (!existsSync11(logDir)) return [];
|
|
954
1097
|
const episodes = [];
|
|
955
1098
|
let entries;
|
|
956
1099
|
try {
|
|
957
|
-
entries =
|
|
1100
|
+
entries = readdirSync8(logDir);
|
|
958
1101
|
} catch {
|
|
959
1102
|
return [];
|
|
960
1103
|
}
|
|
961
1104
|
for (const entry of entries) {
|
|
962
1105
|
if (!entry.startsWith("memory") || !entry.endsWith(".neuron")) continue;
|
|
963
1106
|
try {
|
|
964
|
-
const content = readFileSync3(
|
|
1107
|
+
const content = readFileSync3(join12(logDir, entry), "utf8");
|
|
965
1108
|
if (content.trim()) {
|
|
966
1109
|
episodes.push(JSON.parse(content));
|
|
967
1110
|
}
|
|
@@ -974,7 +1117,7 @@ function readEpisodes(brainRoot) {
|
|
|
974
1117
|
function getNextSlot(logDir) {
|
|
975
1118
|
let maxSlot = 0;
|
|
976
1119
|
try {
|
|
977
|
-
for (const entry of
|
|
1120
|
+
for (const entry of readdirSync8(logDir)) {
|
|
978
1121
|
if (entry.startsWith("memory") && entry.endsWith(".neuron")) {
|
|
979
1122
|
const n = parseInt(entry.replace("memory", "").replace(".neuron", ""), 10);
|
|
980
1123
|
if (!isNaN(n) && n > maxSlot) maxSlot = n;
|
|
@@ -991,8 +1134,8 @@ var INBOX_DIR = "_inbox";
|
|
|
991
1134
|
var CORRECTIONS_FILE = "corrections.jsonl";
|
|
992
1135
|
var DOPAMINE_ALLOWED_ROLES = ["pm", "admin", "lead"];
|
|
993
1136
|
function processInbox(brainRoot) {
|
|
994
|
-
const inboxPath =
|
|
995
|
-
if (!
|
|
1137
|
+
const inboxPath = join13(brainRoot, INBOX_DIR, CORRECTIONS_FILE);
|
|
1138
|
+
if (!existsSync12(inboxPath)) {
|
|
996
1139
|
return { processed: 0, skipped: 0, errors: [] };
|
|
997
1140
|
}
|
|
998
1141
|
const content = readFileSync4(inboxPath, "utf8").trim();
|
|
@@ -1047,16 +1190,18 @@ function processInbox(brainRoot) {
|
|
|
1047
1190
|
}
|
|
1048
1191
|
function applyCorrection(brainRoot, correction) {
|
|
1049
1192
|
const neuronPath = correction.path;
|
|
1050
|
-
const fullPath =
|
|
1193
|
+
const fullPath = join13(brainRoot, neuronPath);
|
|
1051
1194
|
const counterAdd = Math.max(1, correction.counter_add || 1);
|
|
1052
|
-
if (
|
|
1195
|
+
if (existsSync12(fullPath)) {
|
|
1053
1196
|
for (let i = 0; i < counterAdd; i++) {
|
|
1054
1197
|
fireNeuron(brainRoot, neuronPath);
|
|
1055
1198
|
}
|
|
1056
1199
|
} else {
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1200
|
+
const candResult = growCandidate(brainRoot, neuronPath);
|
|
1201
|
+
if (candResult.promoted) {
|
|
1202
|
+
for (let i = 1; i < counterAdd; i++) {
|
|
1203
|
+
fireNeuron(brainRoot, neuronPath);
|
|
1204
|
+
}
|
|
1060
1205
|
}
|
|
1061
1206
|
}
|
|
1062
1207
|
if (correction.dopamine && correction.dopamine > 0) {
|
|
@@ -1075,12 +1220,12 @@ function isPathSafe(path) {
|
|
|
1075
1220
|
return true;
|
|
1076
1221
|
}
|
|
1077
1222
|
function ensureInbox(brainRoot) {
|
|
1078
|
-
const inboxDir =
|
|
1079
|
-
if (!
|
|
1080
|
-
|
|
1223
|
+
const inboxDir = join13(brainRoot, INBOX_DIR);
|
|
1224
|
+
if (!existsSync12(inboxDir)) {
|
|
1225
|
+
mkdirSync7(inboxDir, { recursive: true });
|
|
1081
1226
|
}
|
|
1082
|
-
const filePath =
|
|
1083
|
-
if (!
|
|
1227
|
+
const filePath = join13(inboxDir, CORRECTIONS_FILE);
|
|
1228
|
+
if (!existsSync12(filePath)) {
|
|
1084
1229
|
writeFileSync9(filePath, "", "utf8");
|
|
1085
1230
|
}
|
|
1086
1231
|
return filePath;
|
|
@@ -1153,10 +1298,20 @@ function json(res, data, status = 200) {
|
|
|
1153
1298
|
function error(res, message, status = 400) {
|
|
1154
1299
|
json(res, { error: message }, status);
|
|
1155
1300
|
}
|
|
1301
|
+
var MAX_BODY_BYTES = 1048576;
|
|
1156
1302
|
async function readBody(req) {
|
|
1157
1303
|
return new Promise((resolve3, reject) => {
|
|
1158
1304
|
const chunks = [];
|
|
1159
|
-
|
|
1305
|
+
let total = 0;
|
|
1306
|
+
req.on("data", (chunk) => {
|
|
1307
|
+
total += chunk.length;
|
|
1308
|
+
if (total > MAX_BODY_BYTES) {
|
|
1309
|
+
reject(new Error("Request body too large"));
|
|
1310
|
+
req.destroy();
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
chunks.push(chunk);
|
|
1314
|
+
});
|
|
1160
1315
|
req.on("end", () => resolve3(Buffer.concat(chunks).toString("utf8")));
|
|
1161
1316
|
req.on("error", reject);
|
|
1162
1317
|
});
|
|
@@ -1348,19 +1503,27 @@ function clearReports() {
|
|
|
1348
1503
|
}
|
|
1349
1504
|
|
|
1350
1505
|
// src/hooks.ts
|
|
1351
|
-
import { readFileSync as readFileSync5, writeFileSync as writeFileSync10, existsSync as
|
|
1506
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync10, existsSync as existsSync13, mkdirSync as mkdirSync8, readdirSync as readdirSync9 } from "fs";
|
|
1352
1507
|
import { execSync as execSync2 } from "child_process";
|
|
1353
|
-
import { join as
|
|
1508
|
+
import { join as join14, resolve as resolve2 } from "path";
|
|
1354
1509
|
var SETTINGS_DIR = ".claude";
|
|
1355
1510
|
var SETTINGS_FILE = "settings.local.json";
|
|
1356
|
-
function installHooks(brainRoot, projectRoot) {
|
|
1511
|
+
function installHooks(brainRoot, projectRoot, global) {
|
|
1357
1512
|
const root = projectRoot || process.cwd();
|
|
1358
1513
|
const resolvedBrain = resolve2(brainRoot);
|
|
1359
|
-
if (
|
|
1514
|
+
if (global) {
|
|
1515
|
+
const home = process.env.HOME || "~";
|
|
1516
|
+
if (!brainRoot.startsWith("/") && !brainRoot.startsWith(home)) {
|
|
1517
|
+
console.error("\u274C --global requires an absolute --brain path (e.g. --brain ~/brain)");
|
|
1518
|
+
process.exit(1);
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
if (!existsSync13(resolvedBrain) || !hasBrainRegions(resolvedBrain)) {
|
|
1360
1522
|
initBrain(resolvedBrain);
|
|
1361
1523
|
}
|
|
1362
|
-
const settingsDir =
|
|
1363
|
-
const
|
|
1524
|
+
const settingsDir = global ? resolve2(process.env.HOME || "~", SETTINGS_DIR) : join14(root, SETTINGS_DIR);
|
|
1525
|
+
const settingsFile = global ? "settings.json" : SETTINGS_FILE;
|
|
1526
|
+
const settingsPath = join14(settingsDir, settingsFile);
|
|
1364
1527
|
const defaultBrain = resolve2(root, "brain");
|
|
1365
1528
|
const brainFlag = resolvedBrain === defaultBrain ? "" : ` --brain ${resolvedBrain}`;
|
|
1366
1529
|
let npxBin = "npx";
|
|
@@ -1369,7 +1532,7 @@ function installHooks(brainRoot, projectRoot) {
|
|
|
1369
1532
|
} catch {
|
|
1370
1533
|
}
|
|
1371
1534
|
let settings = {};
|
|
1372
|
-
if (
|
|
1535
|
+
if (existsSync13(settingsPath)) {
|
|
1373
1536
|
try {
|
|
1374
1537
|
settings = JSON.parse(readFileSync5(settingsPath, "utf8"));
|
|
1375
1538
|
} catch {
|
|
@@ -1386,8 +1549,8 @@ function installHooks(brainRoot, projectRoot) {
|
|
|
1386
1549
|
matcher: "startup|resume",
|
|
1387
1550
|
entry: {
|
|
1388
1551
|
type: "command",
|
|
1389
|
-
command: `${npxBin} hebbian emit claude${brainFlag}`,
|
|
1390
|
-
timeout:
|
|
1552
|
+
command: `${npxBin} hebbian emit claude${brainFlag} && ${npxBin} hebbian session start${brainFlag}`,
|
|
1553
|
+
timeout: 15,
|
|
1391
1554
|
statusMessage: `${HOOK_MARKER} refreshing brain`
|
|
1392
1555
|
}
|
|
1393
1556
|
},
|
|
@@ -1395,7 +1558,7 @@ function installHooks(brainRoot, projectRoot) {
|
|
|
1395
1558
|
event: "Stop",
|
|
1396
1559
|
entry: {
|
|
1397
1560
|
type: "command",
|
|
1398
|
-
command: `${npxBin} hebbian digest${brainFlag}`,
|
|
1561
|
+
command: `${npxBin} hebbian digest${brainFlag}; ${npxBin} hebbian session end${brainFlag}`,
|
|
1399
1562
|
timeout: 30,
|
|
1400
1563
|
statusMessage: `${HOOK_MARKER} digesting session`
|
|
1401
1564
|
}
|
|
@@ -1418,18 +1581,20 @@ function installHooks(brainRoot, projectRoot) {
|
|
|
1418
1581
|
hooks[event].push(group);
|
|
1419
1582
|
}
|
|
1420
1583
|
}
|
|
1421
|
-
if (!
|
|
1422
|
-
|
|
1584
|
+
if (!existsSync13(settingsDir)) {
|
|
1585
|
+
mkdirSync8(settingsDir, { recursive: true });
|
|
1423
1586
|
}
|
|
1424
1587
|
writeFileSync10(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
1425
1588
|
console.log(`\u2705 hebbian hooks installed at ${settingsPath}`);
|
|
1426
1589
|
console.log(` SessionStart \u2192 ${npxBin} hebbian emit claude${brainFlag}`);
|
|
1427
1590
|
console.log(` Stop \u2192 ${npxBin} hebbian digest${brainFlag}`);
|
|
1428
1591
|
}
|
|
1429
|
-
function uninstallHooks(projectRoot) {
|
|
1592
|
+
function uninstallHooks(projectRoot, global) {
|
|
1430
1593
|
const root = projectRoot || process.cwd();
|
|
1431
|
-
const
|
|
1432
|
-
|
|
1594
|
+
const settingsDir = global ? resolve2(process.env.HOME || "~", SETTINGS_DIR) : join14(root, SETTINGS_DIR);
|
|
1595
|
+
const settingsFile = global ? "settings.json" : SETTINGS_FILE;
|
|
1596
|
+
const settingsPath = join14(settingsDir, settingsFile);
|
|
1597
|
+
if (!existsSync13(settingsPath)) {
|
|
1433
1598
|
console.log("No hooks installed (settings.local.json not found)");
|
|
1434
1599
|
return;
|
|
1435
1600
|
}
|
|
@@ -1462,15 +1627,17 @@ function uninstallHooks(projectRoot) {
|
|
|
1462
1627
|
writeFileSync10(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
1463
1628
|
console.log(`\u2705 removed ${removed} hebbian hook(s) from ${settingsPath}`);
|
|
1464
1629
|
}
|
|
1465
|
-
function checkHooks(projectRoot) {
|
|
1630
|
+
function checkHooks(projectRoot, global) {
|
|
1466
1631
|
const root = projectRoot || process.cwd();
|
|
1467
|
-
const
|
|
1632
|
+
const settingsDir = global ? resolve2(process.env.HOME || "~", SETTINGS_DIR) : join14(root, SETTINGS_DIR);
|
|
1633
|
+
const settingsFile = global ? "settings.json" : SETTINGS_FILE;
|
|
1634
|
+
const settingsPath = join14(settingsDir, settingsFile);
|
|
1468
1635
|
const status = {
|
|
1469
1636
|
installed: false,
|
|
1470
1637
|
path: settingsPath,
|
|
1471
1638
|
events: []
|
|
1472
1639
|
};
|
|
1473
|
-
if (!
|
|
1640
|
+
if (!existsSync13(settingsPath)) {
|
|
1474
1641
|
console.log(`\u274C hebbian hooks not installed (${settingsPath} not found)`);
|
|
1475
1642
|
return status;
|
|
1476
1643
|
}
|
|
@@ -1506,9 +1673,9 @@ function checkHooks(projectRoot) {
|
|
|
1506
1673
|
return status;
|
|
1507
1674
|
}
|
|
1508
1675
|
function hasBrainRegions(dir) {
|
|
1509
|
-
if (!
|
|
1676
|
+
if (!existsSync13(dir)) return false;
|
|
1510
1677
|
try {
|
|
1511
|
-
const entries =
|
|
1678
|
+
const entries = readdirSync9(dir);
|
|
1512
1679
|
return REGIONS.some((r) => entries.includes(r));
|
|
1513
1680
|
} catch {
|
|
1514
1681
|
return false;
|
|
@@ -1516,8 +1683,8 @@ function hasBrainRegions(dir) {
|
|
|
1516
1683
|
}
|
|
1517
1684
|
|
|
1518
1685
|
// src/digest.ts
|
|
1519
|
-
import { readFileSync as readFileSync6, writeFileSync as writeFileSync11, existsSync as
|
|
1520
|
-
import { join as
|
|
1686
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync11, existsSync as existsSync14, mkdirSync as mkdirSync9 } from "fs";
|
|
1687
|
+
import { join as join15, basename } from "path";
|
|
1521
1688
|
var NEGATION_PATTERNS = [
|
|
1522
1689
|
/\bdon[''\u2019]?t\b/i,
|
|
1523
1690
|
/\bdo not\b/i,
|
|
@@ -1568,13 +1735,13 @@ function readHookInput(stdin) {
|
|
|
1568
1735
|
}
|
|
1569
1736
|
}
|
|
1570
1737
|
function digestTranscript(brainRoot, transcriptPath, sessionId) {
|
|
1571
|
-
if (!
|
|
1738
|
+
if (!existsSync14(transcriptPath)) {
|
|
1572
1739
|
throw new Error(`Transcript not found: ${transcriptPath}`);
|
|
1573
1740
|
}
|
|
1574
1741
|
const resolvedSessionId = sessionId || basename(transcriptPath, ".jsonl");
|
|
1575
|
-
const logDir =
|
|
1576
|
-
const logPath =
|
|
1577
|
-
if (
|
|
1742
|
+
const logDir = join15(brainRoot, DIGEST_LOG_DIR);
|
|
1743
|
+
const logPath = join15(logDir, `${resolvedSessionId}.jsonl`);
|
|
1744
|
+
if (existsSync14(logPath)) {
|
|
1578
1745
|
console.log(`\u23ED already digested session ${resolvedSessionId}, skip`);
|
|
1579
1746
|
return { corrections: 0, skipped: 0, transcriptPath, sessionId: resolvedSessionId };
|
|
1580
1747
|
}
|
|
@@ -1589,7 +1756,7 @@ function digestTranscript(brainRoot, transcriptPath, sessionId) {
|
|
|
1589
1756
|
const auditEntries = [];
|
|
1590
1757
|
for (const correction of corrections) {
|
|
1591
1758
|
try {
|
|
1592
|
-
|
|
1759
|
+
growCandidate(brainRoot, correction.path);
|
|
1593
1760
|
logEpisode(brainRoot, "digest", correction.path, correction.text);
|
|
1594
1761
|
auditEntries.push({ correction, applied: true });
|
|
1595
1762
|
applied++;
|
|
@@ -1641,6 +1808,8 @@ function extractCorrections(messages) {
|
|
|
1641
1808
|
if (text.length < MIN_CORRECTION_LENGTH) continue;
|
|
1642
1809
|
if (/^[\/!]/.test(text.trim())) continue;
|
|
1643
1810
|
if (text.trim().endsWith("?")) continue;
|
|
1811
|
+
if (/^<[a-zA-Z]/.test(text.trim())) continue;
|
|
1812
|
+
if (/^Base directory for this skill:/i.test(text.trim())) continue;
|
|
1644
1813
|
const correction = detectCorrection(text);
|
|
1645
1814
|
if (correction) {
|
|
1646
1815
|
corrections.push(correction);
|
|
@@ -1806,11 +1975,11 @@ function extractKeywords(text) {
|
|
|
1806
1975
|
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
1976
|
}
|
|
1808
1977
|
function writeAuditLog(brainRoot, sessionId, entries) {
|
|
1809
|
-
const logDir =
|
|
1810
|
-
if (!
|
|
1811
|
-
|
|
1978
|
+
const logDir = join15(brainRoot, DIGEST_LOG_DIR);
|
|
1979
|
+
if (!existsSync14(logDir)) {
|
|
1980
|
+
mkdirSync9(logDir, { recursive: true });
|
|
1812
1981
|
}
|
|
1813
|
-
const logPath =
|
|
1982
|
+
const logPath = join15(logDir, `${sessionId}.jsonl`);
|
|
1814
1983
|
const lines = entries.map(
|
|
1815
1984
|
(e) => JSON.stringify({
|
|
1816
1985
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -1824,22 +1993,262 @@ function writeAuditLog(brainRoot, sessionId, entries) {
|
|
|
1824
1993
|
writeFileSync11(logPath, lines.join("\n") + (lines.length > 0 ? "\n" : ""), "utf8");
|
|
1825
1994
|
}
|
|
1826
1995
|
|
|
1996
|
+
// src/evolve.ts
|
|
1997
|
+
import { existsSync as existsSync16, readFileSync as readFileSync8, writeFileSync as writeFileSync13 } from "fs";
|
|
1998
|
+
import { join as join17 } from "path";
|
|
1999
|
+
|
|
2000
|
+
// src/outcome.ts
|
|
2001
|
+
import { execSync as execSync3 } from "child_process";
|
|
2002
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync10, writeFileSync as writeFileSync12, readFileSync as readFileSync7, readdirSync as readdirSync10, rmSync as rmSync2, statSync as statSync4 } from "fs";
|
|
2003
|
+
import { join as join16 } from "path";
|
|
2004
|
+
import { randomUUID } from "crypto";
|
|
2005
|
+
function captureSessionStart(brainRoot) {
|
|
2006
|
+
let sha;
|
|
2007
|
+
try {
|
|
2008
|
+
sha = execSync3("git rev-parse HEAD", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
2009
|
+
} catch {
|
|
2010
|
+
console.log("\u23ED\uFE0F session start: not a git repo, skipping");
|
|
2011
|
+
return null;
|
|
2012
|
+
}
|
|
2013
|
+
let status;
|
|
2014
|
+
try {
|
|
2015
|
+
const raw = execSync3("git status --porcelain", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
2016
|
+
status = raw ? raw.split("\n") : [];
|
|
2017
|
+
} catch {
|
|
2018
|
+
status = [];
|
|
2019
|
+
}
|
|
2020
|
+
const brain = scanBrain(brainRoot);
|
|
2021
|
+
const result = runSubsumption(brain);
|
|
2022
|
+
const neurons = [];
|
|
2023
|
+
for (const region of result.activeRegions) {
|
|
2024
|
+
for (const neuron of region.neurons) {
|
|
2025
|
+
if (!neuron.isDormant && neuron.counter > 0) {
|
|
2026
|
+
neurons.push(`${region.name}/${neuron.path}`);
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
const uuid = randomUUID();
|
|
2031
|
+
const stateDir = join16(brainRoot, SESSION_STATE_DIR);
|
|
2032
|
+
if (!existsSync15(stateDir)) {
|
|
2033
|
+
mkdirSync10(stateDir, { recursive: true });
|
|
2034
|
+
}
|
|
2035
|
+
const state = { ts: (/* @__PURE__ */ new Date()).toISOString(), sha, status, neurons, uuid };
|
|
2036
|
+
writeFileSync12(join16(stateDir, `state_${uuid}.json`), JSON.stringify(state), "utf8");
|
|
2037
|
+
console.log(`\u{1F4F8} session start: SHA ${sha.slice(0, 7)}, ${neurons.length} active neurons`);
|
|
2038
|
+
return state;
|
|
2039
|
+
}
|
|
2040
|
+
function detectOutcome(brainRoot) {
|
|
2041
|
+
const state = readLatestSessionState(brainRoot);
|
|
2042
|
+
if (!state) {
|
|
2043
|
+
console.log("\u23ED\uFE0F session end: no session state found, skipping");
|
|
2044
|
+
return null;
|
|
2045
|
+
}
|
|
2046
|
+
let currentSha;
|
|
2047
|
+
try {
|
|
2048
|
+
currentSha = execSync3("git rev-parse HEAD", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
2049
|
+
} catch {
|
|
2050
|
+
console.log("\u23ED\uFE0F session end: not a git repo, skipping");
|
|
2051
|
+
cleanupSessionState(brainRoot, state.uuid);
|
|
2052
|
+
return null;
|
|
2053
|
+
}
|
|
2054
|
+
let currentStatus;
|
|
2055
|
+
try {
|
|
2056
|
+
const raw = execSync3("git status --porcelain", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
2057
|
+
currentStatus = raw ? filterHebbianPaths(raw.split("\n")) : [];
|
|
2058
|
+
} catch {
|
|
2059
|
+
currentStatus = [];
|
|
2060
|
+
}
|
|
2061
|
+
const filteredStartStatus = filterHebbianPaths(state.status);
|
|
2062
|
+
const outcome = classifyOutcome(
|
|
2063
|
+
{ ...state, status: filteredStartStatus },
|
|
2064
|
+
currentSha,
|
|
2065
|
+
currentStatus
|
|
2066
|
+
);
|
|
2067
|
+
if (!outcome) {
|
|
2068
|
+
console.log("\u{1F4CA} session end: no changes detected (no-op)");
|
|
2069
|
+
cleanupSessionState(brainRoot, state.uuid);
|
|
2070
|
+
return null;
|
|
2071
|
+
}
|
|
2072
|
+
const neurons = state.neurons;
|
|
2073
|
+
logEpisode(brainRoot, "session-end", "", `outcome:${outcome}`, { outcome, neurons });
|
|
2074
|
+
let result;
|
|
2075
|
+
if (outcome === "revert") {
|
|
2076
|
+
const { affected, skipped } = applyContra(brainRoot, neurons);
|
|
2077
|
+
result = {
|
|
2078
|
+
outcome: "revert",
|
|
2079
|
+
neuronsAffected: affected,
|
|
2080
|
+
protectedSkipped: skipped,
|
|
2081
|
+
detail: `${affected} neurons contra'd (${skipped} protected skipped)`
|
|
2082
|
+
};
|
|
2083
|
+
console.log(`\u{1F4CA} session end: revert \u2014 ${result.detail}`);
|
|
2084
|
+
} else {
|
|
2085
|
+
result = {
|
|
2086
|
+
outcome: "acceptance",
|
|
2087
|
+
neuronsAffected: 0,
|
|
2088
|
+
protectedSkipped: 0,
|
|
2089
|
+
detail: "changes accepted"
|
|
2090
|
+
};
|
|
2091
|
+
console.log("\u{1F4CA} session end: acceptance");
|
|
2092
|
+
}
|
|
2093
|
+
cleanupSessionState(brainRoot, state.uuid);
|
|
2094
|
+
return result;
|
|
2095
|
+
}
|
|
2096
|
+
function classifyOutcome(state, currentSha, currentStatus) {
|
|
2097
|
+
const headMoved = state.sha !== currentSha;
|
|
2098
|
+
const startStatusSet = new Set(state.status);
|
|
2099
|
+
const endStatusSet = new Set(currentStatus);
|
|
2100
|
+
const newItems = currentStatus.filter((s) => !startStatusSet.has(s));
|
|
2101
|
+
const removedItems = state.status.filter((s) => !endStatusSet.has(s));
|
|
2102
|
+
if (!headMoved) {
|
|
2103
|
+
if (newItems.length === 0 && removedItems.length === 0) {
|
|
2104
|
+
return null;
|
|
2105
|
+
}
|
|
2106
|
+
if (newItems.length > 0) {
|
|
2107
|
+
return "acceptance";
|
|
2108
|
+
}
|
|
2109
|
+
if (removedItems.length > 0) {
|
|
2110
|
+
return "revert";
|
|
2111
|
+
}
|
|
2112
|
+
return null;
|
|
2113
|
+
}
|
|
2114
|
+
if (newItems.length > 0) {
|
|
2115
|
+
return "acceptance";
|
|
2116
|
+
}
|
|
2117
|
+
try {
|
|
2118
|
+
const diffStat = execSync3(
|
|
2119
|
+
`git diff ${state.sha}..${currentSha} --stat`,
|
|
2120
|
+
{ encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }
|
|
2121
|
+
).trim();
|
|
2122
|
+
const logOutput = execSync3(
|
|
2123
|
+
`git log --oneline ${state.sha}..${currentSha}`,
|
|
2124
|
+
{ encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }
|
|
2125
|
+
).trim();
|
|
2126
|
+
if (/\brevert\b/i.test(logOutput)) {
|
|
2127
|
+
return "revert";
|
|
2128
|
+
}
|
|
2129
|
+
if (!diffStat) {
|
|
2130
|
+
return "revert";
|
|
2131
|
+
}
|
|
2132
|
+
return "acceptance";
|
|
2133
|
+
} catch {
|
|
2134
|
+
return null;
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
function applyContra(brainRoot, neurons) {
|
|
2138
|
+
let affected = 0;
|
|
2139
|
+
let skipped = 0;
|
|
2140
|
+
for (const neuronPath of neurons) {
|
|
2141
|
+
const region = neuronPath.split("/")[0] || "";
|
|
2142
|
+
if (PROTECTED_REGIONS_CONTRA.includes(region)) {
|
|
2143
|
+
skipped++;
|
|
2144
|
+
continue;
|
|
2145
|
+
}
|
|
2146
|
+
const result = contraNeuron(brainRoot, neuronPath);
|
|
2147
|
+
if (result > 0) {
|
|
2148
|
+
affected++;
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
return { affected, skipped };
|
|
2152
|
+
}
|
|
2153
|
+
function buildOutcomeSummary(brainRoot) {
|
|
2154
|
+
const episodes = readEpisodes(brainRoot);
|
|
2155
|
+
const outcomeEpisodes = episodes.filter((e) => e.outcome && e.neurons);
|
|
2156
|
+
if (outcomeEpisodes.length === 0) return "";
|
|
2157
|
+
const stats = /* @__PURE__ */ new Map();
|
|
2158
|
+
for (const ep of outcomeEpisodes) {
|
|
2159
|
+
for (const neuron of ep.neurons) {
|
|
2160
|
+
const existing = stats.get(neuron) || { sessions: 0, reverts: 0, acceptances: 0 };
|
|
2161
|
+
existing.sessions++;
|
|
2162
|
+
if (ep.outcome === "revert") existing.reverts++;
|
|
2163
|
+
if (ep.outcome === "acceptance") existing.acceptances++;
|
|
2164
|
+
stats.set(neuron, existing);
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
const lines = ["## Outcome Signals (from session history)\n"];
|
|
2168
|
+
lines.push("Neurons with high contra_ratio (>0.5) are consistently present in reverted sessions. Consider pruning or modifying them.\n");
|
|
2169
|
+
const sorted = [...stats.entries()].sort((a, b) => {
|
|
2170
|
+
const ratioA = a[1].sessions > 0 ? a[1].reverts / a[1].sessions : 0;
|
|
2171
|
+
const ratioB = b[1].sessions > 0 ? b[1].reverts / b[1].sessions : 0;
|
|
2172
|
+
return ratioB - ratioA;
|
|
2173
|
+
});
|
|
2174
|
+
for (const [neuron, s] of sorted) {
|
|
2175
|
+
const ratio = s.sessions > 0 ? (s.reverts / s.sessions).toFixed(2) : "0.00";
|
|
2176
|
+
const trend = parseFloat(ratio) > 0.5 ? "act on this" : parseFloat(ratio) > 0.3 ? "watch" : "";
|
|
2177
|
+
const safePath = neuron.replace(/[\n\r#]/g, " ").trim();
|
|
2178
|
+
lines.push(`- ${safePath}: sessions=${s.sessions} reverts=${s.reverts} acceptances=${s.acceptances} contra_ratio=${ratio} ${trend}`);
|
|
2179
|
+
}
|
|
2180
|
+
lines.push("");
|
|
2181
|
+
return lines.join("\n");
|
|
2182
|
+
}
|
|
2183
|
+
function readLatestSessionState(brainRoot) {
|
|
2184
|
+
const stateDir = join16(brainRoot, SESSION_STATE_DIR);
|
|
2185
|
+
if (!existsSync15(stateDir)) return null;
|
|
2186
|
+
let latest = null;
|
|
2187
|
+
try {
|
|
2188
|
+
for (const entry of readdirSync10(stateDir)) {
|
|
2189
|
+
if (!entry.startsWith("state_") || !entry.endsWith(".json")) continue;
|
|
2190
|
+
const fullPath = join16(stateDir, entry);
|
|
2191
|
+
const mtime = statSync4(fullPath).mtimeMs;
|
|
2192
|
+
if (!latest || mtime > latest.mtime) {
|
|
2193
|
+
latest = { path: fullPath, mtime };
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
} catch {
|
|
2197
|
+
return null;
|
|
2198
|
+
}
|
|
2199
|
+
if (!latest) return null;
|
|
2200
|
+
try {
|
|
2201
|
+
return JSON.parse(readFileSync7(latest.path, "utf8"));
|
|
2202
|
+
} catch {
|
|
2203
|
+
return null;
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
function filterHebbianPaths(statusLines) {
|
|
2207
|
+
const hebbianPatterns = ["hippocampus/session_state", "hippocampus/session_log", "hippocampus/digest_log", "_inbox/"];
|
|
2208
|
+
return statusLines.filter(
|
|
2209
|
+
(line) => !hebbianPatterns.some((p) => line.includes(p))
|
|
2210
|
+
);
|
|
2211
|
+
}
|
|
2212
|
+
function cleanupSessionState(brainRoot, uuid) {
|
|
2213
|
+
const stateDir = join16(brainRoot, SESSION_STATE_DIR);
|
|
2214
|
+
const filePath = join16(stateDir, `state_${uuid}.json`);
|
|
2215
|
+
try {
|
|
2216
|
+
if (existsSync15(filePath)) rmSync2(filePath);
|
|
2217
|
+
} catch {
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
|
|
1827
2221
|
// src/evolve.ts
|
|
1828
2222
|
var MAX_ACTIONS = 10;
|
|
1829
2223
|
var PROTECTED_REGIONS = ["brainstem", "limbic", "sensors"];
|
|
1830
2224
|
var DEFAULT_MODEL = "gemini-2.0-flash-lite";
|
|
1831
2225
|
var API_TIMEOUT = 3e4;
|
|
1832
2226
|
var RETRY_DELAY = 5e3;
|
|
2227
|
+
var EVOLVE_COOLDOWN_FILE = "hippocampus/evolve_last_run";
|
|
1833
2228
|
async function runEvolve(brainRoot, dryRun) {
|
|
1834
2229
|
const apiKey = process.env.GEMINI_API_KEY;
|
|
1835
2230
|
if (!apiKey) {
|
|
1836
2231
|
console.error("\u274C GEMINI_API_KEY not set. Get one at https://aistudio.google.com/apikey");
|
|
1837
2232
|
return { actions: [], executed: 0, skipped: 0, dryRun };
|
|
1838
2233
|
}
|
|
2234
|
+
if (!dryRun && process.env.EVOLVE_NO_COOLDOWN !== "1") {
|
|
2235
|
+
const cooldownMs = (parseInt(process.env.EVOLVE_COOLDOWN_SECONDS ?? "60", 10) || 60) * 1e3;
|
|
2236
|
+
const cooldownPath = join17(brainRoot, EVOLVE_COOLDOWN_FILE);
|
|
2237
|
+
if (existsSync16(cooldownPath)) {
|
|
2238
|
+
const lastRun = parseInt(readFileSync8(cooldownPath, "utf8").trim(), 10);
|
|
2239
|
+
const elapsed = Date.now() - lastRun;
|
|
2240
|
+
if (elapsed < cooldownMs) {
|
|
2241
|
+
const remaining = Math.ceil((cooldownMs - elapsed) / 1e3);
|
|
2242
|
+
console.log(`\u23F3 evolve cooldown: ${remaining}s remaining (use EVOLVE_NO_COOLDOWN=1 to bypass)`);
|
|
2243
|
+
return { actions: [], executed: 0, skipped: 0, dryRun };
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
1839
2247
|
const episodes = readEpisodes(brainRoot);
|
|
1840
2248
|
const brain = scanBrain(brainRoot);
|
|
1841
2249
|
const summary = buildBrainSummary(brain);
|
|
1842
|
-
const
|
|
2250
|
+
const outcomeSummary = buildOutcomeSummary(brainRoot);
|
|
2251
|
+
const prompt = buildPrompt(summary, episodes, outcomeSummary);
|
|
1843
2252
|
let rawActions;
|
|
1844
2253
|
try {
|
|
1845
2254
|
rawActions = await callGemini(prompt, apiKey);
|
|
@@ -1865,6 +2274,7 @@ async function runEvolve(brainRoot, dryRun) {
|
|
|
1865
2274
|
const executed = executeActions(brainRoot, actions);
|
|
1866
2275
|
logEpisode(brainRoot, "evolve", "", `${executed} action(s) executed, ${skipped} skipped`);
|
|
1867
2276
|
console.log(`\u{1F9E0} evolve: ${executed} action(s) executed, ${skipped} skipped`);
|
|
2277
|
+
writeFileSync13(join17(brainRoot, EVOLVE_COOLDOWN_FILE), String(Date.now()), "utf8");
|
|
1868
2278
|
return { actions, executed, skipped, dryRun: false };
|
|
1869
2279
|
}
|
|
1870
2280
|
function buildBrainSummary(brain) {
|
|
@@ -1887,8 +2297,13 @@ function buildBrainSummary(brain) {
|
|
|
1887
2297
|
}
|
|
1888
2298
|
return lines.join("\n");
|
|
1889
2299
|
}
|
|
1890
|
-
function
|
|
1891
|
-
const
|
|
2300
|
+
function sanitizeForPrompt(text) {
|
|
2301
|
+
const firstLine = (text.split("\n")[0] ?? "").trim();
|
|
2302
|
+
return firstLine.replace(/^#+\s*/g, "").slice(0, 200);
|
|
2303
|
+
}
|
|
2304
|
+
function buildPrompt(summary, episodes, outcomeSummary) {
|
|
2305
|
+
const episodeLines = episodes.length > 0 ? episodes.map((e) => `- [${e.ts}] ${e.type}: ${e.path} \u2014 ${sanitizeForPrompt(e.detail)}`).join("\n") : "(no recent episodes)";
|
|
2306
|
+
const outcomeSection = outcomeSummary || "";
|
|
1892
2307
|
return `You are the evolve engine for a hebbian brain \u2014 a filesystem-based memory system for AI agents.
|
|
1893
2308
|
|
|
1894
2309
|
## Axioms
|
|
@@ -1900,6 +2315,7 @@ function buildPrompt(summary, episodes) {
|
|
|
1900
2315
|
## Current Brain
|
|
1901
2316
|
${summary}
|
|
1902
2317
|
|
|
2318
|
+
${outcomeSection}
|
|
1903
2319
|
## Recent Episodes (last ${episodes.length})
|
|
1904
2320
|
${episodeLines}
|
|
1905
2321
|
|
|
@@ -1925,7 +2341,7 @@ Respond with a JSON array of actions:
|
|
|
1925
2341
|
}
|
|
1926
2342
|
async function callGemini(prompt, apiKey) {
|
|
1927
2343
|
const model = process.env.EVOLVE_MODEL || DEFAULT_MODEL;
|
|
1928
|
-
const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent
|
|
2344
|
+
const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent`;
|
|
1929
2345
|
const body = {
|
|
1930
2346
|
contents: [{ parts: [{ text: prompt }] }],
|
|
1931
2347
|
generationConfig: {
|
|
@@ -1941,7 +2357,7 @@ async function callGemini(prompt, apiKey) {
|
|
|
1941
2357
|
try {
|
|
1942
2358
|
const res = await fetch(url, {
|
|
1943
2359
|
method: "POST",
|
|
1944
|
-
headers: { "Content-Type": "application/json" },
|
|
2360
|
+
headers: { "Content-Type": "application/json", "x-goog-api-key": apiKey },
|
|
1945
2361
|
body: JSON.stringify(body),
|
|
1946
2362
|
signal: AbortSignal.timeout(API_TIMEOUT)
|
|
1947
2363
|
});
|
|
@@ -1995,6 +2411,10 @@ function parseActions(text) {
|
|
|
1995
2411
|
}
|
|
1996
2412
|
function validateActions(actions, _brain) {
|
|
1997
2413
|
return actions.filter((action) => {
|
|
2414
|
+
if (action.path.includes("..") || action.path.startsWith("/")) {
|
|
2415
|
+
console.log(` \u26A0\uFE0F blocked: ${action.type} ${action.path} (path traversal)`);
|
|
2416
|
+
return false;
|
|
2417
|
+
}
|
|
1998
2418
|
const region = action.path.split("/")[0];
|
|
1999
2419
|
if (!region || PROTECTED_REGIONS.includes(region)) {
|
|
2000
2420
|
console.log(` \u{1F6E1}\uFE0F blocked: ${action.type} ${action.path} (protected region)`);
|
|
@@ -2020,7 +2440,7 @@ function executeActions(brainRoot, actions) {
|
|
|
2020
2440
|
fireNeuron(brainRoot, action.path);
|
|
2021
2441
|
break;
|
|
2022
2442
|
case "grow":
|
|
2023
|
-
|
|
2443
|
+
growCandidate(brainRoot, action.path);
|
|
2024
2444
|
break;
|
|
2025
2445
|
case "signal":
|
|
2026
2446
|
signalNeuron(brainRoot, action.path, action.signal || "dopamine");
|
|
@@ -2068,15 +2488,23 @@ export {
|
|
|
2068
2488
|
MAX_CORRECTIONS_PER_SESSION,
|
|
2069
2489
|
MAX_DEPTH,
|
|
2070
2490
|
MIN_CORRECTION_LENGTH,
|
|
2491
|
+
OUTCOME_TYPES,
|
|
2492
|
+
PROTECTED_REGIONS_CONTRA,
|
|
2071
2493
|
REGIONS,
|
|
2072
2494
|
REGION_ICONS,
|
|
2073
2495
|
REGION_KO,
|
|
2074
2496
|
REGION_PRIORITY,
|
|
2497
|
+
SESSION_STATE_DIR,
|
|
2075
2498
|
SIGNAL_TYPES,
|
|
2076
2499
|
SPOTLIGHT_DAYS,
|
|
2077
2500
|
appendCorrection,
|
|
2501
|
+
buildOutcomeSummary,
|
|
2502
|
+
captureSessionStart,
|
|
2078
2503
|
checkHooks,
|
|
2504
|
+
classifyOutcome,
|
|
2079
2505
|
clearReports,
|
|
2506
|
+
contraNeuron,
|
|
2507
|
+
detectOutcome,
|
|
2080
2508
|
digestTranscript,
|
|
2081
2509
|
emitBootstrap,
|
|
2082
2510
|
emitIndex,
|
|
@@ -2085,17 +2513,21 @@ export {
|
|
|
2085
2513
|
ensureInbox,
|
|
2086
2514
|
extractCorrections,
|
|
2087
2515
|
fireNeuron,
|
|
2516
|
+
fromCandidatePath,
|
|
2088
2517
|
getCurrentCounter,
|
|
2089
2518
|
getLastActivity,
|
|
2090
2519
|
getPendingReports,
|
|
2091
2520
|
gitSnapshot,
|
|
2521
|
+
growCandidate,
|
|
2092
2522
|
growNeuron,
|
|
2093
2523
|
initBrain,
|
|
2094
2524
|
installHooks,
|
|
2095
2525
|
jaccardSimilarity,
|
|
2526
|
+
listCandidates,
|
|
2096
2527
|
logEpisode,
|
|
2097
2528
|
printDiag,
|
|
2098
2529
|
processInbox,
|
|
2530
|
+
promoteCandidates,
|
|
2099
2531
|
readEpisodes,
|
|
2100
2532
|
readHookInput,
|
|
2101
2533
|
resolveBrainRoot,
|
|
@@ -2109,6 +2541,7 @@ export {
|
|
|
2109
2541
|
startAPI,
|
|
2110
2542
|
startWatch,
|
|
2111
2543
|
stem,
|
|
2544
|
+
toCandidatePath,
|
|
2112
2545
|
tokenize,
|
|
2113
2546
|
uninstallHooks,
|
|
2114
2547
|
writeAllTiers
|