hebbian 0.5.3 → 0.7.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/dist/bin/hebbian.js +228 -41
- package/dist/bin/hebbian.js.map +1 -1
- package/dist/constants.d.ts +6 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/digest.d.ts +47 -0
- package/dist/digest.d.ts.map +1 -1
- package/dist/evolve.d.ts +2 -1
- package/dist/evolve.d.ts.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +177 -24
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/bin/hebbian.js
CHANGED
|
@@ -10,6 +10,34 @@ var __export = (target, all) => {
|
|
|
10
10
|
};
|
|
11
11
|
|
|
12
12
|
// src/constants.ts
|
|
13
|
+
var constants_exports = {};
|
|
14
|
+
__export(constants_exports, {
|
|
15
|
+
AGENTS_DIR: () => AGENTS_DIR,
|
|
16
|
+
DECAY_DAYS: () => DECAY_DAYS,
|
|
17
|
+
DIGEST_LOG_DIR: () => DIGEST_LOG_DIR,
|
|
18
|
+
EMIT_TARGETS: () => EMIT_TARGETS,
|
|
19
|
+
EMIT_THRESHOLD: () => EMIT_THRESHOLD,
|
|
20
|
+
HOOK_MARKER: () => HOOK_MARKER,
|
|
21
|
+
JACCARD_THRESHOLD: () => JACCARD_THRESHOLD,
|
|
22
|
+
MARKER_END: () => MARKER_END,
|
|
23
|
+
MARKER_START: () => MARKER_START,
|
|
24
|
+
MAX_CORRECTIONS_PER_SESSION: () => MAX_CORRECTIONS_PER_SESSION,
|
|
25
|
+
MAX_DEPTH: () => MAX_DEPTH,
|
|
26
|
+
MIN_CORRECTION_LENGTH: () => MIN_CORRECTION_LENGTH,
|
|
27
|
+
OUTCOME_TYPES: () => OUTCOME_TYPES,
|
|
28
|
+
PROTECTED_REGIONS_CONTRA: () => PROTECTED_REGIONS_CONTRA,
|
|
29
|
+
REGIONS: () => REGIONS,
|
|
30
|
+
REGION_ICONS: () => REGION_ICONS,
|
|
31
|
+
REGION_KO: () => REGION_KO,
|
|
32
|
+
REGION_PRIORITY: () => REGION_PRIORITY,
|
|
33
|
+
SESSION_STATE_DIR: () => SESSION_STATE_DIR,
|
|
34
|
+
SHARED_DIR: () => SHARED_DIR,
|
|
35
|
+
SIGNAL_TYPES: () => SIGNAL_TYPES,
|
|
36
|
+
SPOTLIGHT_DAYS: () => SPOTLIGHT_DAYS,
|
|
37
|
+
resolveAgentBrain: () => resolveAgentBrain,
|
|
38
|
+
resolveBrainRoot: () => resolveBrainRoot,
|
|
39
|
+
resolveSharedBrain: () => resolveSharedBrain
|
|
40
|
+
});
|
|
13
41
|
import { resolve } from "path";
|
|
14
42
|
import { existsSync } from "fs";
|
|
15
43
|
function resolveBrainRoot(brainFlag) {
|
|
@@ -18,7 +46,13 @@ function resolveBrainRoot(brainFlag) {
|
|
|
18
46
|
if (existsSync(resolve("./brain"))) return resolve("./brain");
|
|
19
47
|
return resolve(process.env.HOME || "~", "hebbian", "brain");
|
|
20
48
|
}
|
|
21
|
-
|
|
49
|
+
function resolveAgentBrain(brainRoot, agentName) {
|
|
50
|
+
return resolve(brainRoot, "agents", agentName);
|
|
51
|
+
}
|
|
52
|
+
function resolveSharedBrain(brainRoot) {
|
|
53
|
+
return resolve(brainRoot, "shared");
|
|
54
|
+
}
|
|
55
|
+
var REGIONS, REGION_PRIORITY, REGION_ICONS, REGION_KO, EMIT_THRESHOLD, SPOTLIGHT_DAYS, JACCARD_THRESHOLD, DECAY_DAYS, MAX_DEPTH, EMIT_TARGETS, SIGNAL_TYPES, MARKER_START, MARKER_END, HOOK_MARKER, MAX_CORRECTIONS_PER_SESSION, MIN_CORRECTION_LENGTH, DIGEST_LOG_DIR, OUTCOME_TYPES, SESSION_STATE_DIR, PROTECTED_REGIONS_CONTRA, AGENTS_DIR, SHARED_DIR;
|
|
22
56
|
var init_constants = __esm({
|
|
23
57
|
"src/constants.ts"() {
|
|
24
58
|
"use strict";
|
|
@@ -61,6 +95,7 @@ var init_constants = __esm({
|
|
|
61
95
|
EMIT_THRESHOLD = 5;
|
|
62
96
|
SPOTLIGHT_DAYS = 7;
|
|
63
97
|
JACCARD_THRESHOLD = 0.6;
|
|
98
|
+
DECAY_DAYS = 30;
|
|
64
99
|
MAX_DEPTH = 6;
|
|
65
100
|
EMIT_TARGETS = {
|
|
66
101
|
gemini: ".gemini/GEMINI.md",
|
|
@@ -76,8 +111,11 @@ var init_constants = __esm({
|
|
|
76
111
|
MAX_CORRECTIONS_PER_SESSION = 10;
|
|
77
112
|
MIN_CORRECTION_LENGTH = 15;
|
|
78
113
|
DIGEST_LOG_DIR = "hippocampus/digest_log";
|
|
114
|
+
OUTCOME_TYPES = ["revert", "acceptance"];
|
|
79
115
|
SESSION_STATE_DIR = "hippocampus/session_state";
|
|
80
116
|
PROTECTED_REGIONS_CONTRA = ["brainstem", "limbic", "sensors"];
|
|
117
|
+
AGENTS_DIR = "agents";
|
|
118
|
+
SHARED_DIR = "shared";
|
|
81
119
|
}
|
|
82
120
|
});
|
|
83
121
|
|
|
@@ -86,8 +124,8 @@ var init_exports = {};
|
|
|
86
124
|
__export(init_exports, {
|
|
87
125
|
initBrain: () => initBrain
|
|
88
126
|
});
|
|
89
|
-
import { mkdirSync, writeFileSync, existsSync as existsSync2, readdirSync } from "fs";
|
|
90
|
-
import { join } from "path";
|
|
127
|
+
import { mkdirSync, writeFileSync, readFileSync, existsSync as existsSync2, readdirSync, appendFileSync } from "fs";
|
|
128
|
+
import { join, dirname } from "path";
|
|
91
129
|
function initBrain(brainPath) {
|
|
92
130
|
if (existsSync2(brainPath)) {
|
|
93
131
|
const entries = readdirSync(brainPath);
|
|
@@ -118,6 +156,7 @@ ${template.description}
|
|
|
118
156
|
}
|
|
119
157
|
}
|
|
120
158
|
mkdirSync(join(brainPath, "_agents", "global_inbox"), { recursive: true });
|
|
159
|
+
autoGitignore(brainPath);
|
|
121
160
|
console.log(`\u{1F9E0} Brain initialized at ${brainPath}`);
|
|
122
161
|
console.log(` 7 regions created: ${REGIONS.join(", ")}`);
|
|
123
162
|
console.log("");
|
|
@@ -125,6 +164,30 @@ ${template.description}
|
|
|
125
164
|
console.log(` hebbian grow brainstem/NO_your_rule --brain ${brainPath}`);
|
|
126
165
|
console.log(` hebbian emit claude --brain ${brainPath}`);
|
|
127
166
|
}
|
|
167
|
+
function autoGitignore(brainPath) {
|
|
168
|
+
let dir = dirname(brainPath);
|
|
169
|
+
for (let i = 0; i < 10; i++) {
|
|
170
|
+
if (existsSync2(join(dir, ".git"))) {
|
|
171
|
+
const gitignorePath = join(dir, ".gitignore");
|
|
172
|
+
const brainDirName = brainPath.replace(dir + "/", "") + "/";
|
|
173
|
+
if (existsSync2(gitignorePath)) {
|
|
174
|
+
const content = readFileSync(gitignorePath, "utf8");
|
|
175
|
+
if (content.includes(brainDirName) || content.includes(brainDirName.replace(/\/$/, ""))) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
appendFileSync(gitignorePath, `
|
|
180
|
+
# hebbian brain (personal learning data)
|
|
181
|
+
${brainDirName}
|
|
182
|
+
`, "utf8");
|
|
183
|
+
console.log(` \u{1F4DD} Added ${brainDirName} to .gitignore`);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const parent = dirname(dir);
|
|
187
|
+
if (parent === dir) break;
|
|
188
|
+
dir = parent;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
128
191
|
var REGION_TEMPLATES;
|
|
129
192
|
var init_init = __esm({
|
|
130
193
|
"src/init.ts"() {
|
|
@@ -168,7 +231,7 @@ var scanner_exports = {};
|
|
|
168
231
|
__export(scanner_exports, {
|
|
169
232
|
scanBrain: () => scanBrain
|
|
170
233
|
});
|
|
171
|
-
import { readdirSync as readdirSync2, statSync, readFileSync, existsSync as existsSync3 } from "fs";
|
|
234
|
+
import { readdirSync as readdirSync2, statSync, readFileSync as readFileSync2, existsSync as existsSync3 } from "fs";
|
|
172
235
|
import { join as join2, relative, sep } from "path";
|
|
173
236
|
function scanBrain(brainRoot) {
|
|
174
237
|
const regions = [];
|
|
@@ -295,7 +358,7 @@ function readAxons(regionPath) {
|
|
|
295
358
|
const axonPath = join2(regionPath, ".axon");
|
|
296
359
|
if (!existsSync3(axonPath)) return [];
|
|
297
360
|
try {
|
|
298
|
-
const content =
|
|
361
|
+
const content = readFileSync2(axonPath, "utf8").trim();
|
|
299
362
|
return content.split(/[\n,]+/).map((s) => s.trim()).filter(Boolean);
|
|
300
363
|
} catch {
|
|
301
364
|
return [];
|
|
@@ -366,8 +429,8 @@ __export(emit_exports, {
|
|
|
366
429
|
printDiag: () => printDiag,
|
|
367
430
|
writeAllTiers: () => writeAllTiers
|
|
368
431
|
});
|
|
369
|
-
import { existsSync as existsSync4, readFileSync as
|
|
370
|
-
import { join as join3, dirname } from "path";
|
|
432
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
433
|
+
import { join as join3, dirname as dirname2 } from "path";
|
|
371
434
|
function emitBootstrap(result, brain) {
|
|
372
435
|
const lines = [];
|
|
373
436
|
const now = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d+Z$/, "");
|
|
@@ -543,12 +606,12 @@ function writeAllTiers(brainRoot, result, brain) {
|
|
|
543
606
|
}
|
|
544
607
|
}
|
|
545
608
|
function writeTarget(filePath, content) {
|
|
546
|
-
const dir =
|
|
609
|
+
const dir = dirname2(filePath);
|
|
547
610
|
if (dir !== "." && !existsSync4(dir)) {
|
|
548
611
|
mkdirSync2(dir, { recursive: true });
|
|
549
612
|
}
|
|
550
613
|
if (existsSync4(filePath)) {
|
|
551
|
-
const existing =
|
|
614
|
+
const existing = readFileSync3(filePath, "utf8");
|
|
552
615
|
const startIdx = existing.indexOf(MARKER_START);
|
|
553
616
|
const endIdx = existing.indexOf(MARKER_END);
|
|
554
617
|
if (startIdx !== -1 && endIdx !== -1) {
|
|
@@ -618,7 +681,7 @@ __export(update_check_exports, {
|
|
|
618
681
|
checkForUpdates: () => checkForUpdates,
|
|
619
682
|
formatUpdateBanner: () => formatUpdateBanner
|
|
620
683
|
});
|
|
621
|
-
import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as
|
|
684
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3, statSync as statSync2, unlinkSync } from "fs";
|
|
622
685
|
import { join as join4 } from "path";
|
|
623
686
|
function getStateDir() {
|
|
624
687
|
return join4(process.env.HOME || "~", ".hebbian");
|
|
@@ -642,7 +705,7 @@ function readCache(stateDir) {
|
|
|
642
705
|
const cachePath = join4(stateDir, "last-update-check");
|
|
643
706
|
if (!existsSync5(cachePath)) return null;
|
|
644
707
|
try {
|
|
645
|
-
const line =
|
|
708
|
+
const line = readFileSync4(cachePath, "utf8").trim();
|
|
646
709
|
if (line.startsWith("UP_TO_DATE")) {
|
|
647
710
|
if (isCacheStale(cachePath, "UP_TO_DATE")) return null;
|
|
648
711
|
const ver = line.split(/\s+/)[1];
|
|
@@ -666,7 +729,7 @@ function isSnoozed(stateDir, remoteVersion) {
|
|
|
666
729
|
const snoozePath = join4(stateDir, "update-snoozed");
|
|
667
730
|
if (!existsSync5(snoozePath)) return false;
|
|
668
731
|
try {
|
|
669
|
-
const [ver, levelStr, epochStr] =
|
|
732
|
+
const [ver, levelStr, epochStr] = readFileSync4(snoozePath, "utf8").trim().split(/\s+/);
|
|
670
733
|
if (ver !== remoteVersion) {
|
|
671
734
|
unlinkSync(snoozePath);
|
|
672
735
|
return false;
|
|
@@ -1263,7 +1326,7 @@ __export(candidates_exports, {
|
|
|
1263
1326
|
toCandidatePath: () => toCandidatePath
|
|
1264
1327
|
});
|
|
1265
1328
|
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
|
|
1329
|
+
import { join as join12, dirname as dirname3, relative as relative3 } from "path";
|
|
1267
1330
|
function toCandidatePath(neuronPath) {
|
|
1268
1331
|
const slash = neuronPath.indexOf("/");
|
|
1269
1332
|
if (slash === -1) throw new Error(`Invalid neuron path (missing region): ${neuronPath}`);
|
|
@@ -1290,7 +1353,7 @@ function moveCandidate(brainRoot, candidatePath, targetPath) {
|
|
|
1290
1353
|
fireNeuron(brainRoot, targetPath);
|
|
1291
1354
|
rmSync(src, { recursive: true, force: true });
|
|
1292
1355
|
} else {
|
|
1293
|
-
mkdirSync6(
|
|
1356
|
+
mkdirSync6(dirname3(dst), { recursive: true });
|
|
1294
1357
|
renameSync3(src, dst);
|
|
1295
1358
|
}
|
|
1296
1359
|
console.log(`\u{1F393} promoted: ${candidatePath} \u2192 ${targetPath}`);
|
|
@@ -1383,7 +1446,7 @@ __export(episode_exports, {
|
|
|
1383
1446
|
logEpisode: () => logEpisode,
|
|
1384
1447
|
readEpisodes: () => readEpisodes
|
|
1385
1448
|
});
|
|
1386
|
-
import { readdirSync as readdirSync8, readFileSync as
|
|
1449
|
+
import { readdirSync as readdirSync8, readFileSync as readFileSync5, writeFileSync as writeFileSync9, mkdirSync as mkdirSync7, existsSync as existsSync12 } from "fs";
|
|
1387
1450
|
import { join as join13 } from "path";
|
|
1388
1451
|
function logEpisode(brainRoot, type, path, detail, extra) {
|
|
1389
1452
|
const logDir = join13(brainRoot, SESSION_LOG_DIR);
|
|
@@ -1418,7 +1481,7 @@ function readEpisodes(brainRoot) {
|
|
|
1418
1481
|
for (const entry of entries) {
|
|
1419
1482
|
if (!entry.startsWith("memory") || !entry.endsWith(".neuron")) continue;
|
|
1420
1483
|
try {
|
|
1421
|
-
const content =
|
|
1484
|
+
const content = readFileSync5(join13(logDir, entry), "utf8");
|
|
1422
1485
|
if (content.trim()) {
|
|
1423
1486
|
episodes.push(JSON.parse(content));
|
|
1424
1487
|
}
|
|
@@ -1458,14 +1521,14 @@ __export(inbox_exports, {
|
|
|
1458
1521
|
ensureInbox: () => ensureInbox,
|
|
1459
1522
|
processInbox: () => processInbox
|
|
1460
1523
|
});
|
|
1461
|
-
import { readFileSync as
|
|
1524
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync10, existsSync as existsSync13, mkdirSync as mkdirSync8 } from "fs";
|
|
1462
1525
|
import { join as join14 } from "path";
|
|
1463
1526
|
function processInbox(brainRoot) {
|
|
1464
1527
|
const inboxPath = join14(brainRoot, INBOX_DIR, CORRECTIONS_FILE);
|
|
1465
1528
|
if (!existsSync13(inboxPath)) {
|
|
1466
1529
|
return { processed: 0, skipped: 0, errors: [] };
|
|
1467
1530
|
}
|
|
1468
|
-
const content =
|
|
1531
|
+
const content = readFileSync6(inboxPath, "utf8").trim();
|
|
1469
1532
|
if (!content) {
|
|
1470
1533
|
return { processed: 0, skipped: 0, errors: [] };
|
|
1471
1534
|
}
|
|
@@ -1560,7 +1623,7 @@ function ensureInbox(brainRoot) {
|
|
|
1560
1623
|
function appendCorrection(brainRoot, correction) {
|
|
1561
1624
|
const filePath = ensureInbox(brainRoot);
|
|
1562
1625
|
const line = JSON.stringify(correction) + "\n";
|
|
1563
|
-
const existing =
|
|
1626
|
+
const existing = readFileSync6(filePath, "utf8");
|
|
1564
1627
|
writeFileSync10(filePath, existing + line, "utf8");
|
|
1565
1628
|
}
|
|
1566
1629
|
var INBOX_DIR, CORRECTIONS_FILE, DOPAMINE_ALLOWED_ROLES;
|
|
@@ -1875,7 +1938,7 @@ __export(hooks_exports, {
|
|
|
1875
1938
|
installHooks: () => installHooks,
|
|
1876
1939
|
uninstallHooks: () => uninstallHooks
|
|
1877
1940
|
});
|
|
1878
|
-
import { readFileSync as
|
|
1941
|
+
import { readFileSync as readFileSync7, writeFileSync as writeFileSync11, existsSync as existsSync14, mkdirSync as mkdirSync9, readdirSync as readdirSync9 } from "fs";
|
|
1879
1942
|
import { execSync as execSync2 } from "child_process";
|
|
1880
1943
|
import { join as join15, resolve as resolve2 } from "path";
|
|
1881
1944
|
function installHooks(brainRoot, projectRoot, global) {
|
|
@@ -1904,7 +1967,7 @@ function installHooks(brainRoot, projectRoot, global) {
|
|
|
1904
1967
|
let settings = {};
|
|
1905
1968
|
if (existsSync14(settingsPath)) {
|
|
1906
1969
|
try {
|
|
1907
|
-
settings = JSON.parse(
|
|
1970
|
+
settings = JSON.parse(readFileSync7(settingsPath, "utf8"));
|
|
1908
1971
|
} catch {
|
|
1909
1972
|
console.log(`\u26A0\uFE0F settings.local.json was malformed, overwriting`);
|
|
1910
1973
|
}
|
|
@@ -1970,7 +2033,7 @@ function uninstallHooks(projectRoot, global) {
|
|
|
1970
2033
|
}
|
|
1971
2034
|
let settings;
|
|
1972
2035
|
try {
|
|
1973
|
-
settings = JSON.parse(
|
|
2036
|
+
settings = JSON.parse(readFileSync7(settingsPath, "utf8"));
|
|
1974
2037
|
} catch {
|
|
1975
2038
|
console.log("settings.local.json is malformed, nothing to uninstall");
|
|
1976
2039
|
return;
|
|
@@ -2013,7 +2076,7 @@ function checkHooks(projectRoot, global) {
|
|
|
2013
2076
|
}
|
|
2014
2077
|
let settings;
|
|
2015
2078
|
try {
|
|
2016
|
-
settings = JSON.parse(
|
|
2079
|
+
settings = JSON.parse(readFileSync7(settingsPath, "utf8"));
|
|
2017
2080
|
} catch {
|
|
2018
2081
|
console.log(`\u274C settings.local.json is malformed`);
|
|
2019
2082
|
return status;
|
|
@@ -2065,11 +2128,14 @@ var init_hooks = __esm({
|
|
|
2065
2128
|
// src/digest.ts
|
|
2066
2129
|
var digest_exports = {};
|
|
2067
2130
|
__export(digest_exports, {
|
|
2131
|
+
detectRetryPatterns: () => detectRetryPatterns,
|
|
2132
|
+
detectToolFailure: () => detectToolFailure,
|
|
2068
2133
|
digestTranscript: () => digestTranscript,
|
|
2069
2134
|
extractCorrections: () => extractCorrections,
|
|
2135
|
+
parseToolResults: () => parseToolResults,
|
|
2070
2136
|
readHookInput: () => readHookInput
|
|
2071
2137
|
});
|
|
2072
|
-
import { readFileSync as
|
|
2138
|
+
import { readFileSync as readFileSync8, writeFileSync as writeFileSync12, existsSync as existsSync15, mkdirSync as mkdirSync10 } from "fs";
|
|
2073
2139
|
import { join as join16, basename } from "path";
|
|
2074
2140
|
function readHookInput(stdin) {
|
|
2075
2141
|
if (!stdin.trim()) return null;
|
|
@@ -2093,14 +2159,30 @@ function digestTranscript(brainRoot, transcriptPath, sessionId) {
|
|
|
2093
2159
|
const logPath = join16(logDir, `${resolvedSessionId}.jsonl`);
|
|
2094
2160
|
if (existsSync15(logPath)) {
|
|
2095
2161
|
console.log(`\u23ED already digested session ${resolvedSessionId}, skip`);
|
|
2096
|
-
return { corrections: 0, skipped: 0, transcriptPath, sessionId: resolvedSessionId };
|
|
2162
|
+
return { corrections: 0, skipped: 0, toolFailures: 0, transcriptPath, sessionId: resolvedSessionId };
|
|
2097
2163
|
}
|
|
2098
2164
|
const messages = parseTranscript(transcriptPath);
|
|
2165
|
+
const toolFailures = parseToolResults(transcriptPath);
|
|
2166
|
+
for (const failure of toolFailures) {
|
|
2167
|
+
logEpisode(brainRoot, "tool-failure", failure.toolName, failure.errorText);
|
|
2168
|
+
}
|
|
2169
|
+
const retries = detectRetryPatterns(toolFailures);
|
|
2170
|
+
for (const retry of retries) {
|
|
2171
|
+
logEpisode(brainRoot, "retry-pattern", retry.toolName, retry.errorText);
|
|
2172
|
+
}
|
|
2173
|
+
const totalSignals = toolFailures.length + retries.length;
|
|
2174
|
+
if (totalSignals > 0) {
|
|
2175
|
+
console.log(`\u{1F527} digest: ${toolFailures.length} tool failure(s), ${retries.length} retry pattern(s) logged`);
|
|
2176
|
+
}
|
|
2099
2177
|
const corrections = extractCorrections(messages);
|
|
2100
|
-
if (corrections.length === 0) {
|
|
2178
|
+
if (corrections.length === 0 && toolFailures.length === 0) {
|
|
2101
2179
|
console.log(`\u{1F4DD} digest: no corrections found in session ${resolvedSessionId}`);
|
|
2102
2180
|
writeAuditLog(brainRoot, resolvedSessionId, []);
|
|
2103
|
-
return { corrections: 0, skipped: messages.length, transcriptPath, sessionId: resolvedSessionId };
|
|
2181
|
+
return { corrections: 0, skipped: messages.length, toolFailures: toolFailures.length, transcriptPath, sessionId: resolvedSessionId };
|
|
2182
|
+
}
|
|
2183
|
+
if (corrections.length === 0) {
|
|
2184
|
+
writeAuditLog(brainRoot, resolvedSessionId, []);
|
|
2185
|
+
return { corrections: 0, skipped: messages.length, toolFailures: toolFailures.length, transcriptPath, sessionId: resolvedSessionId };
|
|
2104
2186
|
}
|
|
2105
2187
|
let applied = 0;
|
|
2106
2188
|
const auditEntries = [];
|
|
@@ -2120,12 +2202,13 @@ function digestTranscript(brainRoot, transcriptPath, sessionId) {
|
|
|
2120
2202
|
return {
|
|
2121
2203
|
corrections: applied,
|
|
2122
2204
|
skipped: messages.length - corrections.length,
|
|
2205
|
+
toolFailures: toolFailures.length,
|
|
2123
2206
|
transcriptPath,
|
|
2124
2207
|
sessionId: resolvedSessionId
|
|
2125
2208
|
};
|
|
2126
2209
|
}
|
|
2127
2210
|
function parseTranscript(transcriptPath) {
|
|
2128
|
-
const content =
|
|
2211
|
+
const content = readFileSync8(transcriptPath, "utf8");
|
|
2129
2212
|
const lines = content.split("\n").filter(Boolean);
|
|
2130
2213
|
const messages = [];
|
|
2131
2214
|
for (const line of lines) {
|
|
@@ -2151,6 +2234,63 @@ function extractText(content) {
|
|
|
2151
2234
|
}
|
|
2152
2235
|
return null;
|
|
2153
2236
|
}
|
|
2237
|
+
function parseToolResults(transcriptPath) {
|
|
2238
|
+
const content = readFileSync8(transcriptPath, "utf8");
|
|
2239
|
+
const lines = content.split("\n").filter(Boolean);
|
|
2240
|
+
const failures = [];
|
|
2241
|
+
for (const line of lines) {
|
|
2242
|
+
if (failures.length >= MAX_FAILURES_PER_SESSION) break;
|
|
2243
|
+
let entry;
|
|
2244
|
+
try {
|
|
2245
|
+
entry = JSON.parse(line);
|
|
2246
|
+
} catch {
|
|
2247
|
+
continue;
|
|
2248
|
+
}
|
|
2249
|
+
if (entry.type !== "user") continue;
|
|
2250
|
+
if (!entry.message || !Array.isArray(entry.message.content)) continue;
|
|
2251
|
+
for (const block of entry.message.content) {
|
|
2252
|
+
if (block.type !== "tool_result") continue;
|
|
2253
|
+
if (!block.is_error) continue;
|
|
2254
|
+
const failure = detectToolFailure(block, entry.toolUseResult);
|
|
2255
|
+
if (failure) failures.push(failure);
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
return failures;
|
|
2259
|
+
}
|
|
2260
|
+
function detectRetryPatterns(failures) {
|
|
2261
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2262
|
+
for (const f of failures) {
|
|
2263
|
+
const key = f.toolName.toLowerCase().trim();
|
|
2264
|
+
const existing = counts.get(key);
|
|
2265
|
+
if (existing) {
|
|
2266
|
+
existing.count++;
|
|
2267
|
+
} else {
|
|
2268
|
+
counts.set(key, { failure: f, count: 1 });
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
return [...counts.values()].filter((entry) => entry.count >= 3).map((entry) => ({
|
|
2272
|
+
...entry.failure,
|
|
2273
|
+
toolName: `[retry x${entry.count}] ${entry.failure.toolName}`
|
|
2274
|
+
}));
|
|
2275
|
+
}
|
|
2276
|
+
function detectToolFailure(block, toolUseResult) {
|
|
2277
|
+
if (!block.is_error) return null;
|
|
2278
|
+
let errorText = "";
|
|
2279
|
+
if (typeof block.content === "string") {
|
|
2280
|
+
errorText = block.content;
|
|
2281
|
+
} else if (Array.isArray(block.content)) {
|
|
2282
|
+
errorText = block.content.filter((b) => b.type === "text" && b.text).map((b) => b.text).join("\n");
|
|
2283
|
+
}
|
|
2284
|
+
if (!errorText && typeof toolUseResult === "string") {
|
|
2285
|
+
errorText = toolUseResult;
|
|
2286
|
+
}
|
|
2287
|
+
if (!errorText) return null;
|
|
2288
|
+
const exitMatch = errorText.match(/^Exit code (\d+)/);
|
|
2289
|
+
const exitCode = exitMatch ? parseInt(exitMatch[1], 10) : 1;
|
|
2290
|
+
const firstLine = errorText.split("\n").find((l) => l.trim() && !l.startsWith("Exit code")) || "unknown";
|
|
2291
|
+
const toolName = firstLine.trim().slice(0, 80);
|
|
2292
|
+
return { toolName, exitCode, errorText: errorText.slice(0, 500) };
|
|
2293
|
+
}
|
|
2154
2294
|
function extractCorrections(messages) {
|
|
2155
2295
|
const corrections = [];
|
|
2156
2296
|
for (const text of messages) {
|
|
@@ -2343,7 +2483,7 @@ function writeAuditLog(brainRoot, sessionId, entries) {
|
|
|
2343
2483
|
);
|
|
2344
2484
|
writeFileSync12(logPath, lines.join("\n") + (lines.length > 0 ? "\n" : ""), "utf8");
|
|
2345
2485
|
}
|
|
2346
|
-
var NEGATION_PATTERNS, AFFIRMATION_PATTERNS, MUST_PATTERNS, WARN_PATTERNS;
|
|
2486
|
+
var NEGATION_PATTERNS, AFFIRMATION_PATTERNS, MUST_PATTERNS, WARN_PATTERNS, MAX_FAILURES_PER_SESSION;
|
|
2347
2487
|
var init_digest = __esm({
|
|
2348
2488
|
"src/digest.ts"() {
|
|
2349
2489
|
"use strict";
|
|
@@ -2389,6 +2529,7 @@ var init_digest = __esm({
|
|
|
2389
2529
|
// Korean
|
|
2390
2530
|
/주의/
|
|
2391
2531
|
];
|
|
2532
|
+
MAX_FAILURES_PER_SESSION = 20;
|
|
2392
2533
|
}
|
|
2393
2534
|
});
|
|
2394
2535
|
|
|
@@ -2401,7 +2542,7 @@ __export(outcome_exports, {
|
|
|
2401
2542
|
detectOutcome: () => detectOutcome
|
|
2402
2543
|
});
|
|
2403
2544
|
import { execSync as execSync3 } from "child_process";
|
|
2404
|
-
import { existsSync as existsSync16, mkdirSync as mkdirSync11, writeFileSync as writeFileSync13, readFileSync as
|
|
2545
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync11, writeFileSync as writeFileSync13, readFileSync as readFileSync9, readdirSync as readdirSync10, rmSync as rmSync2, statSync as statSync5 } from "fs";
|
|
2405
2546
|
import { join as join17 } from "path";
|
|
2406
2547
|
import { randomUUID } from "crypto";
|
|
2407
2548
|
function captureSessionStart(brainRoot) {
|
|
@@ -2600,7 +2741,7 @@ function readLatestSessionState(brainRoot) {
|
|
|
2600
2741
|
}
|
|
2601
2742
|
if (!latest) return null;
|
|
2602
2743
|
try {
|
|
2603
|
-
return JSON.parse(
|
|
2744
|
+
return JSON.parse(readFileSync9(latest.path, "utf8"));
|
|
2604
2745
|
} catch {
|
|
2605
2746
|
return null;
|
|
2606
2747
|
}
|
|
@@ -2641,9 +2782,9 @@ __export(evolve_exports, {
|
|
|
2641
2782
|
runEvolve: () => runEvolve,
|
|
2642
2783
|
validateActions: () => validateActions
|
|
2643
2784
|
});
|
|
2644
|
-
import { existsSync as existsSync17, readFileSync as
|
|
2785
|
+
import { existsSync as existsSync17, readFileSync as readFileSync10, writeFileSync as writeFileSync14 } from "fs";
|
|
2645
2786
|
import { join as join18 } from "path";
|
|
2646
|
-
async function runEvolve(brainRoot, dryRun) {
|
|
2787
|
+
async function runEvolve(brainRoot, dryRun, mode = "default") {
|
|
2647
2788
|
const apiKey = process.env.GEMINI_API_KEY;
|
|
2648
2789
|
if (!apiKey) {
|
|
2649
2790
|
console.error("\u274C GEMINI_API_KEY not set. Get one at https://aistudio.google.com/apikey");
|
|
@@ -2653,7 +2794,7 @@ async function runEvolve(brainRoot, dryRun) {
|
|
|
2653
2794
|
const cooldownMs = (parseInt(process.env.EVOLVE_COOLDOWN_SECONDS ?? "60", 10) || 60) * 1e3;
|
|
2654
2795
|
const cooldownPath = join18(brainRoot, EVOLVE_COOLDOWN_FILE);
|
|
2655
2796
|
if (existsSync17(cooldownPath)) {
|
|
2656
|
-
const lastRun = parseInt(
|
|
2797
|
+
const lastRun = parseInt(readFileSync10(cooldownPath, "utf8").trim(), 10);
|
|
2657
2798
|
const elapsed = Date.now() - lastRun;
|
|
2658
2799
|
if (elapsed < cooldownMs) {
|
|
2659
2800
|
const remaining = Math.ceil((cooldownMs - elapsed) / 1e3);
|
|
@@ -2666,7 +2807,7 @@ async function runEvolve(brainRoot, dryRun) {
|
|
|
2666
2807
|
const brain = scanBrain(brainRoot);
|
|
2667
2808
|
const summary = buildBrainSummary(brain);
|
|
2668
2809
|
const outcomeSummary = buildOutcomeSummary(brainRoot);
|
|
2669
|
-
const prompt = buildPrompt(summary, episodes, outcomeSummary);
|
|
2810
|
+
const prompt = mode === "prune" ? buildPrunePrompt(summary, episodes) : buildPrompt(summary, episodes, outcomeSummary);
|
|
2670
2811
|
let rawActions;
|
|
2671
2812
|
try {
|
|
2672
2813
|
rawActions = await callGemini(prompt, apiKey);
|
|
@@ -2757,6 +2898,44 @@ Focus on: strengthening repeatedly-used rules, pruning ineffective ones, growing
|
|
|
2757
2898
|
Respond with a JSON array of actions:
|
|
2758
2899
|
[{"type":"fire","path":"cortex/NO_console_log","reason":"fired 3 times in recent sessions"}]`;
|
|
2759
2900
|
}
|
|
2901
|
+
function buildPrunePrompt(summary, episodes) {
|
|
2902
|
+
const episodeLines = episodes.length > 0 ? episodes.map((e) => `- [${e.ts}] ${e.type}: ${sanitizeForPrompt(e.detail)}`).join("\n") : "(no recent episodes)";
|
|
2903
|
+
return `You are the PRUNING engine for a hebbian brain \u2014 a filesystem-based memory system for AI agents.
|
|
2904
|
+
|
|
2905
|
+
Your job is CLEANUP. Remove what's stale, redundant, or harmful. Healthy forgetting.
|
|
2906
|
+
|
|
2907
|
+
## Axioms
|
|
2908
|
+
- Folder = Neuron, File = Firing Trace, Counter = Activation strength
|
|
2909
|
+
- 7 regions: brainstem(P0) > limbic(P1) > hippocampus(P2) > sensors(P3) > cortex(P4) > ego(P5) > prefrontal(P6)
|
|
2910
|
+
- PROTECTED regions (brainstem, limbic, sensors): NEVER touch these
|
|
2911
|
+
|
|
2912
|
+
## Current Brain
|
|
2913
|
+
${summary}
|
|
2914
|
+
|
|
2915
|
+
## Recent Episodes (last ${episodes.length})
|
|
2916
|
+
${episodeLines}
|
|
2917
|
+
|
|
2918
|
+
## Pruning Criteria
|
|
2919
|
+
1. **Stale neurons** \u2014 counter is low AND no recent episodes mention them. They occupy space but provide no value.
|
|
2920
|
+
2. **High contra ratio** \u2014 neurons present in many reverted sessions (contra_ratio > 0.7). They correlate with bad outcomes.
|
|
2921
|
+
3. **Redundant neurons** \u2014 two neurons in the same region with very similar names/meaning. Keep the stronger one, prune the weaker.
|
|
2922
|
+
4. **Contradicted neurons** \u2014 a newer neuron explicitly overrides an older one. Remove the older.
|
|
2923
|
+
|
|
2924
|
+
## Available Actions (pruning-focused)
|
|
2925
|
+
- prune: Decrement a neuron's counter. Use for rules that aren't working.
|
|
2926
|
+
- decay: Mark inactive neurons as dormant. Use for stale rules with no recent activity.
|
|
2927
|
+
- signal: Add bomb signal to block a problematic neuron. Use for neurons that actively cause harm.
|
|
2928
|
+
|
|
2929
|
+
Do NOT use grow or fire \u2014 this is a pruning pass, not a growth pass.
|
|
2930
|
+
|
|
2931
|
+
## Constraints
|
|
2932
|
+
- Max ${MAX_ACTIONS} actions per cycle
|
|
2933
|
+
- NEVER target brainstem, limbic, or sensors regions
|
|
2934
|
+
- Be conservative \u2014 only prune what you're confident about
|
|
2935
|
+
|
|
2936
|
+
Respond with a JSON array of actions:
|
|
2937
|
+
[{"type":"prune","path":"cortex/WARN_old_rule","reason":"not fired in 30+ days, no recent episodes"}]`;
|
|
2938
|
+
}
|
|
2760
2939
|
async function callGemini(prompt, apiKey) {
|
|
2761
2940
|
const model = process.env.EVOLVE_MODEL || DEFAULT_MODEL;
|
|
2762
2941
|
const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent`;
|
|
@@ -2921,7 +3100,7 @@ var doctor_exports = {};
|
|
|
2921
3100
|
__export(doctor_exports, {
|
|
2922
3101
|
runDoctor: () => runDoctor
|
|
2923
3102
|
});
|
|
2924
|
-
import { existsSync as existsSync18, readFileSync as
|
|
3103
|
+
import { existsSync as existsSync18, readFileSync as readFileSync11, readdirSync as readdirSync11 } from "fs";
|
|
2925
3104
|
import { join as join19 } from "path";
|
|
2926
3105
|
import { execSync as execSync4 } from "child_process";
|
|
2927
3106
|
async function runDoctor(brainRoot) {
|
|
@@ -2952,7 +3131,7 @@ async function runDoctor(brainRoot) {
|
|
|
2952
3131
|
console.log("\nnpm package");
|
|
2953
3132
|
try {
|
|
2954
3133
|
const pkgPath = new URL("../package.json", import.meta.url).pathname;
|
|
2955
|
-
const pkg = JSON.parse(
|
|
3134
|
+
const pkg = JSON.parse(readFileSync11(pkgPath, "utf8"));
|
|
2956
3135
|
const local = pkg.version || "unknown";
|
|
2957
3136
|
let remote = "";
|
|
2958
3137
|
try {
|
|
@@ -2988,7 +3167,7 @@ async function runDoctor(brainRoot) {
|
|
|
2988
3167
|
warn("No .claude/settings.local.json found", "hebbian claude install");
|
|
2989
3168
|
} else {
|
|
2990
3169
|
try {
|
|
2991
|
-
const settings = JSON.parse(
|
|
3170
|
+
const settings = JSON.parse(readFileSync11(settingsPath, "utf8"));
|
|
2992
3171
|
const hooks = settings.hooks || {};
|
|
2993
3172
|
const hasStop = Object.entries(hooks).some(
|
|
2994
3173
|
([event, entries]) => event === "Stop" && Array.isArray(entries) && entries.some(
|
|
@@ -3060,7 +3239,7 @@ var init_doctor = __esm({
|
|
|
3060
3239
|
init_constants();
|
|
3061
3240
|
import { parseArgs } from "util";
|
|
3062
3241
|
import { resolve as resolve3 } from "path";
|
|
3063
|
-
var VERSION = "0.
|
|
3242
|
+
var VERSION = "0.7.0";
|
|
3064
3243
|
var HELP = `
|
|
3065
3244
|
hebbian v${VERSION} \u2014 Folder-as-neuron brain for any AI agent.
|
|
3066
3245
|
|
|
@@ -3086,6 +3265,7 @@ COMMANDS:
|
|
|
3086
3265
|
digest [--transcript <path>] Extract corrections from conversation
|
|
3087
3266
|
candidates [promote] List candidates or promote graduated ones
|
|
3088
3267
|
evolve [--dry-run] LLM-powered brain evolution (Gemini)
|
|
3268
|
+
evolve prune [--dry-run] Pruning mode \u2014 remove stale/redundant neurons
|
|
3089
3269
|
session start|end Capture/detect session outcomes
|
|
3090
3270
|
sessions Show session outcome history
|
|
3091
3271
|
doctor Self-diagnostic (hooks, brain, versions)
|
|
@@ -3131,6 +3311,7 @@ async function main(argv) {
|
|
|
3131
3311
|
transcript: { type: "string", short: "t" },
|
|
3132
3312
|
"dry-run": { type: "boolean" },
|
|
3133
3313
|
global: { type: "boolean", short: "g" },
|
|
3314
|
+
agent: { type: "string", short: "a" },
|
|
3134
3315
|
help: { type: "boolean", short: "h" },
|
|
3135
3316
|
version: { type: "boolean", short: "v" }
|
|
3136
3317
|
},
|
|
@@ -3146,7 +3327,12 @@ async function main(argv) {
|
|
|
3146
3327
|
console.log(HELP);
|
|
3147
3328
|
return;
|
|
3148
3329
|
}
|
|
3149
|
-
|
|
3330
|
+
let brainRoot = resolveBrainRoot(values.brain);
|
|
3331
|
+
const agentName = values.agent;
|
|
3332
|
+
if (agentName) {
|
|
3333
|
+
const { resolveAgentBrain: resolveAgentBrain2 } = await Promise.resolve().then(() => (init_constants(), constants_exports));
|
|
3334
|
+
brainRoot = resolveAgentBrain2(brainRoot, agentName);
|
|
3335
|
+
}
|
|
3150
3336
|
switch (command) {
|
|
3151
3337
|
case "init": {
|
|
3152
3338
|
const target = positionals[1];
|
|
@@ -3321,8 +3507,9 @@ async function main(argv) {
|
|
|
3321
3507
|
}
|
|
3322
3508
|
case "evolve": {
|
|
3323
3509
|
const dryRun = values["dry-run"] === true;
|
|
3510
|
+
const modeArg = positionals[1] === "prune" ? "prune" : "default";
|
|
3324
3511
|
const { runEvolve: runEvolve2 } = await Promise.resolve().then(() => (init_evolve(), evolve_exports));
|
|
3325
|
-
await runEvolve2(brainRoot, dryRun);
|
|
3512
|
+
await runEvolve2(brainRoot, dryRun, modeArg);
|
|
3326
3513
|
break;
|
|
3327
3514
|
}
|
|
3328
3515
|
case "session": {
|