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.
- package/CHANGELOG.md +39 -0
- package/README.ko.md +124 -164
- package/README.md +99 -170
- package/package.json +11 -5
- package/src/adapters/index.ts +246 -35
- package/src/cli.ts +71 -1
- package/src/commands/benchmark.ts +187 -0
- package/src/commands/diagnose.ts +56 -14
- package/src/commands/doctor.ts +243 -0
- package/src/commands/evolution.ts +107 -0
- package/src/commands/fix.ts +22 -2
- package/src/commands/hooks.ts +136 -0
- package/src/commands/mcp.ts +129 -0
- package/src/commands/restart.ts +14 -0
- package/src/commands/score.ts +164 -96
- package/src/commands/start.ts +74 -15
- package/src/commands/stats.ts +103 -0
- package/src/commands/status.ts +157 -0
- package/src/commands/stop.ts +23 -4
- package/src/commands/sync.ts +253 -20
- package/src/commands/vaccine.ts +177 -0
- package/src/constants.ts +25 -1
- package/src/core/boast.ts +27 -12
- package/src/core/db.ts +74 -3
- package/src/core/evolution.ts +215 -0
- package/src/core/hologram/engine.ts +71 -0
- package/src/core/hologram/fallback.ts +11 -0
- package/src/core/hologram/incremental.ts +227 -0
- package/src/core/hologram/py-extractor.ts +132 -0
- package/src/core/hologram/ts-extractor.ts +320 -0
- package/src/core/hologram/types.ts +25 -0
- package/src/core/hologram.ts +64 -236
- package/src/core/hook-manager.ts +259 -0
- package/src/core/i18n/messages.ts +43 -0
- package/src/core/immune.ts +8 -123
- package/src/core/log-rotate.ts +33 -0
- package/src/core/log-utils.ts +38 -0
- package/src/core/lru-map.ts +61 -0
- package/src/core/notify.ts +27 -19
- package/src/core/rule-engine.ts +287 -0
- package/src/core/semantic-diff.ts +432 -0
- package/src/core/telemetry.ts +94 -0
- package/src/core/vaccine-registry.ts +212 -0
- package/src/core/workspace.ts +28 -0
- package/src/core/yaml-minimal.ts +176 -0
- package/src/daemon/client.ts +34 -6
- package/src/daemon/event-batcher.ts +108 -0
- package/src/daemon/guards.ts +13 -0
- package/src/daemon/http-routes.ts +293 -0
- package/src/daemon/mcp-handler.ts +270 -0
- package/src/daemon/server.ts +439 -353
- package/src/daemon/types.ts +100 -0
- package/src/daemon/workspace-map.ts +92 -0
- package/src/platform.ts +23 -2
- 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
|
|
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
|
-
*
|
|
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
|
|
66
|
-
|
|
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(
|
|
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(
|
|
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 {
|
|
3
|
+
import { resolveWorkspacePaths } from "../constants";
|
|
4
4
|
|
|
5
5
|
export function initDb(): Database {
|
|
6
|
-
|
|
7
|
-
|
|
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
|
+
}
|