hebbian 0.3.3 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +155 -70
- package/dist/bin/hebbian.js +988 -71
- package/dist/bin/hebbian.js.map +1 -1
- package/dist/candidates.d.ts +32 -0
- package/dist/candidates.d.ts.map +1 -0
- package/dist/constants.d.ts +4 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/doctor.d.ts +7 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/emit.d.ts.map +1 -1
- package/dist/episode.d.ts +6 -1
- package/dist/episode.d.ts.map +1 -1
- package/dist/evolve.d.ts +22 -0
- package/dist/evolve.d.ts.map +1 -0
- package/dist/fire.d.ts +10 -0
- package/dist/fire.d.ts.map +1 -1
- package/dist/hooks.d.ts +3 -3
- package/dist/hooks.d.ts.map +1 -1
- package/dist/inbox.d.ts.map +1 -1
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +690 -66
- package/dist/index.js.map +1 -1
- package/dist/outcome.d.ts +42 -0
- package/dist/outcome.d.ts.map +1 -0
- package/package.json +1 -1
package/dist/bin/hebbian.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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 {
|
|
@@ -1216,31 +1248,187 @@ var init_watch = __esm({
|
|
|
1216
1248
|
}
|
|
1217
1249
|
});
|
|
1218
1250
|
|
|
1251
|
+
// src/candidates.ts
|
|
1252
|
+
var candidates_exports = {};
|
|
1253
|
+
__export(candidates_exports, {
|
|
1254
|
+
CANDIDATE_DECAY_DAYS: () => CANDIDATE_DECAY_DAYS,
|
|
1255
|
+
CANDIDATE_THRESHOLD: () => CANDIDATE_THRESHOLD,
|
|
1256
|
+
fromCandidatePath: () => fromCandidatePath,
|
|
1257
|
+
growCandidate: () => growCandidate,
|
|
1258
|
+
listCandidates: () => listCandidates,
|
|
1259
|
+
promoteCandidates: () => promoteCandidates,
|
|
1260
|
+
toCandidatePath: () => toCandidatePath
|
|
1261
|
+
});
|
|
1262
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync6, readdirSync as readdirSync7, renameSync as renameSync3, rmSync, statSync as statSync4 } from "fs";
|
|
1263
|
+
import { join as join12, dirname as dirname2, relative as relative3 } from "path";
|
|
1264
|
+
function toCandidatePath(neuronPath) {
|
|
1265
|
+
const slash = neuronPath.indexOf("/");
|
|
1266
|
+
if (slash === -1) throw new Error(`Invalid neuron path (missing region): ${neuronPath}`);
|
|
1267
|
+
return `${neuronPath.slice(0, slash)}/${CANDIDATE_SEGMENT}/${neuronPath.slice(slash + 1)}`;
|
|
1268
|
+
}
|
|
1269
|
+
function fromCandidatePath(candidatePath) {
|
|
1270
|
+
return candidatePath.replace(`/${CANDIDATE_SEGMENT}/`, "/");
|
|
1271
|
+
}
|
|
1272
|
+
function growCandidate(brainRoot, neuronPath) {
|
|
1273
|
+
const candidatePath = toCandidatePath(neuronPath);
|
|
1274
|
+
const result = growNeuron(brainRoot, candidatePath);
|
|
1275
|
+
if (result.counter >= CANDIDATE_THRESHOLD) {
|
|
1276
|
+
const ok = moveCandidate(brainRoot, candidatePath, neuronPath);
|
|
1277
|
+
return { ...result, path: ok ? neuronPath : result.path, promoted: ok };
|
|
1278
|
+
}
|
|
1279
|
+
console.log(` \u{1F331} candidate (${result.counter}/${CANDIDATE_THRESHOLD}): ${candidatePath}`);
|
|
1280
|
+
return { ...result, promoted: false };
|
|
1281
|
+
}
|
|
1282
|
+
function moveCandidate(brainRoot, candidatePath, targetPath) {
|
|
1283
|
+
const src = join12(brainRoot, candidatePath);
|
|
1284
|
+
if (!existsSync11(src)) return false;
|
|
1285
|
+
const dst = join12(brainRoot, targetPath);
|
|
1286
|
+
if (existsSync11(dst)) {
|
|
1287
|
+
fireNeuron(brainRoot, targetPath);
|
|
1288
|
+
rmSync(src, { recursive: true, force: true });
|
|
1289
|
+
} else {
|
|
1290
|
+
mkdirSync6(dirname2(dst), { recursive: true });
|
|
1291
|
+
renameSync3(src, dst);
|
|
1292
|
+
}
|
|
1293
|
+
console.log(`\u{1F393} promoted: ${candidatePath} \u2192 ${targetPath}`);
|
|
1294
|
+
return true;
|
|
1295
|
+
}
|
|
1296
|
+
function promoteCandidates(brainRoot) {
|
|
1297
|
+
const promoted = [];
|
|
1298
|
+
const decayed = [];
|
|
1299
|
+
const decayMs = CANDIDATE_DECAY_DAYS * 24 * 60 * 60 * 1e3;
|
|
1300
|
+
const now = Date.now();
|
|
1301
|
+
for (const region of REGIONS) {
|
|
1302
|
+
const candidateRoot = join12(brainRoot, region, CANDIDATE_SEGMENT);
|
|
1303
|
+
walkNeuronDirs(candidateRoot, (neuronDir) => {
|
|
1304
|
+
const rel = relative3(join12(brainRoot, region), neuronDir);
|
|
1305
|
+
const candidatePath = `${region}/${rel}`;
|
|
1306
|
+
const targetPath = fromCandidatePath(candidatePath);
|
|
1307
|
+
const counter = readCounter(neuronDir);
|
|
1308
|
+
const mtime = statSync4(neuronDir).mtimeMs;
|
|
1309
|
+
if (counter >= CANDIDATE_THRESHOLD) {
|
|
1310
|
+
moveCandidate(brainRoot, candidatePath, targetPath);
|
|
1311
|
+
promoted.push(targetPath);
|
|
1312
|
+
} else if (now - mtime > decayMs) {
|
|
1313
|
+
rmSync(neuronDir, { recursive: true, force: true });
|
|
1314
|
+
decayed.push(candidatePath);
|
|
1315
|
+
console.log(`\u{1F480} candidate decayed: ${candidatePath}`);
|
|
1316
|
+
}
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
return { promoted, decayed };
|
|
1320
|
+
}
|
|
1321
|
+
function listCandidates(brainRoot) {
|
|
1322
|
+
const results = [];
|
|
1323
|
+
const now = Date.now();
|
|
1324
|
+
for (const region of REGIONS) {
|
|
1325
|
+
const candidateRoot = join12(brainRoot, region, CANDIDATE_SEGMENT);
|
|
1326
|
+
walkNeuronDirs(candidateRoot, (neuronDir) => {
|
|
1327
|
+
const rel = relative3(join12(brainRoot, region), neuronDir);
|
|
1328
|
+
const candidatePath = `${region}/${rel}`;
|
|
1329
|
+
const targetPath = fromCandidatePath(candidatePath);
|
|
1330
|
+
const counter = readCounter(neuronDir);
|
|
1331
|
+
const mtime = statSync4(neuronDir).mtimeMs;
|
|
1332
|
+
const daysInactive = Math.floor((now - mtime) / (24 * 60 * 60 * 1e3));
|
|
1333
|
+
results.push({ candidatePath, targetPath, counter, daysInactive });
|
|
1334
|
+
});
|
|
1335
|
+
}
|
|
1336
|
+
return results;
|
|
1337
|
+
}
|
|
1338
|
+
function walkNeuronDirs(dir, cb) {
|
|
1339
|
+
if (!existsSync11(dir)) return;
|
|
1340
|
+
try {
|
|
1341
|
+
const entries = readdirSync7(dir, { withFileTypes: true });
|
|
1342
|
+
const hasNeuron = entries.some((e) => e.isFile() && e.name.endsWith(".neuron"));
|
|
1343
|
+
if (hasNeuron) {
|
|
1344
|
+
cb(dir);
|
|
1345
|
+
return;
|
|
1346
|
+
}
|
|
1347
|
+
for (const entry of entries) {
|
|
1348
|
+
if (entry.isDirectory() && !entry.name.startsWith(".")) {
|
|
1349
|
+
walkNeuronDirs(join12(dir, entry.name), cb);
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
} catch {
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
function readCounter(dir) {
|
|
1356
|
+
try {
|
|
1357
|
+
const files = readdirSync7(dir).filter((f) => /^\d+\.neuron$/.test(f));
|
|
1358
|
+
if (files.length === 0) return 0;
|
|
1359
|
+
return Math.max(...files.map((f) => parseInt(f, 10)));
|
|
1360
|
+
} catch {
|
|
1361
|
+
return 0;
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
var CANDIDATE_THRESHOLD, CANDIDATE_DECAY_DAYS, CANDIDATE_SEGMENT;
|
|
1365
|
+
var init_candidates = __esm({
|
|
1366
|
+
"src/candidates.ts"() {
|
|
1367
|
+
"use strict";
|
|
1368
|
+
init_constants();
|
|
1369
|
+
init_grow();
|
|
1370
|
+
init_fire();
|
|
1371
|
+
CANDIDATE_THRESHOLD = 3;
|
|
1372
|
+
CANDIDATE_DECAY_DAYS = 14;
|
|
1373
|
+
CANDIDATE_SEGMENT = "_candidates";
|
|
1374
|
+
}
|
|
1375
|
+
});
|
|
1376
|
+
|
|
1219
1377
|
// src/episode.ts
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1378
|
+
var episode_exports = {};
|
|
1379
|
+
__export(episode_exports, {
|
|
1380
|
+
logEpisode: () => logEpisode,
|
|
1381
|
+
readEpisodes: () => readEpisodes
|
|
1382
|
+
});
|
|
1383
|
+
import { readdirSync as readdirSync8, readFileSync as readFileSync4, writeFileSync as writeFileSync9, mkdirSync as mkdirSync7, existsSync as existsSync12 } from "fs";
|
|
1384
|
+
import { join as join13 } from "path";
|
|
1385
|
+
function logEpisode(brainRoot, type, path, detail, extra) {
|
|
1386
|
+
const logDir = join13(brainRoot, SESSION_LOG_DIR);
|
|
1387
|
+
if (!existsSync12(logDir)) {
|
|
1388
|
+
mkdirSync7(logDir, { recursive: true });
|
|
1226
1389
|
}
|
|
1227
1390
|
const nextSlot = getNextSlot(logDir);
|
|
1228
1391
|
const episode = {
|
|
1229
1392
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1230
1393
|
type,
|
|
1231
1394
|
path,
|
|
1232
|
-
detail
|
|
1395
|
+
detail,
|
|
1396
|
+
...extra?.outcome ? { outcome: extra.outcome } : {},
|
|
1397
|
+
...extra?.neurons ? { neurons: extra.neurons } : {}
|
|
1233
1398
|
};
|
|
1234
1399
|
writeFileSync9(
|
|
1235
|
-
|
|
1400
|
+
join13(logDir, `memory${nextSlot}.neuron`),
|
|
1236
1401
|
JSON.stringify(episode),
|
|
1237
1402
|
"utf8"
|
|
1238
1403
|
);
|
|
1239
1404
|
}
|
|
1405
|
+
function readEpisodes(brainRoot) {
|
|
1406
|
+
const logDir = join13(brainRoot, SESSION_LOG_DIR);
|
|
1407
|
+
if (!existsSync12(logDir)) return [];
|
|
1408
|
+
const episodes = [];
|
|
1409
|
+
let entries;
|
|
1410
|
+
try {
|
|
1411
|
+
entries = readdirSync8(logDir);
|
|
1412
|
+
} catch {
|
|
1413
|
+
return [];
|
|
1414
|
+
}
|
|
1415
|
+
for (const entry of entries) {
|
|
1416
|
+
if (!entry.startsWith("memory") || !entry.endsWith(".neuron")) continue;
|
|
1417
|
+
try {
|
|
1418
|
+
const content = readFileSync4(join13(logDir, entry), "utf8");
|
|
1419
|
+
if (content.trim()) {
|
|
1420
|
+
episodes.push(JSON.parse(content));
|
|
1421
|
+
}
|
|
1422
|
+
} catch {
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
episodes.sort((a, b) => a.ts.localeCompare(b.ts));
|
|
1426
|
+
return episodes;
|
|
1427
|
+
}
|
|
1240
1428
|
function getNextSlot(logDir) {
|
|
1241
1429
|
let maxSlot = 0;
|
|
1242
1430
|
try {
|
|
1243
|
-
for (const entry of
|
|
1431
|
+
for (const entry of readdirSync8(logDir)) {
|
|
1244
1432
|
if (entry.startsWith("memory") && entry.endsWith(".neuron")) {
|
|
1245
1433
|
const n = parseInt(entry.replace("memory", "").replace(".neuron", ""), 10);
|
|
1246
1434
|
if (!isNaN(n) && n > maxSlot) maxSlot = n;
|
|
@@ -1267,11 +1455,11 @@ __export(inbox_exports, {
|
|
|
1267
1455
|
ensureInbox: () => ensureInbox,
|
|
1268
1456
|
processInbox: () => processInbox
|
|
1269
1457
|
});
|
|
1270
|
-
import { readFileSync as readFileSync5, writeFileSync as writeFileSync10, existsSync as
|
|
1271
|
-
import { join as
|
|
1458
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync10, existsSync as existsSync13, mkdirSync as mkdirSync8 } from "fs";
|
|
1459
|
+
import { join as join14 } from "path";
|
|
1272
1460
|
function processInbox(brainRoot) {
|
|
1273
|
-
const inboxPath =
|
|
1274
|
-
if (!
|
|
1461
|
+
const inboxPath = join14(brainRoot, INBOX_DIR, CORRECTIONS_FILE);
|
|
1462
|
+
if (!existsSync13(inboxPath)) {
|
|
1275
1463
|
return { processed: 0, skipped: 0, errors: [] };
|
|
1276
1464
|
}
|
|
1277
1465
|
const content = readFileSync5(inboxPath, "utf8").trim();
|
|
@@ -1326,16 +1514,18 @@ function processInbox(brainRoot) {
|
|
|
1326
1514
|
}
|
|
1327
1515
|
function applyCorrection(brainRoot, correction) {
|
|
1328
1516
|
const neuronPath = correction.path;
|
|
1329
|
-
const fullPath =
|
|
1517
|
+
const fullPath = join14(brainRoot, neuronPath);
|
|
1330
1518
|
const counterAdd = Math.max(1, correction.counter_add || 1);
|
|
1331
|
-
if (
|
|
1519
|
+
if (existsSync13(fullPath)) {
|
|
1332
1520
|
for (let i = 0; i < counterAdd; i++) {
|
|
1333
1521
|
fireNeuron(brainRoot, neuronPath);
|
|
1334
1522
|
}
|
|
1335
1523
|
} else {
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1524
|
+
const candResult = growCandidate(brainRoot, neuronPath);
|
|
1525
|
+
if (candResult.promoted) {
|
|
1526
|
+
for (let i = 1; i < counterAdd; i++) {
|
|
1527
|
+
fireNeuron(brainRoot, neuronPath);
|
|
1528
|
+
}
|
|
1339
1529
|
}
|
|
1340
1530
|
}
|
|
1341
1531
|
if (correction.dopamine && correction.dopamine > 0) {
|
|
@@ -1354,12 +1544,12 @@ function isPathSafe(path) {
|
|
|
1354
1544
|
return true;
|
|
1355
1545
|
}
|
|
1356
1546
|
function ensureInbox(brainRoot) {
|
|
1357
|
-
const inboxDir =
|
|
1358
|
-
if (!
|
|
1359
|
-
|
|
1547
|
+
const inboxDir = join14(brainRoot, INBOX_DIR);
|
|
1548
|
+
if (!existsSync13(inboxDir)) {
|
|
1549
|
+
mkdirSync8(inboxDir, { recursive: true });
|
|
1360
1550
|
}
|
|
1361
|
-
const filePath =
|
|
1362
|
-
if (!
|
|
1551
|
+
const filePath = join14(inboxDir, CORRECTIONS_FILE);
|
|
1552
|
+
if (!existsSync13(filePath)) {
|
|
1363
1553
|
writeFileSync10(filePath, "", "utf8");
|
|
1364
1554
|
}
|
|
1365
1555
|
return filePath;
|
|
@@ -1375,7 +1565,7 @@ var init_inbox = __esm({
|
|
|
1375
1565
|
"src/inbox.ts"() {
|
|
1376
1566
|
"use strict";
|
|
1377
1567
|
init_constants();
|
|
1378
|
-
|
|
1568
|
+
init_candidates();
|
|
1379
1569
|
init_fire();
|
|
1380
1570
|
init_signal();
|
|
1381
1571
|
init_episode();
|
|
@@ -1672,17 +1862,25 @@ __export(hooks_exports, {
|
|
|
1672
1862
|
installHooks: () => installHooks,
|
|
1673
1863
|
uninstallHooks: () => uninstallHooks
|
|
1674
1864
|
});
|
|
1675
|
-
import { readFileSync as readFileSync6, writeFileSync as writeFileSync11, existsSync as
|
|
1865
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync11, existsSync as existsSync14, mkdirSync as mkdirSync9, readdirSync as readdirSync9 } from "fs";
|
|
1676
1866
|
import { execSync as execSync2 } from "child_process";
|
|
1677
|
-
import { join as
|
|
1678
|
-
function installHooks(brainRoot, projectRoot) {
|
|
1867
|
+
import { join as join15, resolve as resolve2 } from "path";
|
|
1868
|
+
function installHooks(brainRoot, projectRoot, global) {
|
|
1679
1869
|
const root = projectRoot || process.cwd();
|
|
1680
1870
|
const resolvedBrain = resolve2(brainRoot);
|
|
1681
|
-
if (
|
|
1871
|
+
if (global) {
|
|
1872
|
+
const home = process.env.HOME || "~";
|
|
1873
|
+
if (!brainRoot.startsWith("/") && !brainRoot.startsWith(home)) {
|
|
1874
|
+
console.error("\u274C --global requires an absolute --brain path (e.g. --brain ~/brain)");
|
|
1875
|
+
process.exit(1);
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
if (!existsSync14(resolvedBrain) || !hasBrainRegions(resolvedBrain)) {
|
|
1682
1879
|
initBrain(resolvedBrain);
|
|
1683
1880
|
}
|
|
1684
|
-
const settingsDir =
|
|
1685
|
-
const
|
|
1881
|
+
const settingsDir = global ? resolve2(process.env.HOME || "~", SETTINGS_DIR) : join15(root, SETTINGS_DIR);
|
|
1882
|
+
const settingsFile = global ? "settings.json" : SETTINGS_FILE;
|
|
1883
|
+
const settingsPath = join15(settingsDir, settingsFile);
|
|
1686
1884
|
const defaultBrain = resolve2(root, "brain");
|
|
1687
1885
|
const brainFlag = resolvedBrain === defaultBrain ? "" : ` --brain ${resolvedBrain}`;
|
|
1688
1886
|
let npxBin = "npx";
|
|
@@ -1691,7 +1889,7 @@ function installHooks(brainRoot, projectRoot) {
|
|
|
1691
1889
|
} catch {
|
|
1692
1890
|
}
|
|
1693
1891
|
let settings = {};
|
|
1694
|
-
if (
|
|
1892
|
+
if (existsSync14(settingsPath)) {
|
|
1695
1893
|
try {
|
|
1696
1894
|
settings = JSON.parse(readFileSync6(settingsPath, "utf8"));
|
|
1697
1895
|
} catch {
|
|
@@ -1708,8 +1906,8 @@ function installHooks(brainRoot, projectRoot) {
|
|
|
1708
1906
|
matcher: "startup|resume",
|
|
1709
1907
|
entry: {
|
|
1710
1908
|
type: "command",
|
|
1711
|
-
command: `${npxBin} hebbian emit claude${brainFlag}`,
|
|
1712
|
-
timeout:
|
|
1909
|
+
command: `${npxBin} hebbian emit claude${brainFlag} && ${npxBin} hebbian session start${brainFlag}`,
|
|
1910
|
+
timeout: 15,
|
|
1713
1911
|
statusMessage: `${HOOK_MARKER} refreshing brain`
|
|
1714
1912
|
}
|
|
1715
1913
|
},
|
|
@@ -1717,7 +1915,7 @@ function installHooks(brainRoot, projectRoot) {
|
|
|
1717
1915
|
event: "Stop",
|
|
1718
1916
|
entry: {
|
|
1719
1917
|
type: "command",
|
|
1720
|
-
command: `${npxBin} hebbian digest${brainFlag}`,
|
|
1918
|
+
command: `${npxBin} hebbian digest${brainFlag}; ${npxBin} hebbian session end${brainFlag}`,
|
|
1721
1919
|
timeout: 30,
|
|
1722
1920
|
statusMessage: `${HOOK_MARKER} digesting session`
|
|
1723
1921
|
}
|
|
@@ -1740,18 +1938,20 @@ function installHooks(brainRoot, projectRoot) {
|
|
|
1740
1938
|
hooks[event].push(group);
|
|
1741
1939
|
}
|
|
1742
1940
|
}
|
|
1743
|
-
if (!
|
|
1744
|
-
|
|
1941
|
+
if (!existsSync14(settingsDir)) {
|
|
1942
|
+
mkdirSync9(settingsDir, { recursive: true });
|
|
1745
1943
|
}
|
|
1746
1944
|
writeFileSync11(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
1747
1945
|
console.log(`\u2705 hebbian hooks installed at ${settingsPath}`);
|
|
1748
1946
|
console.log(` SessionStart \u2192 ${npxBin} hebbian emit claude${brainFlag}`);
|
|
1749
1947
|
console.log(` Stop \u2192 ${npxBin} hebbian digest${brainFlag}`);
|
|
1750
1948
|
}
|
|
1751
|
-
function uninstallHooks(projectRoot) {
|
|
1949
|
+
function uninstallHooks(projectRoot, global) {
|
|
1752
1950
|
const root = projectRoot || process.cwd();
|
|
1753
|
-
const
|
|
1754
|
-
|
|
1951
|
+
const settingsDir = global ? resolve2(process.env.HOME || "~", SETTINGS_DIR) : join15(root, SETTINGS_DIR);
|
|
1952
|
+
const settingsFile = global ? "settings.json" : SETTINGS_FILE;
|
|
1953
|
+
const settingsPath = join15(settingsDir, settingsFile);
|
|
1954
|
+
if (!existsSync14(settingsPath)) {
|
|
1755
1955
|
console.log("No hooks installed (settings.local.json not found)");
|
|
1756
1956
|
return;
|
|
1757
1957
|
}
|
|
@@ -1784,15 +1984,17 @@ function uninstallHooks(projectRoot) {
|
|
|
1784
1984
|
writeFileSync11(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
1785
1985
|
console.log(`\u2705 removed ${removed} hebbian hook(s) from ${settingsPath}`);
|
|
1786
1986
|
}
|
|
1787
|
-
function checkHooks(projectRoot) {
|
|
1987
|
+
function checkHooks(projectRoot, global) {
|
|
1788
1988
|
const root = projectRoot || process.cwd();
|
|
1789
|
-
const
|
|
1989
|
+
const settingsDir = global ? resolve2(process.env.HOME || "~", SETTINGS_DIR) : join15(root, SETTINGS_DIR);
|
|
1990
|
+
const settingsFile = global ? "settings.json" : SETTINGS_FILE;
|
|
1991
|
+
const settingsPath = join15(settingsDir, settingsFile);
|
|
1790
1992
|
const status = {
|
|
1791
1993
|
installed: false,
|
|
1792
1994
|
path: settingsPath,
|
|
1793
1995
|
events: []
|
|
1794
1996
|
};
|
|
1795
|
-
if (!
|
|
1997
|
+
if (!existsSync14(settingsPath)) {
|
|
1796
1998
|
console.log(`\u274C hebbian hooks not installed (${settingsPath} not found)`);
|
|
1797
1999
|
return status;
|
|
1798
2000
|
}
|
|
@@ -1828,9 +2030,9 @@ function checkHooks(projectRoot) {
|
|
|
1828
2030
|
return status;
|
|
1829
2031
|
}
|
|
1830
2032
|
function hasBrainRegions(dir) {
|
|
1831
|
-
if (!
|
|
2033
|
+
if (!existsSync14(dir)) return false;
|
|
1832
2034
|
try {
|
|
1833
|
-
const entries =
|
|
2035
|
+
const entries = readdirSync9(dir);
|
|
1834
2036
|
return REGIONS.some((r) => entries.includes(r));
|
|
1835
2037
|
} catch {
|
|
1836
2038
|
return false;
|
|
@@ -1854,8 +2056,8 @@ __export(digest_exports, {
|
|
|
1854
2056
|
extractCorrections: () => extractCorrections,
|
|
1855
2057
|
readHookInput: () => readHookInput
|
|
1856
2058
|
});
|
|
1857
|
-
import { readFileSync as readFileSync7, writeFileSync as writeFileSync12, existsSync as
|
|
1858
|
-
import { join as
|
|
2059
|
+
import { readFileSync as readFileSync7, writeFileSync as writeFileSync12, existsSync as existsSync15, mkdirSync as mkdirSync10 } from "fs";
|
|
2060
|
+
import { join as join16, basename } from "path";
|
|
1859
2061
|
function readHookInput(stdin) {
|
|
1860
2062
|
if (!stdin.trim()) return null;
|
|
1861
2063
|
try {
|
|
@@ -1870,13 +2072,13 @@ function readHookInput(stdin) {
|
|
|
1870
2072
|
}
|
|
1871
2073
|
}
|
|
1872
2074
|
function digestTranscript(brainRoot, transcriptPath, sessionId) {
|
|
1873
|
-
if (!
|
|
2075
|
+
if (!existsSync15(transcriptPath)) {
|
|
1874
2076
|
throw new Error(`Transcript not found: ${transcriptPath}`);
|
|
1875
2077
|
}
|
|
1876
2078
|
const resolvedSessionId = sessionId || basename(transcriptPath, ".jsonl");
|
|
1877
|
-
const logDir =
|
|
1878
|
-
const logPath =
|
|
1879
|
-
if (
|
|
2079
|
+
const logDir = join16(brainRoot, DIGEST_LOG_DIR);
|
|
2080
|
+
const logPath = join16(logDir, `${resolvedSessionId}.jsonl`);
|
|
2081
|
+
if (existsSync15(logPath)) {
|
|
1880
2082
|
console.log(`\u23ED already digested session ${resolvedSessionId}, skip`);
|
|
1881
2083
|
return { corrections: 0, skipped: 0, transcriptPath, sessionId: resolvedSessionId };
|
|
1882
2084
|
}
|
|
@@ -1891,7 +2093,7 @@ function digestTranscript(brainRoot, transcriptPath, sessionId) {
|
|
|
1891
2093
|
const auditEntries = [];
|
|
1892
2094
|
for (const correction of corrections) {
|
|
1893
2095
|
try {
|
|
1894
|
-
|
|
2096
|
+
growCandidate(brainRoot, correction.path);
|
|
1895
2097
|
logEpisode(brainRoot, "digest", correction.path, correction.text);
|
|
1896
2098
|
auditEntries.push({ correction, applied: true });
|
|
1897
2099
|
applied++;
|
|
@@ -2108,11 +2310,11 @@ function extractKeywords(text) {
|
|
|
2108
2310
|
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));
|
|
2109
2311
|
}
|
|
2110
2312
|
function writeAuditLog(brainRoot, sessionId, entries) {
|
|
2111
|
-
const logDir =
|
|
2112
|
-
if (!
|
|
2113
|
-
|
|
2313
|
+
const logDir = join16(brainRoot, DIGEST_LOG_DIR);
|
|
2314
|
+
if (!existsSync15(logDir)) {
|
|
2315
|
+
mkdirSync10(logDir, { recursive: true });
|
|
2114
2316
|
}
|
|
2115
|
-
const logPath =
|
|
2317
|
+
const logPath = join16(logDir, `${sessionId}.jsonl`);
|
|
2116
2318
|
const lines = entries.map(
|
|
2117
2319
|
(e) => JSON.stringify({
|
|
2118
2320
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -2130,7 +2332,7 @@ var init_digest = __esm({
|
|
|
2130
2332
|
"src/digest.ts"() {
|
|
2131
2333
|
"use strict";
|
|
2132
2334
|
init_constants();
|
|
2133
|
-
|
|
2335
|
+
init_candidates();
|
|
2134
2336
|
init_episode();
|
|
2135
2337
|
NEGATION_PATTERNS = [
|
|
2136
2338
|
/\bdon[''\u2019]?t\b/i,
|
|
@@ -2171,11 +2373,649 @@ var init_digest = __esm({
|
|
|
2171
2373
|
}
|
|
2172
2374
|
});
|
|
2173
2375
|
|
|
2376
|
+
// src/outcome.ts
|
|
2377
|
+
var outcome_exports = {};
|
|
2378
|
+
__export(outcome_exports, {
|
|
2379
|
+
buildOutcomeSummary: () => buildOutcomeSummary,
|
|
2380
|
+
captureSessionStart: () => captureSessionStart,
|
|
2381
|
+
classifyOutcome: () => classifyOutcome,
|
|
2382
|
+
detectOutcome: () => detectOutcome
|
|
2383
|
+
});
|
|
2384
|
+
import { execSync as execSync3 } from "child_process";
|
|
2385
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync11, writeFileSync as writeFileSync13, readFileSync as readFileSync8, readdirSync as readdirSync10, rmSync as rmSync2, statSync as statSync5 } from "fs";
|
|
2386
|
+
import { join as join17 } from "path";
|
|
2387
|
+
import { randomUUID } from "crypto";
|
|
2388
|
+
function captureSessionStart(brainRoot) {
|
|
2389
|
+
let sha;
|
|
2390
|
+
try {
|
|
2391
|
+
sha = execSync3("git rev-parse HEAD", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
2392
|
+
} catch {
|
|
2393
|
+
console.log("\u23ED\uFE0F session start: not a git repo, skipping");
|
|
2394
|
+
return null;
|
|
2395
|
+
}
|
|
2396
|
+
let status;
|
|
2397
|
+
try {
|
|
2398
|
+
const raw = execSync3("git status --porcelain", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
2399
|
+
status = raw ? raw.split("\n") : [];
|
|
2400
|
+
} catch {
|
|
2401
|
+
status = [];
|
|
2402
|
+
}
|
|
2403
|
+
const brain = scanBrain(brainRoot);
|
|
2404
|
+
const result = runSubsumption(brain);
|
|
2405
|
+
const neurons = [];
|
|
2406
|
+
for (const region of result.activeRegions) {
|
|
2407
|
+
for (const neuron of region.neurons) {
|
|
2408
|
+
if (!neuron.isDormant && neuron.counter > 0) {
|
|
2409
|
+
neurons.push(`${region.name}/${neuron.path}`);
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
const uuid = randomUUID();
|
|
2414
|
+
const stateDir = join17(brainRoot, SESSION_STATE_DIR);
|
|
2415
|
+
if (!existsSync16(stateDir)) {
|
|
2416
|
+
mkdirSync11(stateDir, { recursive: true });
|
|
2417
|
+
}
|
|
2418
|
+
const state = { ts: (/* @__PURE__ */ new Date()).toISOString(), sha, status, neurons, uuid };
|
|
2419
|
+
writeFileSync13(join17(stateDir, `state_${uuid}.json`), JSON.stringify(state), "utf8");
|
|
2420
|
+
console.log(`\u{1F4F8} session start: SHA ${sha.slice(0, 7)}, ${neurons.length} active neurons`);
|
|
2421
|
+
return state;
|
|
2422
|
+
}
|
|
2423
|
+
function detectOutcome(brainRoot) {
|
|
2424
|
+
const state = readLatestSessionState(brainRoot);
|
|
2425
|
+
if (!state) {
|
|
2426
|
+
console.log("\u23ED\uFE0F session end: no session state found, skipping");
|
|
2427
|
+
return null;
|
|
2428
|
+
}
|
|
2429
|
+
let currentSha;
|
|
2430
|
+
try {
|
|
2431
|
+
currentSha = execSync3("git rev-parse HEAD", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
2432
|
+
} catch {
|
|
2433
|
+
console.log("\u23ED\uFE0F session end: not a git repo, skipping");
|
|
2434
|
+
cleanupSessionState(brainRoot, state.uuid);
|
|
2435
|
+
return null;
|
|
2436
|
+
}
|
|
2437
|
+
let currentStatus;
|
|
2438
|
+
try {
|
|
2439
|
+
const raw = execSync3("git status --porcelain", { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
2440
|
+
currentStatus = raw ? filterHebbianPaths(raw.split("\n")) : [];
|
|
2441
|
+
} catch {
|
|
2442
|
+
currentStatus = [];
|
|
2443
|
+
}
|
|
2444
|
+
const filteredStartStatus = filterHebbianPaths(state.status);
|
|
2445
|
+
const outcome = classifyOutcome(
|
|
2446
|
+
{ ...state, status: filteredStartStatus },
|
|
2447
|
+
currentSha,
|
|
2448
|
+
currentStatus
|
|
2449
|
+
);
|
|
2450
|
+
if (!outcome) {
|
|
2451
|
+
console.log("\u{1F4CA} session end: no changes detected (no-op)");
|
|
2452
|
+
cleanupSessionState(brainRoot, state.uuid);
|
|
2453
|
+
return null;
|
|
2454
|
+
}
|
|
2455
|
+
const neurons = state.neurons;
|
|
2456
|
+
logEpisode(brainRoot, "session-end", "", `outcome:${outcome}`, { outcome, neurons });
|
|
2457
|
+
let result;
|
|
2458
|
+
if (outcome === "revert") {
|
|
2459
|
+
const { affected, skipped } = applyContra(brainRoot, neurons);
|
|
2460
|
+
result = {
|
|
2461
|
+
outcome: "revert",
|
|
2462
|
+
neuronsAffected: affected,
|
|
2463
|
+
protectedSkipped: skipped,
|
|
2464
|
+
detail: `${affected} neurons contra'd (${skipped} protected skipped)`
|
|
2465
|
+
};
|
|
2466
|
+
console.log(`\u{1F4CA} session end: revert \u2014 ${result.detail}`);
|
|
2467
|
+
} else {
|
|
2468
|
+
result = {
|
|
2469
|
+
outcome: "acceptance",
|
|
2470
|
+
neuronsAffected: 0,
|
|
2471
|
+
protectedSkipped: 0,
|
|
2472
|
+
detail: "changes accepted"
|
|
2473
|
+
};
|
|
2474
|
+
console.log("\u{1F4CA} session end: acceptance");
|
|
2475
|
+
}
|
|
2476
|
+
cleanupSessionState(brainRoot, state.uuid);
|
|
2477
|
+
return result;
|
|
2478
|
+
}
|
|
2479
|
+
function classifyOutcome(state, currentSha, currentStatus) {
|
|
2480
|
+
const headMoved = state.sha !== currentSha;
|
|
2481
|
+
const startStatusSet = new Set(state.status);
|
|
2482
|
+
const endStatusSet = new Set(currentStatus);
|
|
2483
|
+
const newItems = currentStatus.filter((s) => !startStatusSet.has(s));
|
|
2484
|
+
const removedItems = state.status.filter((s) => !endStatusSet.has(s));
|
|
2485
|
+
if (!headMoved) {
|
|
2486
|
+
if (newItems.length === 0 && removedItems.length === 0) {
|
|
2487
|
+
return null;
|
|
2488
|
+
}
|
|
2489
|
+
if (newItems.length > 0) {
|
|
2490
|
+
return "acceptance";
|
|
2491
|
+
}
|
|
2492
|
+
if (removedItems.length > 0) {
|
|
2493
|
+
return "revert";
|
|
2494
|
+
}
|
|
2495
|
+
return null;
|
|
2496
|
+
}
|
|
2497
|
+
if (newItems.length > 0) {
|
|
2498
|
+
return "acceptance";
|
|
2499
|
+
}
|
|
2500
|
+
try {
|
|
2501
|
+
const diffStat = execSync3(
|
|
2502
|
+
`git diff ${state.sha}..${currentSha} --stat`,
|
|
2503
|
+
{ encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }
|
|
2504
|
+
).trim();
|
|
2505
|
+
const logOutput = execSync3(
|
|
2506
|
+
`git log --oneline ${state.sha}..${currentSha}`,
|
|
2507
|
+
{ encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }
|
|
2508
|
+
).trim();
|
|
2509
|
+
if (/\brevert\b/i.test(logOutput)) {
|
|
2510
|
+
return "revert";
|
|
2511
|
+
}
|
|
2512
|
+
if (!diffStat) {
|
|
2513
|
+
return "revert";
|
|
2514
|
+
}
|
|
2515
|
+
return "acceptance";
|
|
2516
|
+
} catch {
|
|
2517
|
+
return null;
|
|
2518
|
+
}
|
|
2519
|
+
}
|
|
2520
|
+
function applyContra(brainRoot, neurons) {
|
|
2521
|
+
let affected = 0;
|
|
2522
|
+
let skipped = 0;
|
|
2523
|
+
for (const neuronPath of neurons) {
|
|
2524
|
+
const region = neuronPath.split("/")[0] || "";
|
|
2525
|
+
if (PROTECTED_REGIONS_CONTRA.includes(region)) {
|
|
2526
|
+
skipped++;
|
|
2527
|
+
continue;
|
|
2528
|
+
}
|
|
2529
|
+
const result = contraNeuron(brainRoot, neuronPath);
|
|
2530
|
+
if (result > 0) {
|
|
2531
|
+
affected++;
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
return { affected, skipped };
|
|
2535
|
+
}
|
|
2536
|
+
function buildOutcomeSummary(brainRoot) {
|
|
2537
|
+
const episodes = readEpisodes(brainRoot);
|
|
2538
|
+
const outcomeEpisodes = episodes.filter((e) => e.outcome && e.neurons);
|
|
2539
|
+
if (outcomeEpisodes.length === 0) return "";
|
|
2540
|
+
const stats = /* @__PURE__ */ new Map();
|
|
2541
|
+
for (const ep of outcomeEpisodes) {
|
|
2542
|
+
for (const neuron of ep.neurons) {
|
|
2543
|
+
const existing = stats.get(neuron) || { sessions: 0, reverts: 0, acceptances: 0 };
|
|
2544
|
+
existing.sessions++;
|
|
2545
|
+
if (ep.outcome === "revert") existing.reverts++;
|
|
2546
|
+
if (ep.outcome === "acceptance") existing.acceptances++;
|
|
2547
|
+
stats.set(neuron, existing);
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
const lines = ["## Outcome Signals (from session history)\n"];
|
|
2551
|
+
lines.push("Neurons with high contra_ratio (>0.5) are consistently present in reverted sessions. Consider pruning or modifying them.\n");
|
|
2552
|
+
const sorted = [...stats.entries()].sort((a, b) => {
|
|
2553
|
+
const ratioA = a[1].sessions > 0 ? a[1].reverts / a[1].sessions : 0;
|
|
2554
|
+
const ratioB = b[1].sessions > 0 ? b[1].reverts / b[1].sessions : 0;
|
|
2555
|
+
return ratioB - ratioA;
|
|
2556
|
+
});
|
|
2557
|
+
for (const [neuron, s] of sorted) {
|
|
2558
|
+
const ratio = s.sessions > 0 ? (s.reverts / s.sessions).toFixed(2) : "0.00";
|
|
2559
|
+
const trend = parseFloat(ratio) > 0.5 ? "\u2190 act on this" : parseFloat(ratio) > 0.3 ? "\u2190 watch" : "";
|
|
2560
|
+
lines.push(`- ${neuron}: sessions=${s.sessions} reverts=${s.reverts} acceptances=${s.acceptances} contra_ratio=${ratio} ${trend}`);
|
|
2561
|
+
}
|
|
2562
|
+
lines.push("");
|
|
2563
|
+
return lines.join("\n");
|
|
2564
|
+
}
|
|
2565
|
+
function readLatestSessionState(brainRoot) {
|
|
2566
|
+
const stateDir = join17(brainRoot, SESSION_STATE_DIR);
|
|
2567
|
+
if (!existsSync16(stateDir)) return null;
|
|
2568
|
+
let latest = null;
|
|
2569
|
+
try {
|
|
2570
|
+
for (const entry of readdirSync10(stateDir)) {
|
|
2571
|
+
if (!entry.startsWith("state_") || !entry.endsWith(".json")) continue;
|
|
2572
|
+
const fullPath = join17(stateDir, entry);
|
|
2573
|
+
const mtime = statSync5(fullPath).mtimeMs;
|
|
2574
|
+
if (!latest || mtime > latest.mtime) {
|
|
2575
|
+
latest = { path: fullPath, mtime };
|
|
2576
|
+
}
|
|
2577
|
+
}
|
|
2578
|
+
} catch {
|
|
2579
|
+
return null;
|
|
2580
|
+
}
|
|
2581
|
+
if (!latest) return null;
|
|
2582
|
+
try {
|
|
2583
|
+
return JSON.parse(readFileSync8(latest.path, "utf8"));
|
|
2584
|
+
} catch {
|
|
2585
|
+
return null;
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
function filterHebbianPaths(statusLines) {
|
|
2589
|
+
const hebbianPatterns = ["hippocampus/session_state", "hippocampus/session_log", "hippocampus/digest_log", "_inbox/"];
|
|
2590
|
+
return statusLines.filter(
|
|
2591
|
+
(line) => !hebbianPatterns.some((p) => line.includes(p))
|
|
2592
|
+
);
|
|
2593
|
+
}
|
|
2594
|
+
function cleanupSessionState(brainRoot, uuid) {
|
|
2595
|
+
const stateDir = join17(brainRoot, SESSION_STATE_DIR);
|
|
2596
|
+
const filePath = join17(stateDir, `state_${uuid}.json`);
|
|
2597
|
+
try {
|
|
2598
|
+
if (existsSync16(filePath)) rmSync2(filePath);
|
|
2599
|
+
} catch {
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2602
|
+
var init_outcome = __esm({
|
|
2603
|
+
"src/outcome.ts"() {
|
|
2604
|
+
"use strict";
|
|
2605
|
+
init_constants();
|
|
2606
|
+
init_scanner();
|
|
2607
|
+
init_subsumption();
|
|
2608
|
+
init_fire();
|
|
2609
|
+
init_episode();
|
|
2610
|
+
}
|
|
2611
|
+
});
|
|
2612
|
+
|
|
2613
|
+
// src/evolve.ts
|
|
2614
|
+
var evolve_exports = {};
|
|
2615
|
+
__export(evolve_exports, {
|
|
2616
|
+
buildBrainSummary: () => buildBrainSummary,
|
|
2617
|
+
buildPrompt: () => buildPrompt,
|
|
2618
|
+
callGemini: () => callGemini,
|
|
2619
|
+
executeActions: () => executeActions,
|
|
2620
|
+
parseActions: () => parseActions,
|
|
2621
|
+
runEvolve: () => runEvolve,
|
|
2622
|
+
validateActions: () => validateActions
|
|
2623
|
+
});
|
|
2624
|
+
async function runEvolve(brainRoot, dryRun) {
|
|
2625
|
+
const apiKey = process.env.GEMINI_API_KEY;
|
|
2626
|
+
if (!apiKey) {
|
|
2627
|
+
console.error("\u274C GEMINI_API_KEY not set. Get one at https://aistudio.google.com/apikey");
|
|
2628
|
+
return { actions: [], executed: 0, skipped: 0, dryRun };
|
|
2629
|
+
}
|
|
2630
|
+
const episodes = readEpisodes(brainRoot);
|
|
2631
|
+
const brain = scanBrain(brainRoot);
|
|
2632
|
+
const summary = buildBrainSummary(brain);
|
|
2633
|
+
const outcomeSummary = buildOutcomeSummary(brainRoot);
|
|
2634
|
+
const prompt = buildPrompt(summary, episodes, outcomeSummary);
|
|
2635
|
+
let rawActions;
|
|
2636
|
+
try {
|
|
2637
|
+
rawActions = await callGemini(prompt, apiKey);
|
|
2638
|
+
} catch (err) {
|
|
2639
|
+
const msg = err.message;
|
|
2640
|
+
console.log(`\u23ED\uFE0F evolve skipped: ${msg}`);
|
|
2641
|
+
logEpisode(brainRoot, "evolve-error", "", msg);
|
|
2642
|
+
return { actions: [], executed: 0, skipped: 0, dryRun };
|
|
2643
|
+
}
|
|
2644
|
+
const actions = validateActions(rawActions, brain);
|
|
2645
|
+
const skipped = rawActions.length - actions.length;
|
|
2646
|
+
if (actions.length === 0) {
|
|
2647
|
+
console.log("\u{1F9E0} evolve: no valid actions proposed");
|
|
2648
|
+
return { actions: [], executed: 0, skipped, dryRun };
|
|
2649
|
+
}
|
|
2650
|
+
if (dryRun) {
|
|
2651
|
+
console.log(`\u{1F9E0} evolve (dry-run): ${actions.length} action(s) proposed`);
|
|
2652
|
+
for (const action of actions) {
|
|
2653
|
+
console.log(` ${actionIcon(action.type)} ${action.type} ${action.path} \u2014 ${action.reason}`);
|
|
2654
|
+
}
|
|
2655
|
+
return { actions, executed: 0, skipped, dryRun: true };
|
|
2656
|
+
}
|
|
2657
|
+
const executed = executeActions(brainRoot, actions);
|
|
2658
|
+
logEpisode(brainRoot, "evolve", "", `${executed} action(s) executed, ${skipped} skipped`);
|
|
2659
|
+
console.log(`\u{1F9E0} evolve: ${executed} action(s) executed, ${skipped} skipped`);
|
|
2660
|
+
return { actions, executed, skipped, dryRun: false };
|
|
2661
|
+
}
|
|
2662
|
+
function buildBrainSummary(brain) {
|
|
2663
|
+
const lines = ["# Brain State\n"];
|
|
2664
|
+
for (const region of brain.regions) {
|
|
2665
|
+
const neurons = region.neurons;
|
|
2666
|
+
if (neurons.length === 0 && !region.hasBomb) continue;
|
|
2667
|
+
lines.push(`## ${region.name} (P${REGION_PRIORITY[region.name]})`);
|
|
2668
|
+
if (region.hasBomb) lines.push("\u26A0\uFE0F BOMB active \u2014 region blocked");
|
|
2669
|
+
for (const neuron of neurons) {
|
|
2670
|
+
const flags = [];
|
|
2671
|
+
if (neuron.isDormant) flags.push("dormant");
|
|
2672
|
+
if (neuron.hasBomb) flags.push("bomb");
|
|
2673
|
+
if (neuron.hasMemory) flags.push("memory");
|
|
2674
|
+
if (neuron.dopamine > 0) flags.push(`dopamine:${neuron.dopamine}`);
|
|
2675
|
+
const flagStr = flags.length > 0 ? ` [${flags.join(", ")}]` : "";
|
|
2676
|
+
lines.push(`- ${neuron.path} (counter:${neuron.counter}, intensity:${neuron.intensity})${flagStr}`);
|
|
2677
|
+
}
|
|
2678
|
+
lines.push("");
|
|
2679
|
+
}
|
|
2680
|
+
return lines.join("\n");
|
|
2681
|
+
}
|
|
2682
|
+
function buildPrompt(summary, episodes, outcomeSummary) {
|
|
2683
|
+
const episodeLines = episodes.length > 0 ? episodes.map((e) => `- [${e.ts}] ${e.type}: ${e.path} \u2014 ${e.detail}`).join("\n") : "(no recent episodes)";
|
|
2684
|
+
const outcomeSection = outcomeSummary || "";
|
|
2685
|
+
return `You are the evolve engine for a hebbian brain \u2014 a filesystem-based memory system for AI agents.
|
|
2686
|
+
|
|
2687
|
+
## Axioms
|
|
2688
|
+
- Folder = Neuron, File = Firing Trace, Counter = Activation strength
|
|
2689
|
+
- 7 regions in subsumption cascade: brainstem(P0) > limbic(P1) > hippocampus(P2) > sensors(P3) > cortex(P4) > ego(P5) > prefrontal(P6)
|
|
2690
|
+
- Lower priority ALWAYS overrides higher priority
|
|
2691
|
+
- PROTECTED regions (brainstem, limbic, sensors): NEVER propose mutations for these
|
|
2692
|
+
|
|
2693
|
+
## Current Brain
|
|
2694
|
+
${summary}
|
|
2695
|
+
|
|
2696
|
+
${outcomeSection}
|
|
2697
|
+
## Recent Episodes (last ${episodes.length})
|
|
2698
|
+
${episodeLines}
|
|
2699
|
+
|
|
2700
|
+
## Available Actions
|
|
2701
|
+
- grow: Create a new neuron at the given path (region/name). Use for recurring patterns that deserve permanent memory.
|
|
2702
|
+
- fire: Increment an existing neuron's counter. Use for strengthening well-confirmed rules.
|
|
2703
|
+
- signal: Add dopamine (reward), bomb (block), or memory signal. Use sparingly.
|
|
2704
|
+
- prune: Decrement a neuron's counter. Use for rules that aren't working or cause issues.
|
|
2705
|
+
- decay: Mark inactive neurons as dormant. Use for stale rules with no recent activity.
|
|
2706
|
+
|
|
2707
|
+
## Constraints
|
|
2708
|
+
- Max ${MAX_ACTIONS} actions per cycle
|
|
2709
|
+
- PREFER fire over grow \u2014 strengthen existing neurons before creating new ones
|
|
2710
|
+
- NEVER target brainstem, limbic, or sensors regions
|
|
2711
|
+
- Each action needs a "reason" explaining why
|
|
2712
|
+
|
|
2713
|
+
## Task
|
|
2714
|
+
Analyze the brain state and recent episodes. Propose actions to improve the brain.
|
|
2715
|
+
Focus on: strengthening repeatedly-used rules, pruning ineffective ones, growing new neurons from repeated patterns.
|
|
2716
|
+
|
|
2717
|
+
Respond with a JSON array of actions:
|
|
2718
|
+
[{"type":"fire","path":"cortex/NO_console_log","reason":"fired 3 times in recent sessions"}]`;
|
|
2719
|
+
}
|
|
2720
|
+
async function callGemini(prompt, apiKey) {
|
|
2721
|
+
const model = process.env.EVOLVE_MODEL || DEFAULT_MODEL;
|
|
2722
|
+
const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`;
|
|
2723
|
+
const body = {
|
|
2724
|
+
contents: [{ parts: [{ text: prompt }] }],
|
|
2725
|
+
generationConfig: {
|
|
2726
|
+
responseMimeType: "application/json",
|
|
2727
|
+
temperature: 0.2
|
|
2728
|
+
}
|
|
2729
|
+
};
|
|
2730
|
+
let lastError = null;
|
|
2731
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
2732
|
+
if (attempt > 0) {
|
|
2733
|
+
await new Promise((r) => setTimeout(r, RETRY_DELAY));
|
|
2734
|
+
}
|
|
2735
|
+
try {
|
|
2736
|
+
const res = await fetch(url, {
|
|
2737
|
+
method: "POST",
|
|
2738
|
+
headers: { "Content-Type": "application/json" },
|
|
2739
|
+
body: JSON.stringify(body),
|
|
2740
|
+
signal: AbortSignal.timeout(API_TIMEOUT)
|
|
2741
|
+
});
|
|
2742
|
+
if (!res.ok) {
|
|
2743
|
+
lastError = new Error(`Gemini API ${res.status}: ${res.statusText}`);
|
|
2744
|
+
continue;
|
|
2745
|
+
}
|
|
2746
|
+
const data = await res.json();
|
|
2747
|
+
if (data.error) {
|
|
2748
|
+
lastError = new Error(`Gemini error: ${data.error.message || "unknown"}`);
|
|
2749
|
+
continue;
|
|
2750
|
+
}
|
|
2751
|
+
const text = data.candidates?.[0]?.content?.parts?.[0]?.text;
|
|
2752
|
+
if (!text) {
|
|
2753
|
+
lastError = new Error("Gemini returned empty response");
|
|
2754
|
+
continue;
|
|
2755
|
+
}
|
|
2756
|
+
return parseActions(text);
|
|
2757
|
+
} catch (err) {
|
|
2758
|
+
lastError = err;
|
|
2759
|
+
continue;
|
|
2760
|
+
}
|
|
2761
|
+
}
|
|
2762
|
+
throw lastError || new Error("Gemini call failed");
|
|
2763
|
+
}
|
|
2764
|
+
function parseActions(text) {
|
|
2765
|
+
let parsed;
|
|
2766
|
+
try {
|
|
2767
|
+
parsed = JSON.parse(text);
|
|
2768
|
+
} catch {
|
|
2769
|
+
throw new Error(`Failed to parse LLM response as JSON: ${text.slice(0, 100)}`);
|
|
2770
|
+
}
|
|
2771
|
+
if (!Array.isArray(parsed)) {
|
|
2772
|
+
throw new Error("LLM response is not an array");
|
|
2773
|
+
}
|
|
2774
|
+
const validTypes = /* @__PURE__ */ new Set(["grow", "fire", "signal", "prune", "decay"]);
|
|
2775
|
+
const actions = [];
|
|
2776
|
+
for (const item of parsed) {
|
|
2777
|
+
if (!item || typeof item !== "object") continue;
|
|
2778
|
+
const { type, path, reason, signal } = item;
|
|
2779
|
+
if (typeof type !== "string" || !validTypes.has(type)) continue;
|
|
2780
|
+
if (typeof path !== "string" || path.length === 0) continue;
|
|
2781
|
+
if (typeof reason !== "string") continue;
|
|
2782
|
+
const action = { type, path, reason };
|
|
2783
|
+
if (type === "signal" && typeof signal === "string") {
|
|
2784
|
+
action.signal = signal;
|
|
2785
|
+
}
|
|
2786
|
+
actions.push(action);
|
|
2787
|
+
}
|
|
2788
|
+
return actions;
|
|
2789
|
+
}
|
|
2790
|
+
function validateActions(actions, _brain) {
|
|
2791
|
+
return actions.filter((action) => {
|
|
2792
|
+
const region = action.path.split("/")[0];
|
|
2793
|
+
if (!region || PROTECTED_REGIONS.includes(region)) {
|
|
2794
|
+
console.log(` \u{1F6E1}\uFE0F blocked: ${action.type} ${action.path} (protected region)`);
|
|
2795
|
+
return false;
|
|
2796
|
+
}
|
|
2797
|
+
if (!REGIONS.includes(region)) {
|
|
2798
|
+
console.log(` \u26A0\uFE0F skipped: ${action.type} ${action.path} (invalid region)`);
|
|
2799
|
+
return false;
|
|
2800
|
+
}
|
|
2801
|
+
if (action.type === "signal" && action.signal && !["dopamine", "bomb", "memory"].includes(action.signal)) {
|
|
2802
|
+
console.log(` \u26A0\uFE0F skipped: signal ${action.path} (invalid signal type: ${action.signal})`);
|
|
2803
|
+
return false;
|
|
2804
|
+
}
|
|
2805
|
+
return true;
|
|
2806
|
+
}).slice(0, MAX_ACTIONS);
|
|
2807
|
+
}
|
|
2808
|
+
function executeActions(brainRoot, actions) {
|
|
2809
|
+
let executed = 0;
|
|
2810
|
+
for (const action of actions) {
|
|
2811
|
+
try {
|
|
2812
|
+
switch (action.type) {
|
|
2813
|
+
case "fire":
|
|
2814
|
+
fireNeuron(brainRoot, action.path);
|
|
2815
|
+
break;
|
|
2816
|
+
case "grow":
|
|
2817
|
+
growCandidate(brainRoot, action.path);
|
|
2818
|
+
break;
|
|
2819
|
+
case "signal":
|
|
2820
|
+
signalNeuron(brainRoot, action.path, action.signal || "dopamine");
|
|
2821
|
+
break;
|
|
2822
|
+
case "prune":
|
|
2823
|
+
rollbackNeuron(brainRoot, action.path);
|
|
2824
|
+
break;
|
|
2825
|
+
case "decay":
|
|
2826
|
+
runDecay(brainRoot, 0);
|
|
2827
|
+
break;
|
|
2828
|
+
}
|
|
2829
|
+
console.log(` ${actionIcon(action.type)} ${action.type} ${action.path}`);
|
|
2830
|
+
executed++;
|
|
2831
|
+
} catch (err) {
|
|
2832
|
+
console.log(` \u26A0\uFE0F failed: ${action.type} ${action.path} \u2014 ${err.message}`);
|
|
2833
|
+
}
|
|
2834
|
+
}
|
|
2835
|
+
return executed;
|
|
2836
|
+
}
|
|
2837
|
+
function actionIcon(type) {
|
|
2838
|
+
switch (type) {
|
|
2839
|
+
case "fire":
|
|
2840
|
+
return "\u{1F525}";
|
|
2841
|
+
case "grow":
|
|
2842
|
+
return "\u{1F331}";
|
|
2843
|
+
case "signal":
|
|
2844
|
+
return "\u26A1";
|
|
2845
|
+
case "prune":
|
|
2846
|
+
return "\u2702\uFE0F";
|
|
2847
|
+
case "decay":
|
|
2848
|
+
return "\u{1F4A4}";
|
|
2849
|
+
default:
|
|
2850
|
+
return "\u2753";
|
|
2851
|
+
}
|
|
2852
|
+
}
|
|
2853
|
+
var MAX_ACTIONS, PROTECTED_REGIONS, DEFAULT_MODEL, API_TIMEOUT, RETRY_DELAY;
|
|
2854
|
+
var init_evolve = __esm({
|
|
2855
|
+
"src/evolve.ts"() {
|
|
2856
|
+
"use strict";
|
|
2857
|
+
init_episode();
|
|
2858
|
+
init_scanner();
|
|
2859
|
+
init_constants();
|
|
2860
|
+
init_fire();
|
|
2861
|
+
init_candidates();
|
|
2862
|
+
init_signal();
|
|
2863
|
+
init_rollback();
|
|
2864
|
+
init_decay();
|
|
2865
|
+
init_outcome();
|
|
2866
|
+
MAX_ACTIONS = 10;
|
|
2867
|
+
PROTECTED_REGIONS = ["brainstem", "limbic", "sensors"];
|
|
2868
|
+
DEFAULT_MODEL = "gemini-2.0-flash-lite";
|
|
2869
|
+
API_TIMEOUT = 3e4;
|
|
2870
|
+
RETRY_DELAY = 5e3;
|
|
2871
|
+
}
|
|
2872
|
+
});
|
|
2873
|
+
|
|
2874
|
+
// src/doctor.ts
|
|
2875
|
+
var doctor_exports = {};
|
|
2876
|
+
__export(doctor_exports, {
|
|
2877
|
+
runDoctor: () => runDoctor
|
|
2878
|
+
});
|
|
2879
|
+
import { existsSync as existsSync17, readFileSync as readFileSync9, readdirSync as readdirSync11 } from "fs";
|
|
2880
|
+
import { join as join18 } from "path";
|
|
2881
|
+
import { execSync as execSync4 } from "child_process";
|
|
2882
|
+
async function runDoctor(brainRoot) {
|
|
2883
|
+
let passed = 0, warnings = 0, failed = 0;
|
|
2884
|
+
const ok = (msg) => {
|
|
2885
|
+
console.log(` \u2705 ${msg}`);
|
|
2886
|
+
passed++;
|
|
2887
|
+
};
|
|
2888
|
+
const warn = (msg, fix) => {
|
|
2889
|
+
console.log(` \u26A0\uFE0F ${msg}`);
|
|
2890
|
+
if (fix) console.log(` \u2192 ${fix}`);
|
|
2891
|
+
warnings++;
|
|
2892
|
+
};
|
|
2893
|
+
const fail = (msg, fix) => {
|
|
2894
|
+
console.log(` \u274C ${msg}`);
|
|
2895
|
+
if (fix) console.log(` \u2192 ${fix}`);
|
|
2896
|
+
failed++;
|
|
2897
|
+
};
|
|
2898
|
+
console.log("\n\u{1FA7A} hebbian doctor\n");
|
|
2899
|
+
console.log("Node.js");
|
|
2900
|
+
const nodeVer = process.versions.node;
|
|
2901
|
+
const [major] = nodeVer.split(".").map(Number);
|
|
2902
|
+
if ((major ?? 0) >= 22) {
|
|
2903
|
+
ok(`Node.js ${nodeVer} (>= 22 required)`);
|
|
2904
|
+
} else {
|
|
2905
|
+
fail(`Node.js ${nodeVer} \u2014 need >= 22`, "nvm install 22 && nvm use 22");
|
|
2906
|
+
}
|
|
2907
|
+
console.log("\nnpm package");
|
|
2908
|
+
try {
|
|
2909
|
+
const pkgPath = new URL("../package.json", import.meta.url).pathname;
|
|
2910
|
+
const pkg = JSON.parse(readFileSync9(pkgPath, "utf8"));
|
|
2911
|
+
const local = pkg.version || "unknown";
|
|
2912
|
+
let remote = "";
|
|
2913
|
+
try {
|
|
2914
|
+
const out = execSync4("npm view hebbian version 2>/dev/null", { timeout: 5e3 }).toString().trim();
|
|
2915
|
+
remote = out;
|
|
2916
|
+
} catch {
|
|
2917
|
+
}
|
|
2918
|
+
if (remote && remote !== local) {
|
|
2919
|
+
warn(`hebbian ${local} installed, ${remote} available`, "npm i -g hebbian@latest");
|
|
2920
|
+
} else {
|
|
2921
|
+
ok(`hebbian ${local}${remote ? " (up to date)" : ""}`);
|
|
2922
|
+
}
|
|
2923
|
+
} catch {
|
|
2924
|
+
warn("Could not read package.json");
|
|
2925
|
+
}
|
|
2926
|
+
console.log("\nbrain structure");
|
|
2927
|
+
if (!existsSync17(brainRoot)) {
|
|
2928
|
+
fail(`Brain not found at ${brainRoot}`, "hebbian init ./brain");
|
|
2929
|
+
} else {
|
|
2930
|
+
ok(`Brain root: ${brainRoot}`);
|
|
2931
|
+
for (const region of REGIONS) {
|
|
2932
|
+
const regionDir = join18(brainRoot, region);
|
|
2933
|
+
if (existsSync17(regionDir)) {
|
|
2934
|
+
ok(`Region: ${region}`);
|
|
2935
|
+
} else {
|
|
2936
|
+
warn(`Missing region: ${region}`, `mkdir -p ${regionDir}`);
|
|
2937
|
+
}
|
|
2938
|
+
}
|
|
2939
|
+
}
|
|
2940
|
+
console.log("\nClaude Code hooks");
|
|
2941
|
+
const settingsPath = join18(process.cwd(), ".claude", "settings.local.json");
|
|
2942
|
+
if (!existsSync17(settingsPath)) {
|
|
2943
|
+
warn("No .claude/settings.local.json found", "hebbian claude install");
|
|
2944
|
+
} else {
|
|
2945
|
+
try {
|
|
2946
|
+
const settings = JSON.parse(readFileSync9(settingsPath, "utf8"));
|
|
2947
|
+
const hooks = settings.hooks || {};
|
|
2948
|
+
const hasStop = Object.entries(hooks).some(
|
|
2949
|
+
([event, entries]) => event === "Stop" && Array.isArray(entries) && entries.some(
|
|
2950
|
+
(e) => typeof e === "object" && e !== null && "command" in e && typeof e.command === "string" && e.command.includes("hebbian digest")
|
|
2951
|
+
)
|
|
2952
|
+
);
|
|
2953
|
+
const hasStart = Object.entries(hooks).some(
|
|
2954
|
+
([event, entries]) => event === "SessionStart" && Array.isArray(entries) && entries.some(
|
|
2955
|
+
(e) => typeof e === "object" && e !== null && "command" in e && typeof e.command === "string" && e.command.includes("hebbian emit")
|
|
2956
|
+
)
|
|
2957
|
+
);
|
|
2958
|
+
if (hasStop && hasStart) {
|
|
2959
|
+
ok("SessionStart + Stop hooks installed");
|
|
2960
|
+
} else {
|
|
2961
|
+
if (!hasStart) warn("SessionStart hook missing", "hebbian claude install");
|
|
2962
|
+
if (!hasStop) warn("Stop hook missing", "hebbian claude install");
|
|
2963
|
+
}
|
|
2964
|
+
} catch {
|
|
2965
|
+
fail("Malformed .claude/settings.local.json", "hebbian claude install");
|
|
2966
|
+
}
|
|
2967
|
+
}
|
|
2968
|
+
console.log("\nnpx resolution");
|
|
2969
|
+
try {
|
|
2970
|
+
const resolved = execSync4("which npx", { timeout: 3e3 }).toString().trim();
|
|
2971
|
+
ok(`npx: ${resolved}`);
|
|
2972
|
+
} catch {
|
|
2973
|
+
fail("npx not found in PATH", "Install Node.js from https://nodejs.org");
|
|
2974
|
+
}
|
|
2975
|
+
console.log("\ncandidates");
|
|
2976
|
+
try {
|
|
2977
|
+
let total = 0;
|
|
2978
|
+
for (const region of REGIONS) {
|
|
2979
|
+
const candidateDir = join18(brainRoot, region, "_candidates");
|
|
2980
|
+
if (existsSync17(candidateDir)) {
|
|
2981
|
+
const entries = readdirSync11(candidateDir, { withFileTypes: true });
|
|
2982
|
+
const count = entries.filter((e) => e.isDirectory()).length;
|
|
2983
|
+
total += count;
|
|
2984
|
+
}
|
|
2985
|
+
}
|
|
2986
|
+
if (total === 0) {
|
|
2987
|
+
ok("No pending candidates");
|
|
2988
|
+
} else {
|
|
2989
|
+
warn(`${total} candidate(s) pending`, "hebbian candidates \u2014 to view");
|
|
2990
|
+
}
|
|
2991
|
+
} catch {
|
|
2992
|
+
warn("Could not scan candidates");
|
|
2993
|
+
}
|
|
2994
|
+
console.log(`
|
|
2995
|
+
${"\u2500".repeat(40)}`);
|
|
2996
|
+
console.log(` passed: ${passed} warnings: ${warnings} failed: ${failed}`);
|
|
2997
|
+
if (failed > 0) {
|
|
2998
|
+
console.log(" Fix the \u274C issues above, then re-run `hebbian doctor`");
|
|
2999
|
+
} else if (warnings > 0) {
|
|
3000
|
+
console.log(" Looking mostly good! Review \u26A0\uFE0F warnings above.");
|
|
3001
|
+
} else {
|
|
3002
|
+
console.log(" All checks passed. \u{1F389}");
|
|
3003
|
+
}
|
|
3004
|
+
console.log("");
|
|
3005
|
+
return { passed, warnings, failed };
|
|
3006
|
+
}
|
|
3007
|
+
var init_doctor = __esm({
|
|
3008
|
+
"src/doctor.ts"() {
|
|
3009
|
+
"use strict";
|
|
3010
|
+
init_constants();
|
|
3011
|
+
}
|
|
3012
|
+
});
|
|
3013
|
+
|
|
2174
3014
|
// src/cli.ts
|
|
2175
3015
|
init_constants();
|
|
2176
3016
|
import { parseArgs } from "util";
|
|
2177
3017
|
import { resolve as resolve3 } from "path";
|
|
2178
|
-
var VERSION = "0.
|
|
3018
|
+
var VERSION = "0.5.0";
|
|
2179
3019
|
var HELP = `
|
|
2180
3020
|
hebbian v${VERSION} \u2014 Folder-as-neuron brain for any AI agent.
|
|
2181
3021
|
|
|
@@ -2199,6 +3039,11 @@ COMMANDS:
|
|
|
2199
3039
|
inbox Process corrections inbox
|
|
2200
3040
|
claude install|uninstall|status Manage Claude Code hooks
|
|
2201
3041
|
digest [--transcript <path>] Extract corrections from conversation
|
|
3042
|
+
candidates [promote] List candidates or promote graduated ones
|
|
3043
|
+
evolve [--dry-run] LLM-powered brain evolution (Gemini)
|
|
3044
|
+
session start|end Capture/detect session outcomes
|
|
3045
|
+
sessions Show session outcome history
|
|
3046
|
+
doctor Self-diagnostic (hooks, brain, versions)
|
|
2202
3047
|
diag Print brain diagnostics
|
|
2203
3048
|
stats Print brain statistics
|
|
2204
3049
|
|
|
@@ -2213,6 +3058,7 @@ EXAMPLES:
|
|
|
2213
3058
|
hebbian fire cortex/frontend/NO_console_log --brain ./my-brain
|
|
2214
3059
|
hebbian emit claude --brain ./my-brain
|
|
2215
3060
|
hebbian emit all
|
|
3061
|
+
GEMINI_API_KEY=... hebbian evolve --dry-run
|
|
2216
3062
|
`.trim();
|
|
2217
3063
|
function readStdin() {
|
|
2218
3064
|
return new Promise((resolve4) => {
|
|
@@ -2238,6 +3084,8 @@ async function main(argv) {
|
|
|
2238
3084
|
days: { type: "string", short: "d" },
|
|
2239
3085
|
port: { type: "string", short: "p" },
|
|
2240
3086
|
transcript: { type: "string", short: "t" },
|
|
3087
|
+
"dry-run": { type: "boolean" },
|
|
3088
|
+
global: { type: "boolean", short: "g" },
|
|
2241
3089
|
help: { type: "boolean", short: "h" },
|
|
2242
3090
|
version: { type: "boolean", short: "v" }
|
|
2243
3091
|
},
|
|
@@ -2358,18 +3206,23 @@ async function main(argv) {
|
|
|
2358
3206
|
}
|
|
2359
3207
|
case "claude": {
|
|
2360
3208
|
const sub = positionals[1];
|
|
3209
|
+
const isGlobal = values.global === true;
|
|
2361
3210
|
const { installHooks: installHooks2, uninstallHooks: uninstallHooks2, checkHooks: checkHooks2 } = await Promise.resolve().then(() => (init_hooks(), hooks_exports));
|
|
2362
3211
|
switch (sub) {
|
|
2363
3212
|
case "install": {
|
|
2364
|
-
const installBrain = values.brain ? resolve3(values.brain) : resolve3("./brain");
|
|
2365
|
-
|
|
3213
|
+
const installBrain = values.brain ? resolve3(values.brain) : isGlobal ? process.env.HEBBIAN_BRAIN ? resolve3(process.env.HEBBIAN_BRAIN) : "" : resolve3("./brain");
|
|
3214
|
+
if (isGlobal && !installBrain) {
|
|
3215
|
+
console.error("\u274C --global requires --brain <path> or HEBBIAN_BRAIN env var");
|
|
3216
|
+
process.exit(1);
|
|
3217
|
+
}
|
|
3218
|
+
installHooks2(installBrain, void 0, isGlobal);
|
|
2366
3219
|
break;
|
|
2367
3220
|
}
|
|
2368
3221
|
case "uninstall":
|
|
2369
|
-
uninstallHooks2();
|
|
3222
|
+
uninstallHooks2(void 0, isGlobal);
|
|
2370
3223
|
break;
|
|
2371
3224
|
case "status": {
|
|
2372
|
-
checkHooks2();
|
|
3225
|
+
checkHooks2(void 0, isGlobal);
|
|
2373
3226
|
console.log(` version: v${VERSION}`);
|
|
2374
3227
|
const { checkForUpdates: checkUpdates, formatUpdateBanner: formatBanner } = await Promise.resolve().then(() => (init_update_check(), update_check_exports));
|
|
2375
3228
|
const updateStatus = await checkUpdates(VERSION);
|
|
@@ -2378,7 +3231,7 @@ async function main(argv) {
|
|
|
2378
3231
|
break;
|
|
2379
3232
|
}
|
|
2380
3233
|
default:
|
|
2381
|
-
console.error("Usage: hebbian claude <install|uninstall|status>");
|
|
3234
|
+
console.error("Usage: hebbian claude <install|uninstall|status> [--global]");
|
|
2382
3235
|
process.exit(1);
|
|
2383
3236
|
}
|
|
2384
3237
|
break;
|
|
@@ -2401,6 +3254,70 @@ async function main(argv) {
|
|
|
2401
3254
|
}
|
|
2402
3255
|
break;
|
|
2403
3256
|
}
|
|
3257
|
+
case "candidates": {
|
|
3258
|
+
const subCmd = positionals[1];
|
|
3259
|
+
const { listCandidates: listCandidates2, promoteCandidates: promoteCandidates2 } = await Promise.resolve().then(() => (init_candidates(), candidates_exports));
|
|
3260
|
+
if (subCmd === "promote") {
|
|
3261
|
+
const result = promoteCandidates2(brainRoot);
|
|
3262
|
+
console.log(`\u{1F393} promoted: ${result.promoted.length}, decayed: ${result.decayed.length}`);
|
|
3263
|
+
} else {
|
|
3264
|
+
const candidates = listCandidates2(brainRoot);
|
|
3265
|
+
if (candidates.length === 0) {
|
|
3266
|
+
console.log("No pending candidates");
|
|
3267
|
+
} else {
|
|
3268
|
+
console.log(`Candidates (promote at counter=${3}):`);
|
|
3269
|
+
for (const c of candidates) {
|
|
3270
|
+
const bar = "\u2588".repeat(c.counter) + "\u2591".repeat(Math.max(0, 3 - c.counter));
|
|
3271
|
+
console.log(` ${bar} ${c.counter}/3 ${c.targetPath} (${c.daysInactive}d idle)`);
|
|
3272
|
+
}
|
|
3273
|
+
}
|
|
3274
|
+
}
|
|
3275
|
+
break;
|
|
3276
|
+
}
|
|
3277
|
+
case "evolve": {
|
|
3278
|
+
const dryRun = values["dry-run"] === true;
|
|
3279
|
+
const { runEvolve: runEvolve2 } = await Promise.resolve().then(() => (init_evolve(), evolve_exports));
|
|
3280
|
+
await runEvolve2(brainRoot, dryRun);
|
|
3281
|
+
break;
|
|
3282
|
+
}
|
|
3283
|
+
case "session": {
|
|
3284
|
+
const sub = positionals[1];
|
|
3285
|
+
const { captureSessionStart: captureSessionStart2, detectOutcome: detectOutcome2 } = await Promise.resolve().then(() => (init_outcome(), outcome_exports));
|
|
3286
|
+
switch (sub) {
|
|
3287
|
+
case "start":
|
|
3288
|
+
captureSessionStart2(brainRoot);
|
|
3289
|
+
break;
|
|
3290
|
+
case "end":
|
|
3291
|
+
detectOutcome2(brainRoot);
|
|
3292
|
+
break;
|
|
3293
|
+
default:
|
|
3294
|
+
console.error("Usage: hebbian session <start|end>");
|
|
3295
|
+
process.exit(1);
|
|
3296
|
+
}
|
|
3297
|
+
break;
|
|
3298
|
+
}
|
|
3299
|
+
case "sessions": {
|
|
3300
|
+
const { readEpisodes: readEpisodes2 } = await Promise.resolve().then(() => (init_episode(), episode_exports));
|
|
3301
|
+
const episodes = readEpisodes2(brainRoot).filter((e) => e.outcome);
|
|
3302
|
+
if (episodes.length === 0) {
|
|
3303
|
+
console.log("No session outcomes recorded yet");
|
|
3304
|
+
} else {
|
|
3305
|
+
console.log("Session Outcomes:");
|
|
3306
|
+
for (const ep of episodes.reverse()) {
|
|
3307
|
+
const icon = ep.outcome === "revert" ? "\u{1F504}" : "\u2705";
|
|
3308
|
+
const neurons = ep.neurons ? `${ep.neurons.length} neurons` : "";
|
|
3309
|
+
console.log(` ${icon} ${ep.ts.slice(0, 19)} ${ep.outcome} ${neurons}`);
|
|
3310
|
+
}
|
|
3311
|
+
console.log(`
|
|
3312
|
+
Total: ${episodes.length} sessions`);
|
|
3313
|
+
}
|
|
3314
|
+
break;
|
|
3315
|
+
}
|
|
3316
|
+
case "doctor": {
|
|
3317
|
+
const { runDoctor: runDoctor2 } = await Promise.resolve().then(() => (init_doctor(), doctor_exports));
|
|
3318
|
+
await runDoctor2(brainRoot);
|
|
3319
|
+
break;
|
|
3320
|
+
}
|
|
2404
3321
|
case "diag":
|
|
2405
3322
|
case "stats": {
|
|
2406
3323
|
const { scanBrain: scanBrain2 } = await Promise.resolve().then(() => (init_scanner(), scanner_exports));
|