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.
@@ -18,7 +18,7 @@ function resolveBrainRoot(brainFlag) {
18
18
  if (existsSync(resolve("./brain"))) return resolve("./brain");
19
19
  return resolve(process.env.HOME || "~", "hebbian", "brain");
20
20
  }
21
- var REGIONS, REGION_PRIORITY, REGION_ICONS, REGION_KO, EMIT_THRESHOLD, SPOTLIGHT_DAYS, JACCARD_THRESHOLD, MAX_DEPTH, EMIT_TARGETS, SIGNAL_TYPES, MARKER_START, MARKER_END, HOOK_MARKER, MAX_CORRECTIONS_PER_SESSION, MIN_CORRECTION_LENGTH, DIGEST_LOG_DIR;
21
+ var REGIONS, REGION_PRIORITY, REGION_ICONS, REGION_KO, EMIT_THRESHOLD, SPOTLIGHT_DAYS, JACCARD_THRESHOLD, MAX_DEPTH, EMIT_TARGETS, SIGNAL_TYPES, MARKER_START, MARKER_END, HOOK_MARKER, MAX_CORRECTIONS_PER_SESSION, MIN_CORRECTION_LENGTH, DIGEST_LOG_DIR, SESSION_STATE_DIR, PROTECTED_REGIONS_CONTRA;
22
22
  var init_constants = __esm({
23
23
  "src/constants.ts"() {
24
24
  "use strict";
@@ -76,6 +76,8 @@ var init_constants = __esm({
76
76
  MAX_CORRECTIONS_PER_SESSION = 10;
77
77
  MIN_CORRECTION_LENGTH = 15;
78
78
  DIGEST_LOG_DIR = "hippocampus/digest_log";
79
+ SESSION_STATE_DIR = "hippocampus/session_state";
80
+ PROTECTED_REGIONS_CONTRA = ["brainstem", "limbic", "sensors"];
79
81
  }
80
82
  });
81
83
 
@@ -417,7 +419,7 @@ function emitBootstrap(result, brain) {
417
419
  lines.push("|--------|---------|------------|");
418
420
  for (const region of result.activeRegions) {
419
421
  const active = region.neurons.filter((n) => !n.isDormant);
420
- const activation = active.reduce((sum, n) => sum + n.counter, 0);
422
+ const activation = active.reduce((sum, n) => sum + n.intensity, 0);
421
423
  const icon = REGION_ICONS[region.name] || "";
422
424
  lines.push(`| ${icon} ${region.name} | ${active.length} | ${activation} |`);
423
425
  }
@@ -439,7 +441,7 @@ function emitIndex(result, brain) {
439
441
  const allNeurons = result.activeRegions.flatMap(
440
442
  (r) => r.neurons.filter((n) => !n.isDormant && n.counter >= EMIT_THRESHOLD)
441
443
  );
442
- allNeurons.sort((a, b) => b.counter - a.counter);
444
+ allNeurons.sort((a, b) => b.intensity - a.intensity);
443
445
  lines.push("## Top 10 Active Neurons");
444
446
  lines.push("| # | Path | Counter | Strength |");
445
447
  lines.push("|---|------|---------|----------|");
@@ -465,7 +467,7 @@ function emitIndex(result, brain) {
465
467
  for (const region of result.activeRegions) {
466
468
  const active = region.neurons.filter((n) => !n.isDormant);
467
469
  const dormant = region.neurons.filter((n) => n.isDormant);
468
- const activation = active.reduce((sum, n) => sum + n.counter, 0);
470
+ const activation = active.reduce((sum, n) => sum + n.intensity, 0);
469
471
  const icon = REGION_ICONS[region.name] || "";
470
472
  lines.push(`| ${icon} ${region.name} | ${active.length} | ${dormant.length} | ${activation} | [_rules.md](${region.name}/_rules.md) |`);
471
473
  }
@@ -477,7 +479,7 @@ function emitRegionRules(region) {
477
479
  const ko = REGION_KO[region.name] || "";
478
480
  const active = region.neurons.filter((n) => !n.isDormant);
479
481
  const dormant = region.neurons.filter((n) => n.isDormant);
480
- const activation = active.reduce((sum, n) => sum + n.counter, 0);
482
+ const activation = active.reduce((sum, n) => sum + n.intensity, 0);
481
483
  const lines = [];
482
484
  lines.push(`# ${icon} ${region.name} (${ko})`);
483
485
  lines.push(`> Active: ${active.length} | Dormant: ${dormant.length} | Activation: ${activation}`);
@@ -491,7 +493,7 @@ function emitRegionRules(region) {
491
493
  }
492
494
  if (active.length > 0) {
493
495
  lines.push("## Rules");
494
- const sorted = [...active].sort((a, b) => b.counter - a.counter);
496
+ const sorted = [...active].sort((a, b) => b.intensity - a.intensity);
495
497
  for (const n of sorted) {
496
498
  const indent = " ".repeat(Math.min(n.depth, 4));
497
499
  const prefix = strengthPrefix(n.counter);
@@ -574,7 +576,7 @@ function printDiag(brain, result) {
574
576
  const icon = REGION_ICONS[region.name] || "";
575
577
  const active = region.neurons.filter((n) => !n.isDormant);
576
578
  const dormant = region.neurons.filter((n) => n.isDormant);
577
- const activation = active.reduce((sum, n) => sum + n.counter, 0);
579
+ const activation = active.reduce((sum, n) => sum + n.intensity, 0);
578
580
  const isBlocked = result.blockedRegions.some((r) => r.name === region.name);
579
581
  const status = region.hasBomb ? "\u{1F4A3} BOMB" : isBlocked ? "\u{1F6AB} BLOCKED" : "\u2705 ACTIVE";
580
582
  console.log(` ${icon} ${region.name} [${status}]`);
@@ -584,7 +586,8 @@ function printDiag(brain, result) {
584
586
  }
585
587
  const top3 = sortedActive(region.neurons, 3);
586
588
  for (const n of top3) {
587
- console.log(` \u251C ${n.path} (${n.counter})`);
589
+ const contraStr = n.contra > 0 ? ` contra:${n.contra}` : "";
590
+ console.log(` \u251C ${n.path} (counter:${n.counter}${contraStr} intensity:${n.intensity})`);
588
591
  }
589
592
  }
590
593
  console.log("");
@@ -593,7 +596,7 @@ function pathToSentence(path) {
593
596
  return path.replace(/\//g, " > ").replace(/_/g, " ");
594
597
  }
595
598
  function sortedActive(neurons, n) {
596
- return [...neurons].filter((neuron) => !neuron.isDormant).sort((a, b) => b.counter - a.counter).slice(0, n);
599
+ return [...neurons].filter((neuron) => !neuron.isDormant).sort((a, b) => b.intensity - a.intensity).slice(0, n);
597
600
  }
598
601
  function strengthPrefix(counter) {
599
602
  if (counter >= 10) return "**[ABSOLUTE]** ";
@@ -758,7 +761,9 @@ var init_update_check = __esm({
758
761
  // src/fire.ts
759
762
  var fire_exports = {};
760
763
  __export(fire_exports, {
764
+ contraNeuron: () => contraNeuron,
761
765
  fireNeuron: () => fireNeuron,
766
+ getCurrentContra: () => getCurrentContra,
762
767
  getCurrentCounter: () => getCurrentCounter
763
768
  });
764
769
  import { readdirSync as readdirSync3, renameSync, writeFileSync as writeFileSync4, existsSync as existsSync6, mkdirSync as mkdirSync4 } from "fs";
@@ -781,6 +786,33 @@ function fireNeuron(brainRoot, neuronPath) {
781
786
  console.log(`\u{1F525} fired: ${neuronPath} (${current} \u2192 ${newCounter})`);
782
787
  return newCounter;
783
788
  }
789
+ function contraNeuron(brainRoot, neuronPath) {
790
+ const fullPath = join5(brainRoot, neuronPath);
791
+ if (!existsSync6(fullPath)) {
792
+ return 0;
793
+ }
794
+ const current = getCurrentContra(fullPath);
795
+ const newContra = current + 1;
796
+ if (current > 0) {
797
+ renameSync(join5(fullPath, `${current}.contra`), join5(fullPath, `${newContra}.contra`));
798
+ } else {
799
+ writeFileSync4(join5(fullPath, `${newContra}.contra`), "", "utf8");
800
+ }
801
+ return newContra;
802
+ }
803
+ function getCurrentContra(dir) {
804
+ let max = 0;
805
+ try {
806
+ for (const entry of readdirSync3(dir)) {
807
+ if (entry.endsWith(".contra")) {
808
+ const n = parseInt(entry, 10);
809
+ if (!isNaN(n) && n > max) max = n;
810
+ }
811
+ }
812
+ } catch {
813
+ }
814
+ return max;
815
+ }
784
816
  function getCurrentCounter(dir) {
785
817
  let max = 0;
786
818
  try {
@@ -844,6 +876,9 @@ function growNeuron(brainRoot, neuronPath) {
844
876
  const counter = fireNeuron(brainRoot, neuronPath);
845
877
  return { action: "fired", path: neuronPath, counter };
846
878
  }
879
+ if (neuronPath.includes("..") || neuronPath.startsWith("/")) {
880
+ throw new Error(`Invalid neuron path: "${neuronPath}" (path traversal not allowed)`);
881
+ }
847
882
  const parts = neuronPath.split("/");
848
883
  const regionName = parts[0];
849
884
  if (!REGIONS.includes(regionName)) {
@@ -1216,41 +1251,174 @@ var init_watch = __esm({
1216
1251
  }
1217
1252
  });
1218
1253
 
1254
+ // src/candidates.ts
1255
+ var candidates_exports = {};
1256
+ __export(candidates_exports, {
1257
+ CANDIDATE_DECAY_DAYS: () => CANDIDATE_DECAY_DAYS,
1258
+ CANDIDATE_THRESHOLD: () => CANDIDATE_THRESHOLD,
1259
+ fromCandidatePath: () => fromCandidatePath,
1260
+ growCandidate: () => growCandidate,
1261
+ listCandidates: () => listCandidates,
1262
+ promoteCandidates: () => promoteCandidates,
1263
+ toCandidatePath: () => toCandidatePath
1264
+ });
1265
+ import { existsSync as existsSync11, mkdirSync as mkdirSync6, readdirSync as readdirSync7, renameSync as renameSync3, rmSync, statSync as statSync4 } from "fs";
1266
+ import { join as join12, dirname as dirname2, relative as relative3 } from "path";
1267
+ function toCandidatePath(neuronPath) {
1268
+ const slash = neuronPath.indexOf("/");
1269
+ if (slash === -1) throw new Error(`Invalid neuron path (missing region): ${neuronPath}`);
1270
+ return `${neuronPath.slice(0, slash)}/${CANDIDATE_SEGMENT}/${neuronPath.slice(slash + 1)}`;
1271
+ }
1272
+ function fromCandidatePath(candidatePath) {
1273
+ return candidatePath.replace(`/${CANDIDATE_SEGMENT}/`, "/");
1274
+ }
1275
+ function growCandidate(brainRoot, neuronPath) {
1276
+ const candidatePath = toCandidatePath(neuronPath);
1277
+ const result = growNeuron(brainRoot, candidatePath);
1278
+ if (result.counter >= CANDIDATE_THRESHOLD) {
1279
+ const ok = moveCandidate(brainRoot, candidatePath, neuronPath);
1280
+ return { ...result, path: ok ? neuronPath : result.path, promoted: ok };
1281
+ }
1282
+ console.log(` \u{1F331} candidate (${result.counter}/${CANDIDATE_THRESHOLD}): ${candidatePath}`);
1283
+ return { ...result, promoted: false };
1284
+ }
1285
+ function moveCandidate(brainRoot, candidatePath, targetPath) {
1286
+ const src = join12(brainRoot, candidatePath);
1287
+ if (!existsSync11(src)) return false;
1288
+ const dst = join12(brainRoot, targetPath);
1289
+ if (existsSync11(dst)) {
1290
+ fireNeuron(brainRoot, targetPath);
1291
+ rmSync(src, { recursive: true, force: true });
1292
+ } else {
1293
+ mkdirSync6(dirname2(dst), { recursive: true });
1294
+ renameSync3(src, dst);
1295
+ }
1296
+ console.log(`\u{1F393} promoted: ${candidatePath} \u2192 ${targetPath}`);
1297
+ return true;
1298
+ }
1299
+ function promoteCandidates(brainRoot) {
1300
+ const promoted = [];
1301
+ const decayed = [];
1302
+ const decayMs = CANDIDATE_DECAY_DAYS * 24 * 60 * 60 * 1e3;
1303
+ const now = Date.now();
1304
+ for (const region of REGIONS) {
1305
+ const candidateRoot = join12(brainRoot, region, CANDIDATE_SEGMENT);
1306
+ walkNeuronDirs(candidateRoot, (neuronDir) => {
1307
+ const rel = relative3(join12(brainRoot, region), neuronDir);
1308
+ const candidatePath = `${region}/${rel}`;
1309
+ const targetPath = fromCandidatePath(candidatePath);
1310
+ const counter = readCounter(neuronDir);
1311
+ const mtime = statSync4(neuronDir).mtimeMs;
1312
+ if (counter >= CANDIDATE_THRESHOLD) {
1313
+ moveCandidate(brainRoot, candidatePath, targetPath);
1314
+ promoted.push(targetPath);
1315
+ } else if (now - mtime > decayMs) {
1316
+ rmSync(neuronDir, { recursive: true, force: true });
1317
+ decayed.push(candidatePath);
1318
+ console.log(`\u{1F480} candidate decayed: ${candidatePath}`);
1319
+ }
1320
+ });
1321
+ }
1322
+ return { promoted, decayed };
1323
+ }
1324
+ function listCandidates(brainRoot) {
1325
+ const results = [];
1326
+ const now = Date.now();
1327
+ for (const region of REGIONS) {
1328
+ const candidateRoot = join12(brainRoot, region, CANDIDATE_SEGMENT);
1329
+ walkNeuronDirs(candidateRoot, (neuronDir) => {
1330
+ const rel = relative3(join12(brainRoot, region), neuronDir);
1331
+ const candidatePath = `${region}/${rel}`;
1332
+ const targetPath = fromCandidatePath(candidatePath);
1333
+ const counter = readCounter(neuronDir);
1334
+ const mtime = statSync4(neuronDir).mtimeMs;
1335
+ const daysInactive = Math.floor((now - mtime) / (24 * 60 * 60 * 1e3));
1336
+ results.push({ candidatePath, targetPath, counter, daysInactive });
1337
+ });
1338
+ }
1339
+ return results;
1340
+ }
1341
+ function walkNeuronDirs(dir, cb) {
1342
+ if (!existsSync11(dir)) return;
1343
+ try {
1344
+ const entries = readdirSync7(dir, { withFileTypes: true });
1345
+ const hasNeuron = entries.some((e) => e.isFile() && e.name.endsWith(".neuron"));
1346
+ if (hasNeuron) {
1347
+ cb(dir);
1348
+ return;
1349
+ }
1350
+ for (const entry of entries) {
1351
+ if (entry.isDirectory() && !entry.name.startsWith(".")) {
1352
+ walkNeuronDirs(join12(dir, entry.name), cb);
1353
+ }
1354
+ }
1355
+ } catch {
1356
+ }
1357
+ }
1358
+ function readCounter(dir) {
1359
+ try {
1360
+ const files = readdirSync7(dir).filter((f) => /^\d+\.neuron$/.test(f));
1361
+ if (files.length === 0) return 0;
1362
+ return Math.max(...files.map((f) => parseInt(f, 10)));
1363
+ } catch {
1364
+ return 0;
1365
+ }
1366
+ }
1367
+ var CANDIDATE_THRESHOLD, CANDIDATE_DECAY_DAYS, CANDIDATE_SEGMENT;
1368
+ var init_candidates = __esm({
1369
+ "src/candidates.ts"() {
1370
+ "use strict";
1371
+ init_constants();
1372
+ init_grow();
1373
+ init_fire();
1374
+ CANDIDATE_THRESHOLD = 3;
1375
+ CANDIDATE_DECAY_DAYS = 14;
1376
+ CANDIDATE_SEGMENT = "_candidates";
1377
+ }
1378
+ });
1379
+
1219
1380
  // src/episode.ts
1220
- import { readdirSync as readdirSync7, readFileSync as readFileSync4, writeFileSync as writeFileSync9, mkdirSync as mkdirSync6, existsSync as existsSync11 } from "fs";
1221
- import { join as join12 } from "path";
1222
- function logEpisode(brainRoot, type, path, detail) {
1223
- const logDir = join12(brainRoot, SESSION_LOG_DIR);
1224
- if (!existsSync11(logDir)) {
1225
- mkdirSync6(logDir, { recursive: true });
1381
+ var episode_exports = {};
1382
+ __export(episode_exports, {
1383
+ logEpisode: () => logEpisode,
1384
+ readEpisodes: () => readEpisodes
1385
+ });
1386
+ import { readdirSync as readdirSync8, readFileSync as readFileSync4, writeFileSync as writeFileSync9, mkdirSync as mkdirSync7, existsSync as existsSync12 } from "fs";
1387
+ import { join as join13 } from "path";
1388
+ function logEpisode(brainRoot, type, path, detail, extra) {
1389
+ const logDir = join13(brainRoot, SESSION_LOG_DIR);
1390
+ if (!existsSync12(logDir)) {
1391
+ mkdirSync7(logDir, { recursive: true });
1226
1392
  }
1227
1393
  const nextSlot = getNextSlot(logDir);
1228
1394
  const episode = {
1229
1395
  ts: (/* @__PURE__ */ new Date()).toISOString(),
1230
1396
  type,
1231
1397
  path,
1232
- detail
1398
+ detail,
1399
+ ...extra?.outcome ? { outcome: extra.outcome } : {},
1400
+ ...extra?.neurons ? { neurons: extra.neurons } : {}
1233
1401
  };
1234
1402
  writeFileSync9(
1235
- join12(logDir, `memory${nextSlot}.neuron`),
1403
+ join13(logDir, `memory${nextSlot}.neuron`),
1236
1404
  JSON.stringify(episode),
1237
1405
  "utf8"
1238
1406
  );
1239
1407
  }
1240
1408
  function readEpisodes(brainRoot) {
1241
- const logDir = join12(brainRoot, SESSION_LOG_DIR);
1242
- if (!existsSync11(logDir)) return [];
1409
+ const logDir = join13(brainRoot, SESSION_LOG_DIR);
1410
+ if (!existsSync12(logDir)) return [];
1243
1411
  const episodes = [];
1244
1412
  let entries;
1245
1413
  try {
1246
- entries = readdirSync7(logDir);
1414
+ entries = readdirSync8(logDir);
1247
1415
  } catch {
1248
1416
  return [];
1249
1417
  }
1250
1418
  for (const entry of entries) {
1251
1419
  if (!entry.startsWith("memory") || !entry.endsWith(".neuron")) continue;
1252
1420
  try {
1253
- const content = readFileSync4(join12(logDir, entry), "utf8");
1421
+ const content = readFileSync4(join13(logDir, entry), "utf8");
1254
1422
  if (content.trim()) {
1255
1423
  episodes.push(JSON.parse(content));
1256
1424
  }
@@ -1263,7 +1431,7 @@ function readEpisodes(brainRoot) {
1263
1431
  function getNextSlot(logDir) {
1264
1432
  let maxSlot = 0;
1265
1433
  try {
1266
- for (const entry of readdirSync7(logDir)) {
1434
+ for (const entry of readdirSync8(logDir)) {
1267
1435
  if (entry.startsWith("memory") && entry.endsWith(".neuron")) {
1268
1436
  const n = parseInt(entry.replace("memory", "").replace(".neuron", ""), 10);
1269
1437
  if (!isNaN(n) && n > maxSlot) maxSlot = n;
@@ -1290,11 +1458,11 @@ __export(inbox_exports, {
1290
1458
  ensureInbox: () => ensureInbox,
1291
1459
  processInbox: () => processInbox
1292
1460
  });
1293
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync10, existsSync as existsSync12, mkdirSync as mkdirSync7 } from "fs";
1294
- import { join as join13 } from "path";
1461
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync10, existsSync as existsSync13, mkdirSync as mkdirSync8 } from "fs";
1462
+ import { join as join14 } from "path";
1295
1463
  function processInbox(brainRoot) {
1296
- const inboxPath = join13(brainRoot, INBOX_DIR, CORRECTIONS_FILE);
1297
- if (!existsSync12(inboxPath)) {
1464
+ const inboxPath = join14(brainRoot, INBOX_DIR, CORRECTIONS_FILE);
1465
+ if (!existsSync13(inboxPath)) {
1298
1466
  return { processed: 0, skipped: 0, errors: [] };
1299
1467
  }
1300
1468
  const content = readFileSync5(inboxPath, "utf8").trim();
@@ -1349,16 +1517,18 @@ function processInbox(brainRoot) {
1349
1517
  }
1350
1518
  function applyCorrection(brainRoot, correction) {
1351
1519
  const neuronPath = correction.path;
1352
- const fullPath = join13(brainRoot, neuronPath);
1520
+ const fullPath = join14(brainRoot, neuronPath);
1353
1521
  const counterAdd = Math.max(1, correction.counter_add || 1);
1354
- if (existsSync12(fullPath)) {
1522
+ if (existsSync13(fullPath)) {
1355
1523
  for (let i = 0; i < counterAdd; i++) {
1356
1524
  fireNeuron(brainRoot, neuronPath);
1357
1525
  }
1358
1526
  } else {
1359
- growNeuron(brainRoot, neuronPath);
1360
- for (let i = 1; i < counterAdd; i++) {
1361
- fireNeuron(brainRoot, neuronPath);
1527
+ const candResult = growCandidate(brainRoot, neuronPath);
1528
+ if (candResult.promoted) {
1529
+ for (let i = 1; i < counterAdd; i++) {
1530
+ fireNeuron(brainRoot, neuronPath);
1531
+ }
1362
1532
  }
1363
1533
  }
1364
1534
  if (correction.dopamine && correction.dopamine > 0) {
@@ -1377,12 +1547,12 @@ function isPathSafe(path) {
1377
1547
  return true;
1378
1548
  }
1379
1549
  function ensureInbox(brainRoot) {
1380
- const inboxDir = join13(brainRoot, INBOX_DIR);
1381
- if (!existsSync12(inboxDir)) {
1382
- mkdirSync7(inboxDir, { recursive: true });
1550
+ const inboxDir = join14(brainRoot, INBOX_DIR);
1551
+ if (!existsSync13(inboxDir)) {
1552
+ mkdirSync8(inboxDir, { recursive: true });
1383
1553
  }
1384
- const filePath = join13(inboxDir, CORRECTIONS_FILE);
1385
- if (!existsSync12(filePath)) {
1554
+ const filePath = join14(inboxDir, CORRECTIONS_FILE);
1555
+ if (!existsSync13(filePath)) {
1386
1556
  writeFileSync10(filePath, "", "utf8");
1387
1557
  }
1388
1558
  return filePath;
@@ -1398,7 +1568,7 @@ var init_inbox = __esm({
1398
1568
  "src/inbox.ts"() {
1399
1569
  "use strict";
1400
1570
  init_constants();
1401
- init_grow();
1571
+ init_candidates();
1402
1572
  init_fire();
1403
1573
  init_signal();
1404
1574
  init_episode();
@@ -1478,7 +1648,16 @@ function error(res, message, status = 400) {
1478
1648
  async function readBody(req) {
1479
1649
  return new Promise((resolve4, reject) => {
1480
1650
  const chunks = [];
1481
- req.on("data", (chunk) => chunks.push(chunk));
1651
+ let total = 0;
1652
+ req.on("data", (chunk) => {
1653
+ total += chunk.length;
1654
+ if (total > MAX_BODY_BYTES) {
1655
+ reject(new Error("Request body too large"));
1656
+ req.destroy();
1657
+ return;
1658
+ }
1659
+ chunks.push(chunk);
1660
+ });
1482
1661
  req.on("end", () => resolve4(Buffer.concat(chunks).toString("utf8")));
1483
1662
  req.on("error", reject);
1484
1663
  });
@@ -1668,7 +1847,7 @@ function getPendingReports() {
1668
1847
  function clearReports() {
1669
1848
  pendingReports.length = 0;
1670
1849
  }
1671
- var lastAPIActivity, pendingReports;
1850
+ var lastAPIActivity, pendingReports, MAX_BODY_BYTES;
1672
1851
  var init_api = __esm({
1673
1852
  "src/api.ts"() {
1674
1853
  "use strict";
@@ -1685,6 +1864,7 @@ var init_api = __esm({
1685
1864
  init_constants();
1686
1865
  lastAPIActivity = Date.now();
1687
1866
  pendingReports = [];
1867
+ MAX_BODY_BYTES = 1048576;
1688
1868
  }
1689
1869
  });
1690
1870
 
@@ -1695,17 +1875,25 @@ __export(hooks_exports, {
1695
1875
  installHooks: () => installHooks,
1696
1876
  uninstallHooks: () => uninstallHooks
1697
1877
  });
1698
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync11, existsSync as existsSync13, mkdirSync as mkdirSync8, readdirSync as readdirSync8 } from "fs";
1878
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync11, existsSync as existsSync14, mkdirSync as mkdirSync9, readdirSync as readdirSync9 } from "fs";
1699
1879
  import { execSync as execSync2 } from "child_process";
1700
- import { join as join14, resolve as resolve2 } from "path";
1701
- function installHooks(brainRoot, projectRoot) {
1880
+ import { join as join15, resolve as resolve2 } from "path";
1881
+ function installHooks(brainRoot, projectRoot, global) {
1702
1882
  const root = projectRoot || process.cwd();
1703
1883
  const resolvedBrain = resolve2(brainRoot);
1704
- if (!existsSync13(resolvedBrain) || !hasBrainRegions(resolvedBrain)) {
1884
+ if (global) {
1885
+ const home = process.env.HOME || "~";
1886
+ if (!brainRoot.startsWith("/") && !brainRoot.startsWith(home)) {
1887
+ console.error("\u274C --global requires an absolute --brain path (e.g. --brain ~/brain)");
1888
+ process.exit(1);
1889
+ }
1890
+ }
1891
+ if (!existsSync14(resolvedBrain) || !hasBrainRegions(resolvedBrain)) {
1705
1892
  initBrain(resolvedBrain);
1706
1893
  }
1707
- const settingsDir = join14(root, SETTINGS_DIR);
1708
- const settingsPath = join14(settingsDir, SETTINGS_FILE);
1894
+ const settingsDir = global ? resolve2(process.env.HOME || "~", SETTINGS_DIR) : join15(root, SETTINGS_DIR);
1895
+ const settingsFile = global ? "settings.json" : SETTINGS_FILE;
1896
+ const settingsPath = join15(settingsDir, settingsFile);
1709
1897
  const defaultBrain = resolve2(root, "brain");
1710
1898
  const brainFlag = resolvedBrain === defaultBrain ? "" : ` --brain ${resolvedBrain}`;
1711
1899
  let npxBin = "npx";
@@ -1714,7 +1902,7 @@ function installHooks(brainRoot, projectRoot) {
1714
1902
  } catch {
1715
1903
  }
1716
1904
  let settings = {};
1717
- if (existsSync13(settingsPath)) {
1905
+ if (existsSync14(settingsPath)) {
1718
1906
  try {
1719
1907
  settings = JSON.parse(readFileSync6(settingsPath, "utf8"));
1720
1908
  } catch {
@@ -1731,8 +1919,8 @@ function installHooks(brainRoot, projectRoot) {
1731
1919
  matcher: "startup|resume",
1732
1920
  entry: {
1733
1921
  type: "command",
1734
- command: `${npxBin} hebbian emit claude${brainFlag}`,
1735
- timeout: 10,
1922
+ command: `${npxBin} hebbian emit claude${brainFlag} && ${npxBin} hebbian session start${brainFlag}`,
1923
+ timeout: 15,
1736
1924
  statusMessage: `${HOOK_MARKER} refreshing brain`
1737
1925
  }
1738
1926
  },
@@ -1740,7 +1928,7 @@ function installHooks(brainRoot, projectRoot) {
1740
1928
  event: "Stop",
1741
1929
  entry: {
1742
1930
  type: "command",
1743
- command: `${npxBin} hebbian digest${brainFlag}`,
1931
+ command: `${npxBin} hebbian digest${brainFlag}; ${npxBin} hebbian session end${brainFlag}`,
1744
1932
  timeout: 30,
1745
1933
  statusMessage: `${HOOK_MARKER} digesting session`
1746
1934
  }
@@ -1763,18 +1951,20 @@ function installHooks(brainRoot, projectRoot) {
1763
1951
  hooks[event].push(group);
1764
1952
  }
1765
1953
  }
1766
- if (!existsSync13(settingsDir)) {
1767
- mkdirSync8(settingsDir, { recursive: true });
1954
+ if (!existsSync14(settingsDir)) {
1955
+ mkdirSync9(settingsDir, { recursive: true });
1768
1956
  }
1769
1957
  writeFileSync11(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
1770
1958
  console.log(`\u2705 hebbian hooks installed at ${settingsPath}`);
1771
1959
  console.log(` SessionStart \u2192 ${npxBin} hebbian emit claude${brainFlag}`);
1772
1960
  console.log(` Stop \u2192 ${npxBin} hebbian digest${brainFlag}`);
1773
1961
  }
1774
- function uninstallHooks(projectRoot) {
1962
+ function uninstallHooks(projectRoot, global) {
1775
1963
  const root = projectRoot || process.cwd();
1776
- const settingsPath = join14(root, SETTINGS_DIR, SETTINGS_FILE);
1777
- if (!existsSync13(settingsPath)) {
1964
+ const settingsDir = global ? resolve2(process.env.HOME || "~", SETTINGS_DIR) : join15(root, SETTINGS_DIR);
1965
+ const settingsFile = global ? "settings.json" : SETTINGS_FILE;
1966
+ const settingsPath = join15(settingsDir, settingsFile);
1967
+ if (!existsSync14(settingsPath)) {
1778
1968
  console.log("No hooks installed (settings.local.json not found)");
1779
1969
  return;
1780
1970
  }
@@ -1807,15 +1997,17 @@ function uninstallHooks(projectRoot) {
1807
1997
  writeFileSync11(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
1808
1998
  console.log(`\u2705 removed ${removed} hebbian hook(s) from ${settingsPath}`);
1809
1999
  }
1810
- function checkHooks(projectRoot) {
2000
+ function checkHooks(projectRoot, global) {
1811
2001
  const root = projectRoot || process.cwd();
1812
- const settingsPath = join14(root, SETTINGS_DIR, SETTINGS_FILE);
2002
+ const settingsDir = global ? resolve2(process.env.HOME || "~", SETTINGS_DIR) : join15(root, SETTINGS_DIR);
2003
+ const settingsFile = global ? "settings.json" : SETTINGS_FILE;
2004
+ const settingsPath = join15(settingsDir, settingsFile);
1813
2005
  const status = {
1814
2006
  installed: false,
1815
2007
  path: settingsPath,
1816
2008
  events: []
1817
2009
  };
1818
- if (!existsSync13(settingsPath)) {
2010
+ if (!existsSync14(settingsPath)) {
1819
2011
  console.log(`\u274C hebbian hooks not installed (${settingsPath} not found)`);
1820
2012
  return status;
1821
2013
  }
@@ -1851,9 +2043,9 @@ function checkHooks(projectRoot) {
1851
2043
  return status;
1852
2044
  }
1853
2045
  function hasBrainRegions(dir) {
1854
- if (!existsSync13(dir)) return false;
2046
+ if (!existsSync14(dir)) return false;
1855
2047
  try {
1856
- const entries = readdirSync8(dir);
2048
+ const entries = readdirSync9(dir);
1857
2049
  return REGIONS.some((r) => entries.includes(r));
1858
2050
  } catch {
1859
2051
  return false;
@@ -1877,8 +2069,8 @@ __export(digest_exports, {
1877
2069
  extractCorrections: () => extractCorrections,
1878
2070
  readHookInput: () => readHookInput
1879
2071
  });
1880
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync12, existsSync as existsSync14, mkdirSync as mkdirSync9 } from "fs";
1881
- import { join as join15, basename } from "path";
2072
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync12, existsSync as existsSync15, mkdirSync as mkdirSync10 } from "fs";
2073
+ import { join as join16, basename } from "path";
1882
2074
  function readHookInput(stdin) {
1883
2075
  if (!stdin.trim()) return null;
1884
2076
  try {
@@ -1893,13 +2085,13 @@ function readHookInput(stdin) {
1893
2085
  }
1894
2086
  }
1895
2087
  function digestTranscript(brainRoot, transcriptPath, sessionId) {
1896
- if (!existsSync14(transcriptPath)) {
2088
+ if (!existsSync15(transcriptPath)) {
1897
2089
  throw new Error(`Transcript not found: ${transcriptPath}`);
1898
2090
  }
1899
2091
  const resolvedSessionId = sessionId || basename(transcriptPath, ".jsonl");
1900
- const logDir = join15(brainRoot, DIGEST_LOG_DIR);
1901
- const logPath = join15(logDir, `${resolvedSessionId}.jsonl`);
1902
- if (existsSync14(logPath)) {
2092
+ const logDir = join16(brainRoot, DIGEST_LOG_DIR);
2093
+ const logPath = join16(logDir, `${resolvedSessionId}.jsonl`);
2094
+ if (existsSync15(logPath)) {
1903
2095
  console.log(`\u23ED already digested session ${resolvedSessionId}, skip`);
1904
2096
  return { corrections: 0, skipped: 0, transcriptPath, sessionId: resolvedSessionId };
1905
2097
  }
@@ -1914,7 +2106,7 @@ function digestTranscript(brainRoot, transcriptPath, sessionId) {
1914
2106
  const auditEntries = [];
1915
2107
  for (const correction of corrections) {
1916
2108
  try {
1917
- growNeuron(brainRoot, correction.path);
2109
+ growCandidate(brainRoot, correction.path);
1918
2110
  logEpisode(brainRoot, "digest", correction.path, correction.text);
1919
2111
  auditEntries.push({ correction, applied: true });
1920
2112
  applied++;
@@ -1966,6 +2158,8 @@ function extractCorrections(messages) {
1966
2158
  if (text.length < MIN_CORRECTION_LENGTH) continue;
1967
2159
  if (/^[\/!]/.test(text.trim())) continue;
1968
2160
  if (text.trim().endsWith("?")) continue;
2161
+ if (/^<[a-zA-Z]/.test(text.trim())) continue;
2162
+ if (/^Base directory for this skill:/i.test(text.trim())) continue;
1969
2163
  const correction = detectCorrection(text);
1970
2164
  if (correction) {
1971
2165
  corrections.push(correction);
@@ -2131,11 +2325,11 @@ function extractKeywords(text) {
2131
2325
  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));
2132
2326
  }
2133
2327
  function writeAuditLog(brainRoot, sessionId, entries) {
2134
- const logDir = join15(brainRoot, DIGEST_LOG_DIR);
2135
- if (!existsSync14(logDir)) {
2136
- mkdirSync9(logDir, { recursive: true });
2328
+ const logDir = join16(brainRoot, DIGEST_LOG_DIR);
2329
+ if (!existsSync15(logDir)) {
2330
+ mkdirSync10(logDir, { recursive: true });
2137
2331
  }
2138
- const logPath = join15(logDir, `${sessionId}.jsonl`);
2332
+ const logPath = join16(logDir, `${sessionId}.jsonl`);
2139
2333
  const lines = entries.map(
2140
2334
  (e) => JSON.stringify({
2141
2335
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -2153,7 +2347,7 @@ var init_digest = __esm({
2153
2347
  "src/digest.ts"() {
2154
2348
  "use strict";
2155
2349
  init_constants();
2156
- init_grow();
2350
+ init_candidates();
2157
2351
  init_episode();
2158
2352
  NEGATION_PATTERNS = [
2159
2353
  /\bdon[''\u2019]?t\b/i,
@@ -2194,6 +2388,244 @@ var init_digest = __esm({
2194
2388
  }
2195
2389
  });
2196
2390
 
2391
+ // src/outcome.ts
2392
+ var outcome_exports = {};
2393
+ __export(outcome_exports, {
2394
+ buildOutcomeSummary: () => buildOutcomeSummary,
2395
+ captureSessionStart: () => captureSessionStart,
2396
+ classifyOutcome: () => classifyOutcome,
2397
+ detectOutcome: () => detectOutcome
2398
+ });
2399
+ import { execSync as execSync3 } from "child_process";
2400
+ import { existsSync as existsSync16, mkdirSync as mkdirSync11, writeFileSync as writeFileSync13, readFileSync as readFileSync8, readdirSync as readdirSync10, rmSync as rmSync2, statSync as statSync5 } from "fs";
2401
+ import { join as join17 } from "path";
2402
+ import { randomUUID } from "crypto";
2403
+ function captureSessionStart(brainRoot) {
2404
+ let sha;
2405
+ try {
2406
+ sha = execSync3("git rev-parse HEAD", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
2407
+ } catch {
2408
+ console.log("\u23ED\uFE0F session start: not a git repo, skipping");
2409
+ return null;
2410
+ }
2411
+ let status;
2412
+ try {
2413
+ const raw = execSync3("git status --porcelain", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
2414
+ status = raw ? raw.split("\n") : [];
2415
+ } catch {
2416
+ status = [];
2417
+ }
2418
+ const brain = scanBrain(brainRoot);
2419
+ const result = runSubsumption(brain);
2420
+ const neurons = [];
2421
+ for (const region of result.activeRegions) {
2422
+ for (const neuron of region.neurons) {
2423
+ if (!neuron.isDormant && neuron.counter > 0) {
2424
+ neurons.push(`${region.name}/${neuron.path}`);
2425
+ }
2426
+ }
2427
+ }
2428
+ const uuid = randomUUID();
2429
+ const stateDir = join17(brainRoot, SESSION_STATE_DIR);
2430
+ if (!existsSync16(stateDir)) {
2431
+ mkdirSync11(stateDir, { recursive: true });
2432
+ }
2433
+ const state = { ts: (/* @__PURE__ */ new Date()).toISOString(), sha, status, neurons, uuid };
2434
+ writeFileSync13(join17(stateDir, `state_${uuid}.json`), JSON.stringify(state), "utf8");
2435
+ console.log(`\u{1F4F8} session start: SHA ${sha.slice(0, 7)}, ${neurons.length} active neurons`);
2436
+ return state;
2437
+ }
2438
+ function detectOutcome(brainRoot) {
2439
+ const state = readLatestSessionState(brainRoot);
2440
+ if (!state) {
2441
+ console.log("\u23ED\uFE0F session end: no session state found, skipping");
2442
+ return null;
2443
+ }
2444
+ let currentSha;
2445
+ try {
2446
+ currentSha = execSync3("git rev-parse HEAD", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
2447
+ } catch {
2448
+ console.log("\u23ED\uFE0F session end: not a git repo, skipping");
2449
+ cleanupSessionState(brainRoot, state.uuid);
2450
+ return null;
2451
+ }
2452
+ let currentStatus;
2453
+ try {
2454
+ const raw = execSync3("git status --porcelain", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
2455
+ currentStatus = raw ? filterHebbianPaths(raw.split("\n")) : [];
2456
+ } catch {
2457
+ currentStatus = [];
2458
+ }
2459
+ const filteredStartStatus = filterHebbianPaths(state.status);
2460
+ const outcome = classifyOutcome(
2461
+ { ...state, status: filteredStartStatus },
2462
+ currentSha,
2463
+ currentStatus
2464
+ );
2465
+ if (!outcome) {
2466
+ console.log("\u{1F4CA} session end: no changes detected (no-op)");
2467
+ cleanupSessionState(brainRoot, state.uuid);
2468
+ return null;
2469
+ }
2470
+ const neurons = state.neurons;
2471
+ logEpisode(brainRoot, "session-end", "", `outcome:${outcome}`, { outcome, neurons });
2472
+ let result;
2473
+ if (outcome === "revert") {
2474
+ const { affected, skipped } = applyContra(brainRoot, neurons);
2475
+ result = {
2476
+ outcome: "revert",
2477
+ neuronsAffected: affected,
2478
+ protectedSkipped: skipped,
2479
+ detail: `${affected} neurons contra'd (${skipped} protected skipped)`
2480
+ };
2481
+ console.log(`\u{1F4CA} session end: revert \u2014 ${result.detail}`);
2482
+ } else {
2483
+ result = {
2484
+ outcome: "acceptance",
2485
+ neuronsAffected: 0,
2486
+ protectedSkipped: 0,
2487
+ detail: "changes accepted"
2488
+ };
2489
+ console.log("\u{1F4CA} session end: acceptance");
2490
+ }
2491
+ cleanupSessionState(brainRoot, state.uuid);
2492
+ return result;
2493
+ }
2494
+ function classifyOutcome(state, currentSha, currentStatus) {
2495
+ const headMoved = state.sha !== currentSha;
2496
+ const startStatusSet = new Set(state.status);
2497
+ const endStatusSet = new Set(currentStatus);
2498
+ const newItems = currentStatus.filter((s) => !startStatusSet.has(s));
2499
+ const removedItems = state.status.filter((s) => !endStatusSet.has(s));
2500
+ if (!headMoved) {
2501
+ if (newItems.length === 0 && removedItems.length === 0) {
2502
+ return null;
2503
+ }
2504
+ if (newItems.length > 0) {
2505
+ return "acceptance";
2506
+ }
2507
+ if (removedItems.length > 0) {
2508
+ return "revert";
2509
+ }
2510
+ return null;
2511
+ }
2512
+ if (newItems.length > 0) {
2513
+ return "acceptance";
2514
+ }
2515
+ try {
2516
+ const diffStat = execSync3(
2517
+ `git diff ${state.sha}..${currentSha} --stat`,
2518
+ { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }
2519
+ ).trim();
2520
+ const logOutput = execSync3(
2521
+ `git log --oneline ${state.sha}..${currentSha}`,
2522
+ { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }
2523
+ ).trim();
2524
+ if (/\brevert\b/i.test(logOutput)) {
2525
+ return "revert";
2526
+ }
2527
+ if (!diffStat) {
2528
+ return "revert";
2529
+ }
2530
+ return "acceptance";
2531
+ } catch {
2532
+ return null;
2533
+ }
2534
+ }
2535
+ function applyContra(brainRoot, neurons) {
2536
+ let affected = 0;
2537
+ let skipped = 0;
2538
+ for (const neuronPath of neurons) {
2539
+ const region = neuronPath.split("/")[0] || "";
2540
+ if (PROTECTED_REGIONS_CONTRA.includes(region)) {
2541
+ skipped++;
2542
+ continue;
2543
+ }
2544
+ const result = contraNeuron(brainRoot, neuronPath);
2545
+ if (result > 0) {
2546
+ affected++;
2547
+ }
2548
+ }
2549
+ return { affected, skipped };
2550
+ }
2551
+ function buildOutcomeSummary(brainRoot) {
2552
+ const episodes = readEpisodes(brainRoot);
2553
+ const outcomeEpisodes = episodes.filter((e) => e.outcome && e.neurons);
2554
+ if (outcomeEpisodes.length === 0) return "";
2555
+ const stats = /* @__PURE__ */ new Map();
2556
+ for (const ep of outcomeEpisodes) {
2557
+ for (const neuron of ep.neurons) {
2558
+ const existing = stats.get(neuron) || { sessions: 0, reverts: 0, acceptances: 0 };
2559
+ existing.sessions++;
2560
+ if (ep.outcome === "revert") existing.reverts++;
2561
+ if (ep.outcome === "acceptance") existing.acceptances++;
2562
+ stats.set(neuron, existing);
2563
+ }
2564
+ }
2565
+ const lines = ["## Outcome Signals (from session history)\n"];
2566
+ lines.push("Neurons with high contra_ratio (>0.5) are consistently present in reverted sessions. Consider pruning or modifying them.\n");
2567
+ const sorted = [...stats.entries()].sort((a, b) => {
2568
+ const ratioA = a[1].sessions > 0 ? a[1].reverts / a[1].sessions : 0;
2569
+ const ratioB = b[1].sessions > 0 ? b[1].reverts / b[1].sessions : 0;
2570
+ return ratioB - ratioA;
2571
+ });
2572
+ for (const [neuron, s] of sorted) {
2573
+ const ratio = s.sessions > 0 ? (s.reverts / s.sessions).toFixed(2) : "0.00";
2574
+ const trend = parseFloat(ratio) > 0.5 ? "act on this" : parseFloat(ratio) > 0.3 ? "watch" : "";
2575
+ const safePath = neuron.replace(/[\n\r#]/g, " ").trim();
2576
+ lines.push(`- ${safePath}: sessions=${s.sessions} reverts=${s.reverts} acceptances=${s.acceptances} contra_ratio=${ratio} ${trend}`);
2577
+ }
2578
+ lines.push("");
2579
+ return lines.join("\n");
2580
+ }
2581
+ function readLatestSessionState(brainRoot) {
2582
+ const stateDir = join17(brainRoot, SESSION_STATE_DIR);
2583
+ if (!existsSync16(stateDir)) return null;
2584
+ let latest = null;
2585
+ try {
2586
+ for (const entry of readdirSync10(stateDir)) {
2587
+ if (!entry.startsWith("state_") || !entry.endsWith(".json")) continue;
2588
+ const fullPath = join17(stateDir, entry);
2589
+ const mtime = statSync5(fullPath).mtimeMs;
2590
+ if (!latest || mtime > latest.mtime) {
2591
+ latest = { path: fullPath, mtime };
2592
+ }
2593
+ }
2594
+ } catch {
2595
+ return null;
2596
+ }
2597
+ if (!latest) return null;
2598
+ try {
2599
+ return JSON.parse(readFileSync8(latest.path, "utf8"));
2600
+ } catch {
2601
+ return null;
2602
+ }
2603
+ }
2604
+ function filterHebbianPaths(statusLines) {
2605
+ const hebbianPatterns = ["hippocampus/session_state", "hippocampus/session_log", "hippocampus/digest_log", "_inbox/"];
2606
+ return statusLines.filter(
2607
+ (line) => !hebbianPatterns.some((p) => line.includes(p))
2608
+ );
2609
+ }
2610
+ function cleanupSessionState(brainRoot, uuid) {
2611
+ const stateDir = join17(brainRoot, SESSION_STATE_DIR);
2612
+ const filePath = join17(stateDir, `state_${uuid}.json`);
2613
+ try {
2614
+ if (existsSync16(filePath)) rmSync2(filePath);
2615
+ } catch {
2616
+ }
2617
+ }
2618
+ var init_outcome = __esm({
2619
+ "src/outcome.ts"() {
2620
+ "use strict";
2621
+ init_constants();
2622
+ init_scanner();
2623
+ init_subsumption();
2624
+ init_fire();
2625
+ init_episode();
2626
+ }
2627
+ });
2628
+
2197
2629
  // src/evolve.ts
2198
2630
  var evolve_exports = {};
2199
2631
  __export(evolve_exports, {
@@ -2205,16 +2637,32 @@ __export(evolve_exports, {
2205
2637
  runEvolve: () => runEvolve,
2206
2638
  validateActions: () => validateActions
2207
2639
  });
2640
+ import { existsSync as existsSync17, readFileSync as readFileSync9, writeFileSync as writeFileSync14 } from "fs";
2641
+ import { join as join18 } from "path";
2208
2642
  async function runEvolve(brainRoot, dryRun) {
2209
2643
  const apiKey = process.env.GEMINI_API_KEY;
2210
2644
  if (!apiKey) {
2211
2645
  console.error("\u274C GEMINI_API_KEY not set. Get one at https://aistudio.google.com/apikey");
2212
2646
  return { actions: [], executed: 0, skipped: 0, dryRun };
2213
2647
  }
2648
+ if (!dryRun && process.env.EVOLVE_NO_COOLDOWN !== "1") {
2649
+ const cooldownMs = (parseInt(process.env.EVOLVE_COOLDOWN_SECONDS ?? "60", 10) || 60) * 1e3;
2650
+ const cooldownPath = join18(brainRoot, EVOLVE_COOLDOWN_FILE);
2651
+ if (existsSync17(cooldownPath)) {
2652
+ const lastRun = parseInt(readFileSync9(cooldownPath, "utf8").trim(), 10);
2653
+ const elapsed = Date.now() - lastRun;
2654
+ if (elapsed < cooldownMs) {
2655
+ const remaining = Math.ceil((cooldownMs - elapsed) / 1e3);
2656
+ console.log(`\u23F3 evolve cooldown: ${remaining}s remaining (use EVOLVE_NO_COOLDOWN=1 to bypass)`);
2657
+ return { actions: [], executed: 0, skipped: 0, dryRun };
2658
+ }
2659
+ }
2660
+ }
2214
2661
  const episodes = readEpisodes(brainRoot);
2215
2662
  const brain = scanBrain(brainRoot);
2216
2663
  const summary = buildBrainSummary(brain);
2217
- const prompt = buildPrompt(summary, episodes);
2664
+ const outcomeSummary = buildOutcomeSummary(brainRoot);
2665
+ const prompt = buildPrompt(summary, episodes, outcomeSummary);
2218
2666
  let rawActions;
2219
2667
  try {
2220
2668
  rawActions = await callGemini(prompt, apiKey);
@@ -2240,6 +2688,7 @@ async function runEvolve(brainRoot, dryRun) {
2240
2688
  const executed = executeActions(brainRoot, actions);
2241
2689
  logEpisode(brainRoot, "evolve", "", `${executed} action(s) executed, ${skipped} skipped`);
2242
2690
  console.log(`\u{1F9E0} evolve: ${executed} action(s) executed, ${skipped} skipped`);
2691
+ writeFileSync14(join18(brainRoot, EVOLVE_COOLDOWN_FILE), String(Date.now()), "utf8");
2243
2692
  return { actions, executed, skipped, dryRun: false };
2244
2693
  }
2245
2694
  function buildBrainSummary(brain) {
@@ -2262,8 +2711,13 @@ function buildBrainSummary(brain) {
2262
2711
  }
2263
2712
  return lines.join("\n");
2264
2713
  }
2265
- function buildPrompt(summary, episodes) {
2266
- const episodeLines = episodes.length > 0 ? episodes.map((e) => `- [${e.ts}] ${e.type}: ${e.path} \u2014 ${e.detail}`).join("\n") : "(no recent episodes)";
2714
+ function sanitizeForPrompt(text) {
2715
+ const firstLine = (text.split("\n")[0] ?? "").trim();
2716
+ return firstLine.replace(/^#+\s*/g, "").slice(0, 200);
2717
+ }
2718
+ function buildPrompt(summary, episodes, outcomeSummary) {
2719
+ const episodeLines = episodes.length > 0 ? episodes.map((e) => `- [${e.ts}] ${e.type}: ${e.path} \u2014 ${sanitizeForPrompt(e.detail)}`).join("\n") : "(no recent episodes)";
2720
+ const outcomeSection = outcomeSummary || "";
2267
2721
  return `You are the evolve engine for a hebbian brain \u2014 a filesystem-based memory system for AI agents.
2268
2722
 
2269
2723
  ## Axioms
@@ -2275,6 +2729,7 @@ function buildPrompt(summary, episodes) {
2275
2729
  ## Current Brain
2276
2730
  ${summary}
2277
2731
 
2732
+ ${outcomeSection}
2278
2733
  ## Recent Episodes (last ${episodes.length})
2279
2734
  ${episodeLines}
2280
2735
 
@@ -2300,7 +2755,7 @@ Respond with a JSON array of actions:
2300
2755
  }
2301
2756
  async function callGemini(prompt, apiKey) {
2302
2757
  const model = process.env.EVOLVE_MODEL || DEFAULT_MODEL;
2303
- const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`;
2758
+ const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent`;
2304
2759
  const body = {
2305
2760
  contents: [{ parts: [{ text: prompt }] }],
2306
2761
  generationConfig: {
@@ -2316,7 +2771,7 @@ async function callGemini(prompt, apiKey) {
2316
2771
  try {
2317
2772
  const res = await fetch(url, {
2318
2773
  method: "POST",
2319
- headers: { "Content-Type": "application/json" },
2774
+ headers: { "Content-Type": "application/json", "x-goog-api-key": apiKey },
2320
2775
  body: JSON.stringify(body),
2321
2776
  signal: AbortSignal.timeout(API_TIMEOUT)
2322
2777
  });
@@ -2370,6 +2825,10 @@ function parseActions(text) {
2370
2825
  }
2371
2826
  function validateActions(actions, _brain) {
2372
2827
  return actions.filter((action) => {
2828
+ if (action.path.includes("..") || action.path.startsWith("/")) {
2829
+ console.log(` \u26A0\uFE0F blocked: ${action.type} ${action.path} (path traversal)`);
2830
+ return false;
2831
+ }
2373
2832
  const region = action.path.split("/")[0];
2374
2833
  if (!region || PROTECTED_REGIONS.includes(region)) {
2375
2834
  console.log(` \u{1F6E1}\uFE0F blocked: ${action.type} ${action.path} (protected region)`);
@@ -2395,7 +2854,7 @@ function executeActions(brainRoot, actions) {
2395
2854
  fireNeuron(brainRoot, action.path);
2396
2855
  break;
2397
2856
  case "grow":
2398
- growNeuron(brainRoot, action.path);
2857
+ growCandidate(brainRoot, action.path);
2399
2858
  break;
2400
2859
  case "signal":
2401
2860
  signalNeuron(brainRoot, action.path, action.signal || "dopamine");
@@ -2431,7 +2890,7 @@ function actionIcon(type) {
2431
2890
  return "\u2753";
2432
2891
  }
2433
2892
  }
2434
- var MAX_ACTIONS, PROTECTED_REGIONS, DEFAULT_MODEL, API_TIMEOUT, RETRY_DELAY;
2893
+ var MAX_ACTIONS, PROTECTED_REGIONS, DEFAULT_MODEL, API_TIMEOUT, RETRY_DELAY, EVOLVE_COOLDOWN_FILE;
2435
2894
  var init_evolve = __esm({
2436
2895
  "src/evolve.ts"() {
2437
2896
  "use strict";
@@ -2439,15 +2898,157 @@ var init_evolve = __esm({
2439
2898
  init_scanner();
2440
2899
  init_constants();
2441
2900
  init_fire();
2442
- init_grow();
2901
+ init_candidates();
2443
2902
  init_signal();
2444
2903
  init_rollback();
2445
2904
  init_decay();
2905
+ init_outcome();
2446
2906
  MAX_ACTIONS = 10;
2447
2907
  PROTECTED_REGIONS = ["brainstem", "limbic", "sensors"];
2448
2908
  DEFAULT_MODEL = "gemini-2.0-flash-lite";
2449
2909
  API_TIMEOUT = 3e4;
2450
2910
  RETRY_DELAY = 5e3;
2911
+ EVOLVE_COOLDOWN_FILE = "hippocampus/evolve_last_run";
2912
+ }
2913
+ });
2914
+
2915
+ // src/doctor.ts
2916
+ var doctor_exports = {};
2917
+ __export(doctor_exports, {
2918
+ runDoctor: () => runDoctor
2919
+ });
2920
+ import { existsSync as existsSync18, readFileSync as readFileSync10, readdirSync as readdirSync11 } from "fs";
2921
+ import { join as join19 } from "path";
2922
+ import { execSync as execSync4 } from "child_process";
2923
+ async function runDoctor(brainRoot) {
2924
+ let passed = 0, warnings = 0, failed = 0;
2925
+ const ok = (msg) => {
2926
+ console.log(` \u2705 ${msg}`);
2927
+ passed++;
2928
+ };
2929
+ const warn = (msg, fix) => {
2930
+ console.log(` \u26A0\uFE0F ${msg}`);
2931
+ if (fix) console.log(` \u2192 ${fix}`);
2932
+ warnings++;
2933
+ };
2934
+ const fail = (msg, fix) => {
2935
+ console.log(` \u274C ${msg}`);
2936
+ if (fix) console.log(` \u2192 ${fix}`);
2937
+ failed++;
2938
+ };
2939
+ console.log("\n\u{1FA7A} hebbian doctor\n");
2940
+ console.log("Node.js");
2941
+ const nodeVer = process.versions.node;
2942
+ const [major] = nodeVer.split(".").map(Number);
2943
+ if ((major ?? 0) >= 22) {
2944
+ ok(`Node.js ${nodeVer} (>= 22 required)`);
2945
+ } else {
2946
+ fail(`Node.js ${nodeVer} \u2014 need >= 22`, "nvm install 22 && nvm use 22");
2947
+ }
2948
+ console.log("\nnpm package");
2949
+ try {
2950
+ const pkgPath = new URL("../package.json", import.meta.url).pathname;
2951
+ const pkg = JSON.parse(readFileSync10(pkgPath, "utf8"));
2952
+ const local = pkg.version || "unknown";
2953
+ let remote = "";
2954
+ try {
2955
+ const out = execSync4("npm view hebbian version 2>/dev/null", { timeout: 5e3 }).toString().trim();
2956
+ remote = out;
2957
+ } catch {
2958
+ }
2959
+ if (remote && remote !== local) {
2960
+ warn(`hebbian ${local} installed, ${remote} available`, "npm i -g hebbian@latest");
2961
+ } else {
2962
+ ok(`hebbian ${local}${remote ? " (up to date)" : ""}`);
2963
+ }
2964
+ } catch {
2965
+ warn("Could not read package.json");
2966
+ }
2967
+ console.log("\nbrain structure");
2968
+ if (!existsSync18(brainRoot)) {
2969
+ fail(`Brain not found at ${brainRoot}`, "hebbian init ./brain");
2970
+ } else {
2971
+ ok(`Brain root: ${brainRoot}`);
2972
+ for (const region of REGIONS) {
2973
+ const regionDir = join19(brainRoot, region);
2974
+ if (existsSync18(regionDir)) {
2975
+ ok(`Region: ${region}`);
2976
+ } else {
2977
+ warn(`Missing region: ${region}`, `mkdir -p ${regionDir}`);
2978
+ }
2979
+ }
2980
+ }
2981
+ console.log("\nClaude Code hooks");
2982
+ const settingsPath = join19(process.cwd(), ".claude", "settings.local.json");
2983
+ if (!existsSync18(settingsPath)) {
2984
+ warn("No .claude/settings.local.json found", "hebbian claude install");
2985
+ } else {
2986
+ try {
2987
+ const settings = JSON.parse(readFileSync10(settingsPath, "utf8"));
2988
+ const hooks = settings.hooks || {};
2989
+ const hasStop = Object.entries(hooks).some(
2990
+ ([event, entries]) => event === "Stop" && Array.isArray(entries) && entries.some(
2991
+ (e) => typeof e === "object" && e !== null && "command" in e && typeof e.command === "string" && e.command.includes("hebbian digest")
2992
+ )
2993
+ );
2994
+ const hasStart = Object.entries(hooks).some(
2995
+ ([event, entries]) => event === "SessionStart" && Array.isArray(entries) && entries.some(
2996
+ (e) => typeof e === "object" && e !== null && "command" in e && typeof e.command === "string" && e.command.includes("hebbian emit")
2997
+ )
2998
+ );
2999
+ if (hasStop && hasStart) {
3000
+ ok("SessionStart + Stop hooks installed");
3001
+ } else {
3002
+ if (!hasStart) warn("SessionStart hook missing", "hebbian claude install");
3003
+ if (!hasStop) warn("Stop hook missing", "hebbian claude install");
3004
+ }
3005
+ } catch {
3006
+ fail("Malformed .claude/settings.local.json", "hebbian claude install");
3007
+ }
3008
+ }
3009
+ console.log("\nnpx resolution");
3010
+ try {
3011
+ const resolved = execSync4("which npx", { timeout: 3e3 }).toString().trim();
3012
+ ok(`npx: ${resolved}`);
3013
+ } catch {
3014
+ fail("npx not found in PATH", "Install Node.js from https://nodejs.org");
3015
+ }
3016
+ console.log("\ncandidates");
3017
+ try {
3018
+ let total = 0;
3019
+ for (const region of REGIONS) {
3020
+ const candidateDir = join19(brainRoot, region, "_candidates");
3021
+ if (existsSync18(candidateDir)) {
3022
+ const entries = readdirSync11(candidateDir, { withFileTypes: true });
3023
+ const count = entries.filter((e) => e.isDirectory()).length;
3024
+ total += count;
3025
+ }
3026
+ }
3027
+ if (total === 0) {
3028
+ ok("No pending candidates");
3029
+ } else {
3030
+ warn(`${total} candidate(s) pending`, "hebbian candidates \u2014 to view");
3031
+ }
3032
+ } catch {
3033
+ warn("Could not scan candidates");
3034
+ }
3035
+ console.log(`
3036
+ ${"\u2500".repeat(40)}`);
3037
+ console.log(` passed: ${passed} warnings: ${warnings} failed: ${failed}`);
3038
+ if (failed > 0) {
3039
+ console.log(" Fix the \u274C issues above, then re-run `hebbian doctor`");
3040
+ } else if (warnings > 0) {
3041
+ console.log(" Looking mostly good! Review \u26A0\uFE0F warnings above.");
3042
+ } else {
3043
+ console.log(" All checks passed. \u{1F389}");
3044
+ }
3045
+ console.log("");
3046
+ return { passed, warnings, failed };
3047
+ }
3048
+ var init_doctor = __esm({
3049
+ "src/doctor.ts"() {
3050
+ "use strict";
3051
+ init_constants();
2451
3052
  }
2452
3053
  });
2453
3054
 
@@ -2455,7 +3056,7 @@ var init_evolve = __esm({
2455
3056
  init_constants();
2456
3057
  import { parseArgs } from "util";
2457
3058
  import { resolve as resolve3 } from "path";
2458
- var VERSION = "0.3.2";
3059
+ var VERSION = "0.5.0";
2459
3060
  var HELP = `
2460
3061
  hebbian v${VERSION} \u2014 Folder-as-neuron brain for any AI agent.
2461
3062
 
@@ -2479,7 +3080,11 @@ COMMANDS:
2479
3080
  inbox Process corrections inbox
2480
3081
  claude install|uninstall|status Manage Claude Code hooks
2481
3082
  digest [--transcript <path>] Extract corrections from conversation
3083
+ candidates [promote] List candidates or promote graduated ones
2482
3084
  evolve [--dry-run] LLM-powered brain evolution (Gemini)
3085
+ session start|end Capture/detect session outcomes
3086
+ sessions Show session outcome history
3087
+ doctor Self-diagnostic (hooks, brain, versions)
2483
3088
  diag Print brain diagnostics
2484
3089
  stats Print brain statistics
2485
3090
 
@@ -2521,6 +3126,7 @@ async function main(argv) {
2521
3126
  port: { type: "string", short: "p" },
2522
3127
  transcript: { type: "string", short: "t" },
2523
3128
  "dry-run": { type: "boolean" },
3129
+ global: { type: "boolean", short: "g" },
2524
3130
  help: { type: "boolean", short: "h" },
2525
3131
  version: { type: "boolean", short: "v" }
2526
3132
  },
@@ -2641,18 +3247,23 @@ async function main(argv) {
2641
3247
  }
2642
3248
  case "claude": {
2643
3249
  const sub = positionals[1];
3250
+ const isGlobal = values.global === true;
2644
3251
  const { installHooks: installHooks2, uninstallHooks: uninstallHooks2, checkHooks: checkHooks2 } = await Promise.resolve().then(() => (init_hooks(), hooks_exports));
2645
3252
  switch (sub) {
2646
3253
  case "install": {
2647
- const installBrain = values.brain ? resolve3(values.brain) : resolve3("./brain");
2648
- installHooks2(installBrain);
3254
+ const installBrain = values.brain ? resolve3(values.brain) : isGlobal ? process.env.HEBBIAN_BRAIN ? resolve3(process.env.HEBBIAN_BRAIN) : "" : resolve3("./brain");
3255
+ if (isGlobal && !installBrain) {
3256
+ console.error("\u274C --global requires --brain <path> or HEBBIAN_BRAIN env var");
3257
+ process.exit(1);
3258
+ }
3259
+ installHooks2(installBrain, void 0, isGlobal);
2649
3260
  break;
2650
3261
  }
2651
3262
  case "uninstall":
2652
- uninstallHooks2();
3263
+ uninstallHooks2(void 0, isGlobal);
2653
3264
  break;
2654
3265
  case "status": {
2655
- checkHooks2();
3266
+ checkHooks2(void 0, isGlobal);
2656
3267
  console.log(` version: v${VERSION}`);
2657
3268
  const { checkForUpdates: checkUpdates, formatUpdateBanner: formatBanner } = await Promise.resolve().then(() => (init_update_check(), update_check_exports));
2658
3269
  const updateStatus = await checkUpdates(VERSION);
@@ -2661,7 +3272,7 @@ async function main(argv) {
2661
3272
  break;
2662
3273
  }
2663
3274
  default:
2664
- console.error("Usage: hebbian claude <install|uninstall|status>");
3275
+ console.error("Usage: hebbian claude <install|uninstall|status> [--global]");
2665
3276
  process.exit(1);
2666
3277
  }
2667
3278
  break;
@@ -2684,12 +3295,70 @@ async function main(argv) {
2684
3295
  }
2685
3296
  break;
2686
3297
  }
3298
+ case "candidates": {
3299
+ const subCmd = positionals[1];
3300
+ const { listCandidates: listCandidates2, promoteCandidates: promoteCandidates2 } = await Promise.resolve().then(() => (init_candidates(), candidates_exports));
3301
+ if (subCmd === "promote") {
3302
+ const result = promoteCandidates2(brainRoot);
3303
+ console.log(`\u{1F393} promoted: ${result.promoted.length}, decayed: ${result.decayed.length}`);
3304
+ } else {
3305
+ const candidates = listCandidates2(brainRoot);
3306
+ if (candidates.length === 0) {
3307
+ console.log("No pending candidates");
3308
+ } else {
3309
+ console.log(`Candidates (promote at counter=${3}):`);
3310
+ for (const c of candidates) {
3311
+ const bar = "\u2588".repeat(c.counter) + "\u2591".repeat(Math.max(0, 3 - c.counter));
3312
+ console.log(` ${bar} ${c.counter}/3 ${c.targetPath} (${c.daysInactive}d idle)`);
3313
+ }
3314
+ }
3315
+ }
3316
+ break;
3317
+ }
2687
3318
  case "evolve": {
2688
3319
  const dryRun = values["dry-run"] === true;
2689
3320
  const { runEvolve: runEvolve2 } = await Promise.resolve().then(() => (init_evolve(), evolve_exports));
2690
3321
  await runEvolve2(brainRoot, dryRun);
2691
3322
  break;
2692
3323
  }
3324
+ case "session": {
3325
+ const sub = positionals[1];
3326
+ const { captureSessionStart: captureSessionStart2, detectOutcome: detectOutcome2 } = await Promise.resolve().then(() => (init_outcome(), outcome_exports));
3327
+ switch (sub) {
3328
+ case "start":
3329
+ captureSessionStart2(brainRoot);
3330
+ break;
3331
+ case "end":
3332
+ detectOutcome2(brainRoot);
3333
+ break;
3334
+ default:
3335
+ console.error("Usage: hebbian session <start|end>");
3336
+ process.exit(1);
3337
+ }
3338
+ break;
3339
+ }
3340
+ case "sessions": {
3341
+ const { readEpisodes: readEpisodes2 } = await Promise.resolve().then(() => (init_episode(), episode_exports));
3342
+ const episodes = readEpisodes2(brainRoot).filter((e) => e.outcome);
3343
+ if (episodes.length === 0) {
3344
+ console.log("No session outcomes recorded yet");
3345
+ } else {
3346
+ console.log("Session Outcomes:");
3347
+ for (const ep of episodes.reverse()) {
3348
+ const icon = ep.outcome === "revert" ? "\u{1F504}" : "\u2705";
3349
+ const neurons = ep.neurons ? `${ep.neurons.length} neurons` : "";
3350
+ console.log(` ${icon} ${ep.ts.slice(0, 19)} ${ep.outcome} ${neurons}`);
3351
+ }
3352
+ console.log(`
3353
+ Total: ${episodes.length} sessions`);
3354
+ }
3355
+ break;
3356
+ }
3357
+ case "doctor": {
3358
+ const { runDoctor: runDoctor2 } = await Promise.resolve().then(() => (init_doctor(), doctor_exports));
3359
+ await runDoctor2(brainRoot);
3360
+ break;
3361
+ }
2693
3362
  case "diag":
2694
3363
  case "stats": {
2695
3364
  const { scanBrain: scanBrain2 } = await Promise.resolve().then(() => (init_scanner(), scanner_exports));