hebbian 0.6.0 → 0.7.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 +47 -2
- package/dist/bin/hebbian.js +171 -40
- 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 +5 -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 +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +121 -24
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<img src="https://img.shields.io/badge/TypeScript-6.0-3178C6?style=flat-square&logo=typescript" />
|
|
3
3
|
<img src="https://img.shields.io/badge/Node.js-22+-339933?style=flat-square&logo=node.js" />
|
|
4
4
|
<img src="https://img.shields.io/badge/Runtime_Deps-0-brightgreen?style=flat-square" />
|
|
5
|
-
<img src="https://img.shields.io/badge/Tests-
|
|
5
|
+
<img src="https://img.shields.io/badge/Tests-317-blue?style=flat-square" />
|
|
6
6
|
<img src="https://img.shields.io/badge/MIT-green?style=flat-square" />
|
|
7
7
|
</p>
|
|
8
8
|
|
|
@@ -158,16 +158,56 @@ hebbian doctor # full diagnostic
|
|
|
158
158
|
|
|
159
159
|
---
|
|
160
160
|
|
|
161
|
+
## Tool Failure Detection (v0.6.0+)
|
|
162
|
+
|
|
163
|
+
hebbian automatically learns from failed commands — no explicit correction needed:
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
# During a session, a bash command fails (exit code ≠ 0)
|
|
167
|
+
# → hebbian digest auto-logs it as a tool-failure episode
|
|
168
|
+
# → evolve sees the pattern and proposes inhibitory neurons
|
|
169
|
+
|
|
170
|
+
hebbian sessions # see tool-failure episodes in the log
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Retry patterns (same error 3+ times) are flagged separately as `retry-pattern` episodes.
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## Multi-Brain (v0.7.0+)
|
|
178
|
+
|
|
179
|
+
Per-agent brains for multi-agent setups:
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
# Individual brain for each agent
|
|
183
|
+
hebbian grow cortex/SOME_RULE --agent cto --brain ./brain
|
|
184
|
+
hebbian grow cortex/OTHER_RULE --agent coo --brain ./brain
|
|
185
|
+
|
|
186
|
+
# Results in:
|
|
187
|
+
# brain/agents/cto/cortex/SOME_RULE/
|
|
188
|
+
# brain/agents/coo/cortex/OTHER_RULE/
|
|
189
|
+
|
|
190
|
+
# Shared brain (cross-cutting knowledge)
|
|
191
|
+
# brain/shared/cortex/...
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
161
196
|
## LLM Evolution
|
|
162
197
|
|
|
163
198
|
```bash
|
|
164
199
|
GEMINI_API_KEY=... hebbian evolve --dry-run --brain ./brain
|
|
200
|
+
|
|
201
|
+
# Pruning mode (nightly cleaner — remove stale/redundant neurons)
|
|
202
|
+
GEMINI_API_KEY=... hebbian evolve prune --dry-run --brain ./brain
|
|
165
203
|
```
|
|
166
204
|
|
|
167
205
|
The evolve engine reads the last 100 episodes + current brain state, sends it to Gemini, and proposes up to 10 mutations per cycle. Protected regions (brainstem/limbic/sensors) are blocked.
|
|
168
206
|
|
|
169
207
|
Actions it can take: `grow` (new neuron), `fire` (strengthen), `signal` (dopamine/bomb), `prune` (weaken), `decay` (mark dormant).
|
|
170
208
|
|
|
209
|
+
**Pruning mode** uses a cleanup-focused prompt that only removes: stale neurons (30+ days inactive), high contra ratio (>0.7), redundant duplicates. Run nightly via cron.
|
|
210
|
+
|
|
171
211
|
---
|
|
172
212
|
|
|
173
213
|
## CLI Reference
|
|
@@ -200,6 +240,11 @@ hebbian digest [--transcript <path>]
|
|
|
200
240
|
|
|
201
241
|
# Evolution
|
|
202
242
|
GEMINI_API_KEY=... hebbian evolve [--dry-run]
|
|
243
|
+
GEMINI_API_KEY=... hebbian evolve prune [--dry-run] # Pruning mode (청소부)
|
|
244
|
+
|
|
245
|
+
# Multi-brain (per-agent)
|
|
246
|
+
hebbian grow cortex/RULE --agent cto # Routes to brain/agents/cto/
|
|
247
|
+
hebbian emit claude --agent coo # Emits from brain/agents/coo/
|
|
203
248
|
```
|
|
204
249
|
|
|
205
250
|
### Emit Targets
|
|
@@ -219,7 +264,7 @@ GEMINI_API_KEY=... hebbian evolve [--dry-run]
|
|
|
219
264
|
|
|
220
265
|
| Feature | .cursorrules / CLAUDE.md | Mem0 / MemOS | hebbian |
|
|
221
266
|
|---------|--------------------------|-------------|------|
|
|
222
|
-
| Self-learning | ❌ manual | ✅ vector DB | ✅ filesystem |
|
|
267
|
+
| Self-learning | ❌ manual | ✅ vector DB | ✅ filesystem + tool failures |
|
|
223
268
|
| Infrastructure | $0 | $$$ | **$0** |
|
|
224
269
|
| Switch AI | Manual migration | Full re-setup | **`cp -r brain/`** |
|
|
225
270
|
| Immutable guardrails | None | None | **brainstem + bomb** |
|
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,13 +2128,14 @@ var init_hooks = __esm({
|
|
|
2065
2128
|
// src/digest.ts
|
|
2066
2129
|
var digest_exports = {};
|
|
2067
2130
|
__export(digest_exports, {
|
|
2131
|
+
detectRetryPatterns: () => detectRetryPatterns,
|
|
2068
2132
|
detectToolFailure: () => detectToolFailure,
|
|
2069
2133
|
digestTranscript: () => digestTranscript,
|
|
2070
2134
|
extractCorrections: () => extractCorrections,
|
|
2071
2135
|
parseToolResults: () => parseToolResults,
|
|
2072
2136
|
readHookInput: () => readHookInput
|
|
2073
2137
|
});
|
|
2074
|
-
import { readFileSync as
|
|
2138
|
+
import { readFileSync as readFileSync8, writeFileSync as writeFileSync12, existsSync as existsSync15, mkdirSync as mkdirSync10 } from "fs";
|
|
2075
2139
|
import { join as join16, basename } from "path";
|
|
2076
2140
|
function readHookInput(stdin) {
|
|
2077
2141
|
if (!stdin.trim()) return null;
|
|
@@ -2102,8 +2166,13 @@ function digestTranscript(brainRoot, transcriptPath, sessionId) {
|
|
|
2102
2166
|
for (const failure of toolFailures) {
|
|
2103
2167
|
logEpisode(brainRoot, "tool-failure", failure.toolName, failure.errorText);
|
|
2104
2168
|
}
|
|
2105
|
-
|
|
2106
|
-
|
|
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`);
|
|
2107
2176
|
}
|
|
2108
2177
|
const corrections = extractCorrections(messages);
|
|
2109
2178
|
if (corrections.length === 0 && toolFailures.length === 0) {
|
|
@@ -2139,7 +2208,7 @@ function digestTranscript(brainRoot, transcriptPath, sessionId) {
|
|
|
2139
2208
|
};
|
|
2140
2209
|
}
|
|
2141
2210
|
function parseTranscript(transcriptPath) {
|
|
2142
|
-
const content =
|
|
2211
|
+
const content = readFileSync8(transcriptPath, "utf8");
|
|
2143
2212
|
const lines = content.split("\n").filter(Boolean);
|
|
2144
2213
|
const messages = [];
|
|
2145
2214
|
for (const line of lines) {
|
|
@@ -2166,7 +2235,7 @@ function extractText(content) {
|
|
|
2166
2235
|
return null;
|
|
2167
2236
|
}
|
|
2168
2237
|
function parseToolResults(transcriptPath) {
|
|
2169
|
-
const content =
|
|
2238
|
+
const content = readFileSync8(transcriptPath, "utf8");
|
|
2170
2239
|
const lines = content.split("\n").filter(Boolean);
|
|
2171
2240
|
const failures = [];
|
|
2172
2241
|
for (const line of lines) {
|
|
@@ -2188,6 +2257,22 @@ function parseToolResults(transcriptPath) {
|
|
|
2188
2257
|
}
|
|
2189
2258
|
return failures;
|
|
2190
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
|
+
}
|
|
2191
2276
|
function detectToolFailure(block, toolUseResult) {
|
|
2192
2277
|
if (!block.is_error) return null;
|
|
2193
2278
|
let errorText = "";
|
|
@@ -2457,7 +2542,7 @@ __export(outcome_exports, {
|
|
|
2457
2542
|
detectOutcome: () => detectOutcome
|
|
2458
2543
|
});
|
|
2459
2544
|
import { execSync as execSync3 } from "child_process";
|
|
2460
|
-
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";
|
|
2461
2546
|
import { join as join17 } from "path";
|
|
2462
2547
|
import { randomUUID } from "crypto";
|
|
2463
2548
|
function captureSessionStart(brainRoot) {
|
|
@@ -2656,7 +2741,7 @@ function readLatestSessionState(brainRoot) {
|
|
|
2656
2741
|
}
|
|
2657
2742
|
if (!latest) return null;
|
|
2658
2743
|
try {
|
|
2659
|
-
return JSON.parse(
|
|
2744
|
+
return JSON.parse(readFileSync9(latest.path, "utf8"));
|
|
2660
2745
|
} catch {
|
|
2661
2746
|
return null;
|
|
2662
2747
|
}
|
|
@@ -2697,9 +2782,9 @@ __export(evolve_exports, {
|
|
|
2697
2782
|
runEvolve: () => runEvolve,
|
|
2698
2783
|
validateActions: () => validateActions
|
|
2699
2784
|
});
|
|
2700
|
-
import { existsSync as existsSync17, readFileSync as
|
|
2785
|
+
import { existsSync as existsSync17, readFileSync as readFileSync10, writeFileSync as writeFileSync14 } from "fs";
|
|
2701
2786
|
import { join as join18 } from "path";
|
|
2702
|
-
async function runEvolve(brainRoot, dryRun) {
|
|
2787
|
+
async function runEvolve(brainRoot, dryRun, mode = "default") {
|
|
2703
2788
|
const apiKey = process.env.GEMINI_API_KEY;
|
|
2704
2789
|
if (!apiKey) {
|
|
2705
2790
|
console.error("\u274C GEMINI_API_KEY not set. Get one at https://aistudio.google.com/apikey");
|
|
@@ -2709,7 +2794,7 @@ async function runEvolve(brainRoot, dryRun) {
|
|
|
2709
2794
|
const cooldownMs = (parseInt(process.env.EVOLVE_COOLDOWN_SECONDS ?? "60", 10) || 60) * 1e3;
|
|
2710
2795
|
const cooldownPath = join18(brainRoot, EVOLVE_COOLDOWN_FILE);
|
|
2711
2796
|
if (existsSync17(cooldownPath)) {
|
|
2712
|
-
const lastRun = parseInt(
|
|
2797
|
+
const lastRun = parseInt(readFileSync10(cooldownPath, "utf8").trim(), 10);
|
|
2713
2798
|
const elapsed = Date.now() - lastRun;
|
|
2714
2799
|
if (elapsed < cooldownMs) {
|
|
2715
2800
|
const remaining = Math.ceil((cooldownMs - elapsed) / 1e3);
|
|
@@ -2722,7 +2807,7 @@ async function runEvolve(brainRoot, dryRun) {
|
|
|
2722
2807
|
const brain = scanBrain(brainRoot);
|
|
2723
2808
|
const summary = buildBrainSummary(brain);
|
|
2724
2809
|
const outcomeSummary = buildOutcomeSummary(brainRoot);
|
|
2725
|
-
const prompt = buildPrompt(summary, episodes, outcomeSummary);
|
|
2810
|
+
const prompt = mode === "prune" ? buildPrunePrompt(summary, episodes) : buildPrompt(summary, episodes, outcomeSummary);
|
|
2726
2811
|
let rawActions;
|
|
2727
2812
|
try {
|
|
2728
2813
|
rawActions = await callGemini(prompt, apiKey);
|
|
@@ -2813,6 +2898,44 @@ Focus on: strengthening repeatedly-used rules, pruning ineffective ones, growing
|
|
|
2813
2898
|
Respond with a JSON array of actions:
|
|
2814
2899
|
[{"type":"fire","path":"cortex/NO_console_log","reason":"fired 3 times in recent sessions"}]`;
|
|
2815
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
|
+
}
|
|
2816
2939
|
async function callGemini(prompt, apiKey) {
|
|
2817
2940
|
const model = process.env.EVOLVE_MODEL || DEFAULT_MODEL;
|
|
2818
2941
|
const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent`;
|
|
@@ -2977,7 +3100,7 @@ var doctor_exports = {};
|
|
|
2977
3100
|
__export(doctor_exports, {
|
|
2978
3101
|
runDoctor: () => runDoctor
|
|
2979
3102
|
});
|
|
2980
|
-
import { existsSync as existsSync18, readFileSync as
|
|
3103
|
+
import { existsSync as existsSync18, readFileSync as readFileSync11, readdirSync as readdirSync11 } from "fs";
|
|
2981
3104
|
import { join as join19 } from "path";
|
|
2982
3105
|
import { execSync as execSync4 } from "child_process";
|
|
2983
3106
|
async function runDoctor(brainRoot) {
|
|
@@ -3008,7 +3131,7 @@ async function runDoctor(brainRoot) {
|
|
|
3008
3131
|
console.log("\nnpm package");
|
|
3009
3132
|
try {
|
|
3010
3133
|
const pkgPath = new URL("../package.json", import.meta.url).pathname;
|
|
3011
|
-
const pkg = JSON.parse(
|
|
3134
|
+
const pkg = JSON.parse(readFileSync11(pkgPath, "utf8"));
|
|
3012
3135
|
const local = pkg.version || "unknown";
|
|
3013
3136
|
let remote = "";
|
|
3014
3137
|
try {
|
|
@@ -3044,7 +3167,7 @@ async function runDoctor(brainRoot) {
|
|
|
3044
3167
|
warn("No .claude/settings.local.json found", "hebbian claude install");
|
|
3045
3168
|
} else {
|
|
3046
3169
|
try {
|
|
3047
|
-
const settings = JSON.parse(
|
|
3170
|
+
const settings = JSON.parse(readFileSync11(settingsPath, "utf8"));
|
|
3048
3171
|
const hooks = settings.hooks || {};
|
|
3049
3172
|
const hasStop = Object.entries(hooks).some(
|
|
3050
3173
|
([event, entries]) => event === "Stop" && Array.isArray(entries) && entries.some(
|
|
@@ -3116,7 +3239,7 @@ var init_doctor = __esm({
|
|
|
3116
3239
|
init_constants();
|
|
3117
3240
|
import { parseArgs } from "util";
|
|
3118
3241
|
import { resolve as resolve3 } from "path";
|
|
3119
|
-
var VERSION = "0.
|
|
3242
|
+
var VERSION = "0.7.1";
|
|
3120
3243
|
var HELP = `
|
|
3121
3244
|
hebbian v${VERSION} \u2014 Folder-as-neuron brain for any AI agent.
|
|
3122
3245
|
|
|
@@ -3142,6 +3265,7 @@ COMMANDS:
|
|
|
3142
3265
|
digest [--transcript <path>] Extract corrections from conversation
|
|
3143
3266
|
candidates [promote] List candidates or promote graduated ones
|
|
3144
3267
|
evolve [--dry-run] LLM-powered brain evolution (Gemini)
|
|
3268
|
+
evolve prune [--dry-run] Pruning mode \u2014 remove stale/redundant neurons
|
|
3145
3269
|
session start|end Capture/detect session outcomes
|
|
3146
3270
|
sessions Show session outcome history
|
|
3147
3271
|
doctor Self-diagnostic (hooks, brain, versions)
|
|
@@ -3187,6 +3311,7 @@ async function main(argv) {
|
|
|
3187
3311
|
transcript: { type: "string", short: "t" },
|
|
3188
3312
|
"dry-run": { type: "boolean" },
|
|
3189
3313
|
global: { type: "boolean", short: "g" },
|
|
3314
|
+
agent: { type: "string", short: "a" },
|
|
3190
3315
|
help: { type: "boolean", short: "h" },
|
|
3191
3316
|
version: { type: "boolean", short: "v" }
|
|
3192
3317
|
},
|
|
@@ -3202,7 +3327,12 @@ async function main(argv) {
|
|
|
3202
3327
|
console.log(HELP);
|
|
3203
3328
|
return;
|
|
3204
3329
|
}
|
|
3205
|
-
|
|
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
|
+
}
|
|
3206
3336
|
switch (command) {
|
|
3207
3337
|
case "init": {
|
|
3208
3338
|
const target = positionals[1];
|
|
@@ -3377,8 +3507,9 @@ async function main(argv) {
|
|
|
3377
3507
|
}
|
|
3378
3508
|
case "evolve": {
|
|
3379
3509
|
const dryRun = values["dry-run"] === true;
|
|
3510
|
+
const modeArg = positionals[1] === "prune" ? "prune" : "default";
|
|
3380
3511
|
const { runEvolve: runEvolve2 } = await Promise.resolve().then(() => (init_evolve(), evolve_exports));
|
|
3381
|
-
await runEvolve2(brainRoot, dryRun);
|
|
3512
|
+
await runEvolve2(brainRoot, dryRun, modeArg);
|
|
3382
3513
|
break;
|
|
3383
3514
|
}
|
|
3384
3515
|
case "session": {
|