hebbian 0.3.4 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +155 -70
- package/dist/api.d.ts.map +1 -1
- package/dist/bin/hebbian.js +754 -85
- package/dist/bin/hebbian.js.map +1 -1
- package/dist/candidates.d.ts +32 -0
- package/dist/candidates.d.ts.map +1 -0
- package/dist/constants.d.ts +4 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/digest.d.ts.map +1 -1
- package/dist/doctor.d.ts +7 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/emit.d.ts.map +1 -1
- package/dist/episode.d.ts +6 -1
- package/dist/episode.d.ts.map +1 -1
- package/dist/evolve.d.ts +1 -1
- package/dist/evolve.d.ts.map +1 -1
- package/dist/fire.d.ts +10 -0
- package/dist/fire.d.ts.map +1 -1
- package/dist/grow.d.ts.map +1 -1
- package/dist/hooks.d.ts +3 -3
- package/dist/hooks.d.ts.map +1 -1
- package/dist/inbox.d.ts.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +506 -73
- package/dist/index.js.map +1 -1
- package/dist/outcome.d.ts +42 -0
- package/dist/outcome.d.ts.map +1 -0
- package/package.json +1 -1
package/dist/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 {
|
|
@@ -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
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
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
|
-
|
|
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 =
|
|
1242
|
-
if (!
|
|
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 =
|
|
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(
|
|
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
|
|
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
|
|
1294
|
-
import { join as
|
|
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 =
|
|
1297
|
-
if (!
|
|
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 =
|
|
1520
|
+
const fullPath = join14(brainRoot, neuronPath);
|
|
1353
1521
|
const counterAdd = Math.max(1, correction.counter_add || 1);
|
|
1354
|
-
if (
|
|
1522
|
+
if (existsSync13(fullPath)) {
|
|
1355
1523
|
for (let i = 0; i < counterAdd; i++) {
|
|
1356
1524
|
fireNeuron(brainRoot, neuronPath);
|
|
1357
1525
|
}
|
|
1358
1526
|
} else {
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
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 =
|
|
1381
|
-
if (!
|
|
1382
|
-
|
|
1550
|
+
const inboxDir = join14(brainRoot, INBOX_DIR);
|
|
1551
|
+
if (!existsSync13(inboxDir)) {
|
|
1552
|
+
mkdirSync8(inboxDir, { recursive: true });
|
|
1383
1553
|
}
|
|
1384
|
-
const filePath =
|
|
1385
|
-
if (!
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 (
|
|
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 =
|
|
1708
|
-
const
|
|
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 (
|
|
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:
|
|
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 (!
|
|
1767
|
-
|
|
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
|
|
1777
|
-
|
|
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
|
|
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 (!
|
|
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 (!
|
|
2046
|
+
if (!existsSync14(dir)) return false;
|
|
1855
2047
|
try {
|
|
1856
|
-
const entries =
|
|
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
|
|
1881
|
-
import { join as
|
|
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 (!
|
|
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 =
|
|
1901
|
-
const logPath =
|
|
1902
|
-
if (
|
|
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
|
-
|
|
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 =
|
|
2135
|
-
if (!
|
|
2136
|
-
|
|
2328
|
+
const logDir = join16(brainRoot, DIGEST_LOG_DIR);
|
|
2329
|
+
if (!existsSync15(logDir)) {
|
|
2330
|
+
mkdirSync10(logDir, { recursive: true });
|
|
2137
2331
|
}
|
|
2138
|
-
const logPath =
|
|
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
|
-
|
|
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
|
|
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
|
|
2266
|
-
const
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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));
|