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.
package/dist/index.js CHANGED
@@ -73,6 +73,8 @@ function resolveSharedBrain(brainRoot) {
73
73
  }
74
74
  var AGENTS_DIR = "agents";
75
75
  var SHARED_DIR = "shared";
76
+ var SKILLS_DIR = "skills";
77
+ var PROPAGATION_EPISODE_TYPES = ["tool-failure", "retry-pattern"];
76
78
 
77
79
  // src/scanner.ts
78
80
  import { readdirSync, statSync, readFileSync, existsSync as existsSync2 } from "fs";
@@ -198,6 +200,11 @@ function walkRegion(dir, regionRoot, depth) {
198
200
  }
199
201
  return neurons;
200
202
  }
203
+ function scanSkills(brainRoot) {
204
+ const skillsPath = join(brainRoot, SKILLS_DIR);
205
+ if (!existsSync2(skillsPath)) return [];
206
+ return walkRegion(skillsPath, skillsPath, 0);
207
+ }
201
208
  function readAxons(regionPath) {
202
209
  const axonPath = join(regionPath, ".axon");
203
210
  if (!existsSync2(axonPath)) return [];
@@ -413,8 +420,8 @@ function growNeuron(brainRoot, neuronPath) {
413
420
  }
414
421
  const parts = neuronPath.split("/");
415
422
  const regionName = parts[0];
416
- if (!REGIONS.includes(regionName)) {
417
- throw new Error(`Invalid region: ${regionName}. Valid: ${REGIONS.join(", ")}`);
423
+ if (regionName !== SKILLS_DIR && !REGIONS.includes(regionName)) {
424
+ throw new Error(`Invalid region: ${regionName}. Valid: ${REGIONS.join(", ")}, ${SKILLS_DIR}`);
418
425
  }
419
426
  const leafName = parts[parts.length - 1];
420
427
  const newPrefix = leafName.match(/^(NO|DO|MUST|WARN)_/)?.[1] || "";
@@ -952,6 +959,12 @@ ${template.description}
952
959
  }
953
960
  }
954
961
  mkdirSync4(join10(brainPath, "_agents", "global_inbox"), { recursive: true });
962
+ mkdirSync4(join10(brainPath, "skills"), { recursive: true });
963
+ writeFileSync7(
964
+ join10(brainPath, "skills", "_rules.md"),
965
+ "# Skills Library\n\nExecutable patterns learned through experience.\nNot part of the subsumption cascade \u2014 retrieval only.\n",
966
+ "utf8"
967
+ );
955
968
  autoGitignore(brainPath);
956
969
  console.log(`\u{1F9E0} Brain initialized at ${brainPath}`);
957
970
  console.log(` 7 regions created: ${REGIONS.join(", ")}`);
@@ -993,8 +1006,73 @@ import { readFileSync as readFileSync5, writeFileSync as writeFileSync9, existsS
993
1006
  import { join as join13 } from "path";
994
1007
 
995
1008
  // src/candidates.ts
996
- import { existsSync as existsSync10, mkdirSync as mkdirSync5, readdirSync as readdirSync7, renameSync as renameSync3, rmSync, statSync as statSync3 } from "fs";
997
- import { join as join11, dirname as dirname3, relative as relative3 } from "path";
1009
+ import { existsSync as existsSync11, mkdirSync as mkdirSync6, readdirSync as readdirSync8, renameSync as renameSync3, rmSync, statSync as statSync3 } from "fs";
1010
+ import { join as join12, dirname as dirname3, relative as relative3 } from "path";
1011
+
1012
+ // src/episode.ts
1013
+ import { readdirSync as readdirSync7, readFileSync as readFileSync4, writeFileSync as writeFileSync8, mkdirSync as mkdirSync5, existsSync as existsSync10 } from "fs";
1014
+ import { join as join11 } from "path";
1015
+ var MAX_EPISODES = 100;
1016
+ var SESSION_LOG_DIR = "hippocampus/session_log";
1017
+ function logEpisode(brainRoot, type, path, detail, extra) {
1018
+ const logDir = join11(brainRoot, SESSION_LOG_DIR);
1019
+ if (!existsSync10(logDir)) {
1020
+ mkdirSync5(logDir, { recursive: true });
1021
+ }
1022
+ const nextSlot = getNextSlot(logDir);
1023
+ const episode = {
1024
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
1025
+ type,
1026
+ path,
1027
+ detail,
1028
+ ...extra?.outcome ? { outcome: extra.outcome } : {},
1029
+ ...extra?.neurons ? { neurons: extra.neurons } : {}
1030
+ };
1031
+ writeFileSync8(
1032
+ join11(logDir, `memory${nextSlot}.neuron`),
1033
+ JSON.stringify(episode),
1034
+ "utf8"
1035
+ );
1036
+ }
1037
+ function readEpisodes(brainRoot) {
1038
+ const logDir = join11(brainRoot, SESSION_LOG_DIR);
1039
+ if (!existsSync10(logDir)) return [];
1040
+ const episodes = [];
1041
+ let entries;
1042
+ try {
1043
+ entries = readdirSync7(logDir);
1044
+ } catch {
1045
+ return [];
1046
+ }
1047
+ for (const entry of entries) {
1048
+ if (!entry.startsWith("memory") || !entry.endsWith(".neuron")) continue;
1049
+ try {
1050
+ const content = readFileSync4(join11(logDir, entry), "utf8");
1051
+ if (content.trim()) {
1052
+ episodes.push(JSON.parse(content));
1053
+ }
1054
+ } catch {
1055
+ }
1056
+ }
1057
+ episodes.sort((a, b) => a.ts.localeCompare(b.ts));
1058
+ return episodes;
1059
+ }
1060
+ function getNextSlot(logDir) {
1061
+ let maxSlot = 0;
1062
+ try {
1063
+ for (const entry of readdirSync7(logDir)) {
1064
+ if (entry.startsWith("memory") && entry.endsWith(".neuron")) {
1065
+ const n = parseInt(entry.replace("memory", "").replace(".neuron", ""), 10);
1066
+ if (!isNaN(n) && n > maxSlot) maxSlot = n;
1067
+ }
1068
+ }
1069
+ } catch {
1070
+ }
1071
+ const next = maxSlot + 1;
1072
+ return next > MAX_EPISODES ? maxSlot % MAX_EPISODES + 1 : next;
1073
+ }
1074
+
1075
+ // src/candidates.ts
998
1076
  var CANDIDATE_THRESHOLD = 3;
999
1077
  var CANDIDATE_DECAY_DAYS = 14;
1000
1078
  var CANDIDATE_SEGMENT = "_candidates";
@@ -1011,20 +1089,21 @@ function growCandidate(brainRoot, neuronPath) {
1011
1089
  const result = growNeuron(brainRoot, candidatePath);
1012
1090
  if (result.counter >= CANDIDATE_THRESHOLD) {
1013
1091
  const ok = moveCandidate(brainRoot, candidatePath, neuronPath);
1092
+ if (ok) propagateToShared(brainRoot, neuronPath);
1014
1093
  return { ...result, path: ok ? neuronPath : result.path, promoted: ok };
1015
1094
  }
1016
1095
  console.log(` \u{1F331} candidate (${result.counter}/${CANDIDATE_THRESHOLD}): ${candidatePath}`);
1017
1096
  return { ...result, promoted: false };
1018
1097
  }
1019
1098
  function moveCandidate(brainRoot, candidatePath, targetPath) {
1020
- const src = join11(brainRoot, candidatePath);
1021
- if (!existsSync10(src)) return false;
1022
- const dst = join11(brainRoot, targetPath);
1023
- if (existsSync10(dst)) {
1099
+ const src = join12(brainRoot, candidatePath);
1100
+ if (!existsSync11(src)) return false;
1101
+ const dst = join12(brainRoot, targetPath);
1102
+ if (existsSync11(dst)) {
1024
1103
  fireNeuron(brainRoot, targetPath);
1025
1104
  rmSync(src, { recursive: true, force: true });
1026
1105
  } else {
1027
- mkdirSync5(dirname3(dst), { recursive: true });
1106
+ mkdirSync6(dirname3(dst), { recursive: true });
1028
1107
  renameSync3(src, dst);
1029
1108
  }
1030
1109
  console.log(`\u{1F393} promoted: ${candidatePath} \u2192 ${targetPath}`);
@@ -1036,15 +1115,16 @@ function promoteCandidates(brainRoot) {
1036
1115
  const decayMs = CANDIDATE_DECAY_DAYS * 24 * 60 * 60 * 1e3;
1037
1116
  const now = Date.now();
1038
1117
  for (const region of REGIONS) {
1039
- const candidateRoot = join11(brainRoot, region, CANDIDATE_SEGMENT);
1118
+ const candidateRoot = join12(brainRoot, region, CANDIDATE_SEGMENT);
1040
1119
  walkNeuronDirs(candidateRoot, (neuronDir) => {
1041
- const rel = relative3(join11(brainRoot, region), neuronDir);
1120
+ const rel = relative3(join12(brainRoot, region), neuronDir);
1042
1121
  const candidatePath = `${region}/${rel}`;
1043
1122
  const targetPath = fromCandidatePath(candidatePath);
1044
1123
  const counter = readCounter(neuronDir);
1045
1124
  const mtime = statSync3(neuronDir).mtimeMs;
1046
1125
  if (counter >= CANDIDATE_THRESHOLD) {
1047
1126
  moveCandidate(brainRoot, candidatePath, targetPath);
1127
+ propagateToShared(brainRoot, targetPath);
1048
1128
  promoted.push(targetPath);
1049
1129
  } else if (now - mtime > decayMs) {
1050
1130
  rmSync(neuronDir, { recursive: true, force: true });
@@ -1059,9 +1139,9 @@ function listCandidates(brainRoot) {
1059
1139
  const results = [];
1060
1140
  const now = Date.now();
1061
1141
  for (const region of REGIONS) {
1062
- const candidateRoot = join11(brainRoot, region, CANDIDATE_SEGMENT);
1142
+ const candidateRoot = join12(brainRoot, region, CANDIDATE_SEGMENT);
1063
1143
  walkNeuronDirs(candidateRoot, (neuronDir) => {
1064
- const rel = relative3(join11(brainRoot, region), neuronDir);
1144
+ const rel = relative3(join12(brainRoot, region), neuronDir);
1065
1145
  const candidatePath = `${region}/${rel}`;
1066
1146
  const targetPath = fromCandidatePath(candidatePath);
1067
1147
  const counter = readCounter(neuronDir);
@@ -1073,9 +1153,9 @@ function listCandidates(brainRoot) {
1073
1153
  return results;
1074
1154
  }
1075
1155
  function walkNeuronDirs(dir, cb) {
1076
- if (!existsSync10(dir)) return;
1156
+ if (!existsSync11(dir)) return;
1077
1157
  try {
1078
- const entries = readdirSync7(dir, { withFileTypes: true });
1158
+ const entries = readdirSync8(dir, { withFileTypes: true });
1079
1159
  const hasNeuron = entries.some((e) => e.isFile() && e.name.endsWith(".neuron"));
1080
1160
  if (hasNeuron) {
1081
1161
  cb(dir);
@@ -1083,7 +1163,7 @@ function walkNeuronDirs(dir, cb) {
1083
1163
  }
1084
1164
  for (const entry of entries) {
1085
1165
  if (entry.isDirectory() && !entry.name.startsWith(".")) {
1086
- walkNeuronDirs(join11(dir, entry.name), cb);
1166
+ walkNeuronDirs(join12(dir, entry.name), cb);
1087
1167
  }
1088
1168
  }
1089
1169
  } catch {
@@ -1091,75 +1171,32 @@ function walkNeuronDirs(dir, cb) {
1091
1171
  }
1092
1172
  function readCounter(dir) {
1093
1173
  try {
1094
- const files = readdirSync7(dir).filter((f) => /^\d+\.neuron$/.test(f));
1174
+ const files = readdirSync8(dir).filter((f) => /^\d+\.neuron$/.test(f));
1095
1175
  if (files.length === 0) return 0;
1096
1176
  return Math.max(...files.map((f) => parseInt(f, 10)));
1097
1177
  } catch {
1098
1178
  return 0;
1099
1179
  }
1100
1180
  }
1101
-
1102
- // src/episode.ts
1103
- import { readdirSync as readdirSync8, readFileSync as readFileSync4, writeFileSync as writeFileSync8, mkdirSync as mkdirSync6, existsSync as existsSync11 } from "fs";
1104
- import { join as join12 } from "path";
1105
- var MAX_EPISODES = 100;
1106
- var SESSION_LOG_DIR = "hippocampus/session_log";
1107
- function logEpisode(brainRoot, type, path, detail, extra) {
1108
- const logDir = join12(brainRoot, SESSION_LOG_DIR);
1109
- if (!existsSync11(logDir)) {
1110
- mkdirSync6(logDir, { recursive: true });
1111
- }
1112
- const nextSlot = getNextSlot(logDir);
1113
- const episode = {
1114
- ts: (/* @__PURE__ */ new Date()).toISOString(),
1115
- type,
1116
- path,
1117
- detail,
1118
- ...extra?.outcome ? { outcome: extra.outcome } : {},
1119
- ...extra?.neurons ? { neurons: extra.neurons } : {}
1120
- };
1121
- writeFileSync8(
1122
- join12(logDir, `memory${nextSlot}.neuron`),
1123
- JSON.stringify(episode),
1124
- "utf8"
1125
- );
1126
- }
1127
- function readEpisodes(brainRoot) {
1128
- const logDir = join12(brainRoot, SESSION_LOG_DIR);
1129
- if (!existsSync11(logDir)) return [];
1130
- const episodes = [];
1131
- let entries;
1181
+ function propagateToShared(brainRoot, targetPath) {
1132
1182
  try {
1133
- entries = readdirSync8(logDir);
1134
- } catch {
1135
- return [];
1136
- }
1137
- for (const entry of entries) {
1138
- if (!entry.startsWith("memory") || !entry.endsWith(".neuron")) continue;
1139
- try {
1140
- const content = readFileSync4(join12(logDir, entry), "utf8");
1141
- if (content.trim()) {
1142
- episodes.push(JSON.parse(content));
1143
- }
1144
- } catch {
1145
- }
1146
- }
1147
- episodes.sort((a, b) => a.ts.localeCompare(b.ts));
1148
- return episodes;
1149
- }
1150
- function getNextSlot(logDir) {
1151
- let maxSlot = 0;
1152
- try {
1153
- for (const entry of readdirSync8(logDir)) {
1154
- if (entry.startsWith("memory") && entry.endsWith(".neuron")) {
1155
- const n = parseInt(entry.replace("memory", "").replace(".neuron", ""), 10);
1156
- if (!isNaN(n) && n > maxSlot) maxSlot = n;
1157
- }
1158
- }
1183
+ const agentsIdx = brainRoot.indexOf("/agents/");
1184
+ if (agentsIdx === -1) return false;
1185
+ const multiBrainRoot = brainRoot.slice(0, agentsIdx);
1186
+ const sharedRoot = join12(multiBrainRoot, "shared");
1187
+ if (!existsSync11(sharedRoot)) return false;
1188
+ const episodes = readEpisodes(brainRoot);
1189
+ const neuronName = targetPath.split("/").pop() || "";
1190
+ const hasRelevantEpisode = episodes.some(
1191
+ (ep) => PROPAGATION_EPISODE_TYPES.includes(ep.type) && (ep.path.includes(neuronName) || ep.detail.includes(neuronName))
1192
+ );
1193
+ if (!hasRelevantEpisode) return false;
1194
+ growNeuron(sharedRoot, targetPath);
1195
+ console.log(` \u{1F4E1} propagated to shared: ${targetPath}`);
1196
+ return true;
1159
1197
  } catch {
1198
+ return false;
1160
1199
  }
1161
- const next = maxSlot + 1;
1162
- return next > MAX_EPISODES ? maxSlot % MAX_EPISODES + 1 : next;
1163
1200
  }
1164
1201
 
1165
1202
  // src/inbox.ts
@@ -1777,12 +1814,21 @@ function digestTranscript(brainRoot, transcriptPath, sessionId) {
1777
1814
  const resolvedSessionId = sessionId || basename(transcriptPath, ".jsonl");
1778
1815
  const logDir = join15(brainRoot, DIGEST_LOG_DIR);
1779
1816
  const logPath = join15(logDir, `${resolvedSessionId}.jsonl`);
1780
- if (existsSync14(logPath)) {
1817
+ const content = readFileSync7(transcriptPath, "utf8");
1818
+ const allLines = content.split("\n").filter(Boolean);
1819
+ const totalLines = allLines.length;
1820
+ const meta = readAuditMeta(logPath);
1821
+ if (existsSync14(logPath) && !meta) {
1781
1822
  console.log(`\u23ED already digested session ${resolvedSessionId}, skip`);
1782
1823
  return { corrections: 0, skipped: 0, toolFailures: 0, transcriptPath, sessionId: resolvedSessionId };
1783
1824
  }
1784
- const messages = parseTranscript(transcriptPath);
1785
- const toolFailures = parseToolResults(transcriptPath);
1825
+ const skipLines = meta ? meta.lineCount : 0;
1826
+ if (skipLines >= totalLines) {
1827
+ return { corrections: 0, skipped: 0, toolFailures: 0, transcriptPath, sessionId: resolvedSessionId };
1828
+ }
1829
+ const newLines = allLines.slice(skipLines);
1830
+ const messages = parseTranscriptFromLines(newLines);
1831
+ const toolFailures = parseToolResultsFromLines(newLines);
1786
1832
  for (const failure of toolFailures) {
1787
1833
  logEpisode(brainRoot, "tool-failure", failure.toolName, failure.errorText);
1788
1834
  }
@@ -1797,11 +1843,11 @@ function digestTranscript(brainRoot, transcriptPath, sessionId) {
1797
1843
  const corrections = extractCorrections(messages);
1798
1844
  if (corrections.length === 0 && toolFailures.length === 0) {
1799
1845
  console.log(`\u{1F4DD} digest: no corrections found in session ${resolvedSessionId}`);
1800
- writeAuditLog(brainRoot, resolvedSessionId, []);
1846
+ writeAuditLog(brainRoot, resolvedSessionId, [], totalLines);
1801
1847
  return { corrections: 0, skipped: messages.length, toolFailures: toolFailures.length, transcriptPath, sessionId: resolvedSessionId };
1802
1848
  }
1803
1849
  if (corrections.length === 0) {
1804
- writeAuditLog(brainRoot, resolvedSessionId, []);
1850
+ writeAuditLog(brainRoot, resolvedSessionId, [], totalLines);
1805
1851
  return { corrections: 0, skipped: messages.length, toolFailures: toolFailures.length, transcriptPath, sessionId: resolvedSessionId };
1806
1852
  }
1807
1853
  let applied = 0;
@@ -1817,7 +1863,7 @@ function digestTranscript(brainRoot, transcriptPath, sessionId) {
1817
1863
  auditEntries.push({ correction, applied: false });
1818
1864
  }
1819
1865
  }
1820
- writeAuditLog(brainRoot, resolvedSessionId, auditEntries);
1866
+ writeAuditLog(brainRoot, resolvedSessionId, auditEntries, totalLines);
1821
1867
  console.log(`\u{1F4DD} digest: ${applied} correction(s) from session ${resolvedSessionId}`);
1822
1868
  return {
1823
1869
  corrections: applied,
@@ -1827,9 +1873,7 @@ function digestTranscript(brainRoot, transcriptPath, sessionId) {
1827
1873
  sessionId: resolvedSessionId
1828
1874
  };
1829
1875
  }
1830
- function parseTranscript(transcriptPath) {
1831
- const content = readFileSync7(transcriptPath, "utf8");
1832
- const lines = content.split("\n").filter(Boolean);
1876
+ function parseTranscriptFromLines(lines) {
1833
1877
  const messages = [];
1834
1878
  for (const line of lines) {
1835
1879
  let entry;
@@ -1855,9 +1899,19 @@ function extractText(content) {
1855
1899
  return null;
1856
1900
  }
1857
1901
  var MAX_FAILURES_PER_SESSION = 20;
1902
+ var SOFT_ERROR_PATTERNS = [
1903
+ /(?:^|\n)\S*(?:\(\w+\):\d+: )?command not found:/m,
1904
+ // shell: command not found
1905
+ /(?:^|\n)npm error\b/m,
1906
+ // npm error (not npm warn)
1907
+ /(?:^|\n)fatal: /m
1908
+ // git fatal
1909
+ ];
1858
1910
  function parseToolResults(transcriptPath) {
1859
1911
  const content = readFileSync7(transcriptPath, "utf8");
1860
- const lines = content.split("\n").filter(Boolean);
1912
+ return parseToolResultsFromLines(content.split("\n").filter(Boolean));
1913
+ }
1914
+ function parseToolResultsFromLines(lines) {
1861
1915
  const failures = [];
1862
1916
  for (const line of lines) {
1863
1917
  if (failures.length >= MAX_FAILURES_PER_SESSION) break;
@@ -1871,9 +1925,13 @@ function parseToolResults(transcriptPath) {
1871
1925
  if (!entry.message || !Array.isArray(entry.message.content)) continue;
1872
1926
  for (const block of entry.message.content) {
1873
1927
  if (block.type !== "tool_result") continue;
1874
- if (!block.is_error) continue;
1875
- const failure = detectToolFailure(block, entry.toolUseResult);
1876
- if (failure) failures.push(failure);
1928
+ if (block.is_error) {
1929
+ const failure = detectToolFailure(block, entry.toolUseResult);
1930
+ if (failure) failures.push(failure);
1931
+ } else {
1932
+ const failure = detectSoftFailure(block, entry.toolUseResult);
1933
+ if (failure) failures.push(failure);
1934
+ }
1877
1935
  }
1878
1936
  }
1879
1937
  return failures;
@@ -1912,6 +1970,30 @@ function detectToolFailure(block, toolUseResult) {
1912
1970
  const toolName = firstLine.trim().slice(0, 80);
1913
1971
  return { toolName, exitCode, errorText: errorText.slice(0, 500) };
1914
1972
  }
1973
+ function detectSoftFailure(block, toolUseResult) {
1974
+ let text = "";
1975
+ if (typeof block.content === "string") {
1976
+ text = block.content;
1977
+ } else if (Array.isArray(block.content)) {
1978
+ text = block.content.filter((b) => b.type === "text" && b.text).map((b) => b.text).join("\n");
1979
+ }
1980
+ if (toolUseResult && typeof toolUseResult === "object") {
1981
+ if (toolUseResult.stderr) text += "\n" + toolUseResult.stderr;
1982
+ }
1983
+ if (!text) return null;
1984
+ for (const pattern of SOFT_ERROR_PATTERNS) {
1985
+ const match = text.match(pattern);
1986
+ if (match) {
1987
+ const matchedLine = text.split("\n").find((l) => pattern.test(l)) || "unknown";
1988
+ return {
1989
+ toolName: `[soft] ${matchedLine.trim().slice(0, 70)}`,
1990
+ exitCode: 0,
1991
+ errorText: text.slice(0, 500)
1992
+ };
1993
+ }
1994
+ }
1995
+ return null;
1996
+ }
1915
1997
  function extractCorrections(messages) {
1916
1998
  const corrections = [];
1917
1999
  for (const text of messages) {
@@ -2086,13 +2168,28 @@ function extractKeywords(text) {
2086
2168
  ]);
2087
2169
  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));
2088
2170
  }
2089
- function writeAuditLog(brainRoot, sessionId, entries) {
2171
+ function readAuditMeta(logPath) {
2172
+ if (!existsSync14(logPath)) return null;
2173
+ try {
2174
+ const content = readFileSync7(logPath, "utf8");
2175
+ const firstLine = content.split("\n")[0];
2176
+ if (!firstLine) return null;
2177
+ const parsed = JSON.parse(firstLine);
2178
+ if (parsed._meta && typeof parsed.lineCount === "number") {
2179
+ return { lineCount: parsed.lineCount };
2180
+ }
2181
+ } catch {
2182
+ }
2183
+ return null;
2184
+ }
2185
+ function writeAuditLog(brainRoot, sessionId, entries, lineCount) {
2090
2186
  const logDir = join15(brainRoot, DIGEST_LOG_DIR);
2091
2187
  if (!existsSync14(logDir)) {
2092
2188
  mkdirSync9(logDir, { recursive: true });
2093
2189
  }
2094
2190
  const logPath = join15(logDir, `${sessionId}.jsonl`);
2095
- const lines = entries.map(
2191
+ const metaLine = JSON.stringify({ _meta: true, lineCount, ts: (/* @__PURE__ */ new Date()).toISOString() });
2192
+ const entryLines = entries.map(
2096
2193
  (e) => JSON.stringify({
2097
2194
  ts: (/* @__PURE__ */ new Date()).toISOString(),
2098
2195
  path: e.correction.path,
@@ -2102,7 +2199,7 @@ function writeAuditLog(brainRoot, sessionId, entries) {
2102
2199
  applied: e.applied
2103
2200
  })
2104
2201
  );
2105
- writeFileSync11(logPath, lines.join("\n") + (lines.length > 0 ? "\n" : ""), "utf8");
2202
+ writeFileSync11(logPath, [metaLine, ...entryLines].join("\n") + "\n", "utf8");
2106
2203
  }
2107
2204
 
2108
2205
  // src/evolve.ts
@@ -2566,6 +2663,7 @@ function validateActions(actions, _brain) {
2566
2663
  return false;
2567
2664
  }
2568
2665
  const region = action.path.split("/")[0];
2666
+ if (region === SKILLS_DIR) return true;
2569
2667
  if (!region || PROTECTED_REGIONS.includes(region)) {
2570
2668
  console.log(` \u{1F6E1}\uFE0F blocked: ${action.type} ${action.path} (protected region)`);
2571
2669
  return false;
@@ -2590,7 +2688,11 @@ function executeActions(brainRoot, actions) {
2590
2688
  fireNeuron(brainRoot, action.path);
2591
2689
  break;
2592
2690
  case "grow":
2593
- growCandidate(brainRoot, action.path);
2691
+ if (action.path.startsWith(SKILLS_DIR + "/")) {
2692
+ growNeuron(brainRoot, action.path);
2693
+ } else {
2694
+ growCandidate(brainRoot, action.path);
2695
+ }
2594
2696
  break;
2595
2697
  case "signal":
2596
2698
  signalNeuron(brainRoot, action.path, action.signal || "dopamine");
@@ -2626,6 +2728,226 @@ function actionIcon(type) {
2626
2728
  return "\u2753";
2627
2729
  }
2628
2730
  }
2731
+
2732
+ // src/cron.ts
2733
+ import { writeFileSync as writeFileSync14, existsSync as existsSync17, unlinkSync } from "fs";
2734
+ import { join as join18 } from "path";
2735
+ import { execSync as execSync4 } from "child_process";
2736
+ var PLIST_LABEL = "com.hebbian.nightly-prune";
2737
+ var FEEDBACK_PLIST_LABEL = "com.hebbian.feedback";
2738
+ function getLaunchAgentsDir() {
2739
+ return join18(process.env.HOME || "~", "Library", "LaunchAgents");
2740
+ }
2741
+ function getPlistPath(label) {
2742
+ return join18(getLaunchAgentsDir(), `${label}.plist`);
2743
+ }
2744
+ function getNpxPath() {
2745
+ try {
2746
+ return execSync4("which npx", { encoding: "utf8" }).trim();
2747
+ } catch {
2748
+ return "/opt/homebrew/bin/npx";
2749
+ }
2750
+ }
2751
+ function generatePrunePlist(brainRoot, hour = 2, minute = 0) {
2752
+ const npx = getNpxPath();
2753
+ const apiKey = process.env.GEMINI_API_KEY || "";
2754
+ const home = process.env.HOME || "/Users/sweetheart";
2755
+ return `<?xml version="1.0" encoding="UTF-8"?>
2756
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
2757
+ <plist version="1.0">
2758
+ <dict>
2759
+ <key>Label</key>
2760
+ <string>${PLIST_LABEL}</string>
2761
+ <key>ProgramArguments</key>
2762
+ <array>
2763
+ <string>${npx}</string>
2764
+ <string>hebbian</string>
2765
+ <string>evolve</string>
2766
+ <string>prune</string>
2767
+ <string>--brain</string>
2768
+ <string>${brainRoot}</string>
2769
+ </array>
2770
+ <key>EnvironmentVariables</key>
2771
+ <dict>
2772
+ <key>GEMINI_API_KEY</key>
2773
+ <string>${apiKey}</string>
2774
+ <key>PATH</key>
2775
+ <string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
2776
+ </dict>
2777
+ <key>StartCalendarInterval</key>
2778
+ <dict>
2779
+ <key>Hour</key>
2780
+ <integer>${hour}</integer>
2781
+ <key>Minute</key>
2782
+ <integer>${minute}</integer>
2783
+ </dict>
2784
+ <key>StandardOutPath</key>
2785
+ <string>${home}/Library/Logs/hebbian-prune.log</string>
2786
+ <key>StandardErrorPath</key>
2787
+ <string>${home}/Library/Logs/hebbian-prune.log</string>
2788
+ </dict>
2789
+ </plist>`;
2790
+ }
2791
+ function generateFeedbackPlist(brainRoot, intervalMinutes = 15) {
2792
+ const npx = getNpxPath();
2793
+ const home = process.env.HOME || "/Users/sweetheart";
2794
+ return `<?xml version="1.0" encoding="UTF-8"?>
2795
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
2796
+ <plist version="1.0">
2797
+ <dict>
2798
+ <key>Label</key>
2799
+ <string>${FEEDBACK_PLIST_LABEL}</string>
2800
+ <key>ProgramArguments</key>
2801
+ <array>
2802
+ <string>${npx}</string>
2803
+ <string>hebbian</string>
2804
+ <string>feedback</string>
2805
+ <string>scan</string>
2806
+ <string>--brain</string>
2807
+ <string>${brainRoot}</string>
2808
+ </array>
2809
+ <key>EnvironmentVariables</key>
2810
+ <dict>
2811
+ <key>PATH</key>
2812
+ <string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
2813
+ </dict>
2814
+ <key>StartInterval</key>
2815
+ <integer>${intervalMinutes * 60}</integer>
2816
+ <key>StandardOutPath</key>
2817
+ <string>${home}/Library/Logs/hebbian-feedback.log</string>
2818
+ <key>StandardErrorPath</key>
2819
+ <string>${home}/Library/Logs/hebbian-feedback.log</string>
2820
+ </dict>
2821
+ </plist>`;
2822
+ }
2823
+ function installCron(brainRoot, type = "prune") {
2824
+ const label = type === "prune" ? PLIST_LABEL : FEEDBACK_PLIST_LABEL;
2825
+ const plistPath = getPlistPath(label);
2826
+ const plistContent = type === "prune" ? generatePrunePlist(brainRoot) : generateFeedbackPlist(brainRoot);
2827
+ try {
2828
+ execSync4(`launchctl unload ${plistPath} 2>/dev/null`, { encoding: "utf8" });
2829
+ } catch {
2830
+ }
2831
+ writeFileSync14(plistPath, plistContent, "utf8");
2832
+ execSync4(`launchctl load ${plistPath}`, { encoding: "utf8" });
2833
+ console.log(`\u2705 ${type} cron installed: ${plistPath}`);
2834
+ }
2835
+ function uninstallCron(type = "prune") {
2836
+ const label = type === "prune" ? PLIST_LABEL : FEEDBACK_PLIST_LABEL;
2837
+ const plistPath = getPlistPath(label);
2838
+ if (!existsSync17(plistPath)) {
2839
+ console.log(`\u26A0\uFE0F ${type} cron not installed`);
2840
+ return;
2841
+ }
2842
+ try {
2843
+ execSync4(`launchctl unload ${plistPath}`, { encoding: "utf8" });
2844
+ } catch {
2845
+ }
2846
+ unlinkSync(plistPath);
2847
+ console.log(`\u{1F5D1}\uFE0F ${type} cron uninstalled`);
2848
+ }
2849
+ function checkCron(type = "prune") {
2850
+ const label = type === "prune" ? PLIST_LABEL : FEEDBACK_PLIST_LABEL;
2851
+ const plistPath = getPlistPath(label);
2852
+ return { installed: existsSync17(plistPath), path: plistPath };
2853
+ }
2854
+
2855
+ // src/feedback.ts
2856
+ import { existsSync as existsSync18, readdirSync as readdirSync11, statSync as statSync5, readFileSync as readFileSync10, writeFileSync as writeFileSync15, mkdirSync as mkdirSync11 } from "fs";
2857
+ import { join as join19 } from "path";
2858
+ var WATERMARK_FILE = "_feedback_watermark.json";
2859
+ var WARN_PREFIX = "WARN_shared_";
2860
+ function scanSharedBrain(brainRoot) {
2861
+ const sharedRoot = join19(brainRoot, SHARED_DIR);
2862
+ if (!existsSync18(sharedRoot)) return [];
2863
+ const watermark = readWatermark(sharedRoot);
2864
+ const deltas = [];
2865
+ for (const region of REGIONS) {
2866
+ const regionPath = join19(sharedRoot, region);
2867
+ if (!existsSync18(regionPath)) continue;
2868
+ walkForNeurons(regionPath, regionPath, (neuronDir, counter) => {
2869
+ const modTime = statSync5(neuronDir).mtime;
2870
+ if (modTime.getTime() <= watermark) return;
2871
+ const name = neuronDir.split("/").pop() || "";
2872
+ if (name.startsWith(WARN_PREFIX)) return;
2873
+ const relPath = region + "/" + neuronDir.slice(regionPath.length + 1);
2874
+ deltas.push({ path: relPath, counter, modTime });
2875
+ });
2876
+ }
2877
+ return deltas;
2878
+ }
2879
+ function propagateToAgents(brainRoot, deltas) {
2880
+ const agentsDir = join19(brainRoot, AGENTS_DIR);
2881
+ if (!existsSync18(agentsDir) || deltas.length === 0) {
2882
+ return { scanned: deltas.length, propagated: 0, agents: [] };
2883
+ }
2884
+ const agentNames = readdirSync11(agentsDir, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".") && !e.name.startsWith("_")).map((e) => e.name);
2885
+ let propagated = 0;
2886
+ const touchedAgents = /* @__PURE__ */ new Set();
2887
+ for (const delta of deltas) {
2888
+ const neuronName = delta.path.split("/").pop() || "";
2889
+ const warnPath = delta.path.replace(/\/([^/]+)$/, `/${WARN_PREFIX}${neuronName}`);
2890
+ for (const agent of agentNames) {
2891
+ const agentBrain = join19(agentsDir, agent);
2892
+ if (!existsSync18(join19(agentBrain, "cortex")) && !existsSync18(join19(agentBrain, "brainstem"))) continue;
2893
+ try {
2894
+ growNeuron(agentBrain, warnPath);
2895
+ logEpisode(agentBrain, "feedback", warnPath, `shared learning: ${delta.path}`);
2896
+ propagated++;
2897
+ touchedAgents.add(agent);
2898
+ } catch {
2899
+ }
2900
+ }
2901
+ }
2902
+ return { scanned: deltas.length, propagated, agents: [...touchedAgents] };
2903
+ }
2904
+ function runFeedback(brainRoot) {
2905
+ const deltas = scanSharedBrain(brainRoot);
2906
+ if (deltas.length === 0) {
2907
+ console.log("\u{1F4E1} feedback: no new shared neurons");
2908
+ return { scanned: 0, propagated: 0, agents: [] };
2909
+ }
2910
+ const result = propagateToAgents(brainRoot, deltas);
2911
+ const latestTime = Math.max(...deltas.map((d) => d.modTime.getTime()));
2912
+ writeWatermark(join19(brainRoot, SHARED_DIR), latestTime);
2913
+ console.log(`\u{1F4E1} feedback: ${result.scanned} shared neuron(s) \u2192 ${result.propagated} warning(s) to ${result.agents.join(", ")}`);
2914
+ return result;
2915
+ }
2916
+ function readWatermark(sharedRoot) {
2917
+ const wmPath = join19(sharedRoot, WATERMARK_FILE);
2918
+ if (!existsSync18(wmPath)) return 0;
2919
+ try {
2920
+ const data = JSON.parse(readFileSync10(wmPath, "utf8"));
2921
+ return data.timestamp || 0;
2922
+ } catch {
2923
+ return 0;
2924
+ }
2925
+ }
2926
+ function writeWatermark(sharedRoot, timestamp) {
2927
+ const wmPath = join19(sharedRoot, WATERMARK_FILE);
2928
+ mkdirSync11(sharedRoot, { recursive: true });
2929
+ writeFileSync15(wmPath, JSON.stringify({ timestamp, ts: new Date(timestamp).toISOString() }), "utf8");
2930
+ }
2931
+ function walkForNeurons(dir, regionRoot, cb) {
2932
+ let entries;
2933
+ try {
2934
+ entries = readdirSync11(dir, { withFileTypes: true });
2935
+ } catch {
2936
+ return;
2937
+ }
2938
+ const neuronFiles = entries.filter((e) => e.isFile() && /^\d+\.neuron$/.test(e.name));
2939
+ if (neuronFiles.length > 0) {
2940
+ const counter = Math.max(...neuronFiles.map((f) => parseInt(f.name, 10)));
2941
+ cb(dir, counter);
2942
+ return;
2943
+ }
2944
+ for (const entry of entries) {
2945
+ if (entry.name.startsWith("_") || entry.name.startsWith(".")) continue;
2946
+ if (entry.isDirectory()) {
2947
+ walkForNeurons(join19(dir, entry.name), regionRoot, cb);
2948
+ }
2949
+ }
2950
+ }
2629
2951
  export {
2630
2952
  AGENTS_DIR,
2631
2953
  DECAY_DAYS,
@@ -2640,6 +2962,7 @@ export {
2640
2962
  MAX_DEPTH,
2641
2963
  MIN_CORRECTION_LENGTH,
2642
2964
  OUTCOME_TYPES,
2965
+ PROPAGATION_EPISODE_TYPES,
2643
2966
  PROTECTED_REGIONS_CONTRA,
2644
2967
  REGIONS,
2645
2968
  REGION_ICONS,
@@ -2648,10 +2971,12 @@ export {
2648
2971
  SESSION_STATE_DIR,
2649
2972
  SHARED_DIR,
2650
2973
  SIGNAL_TYPES,
2974
+ SKILLS_DIR,
2651
2975
  SPOTLIGHT_DAYS,
2652
2976
  appendCorrection,
2653
2977
  buildOutcomeSummary,
2654
2978
  captureSessionStart,
2979
+ checkCron,
2655
2980
  checkHooks,
2656
2981
  classifyOutcome,
2657
2982
  clearReports,
@@ -2668,6 +2993,8 @@ export {
2668
2993
  extractCorrections,
2669
2994
  fireNeuron,
2670
2995
  fromCandidatePath,
2996
+ generateFeedbackPlist,
2997
+ generatePrunePlist,
2671
2998
  getCurrentCounter,
2672
2999
  getLastActivity,
2673
3000
  getPendingReports,
@@ -2675,6 +3002,7 @@ export {
2675
3002
  growCandidate,
2676
3003
  growNeuron,
2677
3004
  initBrain,
3005
+ installCron,
2678
3006
  installHooks,
2679
3007
  jaccardSimilarity,
2680
3008
  listCandidates,
@@ -2683,6 +3011,8 @@ export {
2683
3011
  printDiag,
2684
3012
  processInbox,
2685
3013
  promoteCandidates,
3014
+ propagateToAgents,
3015
+ propagateToShared,
2686
3016
  readEpisodes,
2687
3017
  readHookInput,
2688
3018
  resolveAgentBrain,
@@ -2692,14 +3022,18 @@ export {
2692
3022
  runDecay,
2693
3023
  runDedup,
2694
3024
  runEvolve,
3025
+ runFeedback,
2695
3026
  runSubsumption,
2696
3027
  scanBrain,
3028
+ scanSharedBrain,
3029
+ scanSkills,
2697
3030
  signalNeuron,
2698
3031
  startAPI,
2699
3032
  startWatch,
2700
3033
  stem,
2701
3034
  toCandidatePath,
2702
3035
  tokenize,
3036
+ uninstallCron,
2703
3037
  uninstallHooks,
2704
3038
  writeAllTiers
2705
3039
  };