hebbian 0.3.4 → 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 +707 -79
- 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 +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/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 +459 -69
- 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(),
|
|
@@ -1824,6 +1978,226 @@ function writeAuditLog(brainRoot, sessionId, entries) {
|
|
|
1824
1978
|
writeFileSync11(logPath, lines.join("\n") + (lines.length > 0 ? "\n" : ""), "utf8");
|
|
1825
1979
|
}
|
|
1826
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
|
+
|
|
1827
2201
|
// src/evolve.ts
|
|
1828
2202
|
var MAX_ACTIONS = 10;
|
|
1829
2203
|
var PROTECTED_REGIONS = ["brainstem", "limbic", "sensors"];
|
|
@@ -1839,7 +2213,8 @@ async function runEvolve(brainRoot, dryRun) {
|
|
|
1839
2213
|
const episodes = readEpisodes(brainRoot);
|
|
1840
2214
|
const brain = scanBrain(brainRoot);
|
|
1841
2215
|
const summary = buildBrainSummary(brain);
|
|
1842
|
-
const
|
|
2216
|
+
const outcomeSummary = buildOutcomeSummary(brainRoot);
|
|
2217
|
+
const prompt = buildPrompt(summary, episodes, outcomeSummary);
|
|
1843
2218
|
let rawActions;
|
|
1844
2219
|
try {
|
|
1845
2220
|
rawActions = await callGemini(prompt, apiKey);
|
|
@@ -1887,8 +2262,9 @@ function buildBrainSummary(brain) {
|
|
|
1887
2262
|
}
|
|
1888
2263
|
return lines.join("\n");
|
|
1889
2264
|
}
|
|
1890
|
-
function buildPrompt(summary, episodes) {
|
|
2265
|
+
function buildPrompt(summary, episodes, outcomeSummary) {
|
|
1891
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 || "";
|
|
1892
2268
|
return `You are the evolve engine for a hebbian brain \u2014 a filesystem-based memory system for AI agents.
|
|
1893
2269
|
|
|
1894
2270
|
## Axioms
|
|
@@ -1900,6 +2276,7 @@ function buildPrompt(summary, episodes) {
|
|
|
1900
2276
|
## Current Brain
|
|
1901
2277
|
${summary}
|
|
1902
2278
|
|
|
2279
|
+
${outcomeSection}
|
|
1903
2280
|
## Recent Episodes (last ${episodes.length})
|
|
1904
2281
|
${episodeLines}
|
|
1905
2282
|
|
|
@@ -2020,7 +2397,7 @@ function executeActions(brainRoot, actions) {
|
|
|
2020
2397
|
fireNeuron(brainRoot, action.path);
|
|
2021
2398
|
break;
|
|
2022
2399
|
case "grow":
|
|
2023
|
-
|
|
2400
|
+
growCandidate(brainRoot, action.path);
|
|
2024
2401
|
break;
|
|
2025
2402
|
case "signal":
|
|
2026
2403
|
signalNeuron(brainRoot, action.path, action.signal || "dopamine");
|
|
@@ -2068,15 +2445,23 @@ export {
|
|
|
2068
2445
|
MAX_CORRECTIONS_PER_SESSION,
|
|
2069
2446
|
MAX_DEPTH,
|
|
2070
2447
|
MIN_CORRECTION_LENGTH,
|
|
2448
|
+
OUTCOME_TYPES,
|
|
2449
|
+
PROTECTED_REGIONS_CONTRA,
|
|
2071
2450
|
REGIONS,
|
|
2072
2451
|
REGION_ICONS,
|
|
2073
2452
|
REGION_KO,
|
|
2074
2453
|
REGION_PRIORITY,
|
|
2454
|
+
SESSION_STATE_DIR,
|
|
2075
2455
|
SIGNAL_TYPES,
|
|
2076
2456
|
SPOTLIGHT_DAYS,
|
|
2077
2457
|
appendCorrection,
|
|
2458
|
+
buildOutcomeSummary,
|
|
2459
|
+
captureSessionStart,
|
|
2078
2460
|
checkHooks,
|
|
2461
|
+
classifyOutcome,
|
|
2079
2462
|
clearReports,
|
|
2463
|
+
contraNeuron,
|
|
2464
|
+
detectOutcome,
|
|
2080
2465
|
digestTranscript,
|
|
2081
2466
|
emitBootstrap,
|
|
2082
2467
|
emitIndex,
|
|
@@ -2085,17 +2470,21 @@ export {
|
|
|
2085
2470
|
ensureInbox,
|
|
2086
2471
|
extractCorrections,
|
|
2087
2472
|
fireNeuron,
|
|
2473
|
+
fromCandidatePath,
|
|
2088
2474
|
getCurrentCounter,
|
|
2089
2475
|
getLastActivity,
|
|
2090
2476
|
getPendingReports,
|
|
2091
2477
|
gitSnapshot,
|
|
2478
|
+
growCandidate,
|
|
2092
2479
|
growNeuron,
|
|
2093
2480
|
initBrain,
|
|
2094
2481
|
installHooks,
|
|
2095
2482
|
jaccardSimilarity,
|
|
2483
|
+
listCandidates,
|
|
2096
2484
|
logEpisode,
|
|
2097
2485
|
printDiag,
|
|
2098
2486
|
processInbox,
|
|
2487
|
+
promoteCandidates,
|
|
2099
2488
|
readEpisodes,
|
|
2100
2489
|
readHookInput,
|
|
2101
2490
|
resolveBrainRoot,
|
|
@@ -2109,6 +2498,7 @@ export {
|
|
|
2109
2498
|
startAPI,
|
|
2110
2499
|
startWatch,
|
|
2111
2500
|
stem,
|
|
2501
|
+
toCandidatePath,
|
|
2112
2502
|
tokenize,
|
|
2113
2503
|
uninstallHooks,
|
|
2114
2504
|
writeAllTiers
|