autonomous-flow-daemon 1.1.0 → 1.6.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.
Files changed (55) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.ko.md +124 -164
  3. package/README.md +99 -170
  4. package/package.json +11 -5
  5. package/src/adapters/index.ts +246 -35
  6. package/src/cli.ts +71 -1
  7. package/src/commands/benchmark.ts +187 -0
  8. package/src/commands/diagnose.ts +56 -14
  9. package/src/commands/doctor.ts +243 -0
  10. package/src/commands/evolution.ts +107 -0
  11. package/src/commands/fix.ts +22 -2
  12. package/src/commands/hooks.ts +136 -0
  13. package/src/commands/mcp.ts +129 -0
  14. package/src/commands/restart.ts +14 -0
  15. package/src/commands/score.ts +164 -96
  16. package/src/commands/start.ts +74 -15
  17. package/src/commands/stats.ts +103 -0
  18. package/src/commands/status.ts +157 -0
  19. package/src/commands/stop.ts +23 -4
  20. package/src/commands/sync.ts +253 -20
  21. package/src/commands/vaccine.ts +177 -0
  22. package/src/constants.ts +25 -1
  23. package/src/core/boast.ts +27 -12
  24. package/src/core/db.ts +74 -3
  25. package/src/core/evolution.ts +215 -0
  26. package/src/core/hologram/engine.ts +71 -0
  27. package/src/core/hologram/fallback.ts +11 -0
  28. package/src/core/hologram/incremental.ts +227 -0
  29. package/src/core/hologram/py-extractor.ts +132 -0
  30. package/src/core/hologram/ts-extractor.ts +320 -0
  31. package/src/core/hologram/types.ts +25 -0
  32. package/src/core/hologram.ts +64 -236
  33. package/src/core/hook-manager.ts +259 -0
  34. package/src/core/i18n/messages.ts +43 -0
  35. package/src/core/immune.ts +8 -123
  36. package/src/core/log-rotate.ts +33 -0
  37. package/src/core/log-utils.ts +38 -0
  38. package/src/core/lru-map.ts +61 -0
  39. package/src/core/notify.ts +27 -19
  40. package/src/core/rule-engine.ts +287 -0
  41. package/src/core/semantic-diff.ts +432 -0
  42. package/src/core/telemetry.ts +94 -0
  43. package/src/core/vaccine-registry.ts +212 -0
  44. package/src/core/workspace.ts +28 -0
  45. package/src/core/yaml-minimal.ts +176 -0
  46. package/src/daemon/client.ts +34 -6
  47. package/src/daemon/event-batcher.ts +108 -0
  48. package/src/daemon/guards.ts +13 -0
  49. package/src/daemon/http-routes.ts +293 -0
  50. package/src/daemon/mcp-handler.ts +270 -0
  51. package/src/daemon/server.ts +439 -353
  52. package/src/daemon/types.ts +100 -0
  53. package/src/daemon/workspace-map.ts +92 -0
  54. package/src/platform.ts +23 -2
  55. package/src/version.ts +15 -0
@@ -0,0 +1,177 @@
1
+ /**
2
+ * afd vaccine — Vaccine registry CLI
3
+ *
4
+ * Sub-commands:
5
+ * afd vaccine list — list available packages
6
+ * afd vaccine search <query> — search packages
7
+ * afd vaccine install <name> — install a vaccine package
8
+ * afd vaccine publish <file> — publish a vaccine package
9
+ * afd vaccine installed — list installed packages
10
+ */
11
+
12
+ import { readFileSync, existsSync } from "fs";
13
+ import {
14
+ publishPackage,
15
+ searchPackages,
16
+ installPackage,
17
+ listInstalled,
18
+ getPackage,
19
+ } from "../core/vaccine-registry";
20
+ import type { VaccinePackage } from "../core/vaccine-registry";
21
+ import { getSystemLanguage } from "../core/locale";
22
+
23
+ const msgs = {
24
+ en: {
25
+ title: "afd vaccine — Registry",
26
+ list: "Available Packages",
27
+ search: "Search Results",
28
+ install: "Install",
29
+ publish: "Publish",
30
+ installed: "Installed Packages",
31
+ noPackages: "No packages found.",
32
+ noResults: "No matching packages.",
33
+ notFound: "Package not found.",
34
+ usage: `Usage:
35
+ afd vaccine list List available packages
36
+ afd vaccine search <query> Search packages
37
+ afd vaccine install <name> Install a vaccine package
38
+ afd vaccine publish <file> Publish from vaccine.json
39
+ afd vaccine installed List installed packages`,
40
+ },
41
+ ko: {
42
+ title: "afd vaccine — 레지스트리",
43
+ list: "사용 가능한 패키지",
44
+ search: "검색 결과",
45
+ install: "설치",
46
+ publish: "발행",
47
+ installed: "설치된 패키지",
48
+ noPackages: "패키지가 없습니다.",
49
+ noResults: "일치하는 패키지가 없습니다.",
50
+ notFound: "패키지를 찾을 수 없습니다.",
51
+ usage: `사용법:
52
+ afd vaccine list 사용 가능한 패키지 목록
53
+ afd vaccine search <query> 패키지 검색
54
+ afd vaccine install <name> 백신 패키지 설치
55
+ afd vaccine publish <file> vaccine.json에서 발행
56
+ afd vaccine installed 설치된 패키지 목록`,
57
+ },
58
+ };
59
+
60
+ const BOX = { tl: "┌", tr: "┐", bl: "└", br: "┘", h: "─", v: "│", ml: "├", mr: "┤" };
61
+ const W = 54;
62
+ function hline(l: string, r: string) { return `${l}${BOX.h.repeat(W)}${r}`; }
63
+ function row(s: string) {
64
+ const pad = Math.max(0, W - 2 - s.length);
65
+ return `${BOX.v} ${s}${" ".repeat(pad)} ${BOX.v}`;
66
+ }
67
+
68
+ export async function vaccineCommand(subcommand?: string, arg?: string) {
69
+ const lang = getSystemLanguage();
70
+ const m = msgs[lang];
71
+
72
+ if (!subcommand) {
73
+ console.log(m.usage);
74
+ return;
75
+ }
76
+
77
+ switch (subcommand) {
78
+ case "list": {
79
+ const packages = searchPackages();
80
+ console.log(hline(BOX.tl, BOX.tr));
81
+ console.log(row(`💉 ${m.title} — ${m.list}`));
82
+ console.log(hline(BOX.ml, BOX.mr));
83
+ if (packages.length === 0) {
84
+ console.log(row(m.noPackages));
85
+ } else {
86
+ for (const p of packages) {
87
+ console.log(row(`📦 ${p.name}@${p.version} (${p.antibodyCount} rules)`));
88
+ console.log(row(` ${p.description}`));
89
+ console.log(row(` ${p.ecosystem} | by ${p.author}`));
90
+ console.log(row(""));
91
+ }
92
+ }
93
+ console.log(hline(BOX.bl, BOX.br));
94
+ break;
95
+ }
96
+
97
+ case "search": {
98
+ const results = searchPackages(arg);
99
+ console.log(hline(BOX.tl, BOX.tr));
100
+ console.log(row(`🔍 ${m.title} — ${m.search}: "${arg ?? ""}"`));
101
+ console.log(hline(BOX.ml, BOX.mr));
102
+ if (results.length === 0) {
103
+ console.log(row(m.noResults));
104
+ } else {
105
+ for (const p of results) {
106
+ console.log(row(`📦 ${p.name}@${p.version} — ${p.description}`));
107
+ }
108
+ }
109
+ console.log(hline(BOX.bl, BOX.br));
110
+ break;
111
+ }
112
+
113
+ case "install": {
114
+ if (!arg) {
115
+ console.error("Usage: afd vaccine install <name>");
116
+ process.exit(1);
117
+ }
118
+ const result = installPackage(arg);
119
+ if (!result.success) {
120
+ console.error(`[afd vaccine] ${result.message}`);
121
+ process.exit(1);
122
+ }
123
+ console.log(hline(BOX.tl, BOX.tr));
124
+ console.log(row(`✅ ${m.install}: ${arg}`));
125
+ console.log(hline(BOX.ml, BOX.mr));
126
+ console.log(row(result.message));
127
+ console.log(hline(BOX.bl, BOX.br));
128
+ break;
129
+ }
130
+
131
+ case "publish": {
132
+ const filePath = arg ?? "vaccine.json";
133
+ if (!existsSync(filePath)) {
134
+ console.error(`[afd vaccine] File not found: ${filePath}`);
135
+ process.exit(1);
136
+ }
137
+ let pkg: VaccinePackage;
138
+ try {
139
+ pkg = JSON.parse(readFileSync(filePath, "utf-8"));
140
+ } catch {
141
+ console.error("[afd vaccine] Invalid JSON in vaccine file.");
142
+ process.exit(1);
143
+ }
144
+ const result = publishPackage(pkg);
145
+ console.log(hline(BOX.tl, BOX.tr));
146
+ console.log(row(`📤 ${m.publish}`));
147
+ console.log(hline(BOX.ml, BOX.mr));
148
+ console.log(row(result.message));
149
+ console.log(hline(BOX.bl, BOX.br));
150
+ break;
151
+ }
152
+
153
+ case "installed": {
154
+ const pkgs = listInstalled();
155
+ console.log(hline(BOX.tl, BOX.tr));
156
+ console.log(row(`📋 ${m.title} — ${m.installed}`));
157
+ console.log(hline(BOX.ml, BOX.mr));
158
+ if (pkgs.length === 0) {
159
+ console.log(row(m.noPackages));
160
+ } else {
161
+ for (const name of pkgs) {
162
+ const pkg = getPackage(name);
163
+ if (pkg) {
164
+ console.log(row(`📦 ${pkg.name}@${pkg.version} (${pkg.antibodies.length} rules)`));
165
+ } else {
166
+ console.log(row(`📦 ${name}`));
167
+ }
168
+ }
169
+ }
170
+ console.log(hline(BOX.bl, BOX.br));
171
+ break;
172
+ }
173
+
174
+ default:
175
+ console.log(m.usage);
176
+ }
177
+ }
package/src/constants.ts CHANGED
@@ -1,8 +1,32 @@
1
1
  import { join } from "path";
2
+ import { findWorkspaceRoot } from "./core/workspace";
2
3
 
4
+ // Relative paths (used when cwd is already the workspace root)
3
5
  export const AFD_DIR = ".afd";
4
6
  export const PID_FILE = join(AFD_DIR, "daemon.pid");
5
7
  export const PORT_FILE = join(AFD_DIR, "daemon.port");
6
8
  export const DB_FILE = join(AFD_DIR, "antibodies.sqlite");
7
9
  export const LOG_FILE = join(AFD_DIR, "daemon.log");
8
- export const WATCH_TARGETS = [".claude/", "CLAUDE.md", ".cursorrules", ".claudeignore", ".gitignore"];
10
+ export const QUARANTINE_DIR = join(AFD_DIR, "quarantine");
11
+ export const WATCH_TARGETS = [
12
+ ".claude/", "CLAUDE.md", ".cursorrules", ".claudeignore", ".gitignore",
13
+ ".windsurfrules", ".windsurf/", "codex.md", ".codex/",
14
+ ".cursorignore", ".windsurfignore", ".codexignore",
15
+ ];
16
+
17
+ /**
18
+ * Resolve all `.afd/` paths against the workspace root.
19
+ * Works correctly even when CLI is invoked from a subdirectory.
20
+ */
21
+ export function resolveWorkspacePaths(from?: string) {
22
+ const root = findWorkspaceRoot(from);
23
+ return {
24
+ root,
25
+ afdDir: join(root, AFD_DIR),
26
+ pidFile: join(root, PID_FILE),
27
+ portFile: join(root, PORT_FILE),
28
+ dbFile: join(root, DB_FILE),
29
+ logFile: join(root, LOG_FILE),
30
+ quarantineDir: join(root, QUARANTINE_DIR),
31
+ };
32
+ }
package/src/core/boast.ts CHANGED
@@ -1,9 +1,7 @@
1
1
  /**
2
2
  * Boastful Doctor — Gamification & Delightful Logging
3
3
  *
4
- * Lightweight value calculations that stay well under the 270ms budget.
5
- * All math is O(1) — no I/O, no async.
6
- * All strings are localized via the i18n dictionary.
4
+ * O(1) math only no I/O, no async. All strings localized via i18n.
7
5
  */
8
6
 
9
7
  import { getSystemLanguage } from "./locale";
@@ -13,13 +11,8 @@ import type { MessageDict } from "./i18n/messages";
13
11
 
14
12
  // ── Token & Cost Estimation ──
15
13
 
16
- /** Rough chars-per-token ratio for code (conservative estimate) */
17
14
  const CHARS_PER_TOKEN = 3.5;
18
-
19
- /** Average cost per 1K input tokens (Claude Sonnet ballpark) */
20
15
  const COST_PER_1K_TOKENS = 0.003;
21
-
22
- /** Estimated minutes a developer spends debugging a missing config */
23
16
  const DEBUG_MINUTES_BASE = 8;
24
17
  const DEBUG_MINUTES_PER_KB = 2;
25
18
 
@@ -50,6 +43,11 @@ export interface ShiftSummary {
50
43
  suppressionsSkipped: number;
51
44
  dormantTransitions: number;
52
45
  boast: string;
46
+ // Unified ROI breakdown
47
+ healTokensSaved: number;
48
+ healCostSaved: number;
49
+ hologramTokensSaved: number;
50
+ hologramCostSaved: number;
53
51
  }
54
52
 
55
53
  /** Build a shift summary from aggregated daemon stats. */
@@ -60,12 +58,24 @@ export function buildShiftSummary(stats: {
60
58
  totalFileBytesSaved: number;
61
59
  suppressionsSkipped: number;
62
60
  dormantTransitions: number;
61
+ hologramSavedChars?: number;
63
62
  }, lang?: SupportedLang): ShiftSummary {
64
63
  const l = lang ?? getSystemLanguage();
65
- const msg = getMessages(l);
66
- const totalTokensSaved = Math.round(stats.totalFileBytesSaved / CHARS_PER_TOKEN);
64
+ const m = getMessages(l);
65
+
66
+ // Auto-Heal ROI
67
+ const healTokensSaved = Math.round(stats.totalFileBytesSaved / CHARS_PER_TOKEN);
68
+ const healCostSaved = Math.round(healTokensSaved / 1000 * COST_PER_1K_TOKENS * 100) / 100;
69
+
70
+ // Hologram ROI
71
+ const holoSavedChars = stats.hologramSavedChars ?? 0;
72
+ const hologramTokensSaved = Math.round(holoSavedChars / CHARS_PER_TOKEN);
73
+ const hologramCostSaved = Math.round(hologramTokensSaved / 1000 * COST_PER_1K_TOKENS * 100) / 100;
74
+
75
+ // Unified totals
76
+ const totalTokensSaved = healTokensSaved + hologramTokensSaved;
67
77
  const totalMinutesSaved = stats.healsPerformed * DEBUG_MINUTES_BASE;
68
- const totalCostSaved = Math.round(totalTokensSaved / 1000 * COST_PER_1K_TOKENS * 100) / 100;
78
+ const totalCostSaved = Math.round((healCostSaved + hologramCostSaved) * 100) / 100;
69
79
 
70
80
  return {
71
81
  uptimeFormatted: formatUptime(stats.uptimeSeconds),
@@ -76,7 +86,11 @@ export function buildShiftSummary(stats: {
76
86
  totalCostSaved,
77
87
  suppressionsSkipped: stats.suppressionsSkipped,
78
88
  dormantTransitions: stats.dormantTransitions,
79
- boast: pick(msg.BOAST_SHIFT_END),
89
+ boast: pick(m.BOAST_SHIFT_END),
90
+ healTokensSaved,
91
+ healCostSaved,
92
+ hologramTokensSaved,
93
+ hologramCostSaved,
80
94
  };
81
95
  }
82
96
 
@@ -180,6 +194,7 @@ export function formatValueSection(s: ShiftSummary, lang?: SupportedLang): strin
180
194
  // ── Helpers ──
181
195
 
182
196
  function pick<T>(arr: T[]): T {
197
+ if (arr.length === 0) return "" as unknown as T;
183
198
  return arr[Math.floor(Math.random() * arr.length)];
184
199
  }
185
200
 
package/src/core/db.ts CHANGED
@@ -1,10 +1,11 @@
1
1
  import { mkdirSync } from "fs";
2
2
  import { Database } from "bun:sqlite";
3
- import { AFD_DIR, DB_FILE } from "../constants";
3
+ import { resolveWorkspacePaths } from "../constants";
4
4
 
5
5
  export function initDb(): Database {
6
- mkdirSync(AFD_DIR, { recursive: true });
7
- const db = new Database(DB_FILE);
6
+ const paths = resolveWorkspacePaths();
7
+ mkdirSync(paths.afdDir, { recursive: true });
8
+ const db = new Database(paths.dbFile);
8
9
  db.exec("PRAGMA journal_mode = WAL");
9
10
 
10
11
  db.exec(`
@@ -42,5 +43,75 @@ export function initDb(): Database {
42
43
  )
43
44
  `);
44
45
 
46
+ // ── Hologram Stats: lifetime (single row) + daily (7-day rolling) ──
47
+ db.exec(`
48
+ CREATE TABLE IF NOT EXISTS hologram_lifetime (
49
+ id INTEGER PRIMARY KEY CHECK (id = 1),
50
+ total_requests INTEGER NOT NULL DEFAULT 0,
51
+ total_original_chars INTEGER NOT NULL DEFAULT 0,
52
+ total_hologram_chars INTEGER NOT NULL DEFAULT 0
53
+ )
54
+ `);
55
+ db.exec(`INSERT OR IGNORE INTO hologram_lifetime (id) VALUES (1)`);
56
+
57
+ db.exec(`
58
+ CREATE TABLE IF NOT EXISTS hologram_daily (
59
+ date TEXT PRIMARY KEY,
60
+ requests INTEGER NOT NULL DEFAULT 0,
61
+ original_chars INTEGER NOT NULL DEFAULT 0,
62
+ hologram_chars INTEGER NOT NULL DEFAULT 0
63
+ )
64
+ `);
65
+ // Purge entries older than 7 days
66
+ db.exec(`DELETE FROM hologram_daily WHERE date < date('now', '-7 days')`);
67
+
68
+ // ── Telemetry: feature usage tracking ──
69
+ db.exec(`
70
+ CREATE TABLE IF NOT EXISTS telemetry (
71
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
72
+ category TEXT NOT NULL,
73
+ action TEXT NOT NULL,
74
+ detail TEXT,
75
+ duration_ms REAL,
76
+ timestamp INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
77
+ )
78
+ `);
79
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_telemetry_cat_ts ON telemetry(category, timestamp)`);
80
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_telemetry_action ON telemetry(action)`);
81
+ // Purge raw telemetry older than 30 days
82
+ db.exec(`DELETE FROM telemetry WHERE timestamp < unixepoch() * 1000 - 30 * 86400000`);
83
+
84
+ // ── Mistake History: passive defense tracking ──
85
+ db.exec(`
86
+ CREATE TABLE IF NOT EXISTS mistake_history (
87
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
88
+ file_path TEXT NOT NULL,
89
+ mistake_type TEXT NOT NULL,
90
+ description TEXT NOT NULL,
91
+ antibody_id TEXT,
92
+ timestamp INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
93
+ )
94
+ `);
95
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_mistake_history_path ON mistake_history(file_path)`);
96
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_mistake_history_ts ON mistake_history(timestamp)`);
97
+ db.exec(`DELETE FROM mistake_history WHERE timestamp < unixepoch() * 1000 - 30 * 86400000`);
98
+
99
+ // Migration: move data from old hologram_stats table if it exists
100
+ try {
101
+ const old = db.prepare("SELECT total_requests, total_original_chars, total_hologram_chars FROM hologram_stats WHERE id = 1").get() as {
102
+ total_requests: number; total_original_chars: number; total_hologram_chars: number;
103
+ } | null;
104
+ if (old && old.total_requests > 0) {
105
+ db.transaction(() => {
106
+ db.prepare(
107
+ "UPDATE hologram_lifetime SET total_requests = ?, total_original_chars = ?, total_hologram_chars = ? WHERE id = 1"
108
+ ).run(old.total_requests, old.total_original_chars, old.total_hologram_chars);
109
+ db.exec("DROP TABLE hologram_stats");
110
+ })();
111
+ }
112
+ } catch {
113
+ // Old table doesn't exist — clean install
114
+ }
115
+
45
116
  return db;
46
117
  }
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Self-Evolution Engine
3
+ *
4
+ * Analyzes quarantined (corrupted) files against their restored originals,
5
+ * generates failure lessons, and writes them to afd-lessons.md so AI agents
6
+ * learn from their own mistakes.
7
+ */
8
+
9
+ import { readdirSync, readFileSync, writeFileSync, renameSync, existsSync } from "fs";
10
+ import { resolve, basename } from "path";
11
+ import { QUARANTINE_DIR } from "../constants";
12
+ import { lineDiff } from "./log-utils";
13
+
14
+ const LESSONS_FILE = "afd-lessons.md";
15
+ const LEARNED_SUFFIX = ".learned";
16
+
17
+ export interface QuarantineEntry {
18
+ /** Full path inside .afd/quarantine/ */
19
+ quarantinePath: string;
20
+ /** Original file path (reconstructed from quarantine filename) */
21
+ originalPath: string;
22
+ /** Timestamp string extracted from filename */
23
+ timestamp: string;
24
+ /** Whether this entry has already been learned */
25
+ learned: boolean;
26
+ }
27
+
28
+ export interface EvolutionLesson {
29
+ entry: QuarantineEntry;
30
+ diff: string[];
31
+ corruptedContent: string;
32
+ restoredContent: string | null;
33
+ failureType: "corruption" | "deletion";
34
+ suggestion: string;
35
+ }
36
+
37
+ export interface EvolutionStats {
38
+ totalQuarantined: number;
39
+ totalLearned: number;
40
+ pending: number;
41
+ lessons: EvolutionLesson[];
42
+ }
43
+
44
+ /** Parse quarantine filename: YYYYMMDD_HHMMSS_originalname → { timestamp, originalPath } */
45
+ function parseQuarantineName(filename: string): { timestamp: string; originalPath: string } | null {
46
+ // e.g. 20260401_021028_.claude_hooks.json or 20260401_020741_.claudeignore
47
+ const match = filename.replace(LEARNED_SUFFIX, "").match(/^(\d{8}_\d{6})_(.+)$/);
48
+ if (!match) return null;
49
+ const ts = match[1];
50
+ // Reconstruct path: underscores that were separators become path separators
51
+ // Heuristic: first underscore-delimited segment starting with "." is likely a directory
52
+ const rawName = match[2];
53
+ // Reverse the flatten: `.claude_hooks.json` → `.claude/hooks.json`
54
+ const originalPath = rawName.replace(/^\.([^.]+)_/, ".$1/");
55
+ return { timestamp: ts, originalPath };
56
+ }
57
+
58
+ /** List all quarantined entries */
59
+ export function listQuarantine(): QuarantineEntry[] {
60
+ if (!existsSync(QUARANTINE_DIR)) return [];
61
+ const files = readdirSync(QUARANTINE_DIR);
62
+ const entries: QuarantineEntry[] = [];
63
+
64
+ for (const file of files) {
65
+ const isLearned = file.endsWith(LEARNED_SUFFIX);
66
+ const cleanName = file.replace(LEARNED_SUFFIX, "");
67
+ const parsed = parseQuarantineName(cleanName);
68
+ if (!parsed) continue;
69
+ entries.push({
70
+ quarantinePath: resolve(QUARANTINE_DIR, file),
71
+ originalPath: parsed.originalPath,
72
+ timestamp: parsed.timestamp,
73
+ learned: isLearned,
74
+ });
75
+ }
76
+
77
+ return entries.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
78
+ }
79
+
80
+ /** Detect what type of corruption occurred */
81
+ function detectFailureType(content: string): "corruption" | "deletion" {
82
+ return content.trim() === "DELETED" ? "deletion" : "corruption";
83
+ }
84
+
85
+ /** Generate a human-readable suggestion based on the failure */
86
+ function generateSuggestion(entry: QuarantineEntry, failureType: string, corruptedContent: string, restoredContent: string | null): string {
87
+ const file = entry.originalPath;
88
+
89
+ if (failureType === "deletion") {
90
+ return `Do NOT delete \`${file}\`. This is an immune-critical file protected by afd. ` +
91
+ `If you need to modify it, edit the content instead of removing the file.`;
92
+ }
93
+
94
+ // Corruption analysis
95
+ if (file.endsWith(".json")) {
96
+ try {
97
+ JSON.parse(corruptedContent);
98
+ } catch (e) {
99
+ const msg = e instanceof Error ? e.message : String(e);
100
+ return `When editing \`${file}\`, ensure valid JSON syntax. ` +
101
+ `Common mistake: ${msg}. Always validate JSON structure after editing.`;
102
+ }
103
+ }
104
+
105
+ if (corruptedContent.trim().length === 0) {
106
+ return `Do NOT empty \`${file}\`. The file was overwritten with blank content. ` +
107
+ `Preserve existing content when making changes.`;
108
+ }
109
+
110
+ if (restoredContent && corruptedContent.length < restoredContent.length * 0.1) {
111
+ return `Do NOT truncate \`${file}\`. Content was reduced by >90%. ` +
112
+ `When editing, preserve the existing structure and only modify specific sections.`;
113
+ }
114
+
115
+ return `When editing \`${file}\`, be careful not to corrupt its structure. ` +
116
+ `Always verify the file is still valid after changes.`;
117
+ }
118
+
119
+ /** Analyze pending (unlearned) quarantine entries and produce lessons */
120
+ export function analyzeQuarantine(): EvolutionStats {
121
+ const entries = listQuarantine();
122
+ const totalQuarantined = entries.length;
123
+ const totalLearned = entries.filter(e => e.learned).length;
124
+ const pending = entries.filter(e => !e.learned);
125
+
126
+ const lessons: EvolutionLesson[] = [];
127
+
128
+ for (const entry of pending) {
129
+ const corruptedContent = readFileSync(entry.quarantinePath, "utf-8");
130
+ const failureType = detectFailureType(corruptedContent);
131
+
132
+ let restoredContent: string | null = null;
133
+ let diff: string[] = [];
134
+
135
+ if (existsSync(entry.originalPath)) {
136
+ restoredContent = readFileSync(entry.originalPath, "utf-8");
137
+ if (failureType === "corruption") {
138
+ diff = lineDiff(corruptedContent, restoredContent, 20);
139
+ } else {
140
+ diff = [` (file was deleted — restored from antibody snapshot)`];
141
+ }
142
+ } else {
143
+ diff = [` (original file not found — may still be deleted)`];
144
+ }
145
+
146
+ const suggestion = generateSuggestion(entry, failureType, corruptedContent, restoredContent);
147
+
148
+ lessons.push({ entry, diff, corruptedContent, restoredContent, failureType, suggestion });
149
+ }
150
+
151
+ return { totalQuarantined, totalLearned, pending: pending.length, lessons };
152
+ }
153
+
154
+ /** Mark a quarantine entry as learned by renaming with .learned suffix */
155
+ export function markLearned(entry: QuarantineEntry): void {
156
+ if (entry.learned) return;
157
+ const newPath = entry.quarantinePath + LEARNED_SUFFIX;
158
+ renameSync(entry.quarantinePath, newPath);
159
+ }
160
+
161
+ /** Build the lesson block for afd-lessons.md */
162
+ function buildLessonBlock(lesson: EvolutionLesson): string {
163
+ const ts = lesson.entry.timestamp.replace("_", "T");
164
+ const formattedTs = `${ts.slice(0, 4)}-${ts.slice(4, 6)}-${ts.slice(6, 8)} ${ts.slice(9, 11)}:${ts.slice(11, 13)}:${ts.slice(13, 15)}`;
165
+ const lines: string[] = [
166
+ `### ${lesson.entry.originalPath} (${formattedTs})`,
167
+ `- **Type**: ${lesson.failureType === "deletion" ? "Unauthorized Deletion" : "Content Corruption"}`,
168
+ `- **Rule**: ${lesson.suggestion}`,
169
+ ];
170
+ if (lesson.diff.length > 0) {
171
+ lines.push(`- **Diff**:`);
172
+ lines.push("```");
173
+ lines.push(...lesson.diff);
174
+ lines.push("```");
175
+ }
176
+ return lines.join("\n");
177
+ }
178
+
179
+ /**
180
+ * Write lessons to afd-lessons.md and mark entries as learned.
181
+ * Returns the number of new lessons written.
182
+ */
183
+ export function evolve(): { lessonsWritten: number; totalLessons: number } {
184
+ const stats = analyzeQuarantine();
185
+ if (stats.lessons.length === 0) {
186
+ return { lessonsWritten: 0, totalLessons: stats.totalLearned };
187
+ }
188
+
189
+ // Build new lesson blocks
190
+ const newBlocks = stats.lessons.map(buildLessonBlock);
191
+
192
+ // Read or initialize afd-lessons.md
193
+ let existing = "";
194
+ if (existsSync(LESSONS_FILE)) {
195
+ existing = readFileSync(LESSONS_FILE, "utf-8");
196
+ }
197
+
198
+ if (!existing.includes("# Failure Lessons")) {
199
+ existing = `# Failure Lessons\n\n` +
200
+ `> Auto-generated by afd Self-Evolution engine.\n` +
201
+ `> AI agents: read these rules to avoid repeating past mistakes.\n\n` +
202
+ existing;
203
+ }
204
+
205
+ // Append new lessons
206
+ const updated = existing.trimEnd() + "\n\n" + newBlocks.join("\n\n") + "\n";
207
+ writeFileSync(LESSONS_FILE, updated, "utf-8");
208
+
209
+ // Mark all analyzed entries as learned
210
+ for (const lesson of stats.lessons) {
211
+ markLearned(lesson.entry);
212
+ }
213
+
214
+ return { lessonsWritten: stats.lessons.length, totalLessons: stats.totalLearned + stats.lessons.length };
215
+ }
@@ -0,0 +1,71 @@
1
+ import { Parser, Language, type Tree } from "web-tree-sitter";
2
+ import { resolve, dirname } from "path";
3
+
4
+ /**
5
+ * Singleton Tree-sitter engine with grammar caching.
6
+ * Parser.init() runs once; grammar WASMs are lazy-loaded and cached in-memory.
7
+ */
8
+ export class TreeSitterEngine {
9
+ private static instance: TreeSitterEngine | null = null;
10
+ private parser: Parser | null = null;
11
+ private grammarCache = new Map<string, Language>();
12
+ private initPromise: Promise<void> | null = null;
13
+
14
+ static async getInstance(): Promise<TreeSitterEngine> {
15
+ if (!this.instance) {
16
+ this.instance = new TreeSitterEngine();
17
+ await this.instance.init();
18
+ }
19
+ return this.instance;
20
+ }
21
+
22
+ /** Reset singleton — for testing only */
23
+ static resetForTest(): void {
24
+ if (this.instance?.parser) {
25
+ this.instance.parser.delete();
26
+ }
27
+ this.instance = null;
28
+ }
29
+
30
+ private async init(): Promise<void> {
31
+ if (this.initPromise) return this.initPromise;
32
+ this.initPromise = (async () => {
33
+ await Parser.init();
34
+ this.parser = new Parser();
35
+ })();
36
+ return this.initPromise;
37
+ }
38
+
39
+ async parse(source: string, grammarName: string): Promise<Tree> {
40
+ const grammar = await this.loadGrammar(grammarName);
41
+ this.parser!.setLanguage(grammar);
42
+ const tree = this.parser!.parse(source);
43
+ if (!tree) throw new Error(`Failed to parse with grammar: ${grammarName}`);
44
+ return tree;
45
+ }
46
+
47
+ private async loadGrammar(grammarName: string): Promise<Language> {
48
+ const cached = this.grammarCache.get(grammarName);
49
+ if (cached) return cached;
50
+
51
+ const wasmPath = resolveGrammarWasm(grammarName);
52
+ const lang = await Language.load(wasmPath);
53
+ this.grammarCache.set(grammarName, lang);
54
+ return lang;
55
+ }
56
+ }
57
+
58
+ /** Resolve WASM file path from installed npm grammar package */
59
+ function resolveGrammarWasm(grammarName: string): string {
60
+ // Grammar packages: tree-sitter-typescript, tree-sitter-python, etc.
61
+ // WASM file is at package root: node_modules/tree-sitter-{name}/tree-sitter-{name}.wasm
62
+ const packageName = `tree-sitter-${grammarName}`;
63
+ try {
64
+ // require.resolve returns bindings/node/index.js — walk up to package root
65
+ const pkgJson = require.resolve(`${packageName}/package.json`);
66
+ return resolve(dirname(pkgJson), `${packageName}.wasm`);
67
+ } catch {
68
+ // Fallback: try direct node_modules path
69
+ return resolve(process.cwd(), "node_modules", packageName, `${packageName}.wasm`);
70
+ }
71
+ }
@@ -0,0 +1,11 @@
1
+ import type { HologramResult } from "./types";
2
+
3
+ /** L0 fallback: return full source when tree-sitter cannot parse */
4
+ export function fallbackL0(filePath: string, source: string): HologramResult {
5
+ return {
6
+ hologram: source,
7
+ originalLength: source.length,
8
+ hologramLength: source.length,
9
+ savings: 0,
10
+ };
11
+ }