hebbian 0.7.1 → 0.8.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.
@@ -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, {
@@ -3013,6 +3053,7 @@ function validateActions(actions, _brain) {
3013
3053
  return false;
3014
3054
  }
3015
3055
  const region = action.path.split("/")[0];
3056
+ if (region === SKILLS_DIR) return true;
3016
3057
  if (!region || PROTECTED_REGIONS.includes(region)) {
3017
3058
  console.log(` \u{1F6E1}\uFE0F blocked: ${action.type} ${action.path} (protected region)`);
3018
3059
  return false;
@@ -3037,7 +3078,11 @@ function executeActions(brainRoot, actions) {
3037
3078
  fireNeuron(brainRoot, action.path);
3038
3079
  break;
3039
3080
  case "grow":
3040
- growCandidate(brainRoot, action.path);
3081
+ if (action.path.startsWith(SKILLS_DIR + "/")) {
3082
+ growNeuron(brainRoot, action.path);
3083
+ } else {
3084
+ growCandidate(brainRoot, action.path);
3085
+ }
3041
3086
  break;
3042
3087
  case "signal":
3043
3088
  signalNeuron(brainRoot, action.path, action.signal || "dopamine");
@@ -3082,6 +3127,7 @@ var init_evolve = __esm({
3082
3127
  init_constants();
3083
3128
  init_fire();
3084
3129
  init_candidates();
3130
+ init_grow();
3085
3131
  init_signal();
3086
3132
  init_rollback();
3087
3133
  init_decay();
@@ -3235,11 +3281,260 @@ var init_doctor = __esm({
3235
3281
  }
3236
3282
  });
3237
3283
 
3284
+ // src/cron.ts
3285
+ var cron_exports = {};
3286
+ __export(cron_exports, {
3287
+ checkCron: () => checkCron,
3288
+ generateFeedbackPlist: () => generateFeedbackPlist,
3289
+ generatePrunePlist: () => generatePrunePlist,
3290
+ installCron: () => installCron,
3291
+ uninstallCron: () => uninstallCron
3292
+ });
3293
+ import { writeFileSync as writeFileSync15, existsSync as existsSync19, unlinkSync as unlinkSync2 } from "fs";
3294
+ import { join as join20 } from "path";
3295
+ import { execSync as execSync5 } from "child_process";
3296
+ function getLaunchAgentsDir() {
3297
+ return join20(process.env.HOME || "~", "Library", "LaunchAgents");
3298
+ }
3299
+ function getPlistPath(label) {
3300
+ return join20(getLaunchAgentsDir(), `${label}.plist`);
3301
+ }
3302
+ function getNpxPath() {
3303
+ try {
3304
+ return execSync5("which npx", { encoding: "utf8" }).trim();
3305
+ } catch {
3306
+ return "/opt/homebrew/bin/npx";
3307
+ }
3308
+ }
3309
+ function generatePrunePlist(brainRoot, hour = 2, minute = 0) {
3310
+ const npx = getNpxPath();
3311
+ const apiKey = process.env.GEMINI_API_KEY || "";
3312
+ const home = process.env.HOME || "/Users/sweetheart";
3313
+ return `<?xml version="1.0" encoding="UTF-8"?>
3314
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3315
+ <plist version="1.0">
3316
+ <dict>
3317
+ <key>Label</key>
3318
+ <string>${PLIST_LABEL}</string>
3319
+ <key>ProgramArguments</key>
3320
+ <array>
3321
+ <string>${npx}</string>
3322
+ <string>hebbian</string>
3323
+ <string>evolve</string>
3324
+ <string>prune</string>
3325
+ <string>--brain</string>
3326
+ <string>${brainRoot}</string>
3327
+ </array>
3328
+ <key>EnvironmentVariables</key>
3329
+ <dict>
3330
+ <key>GEMINI_API_KEY</key>
3331
+ <string>${apiKey}</string>
3332
+ <key>PATH</key>
3333
+ <string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
3334
+ </dict>
3335
+ <key>StartCalendarInterval</key>
3336
+ <dict>
3337
+ <key>Hour</key>
3338
+ <integer>${hour}</integer>
3339
+ <key>Minute</key>
3340
+ <integer>${minute}</integer>
3341
+ </dict>
3342
+ <key>StandardOutPath</key>
3343
+ <string>${home}/Library/Logs/hebbian-prune.log</string>
3344
+ <key>StandardErrorPath</key>
3345
+ <string>${home}/Library/Logs/hebbian-prune.log</string>
3346
+ </dict>
3347
+ </plist>`;
3348
+ }
3349
+ function generateFeedbackPlist(brainRoot, intervalMinutes = 15) {
3350
+ const npx = getNpxPath();
3351
+ const home = process.env.HOME || "/Users/sweetheart";
3352
+ return `<?xml version="1.0" encoding="UTF-8"?>
3353
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3354
+ <plist version="1.0">
3355
+ <dict>
3356
+ <key>Label</key>
3357
+ <string>${FEEDBACK_PLIST_LABEL}</string>
3358
+ <key>ProgramArguments</key>
3359
+ <array>
3360
+ <string>${npx}</string>
3361
+ <string>hebbian</string>
3362
+ <string>feedback</string>
3363
+ <string>scan</string>
3364
+ <string>--brain</string>
3365
+ <string>${brainRoot}</string>
3366
+ </array>
3367
+ <key>EnvironmentVariables</key>
3368
+ <dict>
3369
+ <key>PATH</key>
3370
+ <string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
3371
+ </dict>
3372
+ <key>StartInterval</key>
3373
+ <integer>${intervalMinutes * 60}</integer>
3374
+ <key>StandardOutPath</key>
3375
+ <string>${home}/Library/Logs/hebbian-feedback.log</string>
3376
+ <key>StandardErrorPath</key>
3377
+ <string>${home}/Library/Logs/hebbian-feedback.log</string>
3378
+ </dict>
3379
+ </plist>`;
3380
+ }
3381
+ function installCron(brainRoot, type = "prune") {
3382
+ const label = type === "prune" ? PLIST_LABEL : FEEDBACK_PLIST_LABEL;
3383
+ const plistPath = getPlistPath(label);
3384
+ const plistContent = type === "prune" ? generatePrunePlist(brainRoot) : generateFeedbackPlist(brainRoot);
3385
+ try {
3386
+ execSync5(`launchctl unload ${plistPath} 2>/dev/null`, { encoding: "utf8" });
3387
+ } catch {
3388
+ }
3389
+ writeFileSync15(plistPath, plistContent, "utf8");
3390
+ execSync5(`launchctl load ${plistPath}`, { encoding: "utf8" });
3391
+ console.log(`\u2705 ${type} cron installed: ${plistPath}`);
3392
+ }
3393
+ function uninstallCron(type = "prune") {
3394
+ const label = type === "prune" ? PLIST_LABEL : FEEDBACK_PLIST_LABEL;
3395
+ const plistPath = getPlistPath(label);
3396
+ if (!existsSync19(plistPath)) {
3397
+ console.log(`\u26A0\uFE0F ${type} cron not installed`);
3398
+ return;
3399
+ }
3400
+ try {
3401
+ execSync5(`launchctl unload ${plistPath}`, { encoding: "utf8" });
3402
+ } catch {
3403
+ }
3404
+ unlinkSync2(plistPath);
3405
+ console.log(`\u{1F5D1}\uFE0F ${type} cron uninstalled`);
3406
+ }
3407
+ function checkCron(type = "prune") {
3408
+ const label = type === "prune" ? PLIST_LABEL : FEEDBACK_PLIST_LABEL;
3409
+ const plistPath = getPlistPath(label);
3410
+ return { installed: existsSync19(plistPath), path: plistPath };
3411
+ }
3412
+ var PLIST_LABEL, FEEDBACK_PLIST_LABEL;
3413
+ var init_cron = __esm({
3414
+ "src/cron.ts"() {
3415
+ "use strict";
3416
+ PLIST_LABEL = "com.hebbian.nightly-prune";
3417
+ FEEDBACK_PLIST_LABEL = "com.hebbian.feedback";
3418
+ }
3419
+ });
3420
+
3421
+ // src/feedback.ts
3422
+ var feedback_exports = {};
3423
+ __export(feedback_exports, {
3424
+ propagateToAgents: () => propagateToAgents,
3425
+ runFeedback: () => runFeedback,
3426
+ scanSharedBrain: () => scanSharedBrain
3427
+ });
3428
+ import { existsSync as existsSync20, readdirSync as readdirSync12, statSync as statSync6, readFileSync as readFileSync12, writeFileSync as writeFileSync16, mkdirSync as mkdirSync12 } from "fs";
3429
+ import { join as join21 } from "path";
3430
+ function scanSharedBrain(brainRoot) {
3431
+ const sharedRoot = join21(brainRoot, SHARED_DIR);
3432
+ if (!existsSync20(sharedRoot)) return [];
3433
+ const watermark = readWatermark(sharedRoot);
3434
+ const deltas = [];
3435
+ for (const region of REGIONS) {
3436
+ const regionPath = join21(sharedRoot, region);
3437
+ if (!existsSync20(regionPath)) continue;
3438
+ walkForNeurons(regionPath, regionPath, (neuronDir, counter) => {
3439
+ const modTime = statSync6(neuronDir).mtime;
3440
+ if (modTime.getTime() <= watermark) return;
3441
+ const name = neuronDir.split("/").pop() || "";
3442
+ if (name.startsWith(WARN_PREFIX)) return;
3443
+ const relPath = region + "/" + neuronDir.slice(regionPath.length + 1);
3444
+ deltas.push({ path: relPath, counter, modTime });
3445
+ });
3446
+ }
3447
+ return deltas;
3448
+ }
3449
+ function propagateToAgents(brainRoot, deltas) {
3450
+ const agentsDir = join21(brainRoot, AGENTS_DIR);
3451
+ if (!existsSync20(agentsDir) || deltas.length === 0) {
3452
+ return { scanned: deltas.length, propagated: 0, agents: [] };
3453
+ }
3454
+ const agentNames = readdirSync12(agentsDir, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".") && !e.name.startsWith("_")).map((e) => e.name);
3455
+ let propagated = 0;
3456
+ const touchedAgents = /* @__PURE__ */ new Set();
3457
+ for (const delta of deltas) {
3458
+ const neuronName = delta.path.split("/").pop() || "";
3459
+ const warnPath = delta.path.replace(/\/([^/]+)$/, `/${WARN_PREFIX}${neuronName}`);
3460
+ for (const agent of agentNames) {
3461
+ const agentBrain = join21(agentsDir, agent);
3462
+ if (!existsSync20(join21(agentBrain, "cortex")) && !existsSync20(join21(agentBrain, "brainstem"))) continue;
3463
+ try {
3464
+ growNeuron(agentBrain, warnPath);
3465
+ logEpisode(agentBrain, "feedback", warnPath, `shared learning: ${delta.path}`);
3466
+ propagated++;
3467
+ touchedAgents.add(agent);
3468
+ } catch {
3469
+ }
3470
+ }
3471
+ }
3472
+ return { scanned: deltas.length, propagated, agents: [...touchedAgents] };
3473
+ }
3474
+ function runFeedback(brainRoot) {
3475
+ const deltas = scanSharedBrain(brainRoot);
3476
+ if (deltas.length === 0) {
3477
+ console.log("\u{1F4E1} feedback: no new shared neurons");
3478
+ return { scanned: 0, propagated: 0, agents: [] };
3479
+ }
3480
+ const result = propagateToAgents(brainRoot, deltas);
3481
+ const latestTime = Math.max(...deltas.map((d) => d.modTime.getTime()));
3482
+ writeWatermark(join21(brainRoot, SHARED_DIR), latestTime);
3483
+ console.log(`\u{1F4E1} feedback: ${result.scanned} shared neuron(s) \u2192 ${result.propagated} warning(s) to ${result.agents.join(", ")}`);
3484
+ return result;
3485
+ }
3486
+ function readWatermark(sharedRoot) {
3487
+ const wmPath = join21(sharedRoot, WATERMARK_FILE);
3488
+ if (!existsSync20(wmPath)) return 0;
3489
+ try {
3490
+ const data = JSON.parse(readFileSync12(wmPath, "utf8"));
3491
+ return data.timestamp || 0;
3492
+ } catch {
3493
+ return 0;
3494
+ }
3495
+ }
3496
+ function writeWatermark(sharedRoot, timestamp) {
3497
+ const wmPath = join21(sharedRoot, WATERMARK_FILE);
3498
+ mkdirSync12(sharedRoot, { recursive: true });
3499
+ writeFileSync16(wmPath, JSON.stringify({ timestamp, ts: new Date(timestamp).toISOString() }), "utf8");
3500
+ }
3501
+ function walkForNeurons(dir, regionRoot, cb) {
3502
+ let entries;
3503
+ try {
3504
+ entries = readdirSync12(dir, { withFileTypes: true });
3505
+ } catch {
3506
+ return;
3507
+ }
3508
+ const neuronFiles = entries.filter((e) => e.isFile() && /^\d+\.neuron$/.test(e.name));
3509
+ if (neuronFiles.length > 0) {
3510
+ const counter = Math.max(...neuronFiles.map((f) => parseInt(f.name, 10)));
3511
+ cb(dir, counter);
3512
+ return;
3513
+ }
3514
+ for (const entry of entries) {
3515
+ if (entry.name.startsWith("_") || entry.name.startsWith(".")) continue;
3516
+ if (entry.isDirectory()) {
3517
+ walkForNeurons(join21(dir, entry.name), regionRoot, cb);
3518
+ }
3519
+ }
3520
+ }
3521
+ var WATERMARK_FILE, WARN_PREFIX;
3522
+ var init_feedback = __esm({
3523
+ "src/feedback.ts"() {
3524
+ "use strict";
3525
+ init_constants();
3526
+ init_grow();
3527
+ init_episode();
3528
+ WATERMARK_FILE = "_feedback_watermark.json";
3529
+ WARN_PREFIX = "WARN_shared_";
3530
+ }
3531
+ });
3532
+
3238
3533
  // src/cli.ts
3239
3534
  init_constants();
3240
3535
  import { parseArgs } from "util";
3241
3536
  import { resolve as resolve3 } from "path";
3242
- var VERSION = "0.7.1";
3537
+ var VERSION = "0.8.0";
3243
3538
  var HELP = `
3244
3539
  hebbian v${VERSION} \u2014 Folder-as-neuron brain for any AI agent.
3245
3540
 
@@ -3550,6 +3845,44 @@ Total: ${episodes.length} sessions`);
3550
3845
  await runDoctor2(brainRoot);
3551
3846
  break;
3552
3847
  }
3848
+ case "cron": {
3849
+ const sub = positionals[1];
3850
+ const { installCron: installCron2, uninstallCron: uninstallCron2, checkCron: checkCron2 } = await Promise.resolve().then(() => (init_cron(), cron_exports));
3851
+ switch (sub) {
3852
+ case "install":
3853
+ installCron2(brainRoot, "prune");
3854
+ break;
3855
+ case "uninstall":
3856
+ uninstallCron2("prune");
3857
+ break;
3858
+ case "status": {
3859
+ const status = checkCron2("prune");
3860
+ console.log(`Pruning cron: ${status.installed ? "\u2705 installed" : "\u274C not installed"}`);
3861
+ if (status.installed) console.log(` ${status.path}`);
3862
+ const fbStatus = checkCron2("feedback");
3863
+ console.log(`Feedback cron: ${fbStatus.installed ? "\u2705 installed" : "\u274C not installed"}`);
3864
+ if (fbStatus.installed) console.log(` ${fbStatus.path}`);
3865
+ break;
3866
+ }
3867
+ default:
3868
+ console.error("Usage: hebbian cron <install|uninstall|status>");
3869
+ process.exit(1);
3870
+ }
3871
+ break;
3872
+ }
3873
+ case "feedback": {
3874
+ const sub = positionals[1];
3875
+ const { runFeedback: runFeedback2 } = await Promise.resolve().then(() => (init_feedback(), feedback_exports));
3876
+ switch (sub) {
3877
+ case "scan":
3878
+ runFeedback2(brainRoot);
3879
+ break;
3880
+ default:
3881
+ console.error("Usage: hebbian feedback <scan>");
3882
+ process.exit(1);
3883
+ }
3884
+ break;
3885
+ }
3553
3886
  case "diag":
3554
3887
  case "stats": {
3555
3888
  const { scanBrain: scanBrain2 } = await Promise.resolve().then(() => (init_scanner(), scanner_exports));