hebbian 0.7.1 → 0.8.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.
@@ -25,6 +25,7 @@ __export(constants_exports, {
25
25
  MAX_DEPTH: () => MAX_DEPTH,
26
26
  MIN_CORRECTION_LENGTH: () => MIN_CORRECTION_LENGTH,
27
27
  OUTCOME_TYPES: () => OUTCOME_TYPES,
28
+ PROPAGATION_EPISODE_TYPES: () => PROPAGATION_EPISODE_TYPES,
28
29
  PROTECTED_REGIONS_CONTRA: () => PROTECTED_REGIONS_CONTRA,
29
30
  REGIONS: () => REGIONS,
30
31
  REGION_ICONS: () => REGION_ICONS,
@@ -33,6 +34,7 @@ __export(constants_exports, {
33
34
  SESSION_STATE_DIR: () => SESSION_STATE_DIR,
34
35
  SHARED_DIR: () => SHARED_DIR,
35
36
  SIGNAL_TYPES: () => SIGNAL_TYPES,
37
+ SKILLS_DIR: () => SKILLS_DIR,
36
38
  SPOTLIGHT_DAYS: () => SPOTLIGHT_DAYS,
37
39
  resolveAgentBrain: () => resolveAgentBrain,
38
40
  resolveBrainRoot: () => resolveBrainRoot,
@@ -52,7 +54,7 @@ function resolveAgentBrain(brainRoot, agentName) {
52
54
  function resolveSharedBrain(brainRoot) {
53
55
  return resolve(brainRoot, "shared");
54
56
  }
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;
57
+ 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, SKILLS_DIR, PROPAGATION_EPISODE_TYPES;
56
58
  var init_constants = __esm({
57
59
  "src/constants.ts"() {
58
60
  "use strict";
@@ -116,6 +118,8 @@ var init_constants = __esm({
116
118
  PROTECTED_REGIONS_CONTRA = ["brainstem", "limbic", "sensors"];
117
119
  AGENTS_DIR = "agents";
118
120
  SHARED_DIR = "shared";
121
+ SKILLS_DIR = "skills";
122
+ PROPAGATION_EPISODE_TYPES = ["tool-failure", "retry-pattern"];
119
123
  }
120
124
  });
121
125
 
@@ -156,6 +160,12 @@ ${template.description}
156
160
  }
157
161
  }
158
162
  mkdirSync(join(brainPath, "_agents", "global_inbox"), { recursive: true });
163
+ mkdirSync(join(brainPath, "skills"), { recursive: true });
164
+ writeFileSync(
165
+ join(brainPath, "skills", "_rules.md"),
166
+ "# Skills Library\n\nExecutable patterns learned through experience.\nNot part of the subsumption cascade \u2014 retrieval only.\n",
167
+ "utf8"
168
+ );
159
169
  autoGitignore(brainPath);
160
170
  console.log(`\u{1F9E0} Brain initialized at ${brainPath}`);
161
171
  console.log(` 7 regions created: ${REGIONS.join(", ")}`);
@@ -229,7 +239,8 @@ var init_init = __esm({
229
239
  // src/scanner.ts
230
240
  var scanner_exports = {};
231
241
  __export(scanner_exports, {
232
- scanBrain: () => scanBrain
242
+ scanBrain: () => scanBrain,
243
+ scanSkills: () => scanSkills
233
244
  });
234
245
  import { readdirSync as readdirSync2, statSync, readFileSync as readFileSync2, existsSync as existsSync3 } from "fs";
235
246
  import { join as join2, relative, sep } from "path";
@@ -354,6 +365,11 @@ function walkRegion(dir, regionRoot, depth) {
354
365
  }
355
366
  return neurons;
356
367
  }
368
+ function scanSkills(brainRoot) {
369
+ const skillsPath = join2(brainRoot, SKILLS_DIR);
370
+ if (!existsSync3(skillsPath)) return [];
371
+ return walkRegion(skillsPath, skillsPath, 0);
372
+ }
357
373
  function readAxons(regionPath) {
358
374
  const axonPath = join2(regionPath, ".axon");
359
375
  if (!existsSync3(axonPath)) return [];
@@ -944,8 +960,8 @@ function growNeuron(brainRoot, neuronPath) {
944
960
  }
945
961
  const parts = neuronPath.split("/");
946
962
  const regionName = parts[0];
947
- if (!REGIONS.includes(regionName)) {
948
- throw new Error(`Invalid region: ${regionName}. Valid: ${REGIONS.join(", ")}`);
963
+ if (regionName !== SKILLS_DIR && !REGIONS.includes(regionName)) {
964
+ throw new Error(`Invalid region: ${regionName}. Valid: ${REGIONS.join(", ")}, ${SKILLS_DIR}`);
949
965
  }
950
966
  const leafName = parts[parts.length - 1];
951
967
  const newPrefix = leafName.match(/^(NO|DO|MUST|WARN)_/)?.[1] || "";
@@ -1314,6 +1330,80 @@ var init_watch = __esm({
1314
1330
  }
1315
1331
  });
1316
1332
 
1333
+ // src/episode.ts
1334
+ var episode_exports = {};
1335
+ __export(episode_exports, {
1336
+ logEpisode: () => logEpisode,
1337
+ readEpisodes: () => readEpisodes
1338
+ });
1339
+ import { readdirSync as readdirSync7, readFileSync as readFileSync5, writeFileSync as writeFileSync9, mkdirSync as mkdirSync6, existsSync as existsSync11 } from "fs";
1340
+ import { join as join12 } from "path";
1341
+ function logEpisode(brainRoot, type, path, detail, extra) {
1342
+ const logDir = join12(brainRoot, SESSION_LOG_DIR);
1343
+ if (!existsSync11(logDir)) {
1344
+ mkdirSync6(logDir, { recursive: true });
1345
+ }
1346
+ const nextSlot = getNextSlot(logDir);
1347
+ const episode = {
1348
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
1349
+ type,
1350
+ path,
1351
+ detail,
1352
+ ...extra?.outcome ? { outcome: extra.outcome } : {},
1353
+ ...extra?.neurons ? { neurons: extra.neurons } : {}
1354
+ };
1355
+ writeFileSync9(
1356
+ join12(logDir, `memory${nextSlot}.neuron`),
1357
+ JSON.stringify(episode),
1358
+ "utf8"
1359
+ );
1360
+ }
1361
+ function readEpisodes(brainRoot) {
1362
+ const logDir = join12(brainRoot, SESSION_LOG_DIR);
1363
+ if (!existsSync11(logDir)) return [];
1364
+ const episodes = [];
1365
+ let entries;
1366
+ try {
1367
+ entries = readdirSync7(logDir);
1368
+ } catch {
1369
+ return [];
1370
+ }
1371
+ for (const entry of entries) {
1372
+ if (!entry.startsWith("memory") || !entry.endsWith(".neuron")) continue;
1373
+ try {
1374
+ const content = readFileSync5(join12(logDir, entry), "utf8");
1375
+ if (content.trim()) {
1376
+ episodes.push(JSON.parse(content));
1377
+ }
1378
+ } catch {
1379
+ }
1380
+ }
1381
+ episodes.sort((a, b) => a.ts.localeCompare(b.ts));
1382
+ return episodes;
1383
+ }
1384
+ function getNextSlot(logDir) {
1385
+ let maxSlot = 0;
1386
+ try {
1387
+ for (const entry of readdirSync7(logDir)) {
1388
+ if (entry.startsWith("memory") && entry.endsWith(".neuron")) {
1389
+ const n = parseInt(entry.replace("memory", "").replace(".neuron", ""), 10);
1390
+ if (!isNaN(n) && n > maxSlot) maxSlot = n;
1391
+ }
1392
+ }
1393
+ } catch {
1394
+ }
1395
+ const next = maxSlot + 1;
1396
+ return next > MAX_EPISODES ? maxSlot % MAX_EPISODES + 1 : next;
1397
+ }
1398
+ var MAX_EPISODES, SESSION_LOG_DIR;
1399
+ var init_episode = __esm({
1400
+ "src/episode.ts"() {
1401
+ "use strict";
1402
+ MAX_EPISODES = 100;
1403
+ SESSION_LOG_DIR = "hippocampus/session_log";
1404
+ }
1405
+ });
1406
+
1317
1407
  // src/candidates.ts
1318
1408
  var candidates_exports = {};
1319
1409
  __export(candidates_exports, {
@@ -1323,10 +1413,11 @@ __export(candidates_exports, {
1323
1413
  growCandidate: () => growCandidate,
1324
1414
  listCandidates: () => listCandidates,
1325
1415
  promoteCandidates: () => promoteCandidates,
1416
+ propagateToShared: () => propagateToShared,
1326
1417
  toCandidatePath: () => toCandidatePath
1327
1418
  });
1328
- import { existsSync as existsSync11, mkdirSync as mkdirSync6, readdirSync as readdirSync7, renameSync as renameSync3, rmSync, statSync as statSync4 } from "fs";
1329
- import { join as join12, dirname as dirname3, relative as relative3 } from "path";
1419
+ import { existsSync as existsSync12, mkdirSync as mkdirSync7, readdirSync as readdirSync8, renameSync as renameSync3, rmSync, statSync as statSync4 } from "fs";
1420
+ import { join as join13, dirname as dirname3, relative as relative3 } from "path";
1330
1421
  function toCandidatePath(neuronPath) {
1331
1422
  const slash = neuronPath.indexOf("/");
1332
1423
  if (slash === -1) throw new Error(`Invalid neuron path (missing region): ${neuronPath}`);
@@ -1340,20 +1431,21 @@ function growCandidate(brainRoot, neuronPath) {
1340
1431
  const result = growNeuron(brainRoot, candidatePath);
1341
1432
  if (result.counter >= CANDIDATE_THRESHOLD) {
1342
1433
  const ok = moveCandidate(brainRoot, candidatePath, neuronPath);
1434
+ if (ok) propagateToShared(brainRoot, neuronPath);
1343
1435
  return { ...result, path: ok ? neuronPath : result.path, promoted: ok };
1344
1436
  }
1345
1437
  console.log(` \u{1F331} candidate (${result.counter}/${CANDIDATE_THRESHOLD}): ${candidatePath}`);
1346
1438
  return { ...result, promoted: false };
1347
1439
  }
1348
1440
  function moveCandidate(brainRoot, candidatePath, targetPath) {
1349
- const src = join12(brainRoot, candidatePath);
1350
- if (!existsSync11(src)) return false;
1351
- const dst = join12(brainRoot, targetPath);
1352
- if (existsSync11(dst)) {
1441
+ const src = join13(brainRoot, candidatePath);
1442
+ if (!existsSync12(src)) return false;
1443
+ const dst = join13(brainRoot, targetPath);
1444
+ if (existsSync12(dst)) {
1353
1445
  fireNeuron(brainRoot, targetPath);
1354
1446
  rmSync(src, { recursive: true, force: true });
1355
1447
  } else {
1356
- mkdirSync6(dirname3(dst), { recursive: true });
1448
+ mkdirSync7(dirname3(dst), { recursive: true });
1357
1449
  renameSync3(src, dst);
1358
1450
  }
1359
1451
  console.log(`\u{1F393} promoted: ${candidatePath} \u2192 ${targetPath}`);
@@ -1365,15 +1457,16 @@ function promoteCandidates(brainRoot) {
1365
1457
  const decayMs = CANDIDATE_DECAY_DAYS * 24 * 60 * 60 * 1e3;
1366
1458
  const now = Date.now();
1367
1459
  for (const region of REGIONS) {
1368
- const candidateRoot = join12(brainRoot, region, CANDIDATE_SEGMENT);
1460
+ const candidateRoot = join13(brainRoot, region, CANDIDATE_SEGMENT);
1369
1461
  walkNeuronDirs(candidateRoot, (neuronDir) => {
1370
- const rel = relative3(join12(brainRoot, region), neuronDir);
1462
+ const rel = relative3(join13(brainRoot, region), neuronDir);
1371
1463
  const candidatePath = `${region}/${rel}`;
1372
1464
  const targetPath = fromCandidatePath(candidatePath);
1373
1465
  const counter = readCounter(neuronDir);
1374
1466
  const mtime = statSync4(neuronDir).mtimeMs;
1375
1467
  if (counter >= CANDIDATE_THRESHOLD) {
1376
1468
  moveCandidate(brainRoot, candidatePath, targetPath);
1469
+ propagateToShared(brainRoot, targetPath);
1377
1470
  promoted.push(targetPath);
1378
1471
  } else if (now - mtime > decayMs) {
1379
1472
  rmSync(neuronDir, { recursive: true, force: true });
@@ -1388,9 +1481,9 @@ function listCandidates(brainRoot) {
1388
1481
  const results = [];
1389
1482
  const now = Date.now();
1390
1483
  for (const region of REGIONS) {
1391
- const candidateRoot = join12(brainRoot, region, CANDIDATE_SEGMENT);
1484
+ const candidateRoot = join13(brainRoot, region, CANDIDATE_SEGMENT);
1392
1485
  walkNeuronDirs(candidateRoot, (neuronDir) => {
1393
- const rel = relative3(join12(brainRoot, region), neuronDir);
1486
+ const rel = relative3(join13(brainRoot, region), neuronDir);
1394
1487
  const candidatePath = `${region}/${rel}`;
1395
1488
  const targetPath = fromCandidatePath(candidatePath);
1396
1489
  const counter = readCounter(neuronDir);
@@ -1402,9 +1495,9 @@ function listCandidates(brainRoot) {
1402
1495
  return results;
1403
1496
  }
1404
1497
  function walkNeuronDirs(dir, cb) {
1405
- if (!existsSync11(dir)) return;
1498
+ if (!existsSync12(dir)) return;
1406
1499
  try {
1407
- const entries = readdirSync7(dir, { withFileTypes: true });
1500
+ const entries = readdirSync8(dir, { withFileTypes: true });
1408
1501
  const hasNeuron = entries.some((e) => e.isFile() && e.name.endsWith(".neuron"));
1409
1502
  if (hasNeuron) {
1410
1503
  cb(dir);
@@ -1412,7 +1505,7 @@ function walkNeuronDirs(dir, cb) {
1412
1505
  }
1413
1506
  for (const entry of entries) {
1414
1507
  if (entry.isDirectory() && !entry.name.startsWith(".")) {
1415
- walkNeuronDirs(join12(dir, entry.name), cb);
1508
+ walkNeuronDirs(join13(dir, entry.name), cb);
1416
1509
  }
1417
1510
  }
1418
1511
  } catch {
@@ -1420,13 +1513,33 @@ function walkNeuronDirs(dir, cb) {
1420
1513
  }
1421
1514
  function readCounter(dir) {
1422
1515
  try {
1423
- const files = readdirSync7(dir).filter((f) => /^\d+\.neuron$/.test(f));
1516
+ const files = readdirSync8(dir).filter((f) => /^\d+\.neuron$/.test(f));
1424
1517
  if (files.length === 0) return 0;
1425
1518
  return Math.max(...files.map((f) => parseInt(f, 10)));
1426
1519
  } catch {
1427
1520
  return 0;
1428
1521
  }
1429
1522
  }
1523
+ function propagateToShared(brainRoot, targetPath) {
1524
+ try {
1525
+ const agentsIdx = brainRoot.indexOf("/agents/");
1526
+ if (agentsIdx === -1) return false;
1527
+ const multiBrainRoot = brainRoot.slice(0, agentsIdx);
1528
+ const sharedRoot = join13(multiBrainRoot, "shared");
1529
+ if (!existsSync12(sharedRoot)) return false;
1530
+ const episodes = readEpisodes(brainRoot);
1531
+ const neuronName = targetPath.split("/").pop() || "";
1532
+ const hasRelevantEpisode = episodes.some(
1533
+ (ep) => PROPAGATION_EPISODE_TYPES.includes(ep.type) && (ep.path.includes(neuronName) || ep.detail.includes(neuronName))
1534
+ );
1535
+ if (!hasRelevantEpisode) return false;
1536
+ growNeuron(sharedRoot, targetPath);
1537
+ console.log(` \u{1F4E1} propagated to shared: ${targetPath}`);
1538
+ return true;
1539
+ } catch {
1540
+ return false;
1541
+ }
1542
+ }
1430
1543
  var CANDIDATE_THRESHOLD, CANDIDATE_DECAY_DAYS, CANDIDATE_SEGMENT;
1431
1544
  var init_candidates = __esm({
1432
1545
  "src/candidates.ts"() {
@@ -1434,86 +1547,13 @@ var init_candidates = __esm({
1434
1547
  init_constants();
1435
1548
  init_grow();
1436
1549
  init_fire();
1550
+ init_episode();
1437
1551
  CANDIDATE_THRESHOLD = 3;
1438
1552
  CANDIDATE_DECAY_DAYS = 14;
1439
1553
  CANDIDATE_SEGMENT = "_candidates";
1440
1554
  }
1441
1555
  });
1442
1556
 
1443
- // src/episode.ts
1444
- var episode_exports = {};
1445
- __export(episode_exports, {
1446
- logEpisode: () => logEpisode,
1447
- readEpisodes: () => readEpisodes
1448
- });
1449
- import { readdirSync as readdirSync8, readFileSync as readFileSync5, writeFileSync as writeFileSync9, mkdirSync as mkdirSync7, existsSync as existsSync12 } from "fs";
1450
- import { join as join13 } from "path";
1451
- function logEpisode(brainRoot, type, path, detail, extra) {
1452
- const logDir = join13(brainRoot, SESSION_LOG_DIR);
1453
- if (!existsSync12(logDir)) {
1454
- mkdirSync7(logDir, { recursive: true });
1455
- }
1456
- const nextSlot = getNextSlot(logDir);
1457
- const episode = {
1458
- ts: (/* @__PURE__ */ new Date()).toISOString(),
1459
- type,
1460
- path,
1461
- detail,
1462
- ...extra?.outcome ? { outcome: extra.outcome } : {},
1463
- ...extra?.neurons ? { neurons: extra.neurons } : {}
1464
- };
1465
- writeFileSync9(
1466
- join13(logDir, `memory${nextSlot}.neuron`),
1467
- JSON.stringify(episode),
1468
- "utf8"
1469
- );
1470
- }
1471
- function readEpisodes(brainRoot) {
1472
- const logDir = join13(brainRoot, SESSION_LOG_DIR);
1473
- if (!existsSync12(logDir)) return [];
1474
- const episodes = [];
1475
- let entries;
1476
- try {
1477
- entries = readdirSync8(logDir);
1478
- } catch {
1479
- return [];
1480
- }
1481
- for (const entry of entries) {
1482
- if (!entry.startsWith("memory") || !entry.endsWith(".neuron")) continue;
1483
- try {
1484
- const content = readFileSync5(join13(logDir, entry), "utf8");
1485
- if (content.trim()) {
1486
- episodes.push(JSON.parse(content));
1487
- }
1488
- } catch {
1489
- }
1490
- }
1491
- episodes.sort((a, b) => a.ts.localeCompare(b.ts));
1492
- return episodes;
1493
- }
1494
- function getNextSlot(logDir) {
1495
- let maxSlot = 0;
1496
- try {
1497
- for (const entry of readdirSync8(logDir)) {
1498
- if (entry.startsWith("memory") && entry.endsWith(".neuron")) {
1499
- const n = parseInt(entry.replace("memory", "").replace(".neuron", ""), 10);
1500
- if (!isNaN(n) && n > maxSlot) maxSlot = n;
1501
- }
1502
- }
1503
- } catch {
1504
- }
1505
- const next = maxSlot + 1;
1506
- return next > MAX_EPISODES ? maxSlot % MAX_EPISODES + 1 : next;
1507
- }
1508
- var MAX_EPISODES, SESSION_LOG_DIR;
1509
- var init_episode = __esm({
1510
- "src/episode.ts"() {
1511
- "use strict";
1512
- MAX_EPISODES = 100;
1513
- SESSION_LOG_DIR = "hippocampus/session_log";
1514
- }
1515
- });
1516
-
1517
1557
  // src/inbox.ts
1518
1558
  var inbox_exports = {};
1519
1559
  __export(inbox_exports, {
@@ -2129,6 +2169,7 @@ var init_hooks = __esm({
2129
2169
  var digest_exports = {};
2130
2170
  __export(digest_exports, {
2131
2171
  detectRetryPatterns: () => detectRetryPatterns,
2172
+ detectSoftFailure: () => detectSoftFailure,
2132
2173
  detectToolFailure: () => detectToolFailure,
2133
2174
  digestTranscript: () => digestTranscript,
2134
2175
  extractCorrections: () => extractCorrections,
@@ -2157,12 +2198,21 @@ function digestTranscript(brainRoot, transcriptPath, sessionId) {
2157
2198
  const resolvedSessionId = sessionId || basename(transcriptPath, ".jsonl");
2158
2199
  const logDir = join16(brainRoot, DIGEST_LOG_DIR);
2159
2200
  const logPath = join16(logDir, `${resolvedSessionId}.jsonl`);
2160
- if (existsSync15(logPath)) {
2201
+ const content = readFileSync8(transcriptPath, "utf8");
2202
+ const allLines = content.split("\n").filter(Boolean);
2203
+ const totalLines = allLines.length;
2204
+ const meta = readAuditMeta(logPath);
2205
+ if (existsSync15(logPath) && !meta) {
2161
2206
  console.log(`\u23ED already digested session ${resolvedSessionId}, skip`);
2162
2207
  return { corrections: 0, skipped: 0, toolFailures: 0, transcriptPath, sessionId: resolvedSessionId };
2163
2208
  }
2164
- const messages = parseTranscript(transcriptPath);
2165
- const toolFailures = parseToolResults(transcriptPath);
2209
+ const skipLines = meta ? meta.lineCount : 0;
2210
+ if (skipLines >= totalLines) {
2211
+ return { corrections: 0, skipped: 0, toolFailures: 0, transcriptPath, sessionId: resolvedSessionId };
2212
+ }
2213
+ const newLines = allLines.slice(skipLines);
2214
+ const messages = parseTranscriptFromLines(newLines);
2215
+ const toolFailures = parseToolResultsFromLines(newLines);
2166
2216
  for (const failure of toolFailures) {
2167
2217
  logEpisode(brainRoot, "tool-failure", failure.toolName, failure.errorText);
2168
2218
  }
@@ -2177,11 +2227,11 @@ function digestTranscript(brainRoot, transcriptPath, sessionId) {
2177
2227
  const corrections = extractCorrections(messages);
2178
2228
  if (corrections.length === 0 && toolFailures.length === 0) {
2179
2229
  console.log(`\u{1F4DD} digest: no corrections found in session ${resolvedSessionId}`);
2180
- writeAuditLog(brainRoot, resolvedSessionId, []);
2230
+ writeAuditLog(brainRoot, resolvedSessionId, [], totalLines);
2181
2231
  return { corrections: 0, skipped: messages.length, toolFailures: toolFailures.length, transcriptPath, sessionId: resolvedSessionId };
2182
2232
  }
2183
2233
  if (corrections.length === 0) {
2184
- writeAuditLog(brainRoot, resolvedSessionId, []);
2234
+ writeAuditLog(brainRoot, resolvedSessionId, [], totalLines);
2185
2235
  return { corrections: 0, skipped: messages.length, toolFailures: toolFailures.length, transcriptPath, sessionId: resolvedSessionId };
2186
2236
  }
2187
2237
  let applied = 0;
@@ -2197,7 +2247,7 @@ function digestTranscript(brainRoot, transcriptPath, sessionId) {
2197
2247
  auditEntries.push({ correction, applied: false });
2198
2248
  }
2199
2249
  }
2200
- writeAuditLog(brainRoot, resolvedSessionId, auditEntries);
2250
+ writeAuditLog(brainRoot, resolvedSessionId, auditEntries, totalLines);
2201
2251
  console.log(`\u{1F4DD} digest: ${applied} correction(s) from session ${resolvedSessionId}`);
2202
2252
  return {
2203
2253
  corrections: applied,
@@ -2207,9 +2257,7 @@ function digestTranscript(brainRoot, transcriptPath, sessionId) {
2207
2257
  sessionId: resolvedSessionId
2208
2258
  };
2209
2259
  }
2210
- function parseTranscript(transcriptPath) {
2211
- const content = readFileSync8(transcriptPath, "utf8");
2212
- const lines = content.split("\n").filter(Boolean);
2260
+ function parseTranscriptFromLines(lines) {
2213
2261
  const messages = [];
2214
2262
  for (const line of lines) {
2215
2263
  let entry;
@@ -2236,7 +2284,9 @@ function extractText(content) {
2236
2284
  }
2237
2285
  function parseToolResults(transcriptPath) {
2238
2286
  const content = readFileSync8(transcriptPath, "utf8");
2239
- const lines = content.split("\n").filter(Boolean);
2287
+ return parseToolResultsFromLines(content.split("\n").filter(Boolean));
2288
+ }
2289
+ function parseToolResultsFromLines(lines) {
2240
2290
  const failures = [];
2241
2291
  for (const line of lines) {
2242
2292
  if (failures.length >= MAX_FAILURES_PER_SESSION) break;
@@ -2250,9 +2300,13 @@ function parseToolResults(transcriptPath) {
2250
2300
  if (!entry.message || !Array.isArray(entry.message.content)) continue;
2251
2301
  for (const block of entry.message.content) {
2252
2302
  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);
2303
+ if (block.is_error) {
2304
+ const failure = detectToolFailure(block, entry.toolUseResult);
2305
+ if (failure) failures.push(failure);
2306
+ } else {
2307
+ const failure = detectSoftFailure(block, entry.toolUseResult);
2308
+ if (failure) failures.push(failure);
2309
+ }
2256
2310
  }
2257
2311
  }
2258
2312
  return failures;
@@ -2291,6 +2345,30 @@ function detectToolFailure(block, toolUseResult) {
2291
2345
  const toolName = firstLine.trim().slice(0, 80);
2292
2346
  return { toolName, exitCode, errorText: errorText.slice(0, 500) };
2293
2347
  }
2348
+ function detectSoftFailure(block, toolUseResult) {
2349
+ let text = "";
2350
+ if (typeof block.content === "string") {
2351
+ text = block.content;
2352
+ } else if (Array.isArray(block.content)) {
2353
+ text = block.content.filter((b) => b.type === "text" && b.text).map((b) => b.text).join("\n");
2354
+ }
2355
+ if (toolUseResult && typeof toolUseResult === "object") {
2356
+ if (toolUseResult.stderr) text += "\n" + toolUseResult.stderr;
2357
+ }
2358
+ if (!text) return null;
2359
+ for (const pattern of SOFT_ERROR_PATTERNS) {
2360
+ const match = text.match(pattern);
2361
+ if (match) {
2362
+ const matchedLine = text.split("\n").find((l) => pattern.test(l)) || "unknown";
2363
+ return {
2364
+ toolName: `[soft] ${matchedLine.trim().slice(0, 70)}`,
2365
+ exitCode: 0,
2366
+ errorText: text.slice(0, 500)
2367
+ };
2368
+ }
2369
+ }
2370
+ return null;
2371
+ }
2294
2372
  function extractCorrections(messages) {
2295
2373
  const corrections = [];
2296
2374
  for (const text of messages) {
@@ -2465,13 +2543,28 @@ function extractKeywords(text) {
2465
2543
  ]);
2466
2544
  return text.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[^a-zA-Z0-9\u3000-\u9FFF\uAC00-\uD7AF]+/g, " ").toLowerCase().split(/\s+/).filter((t) => t.length > 2 && !STOP_WORDS.has(t));
2467
2545
  }
2468
- function writeAuditLog(brainRoot, sessionId, entries) {
2546
+ function readAuditMeta(logPath) {
2547
+ if (!existsSync15(logPath)) return null;
2548
+ try {
2549
+ const content = readFileSync8(logPath, "utf8");
2550
+ const firstLine = content.split("\n")[0];
2551
+ if (!firstLine) return null;
2552
+ const parsed = JSON.parse(firstLine);
2553
+ if (parsed._meta && typeof parsed.lineCount === "number") {
2554
+ return { lineCount: parsed.lineCount };
2555
+ }
2556
+ } catch {
2557
+ }
2558
+ return null;
2559
+ }
2560
+ function writeAuditLog(brainRoot, sessionId, entries, lineCount) {
2469
2561
  const logDir = join16(brainRoot, DIGEST_LOG_DIR);
2470
2562
  if (!existsSync15(logDir)) {
2471
2563
  mkdirSync10(logDir, { recursive: true });
2472
2564
  }
2473
2565
  const logPath = join16(logDir, `${sessionId}.jsonl`);
2474
- const lines = entries.map(
2566
+ const metaLine = JSON.stringify({ _meta: true, lineCount, ts: (/* @__PURE__ */ new Date()).toISOString() });
2567
+ const entryLines = entries.map(
2475
2568
  (e) => JSON.stringify({
2476
2569
  ts: (/* @__PURE__ */ new Date()).toISOString(),
2477
2570
  path: e.correction.path,
@@ -2481,9 +2574,9 @@ function writeAuditLog(brainRoot, sessionId, entries) {
2481
2574
  applied: e.applied
2482
2575
  })
2483
2576
  );
2484
- writeFileSync12(logPath, lines.join("\n") + (lines.length > 0 ? "\n" : ""), "utf8");
2577
+ writeFileSync12(logPath, [metaLine, ...entryLines].join("\n") + "\n", "utf8");
2485
2578
  }
2486
- var NEGATION_PATTERNS, AFFIRMATION_PATTERNS, MUST_PATTERNS, WARN_PATTERNS, MAX_FAILURES_PER_SESSION;
2579
+ var NEGATION_PATTERNS, AFFIRMATION_PATTERNS, MUST_PATTERNS, WARN_PATTERNS, MAX_FAILURES_PER_SESSION, SOFT_ERROR_PATTERNS;
2487
2580
  var init_digest = __esm({
2488
2581
  "src/digest.ts"() {
2489
2582
  "use strict";
@@ -2530,6 +2623,14 @@ var init_digest = __esm({
2530
2623
  /주의/
2531
2624
  ];
2532
2625
  MAX_FAILURES_PER_SESSION = 20;
2626
+ SOFT_ERROR_PATTERNS = [
2627
+ /(?:^|\n)\S*(?:\(\w+\):\d+: )?command not found:/m,
2628
+ // shell: command not found
2629
+ /(?:^|\n)npm error\b/m,
2630
+ // npm error (not npm warn)
2631
+ /(?:^|\n)fatal: /m
2632
+ // git fatal
2633
+ ];
2533
2634
  }
2534
2635
  });
2535
2636
 
@@ -3013,6 +3114,7 @@ function validateActions(actions, _brain) {
3013
3114
  return false;
3014
3115
  }
3015
3116
  const region = action.path.split("/")[0];
3117
+ if (region === SKILLS_DIR) return true;
3016
3118
  if (!region || PROTECTED_REGIONS.includes(region)) {
3017
3119
  console.log(` \u{1F6E1}\uFE0F blocked: ${action.type} ${action.path} (protected region)`);
3018
3120
  return false;
@@ -3037,7 +3139,11 @@ function executeActions(brainRoot, actions) {
3037
3139
  fireNeuron(brainRoot, action.path);
3038
3140
  break;
3039
3141
  case "grow":
3040
- growCandidate(brainRoot, action.path);
3142
+ if (action.path.startsWith(SKILLS_DIR + "/")) {
3143
+ growNeuron(brainRoot, action.path);
3144
+ } else {
3145
+ growCandidate(brainRoot, action.path);
3146
+ }
3041
3147
  break;
3042
3148
  case "signal":
3043
3149
  signalNeuron(brainRoot, action.path, action.signal || "dopamine");
@@ -3082,6 +3188,7 @@ var init_evolve = __esm({
3082
3188
  init_constants();
3083
3189
  init_fire();
3084
3190
  init_candidates();
3191
+ init_grow();
3085
3192
  init_signal();
3086
3193
  init_rollback();
3087
3194
  init_decay();
@@ -3235,11 +3342,260 @@ var init_doctor = __esm({
3235
3342
  }
3236
3343
  });
3237
3344
 
3345
+ // src/cron.ts
3346
+ var cron_exports = {};
3347
+ __export(cron_exports, {
3348
+ checkCron: () => checkCron,
3349
+ generateFeedbackPlist: () => generateFeedbackPlist,
3350
+ generatePrunePlist: () => generatePrunePlist,
3351
+ installCron: () => installCron,
3352
+ uninstallCron: () => uninstallCron
3353
+ });
3354
+ import { writeFileSync as writeFileSync15, existsSync as existsSync19, unlinkSync as unlinkSync2 } from "fs";
3355
+ import { join as join20 } from "path";
3356
+ import { execSync as execSync5 } from "child_process";
3357
+ function getLaunchAgentsDir() {
3358
+ return join20(process.env.HOME || "~", "Library", "LaunchAgents");
3359
+ }
3360
+ function getPlistPath(label) {
3361
+ return join20(getLaunchAgentsDir(), `${label}.plist`);
3362
+ }
3363
+ function getNpxPath() {
3364
+ try {
3365
+ return execSync5("which npx", { encoding: "utf8" }).trim();
3366
+ } catch {
3367
+ return "/opt/homebrew/bin/npx";
3368
+ }
3369
+ }
3370
+ function generatePrunePlist(brainRoot, hour = 2, minute = 0) {
3371
+ const npx = getNpxPath();
3372
+ const apiKey = process.env.GEMINI_API_KEY || "";
3373
+ const home = process.env.HOME || "/Users/sweetheart";
3374
+ return `<?xml version="1.0" encoding="UTF-8"?>
3375
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3376
+ <plist version="1.0">
3377
+ <dict>
3378
+ <key>Label</key>
3379
+ <string>${PLIST_LABEL}</string>
3380
+ <key>ProgramArguments</key>
3381
+ <array>
3382
+ <string>${npx}</string>
3383
+ <string>hebbian</string>
3384
+ <string>evolve</string>
3385
+ <string>prune</string>
3386
+ <string>--brain</string>
3387
+ <string>${brainRoot}</string>
3388
+ </array>
3389
+ <key>EnvironmentVariables</key>
3390
+ <dict>
3391
+ <key>GEMINI_API_KEY</key>
3392
+ <string>${apiKey}</string>
3393
+ <key>PATH</key>
3394
+ <string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
3395
+ </dict>
3396
+ <key>StartCalendarInterval</key>
3397
+ <dict>
3398
+ <key>Hour</key>
3399
+ <integer>${hour}</integer>
3400
+ <key>Minute</key>
3401
+ <integer>${minute}</integer>
3402
+ </dict>
3403
+ <key>StandardOutPath</key>
3404
+ <string>${home}/Library/Logs/hebbian-prune.log</string>
3405
+ <key>StandardErrorPath</key>
3406
+ <string>${home}/Library/Logs/hebbian-prune.log</string>
3407
+ </dict>
3408
+ </plist>`;
3409
+ }
3410
+ function generateFeedbackPlist(brainRoot, intervalMinutes = 15) {
3411
+ const npx = getNpxPath();
3412
+ const home = process.env.HOME || "/Users/sweetheart";
3413
+ return `<?xml version="1.0" encoding="UTF-8"?>
3414
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3415
+ <plist version="1.0">
3416
+ <dict>
3417
+ <key>Label</key>
3418
+ <string>${FEEDBACK_PLIST_LABEL}</string>
3419
+ <key>ProgramArguments</key>
3420
+ <array>
3421
+ <string>${npx}</string>
3422
+ <string>hebbian</string>
3423
+ <string>feedback</string>
3424
+ <string>scan</string>
3425
+ <string>--brain</string>
3426
+ <string>${brainRoot}</string>
3427
+ </array>
3428
+ <key>EnvironmentVariables</key>
3429
+ <dict>
3430
+ <key>PATH</key>
3431
+ <string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
3432
+ </dict>
3433
+ <key>StartInterval</key>
3434
+ <integer>${intervalMinutes * 60}</integer>
3435
+ <key>StandardOutPath</key>
3436
+ <string>${home}/Library/Logs/hebbian-feedback.log</string>
3437
+ <key>StandardErrorPath</key>
3438
+ <string>${home}/Library/Logs/hebbian-feedback.log</string>
3439
+ </dict>
3440
+ </plist>`;
3441
+ }
3442
+ function installCron(brainRoot, type = "prune") {
3443
+ const label = type === "prune" ? PLIST_LABEL : FEEDBACK_PLIST_LABEL;
3444
+ const plistPath = getPlistPath(label);
3445
+ const plistContent = type === "prune" ? generatePrunePlist(brainRoot) : generateFeedbackPlist(brainRoot);
3446
+ try {
3447
+ execSync5(`launchctl unload ${plistPath} 2>/dev/null`, { encoding: "utf8" });
3448
+ } catch {
3449
+ }
3450
+ writeFileSync15(plistPath, plistContent, "utf8");
3451
+ execSync5(`launchctl load ${plistPath}`, { encoding: "utf8" });
3452
+ console.log(`\u2705 ${type} cron installed: ${plistPath}`);
3453
+ }
3454
+ function uninstallCron(type = "prune") {
3455
+ const label = type === "prune" ? PLIST_LABEL : FEEDBACK_PLIST_LABEL;
3456
+ const plistPath = getPlistPath(label);
3457
+ if (!existsSync19(plistPath)) {
3458
+ console.log(`\u26A0\uFE0F ${type} cron not installed`);
3459
+ return;
3460
+ }
3461
+ try {
3462
+ execSync5(`launchctl unload ${plistPath}`, { encoding: "utf8" });
3463
+ } catch {
3464
+ }
3465
+ unlinkSync2(plistPath);
3466
+ console.log(`\u{1F5D1}\uFE0F ${type} cron uninstalled`);
3467
+ }
3468
+ function checkCron(type = "prune") {
3469
+ const label = type === "prune" ? PLIST_LABEL : FEEDBACK_PLIST_LABEL;
3470
+ const plistPath = getPlistPath(label);
3471
+ return { installed: existsSync19(plistPath), path: plistPath };
3472
+ }
3473
+ var PLIST_LABEL, FEEDBACK_PLIST_LABEL;
3474
+ var init_cron = __esm({
3475
+ "src/cron.ts"() {
3476
+ "use strict";
3477
+ PLIST_LABEL = "com.hebbian.nightly-prune";
3478
+ FEEDBACK_PLIST_LABEL = "com.hebbian.feedback";
3479
+ }
3480
+ });
3481
+
3482
+ // src/feedback.ts
3483
+ var feedback_exports = {};
3484
+ __export(feedback_exports, {
3485
+ propagateToAgents: () => propagateToAgents,
3486
+ runFeedback: () => runFeedback,
3487
+ scanSharedBrain: () => scanSharedBrain
3488
+ });
3489
+ import { existsSync as existsSync20, readdirSync as readdirSync12, statSync as statSync6, readFileSync as readFileSync12, writeFileSync as writeFileSync16, mkdirSync as mkdirSync12 } from "fs";
3490
+ import { join as join21 } from "path";
3491
+ function scanSharedBrain(brainRoot) {
3492
+ const sharedRoot = join21(brainRoot, SHARED_DIR);
3493
+ if (!existsSync20(sharedRoot)) return [];
3494
+ const watermark = readWatermark(sharedRoot);
3495
+ const deltas = [];
3496
+ for (const region of REGIONS) {
3497
+ const regionPath = join21(sharedRoot, region);
3498
+ if (!existsSync20(regionPath)) continue;
3499
+ walkForNeurons(regionPath, regionPath, (neuronDir, counter) => {
3500
+ const modTime = statSync6(neuronDir).mtime;
3501
+ if (modTime.getTime() <= watermark) return;
3502
+ const name = neuronDir.split("/").pop() || "";
3503
+ if (name.startsWith(WARN_PREFIX)) return;
3504
+ const relPath = region + "/" + neuronDir.slice(regionPath.length + 1);
3505
+ deltas.push({ path: relPath, counter, modTime });
3506
+ });
3507
+ }
3508
+ return deltas;
3509
+ }
3510
+ function propagateToAgents(brainRoot, deltas) {
3511
+ const agentsDir = join21(brainRoot, AGENTS_DIR);
3512
+ if (!existsSync20(agentsDir) || deltas.length === 0) {
3513
+ return { scanned: deltas.length, propagated: 0, agents: [] };
3514
+ }
3515
+ const agentNames = readdirSync12(agentsDir, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".") && !e.name.startsWith("_")).map((e) => e.name);
3516
+ let propagated = 0;
3517
+ const touchedAgents = /* @__PURE__ */ new Set();
3518
+ for (const delta of deltas) {
3519
+ const neuronName = delta.path.split("/").pop() || "";
3520
+ const warnPath = delta.path.replace(/\/([^/]+)$/, `/${WARN_PREFIX}${neuronName}`);
3521
+ for (const agent of agentNames) {
3522
+ const agentBrain = join21(agentsDir, agent);
3523
+ if (!existsSync20(join21(agentBrain, "cortex")) && !existsSync20(join21(agentBrain, "brainstem"))) continue;
3524
+ try {
3525
+ growNeuron(agentBrain, warnPath);
3526
+ logEpisode(agentBrain, "feedback", warnPath, `shared learning: ${delta.path}`);
3527
+ propagated++;
3528
+ touchedAgents.add(agent);
3529
+ } catch {
3530
+ }
3531
+ }
3532
+ }
3533
+ return { scanned: deltas.length, propagated, agents: [...touchedAgents] };
3534
+ }
3535
+ function runFeedback(brainRoot) {
3536
+ const deltas = scanSharedBrain(brainRoot);
3537
+ if (deltas.length === 0) {
3538
+ console.log("\u{1F4E1} feedback: no new shared neurons");
3539
+ return { scanned: 0, propagated: 0, agents: [] };
3540
+ }
3541
+ const result = propagateToAgents(brainRoot, deltas);
3542
+ const latestTime = Math.max(...deltas.map((d) => d.modTime.getTime()));
3543
+ writeWatermark(join21(brainRoot, SHARED_DIR), latestTime);
3544
+ console.log(`\u{1F4E1} feedback: ${result.scanned} shared neuron(s) \u2192 ${result.propagated} warning(s) to ${result.agents.join(", ")}`);
3545
+ return result;
3546
+ }
3547
+ function readWatermark(sharedRoot) {
3548
+ const wmPath = join21(sharedRoot, WATERMARK_FILE);
3549
+ if (!existsSync20(wmPath)) return 0;
3550
+ try {
3551
+ const data = JSON.parse(readFileSync12(wmPath, "utf8"));
3552
+ return data.timestamp || 0;
3553
+ } catch {
3554
+ return 0;
3555
+ }
3556
+ }
3557
+ function writeWatermark(sharedRoot, timestamp) {
3558
+ const wmPath = join21(sharedRoot, WATERMARK_FILE);
3559
+ mkdirSync12(sharedRoot, { recursive: true });
3560
+ writeFileSync16(wmPath, JSON.stringify({ timestamp, ts: new Date(timestamp).toISOString() }), "utf8");
3561
+ }
3562
+ function walkForNeurons(dir, regionRoot, cb) {
3563
+ let entries;
3564
+ try {
3565
+ entries = readdirSync12(dir, { withFileTypes: true });
3566
+ } catch {
3567
+ return;
3568
+ }
3569
+ const neuronFiles = entries.filter((e) => e.isFile() && /^\d+\.neuron$/.test(e.name));
3570
+ if (neuronFiles.length > 0) {
3571
+ const counter = Math.max(...neuronFiles.map((f) => parseInt(f.name, 10)));
3572
+ cb(dir, counter);
3573
+ return;
3574
+ }
3575
+ for (const entry of entries) {
3576
+ if (entry.name.startsWith("_") || entry.name.startsWith(".")) continue;
3577
+ if (entry.isDirectory()) {
3578
+ walkForNeurons(join21(dir, entry.name), regionRoot, cb);
3579
+ }
3580
+ }
3581
+ }
3582
+ var WATERMARK_FILE, WARN_PREFIX;
3583
+ var init_feedback = __esm({
3584
+ "src/feedback.ts"() {
3585
+ "use strict";
3586
+ init_constants();
3587
+ init_grow();
3588
+ init_episode();
3589
+ WATERMARK_FILE = "_feedback_watermark.json";
3590
+ WARN_PREFIX = "WARN_shared_";
3591
+ }
3592
+ });
3593
+
3238
3594
  // src/cli.ts
3239
3595
  init_constants();
3240
3596
  import { parseArgs } from "util";
3241
3597
  import { resolve as resolve3 } from "path";
3242
- var VERSION = "0.7.1";
3598
+ var VERSION = "0.8.0";
3243
3599
  var HELP = `
3244
3600
  hebbian v${VERSION} \u2014 Folder-as-neuron brain for any AI agent.
3245
3601
 
@@ -3550,6 +3906,44 @@ Total: ${episodes.length} sessions`);
3550
3906
  await runDoctor2(brainRoot);
3551
3907
  break;
3552
3908
  }
3909
+ case "cron": {
3910
+ const sub = positionals[1];
3911
+ const { installCron: installCron2, uninstallCron: uninstallCron2, checkCron: checkCron2 } = await Promise.resolve().then(() => (init_cron(), cron_exports));
3912
+ switch (sub) {
3913
+ case "install":
3914
+ installCron2(brainRoot, "prune");
3915
+ break;
3916
+ case "uninstall":
3917
+ uninstallCron2("prune");
3918
+ break;
3919
+ case "status": {
3920
+ const status = checkCron2("prune");
3921
+ console.log(`Pruning cron: ${status.installed ? "\u2705 installed" : "\u274C not installed"}`);
3922
+ if (status.installed) console.log(` ${status.path}`);
3923
+ const fbStatus = checkCron2("feedback");
3924
+ console.log(`Feedback cron: ${fbStatus.installed ? "\u2705 installed" : "\u274C not installed"}`);
3925
+ if (fbStatus.installed) console.log(` ${fbStatus.path}`);
3926
+ break;
3927
+ }
3928
+ default:
3929
+ console.error("Usage: hebbian cron <install|uninstall|status>");
3930
+ process.exit(1);
3931
+ }
3932
+ break;
3933
+ }
3934
+ case "feedback": {
3935
+ const sub = positionals[1];
3936
+ const { runFeedback: runFeedback2 } = await Promise.resolve().then(() => (init_feedback(), feedback_exports));
3937
+ switch (sub) {
3938
+ case "scan":
3939
+ runFeedback2(brainRoot);
3940
+ break;
3941
+ default:
3942
+ console.error("Usage: hebbian feedback <scan>");
3943
+ process.exit(1);
3944
+ }
3945
+ break;
3946
+ }
3553
3947
  case "diag":
3554
3948
  case "stats": {
3555
3949
  const { scanBrain: scanBrain2 } = await Promise.resolve().then(() => (init_scanner(), scanner_exports));