autonomous-flow-daemon 1.0.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 (59) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.ko.md +142 -125
  3. package/README.md +119 -134
  4. package/package.json +11 -5
  5. package/src/adapters/index.ts +247 -35
  6. package/src/cli.ts +79 -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/lang.ts +41 -0
  14. package/src/commands/mcp.ts +129 -0
  15. package/src/commands/restart.ts +14 -0
  16. package/src/commands/score.ts +192 -64
  17. package/src/commands/start.ts +137 -37
  18. package/src/commands/stats.ts +103 -0
  19. package/src/commands/status.ts +157 -0
  20. package/src/commands/stop.ts +42 -9
  21. package/src/commands/sync.ts +253 -20
  22. package/src/commands/vaccine.ts +177 -0
  23. package/src/constants.ts +26 -1
  24. package/src/core/boast.ts +280 -0
  25. package/src/core/config.ts +49 -0
  26. package/src/core/db.ts +74 -3
  27. package/src/core/discovery.ts +65 -0
  28. package/src/core/evolution.ts +215 -0
  29. package/src/core/hologram/engine.ts +71 -0
  30. package/src/core/hologram/fallback.ts +11 -0
  31. package/src/core/hologram/incremental.ts +227 -0
  32. package/src/core/hologram/py-extractor.ts +132 -0
  33. package/src/core/hologram/ts-extractor.ts +320 -0
  34. package/src/core/hologram/types.ts +25 -0
  35. package/src/core/hologram.ts +64 -236
  36. package/src/core/hook-manager.ts +259 -0
  37. package/src/core/i18n/messages.ts +309 -0
  38. package/src/core/immune.ts +8 -123
  39. package/src/core/locale.ts +88 -0
  40. package/src/core/log-rotate.ts +33 -0
  41. package/src/core/log-utils.ts +38 -0
  42. package/src/core/lru-map.ts +61 -0
  43. package/src/core/notify.ts +53 -14
  44. package/src/core/rule-engine.ts +287 -0
  45. package/src/core/semantic-diff.ts +432 -0
  46. package/src/core/telemetry.ts +94 -0
  47. package/src/core/vaccine-registry.ts +212 -0
  48. package/src/core/workspace.ts +28 -0
  49. package/src/core/yaml-minimal.ts +176 -0
  50. package/src/daemon/client.ts +34 -6
  51. package/src/daemon/event-batcher.ts +108 -0
  52. package/src/daemon/guards.ts +13 -0
  53. package/src/daemon/http-routes.ts +293 -0
  54. package/src/daemon/mcp-handler.ts +270 -0
  55. package/src/daemon/server.ts +492 -273
  56. package/src/daemon/types.ts +100 -0
  57. package/src/daemon/workspace-map.ts +92 -0
  58. package/src/platform.ts +60 -0
  59. package/src/version.ts +15 -0
@@ -1,50 +1,46 @@
1
+ /**
2
+ * afd daemon — S.E.A.M engine + module orchestrator.
3
+ *
4
+ * Modules:
5
+ * types.ts — shared types and constants
6
+ * workspace-map.ts — project structure cache
7
+ * mcp-handler.ts — MCP stdio JSON-RPC dispatcher
8
+ * http-routes.ts — HTTP IPC endpoints
9
+ */
10
+
1
11
  import { watch } from "chokidar";
2
- import { mkdirSync, writeFileSync, unlinkSync, readFileSync, existsSync } from "fs";
3
- import { resolve } from "path";
4
- import { AFD_DIR, PID_FILE, PORT_FILE, WATCH_TARGETS } from "../constants";
12
+ import { mkdirSync, writeFileSync, unlinkSync, readFileSync, existsSync, watch as fsWatch, readdirSync } from "fs";
13
+ import { resolve, join } from "path";
14
+ import { QUARANTINE_DIR, WATCH_TARGETS, resolveWorkspacePaths } from "../constants";
5
15
  import { initDb } from "../core/db";
6
16
  import { generateHologram } from "../core/hologram";
7
- import { diagnose } from "../core/immune";
17
+ import { EventBatcher } from "./event-batcher";
8
18
  import type { PatchOp } from "../core/immune";
9
19
  import { detectEcosystem } from "../adapters/index";
10
- import type { DetectionResult } from "../adapters/index";
11
-
12
- // ── Suppression Safety Constants ──
13
- const DOUBLE_TAP_WINDOW_MS = 60_000; // 60 seconds
14
- const MASS_EVENT_THRESHOLD = 3; // >3 unlinks in 1 second
15
- const MASS_EVENT_WINDOW_MS = 1_000; // 1 second window
16
-
17
- interface HologramStats {
18
- totalRequests: number;
19
- totalOriginalChars: number;
20
- totalHologramChars: number;
21
- }
22
-
23
- interface DaemonState {
24
- startedAt: number;
25
- filesDetected: number;
26
- lastEvent: string | null;
27
- lastEventAt: number | null;
28
- watchedFiles: string[];
29
- hologramStats: HologramStats;
30
- ecosystems: DetectionResult[];
31
- autoHealCount: number;
32
- autoHealLog: { id: string; at: number }[];
33
- // Suppression safety: recent unlink timestamps for mass-event detection
34
- recentUnlinks: number[];
35
- // Suppression safety: per-file first-tap timestamps for double-tap detection
36
- firstTapTimestamps: Map<string, number>;
37
- suppressionSkippedCount: number;
38
- dormantTransitions: { antibodyId: string; at: number }[];
39
- }
40
-
20
+ import { calcHealMetrics, maybeHealBoast, formatHealLog, formatDormantLog } from "../core/boast";
21
+ import { discoverWatchTargets } from "../core/discovery";
22
+ import { formatTimestamp, lineDiff } from "../core/log-utils";
23
+ import { semanticDiff, isAstSupported } from "../core/semantic-diff";
24
+ import { LruStringMap } from "../core/lru-map";
25
+
26
+ import {
27
+ DOUBLE_TAP_WINDOW_MS, MASS_EVENT_THRESHOLD, MASS_EVENT_WINDOW_MS,
28
+ TAP_CLEANUP_INTERVAL_MS, SELF_WRITE_DEBOUNCE_MS, VALIDATOR_TIMEOUT_MS, VALIDATORS_DIR,
29
+ } from "./types";
30
+ import type { DaemonState, DaemonContext, DaemonOptions, ValidatorFn } from "./types";
31
+ import { createWorkspaceMap } from "./workspace-map";
32
+ import { startMcpStdio } from "./mcp-handler";
33
+ import { createHttpHandler } from "./http-routes";
34
+ import { assertInsideWorkspace } from "./guards";
35
+
36
+ // ── State ──
41
37
  const state: DaemonState = {
42
38
  startedAt: Date.now(),
43
39
  filesDetected: 0,
44
40
  lastEvent: null,
45
41
  lastEventAt: null,
46
- watchedFiles: [],
47
- hologramStats: { totalRequests: 0, totalOriginalChars: 0, totalHologramChars: 0 },
42
+ watchedFiles: new Set(),
43
+ hologramStats: { totalRequests: 0, totalOriginalChars: 0, totalHologramChars: 0, sessionOriginalChars: 0, sessionHologramChars: 0 },
48
44
  ecosystems: [],
49
45
  autoHealCount: 0,
50
46
  autoHealLog: [],
@@ -52,18 +48,69 @@ const state: DaemonState = {
52
48
  firstTapTimestamps: new Map(),
53
49
  suppressionSkippedCount: 0,
54
50
  dormantTransitions: [],
51
+ totalFileBytesSaved: 0,
52
+ totalSavedTokens: 0,
53
+ fileSnapshots: new LruStringMap(10 * 1024 * 1024),
54
+ sseClients: new Set(),
55
+ customValidators: new Map(),
56
+ mistakeCache: new Map(),
55
57
  };
56
58
 
59
+ const _ws = resolveWorkspacePaths();
60
+
61
+ let _cleanupResources: {
62
+ watcher?: ReturnType<typeof watch>;
63
+ interval?: ReturnType<typeof setInterval>;
64
+ wsMapGetTimer?: () => ReturnType<typeof setTimeout> | null;
65
+ validatorWatcher?: ReturnType<typeof fsWatch>;
66
+ db?: { close(): void };
67
+ eventBatcher?: EventBatcher;
68
+ } = {};
69
+
57
70
  function cleanup() {
58
- try { unlinkSync(PID_FILE); } catch {}
59
- try { unlinkSync(PORT_FILE); } catch {}
71
+ try { _cleanupResources.eventBatcher?.destroy(); } catch {}
72
+ try { _cleanupResources.interval && clearInterval(_cleanupResources.interval); } catch {}
73
+ try { const mt = _cleanupResources.wsMapGetTimer?.(); mt && clearTimeout(mt); } catch {}
74
+ try { _cleanupResources.watcher?.close(); } catch {}
75
+ try { _cleanupResources.validatorWatcher?.close(); } catch {}
76
+ try { _cleanupResources.db?.close(); } catch {}
77
+ try { unlinkSync(_ws.pidFile); } catch {}
78
+ try { unlinkSync(_ws.portFile); } catch {}
60
79
  }
61
80
 
62
- function main() {
63
- // Detect ecosystem at startup
81
+ // ── S.E.A.M Logger ──
82
+ const GUARD_LINE = "========== GUARDED ==========";
83
+ const GUARD_PHASES = new Set(["Mutate", "Quarantine"]);
84
+
85
+ function createSeamLogger(mcp: boolean) {
86
+ const log = mcp ? console.error.bind(console) : console.log.bind(console);
87
+ return function seam(phase: string, msg: string) {
88
+ if (GUARD_PHASES.has(phase)) {
89
+ log(`\n${GUARD_LINE}`);
90
+ log(`[${formatTimestamp()}] [afd] [${phase}] ${msg}`);
91
+ log(`${GUARD_LINE}\n`);
92
+ } else {
93
+ log(`[${formatTimestamp()}] [afd] [${phase}] ${msg}`);
94
+ }
95
+ const encoder = new TextEncoder();
96
+ const payload = JSON.stringify({ phase, msg, ts: Date.now() });
97
+ const sseData = encoder.encode(`data: ${payload}\n\n`);
98
+ const dead: ReadableStreamDefaultController<Uint8Array>[] = [];
99
+ for (const controller of state.sseClients) {
100
+ try { controller.enqueue(sseData); } catch { dead.push(controller); }
101
+ }
102
+ for (const c of dead) state.sseClients.delete(c);
103
+ };
104
+ }
105
+
106
+ // ══════════════════════════════════════════════════════════
107
+ export function main(options: DaemonOptions = {}) {
64
108
  state.ecosystems = detectEcosystem(process.cwd());
65
109
 
66
110
  const db = initDb();
111
+ _cleanupResources.db = db;
112
+
113
+ // ── Prepared statements ──
67
114
  const insertEvent = db.prepare("INSERT INTO events (type, path, timestamp) VALUES (?, ?, ?)");
68
115
  const insertAntibody = db.prepare(
69
116
  "INSERT OR REPLACE INTO antibodies (id, pattern_type, file_target, patch_op, created_at) VALUES (?, ?, ?, ?, datetime('now'))"
@@ -73,299 +120,471 @@ function main() {
73
120
  const countAntibodies = db.prepare("SELECT COUNT(*) as cnt FROM antibodies");
74
121
  const insertUnlinkLog = db.prepare("INSERT INTO unlink_log (file_path, timestamp) VALUES (?, ?)");
75
122
  const findAntibodyByFile = db.prepare("SELECT id, dormant FROM antibodies WHERE file_target = ? AND dormant = 0");
123
+ const findAntibodyById = db.prepare("SELECT * FROM antibodies WHERE id = ?");
76
124
  const setAntibodyDormant = db.prepare("UPDATE antibodies SET dormant = 1 WHERE id = ?");
77
125
 
78
- // ── Suppression Safety: Helper Functions ──
126
+ // Hologram stats
127
+ const getLifetime = db.prepare("SELECT total_requests, total_original_chars, total_hologram_chars FROM hologram_lifetime WHERE id = 1");
128
+ const updateLifetime = db.prepare(
129
+ "UPDATE hologram_lifetime SET total_requests = ?, total_original_chars = ?, total_hologram_chars = ? WHERE id = 1"
130
+ );
131
+ const upsertDaily = db.prepare(`
132
+ INSERT INTO hologram_daily (date, requests, original_chars, hologram_chars) VALUES (?, ?, ?, ?)
133
+ ON CONFLICT(date) DO UPDATE SET requests = requests + excluded.requests,
134
+ original_chars = original_chars + excluded.original_chars,
135
+ hologram_chars = hologram_chars + excluded.hologram_chars
136
+ `);
137
+ const getDailyAll = db.prepare("SELECT date, requests, original_chars, hologram_chars FROM hologram_daily ORDER BY date DESC LIMIT 7");
138
+ const purgeOldDaily = db.prepare("DELETE FROM hologram_daily WHERE date < date('now', '-7 days')");
139
+
140
+ // ── Telemetry ──
141
+ const insertTelemetry = db.prepare(
142
+ "INSERT INTO telemetry (category, action, detail, duration_ms, timestamp) VALUES (?, ?, ?, ?, ?)"
143
+ );
144
+ function trackEvent(category: string, action: string, detail?: string, durationMs?: number) {
145
+ try { insertTelemetry.run(category, action, detail ?? null, durationMs ?? null, Date.now()); } catch { /* crash-only */ }
146
+ }
147
+
148
+ // ── Mistake History (Passive Defense) ──
149
+ const insertMistakeHistory = db.prepare(
150
+ "INSERT INTO mistake_history (file_path, mistake_type, description, antibody_id, timestamp) VALUES (?, ?, ?, ?, ?)"
151
+ );
152
+ const queryMistakesByFile = db.prepare(
153
+ "SELECT mistake_type, description, timestamp FROM mistake_history WHERE file_path = ? ORDER BY timestamp DESC LIMIT 5"
154
+ );
155
+ const deleteMistakeOverflow = db.prepare(
156
+ "DELETE FROM mistake_history WHERE file_path = ? AND id NOT IN (SELECT id FROM mistake_history WHERE file_path = ? ORDER BY timestamp DESC LIMIT 5)"
157
+ );
158
+
159
+ function recordMistake(filePath: string, mistakeType: string, description: string, antibodyId?: string) {
160
+ try {
161
+ const normalizedPath = filePath.replace(/\\/g, "/");
162
+ const truncatedDesc = description.slice(0, 200);
163
+ insertMistakeHistory.run(normalizedPath, mistakeType, truncatedDesc, antibodyId ?? null, Date.now());
164
+ deleteMistakeOverflow.run(normalizedPath, normalizedPath);
165
+ const cached = state.mistakeCache.get(normalizedPath) ?? [];
166
+ cached.unshift({ mistake_type: mistakeType, description: truncatedDesc, timestamp: Date.now() });
167
+ if (cached.length > 5) cached.length = 5;
168
+ state.mistakeCache.set(normalizedPath, cached);
169
+ } catch { /* crash-only */ }
170
+ }
171
+
172
+ try {
173
+ const allMistakes = db.prepare("SELECT file_path, mistake_type, description, timestamp FROM mistake_history ORDER BY timestamp DESC").all() as { file_path: string; mistake_type: string; description: string; timestamp: number }[];
174
+ for (const row of allMistakes) {
175
+ const cached = state.mistakeCache.get(row.file_path) ?? [];
176
+ if (cached.length < 5) {
177
+ cached.push({ mistake_type: row.mistake_type, description: row.description, timestamp: row.timestamp });
178
+ state.mistakeCache.set(row.file_path, cached);
179
+ }
180
+ }
181
+ } catch { /* crash-only — empty cache is fine */ }
182
+
183
+ function today(): string { return new Date().toISOString().slice(0, 10); }
184
+
185
+ // Load persisted hologram stats
186
+ const persisted = getLifetime.get() as { total_requests: number; total_original_chars: number; total_hologram_chars: number } | null;
187
+ if (persisted) {
188
+ state.hologramStats.totalRequests = persisted.total_requests;
189
+ state.hologramStats.totalOriginalChars = persisted.total_original_chars;
190
+ state.hologramStats.totalHologramChars = persisted.total_hologram_chars;
191
+ }
192
+
193
+ const seam = createSeamLogger(!!options.mcp);
194
+
195
+ function persistHologramStats(originalChars: number, hologramChars: number) {
196
+ state.hologramStats.totalRequests++;
197
+ state.hologramStats.totalOriginalChars += originalChars;
198
+ state.totalSavedTokens += Math.max(0, Math.floor((originalChars - hologramChars) / 4));
199
+ state.hologramStats.totalHologramChars += hologramChars;
200
+ state.hologramStats.sessionOriginalChars += originalChars;
201
+ state.hologramStats.sessionHologramChars += hologramChars;
202
+ try {
203
+ const hs = state.hologramStats;
204
+ updateLifetime.run(hs.totalRequests, hs.totalOriginalChars, hs.totalHologramChars);
205
+ upsertDaily.run(today(), 1, originalChars, hologramChars);
206
+ purgeOldDaily.run();
207
+ } catch (err) {
208
+ console.error("[afd] Failed to persist hologram stats:", err instanceof Error ? err.message : String(err));
209
+ }
210
+ }
211
+
212
+ function snapshotFile(filePath: string) {
213
+ try {
214
+ if (existsSync(filePath)) state.fileSnapshots.set(filePath, readFileSync(filePath, "utf-8"));
215
+ } catch { /* ignore */ }
216
+ }
217
+
218
+ async function safeHologram(filePath: string, source: string): Promise<string> {
219
+ try {
220
+ const result = await generateHologram(filePath, source);
221
+ persistHologramStats(result.originalLength, result.hologramLength);
222
+ return result.hologram;
223
+ } catch {
224
+ const lines = source.split("\n");
225
+ return lines.slice(0, 50).join("\n") + (lines.length > 50 ? "\n// … (truncated, AST parse failed)" : "");
226
+ }
227
+ }
228
+
229
+ function quarantineFile(originalPath: string, corruptedContent: string | null): void {
230
+ try {
231
+ mkdirSync(QUARANTINE_DIR, { recursive: true });
232
+ const now = new Date();
233
+ const ts = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, "0")}${String(now.getDate()).padStart(2, "0")}_${String(now.getHours()).padStart(2, "0")}${String(now.getMinutes()).padStart(2, "0")}${String(now.getSeconds()).padStart(2, "0")}`;
234
+ const baseName = originalPath.replace(/[\\/]/g, "_").replace(/^_+/, "");
235
+ const quarantinePath = resolve(QUARANTINE_DIR, `${ts}_${baseName}`);
236
+ writeFileSync(quarantinePath, corruptedContent ?? "DELETED", "utf-8");
237
+ seam("Quarantine", `Saved corrupted state → .afd/quarantine/${ts}_${baseName}`);
238
+ } catch (err) {
239
+ seam("Quarantine", `FAILED to quarantine ${originalPath}: ${err instanceof Error ? err.message : String(err)}`);
240
+ }
241
+ }
242
+
243
+ // ── Auto-Seed Antibodies ──
244
+ function seedAntibodies() {
245
+ const immuneFiles = [
246
+ { id: "IMM-001", path: ".claudeignore" },
247
+ { id: "IMM-002", path: ".claude/hooks.json" },
248
+ { id: "IMM-003", path: "CLAUDE.md" },
249
+ ];
250
+ for (const { id, path: filePath } of immuneFiles) {
251
+ if (existsSync(filePath)) {
252
+ const content = readFileSync(filePath, "utf-8");
253
+ const patches: PatchOp[] = [{ op: "add", path: `/${filePath}`, value: content }];
254
+ insertAntibody.run(id, "auto-seed", filePath, JSON.stringify(patches));
255
+ state.fileSnapshots.set(filePath, content);
256
+ seam("Adapt", `Antibody ${id} seeded for ${filePath} (${content.length} bytes)`);
257
+ }
258
+ }
259
+ }
79
260
 
80
- /** Check if we're in a mass-event burst (>3 unlinks within 1 second) */
261
+ const selfWrites = new Set<string>();
262
+ seedAntibodies();
263
+
264
+ // ── Dynamic Immune Synthesis ──
265
+ const validatorsDir = join(process.cwd(), VALIDATORS_DIR);
266
+
267
+ async function loadValidators() {
268
+ state.customValidators.clear();
269
+ if (!existsSync(validatorsDir)) return;
270
+ let files: string[];
271
+ try { files = readdirSync(validatorsDir).filter(f => f.endsWith(".js")); } catch { return; }
272
+ for (const file of files) {
273
+ const absPath = resolve(validatorsDir, file);
274
+ try {
275
+ const mod = await import(absPath);
276
+ const fn = mod.default ?? mod;
277
+ if (typeof fn === "function") {
278
+ state.customValidators.set(file, fn as ValidatorFn);
279
+ seam("Adapt", `🧬 Validator loaded: ${file}`);
280
+ } else {
281
+ seam("Adapt", `⚠️ Validator ${file} does not export a function — skipped`);
282
+ }
283
+ } catch (err) {
284
+ seam("Adapt", `⚠️ Failed to load validator ${file}: ${err instanceof Error ? err.message : String(err)}`);
285
+ }
286
+ }
287
+ if (state.customValidators.size > 0) {
288
+ seam("Adapt", `Dynamic Immune Synthesis: ${state.customValidators.size} active validator(s)`);
289
+ }
290
+ }
291
+
292
+ loadValidators();
293
+
294
+ try {
295
+ mkdirSync(validatorsDir, { recursive: true });
296
+ let reloadTimer: ReturnType<typeof setTimeout> | null = null;
297
+ const validatorWatcher = fsWatch(validatorsDir, (_eventType, filename) => {
298
+ if (!filename?.endsWith(".js")) return;
299
+ if (reloadTimer) clearTimeout(reloadTimer);
300
+ reloadTimer = setTimeout(() => { seam("Sense", `Validator change detected: ${filename} — reloading...`); loadValidators(); reloadTimer = null; }, 200);
301
+ });
302
+ _cleanupResources.validatorWatcher = validatorWatcher;
303
+ } catch (err) {
304
+ seam("Adapt", `⚠️ Cannot watch ${VALIDATORS_DIR}: ${err instanceof Error ? err.message : String(err)}`);
305
+ }
306
+
307
+ function runCustomValidators(newContent: string, filePath: string): boolean {
308
+ for (const [name, fn] of state.customValidators) {
309
+ try {
310
+ const t0 = performance.now();
311
+ const result = fn(newContent, filePath);
312
+ const elapsed = performance.now() - t0;
313
+ if (elapsed > VALIDATOR_TIMEOUT_MS) seam("Adapt", `⚠️ Validator ${name} took ${Math.round(elapsed)}ms (>${VALIDATOR_TIMEOUT_MS}ms)`);
314
+ if (result === true) { trackEvent("validator", name, filePath); seam("Adapt", `🛡️ Custom validator ${name} flagged corruption in ${filePath}`); return true; }
315
+ } catch (err) {
316
+ seam("Adapt", `⚠️ Validator ${name} threw error — ignored: ${err instanceof Error ? err.message : String(err)}`);
317
+ }
318
+ }
319
+ return false;
320
+ }
321
+
322
+ // ── Periodic cleanup ──
323
+ const tapCleanupInterval = setInterval(() => {
324
+ const now = Date.now();
325
+ for (const [file, ts] of state.firstTapTimestamps) { if (now - ts > DOUBLE_TAP_WINDOW_MS) state.firstTapTimestamps.delete(file); }
326
+ for (const [file, ts] of corruptionTaps) { if (now - ts > DOUBLE_TAP_WINDOW_MS) corruptionTaps.delete(file); }
327
+ }, TAP_CLEANUP_INTERVAL_MS);
328
+ _cleanupResources.interval = tapCleanupInterval;
329
+
330
+ // ── Suppression Safety ──
81
331
  function isMassEvent(now: number): boolean {
82
- // Prune old entries beyond the window
83
332
  state.recentUnlinks = state.recentUnlinks.filter(t => now - t < MASS_EVENT_WINDOW_MS);
84
333
  return state.recentUnlinks.length > MASS_EVENT_THRESHOLD;
85
334
  }
86
335
 
87
- /** Clear first-tap state when mass event is detected (bulk ops are not intentional user deletes) */
88
- function clearTapsOnMassEvent() {
89
- state.firstTapTimestamps.clear();
90
- }
91
-
92
- /** Auto-heal: re-apply patches for a given antibody */
93
336
  function autoHealFile(antibodyId: string, fileTarget: string, patchOp: string) {
337
+ const t0 = performance.now();
94
338
  try {
339
+ seam("Mutate", `Restoring ${fileTarget} via antibody ${antibodyId}...`);
95
340
  const patches = JSON.parse(patchOp) as PatchOp[];
341
+ let bytesWritten = 0;
96
342
  for (const patch of patches) {
97
343
  if (patch.op === "add" && patch.value) {
98
344
  const targetPath = resolve(patch.path.replace(/^\//, ""));
345
+ assertInsideWorkspace(targetPath, _ws.root);
99
346
  writeFileSync(targetPath, patch.value, "utf-8");
347
+ bytesWritten += patch.value.length;
100
348
  }
101
349
  }
350
+ const healMs = Math.round(performance.now() - t0);
102
351
  state.autoHealCount++;
352
+ state.totalFileBytesSaved += bytesWritten;
103
353
  state.autoHealLog.push({ id: antibodyId, at: Date.now() });
354
+ trackEvent("immune", "heal_hit", JSON.stringify({ antibodyId, fileTarget, bytesWritten, healMs }));
355
+ recordMistake(fileTarget, "file-deleted", `File deleted and restored via antibody ${antibodyId}`, antibodyId);
104
356
  if (state.autoHealLog.length > 100) state.autoHealLog.shift();
105
- } catch {
106
- // Crash-only: if healing fails, let it go
357
+ const metrics = calcHealMetrics(bytesWritten, healMs);
358
+ const boast = maybeHealBoast(5);
359
+ const fileName = fileTarget.split("/").pop() ?? fileTarget;
360
+ (options.mcp ? console.error : console.log)(formatHealLog(fileName, metrics, boast));
361
+ } catch (err) {
362
+ seam("Mutate", `FAILED to restore ${fileTarget}: ${err instanceof Error ? err.message : String(err)}`);
107
363
  }
108
364
  }
109
365
 
110
- /**
111
- * Handle unlink event with Double-Tap and Mass-Event heuristics.
112
- * Returns true if the event was handled (healed or made dormant).
113
- */
114
- function handleUnlink(filePath: string, now: number): boolean {
115
- // Record for mass-event detection
366
+ function handleUnlink(filePath: string, now: number): "healed" | "dormant" | null {
116
367
  state.recentUnlinks.push(now);
117
368
  insertUnlinkLog.run(filePath, now);
118
-
119
- // Mass-event check: if >3 unlinks in 1s, skip ALL suppression logic
120
- if (isMassEvent(now)) {
121
- state.suppressionSkippedCount++;
122
- clearTapsOnMassEvent();
123
- return false; // Do nothing — likely git checkout or bulk operation
124
- }
125
-
126
- // Find active (non-dormant) antibody protecting this file
369
+ if (isMassEvent(now)) { state.suppressionSkippedCount++; trackEvent("immune", "suppression", JSON.stringify({ filePath })); state.firstTapTimestamps.clear(); return null; }
127
370
  const antibody = findAntibodyByFile.get(filePath) as { id: string; dormant: number } | null;
128
- if (!antibody) return false; // No antibody for this file
129
-
130
- // Fetch full antibody data for healing
131
- const fullAntibody = db.prepare("SELECT * FROM antibodies WHERE id = ?").get(antibody.id) as {
132
- id: string; patch_op: string; file_target: string;
133
- } | null;
134
- if (!fullAntibody) return false;
135
-
136
- // Double-Tap detection
371
+ if (!antibody) return null;
372
+ const fullAntibody = findAntibodyById.get(antibody.id) as { id: string; patch_op: string; file_target: string } | null;
373
+ if (!fullAntibody) return null;
137
374
  const previousTap = state.firstTapTimestamps.get(filePath);
138
-
139
375
  if (previousTap && (now - previousTap) < DOUBLE_TAP_WINDOW_MS) {
140
- // SECOND TAP within window → user is intentional → make dormant
141
376
  setAntibodyDormant.run(antibody.id);
142
377
  state.firstTapTimestamps.delete(filePath);
143
378
  state.dormantTransitions.push({ antibodyId: antibody.id, at: now });
144
- return true; // Dormant — do NOT heal
379
+ if (state.dormantTransitions.length > 100) state.dormantTransitions.shift();
380
+ trackEvent("immune", "dormant", JSON.stringify({ antibodyId: antibody.id }));
381
+ (options.mcp ? console.error : console.log)(formatDormantLog(antibody.id));
382
+ return "dormant";
145
383
  }
146
-
147
- // FIRST TAP: record timestamp and auto-heal
148
384
  state.firstTapTimestamps.set(filePath, now);
385
+ quarantineFile(filePath, null);
149
386
  autoHealFile(fullAntibody.id, fullAntibody.file_target, fullAntibody.patch_op);
150
- return true;
387
+ return "healed";
151
388
  }
152
389
 
153
- // File watcher
154
- const watcher = watch(WATCH_TARGETS, {
155
- ignoreInitial: false,
156
- persistent: true,
157
- });
390
+ // ── Smart Discovery + Watcher ──
391
+ const discovery = discoverWatchTargets(WATCH_TARGETS);
392
+ seam("Sense", `Smart Discovery: ${discovery.targets.length} targets found in ${discovery.elapsedMs}ms`);
393
+ if (discovery.discoveredCount > 0) {
394
+ seam("Sense", `Discovered ${discovery.discoveredCount} extra: ${discovery.targets.filter(t => !WATCH_TARGETS.includes(t)).join(", ")}`);
395
+ }
158
396
 
159
- watcher.on("all", (event, path) => {
160
- state.filesDetected++;
161
- state.lastEvent = `${event}:${path}`;
162
- state.lastEventAt = Date.now();
163
- if (!state.watchedFiles.includes(path)) {
164
- state.watchedFiles.push(path);
165
- }
166
- insertEvent.run(event, path, Date.now());
397
+ const watcher = watch(discovery.targets, { ignoreInitial: false, persistent: true, atomic: 100 });
398
+ _cleanupResources.watcher = watcher;
167
399
 
168
- // Suppression safety: handle unlink events
169
- if (event === "unlink") {
170
- handleUnlink(path, Date.now());
171
- }
172
- });
400
+ const immuneMap: Record<string, string> = { ".claudeignore": "IMM-001", ".claude/hooks.json": "IMM-002", "CLAUDE.md": "IMM-003" };
401
+ const corruptionTaps = new Map<string, number>();
173
402
 
174
- // HTTP server for IPC
175
- const server = Bun.serve({
176
- port: 0,
177
- async fetch(req) {
178
- const url = new URL(req.url);
403
+ function isInternalPath(p: string): boolean {
404
+ const normalized = p.replace(/\\/g, "/");
405
+ return normalized.startsWith(".afd/") || normalized.includes("/.afd/");
406
+ }
179
407
 
180
- if (url.pathname === "/health") {
181
- return Response.json({ status: "alive", pid: process.pid });
182
- }
408
+ function isCorrupted(oldContent: string, newContent: string, filePath: string): boolean {
409
+ if (runCustomValidators(newContent, filePath)) return true;
410
+ const trimmed = newContent.trim();
411
+ if (trimmed.length === 0) return true;
412
+ if (filePath.endsWith(".json")) {
413
+ if (trimmed === "{}" || trimmed === "[]") return true;
414
+ try { JSON.parse(newContent); } catch { return true; }
415
+ }
416
+ if (oldContent.length > 50 && newContent.length < oldContent.length * 0.1) return true;
417
+ return false;
418
+ }
183
419
 
184
- if (url.pathname === "/mini-status") {
185
- const last = state.autoHealLog.length > 0
186
- ? state.autoHealLog[state.autoHealLog.length - 1].id
187
- : null;
188
- return Response.json({
189
- status: "ON",
190
- healed_count: state.autoHealCount,
191
- last_healed: last,
192
- });
420
+ // ── Event Batcher (Adaptive Debounce) ──
421
+ const eventBatcher = new EventBatcher({
422
+ debounceMs: 300,
423
+ isImmunePath: (p: string) => {
424
+ const normalized = p.replace(/\\/g, "/");
425
+ return normalized in immuneMap;
426
+ },
427
+ onImmediate: (event: string, path: string) => handleFileEvent(event, path),
428
+ onBatch: (events) => {
429
+ if (events.length > 1) {
430
+ seam("Sense", `[Batch] Processing ${events.length} events as single batch`);
193
431
  }
432
+ for (const e of events) handleFileEvent(e.event, e.path);
433
+ },
434
+ });
435
+ _cleanupResources.eventBatcher = eventBatcher;
194
436
 
195
- if (url.pathname === "/hologram") {
196
- const file = url.searchParams.get("file");
197
- if (!file) return Response.json({ error: "?file= required" }, { status: 400 });
198
- try {
199
- const absPath = resolve(file);
200
- const source = readFileSync(absPath, "utf-8");
201
- const result = generateHologram(file, source);
202
- state.hologramStats.totalRequests++;
203
- state.hologramStats.totalOriginalChars += result.originalLength;
204
- state.hologramStats.totalHologramChars += result.hologramLength;
205
- return Response.json(result);
206
- } catch (err: unknown) {
207
- const msg = err instanceof Error ? err.message : String(err);
208
- return Response.json({ error: msg }, { status: 404 });
209
- }
210
- }
437
+ // ── Watcher Event Handler (S.E.A.M) ──
438
+ watcher.on("all", (event, path) => {
439
+ if (isInternalPath(path)) return;
440
+ if (selfWrites.has(path)) return;
441
+ eventBatcher.push(event, path);
442
+ });
211
443
 
212
- if (url.pathname === "/diagnose") {
213
- const raw = url.searchParams.get("raw") === "true";
214
- const known = (antibodyIds.all() as { id: string }[]).map(r => r.id);
215
- const result = diagnose(known, { raw });
216
- return Response.json(result);
217
- }
444
+ function handleFileEvent(event: string, path: string) {
445
+ const _seamStart = performance.now();
218
446
 
219
- if (url.pathname === "/antibodies") {
220
- const rows = listAntibodies.all();
221
- return Response.json({ antibodies: rows });
222
- }
447
+ state.filesDetected++;
448
+ state.lastEvent = `${event}:${path}`;
449
+ state.lastEventAt = Date.now();
450
+ state.watchedFiles.add(path);
451
+ insertEvent.run(event, path, Date.now());
223
452
 
224
- if (url.pathname === "/antibodies/learn" && req.method === "POST") {
225
- try {
226
- const body = await req.json() as {
227
- id: string;
228
- patternType: string;
229
- fileTarget: string;
230
- patches: PatchOp[];
231
- };
232
- insertAntibody.run(
233
- body.id,
234
- body.patternType,
235
- body.fileTarget,
236
- JSON.stringify(body.patches)
237
- );
238
- return Response.json({ status: "learned", id: body.id });
239
- } catch (err: unknown) {
240
- const msg = err instanceof Error ? err.message : String(err);
241
- return Response.json({ error: msg }, { status: 400 });
242
- }
453
+ if (event === "add" || event === "addDir") { seam("Sense", `${event} → ${path}`); snapshotFile(path); trackEvent("seam", "sense", null, Math.round(performance.now() - _seamStart)); return; }
454
+
455
+ if (event === "unlink") {
456
+ seam("Sense", `unlink → ${path}`);
457
+ state.fileSnapshots.delete(path);
458
+ state.watchedFiles.delete(path);
459
+ const result = handleUnlink(path, Date.now());
460
+ if (result === "healed") {
461
+ selfWrites.add(path);
462
+ setTimeout(() => selfWrites.delete(path), SELF_WRITE_DEBOUNCE_MS);
463
+ seam("Mutate", `Restored ${path} from antibody snapshot`);
464
+ seam("Extract", `💡 Tip: Use the MCP tool 'afd_hologram' on ${path} to safely inspect the file's structure before attempting another edit.`);
465
+ snapshotFile(path);
466
+ watcher.add(path);
467
+ } else if (result === "dormant") {
468
+ seam("Adapt", `Double-tap confirmed user intentionally deleted ${path}, antibody deactivated`);
243
469
  }
470
+ trackEvent("seam", event, path, Math.round(performance.now() - _seamStart));
471
+ return;
472
+ }
244
473
 
245
- if (url.pathname === "/auto-heal/record" && req.method === "POST") {
246
- try {
247
- const body = await req.json() as { id: string };
248
- state.autoHealCount++;
249
- state.autoHealLog.push({ id: body.id, at: Date.now() });
250
- // Keep log bounded
251
- if (state.autoHealLog.length > 100) state.autoHealLog.shift();
252
- return Response.json({ status: "recorded", total: state.autoHealCount });
253
- } catch (err: unknown) {
254
- const msg = err instanceof Error ? err.message : String(err);
255
- return Response.json({ error: msg }, { status: 400 });
474
+ if (event === "change") {
475
+ if (!existsSync(path)) return;
476
+ let newContent: string;
477
+ try { newContent = readFileSync(path, "utf-8"); } catch { return; }
478
+ const oldContent = state.fileSnapshots.get(path);
479
+ const newSize = newContent.length;
480
+
481
+ if (oldContent !== undefined && oldContent !== newContent) {
482
+ if (isAstSupported(path)) {
483
+ try {
484
+ const sdiff = semanticDiff(path, oldContent, newContent);
485
+ const breakingTag = sdiff.hasBreakingChanges ? " ⚠️ BREAKING" : "";
486
+ seam("Sense", `change → ${path} (${newSize} bytes)${breakingTag}\n [semantic] ${sdiff.summary}`);
487
+ } catch {
488
+ const diffs = lineDiff(oldContent, newContent);
489
+ seam("Sense", `change → ${path} (${newSize} bytes)\n${diffs.join("\n")}`);
490
+ }
491
+ } else {
492
+ const diffs = lineDiff(oldContent, newContent);
493
+ if (diffs.length > 0) seam("Sense", `change → ${path} (${newSize} bytes)\n${diffs.join("\n")}`);
494
+ else seam("Sense", `change → ${path} (${newSize} bytes, whitespace-only diff)`);
256
495
  }
496
+ } else if (oldContent === undefined) {
497
+ seam("Sense", `change → ${path} (${newSize} bytes, no previous snapshot)`);
498
+ } else {
499
+ seam("Sense", `change → ${path} (${newSize} bytes, content identical — touch or metadata)`);
257
500
  }
258
501
 
259
- if (url.pathname === "/score") {
260
- const uptime = Math.floor((Date.now() - state.startedAt) / 1000);
261
- const eventCount = db.query("SELECT COUNT(*) as cnt FROM events").get() as { cnt: number };
262
- const abCount = countAntibodies.get() as { cnt: number };
263
- const hs = state.hologramStats;
264
- const globalSavings = hs.totalOriginalChars > 0
265
- ? Math.round((hs.totalOriginalChars - hs.totalHologramChars) / hs.totalOriginalChars * 1000) / 10
266
- : 0;
267
- return Response.json({
268
- uptime,
269
- filesDetected: state.filesDetected,
270
- totalEvents: eventCount.cnt,
271
- lastEvent: state.lastEvent,
272
- lastEventAt: state.lastEventAt,
273
- watchedFiles: state.watchedFiles,
274
- watchTargets: WATCH_TARGETS,
275
- hologram: {
276
- requests: hs.totalRequests,
277
- originalChars: hs.totalOriginalChars,
278
- hologramChars: hs.totalHologramChars,
279
- savings: globalSavings,
280
- },
281
- immune: {
282
- antibodies: abCount.cnt,
283
- autoHealed: state.autoHealCount,
284
- lastAutoHeal: state.autoHealLog.length > 0
285
- ? state.autoHealLog[state.autoHealLog.length - 1]
286
- : null,
287
- },
288
- ecosystem: {
289
- detected: state.ecosystems.map(e => ({
290
- name: e.adapter.name,
291
- confidence: e.confidence,
292
- schema: e.adapter.getHarnessSchema(),
293
- })),
294
- primary: state.ecosystems[0]?.adapter.name ?? "Unknown",
295
- },
296
- suppression: {
297
- massEventsSkipped: state.suppressionSkippedCount,
298
- dormantTransitions: state.dormantTransitions.length,
299
- activeTaps: state.firstTapTimestamps.size,
300
- },
301
- });
502
+ const normalizedPath = path.replace(/\\/g, "/");
503
+ const abId = immuneMap[normalizedPath];
504
+ if (abId && oldContent !== undefined) {
505
+ if (isCorrupted(oldContent, newContent, path)) {
506
+ const prevCorruption = corruptionTaps.get(path);
507
+ const now = Date.now();
508
+ if (prevCorruption && (now - prevCorruption) < DOUBLE_TAP_WINDOW_MS) {
509
+ corruptionTaps.delete(path);
510
+ state.fileSnapshots.set(path, newContent);
511
+ insertAntibody.run(abId, "auto-seed", path, JSON.stringify([{ op: "add", path: `/${path}`, value: newContent }]));
512
+ trackEvent("immune", "heal_false_positive", JSON.stringify({ filePath: path, abId }));
513
+ seam("Adapt", `Corruption double-tap on ${path} — standing down, accepting new content`);
514
+ } else {
515
+ corruptionTaps.set(path, now);
516
+ quarantineFile(path, newContent);
517
+ selfWrites.add(path);
518
+ setTimeout(() => selfWrites.delete(path), SELF_WRITE_DEBOUNCE_MS);
519
+ writeFileSync(resolve(path), oldContent, "utf-8");
520
+ trackEvent("immune", "heal_hit", JSON.stringify({ filePath: path, abId }));
521
+ recordMistake(path, "corruption", `Silent corruption detected (${oldContent.length} → ${newSize} bytes) — restored`, abId);
522
+ seam("Mutate", `Silent corruption detected in ${path} (${oldContent.length} → ${newSize} bytes) — restored from snapshot`);
523
+ seam("Extract", `💡 Tip: Use the MCP tool 'afd_hologram' on ${path} to safely inspect the file's structure before attempting another edit.`);
524
+ }
525
+ } else {
526
+ state.fileSnapshots.set(path, newContent);
527
+ const patches: PatchOp[] = [{ op: "add", path: `/${path}`, value: newContent }];
528
+ insertAntibody.run(abId, "auto-seed", path, JSON.stringify(patches));
529
+ trackEvent("immune", "heal_pass", JSON.stringify({ filePath: path, abId }));
530
+ seam("Adapt", `Antibody ${abId} updated: stored latest ${path} (${newSize} bytes) for auto-restore`);
531
+ }
532
+ } else {
533
+ state.fileSnapshots.set(path, newContent);
302
534
  }
535
+ trackEvent("seam", "change", path, Math.round(performance.now() - _seamStart));
536
+ return;
537
+ }
303
538
 
304
- if (url.pathname === "/sync") {
305
- const rows = listAntibodies.all() as {
306
- id: string;
307
- pattern_type: string;
308
- file_target: string;
309
- patch_op: string;
310
- created_at: string;
311
- }[];
312
- // Sanitize: strip absolute paths, keep only relative patterns
313
- const sanitized = rows.map(r => {
314
- const patches = JSON.parse(r.patch_op) as PatchOp[];
315
- const cleanPatches = patches.map(p => ({
316
- ...p,
317
- // Ensure paths are relative (strip any leading drive/abs prefix)
318
- path: p.path.replace(/^[A-Za-z]:/, "").replace(/\\/g, "/"),
319
- // Strip absolute paths from values
320
- value: p.value?.replace(/[A-Za-z]:\\[^\s"']*/g, "<redacted>"),
321
- }));
322
- return {
323
- id: r.id,
324
- patternType: r.pattern_type,
325
- fileTarget: r.file_target.replace(/^[A-Za-z]:/, "").replace(/\\/g, "/"),
326
- patches: cleanPatches,
327
- learnedAt: r.created_at,
328
- };
329
- });
330
- const payload = {
331
- version: "0.1.0",
332
- generatedAt: new Date().toISOString(),
333
- ecosystem: state.ecosystems[0]?.adapter.name ?? "Unknown",
334
- antibodyCount: sanitized.length,
335
- antibodies: sanitized,
336
- };
337
- // Write payload to disk
338
- const payloadPath = resolve(AFD_DIR, "global-vaccine-payload.json");
339
- writeFileSync(payloadPath, JSON.stringify(payload, null, 2), "utf-8");
340
- return Response.json({ status: "exported", path: payloadPath, count: sanitized.length });
341
- }
539
+ seam("Sense", `${event} → ${path}`);
540
+ trackEvent("seam", event, path, Math.round(performance.now() - _seamStart));
541
+ }
342
542
 
343
- if (url.pathname === "/stop") {
344
- cleanup();
345
- setTimeout(() => process.exit(0), 100);
346
- return Response.json({ status: "stopping" });
347
- }
543
+ // ── Workspace Map ──
544
+ const wsMap = createWorkspaceMap();
545
+ _cleanupResources.wsMapGetTimer = wsMap.getTimer;
546
+ watcher.on("add", () => wsMap.markDirty());
547
+ watcher.on("unlink", () => wsMap.markDirty());
548
+ wsMap.get(); // initial build
549
+
550
+ // ── Build DaemonContext ──
551
+ const ctx: DaemonContext = {
552
+ state, db: db as unknown as DaemonContext["db"], ws: _ws, options,
553
+ insertEvent, insertAntibody, listAntibodies, antibodyIds: antibodyIds as unknown as DaemonContext["antibodyIds"],
554
+ countAntibodies: countAntibodies as unknown as DaemonContext["countAntibodies"],
555
+ getDailyAll: getDailyAll as unknown as DaemonContext["getDailyAll"],
556
+ insertTelemetry: insertTelemetry as unknown as DaemonContext["insertTelemetry"],
557
+ insertMistakeHistory: insertMistakeHistory as unknown as DaemonContext["insertMistakeHistory"],
558
+ queryMistakesByFile: queryMistakesByFile as unknown as DaemonContext["queryMistakesByFile"],
559
+ deleteMistakeOverflow: deleteMistakeOverflow as unknown as DaemonContext["deleteMistakeOverflow"],
560
+ seam, persistHologramStats, safeHologram,
561
+ getWorkspaceMap: wsMap.get, today, discoveryTargets: discovery.targets,
562
+ port: 0,
563
+ };
348
564
 
349
- return Response.json({ error: "not found" }, { status: 404 });
350
- },
351
- });
565
+ // ── MCP Mode ──
566
+ if (options.mcp) {
567
+ startMcpStdio(ctx);
568
+ return;
569
+ }
352
570
 
571
+ // ── HTTP Server ──
572
+ const server = Bun.serve({ port: 0, fetch: createHttpHandler(ctx, cleanup) });
353
573
  const port = server.port;
574
+ ctx.port = port;
354
575
 
355
- mkdirSync(AFD_DIR, { recursive: true });
356
- writeFileSync(PID_FILE, String(process.pid));
357
- writeFileSync(PORT_FILE, String(port));
576
+ mkdirSync(_ws.afdDir, { recursive: true });
577
+ writeFileSync(_ws.pidFile, String(process.pid));
578
+ writeFileSync(_ws.portFile, String(port));
358
579
 
359
- console.log(`[afd daemon] pid=${process.pid} port=${port}`);
360
-
361
- process.on("uncaughtException", (err) => {
362
- console.error("[afd daemon] FATAL:", err.message);
363
- cleanup();
364
- process.exit(1);
365
- });
580
+ console.log(`[afd daemon] pid=${process.pid} port=${port} workspace=${_ws.root}`);
366
581
 
582
+ process.on("uncaughtException", (err) => { console.error("[afd daemon] FATAL:", err.message); cleanup(); process.exit(1); });
367
583
  process.on("SIGTERM", () => { cleanup(); process.exit(0); });
368
584
  process.on("SIGINT", () => { cleanup(); process.exit(0); });
369
585
  }
370
586
 
371
- main();
587
+ if (import.meta.main) {
588
+ const mcp = process.argv.includes("--mcp") || process.env.AFD_MCP === "1";
589
+ main({ mcp });
590
+ }