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.
@@ -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,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 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";
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 = readFileSync7(transcriptPath, "utf8");
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 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";
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(readFileSync8(latest.path, "utf8"));
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 readFileSync9, writeFileSync as writeFileSync14 } from "fs";
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(readFileSync9(cooldownPath, "utf8").trim(), 10);
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 readFileSync10, readdirSync as readdirSync11 } from "fs";
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(readFileSync10(pkgPath, "utf8"));
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(readFileSync10(settingsPath, "utf8"));
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.5.3";
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
- 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
+ }
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": {