hebbian 0.6.0 → 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.
@@ -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
- 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;
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 = readFileSync(axonPath, "utf8").trim();
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 readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
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 = dirname(filePath);
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 = readFileSync2(filePath, "utf8");
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 readFileSync3, writeFileSync as writeFileSync3, statSync as statSync2, unlinkSync } from "fs";
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 = readFileSync3(cachePath, "utf8").trim();
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] = readFileSync3(snoozePath, "utf8").trim().split(/\s+/);
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 dirname2, relative as relative3 } from "path";
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(dirname2(dst), { recursive: true });
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 readFileSync4, writeFileSync as writeFileSync9, mkdirSync as mkdirSync7, existsSync as existsSync12 } from "fs";
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 = readFileSync4(join13(logDir, entry), "utf8");
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 readFileSync5, writeFileSync as writeFileSync10, existsSync as existsSync13, mkdirSync as mkdirSync8 } from "fs";
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 = readFileSync5(inboxPath, "utf8").trim();
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 = readFileSync5(filePath, "utf8");
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 readFileSync6, writeFileSync as writeFileSync11, existsSync as existsSync14, mkdirSync as mkdirSync9, readdirSync as readdirSync9 } from "fs";
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(readFileSync6(settingsPath, "utf8"));
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(readFileSync6(settingsPath, "utf8"));
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(readFileSync6(settingsPath, "utf8"));
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 readFileSync7, writeFileSync as writeFileSync12, existsSync as existsSync15, mkdirSync as mkdirSync10 } from "fs";
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
- if (toolFailures.length > 0) {
2106
- console.log(`\u{1F527} digest: ${toolFailures.length} tool failure(s) logged as episodes`);
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 = readFileSync7(transcriptPath, "utf8");
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 = readFileSync7(transcriptPath, "utf8");
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 readFileSync8, readdirSync as readdirSync10, rmSync as rmSync2, statSync as statSync5 } from "fs";
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(readFileSync8(latest.path, "utf8"));
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 readFileSync9, writeFileSync as writeFileSync14 } from "fs";
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(readFileSync9(cooldownPath, "utf8").trim(), 10);
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 readFileSync10, readdirSync as readdirSync11 } from "fs";
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(readFileSync10(pkgPath, "utf8"));
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(readFileSync10(settingsPath, "utf8"));
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.6.0";
3242
+ var VERSION = "0.7.0";
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
- const brainRoot = resolveBrainRoot(values.brain);
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": {