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/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.counter, 0);
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.counter - a.counter);
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.counter, 0);
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.counter, 0);
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.counter - a.counter);
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.counter, 0);
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
- console.log(` \u251C ${n.path} (${n.counter})`);
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.counter - a.counter).slice(0, n);
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 existsSync11, mkdirSync as mkdirSync6 } from "fs";
926
- import { join as join12 } from "path";
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 readdirSync7, readFileSync as readFileSync3, writeFileSync as writeFileSync8, mkdirSync as mkdirSync5, existsSync as existsSync10 } from "fs";
930
- import { join as join11 } from "path";
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 = join11(brainRoot, SESSION_LOG_DIR);
935
- if (!existsSync10(logDir)) {
936
- mkdirSync5(logDir, { recursive: true });
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
- join11(logDir, `memory${nextSlot}.neuron`),
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 = join11(brainRoot, SESSION_LOG_DIR);
953
- if (!existsSync10(logDir)) return [];
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 = readdirSync7(logDir);
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(join11(logDir, entry), "utf8");
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 readdirSync7(logDir)) {
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 = join12(brainRoot, INBOX_DIR, CORRECTIONS_FILE);
995
- if (!existsSync11(inboxPath)) {
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 = join12(brainRoot, neuronPath);
1193
+ const fullPath = join13(brainRoot, neuronPath);
1051
1194
  const counterAdd = Math.max(1, correction.counter_add || 1);
1052
- if (existsSync11(fullPath)) {
1195
+ if (existsSync12(fullPath)) {
1053
1196
  for (let i = 0; i < counterAdd; i++) {
1054
1197
  fireNeuron(brainRoot, neuronPath);
1055
1198
  }
1056
1199
  } else {
1057
- growNeuron(brainRoot, neuronPath);
1058
- for (let i = 1; i < counterAdd; i++) {
1059
- fireNeuron(brainRoot, neuronPath);
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 = join12(brainRoot, INBOX_DIR);
1079
- if (!existsSync11(inboxDir)) {
1080
- mkdirSync6(inboxDir, { recursive: true });
1223
+ const inboxDir = join13(brainRoot, INBOX_DIR);
1224
+ if (!existsSync12(inboxDir)) {
1225
+ mkdirSync7(inboxDir, { recursive: true });
1081
1226
  }
1082
- const filePath = join12(inboxDir, CORRECTIONS_FILE);
1083
- if (!existsSync11(filePath)) {
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
- req.on("data", (chunk) => chunks.push(chunk));
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 existsSync12, mkdirSync as mkdirSync7, readdirSync as readdirSync8 } from "fs";
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 join13, resolve as resolve2 } from "path";
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 (!existsSync12(resolvedBrain) || !hasBrainRegions(resolvedBrain)) {
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 = join13(root, SETTINGS_DIR);
1363
- const settingsPath = join13(settingsDir, SETTINGS_FILE);
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 (existsSync12(settingsPath)) {
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: 10,
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 (!existsSync12(settingsDir)) {
1422
- mkdirSync7(settingsDir, { recursive: true });
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 settingsPath = join13(root, SETTINGS_DIR, SETTINGS_FILE);
1432
- if (!existsSync12(settingsPath)) {
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 settingsPath = join13(root, SETTINGS_DIR, SETTINGS_FILE);
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 (!existsSync12(settingsPath)) {
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 (!existsSync12(dir)) return false;
1676
+ if (!existsSync13(dir)) return false;
1510
1677
  try {
1511
- const entries = readdirSync8(dir);
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 existsSync13, mkdirSync as mkdirSync8 } from "fs";
1520
- import { join as join14, basename } from "path";
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 (!existsSync13(transcriptPath)) {
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 = join14(brainRoot, DIGEST_LOG_DIR);
1576
- const logPath = join14(logDir, `${resolvedSessionId}.jsonl`);
1577
- if (existsSync13(logPath)) {
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
- growNeuron(brainRoot, correction.path);
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 = join14(brainRoot, DIGEST_LOG_DIR);
1810
- if (!existsSync13(logDir)) {
1811
- mkdirSync8(logDir, { recursive: true });
1978
+ const logDir = join15(brainRoot, DIGEST_LOG_DIR);
1979
+ if (!existsSync14(logDir)) {
1980
+ mkdirSync9(logDir, { recursive: true });
1812
1981
  }
1813
- const logPath = join14(logDir, `${sessionId}.jsonl`);
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 prompt = buildPrompt(summary, episodes);
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 buildPrompt(summary, episodes) {
1891
- const episodeLines = episodes.length > 0 ? episodes.map((e) => `- [${e.ts}] ${e.type}: ${e.path} \u2014 ${e.detail}`).join("\n") : "(no recent episodes)";
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?key=${apiKey}`;
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
- growNeuron(brainRoot, action.path);
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